Skip to content

Commit b97abcc

Browse files
authored
feat(popupLyrics): implement lyrics caching & Musixmatch token refresh (#3328)
1 parent fecdb4d commit b97abcc

File tree

1 file changed

+206
-38
lines changed

1 file changed

+206
-38
lines changed

Extensions/popupLyrics.js

+206-38
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ if (!navigator.serviceWorker) {
2525
PopupLyrics();
2626
}
2727

28+
let CACHE = {};
29+
2830
function PopupLyrics() {
2931
const { Player, CosmosAsync, LocalStorage, ContextMenu } = Spicetify;
3032

@@ -324,7 +326,7 @@ function PopupLyrics() {
324326
musixmatch: {
325327
on: boolLocalStorage("popup-lyrics:services:musixmatch:on"),
326328
call: LyricProviders.fetchMusixmatch,
327-
desc: `Fully compatible with Spotify. Requires a token that can be retrieved from the official Musixmatch app. Follow instructions on <a href="https://spicetify.app/docs/faq#sometimes-popup-lyrics-andor-lyrics-plus-seem-to-not-work">Spicetify Docs</a>.`,
329+
desc: "Fully compatible with Spotify. Requires a token that can be retrieved from the official Musixmatch app. If you have problems with retrieving lyrics, try refreshing the token by clicking <code>Refresh Token</code> button.",
328330
token: LocalStorage.get("popup-lyrics:services:musixmatch:token") || "2005218b74f939209bda92cb633c7380612e14cb7fe92dcd6a780f",
329331
},
330332
spotify: {
@@ -421,9 +423,11 @@ function PopupLyrics() {
421423

422424
let sharedData = {};
423425

424-
Player.addEventListener("songchange", updateTrack);
426+
Player.addEventListener("songchange", () => {
427+
updateTrack();
428+
});
425429

426-
async function updateTrack() {
430+
async function updateTrack(refresh = false) {
427431
if (!lyricVideoIsOpen) {
428432
return;
429433
}
@@ -443,20 +447,26 @@ function PopupLyrics() {
443447
uri: Player.data.item.uri,
444448
};
445449

446-
for (const name of userConfigs.servicesOrder) {
447-
const service = userConfigs.services[name];
448-
if (!service.on) continue;
449-
sharedData = { lyrics: [] };
450+
if (CACHE?.[info.uri]?.lyrics?.length && !refresh) {
451+
sharedData = CACHE[info.uri];
452+
} else {
453+
for (const name of userConfigs.servicesOrder) {
454+
const service = userConfigs.services[name];
455+
if (!service.on) continue;
456+
sharedData = { lyrics: [] };
457+
458+
try {
459+
const data = await service.call(info);
460+
console.log(data);
461+
sharedData = data;
462+
CACHE[info.uri] = sharedData;
450463

451-
try {
452-
const data = await service.call(info);
453-
console.log(data);
454-
sharedData = data;
455-
if (!sharedData.error) {
456-
return;
464+
if (!sharedData.error) {
465+
return;
466+
}
467+
} catch (err) {
468+
sharedData = { error: "No lyrics" };
457469
}
458-
} catch (err) {
459-
sharedData = { error: "No lyrics" };
460470
}
461471
}
462472
}
@@ -815,11 +825,20 @@ function PopupLyrics() {
815825

816826
function openConfig(event) {
817827
event.preventDefault();
818-
if (!configContainer) {
828+
829+
// Reset on reopen
830+
if (configContainer) {
831+
resetTokenButton(configContainer);
832+
} else {
819833
configContainer = document.createElement("div");
820834
configContainer.id = "popup-config-container";
821835
const style = document.createElement("style");
822836
style.innerHTML = `
837+
.setting-row {
838+
display: flex;
839+
justify-content: space-between;
840+
align-items: center;
841+
}
823842
.setting-row::after {
824843
content: "";
825844
display: table;
@@ -831,13 +850,16 @@ function PopupLyrics() {
831850
align-items: center;
832851
}
833852
.setting-row .col.description {
834-
float: left;
835853
padding-right: 15px;
836854
cursor: default;
855+
width: 50%;
837856
}
838857
.setting-row .col.action {
839-
float: right;
840-
text-align: right;
858+
justify-content: flex-end;
859+
width: 50%;
860+
}
861+
.popup-config-col-margin {
862+
margin-top: 10px;
841863
}
842864
button.switch {
843865
align-items: center;
@@ -859,6 +881,27 @@ button.switch.small {
859881
height: 22px;
860882
padding: 6px;
861883
}
884+
button.btn {
885+
font-weight: 700;
886+
display: block;
887+
background-color: rgba(var(--spice-rgb-shadow), .7);
888+
border-radius: 500px;
889+
transition-duration: 33ms;
890+
transition-property: background-color, border-color, color, box-shadow, filter, transform;
891+
padding-inline: 15px;
892+
border: 1px solid #727272;
893+
color: var(--spice-text);
894+
min-block-size: 32px;
895+
cursor: pointer;
896+
}
897+
button.btn:hover {
898+
transform: scale(1.04);
899+
border-color: var(--spice-text);
900+
}
901+
button.btn:disabled {
902+
opacity: 0.5;
903+
cursor: not-allowed;
904+
}
862905
#popup-config-container select {
863906
color: var(--spice-text);
864907
background: rgba(var(--spice-rgb-shadow), .7);
@@ -945,6 +988,13 @@ button.switch.small {
945988
userConfigs.delay = Number(state);
946989
LocalStorage.set("popup-lyrics:delay", state);
947990
});
991+
const clearCache = descriptiveElement(
992+
createButton("Clear Memory Cache", "Clear Memory Cache", () => {
993+
CACHE = {};
994+
updateTrack();
995+
}),
996+
"Loaded lyrics are cached in memory for faster reloading. Press this button to clear the cached lyrics from memory without restarting Spotify."
997+
);
948998

949999
const serviceHeader = document.createElement("h2");
9501000
serviceHeader.innerText = "Services";
@@ -975,7 +1025,7 @@ button.switch.small {
9751025
const id = el.dataset.id;
9761026
userConfigs.services[id].on = state;
9771027
LocalStorage.set(`popup-lyrics:services:${id}:on`, state);
978-
updateTrack();
1028+
updateTrack(true);
9791029
}
9801030

9811031
function posCallback(el, dir) {
@@ -990,23 +1040,28 @@ button.switch.small {
9901040
LocalStorage.set("popup-lyrics:services-order", JSON.stringify(userConfigs.servicesOrder));
9911041

9921042
stackServiceElements();
993-
updateTrack();
994-
}
995-
996-
function tokenChangeCallback(el, inputEl) {
997-
const newVal = inputEl.value;
998-
const id = el.dataset.id;
999-
userConfigs.services[id].token = newVal;
1000-
LocalStorage.set(`popup-lyrics:services:${id}:token`, newVal);
1001-
updateTrack();
1043+
updateTrack(true);
10021044
}
10031045

10041046
for (const name of userConfigs.servicesOrder) {
1005-
userConfigs.services[name].element = createServiceOption(name, userConfigs.services[name], switchCallback, posCallback, tokenChangeCallback);
1047+
userConfigs.services[name].element = createServiceOption(name, userConfigs.services[name], switchCallback, posCallback);
10061048
}
10071049
stackServiceElements();
10081050

1009-
configContainer.append(style, optionHeader, smooth, center, cover, blurSize, fontSize, ratio, delay, serviceHeader, serviceContainer);
1051+
configContainer.append(
1052+
style,
1053+
optionHeader,
1054+
smooth,
1055+
center,
1056+
cover,
1057+
blurSize,
1058+
fontSize,
1059+
ratio,
1060+
delay,
1061+
clearCache,
1062+
serviceHeader,
1063+
serviceContainer
1064+
);
10101065
}
10111066
Spicetify.PopupModal.display({
10121067
title: "Popup Lyrics",
@@ -1084,8 +1139,125 @@ button.switch.small {
10841139

10851140
return container;
10861141
}
1142+
// if name is null, the element can be used without a description.
1143+
function createButton(name, defaultValue, callback) {
1144+
let container;
1145+
1146+
if (name) {
1147+
container = document.createElement("div");
1148+
container.innerHTML = `
1149+
<div class="setting-row">
1150+
<label class="col description">${name}</label>
1151+
<div class="col action">
1152+
<button id="popup-lyrics-clickbutton" class="btn">${defaultValue}</button>
1153+
</div>
1154+
</div>`;
1155+
1156+
const button = container.querySelector("#popup-lyrics-clickbutton");
1157+
button.onclick = () => {
1158+
callback();
1159+
};
1160+
} else {
1161+
container = document.createElement("button");
1162+
container.innerHTML = defaultValue;
1163+
container.className = "btn ";
1164+
1165+
container.onclick = () => {
1166+
callback();
1167+
};
1168+
}
1169+
1170+
return container;
1171+
}
1172+
// if name is null, the element can be used without a description.
1173+
function createTextfield(name, defaultValue, placeholder, callback) {
1174+
let container;
1175+
1176+
if (name) {
1177+
container = document.createElement("div");
1178+
container.className = "setting-column";
1179+
container.innerHTML = `
1180+
<label class="row-description">${name}</label>
1181+
<div class="popup-row-option action">
1182+
<input id="popup-lyrics-textfield" placeholder="${placeholder}" value="${defaultValue}" />
1183+
</div>`;
1184+
1185+
const textfield = container.querySelector("#popup-lyrics-textfield");
1186+
textfield.onchange = () => {
1187+
callback();
1188+
};
1189+
} else {
1190+
container = document.createElement("input");
1191+
container.placeholder = placeholder;
1192+
container.value = defaultValue;
1193+
1194+
container.onchange = (e) => {
1195+
callback(e.target.value);
1196+
};
1197+
}
1198+
1199+
return container;
1200+
}
1201+
function descriptiveElement(element, description) {
1202+
const desc = document.createElement("span");
1203+
desc.innerHTML = description;
1204+
element.append(desc);
1205+
return element;
1206+
}
1207+
1208+
function resetTokenButton(container) {
1209+
const button = container.querySelector("#popup-lyrics-refresh-token");
1210+
if (button) {
1211+
button.innerHTML = "Refresh token";
1212+
button.disabled = false;
1213+
}
1214+
}
1215+
1216+
function musixmatchTokenElements(defaultVal, id) {
1217+
const button = createButton(null, "Refresh token", clickRefresh);
1218+
button.className += "popup-config-col-margin";
1219+
button.id = "popup-lyrics-refresh-token";
1220+
const textfield = createTextfield(null, defaultVal.token, `Place your ${id} token here`, changeTokenfield);
1221+
textfield.className += "popup-config-col-margin";
1222+
1223+
function clickRefresh() {
1224+
button.innerHTML = "Refreshing token...";
1225+
button.disabled = true;
1226+
1227+
Spicetify.CosmosAsync.get("https://apic-desktop.musixmatch.com/ws/1.1/token.get?app_id=web-desktop-app-v1.0", null, {
1228+
authority: "apic-desktop.musixmatch.com",
1229+
})
1230+
.then(({ message: response }) => {
1231+
if (response.header.status_code === 200 && response.body.user_token) {
1232+
button.innerHTML = "Token refreshed";
1233+
textfield.value = response.body.user_token;
1234+
textfield.dispatchEvent(new Event("change"));
1235+
} else if (response.header.status_code === 401) {
1236+
button.innerHTML = "Too many attempts";
1237+
} else {
1238+
button.innerHTML = "Failed to refresh token";
1239+
console.error("Failed to refresh token", response);
1240+
}
1241+
})
1242+
.catch((error) => {
1243+
button.innerHTML = "Failed to refresh token";
1244+
console.error("Failed to refresh token", error);
1245+
});
1246+
}
1247+
1248+
function changeTokenfield(value) {
1249+
userConfigs.services.musixmatch.token = value;
1250+
LocalStorage.set("popup-lyrics:services:musixmatch:token", value);
1251+
updateTrack(true);
1252+
}
1253+
1254+
const container = document.createElement("div");
1255+
container.append(button);
1256+
container.append(textfield);
1257+
return container;
1258+
}
10871259

1088-
function createServiceOption(id, defaultVal, switchCallback, posCallback, tokenCallback) {
1260+
function createServiceOption(id, defaultVal, switchCallback, posCallback) {
10891261
const name = id.replace(/^./, (c) => c.toUpperCase());
10901262

10911263
const container = document.createElement("div");
@@ -1113,12 +1285,8 @@ button.switch.small {
11131285
</div>
11141286
<span>${defaultVal.desc}</span>`;
11151287

1116-
if (defaultVal.token !== undefined) {
1117-
const input = document.createElement("input");
1118-
input.placeholder = `Place your ${id} token here`;
1119-
input.value = defaultVal.token;
1120-
input.onchange = () => tokenCallback(container, input);
1121-
container.append(input);
1288+
if (id === "musixmatch") {
1289+
container.append(musixmatchTokenElements(defaultVal));
11221290
}
11231291

11241292
const [up, down, slider] = container.querySelectorAll("button");

0 commit comments

Comments
 (0)