Skip to content

Commit 07470b4

Browse files
authored
feat(lyrics-plus): enhance Musixmatch integration (#3562)
This pull request adds translation status and language handling
1 parent d01ef48 commit 07470b4

File tree

5 files changed

+405
-78
lines changed

5 files changed

+405
-78
lines changed

CustomApps/lyrics-plus/OptionsMenu.js

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,16 @@ const OptionsMenu = react.memo(({ options, onSelect, selected, defaultValue, bol
8686
);
8787
});
8888

89-
const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => {
89+
function getMusixmatchTranslationPrefix() {
90+
if (typeof window !== "undefined" && typeof window.__lyricsPlusMusixmatchTranslationPrefix === "string") {
91+
return window.__lyricsPlusMusixmatchTranslationPrefix;
92+
}
93+
94+
return "musixmatchTranslation:";
95+
}
96+
97+
const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmatchLanguages, musixmatchSelectedLanguage }) => {
98+
const musixmatchTranslationPrefix = getMusixmatchTranslationPrefix();
9099
const items = useMemo(() => {
91100
let sourceOptions = {
92101
none: "None",
@@ -109,16 +118,20 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => {
109118
none: "None",
110119
};
111120

112-
if (hasTranslation.musixmatch) {
113-
const selectedLanguage = CONFIG.visual["musixmatch-translation-language"];
114-
if (selectedLanguage === "none") return;
115-
const languageName = new Intl.DisplayNames([selectedLanguage], {
116-
type: "language",
117-
}).of(selectedLanguage);
118-
sourceOptions = {
119-
...sourceOptions,
120-
musixmatchTranslation: `${languageName} (Musixmatch)`,
121-
};
121+
const musixmatchDisplay = new Intl.DisplayNames(["en"], { type: "language" });
122+
const availableMusixmatchLanguages = Array.isArray(musixmatchLanguages) ? [...new Set(musixmatchLanguages.filter(Boolean))] : [];
123+
const activeMusixmatchLanguage = musixmatchSelectedLanguage && musixmatchSelectedLanguage !== "none" ? musixmatchSelectedLanguage : null;
124+
if (hasTranslation.musixmatch && activeMusixmatchLanguage) {
125+
availableMusixmatchLanguages.push(activeMusixmatchLanguage);
126+
}
127+
128+
if (availableMusixmatchLanguages.length) {
129+
const musixmatchOptions = availableMusixmatchLanguages.reduce((acc, code) => {
130+
const label = musixmatchDisplay.of(code) || code.toUpperCase();
131+
acc[`${musixmatchTranslationPrefix}${code}`] = `${label} (Musixmatch)`;
132+
return acc;
133+
}, {});
134+
sourceOptions = { ...sourceOptions, ...musixmatchOptions };
122135
}
123136

124137
if (hasTranslation.netease) {
@@ -154,7 +167,7 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => {
154167
}
155168
}
156169

157-
return [
170+
const configItems = [
158171
{
159172
desc: "Translation Provider",
160173
key: "translate:translated-lyrics-source",
@@ -198,7 +211,16 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => {
198211
when: () => friendlyLanguage,
199212
},
200213
];
201-
}, [friendlyLanguage]);
214+
215+
return configItems;
216+
}, [
217+
friendlyLanguage,
218+
hasTranslation.musixmatch,
219+
hasTranslation.netease,
220+
Array.isArray(musixmatchLanguages) ? musixmatchLanguages.join(",") : "",
221+
musixmatchSelectedLanguage || "",
222+
musixmatchTranslationPrefix,
223+
]);
202224

203225
useEffect(() => {
204226
// Currently opened Context Menu does not receive prop changes
@@ -210,7 +232,7 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => {
210232
},
211233
});
212234
document.dispatchEvent(event);
213-
}, [friendlyLanguage]);
235+
}, [friendlyLanguage, items]);
214236

