mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Repoints the `@sansenjian/qq-music-api` dependency from the public npm
release to a local fork at ../qq-music-api, which ships a corrected
getMusicPlay that:
- drops the hardcoded-sign GET path (no longer honored by QQ's vkey
server for VIP entitlement lookups)
- POSTs JSON directly to u.y.qq.com/cgi-bin/musicu.fcg (mirroring
the library's own getLyric.ts pattern)
- extracts qqmusic_key from the forwarded cookie and passes it as
`comm.authst` — the inline auth field the jsososo/QQMusicApi
reference implementation sets
- uses `ct: 19` (was 24) to match the community reference
For accounts that actually have entitlement to a given track, this
now returns the real VIP URL. For accounts that don't, QQ's vkey
server still returns result=104003 with empty purl — this is correct
server-side behavior and not a bug. Verified by observing the real
QQ Music web player on y.qq.com fall back to the same 30-second
preview on a logged-in account that lacks the specific track tier.
Supporting changes:
src/music/api-server.ts
The fork (v2.2.11) stopped auto-starting a Koa server on import —
it only listens when run as `require.main`. Explicitly import the
default Koa app and call .listen() with a server handle we can
clean up on shutdown. Without this fix, port 3200 silently fails
to bind and every QQ endpoint 502s.
src/music/qq.ts (getSongDetail)
The library's /getSongInfo endpoint returns upstream code 500001
because its param format no longer matches QQ's current API.
resolveAndPlay only needs `id` + `platform` to fetch a play URL,
so fall through to a minimal stub on /getSongInfo failure. This
unblocks /play-by-id and /add-by-id for QQ — they had been
returning "Song not found" for every QQ track regardless of
entitlement.
scripts/qq_browser_login.py
Visible-browser diagnostic tool that opens Chromium at y.qq.com,
auto-detects login via uin cookie poll, captures the full
post-login cookie set, tests it against /getMusicPlay for 稻香,
and writes the cookie to data/cookies/qq.json only if VIP
actually unlocks. On failure, dumps the full cookie to
data/cookies/qq.browser-capture.json for OAuth-vs-browser diff.
scripts/qq_verify_entitlement.py
Companion diagnostic: opens the real QQ Music web player at a
specific song's detail page so the user can manually click play
and verify whether their account has entitlement — independent
of any code path in this project. If the browser plays the full
song, HTTP 104003 is a request-signing issue; if the browser
also falls back to a 30-second preview, the account lacks the
tier/album purchase and no code fix can change that.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
3.8 KiB
TypeScript
124 lines
3.8 KiB
TypeScript
import net from "node:net";
|
|
import type { Logger } from "../logger.js";
|
|
import type { Server } from "node:http";
|
|
|
|
export interface ApiServerOptions {
|
|
neteasePort: number;
|
|
qqMusicPort: number;
|
|
}
|
|
|
|
export interface ApiServerManager {
|
|
start(): Promise<void>;
|
|
stop(): void;
|
|
getNeteaseBaseUrl(): string;
|
|
getQQMusicBaseUrl(): string;
|
|
}
|
|
|
|
function isPortFree(port: number): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const server = net.createServer();
|
|
server.once("error", () => {
|
|
server.close(() => resolve(false));
|
|
});
|
|
server.once("listening", () => {
|
|
server.close(() => resolve(true));
|
|
});
|
|
server.listen(port, "127.0.0.1");
|
|
});
|
|
}
|
|
|
|
export function createApiServerManager(
|
|
options: ApiServerOptions,
|
|
logger: Logger
|
|
): ApiServerManager {
|
|
let neteaseServer: Server | null = null;
|
|
let qqMusicServer: Server | null = null;
|
|
|
|
const neteaseBaseUrl = `http://127.0.0.1:${options.neteasePort}`;
|
|
const qqMusicBaseUrl = `http://127.0.0.1:${options.qqMusicPort}`;
|
|
|
|
return {
|
|
async start(): Promise<void> {
|
|
logger.info("Starting embedded music API servers...");
|
|
|
|
// Start NetEase Cloud Music API
|
|
try {
|
|
const portFree = await isPortFree(options.neteasePort);
|
|
if (!portFree) {
|
|
logger.info(
|
|
{ port: options.neteasePort },
|
|
"NetEase API port already in use — reusing existing instance"
|
|
);
|
|
} else {
|
|
const ncmModule = await import("NeteaseCloudMusicApi") as any;
|
|
const serverObj = ncmModule.server ?? ncmModule.default?.server;
|
|
const app = await serverObj.serveNcmApi({ port: options.neteasePort });
|
|
neteaseServer = app;
|
|
logger.info(
|
|
{ port: options.neteasePort },
|
|
"NetEase Cloud Music API started"
|
|
);
|
|
}
|
|
} catch (err) {
|
|
logger.error({ err }, "Failed to start NetEase Cloud Music API");
|
|
}
|
|
|
|
// Start QQ Music API. Older versions auto-started on import; the
|
|
// current fork (2.2.11+) only listens when run as `require.main`,
|
|
// so we explicitly call .listen() on the imported Koa app and keep
|
|
// the server handle for clean shutdown.
|
|
try {
|
|
const portFree = await isPortFree(options.qqMusicPort);
|
|
if (!portFree) {
|
|
logger.info(
|
|
{ port: options.qqMusicPort },
|
|
"QQ Music API port already in use — reusing existing instance"
|
|
);
|
|
} else {
|
|
const qqModule = (await import("@sansenjian/qq-music-api")) as any;
|
|
const koaApp = qqModule.default ?? qqModule;
|
|
if (koaApp && typeof koaApp.listen === "function") {
|
|
qqMusicServer = await new Promise<Server>((resolve, reject) => {
|
|
const srv = koaApp.listen(options.qqMusicPort, "127.0.0.1", () =>
|
|
resolve(srv)
|
|
);
|
|
srv.on("error", reject);
|
|
});
|
|
logger.info(
|
|
{ port: options.qqMusicPort },
|
|
"QQ Music API started"
|
|
);
|
|
} else {
|
|
logger.warn("QQ Music API module does not expose a Koa app");
|
|
}
|
|
}
|
|
} catch (err) {
|
|
logger.warn(
|
|
{ err },
|
|
"QQ Music API not available — QQ Music features may be limited"
|
|
);
|
|
}
|
|
},
|
|
|
|
stop(): void {
|
|
logger.info("Stopping music API servers");
|
|
if (neteaseServer && typeof (neteaseServer as any).close === "function") {
|
|
(neteaseServer as any).close();
|
|
}
|
|
neteaseServer = null;
|
|
if (qqMusicServer && typeof (qqMusicServer as any).close === "function") {
|
|
(qqMusicServer as any).close();
|
|
}
|
|
qqMusicServer = null;
|
|
},
|
|
|
|
getNeteaseBaseUrl(): string {
|
|
return neteaseBaseUrl;
|
|
},
|
|
|
|
getQQMusicBaseUrl(): string {
|
|
return qqMusicBaseUrl;
|
|
},
|
|
};
|
|
}
|