diff --git a/src/music/qq.ts b/src/music/qq.ts index 1a96790..78f31a2 100644 --- a/src/music/qq.ts +++ b/src/music/qq.ts @@ -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> { + 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 = + res.data?.data?.playUrl ?? {}; + const playable = new Set(); + 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 { // Try /getSongInfo for full metadata, but fall through to a minimal // stub if the library endpoint fails (current @sansenjian/qq-music-api diff --git a/src/web/api/player.ts b/src/web/api/player.ts index faf74f9..63b9267 100644 --- a/src/web/api/player.ts +++ b/src/web/api/player.ts @@ -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> }; + 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");