From 6e50fbb7c99c72e9436d611772848b277185bb05 Mon Sep 17 00:00:00 2001 From: GDjkhp Date: Fri, 16 Oct 2026 13:05:43 +0800 Subject: [PATCH] fix: musixmatch cookie + lyric manager --- dist/src/lyrics/musixmatch.js | 11 ++++--- dist/src/managers/lyricsManager.js | 38 ++++++++++++++++++---- src/lyrics/musixmatch.ts | 14 ++++---- src/managers/lyricsManager.ts | 51 ++++++++++++++++++++++++++---- 4 files changed, 92 insertions(+), 22 deletions(-) diff --git a/dist/src/lyrics/musixmatch.js b/dist/src/lyrics/musixmatch.js index 4370a1c3..454e3084 100644 --- a/dist/src/lyrics/musixmatch.js +++ b/dist/src/lyrics/musixmatch.js @@ -44,7 +44,6 @@ const BRACKET_JUNK = /\s*\[([^\]]*(?:official|lyrics?|video|audio|mv|visualizer| const FEAT_PATTERN = /\s*[([]\s*(?:ft\.?|feat\.?|featuring)\s+[^)\]]+[)\]]/gi; const SEPARATORS = [' - ', ' – ', ' — ', ' ~ ']; const DEFAULT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'; -const DEFAULT_COOKIE = 'AWSELB=unknown; x-mxm-user-id=undefined; x-mxm-token-guid=undefined; mxm-encrypted-token='; const isAuthCode = (code) => code === 401 || code === 403; const extractMacroCalls = (body) => { const calls = body?.macro_calls; @@ -287,6 +286,11 @@ export default class MusixmatchLyrics { ? '' : Array.from(this.cookies, ([k, v]) => `${k}=${v}`).join('; '); } + /* INFO: Musixmatch's AWS edge returns 403 for the old placeholder cookie, so only send cookies it actually issued */ + _cookieHeader() { + const cookie = this._getCookies(); + return cookie ? { cookie } : {}; + } async _fetchToken() { const url = _buildUrl(ENDPOINTS.TOKEN, { app_id: APP_ID }); const { statusCode, headers, body } = await http1makeRequest(url, { @@ -294,7 +298,6 @@ export default class MusixmatchLyrics { headers: { accept: '*/*', 'accept-language': 'en', - cookie: DEFAULT_COOKIE, 'user-agent': DEFAULT_UA } }); @@ -407,7 +410,7 @@ export default class MusixmatchLyrics { headers: { accept: 'application/json', 'user-agent': DEFAULT_UA, - cookie: this._getCookies() + ...this._cookieHeader() } }); if (!this.useManualToken && headers?.['set-cookie']) { @@ -432,7 +435,7 @@ export default class MusixmatchLyrics { headers: { accept: 'application/json', 'user-agent': DEFAULT_UA, - cookie: this._getCookies() + ...this._cookieHeader() } }); if (retryHeaders?.['set-cookie']) { diff --git a/dist/src/managers/lyricsManager.js b/dist/src/managers/lyricsManager.js index a31dd149..99be90d5 100644 --- a/dist/src/managers/lyricsManager.js +++ b/dist/src/managers/lyricsManager.js @@ -76,6 +76,17 @@ export default class LyricsManager { await fs.mkdir(lyricsDir, { recursive: true }); } } + /* INFO: A provider that throws must not abort the search, so a throw becomes an error result like any other failure */ + async _fetchFrom(source, name, trackInfo, language) { + try { + return await source.getLyrics(trackInfo, language); + } + catch (e) { + const message = getErrorMessage(e); + logger('warn', 'Lyrics', `Lyrics source ${name} threw: ${message}`); + return { loadType: 'error', data: { message, severity: 'fault' } }; + } + } async loadLyrics(decodedTrack, language, skipTrackSource = false, requestedSource) { if (!decodedTrack?.info?.sourceName || !decodedTrack.info?.uri) { logger('warn', 'Lyrics', 'Invalid track object provided to loadLyrics', decodedTrack); @@ -133,6 +144,8 @@ export default class LyricsManager { : undefined; const isYouTube = sourceName === 'youtube' || sourceName === 'ytmusic'; let youtubeCaptions = null; + let lastError = null; + let sawEmpty = false; if (lyricsSource && !skipTrackSource) { if (isYouTube) { try { @@ -146,13 +159,17 @@ export default class LyricsManager { } } else { - const lyrics = await lyricsSource.getLyrics(trackInfo, language); - if (lyrics && lyrics.loadType !== 'empty') { - if (lyrics.loadType === 'lyrics') { - lyrics.data.provider = sourceName; - } + const lyrics = await this._fetchFrom(lyricsSource, sourceName, trackInfo, language); + if (lyrics?.loadType === 'lyrics') { + lyrics.data.provider = sourceName; return lyrics; } + if (lyrics?.loadType === 'error') { + lastError = lyrics; + } + else { + sawEmpty = true; + } } } const sources = new Set([ @@ -164,7 +181,11 @@ export default class LyricsManager { if (!source || name === sourceName) continue; logger('debug', 'Lyrics', `Trying lyrics source ${name} for ${trackInfo?.title || 'Unknown Title'}.`); - const lyrics = await source.getLyrics(trackInfo, language); + const lyrics = await this._fetchFrom(source, name, trackInfo, language); + if (lyrics?.loadType === 'error') { + lastError = lyrics; + continue; + } if (lyrics && lyrics.loadType !== 'empty') { if (isYouTube && youtubeCaptions?.loadType === 'lyrics' && @@ -186,11 +207,16 @@ export default class LyricsManager { } return lyrics; } + sawEmpty = true; } if (isYouTube && youtubeCaptions?.loadType === 'lyrics') { youtubeCaptions.data.provider = sourceName; return youtubeCaptions; } + /* INFO: Only report a failure when every provider that answered failed; otherwise the track simply has no lyrics */ + if (lastError && !sawEmpty) { + return lastError; + } logger('debug', 'Lyrics', `No lyrics found for ${trackInfo?.title || 'Unknown Track'}`); return { loadType: 'empty', data: {} }; } diff --git a/src/lyrics/musixmatch.ts b/src/lyrics/musixmatch.ts index f40beb24..6793aa3c 100644 --- a/src/lyrics/musixmatch.ts +++ b/src/lyrics/musixmatch.ts @@ -71,9 +71,6 @@ const SEPARATORS = [' - ', ' – ', ' — ', ' ~ '] const DEFAULT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' -const DEFAULT_COOKIE = - 'AWSELB=unknown; x-mxm-user-id=undefined; x-mxm-token-guid=undefined; mxm-encrypted-token=' - const isAuthCode = (code: number): boolean => code === 401 || code === 403 const extractMacroCalls = (body: MxmMacroBody | unknown) => { @@ -348,6 +345,12 @@ export default class MusixmatchLyrics { : Array.from(this.cookies, ([k, v]) => `${k}=${v}`).join('; ') } + /* INFO: Musixmatch's AWS edge returns 403 for the old placeholder cookie, so only send cookies it actually issued */ + private _cookieHeader(): { cookie?: string } { + const cookie = this._getCookies() + return cookie ? { cookie } : {} + } + private async _fetchToken(): Promise { const url = _buildUrl(ENDPOINTS.TOKEN, { app_id: APP_ID }) const { statusCode, headers, body } = await http1makeRequest(url, { @@ -355,7 +358,6 @@ export default class MusixmatchLyrics { headers: { accept: '*/*', 'accept-language': 'en', - cookie: DEFAULT_COOKIE, 'user-agent': DEFAULT_UA } }) @@ -492,7 +494,7 @@ export default class MusixmatchLyrics { headers: { accept: 'application/json', 'user-agent': DEFAULT_UA, - cookie: this._getCookies() + ...this._cookieHeader() } }) @@ -525,7 +527,7 @@ export default class MusixmatchLyrics { headers: { accept: 'application/json', 'user-agent': DEFAULT_UA, - cookie: this._getCookies() + ...this._cookieHeader() } }) diff --git a/src/managers/lyricsManager.ts b/src/managers/lyricsManager.ts index 441ab94c..3f730765 100644 --- a/src/managers/lyricsManager.ts +++ b/src/managers/lyricsManager.ts @@ -215,6 +215,22 @@ export default class LyricsManager { } } + /* INFO: A provider that throws must not abort the search, so a throw becomes an error result like any other failure */ + private async _fetchFrom( + source: LyricsSourceInstance, + name: string | undefined, + trackInfo: TrackInfoLike, + language?: string + ): Promise { + try { + return await source.getLyrics(trackInfo, language) + } catch (e) { + const message = getErrorMessage(e) + logger('warn', 'Lyrics', `Lyrics source ${name} threw: ${message}`) + return { loadType: 'error', data: { message, severity: 'fault' } } + } + } + async loadLyrics( decodedTrack: DecodedTrack | null | undefined, language?: string, @@ -320,6 +336,8 @@ export default class LyricsManager { const isYouTube = sourceName === 'youtube' || sourceName === 'ytmusic' let youtubeCaptions: LyricsLoadResult | null = null + let lastError: LyricsLoadResult | null = null + let sawEmpty = false if (lyricsSource && !skipTrackSource) { if (isYouTube) { @@ -336,13 +354,22 @@ export default class LyricsManager { ) } } else { - const lyrics = await lyricsSource.getLyrics(trackInfo, language) - if (lyrics && lyrics.loadType !== 'empty') { - if (lyrics.loadType === 'lyrics') { - lyrics.data.provider = sourceName - } + const lyrics = await this._fetchFrom( + lyricsSource, + sourceName, + trackInfo, + language + ) + if (lyrics?.loadType === 'lyrics') { + lyrics.data.provider = sourceName return lyrics } + + if (lyrics?.loadType === 'error') { + lastError = lyrics + } else { + sawEmpty = true + } } } @@ -359,7 +386,12 @@ export default class LyricsManager { 'Lyrics', `Trying lyrics source ${name} for ${trackInfo?.title || 'Unknown Title'}.` ) - const lyrics = await source.getLyrics(trackInfo, language) + const lyrics = await this._fetchFrom(source, name, trackInfo, language) + + if (lyrics?.loadType === 'error') { + lastError = lyrics + continue + } if (lyrics && lyrics.loadType !== 'empty') { if ( @@ -395,6 +427,8 @@ export default class LyricsManager { } return lyrics } + + sawEmpty = true } if (isYouTube && youtubeCaptions?.loadType === 'lyrics') { @@ -402,6 +436,11 @@ export default class LyricsManager { return youtubeCaptions } + /* INFO: Only report a failure when every provider that answered failed; otherwise the track simply has no lyrics */ + if (lastError && !sawEmpty) { + return lastError + } + logger( 'debug', 'Lyrics',