mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-11 17:32:49 +08:00
fix(bilibili): keep long videos playing when the CDN drops the stream (#161)
Long B站 videos (2-3 h) still stopped ~15-20 min in, the same symptom as #89. Reconnecting to the same URL is not enough once the CDN session is gone, so: - Prefer an upos/cos mirror from baseUrl + backupUrl over PCDN hosts (*.mcdn.bilivideo.cn, *.szbdyd.com), which are the ones that cut off. - When a B站 track ends more than 30 s before its known duration, fetch a fresh URL and resume at the current position instead of advancing. Up to 3 attempts without real progress, then advance as before; a track the user started meanwhile is never clobbered. - Seek B站 URLs input-side (-ss before -i). Their CDN serves Range, so a resume jumps to the byte offset instead of re-downloading everything before it (measured locally: 0.2 s vs a full-file download at 1.5 h into a 2 h fMP4), which would otherwise trip the 60 s stall watchdog. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8ff51ea6e0
commit
07ad861ecf
6 files changed
+263
-8
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { BiliBiliProvider } from "./bilibili.js";
|
||||
import { BiliBiliProvider, pickStableAudioUrl } from "./bilibili.js";
|
||||
|
||||
describe("BiliBiliProvider.search pagination", () => {
|
||||
function mockProvider() {
|
||||
@@ -162,3 +162,44 @@ describe("BiliBiliProvider multi-P support", () => {
|
||||
expect(playurlCall![1].params.bvid).toBe("BV1multiP"); // 纯净 bvid
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickStableAudioUrl (#161 long streams dying mid-play)", () => {
|
||||
const pcdn = "https://xy1x2x3x4xy.mcdn.bilivideo.cn:4483/upgcxcode/1/2/3/3-1-30280.m4s?e=x&deadline=1";
|
||||
const szbdyd = "https://cn-hk-eq-01-01.szbdyd.com/upgcxcode/1/2/3/3-1-30280.m4s?deadline=1";
|
||||
const upos = "https://upos-sz-mirrorcos.bilivideo.com/upgcxcode/1/2/3/3-1-30280.m4s?deadline=1";
|
||||
const upos2 = "https://upos-sz-mirror08c.bilivideo.com/upgcxcode/1/2/3/3-1-30280.m4s?deadline=1";
|
||||
|
||||
it("prefers an upos/cos mirror over a PCDN baseUrl", () => {
|
||||
expect(pickStableAudioUrl({ baseUrl: pcdn, backupUrl: [szbdyd, upos] })).toBe(upos);
|
||||
});
|
||||
|
||||
it("keeps the baseUrl when it is already a stable host", () => {
|
||||
expect(pickStableAudioUrl({ baseUrl: upos, backupUrl: [upos2] })).toBe(upos);
|
||||
});
|
||||
|
||||
it("accepts the snake_case field names", () => {
|
||||
expect(pickStableAudioUrl({ base_url: pcdn, backup_url: [upos2] })).toBe(upos2);
|
||||
});
|
||||
|
||||
it("falls back to the baseUrl when every candidate is PCDN", () => {
|
||||
expect(pickStableAudioUrl({ baseUrl: pcdn, backupUrl: [szbdyd] })).toBe(pcdn);
|
||||
});
|
||||
|
||||
it("returns undefined when there is no url at all", () => {
|
||||
expect(pickStableAudioUrl({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("getSongUrl returns the stable mirror of the best stream", async () => {
|
||||
const p = new BiliBiliProvider();
|
||||
(p as any).cidCache.set("BV1abc", 42);
|
||||
(p as any).api = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: { data: { dash: { audio: [
|
||||
{ bandwidth: 64000, baseUrl: "https://upos-sz-mirrorcos.bilivideo.com/low.m4s" },
|
||||
{ bandwidth: 320000, baseUrl: pcdn, backupUrl: [upos] },
|
||||
] } } },
|
||||
}),
|
||||
};
|
||||
expect((await p.getSongUrl("BV1abc"))?.url).toBe(upos);
|
||||
});
|
||||
});
|
||||
+32
-1
@@ -42,6 +42,37 @@ export interface BiliVideoPartsResult {
|
||||
parts: BiliVideoPart[];
|
||||
}
|
||||
|
||||
/**
|
||||
* PCDN / P2P edge hosts (xy*.mcdn.bilivideo.cn:<port>, *.szbdyd.com). Their
|
||||
* sessions get cut mid-file, which kills long streams partway (#89, #161),
|
||||
* and a reconnect to the same host rarely recovers.
|
||||
*/
|
||||
const BILI_PCDN_HOST = /\.mcdn\.bilivideo\.cn$|\.szbdyd\.com$/i;
|
||||
|
||||
/**
|
||||
* Pick the audio URL least likely to die mid-stream: the first upos/cos
|
||||
* mirror among baseUrl + backupUrl, else the baseUrl as before.
|
||||
*/
|
||||
export function pickStableAudioUrl(stream: {
|
||||
baseUrl?: string;
|
||||
base_url?: string;
|
||||
backupUrl?: string[];
|
||||
backup_url?: string[];
|
||||
}): string | undefined {
|
||||
const primary = stream.baseUrl ?? stream.base_url;
|
||||
const candidates = [primary, ...(stream.backupUrl ?? stream.backup_url ?? [])].filter(
|
||||
(u): u is string => typeof u === "string" && u.length > 0,
|
||||
);
|
||||
const stable = candidates.find((u) => {
|
||||
try {
|
||||
return !BILI_PCDN_HOST.test(new URL(u).hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return stable ?? primary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析带有分P信息的 B站 ID 或 URL。
|
||||
* 支持形如 "BVxxxx", "BVxxxx?p=2", "BVxxxx:p2" 以及完整 URL 等格式,默认 page 为 1。
|
||||
@@ -353,7 +384,7 @@ export class BiliBiliProvider implements MusicProvider {
|
||||
(b.bandwidth ?? 0) > (a.bandwidth ?? 0) ? b : a
|
||||
);
|
||||
|
||||
const biliUrl = best.baseUrl ?? best.base_url;
|
||||
const biliUrl = pickStableAudioUrl(best);
|
||||
return biliUrl ? { url: biliUrl } : null;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
Reference in new issue
Block a user