mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
fix(qq): batch-precheck playable URLs before queueing playlist
Many users' QQ playlists (especially collected/subscribed ones) contain a large fraction of songs that return result=104003 (region/copyright restricted) for the current account. Sequential retry-skip wasted time guessing — for the user's ACG古风 collected playlist, only 3 of 115 songs are actually streamable, so a 20-retry budget had < 50% chance of finding a hit before giving up. Add a getPlayableSongIds(ids) method to the QQ provider that calls the upstream /getMusicPlay with a comma-separated batch of mids — the wrapper resolves all of them in a single upstream call (~2-3s for 100+ songs). /play-playlist now duck-types this method and, when present, filters the playlist down to playable songs before queueing. Response message reports "Loaded N of M songs (rest are copyright/ region restricted)" so the user sees exactly what's happening instead of guessing why the queue is short or playback didn't start. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4301da2e37
commit
3d0aca52d0
2 files changed
+57
-3
No files matched your search
@@ -115,6 +115,36 @@ export class QQMusicProvider implements MusicProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-check which song mids are actually streamable. QQ playlists
|
||||
* (especially collected ones) frequently contain a majority of songs
|
||||
* that return result=104003 ("no copyright/region restricted") for the
|
||||
* current user — a sequential retry loop wastes time guessing.
|
||||
*
|
||||
* The wrapper's /getMusicPlay accepts a comma-separated songmid list
|
||||
* and resolves all of them in a single upstream call (~2-3s for 100+
|
||||
* songs), so this is much cheaper than per-song probing.
|
||||
*/
|
||||
async getPlayableSongIds(songIds: string[]): Promise<Set<string>> {
|
||||
if (songIds.length === 0) return new Set();
|
||||
try {
|
||||
const res = await this.api.get("/getMusicPlay", {
|
||||
params: { songmid: songIds.join(","), quality: this.quality, ...this.cookieParams },
|
||||
});
|
||||
const playUrlMap: Record<string, { url?: string }> =
|
||||
res.data?.data?.playUrl ?? {};
|
||||
const playable = new Set<string>();
|
||||
for (const [mid, info] of Object.entries(playUrlMap)) {
|
||||
if (info?.url) playable.add(mid);
|
||||
}
|
||||
return playable;
|
||||
} catch {
|
||||
// On any error, fall through to per-song retry path — return empty
|
||||
// and let the caller try songs sequentially via getSongUrl.
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
async getSongDetail(songId: string): Promise<Song | null> {
|
||||
// Try /getSongInfo for full metadata, but fall through to a minimal
|
||||
// stub if the library endpoint fails (current @sansenjian/qq-music-api
|
||||
|
||||
+27
-3
@@ -249,9 +249,30 @@ export function createPlayerRouter(
|
||||
return;
|
||||
}
|
||||
|
||||
// QQ-specific optimization: many users' QQ playlists contain a
|
||||
// large fraction of songs that return result=104003 (region/copyright
|
||||
// restricted). Batch-resolve URLs once and only queue the playable
|
||||
// ones, otherwise the playback retry loop wastes time guessing.
|
||||
let queueable: { id: string }[] = songs;
|
||||
const totalCount = songs.length;
|
||||
const qqLike = provider as { getPlayableSongIds?: (ids: string[]) => Promise<Set<string>> };
|
||||
if (typeof qqLike.getPlayableSongIds === "function") {
|
||||
const playable = await qqLike.getPlayableSongIds(songs.map((s: { id: string }) => s.id));
|
||||
if (playable.size > 0) {
|
||||
queueable = songs.filter((s: { id: string }) => playable.has(s.id));
|
||||
}
|
||||
// If batch returned nothing, leave queueable as-is and let the
|
||||
// sequential retry path try anyway (handles the case where the
|
||||
// batch endpoint failed entirely).
|
||||
}
|
||||
if (queueable.length === 0) {
|
||||
res.json({ message: `Loaded ${totalCount} songs but none were playable (likely copyright/region restrictions).` });
|
||||
return;
|
||||
}
|
||||
|
||||
const queue = bot.getQueueManager();
|
||||
queue.clear();
|
||||
for (const song of songs) {
|
||||
for (const song of queueable) {
|
||||
queue.add({ ...song, platform: provider.platform });
|
||||
}
|
||||
|
||||
@@ -276,10 +297,13 @@ export function createPlayerRouter(
|
||||
}
|
||||
|
||||
const playing = queue.current();
|
||||
const loadedMsg = queueable.length < totalCount
|
||||
? `Loaded ${queueable.length} of ${totalCount} songs (rest are copyright/region restricted)`
|
||||
: `Loaded ${queueable.length} songs`;
|
||||
if (started && playing) {
|
||||
res.json({ message: `Loaded ${songs.length} songs. Now playing: ${playing.name}` });
|
||||
res.json({ message: `${loadedMsg}. Now playing: ${playing.name}` });
|
||||
} else {
|
||||
res.json({ message: `Loaded ${songs.length} songs but none were playable (likely copyright/region restrictions).` });
|
||||
res.json({ message: `${loadedMsg}, but couldn't start playback.` });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Play playlist failed");
|
||||
|
||||
Reference in new issue
Block a user