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:
saopig1andClaude Opus 4.7 committed 2026-05-06 15:34:16 +08:00
1 parent 4301da2e37
commit 3d0aca52d0
2 files changed
+57 -3

No files matched your search

+30
View File
@@ -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