From be409c356b73c9c4af336c258c5c059bef67c69a Mon Sep 17 00:00:00 2001 From: ianz56 Date: Mon, 20 Oct 2025 21:26:35 +0700 Subject: [PATCH 1/6] feat(lyrics-plus): enhance Musixmatch integration with translation status and language handling --- CustomApps/lyrics-plus/OptionsMenu.js | 63 +++-- CustomApps/lyrics-plus/ProviderMusixmatch.js | 51 ++++- CustomApps/lyrics-plus/Providers.js | 13 +- CustomApps/lyrics-plus/Settings.js | 2 +- CustomApps/lyrics-plus/index.js | 227 +++++++++++++++++-- 5 files changed, 316 insertions(+), 40 deletions(-) diff --git a/CustomApps/lyrics-plus/OptionsMenu.js b/CustomApps/lyrics-plus/OptionsMenu.js index 1dfaf49b8a..be9bd0eb58 100644 --- a/CustomApps/lyrics-plus/OptionsMenu.js +++ b/CustomApps/lyrics-plus/OptionsMenu.js @@ -86,7 +86,16 @@ const OptionsMenu = react.memo(({ options, onSelect, selected, defaultValue, bol ); }); -const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => { +function getMusixmatchTranslationPrefix() { + if (typeof window !== "undefined" && typeof window.__lyricsPlusMusixmatchTranslationPrefix === "string") { + return window.__lyricsPlusMusixmatchTranslationPrefix; + } + + return "musixmatchTranslation:"; +} + +const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmatchLanguages, musixmatchSelectedLanguage }) => { + const musixmatchTranslationPrefix = getMusixmatchTranslationPrefix(); const items = useMemo(() => { let sourceOptions = { none: "None", @@ -109,16 +118,20 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => { none: "None", }; - if (hasTranslation.musixmatch) { - const selectedLanguage = CONFIG.visual["musixmatch-translation-language"]; - if (selectedLanguage === "none") return; - const languageName = new Intl.DisplayNames([selectedLanguage], { - type: "language", - }).of(selectedLanguage); - sourceOptions = { - ...sourceOptions, - musixmatchTranslation: `${languageName} (Musixmatch)`, - }; + const musixmatchDisplay = new Intl.DisplayNames(["en"], { type: "language" }); + const availableMusixmatchLanguages = Array.isArray(musixmatchLanguages) ? [...new Set(musixmatchLanguages.filter(Boolean))] : []; + const activeMusixmatchLanguage = musixmatchSelectedLanguage && musixmatchSelectedLanguage !== "none" ? musixmatchSelectedLanguage : null; + if (hasTranslation.musixmatch && activeMusixmatchLanguage) { + availableMusixmatchLanguages.push(activeMusixmatchLanguage); + } + + if (availableMusixmatchLanguages.length) { + const musixmatchOptions = availableMusixmatchLanguages.reduce((acc, code) => { + const label = musixmatchDisplay.of(code) || code.toUpperCase(); + acc[`${musixmatchTranslationPrefix}${code}`] = `${label} (Musixmatch)`; + return acc; + }, {}); + sourceOptions = { ...sourceOptions, ...musixmatchOptions }; } if (hasTranslation.netease) { @@ -154,7 +167,7 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => { } } - return [ + const configItems = [ { desc: "Translation Provider", key: "translate:translated-lyrics-source", @@ -198,7 +211,16 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => { when: () => friendlyLanguage, }, ]; - }, [friendlyLanguage]); + + return configItems; + }, [ + friendlyLanguage, + hasTranslation.musixmatch, + hasTranslation.netease, + Array.isArray(musixmatchLanguages) ? musixmatchLanguages.join(",") : "", + musixmatchSelectedLanguage || "", + musixmatchTranslationPrefix, + ]); useEffect(() => { // Currently opened Context Menu does not receive prop changes @@ -210,7 +232,7 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => { }, }); document.dispatchEvent(event); - }, [friendlyLanguage]); + }, [friendlyLanguage, items]); return react.createElement( Spicetify.ReactComponent.TooltipWrapper, @@ -241,6 +263,19 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => { CONFIG.visual["translate:translated-lyrics-source"] = "none"; localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none"); } + if (name === "translate:translated-lyrics-source") { + let nextMusixmatchLanguage = null; + if (typeof value === "string" && value.startsWith(musixmatchTranslationPrefix)) { + nextMusixmatchLanguage = value.slice(musixmatchTranslationPrefix.length) || "none"; + } else { + nextMusixmatchLanguage = "none"; + } + + if (nextMusixmatchLanguage !== null && CONFIG.visual["musixmatch-translation-language"] !== nextMusixmatchLanguage) { + CONFIG.visual["musixmatch-translation-language"] = nextMusixmatchLanguage; + localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, nextMusixmatchLanguage); + } + } CONFIG.visual[name] = value; localStorage.setItem(`${APP_NAME}:visual:${name}`, value); diff --git a/CustomApps/lyrics-plus/ProviderMusixmatch.js b/CustomApps/lyrics-plus/ProviderMusixmatch.js index 92db4dfbcd..b82f844906 100644 --- a/CustomApps/lyrics-plus/ProviderMusixmatch.js +++ b/CustomApps/lyrics-plus/ProviderMusixmatch.js @@ -4,6 +4,36 @@ const ProviderMusixmatch = (() => { cookie: "x-mxm-token-guid=", }; + function findTranslationStatus(body) { + if (!body || typeof body !== "object") { + return null; + } + + if (Array.isArray(body)) { + for (const item of body) { + const result = findTranslationStatus(item); + if (result) { + return result; + } + } + + return null; + } + + if (Array.isArray(body.track_lyrics_translation_status)) { + return body.track_lyrics_translation_status; + } + + for (const value of Object.values(body)) { + const result = findTranslationStatus(value); + if (result) { + return result; + } + } + + return null; + } + async function findLyrics(info) { const baseURL = "https://apic-desktop.musixmatch.com/ws/1.1/macro.subtitles.get?format=json&namespace=lyrics_richsynched&subtitle_format=mxm&app_id=web-desktop-app-v1.0&"; @@ -19,6 +49,7 @@ const ProviderMusixmatch = (() => { q_duration: durr, f_subtitle_length: Math.floor(durr), usertoken: CONFIG.providers.musixmatch.token, + part: "track_lyrics_translation_status", }; const finalURL = @@ -44,6 +75,19 @@ const ProviderMusixmatch = (() => { }; } + const translationStatus = findTranslationStatus(body); + const meta = body?.["matcher.track.get"]?.message?.body; + const availableTranslations = Array.isArray(translationStatus) ? [...new Set(translationStatus.map((status) => status?.to).filter(Boolean))] : []; + + Object.defineProperties(body, { + __musixmatchTranslationStatus: { + value: availableTranslations, + }, + __musixmatchTrackId: { + value: meta?.track?.track_id ?? null, + }, + }); + return body; } @@ -158,9 +202,8 @@ const ProviderMusixmatch = (() => { return null; } - async function getTranslation(body) { - const track_id = body?.["matcher.track.get"]?.message?.body?.track?.track_id; - if (!track_id) return null; + async function getTranslation(trackId) { + if (!trackId) return null; const selectedLanguage = CONFIG.visual["musixmatch-translation-language"] || "none"; if (selectedLanguage === "none") return null; @@ -169,7 +212,7 @@ const ProviderMusixmatch = (() => { "https://apic-desktop.musixmatch.com/ws/1.1/crowd.track.translations.get?translation_fields_set=minimal&comment_format=text&format=json&app_id=web-desktop-app-v1.0&"; const params = { - track_id, + track_id: trackId, selected_language: selectedLanguage, usertoken: CONFIG.providers.musixmatch.token, }; diff --git a/CustomApps/lyrics-plus/Providers.js b/CustomApps/lyrics-plus/Providers.js index d542253cea..e76bebd163 100644 --- a/CustomApps/lyrics-plus/Providers.js +++ b/CustomApps/lyrics-plus/Providers.js @@ -51,6 +51,9 @@ const Providers = { synced: null, unsynced: null, musixmatchTranslation: null, + musixmatchAvailableTranslations: [], + musixmatchTrackId: null, + musixmatchTranslationLanguage: null, provider: "Musixmatch", copyright: null, }; @@ -81,7 +84,14 @@ const Providers = { result.unsynced = unsynced; result.copyright = list["track.lyrics.get"].message?.body?.lyrics?.lyrics_copyright?.trim(); } - const translation = await ProviderMusixmatch.getTranslation(list); + result.musixmatchAvailableTranslations = Array.isArray(list.__musixmatchTranslationStatus) ? list.__musixmatchTranslationStatus : []; + result.musixmatchTrackId = list.__musixmatchTrackId ?? null; + + const selectedLanguage = CONFIG.visual["musixmatch-translation-language"]; + const canRequestTranslation = + selectedLanguage && selectedLanguage !== "none" && result.musixmatchAvailableTranslations.includes(selectedLanguage); + + const translation = canRequestTranslation ? await ProviderMusixmatch.getTranslation(result.musixmatchTrackId) : null; if ((synced || unsynced) && translation) { const baseLyrics = synced ?? unsynced; result.musixmatchTranslation = baseLyrics.map((line) => ({ @@ -89,6 +99,7 @@ const Providers = { text: translation.find((t) => t.matchedLine === line.text)?.translation ?? line.text, originalText: line.text, })); + result.musixmatchTranslationLanguage = selectedLanguage; } return result; diff --git a/CustomApps/lyrics-plus/Settings.js b/CustomApps/lyrics-plus/Settings.js index 84ad23278b..79d25dcdd8 100644 --- a/CustomApps/lyrics-plus/Settings.js +++ b/CustomApps/lyrics-plus/Settings.js @@ -703,7 +703,7 @@ function openConfig() { CONFIG.visual["translate:translated-lyrics-source"] = "none"; localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none"); } - reloadLyrics?.(); + lyricContainerUpdate?.(); } else { lyricContainerUpdate?.(); } diff --git a/CustomApps/lyrics-plus/index.js b/CustomApps/lyrics-plus/index.js index 72952a6901..40c75b86b5 100644 --- a/CustomApps/lyrics-plus/index.js +++ b/CustomApps/lyrics-plus/index.js @@ -21,6 +21,16 @@ function getConfig(name, defaultVal = true) { } const APP_NAME = "lyrics-plus"; +const MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT = "musixmatchTranslation:"; +const MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY = "__lyricsPlusMusixmatchTranslationPrefix"; +const MUSIXMATCH_TRANSLATION_PREFIX = + typeof window !== "undefined" && typeof window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] === "string" + ? window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] + : MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT; + +if (typeof window !== "undefined") { + window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] = MUSIXMATCH_TRANSLATION_PREFIX; +} const KARAOKE = 0; const SYNCED = 1; @@ -112,6 +122,25 @@ CONFIG.visual["font-size"] = Number.parseInt(CONFIG.visual["font-size"]); CONFIG.visual["ja-detect-threshold"] = Number.parseInt(CONFIG.visual["ja-detect-threshold"]); CONFIG.visual["hans-detect-threshold"] = Number.parseInt(CONFIG.visual["hans-detect-threshold"]); +if (CONFIG.visual["translate:translated-lyrics-source"] === "musixmatchTranslation") { + const language = CONFIG.visual["musixmatch-translation-language"]; + const normalizedLanguage = language && language !== "none" ? language : "none"; + const upgradedValue = normalizedLanguage !== "none" ? `${MUSIXMATCH_TRANSLATION_PREFIX}${normalizedLanguage}` : "none"; + CONFIG.visual["translate:translated-lyrics-source"] = upgradedValue; + localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, upgradedValue); +} + +if (typeof CONFIG.visual["translate:translated-lyrics-source"] === "string") { + const sourceValue = CONFIG.visual["translate:translated-lyrics-source"]; + if (sourceValue.startsWith(MUSIXMATCH_TRANSLATION_PREFIX)) { + const language = sourceValue.slice(MUSIXMATCH_TRANSLATION_PREFIX.length) || "none"; + if (CONFIG.visual["musixmatch-translation-language"] !== language) { + CONFIG.visual["musixmatch-translation-language"] = language; + localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, language); + } + } +} + let CACHE = {}; const emptyState = { @@ -121,15 +150,32 @@ const emptyState = { genius: null, genius2: null, currentLyrics: null, + musixmatchAvailableTranslations: null, + musixmatchTrackId: null, + musixmatchTranslationLanguage: null, }; let lyricContainerUpdate; let reloadLyrics; +let refreshMusixmatchTranslation; const fontSizeLimit = { min: 16, max: 256, step: 4 }; const thresholdSizeLimit = { min: 0, max: 100, step: 5 }; +function resolveTranslationSource(source) { + if (typeof source !== "string") { + return { key: source, language: null }; + } + + if (source.startsWith(MUSIXMATCH_TRANSLATION_PREFIX)) { + const language = source.slice(MUSIXMATCH_TRANSLATION_PREFIX.length) || null; + return { key: "musixmatchTranslation", language }; + } + + return { key: source, language: null }; +} + class LyricsContainer extends react.Component { constructor() { super(); @@ -150,6 +196,9 @@ class LyricsContainer extends react.Component { hk: null, tw: null, musixmatchTranslation: null, + musixmatchTranslationLanguage: null, + musixmatchAvailableTranslations: [], + musixmatchTrackId: null, neteaseTranslation: null, uri: "", provider: "", @@ -184,6 +233,7 @@ class LyricsContainer extends react.Component { this.translate = CONFIG.visual.translate; this.reRenderLyricsPage = false; this.displayMode = null; + this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"]; } infoFromTrack(track) { @@ -244,6 +294,90 @@ class LyricsContainer extends react.Component { }); } + async refreshMusixmatchTranslation() { + const selectedLanguage = CONFIG.visual["musixmatch-translation-language"] || "none"; + const availableTranslations = this.state.musixmatchAvailableTranslations || []; + const trackId = this.state.musixmatchTrackId; + const currentUri = this.state.uri; + + const clearTranslation = () => { + if (this.state.musixmatchTranslation !== null || this.state.musixmatchTranslationLanguage !== null) { + this.setState({ + musixmatchTranslation: null, + musixmatchTranslationLanguage: null, + }); + } + if (CACHE[currentUri]) { + CACHE[currentUri].musixmatchTranslation = null; + CACHE[currentUri].musixmatchTranslationLanguage = null; + } + }; + + if (!trackId || !selectedLanguage || selectedLanguage === "none") { + clearTranslation(); + return; + } + + if (!availableTranslations.includes(selectedLanguage)) { + clearTranslation(); + return; + } + + const baseLyrics = this.state.synced ?? this.state.unsynced; + if (!baseLyrics) { + return; + } + + const currentLanguage = selectedLanguage; + + this.setState({ + musixmatchTranslation: null, + musixmatchTranslationLanguage: null, + }); + + const translation = await ProviderMusixmatch.getTranslation(trackId); + if (!translation) { + if (CACHE[currentUri]) { + CACHE[currentUri].musixmatchTranslation = null; + CACHE[currentUri].musixmatchTranslationLanguage = null; + } + return; + } + + if ( + currentLanguage !== CONFIG.visual["musixmatch-translation-language"] || + trackId !== this.state.musixmatchTrackId || + currentUri !== this.state.uri + ) { + return; + } + + const latestBaseLyrics = this.state.synced ?? this.state.unsynced; + if (!latestBaseLyrics) { + return; + } + + const mappedTranslation = latestBaseLyrics.map((line) => { + const originalText = line.originalText ?? line.text; + const matched = translation.find((entry) => Utils.processLyrics(entry.matchedLine) === Utils.processLyrics(originalText)); + + return { + ...line, + text: matched?.translation ?? line.text, + originalText, + }; + }); + + this.setState({ + musixmatchTranslation: mappedTranslation, + musixmatchTranslationLanguage: currentLanguage, + }); + if (CACHE[currentUri]) { + CACHE[currentUri].musixmatchTranslation = mappedTranslation; + CACHE[currentUri].musixmatchTranslationLanguage = currentLanguage; + } + } + async tryServices(trackInfo, mode = -1) { const currentMode = CONFIG.modes[mode] || ""; let finalData = { ...emptyState, uri: trackInfo.uri }; @@ -320,7 +454,7 @@ class LyricsContainer extends react.Component { let tempState; // if lyrics are cached if ((mode === -1 && CACHE[info.uri]) || CACHE[info.uri]?.[CONFIG.modes?.[mode]]) { - tempState = { ...CACHE[info.uri], isCached }; + tempState = { ...emptyState, ...CACHE[info.uri], isCached }; if (CACHE[info.uri]?.mode) { this.state.explicitMode = CACHE[info.uri]?.mode; tempState = { ...tempState, mode: CACHE[info.uri]?.mode }; @@ -340,12 +474,35 @@ class LyricsContainer extends react.Component { // In case user skips tracks too fast and multiple callbacks // set wrong lyrics to current track. if (resp.uri === this.currentTrackUri) { - tempState = { ...resp, isLoading: false, isCached }; + tempState = { ...emptyState, ...resp, isLoading: false, isCached }; } else { return; } } + const selectedMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"] || "none"; + const shouldRefreshMusixmatchTranslation = + tempState.musixmatchTrackId && + selectedMusixmatchLanguage !== "none" && + Array.isArray(tempState.musixmatchAvailableTranslations) && + tempState.musixmatchAvailableTranslations.includes(selectedMusixmatchLanguage) && + (tempState.musixmatchTranslationLanguage !== selectedMusixmatchLanguage || !tempState.musixmatchTranslation); + if ( + selectedMusixmatchLanguage !== "none" && + (!Array.isArray(tempState.musixmatchAvailableTranslations) || !tempState.musixmatchAvailableTranslations.includes(selectedMusixmatchLanguage)) + ) { + if ( + typeof CONFIG.visual["translate:translated-lyrics-source"] === "string" && + CONFIG.visual["translate:translated-lyrics-source"].startsWith(MUSIXMATCH_TRANSLATION_PREFIX) + ) { + CONFIG.visual["translate:translated-lyrics-source"] = "none"; + localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none"); + } + CONFIG.visual["musixmatch-translation-language"] = "none"; + localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, "none"); + } + const translationOverrides = shouldRefreshMusixmatchTranslation ? { musixmatchTranslation: null, musixmatchTranslationLanguage: null } : {}; + let finalMode = mode; if (mode === -1) { if (this.state.explicitMode !== -1) { @@ -389,25 +546,38 @@ class LyricsContainer extends react.Component { } // reset and apply - this.setState({ - furigana: null, - romaji: null, - hiragana: null, - katakana: null, - hangul: null, - romaja: null, - cn: null, - hk: null, - tw: null, - musixmatchTranslation: null, - neteaseTranslation: null, - ...tempState, - language: defaultLanguage, - }); + this.setState( + { + furigana: null, + romaji: null, + hiragana: null, + katakana: null, + hangul: null, + romaja: null, + cn: null, + hk: null, + tw: null, + neteaseTranslation: null, + ...tempState, + ...translationOverrides, + language: defaultLanguage, + }, + () => { + this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"]; + if (shouldRefreshMusixmatchTranslation) { + this.refreshMusixmatchTranslation(); + } + } + ); return; } - this.setState({ ...tempState }); + this.setState({ ...tempState, ...translationOverrides }, () => { + this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"]; + if (shouldRefreshMusixmatchTranslation) { + this.refreshMusixmatchTranslation(); + } + }); } lyricsSource(lyricsState, mode) { @@ -422,11 +592,17 @@ class LyricsContainer extends react.Component { // get original Lyrics const lyrics = lyricsState[CONFIG.modes[mode]]; + const translationSourceConfig = resolveTranslationSource(CONFIG.visual["translate:translated-lyrics-source"]); + + if (translationSourceConfig.language && CONFIG.visual["musixmatch-translation-language"] !== translationSourceConfig.language) { + CONFIG.visual["musixmatch-translation-language"] = translationSourceConfig.language; + localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, translationSourceConfig.language); + } if (CONFIG.visual.translate) { this.state.currentLyrics = lyricsState[CONFIG.visual[`translation-mode:${friendlyLanguage}`]] ?? lyrics; } else { - this.state.currentLyrics = lyricsState[CONFIG.visual["translate:translated-lyrics-source"]] ?? lyrics; + this.state.currentLyrics = lyricsState[translationSourceConfig.key] ?? lyrics; } // Convert Mode re-fresh @@ -688,8 +864,15 @@ class LyricsContainer extends react.Component { this.reRenderLyricsPage = !this.reRenderLyricsPage; this.updateVisualOnConfigChange(); this.forceUpdate(); + + if (this.currentMusixmatchLanguage !== CONFIG.visual["musixmatch-translation-language"]) { + this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"]; + this.refreshMusixmatchTranslation(); + } }; + refreshMusixmatchTranslation = this.refreshMusixmatchTranslation.bind(this); + reloadLyrics = () => { CACHE = {}; this.updateVisualOnConfigChange(); @@ -743,6 +926,7 @@ class LyricsContainer extends react.Component { this.configButton.deregister(); this.mousetrap.reset(); window.removeEventListener("fad-request", lyricContainerUpdate); + refreshMusixmatchTranslation = null; } updateVisualOnConfigChange() { @@ -819,7 +1003,8 @@ class LyricsContainer extends react.Component { this.lyricsSource(this.state, mode); const lang = this.provideLanguageCode(this.state.currentLyrics); const friendlyLanguage = lang && new Intl.DisplayNames(["en"], { type: "language" }).of(lang.split("-")[0])?.toLowerCase(); - const hasTranslation = this.state.neteaseTranslation !== null || this.state.musixmatchTranslation !== null; + const hasMusixmatchLanguages = Array.isArray(this.state.musixmatchAvailableTranslations) && this.state.musixmatchAvailableTranslations.length > 0; + const hasTranslation = this.state.neteaseTranslation !== null || this.state.musixmatchTranslation !== null || hasMusixmatchLanguages; if (mode !== -1) { showTranslationButton = (friendlyLanguage || hasTranslation) && (mode === SYNCED || mode === UNSYNCED); @@ -912,6 +1097,8 @@ class LyricsContainer extends react.Component { musixmatch: this.state.musixmatchTranslation !== null, netease: this.state.neteaseTranslation !== null, }, + musixmatchLanguages: this.state.musixmatchAvailableTranslations || [], + musixmatchSelectedLanguage: this.state.musixmatchTranslationLanguage || CONFIG.visual["musixmatch-translation-language"], }), react.createElement(AdjustmentsMenu, { mode }), react.createElement( From a208710016aa834a61fdb5854543caa1e42abf76 Mon Sep 17 00:00:00 2001 From: ianz56 Date: Tue, 21 Oct 2025 12:25:52 +0700 Subject: [PATCH 2/6] fix(lyrics-plus): improve translation matching reliability --- CustomApps/lyrics-plus/Providers.js | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/CustomApps/lyrics-plus/Providers.js b/CustomApps/lyrics-plus/Providers.js index e76bebd163..5f83bd1a28 100644 --- a/CustomApps/lyrics-plus/Providers.js +++ b/CustomApps/lyrics-plus/Providers.js @@ -92,13 +92,31 @@ const Providers = { selectedLanguage && selectedLanguage !== "none" && result.musixmatchAvailableTranslations.includes(selectedLanguage); const translation = canRequestTranslation ? await ProviderMusixmatch.getTranslation(result.musixmatchTrackId) : null; - if ((synced || unsynced) && translation) { + if ((synced || unsynced) && Array.isArray(translation) && translation.length) { + const normalizeLyrics = + typeof Utils !== "undefined" && typeof Utils.processLyrics === "function" + ? (value) => Utils.processLyrics(value ?? "") + : (value) => + typeof value === "string" ? value.replace(/ | /g, "").replace(/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~?!,。、《》【】「」]/g, "") : ""; + + const translationMap = new Map(); + for (const entry of translation) { + const normalizedMatched = normalizeLyrics(entry.matchedLine); + if (!translationMap.has(normalizedMatched)) { + translationMap.set(normalizedMatched, entry.translation); + } + } + const baseLyrics = synced ?? unsynced; - result.musixmatchTranslation = baseLyrics.map((line) => ({ - ...line, - text: translation.find((t) => t.matchedLine === line.text)?.translation ?? line.text, - originalText: line.text, - })); + result.musixmatchTranslation = baseLyrics.map((line) => { + const originalText = line.text; + const normalizedOriginal = normalizeLyrics(originalText); + return { + ...line, + text: translationMap.get(normalizedOriginal) ?? line.text, + originalText, + }; + }); result.musixmatchTranslationLanguage = selectedLanguage; } From cf3bea09cae67a167f5b326015a3ef14f81d2ad1 Mon Sep 17 00:00:00 2001 From: ianz56 Date: Tue, 21 Oct 2025 13:02:16 +0700 Subject: [PATCH 3/6] refactor(lyrics-plus): remove obsolete Musixmatch translation language setting --- CustomApps/lyrics-plus/Settings.js | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/CustomApps/lyrics-plus/Settings.js b/CustomApps/lyrics-plus/Settings.js index 79d25dcdd8..b66ff877a2 100644 --- a/CustomApps/lyrics-plus/Settings.js +++ b/CustomApps/lyrics-plus/Settings.js @@ -547,17 +547,6 @@ const OptionList = ({ type, items, onChange }) => { }); }; -const languageCodes = - "none,en,af,ar,bg,bn,ca,zh,cs,da,de,el,es,et,fa,fi,fr,gu,he,hi,hr,hu,id,is,it,ja,jv,kn,ko,lt,lv,ml,mr,ms,nl,no,pl,pt,ro,ru,sk,sl,sr,su,sv,ta,te,th,tr,uk,ur,vi,zu".split( - "," - ); - -const displayNames = new Intl.DisplayNames(["en"], { type: "language" }); -const languageOptions = languageCodes.reduce((acc, code) => { - acc[code] = code === "none" ? "None" : displayNames.of(code); - return acc; -}, {}); - function openConfig() { const configContainer = react.createElement( "div", @@ -675,13 +664,6 @@ function openConfig() { max: thresholdSizeLimit.max, step: thresholdSizeLimit.step, }, - { - desc: "Musixmatch Translation Language.", - info: "Choose the language you want to translate the lyrics to. When the language is changed, the lyrics reloads.", - key: "musixmatch-translation-language", - type: ConfigSelection, - options: languageOptions, - }, { desc: "Clear Memory Cache", info: "Loaded lyrics are cached in memory for faster reloading. Press this button to clear the cached lyrics from memory without restarting Spotify.", @@ -696,17 +678,7 @@ function openConfig() { onChange: (name, value) => { CONFIG.visual[name] = value; localStorage.setItem(`${APP_NAME}:visual:${name}`, value); - - // Reload Lyrics if translation language is changed - if (name === "musixmatch-translation-language") { - if (value === "none") { - CONFIG.visual["translate:translated-lyrics-source"] = "none"; - localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none"); - } - lyricContainerUpdate?.(); - } else { - lyricContainerUpdate?.(); - } + lyricContainerUpdate?.(); const configChange = new CustomEvent("lyrics-plus", { detail: { From 44feafa42d159eb911c149d7e33770bf019dbc53 Mon Sep 17 00:00:00 2001 From: ianz56 Date: Tue, 11 Nov 2025 19:15:28 +0700 Subject: [PATCH 4/6] feat(lyrics-plus): add translation fetch notifications and handle translation source updates --- CustomApps/lyrics-plus/OptionsMenu.js | 16 +++++++-------- CustomApps/lyrics-plus/index.js | 28 ++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/CustomApps/lyrics-plus/OptionsMenu.js b/CustomApps/lyrics-plus/OptionsMenu.js index be9bd0eb58..10f5d8605f 100644 --- a/CustomApps/lyrics-plus/OptionsMenu.js +++ b/CustomApps/lyrics-plus/OptionsMenu.js @@ -255,23 +255,23 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmat type: "translation-menu", items, onChange: (name, value) => { - if (name === "translate:translated-lyrics-source" && friendlyLanguage) { - CONFIG.visual.translate = false; - localStorage.setItem(`${APP_NAME}:visual:translate`, false); - } if (name === "translate") { CONFIG.visual["translate:translated-lyrics-source"] = "none"; localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none"); } if (name === "translate:translated-lyrics-source") { - let nextMusixmatchLanguage = null; + const hasTranslationProvider = typeof value === "string" && value !== "none"; + if (hasTranslationProvider && CONFIG.visual.translate) { + CONFIG.visual.translate = false; + localStorage.setItem(`${APP_NAME}:visual:translate`, "false"); + } + + let nextMusixmatchLanguage = "none"; if (typeof value === "string" && value.startsWith(musixmatchTranslationPrefix)) { nextMusixmatchLanguage = value.slice(musixmatchTranslationPrefix.length) || "none"; - } else { - nextMusixmatchLanguage = "none"; } - if (nextMusixmatchLanguage !== null && CONFIG.visual["musixmatch-translation-language"] !== nextMusixmatchLanguage) { + if (CONFIG.visual["musixmatch-translation-language"] !== nextMusixmatchLanguage) { CONFIG.visual["musixmatch-translation-language"] = nextMusixmatchLanguage; localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, nextMusixmatchLanguage); } diff --git a/CustomApps/lyrics-plus/index.js b/CustomApps/lyrics-plus/index.js index 40c75b86b5..120fe014b6 100644 --- a/CustomApps/lyrics-plus/index.js +++ b/CustomApps/lyrics-plus/index.js @@ -23,6 +23,8 @@ function getConfig(name, defaultVal = true) { const APP_NAME = "lyrics-plus"; const MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT = "musixmatchTranslation:"; const MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY = "__lyricsPlusMusixmatchTranslationPrefix"; +const MUSIXMATCH_TRANSLATION_FETCH_MESSAGE = "Fetching translation..."; +const MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE = "Failed to fetch translation, please try again in a few minutes"; const MUSIXMATCH_TRANSLATION_PREFIX = typeof window !== "undefined" && typeof window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] === "string" ? window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] @@ -141,6 +143,15 @@ if (typeof CONFIG.visual["translate:translated-lyrics-source"] === "string") { } } +if ( + CONFIG.visual.translate && + typeof CONFIG.visual["translate:translated-lyrics-source"] === "string" && + CONFIG.visual["translate:translated-lyrics-source"] !== "none" +) { + CONFIG.visual.translate = false; + localStorage.setItem(`${APP_NAME}:visual:translate`, "false"); +} + let CACHE = {}; const emptyState = { @@ -330,13 +341,28 @@ class LyricsContainer extends react.Component { const currentLanguage = selectedLanguage; + Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_MESSAGE, false, 1000); + this.setState({ musixmatchTranslation: null, musixmatchTranslationLanguage: null, }); - const translation = await ProviderMusixmatch.getTranslation(trackId); + let translation; + try { + translation = await ProviderMusixmatch.getTranslation(trackId); + } catch (error) { + console.error(error); + Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000); + if (CACHE[currentUri]) { + CACHE[currentUri].musixmatchTranslation = null; + CACHE[currentUri].musixmatchTranslationLanguage = null; + } + return; + } + if (!translation) { + Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000); if (CACHE[currentUri]) { CACHE[currentUri].musixmatchTranslation = null; CACHE[currentUri].musixmatchTranslationLanguage = null; From ad97b767f68e824ee06d3ce8767bc1f4961283f9 Mon Sep 17 00:00:00 2001 From: ianz56 Date: Tue, 11 Nov 2025 19:48:52 +0700 Subject: [PATCH 5/6] feat(lyrics-plus): implement request ID handling for Musixmatch translation fetching --- CustomApps/lyrics-plus/index.js | 45 ++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/CustomApps/lyrics-plus/index.js b/CustomApps/lyrics-plus/index.js index 120fe014b6..6d4debe383 100644 --- a/CustomApps/lyrics-plus/index.js +++ b/CustomApps/lyrics-plus/index.js @@ -245,6 +245,7 @@ class LyricsContainer extends react.Component { this.reRenderLyricsPage = false; this.displayMode = null; this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"]; + this._musixmatchTranslationRequestId = null; } infoFromTrack(track) { @@ -310,6 +311,14 @@ class LyricsContainer extends react.Component { const availableTranslations = this.state.musixmatchAvailableTranslations || []; const trackId = this.state.musixmatchTrackId; const currentUri = this.state.uri; + const currentRequestId = Symbol("musixmatchTranslationRequest"); + this._musixmatchTranslationRequestId = currentRequestId; + const isLatestRequest = () => this._musixmatchTranslationRequestId === currentRequestId; + const finishRequest = () => { + if (isLatestRequest()) { + this._musixmatchTranslationRequestId = null; + } + }; const clearTranslation = () => { if (this.state.musixmatchTranslation !== null || this.state.musixmatchTranslationLanguage !== null) { @@ -326,16 +335,19 @@ class LyricsContainer extends react.Component { if (!trackId || !selectedLanguage || selectedLanguage === "none") { clearTranslation(); + finishRequest(); return; } if (!availableTranslations.includes(selectedLanguage)) { clearTranslation(); + finishRequest(); return; } const baseLyrics = this.state.synced ?? this.state.unsynced; if (!baseLyrics) { + finishRequest(); return; } @@ -353,33 +365,42 @@ class LyricsContainer extends react.Component { translation = await ProviderMusixmatch.getTranslation(trackId); } catch (error) { console.error(error); - Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000); - if (CACHE[currentUri]) { - CACHE[currentUri].musixmatchTranslation = null; - CACHE[currentUri].musixmatchTranslationLanguage = null; + if (isLatestRequest()) { + Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000); + if (CACHE[currentUri]) { + CACHE[currentUri].musixmatchTranslation = null; + CACHE[currentUri].musixmatchTranslationLanguage = null; + } } + finishRequest(); return; } if (!translation) { - Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000); - if (CACHE[currentUri]) { - CACHE[currentUri].musixmatchTranslation = null; - CACHE[currentUri].musixmatchTranslationLanguage = null; + if (isLatestRequest()) { + Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000); + if (CACHE[currentUri]) { + CACHE[currentUri].musixmatchTranslation = null; + CACHE[currentUri].musixmatchTranslationLanguage = null; + } } + finishRequest(); return; } if ( currentLanguage !== CONFIG.visual["musixmatch-translation-language"] || trackId !== this.state.musixmatchTrackId || - currentUri !== this.state.uri + currentUri !== this.state.uri || + !isLatestRequest() ) { + finishRequest(); return; } const latestBaseLyrics = this.state.synced ?? this.state.unsynced; if (!latestBaseLyrics) { + finishRequest(); return; } @@ -394,6 +415,11 @@ class LyricsContainer extends react.Component { }; }); + if (!isLatestRequest()) { + finishRequest(); + return; + } + this.setState({ musixmatchTranslation: mappedTranslation, musixmatchTranslationLanguage: currentLanguage, @@ -402,6 +428,7 @@ class LyricsContainer extends react.Component { CACHE[currentUri].musixmatchTranslation = mappedTranslation; CACHE[currentUri].musixmatchTranslationLanguage = currentLanguage; } + finishRequest(); } async tryServices(trackInfo, mode = -1) { From a23f5c3ac53e76ce304eb7650045f983cd84d0ef Mon Sep 17 00:00:00 2001 From: ianz56 Date: Thu, 13 Nov 2025 22:13:58 +0700 Subject: [PATCH 6/6] feat(lyrics-plus): optimize translation language handling in local storage --- CustomApps/lyrics-plus/index.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CustomApps/lyrics-plus/index.js b/CustomApps/lyrics-plus/index.js index 6d4debe383..d2ff66e312 100644 --- a/CustomApps/lyrics-plus/index.js +++ b/CustomApps/lyrics-plus/index.js @@ -647,9 +647,17 @@ class LyricsContainer extends react.Component { const lyrics = lyricsState[CONFIG.modes[mode]]; const translationSourceConfig = resolveTranslationSource(CONFIG.visual["translate:translated-lyrics-source"]); - if (translationSourceConfig.language && CONFIG.visual["musixmatch-translation-language"] !== translationSourceConfig.language) { - CONFIG.visual["musixmatch-translation-language"] = translationSourceConfig.language; - localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, translationSourceConfig.language); + if (translationSourceConfig.language) { + const translationLanguageKey = `${APP_NAME}:visual:musixmatch-translation-language`; + const storedLanguage = localStorage.getItem(translationLanguageKey); + + if (storedLanguage !== translationSourceConfig.language) { + localStorage.setItem(translationLanguageKey, translationSourceConfig.language); + } + + if (CONFIG.visual["musixmatch-translation-language"] !== translationSourceConfig.language) { + CONFIG.visual["musixmatch-translation-language"] = translationSourceConfig.language; + } } if (CONFIG.visual.translate) {