mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
The old jdymusic CDN (e.g. m701/m801.music.126.net/.../jdymusic/obj/...) intermittently drops connections from Node's HTTP stack — even with a browser UA — while the same URL fetched via WinHTTP works. Newer paths (/jd-musicrep-ts/, /ymusic/) do not have this restriction. On Windows, dispatch /jdymusic/ URLs through `System.Net.WebClient` in PowerShell into a temp file, then run ffmpeg against the file. Other URLs and non-Windows platforms keep the direct ffmpeg + browser-UA path from the previous commit. Also fix buildFfmpegArgs to omit HTTP-only flags (-reconnect_*, -headers) when the input is a local file path — new ffmpeg builds reject these as "Option not found". stop() now also kills any in-flight PowerShell downloader and removes the temp dir, so cancellation is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
5.0 KiB
TypeScript
136 lines
5.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { mkdtempSync, writeFileSync, existsSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { buildFfmpegArgs, shouldUsePowerShellDownload, cleanupTempDir } from "./player.js";
|
|
|
|
function getHeadersArg(args: string[]): string {
|
|
const idx = args.indexOf("-headers");
|
|
if (idx === -1) return "";
|
|
return args[idx + 1] ?? "";
|
|
}
|
|
|
|
describe("buildFfmpegArgs", () => {
|
|
it("includes browser User-Agent and Referer for Netease CDN URLs", () => {
|
|
const url = "http://m701.music.126.net/some/path/song.mp3?vuutv=abc";
|
|
const args = buildFfmpegArgs(url, 0);
|
|
const headers = getHeadersArg(args);
|
|
expect(headers).toContain("User-Agent:");
|
|
expect(headers).toContain("Mozilla/5.0");
|
|
expect(headers).toContain("Referer: https://music.163.com/");
|
|
});
|
|
|
|
it("keeps Bilibili Referer + UA for bilibili URLs", () => {
|
|
const url = "https://upos-sz-mirrorcoso1.bilivideo.com/foo/bar.mp3";
|
|
const args = buildFfmpegArgs(url, 0);
|
|
const headers = getHeadersArg(args);
|
|
expect(headers).toContain("Referer: https://www.bilibili.com");
|
|
expect(headers).toContain("User-Agent: Mozilla/5.0");
|
|
});
|
|
|
|
it("does not set custom headers for unknown URLs", () => {
|
|
const url = "https://example.com/song.mp3";
|
|
const args = buildFfmpegArgs(url, 0);
|
|
expect(args).not.toContain("-headers");
|
|
});
|
|
|
|
it("includes resilient reconnect flags for all URLs", () => {
|
|
const args = buildFfmpegArgs("https://example.com/song.mp3", 0);
|
|
expect(args).toContain("-reconnect");
|
|
expect(args).toContain("-reconnect_streamed");
|
|
expect(args).toContain("-reconnect_delay_max");
|
|
expect(args).toContain("-reconnect_on_network_error");
|
|
expect(args).toContain("-reconnect_on_http_error");
|
|
const idx = args.indexOf("-reconnect_delay_max");
|
|
expect(Number(args[idx + 1])).toBeGreaterThanOrEqual(30);
|
|
});
|
|
|
|
it("inserts -ss before -i when seekSeconds > 0", () => {
|
|
const args = buildFfmpegArgs("https://example.com/song.mp3", 42);
|
|
const ssIdx = args.indexOf("-ss");
|
|
const iIdx = args.indexOf("-i");
|
|
expect(ssIdx).toBeGreaterThan(-1);
|
|
expect(args[ssIdx + 1]).toBe("42");
|
|
expect(ssIdx).toBeLessThan(iIdx);
|
|
});
|
|
|
|
it("does not insert -ss when seekSeconds is 0", () => {
|
|
const args = buildFfmpegArgs("https://example.com/song.mp3", 0);
|
|
expect(args).not.toContain("-ss");
|
|
});
|
|
|
|
it("omits HTTP-only flags when input is a local file path", () => {
|
|
const args = buildFfmpegArgs("C:/temp/song.mp3", 0);
|
|
expect(args).not.toContain("-reconnect");
|
|
expect(args).not.toContain("-reconnect_on_network_error");
|
|
expect(args).not.toContain("-reconnect_on_http_error");
|
|
expect(args).not.toContain("-headers");
|
|
expect(args).toContain("-i");
|
|
expect(args[args.indexOf("-i") + 1]).toBe("C:/temp/song.mp3");
|
|
});
|
|
|
|
it("ends args with the input URL and PCM output spec", () => {
|
|
const url = "https://example.com/song.mp3";
|
|
const args = buildFfmpegArgs(url, 0);
|
|
const iIdx = args.indexOf("-i");
|
|
expect(args[iIdx + 1]).toBe(url);
|
|
expect(args).toContain("-f");
|
|
expect(args).toContain("s16le");
|
|
expect(args[args.length - 1]).toBe("-");
|
|
});
|
|
});
|
|
|
|
describe("shouldUsePowerShellDownload", () => {
|
|
const jdymusicUrl =
|
|
"http://m801.music.126.net/20260507/abc/jdymusic/obj/xyz/song.mp3?vuutv=tok";
|
|
const newCdnUrl =
|
|
"http://m801.music.126.net/20260507/abc/jd-musicrep-ts/obj/xyz/song.mp3?vuutv=tok";
|
|
const ymusicUrl =
|
|
"http://m801.music.126.net/20260507/abc/ymusic/obj/xyz/song.mp3?vuutv=tok";
|
|
|
|
it("returns true for /jdymusic/ URL on win32", () => {
|
|
expect(shouldUsePowerShellDownload(jdymusicUrl, "win32")).toBe(true);
|
|
});
|
|
|
|
it("returns false for /jdymusic/ URL on linux", () => {
|
|
expect(shouldUsePowerShellDownload(jdymusicUrl, "linux")).toBe(false);
|
|
});
|
|
|
|
it("returns false for /jdymusic/ URL on darwin", () => {
|
|
expect(shouldUsePowerShellDownload(jdymusicUrl, "darwin")).toBe(false);
|
|
});
|
|
|
|
it("returns false for new-format /jd-musicrep-ts/ URL on win32", () => {
|
|
expect(shouldUsePowerShellDownload(newCdnUrl, "win32")).toBe(false);
|
|
});
|
|
|
|
it("returns false for /ymusic/ URL on win32", () => {
|
|
expect(shouldUsePowerShellDownload(ymusicUrl, "win32")).toBe(false);
|
|
});
|
|
|
|
it("returns false for unrelated URLs", () => {
|
|
expect(shouldUsePowerShellDownload("https://example.com/x.mp3", "win32")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("cleanupTempDir", () => {
|
|
it("removes a directory and its contents", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "tsbot-test-"));
|
|
writeFileSync(join(dir, "song.mp3"), "fake-bytes");
|
|
expect(existsSync(dir)).toBe(true);
|
|
cleanupTempDir(dir);
|
|
expect(existsSync(dir)).toBe(false);
|
|
});
|
|
|
|
it("does not throw when directory does not exist", () => {
|
|
const missing = join(tmpdir(), "tsbot-test-does-not-exist-xyz");
|
|
expect(() => cleanupTempDir(missing)).not.toThrow();
|
|
});
|
|
|
|
it("does not throw when called twice", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "tsbot-test-"));
|
|
cleanupTempDir(dir);
|
|
expect(() => cleanupTempDir(dir)).not.toThrow();
|
|
});
|
|
});
|