215237
return react.createElement(
216238
Spicetify.ReactComponent.TooltipWrapper,
@@ -233,14 +255,27 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation }) => {
233255
type: "translation-menu",
234256
items,
235257
onChange: (name, value) => {
236-
if (name === "translate:translated-lyrics-source" && friendlyLanguage) {
237-
CONFIG.visual.translate = false;
238-
localStorage.setItem(`${APP_NAME}:visual:translate`, false);
239-
}
240258
if (name === "translate") {
241259
CONFIG.visual["translate:translated-lyrics-source"] = "none";
242260
localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none");
243261
}
262+
if (name === "translate:translated-lyrics-source") {
263+
const hasTranslationProvider = typeof value === "string" && value !== "none";
264+
if (hasTranslationProvider && CONFIG.visual.translate) {
265+
CONFIG.visual.translate = false;
266+
localStorage.setItem(`${APP_NAME}:visual:translate`, "false");
267+
}
268+
269+
let nextMusixmatchLanguage = "none";
270+
if (typeof value === "string" && value.startsWith(musixmatchTranslationPrefix)) {
271+
nextMusixmatchLanguage = value.slice(musixmatchTranslationPrefix.length) || "none";
272+
}
273+
274+
if (CONFIG.visual["musixmatch-translation-language"] !== nextMusixmatchLanguage) {
275+
CONFIG.visual["musixmatch-translation-language"] = nextMusixmatchLanguage;
276+
localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, nextMusixmatchLanguage);
277+
}
278+
}
244279

245280
CONFIG.visual[name] = value;
246281
localStorage.setItem(`${APP_NAME}:visual:${name}`, value);

CustomApps/lyrics-plus/ProviderMusixmatch.js

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ const ProviderMusixmatch = (() => {
44
cookie: "x-mxm-token-guid=",
55
};
66

7+
function findTranslationStatus(body) {
8+
if (!body || typeof body !== "object") {
9+
return null;
10+
}
11+
12+
if (Array.isArray(body)) {
13+
for (const item of body) {
14+
const result = findTranslationStatus(item);
15+
if (result) {
16+
return result;
17+
}
18+
}
19+
20+
return null;
21+
}
22+
23+
if (Array.isArray(body.track_lyrics_translation_status)) {
24+
return body.track_lyrics_translation_status;
25+
}
26+
27+
for (const value of Object.values(body)) {
28+
const result = findTranslationStatus(value);
29+
if (result) {
30+
return result;
31+
}
32+
}
33+
34+
return null;
35+
}
36+
737
async function findLyrics(info) {
838
const baseURL =
939
"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 = (() => {
1949
q_duration: durr,
2050
f_subtitle_length: Math.floor(durr),
2151
usertoken: CONFIG.providers.musixmatch.token,
52+
part: "track_lyrics_translation_status",
2253
};
2354

2455
const finalURL =
@@ -44,6 +75,19 @@ const ProviderMusixmatch = (() => {
4475
};
4576
}
4677

78+
const translationStatus = findTranslationStatus(body);
79+
const meta = body?.["matcher.track.get"]?.message?.body;
80+
const availableTranslations = Array.isArray(translationStatus) ? [...new Set(translationStatus.map((status) => status?.to).filter(Boolean))] : [];
81+
82+
Object.defineProperties(body, {
83+
__musixmatchTranslationStatus: {
84+
value: availableTranslations,
85+
},
86+
__musixmatchTrackId: {
87+
value: meta?.track?.track_id ?? null,
88+
},
89+
});
90+
4791
return body;
4892
}
4993

@@ -158,9 +202,8 @@ const ProviderMusixmatch = (() => {
158202
return null;
159203
}
160204

161-
async function getTranslation(body) {
162-
const track_id = body?.["matcher.track.get"]?.message?.body?.track?.track_id;
163-
if (!track_id) return null;
205+
async function getTranslation(trackId) {
206+
if (!trackId) return null;
164207

165208
const selectedLanguage = CONFIG.visual["musixmatch-translation-language"] || "none";
166209
if (selectedLanguage === "none") return null;
@@ -169,7 +212,7 @@ const ProviderMusixmatch = (() => {
169212
"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&";
170213

171214
const params = {
172-
track_id,
215+
track_id: trackId,
173216
selected_language: selectedLanguage,
174217
usertoken: CONFIG.providers.musixmatch.token,
175218
};

CustomApps/lyrics-plus/Providers.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ const Providers = {
5151
synced: null,
5252
unsynced: null,
5353
musixmatchTranslation: null,
54+
musixmatchAvailableTranslations: [],
55+
musixmatchTrackId: null,
56+
musixmatchTranslationLanguage: null,
5457
provider: "Musixmatch",
5558
copyright: null,
5659
};
@@ -81,14 +84,40 @@ const Providers = {
8184
result.unsynced = unsynced;
8285
result.copyright = list["track.lyrics.get"].message?.body?.lyrics?.lyrics_copyright?.trim();
8386
}
84-
const translation = await ProviderMusixmatch.getTranslation(list);
85-
if ((synced || unsynced) && translation) {
87+
result.musixmatchAvailableTranslations = Array.isArray(list.__musixmatchTranslationStatus) ? list.__musixmatchTranslationStatus : [];
88+
result.musixmatchTrackId = list.__musixmatchTrackId ?? null;
89+
90+
const selectedLanguage = CONFIG.visual["musixmatch-translation-language"];
91+
const canRequestTranslation =
92+
selectedLanguage && selectedLanguage !== "none" && result.musixmatchAvailableTranslations.includes(selectedLanguage);
93+
94+
const translation = canRequestTranslation ? await ProviderMusixmatch.getTranslation(result.musixmatchTrackId) : null;
95+
if ((synced || unsynced) && Array.isArray(translation) && translation.length) {
96+
const normalizeLyrics =
97+
typeof Utils !== "undefined" && typeof Utils.processLyrics === "function"
98+
? (value) => Utils.processLyrics(value ?? "")
99+
: (value) =>
100+
typeof value === "string" ? value.replace(/ | /g, "").replace(/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/g, "") : "";
101+
102+
const translationMap = new Map();
103+
for (const entry of translation) {
104+
const normalizedMatched = normalizeLyrics(entry.matchedLine);
105+
if (!translationMap.has(normalizedMatched)) {
106+
translationMap.set(normalizedMatched, entry.translation);
107+
}
108+
}
109+
86110
const baseLyrics = synced ?? unsynced;
87-
result.musixmatchTranslation = baseLyrics.map((line) => ({
88-
...line,
89-
text: translation.find((t) => t.matchedLine === line.text)?.translation ?? line.text,
90-
originalText: line.text,
91-
}));
111+
result.musixmatchTranslation = baseLyrics.map((line) => {
112+
const originalText = line.text;
113+
const normalizedOriginal = normalizeLyrics(originalText);
114+
return {
115+
...line,
116+
text: translationMap.get(normalizedOriginal) ?? line.text,
117+
originalText,
118+
};
119+
});
120+
result.musixmatchTranslationLanguage = selectedLanguage;
92121
}
93122

94123
return result;

CustomApps/lyrics-plus/Settings.js

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -547,17 +547,6 @@ const OptionList = ({ type, items, onChange }) => {
547547
});
548548
};
549549

550-
const languageCodes =
551-
"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(
552-
","
553-
);
554-
555-
const displayNames = new Intl.DisplayNames(["en"], { type: "language" });
556-
const languageOptions = languageCodes.reduce((acc, code) => {
557-
acc[code] = code === "none" ? "None" : displayNames.of(code);
558-
return acc;
559-
}, {});
560-
561550
function openConfig() {
562551
const configContainer = react.createElement(
563552
"div",
@@ -675,13 +664,6 @@ function openConfig() {
675664
max: thresholdSizeLimit.max,
676665
step: thresholdSizeLimit.step,
677666
},
678-
{
679-
desc: "Musixmatch Translation Language.",
680-
info: "Choose the language you want to translate the lyrics to. When the language is changed, the lyrics reloads.",
681-
key: "musixmatch-translation-language",
682-
type: ConfigSelection,
683-
options: languageOptions,
684-
},
685667
{
686668
desc: "Clear Memory Cache",
687669
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() {
696678
onChange: (name, value) => {
697679
CONFIG.visual[name] = value;
698680
localStorage.setItem(`${APP_NAME}:visual:${name}`, value);
699-
700-
// Reload Lyrics if translation language is changed
701-
if (name === "musixmatch-translation-language") {
702-
if (value === "none") {
703-
CONFIG.visual["translate:translated-lyrics-source"] = "none";
704-
localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none");
705-
}
706-
reloadLyrics?.();
707-
} else {
708-
lyricContainerUpdate?.();
709-
}
681+
lyricContainerUpdate?.();
710682

711683
const configChange = new CustomEvent("lyrics-plus", {
712684
detail: {

0 commit comments

Comments
 (0)