Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(popupLyrics): implement lyrics caching & Musixmatch token refresh #3328

Merged
merged 6 commits into from
Feb 27, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 206 additions & 38 deletions Extensions/popupLyrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ if (!navigator.serviceWorker) {
PopupLyrics();
}

let CACHE = {};

function PopupLyrics() {
const { Player, CosmosAsync, LocalStorage, ContextMenu } = Spicetify;

Expand Down Expand Up @@ -324,7 +326,7 @@ function PopupLyrics() {
musixmatch: {
on: boolLocalStorage("popup-lyrics:services:musixmatch:on"),
call: LyricProviders.fetchMusixmatch,
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>.`,
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.",
token: LocalStorage.get("popup-lyrics:services:musixmatch:token") || "2005218b74f939209bda92cb633c7380612e14cb7fe92dcd6a780f",
},
spotify: {
Expand Down Expand Up @@ -421,9 +423,11 @@ function PopupLyrics() {

let sharedData = {};

Player.addEventListener("songchange", updateTrack);
Player.addEventListener("songchange", () => {
updateTrack();
});

async function updateTrack() {
async function updateTrack(refresh = false) {
if (!lyricVideoIsOpen) {
return;
}
Expand All @@ -443,20 +447,26 @@ function PopupLyrics() {
uri: Player.data.item.uri,
};

for (const name of userConfigs.servicesOrder) {
const service = userConfigs.services[name];
if (!service.on) continue;
sharedData = { lyrics: [] };
if (CACHE?.[info.uri]?.lyrics?.length && !refresh) {
sharedData = CACHE[info.uri];
} else {
for (const name of userConfigs.servicesOrder) {
const service = userConfigs.services[name];
if (!service.on) continue;
sharedData = { lyrics: [] };

try {
const data = await service.call(info);
console.log(data);
sharedData = data;
CACHE[info.uri] = sharedData;

try {
const data = await service.call(info);
console.log(data);
sharedData = data;
if (!sharedData.error) {
return;
if (!sharedData.error) {
return;
}
} catch (err) {
sharedData = { error: "No lyrics" };
}
} catch (err) {
sharedData = { error: "No lyrics" };
}
}
}
Expand Down Expand Up @@ -815,11 +825,20 @@ function PopupLyrics() {

function openConfig(event) {
event.preventDefault();
if (!configContainer) {

// Reset on reopen
if (configContainer) {
resetTokenButton(configContainer);
} else {
configContainer = document.createElement("div");
configContainer.id = "popup-config-container";
const style = document.createElement("style");
style.innerHTML = `
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.setting-row::after {
content: "";
display: table;
Expand All @@ -831,13 +850,16 @@ function PopupLyrics() {
align-items: center;
}
.setting-row .col.description {
float: left;
padding-right: 15px;
cursor: default;
width: 50%;
}
.setting-row .col.action {
float: right;
text-align: right;
justify-content: flex-end;
width: 50%;
}
.popup-config-col-margin {
margin-top: 10px;
}
button.switch {
align-items: center;
Expand All @@ -859,6 +881,27 @@ button.switch.small {
height: 22px;
padding: 6px;
}
button.btn {
font-weight: 700;
display: block;
background-color: rgba(var(--spice-rgb-shadow), .7);
border-radius: 500px;
transition-duration: 33ms;
transition-property: background-color, border-color, color, box-shadow, filter, transform;
padding-inline: 15px;
border: 1px solid #727272;
color: var(--spice-text);
min-block-size: 32px;
cursor: pointer;
}
button.btn:hover {
transform: scale(1.04);
border-color: var(--spice-text);
}
button.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#popup-config-container select {
color: var(--spice-text);
background: rgba(var(--spice-rgb-shadow), .7);
Expand Down Expand Up @@ -945,6 +988,13 @@ button.switch.small {
userConfigs.delay = Number(state);
LocalStorage.set("popup-lyrics:delay", state);
});
const clearCache = descriptiveElement(
createButton("Clear Memory Cache", "Clear Memory Cache", () => {
CACHE = {};
updateTrack();
}),
"Loaded lyrics are cached in memory for faster reloading. Press this button to clear the cached lyrics from memory without restarting Spotify."
);

const serviceHeader = document.createElement("h2");
serviceHeader.innerText = "Services";
Expand Down Expand Up @@ -975,7 +1025,7 @@ button.switch.small {
const id = el.dataset.id;
userConfigs.services[id].on = state;
LocalStorage.set(`popup-lyrics:services:${id}:on`, state);
updateTrack();
updateTrack(true);
}

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

stackServiceElements();
updateTrack();
}

function tokenChangeCallback(el, inputEl) {
const newVal = inputEl.value;
const id = el.dataset.id;
userConfigs.services[id].token = newVal;
LocalStorage.set(`popup-lyrics:services:${id}:token`, newVal);
updateTrack();
updateTrack(true);
}

for (const name of userConfigs.servicesOrder) {
userConfigs.services[name].element = createServiceOption(name, userConfigs.services[name], switchCallback, posCallback, tokenChangeCallback);
userConfigs.services[name].element = createServiceOption(name, userConfigs.services[name], switchCallback, posCallback);
}
stackServiceElements();

configContainer.append(style, optionHeader, smooth, center, cover, blurSize, fontSize, ratio, delay, serviceHeader, serviceContainer);
configContainer.append(
style,
optionHeader,
smooth,
center,
cover,
blurSize,
fontSize,
ratio,
delay,
clearCache,
serviceHeader,
serviceContainer
);
}
Spicetify.PopupModal.display({
title: "Popup Lyrics",
Expand Down Expand Up @@ -1084,8 +1139,125 @@ button.switch.small {

return container;
}
// if name is null, the element can be used without a description.
function createButton(name, defaultValue, callback) {
let container;

if (name) {
container = document.createElement("div");
container.innerHTML = `
<div class="setting-row">
<label class="col description">${name}</label>
<div class="col action">
<button id="popup-lyrics-clickbutton" class="btn">${defaultValue}</button>
</div>
</div>`;

const button = container.querySelector("#popup-lyrics-clickbutton");
button.onclick = () => {
callback();
};
} else {
container = document.createElement("button");
container.innerHTML = defaultValue;
container.className = "btn ";

container.onclick = () => {
callback();
};
}

return container;
}
// if name is null, the element can be used without a description.
function createTextfield(name, defaultValue, placeholder, callback) {
let container;

if (name) {
container = document.createElement("div");
container.className = "setting-column";
container.innerHTML = `
<label class="row-description">${name}</label>
<div class="popup-row-option action">
<input id="popup-lyrics-textfield" placeholder="${placeholder}" value="${defaultValue}" />
</div>`;

const textfield = container.querySelector("#popup-lyrics-textfield");
textfield.onchange = () => {
callback();
};
} else {
container = document.createElement("input");
container.placeholder = placeholder;
container.value = defaultValue;

container.onchange = (e) => {
callback(e.target.value);
};
}

return container;
}
function descriptiveElement(element, description) {
const desc = document.createElement("span");
desc.innerHTML = description;
element.append(desc);
return element;
}

function resetTokenButton(container) {
const button = container.querySelector("#popup-lyrics-refresh-token");
if (button) {
button.innerHTML = "Refresh token";
button.disabled = false;
}
}

function musixmatchTokenElements(defaultVal, id) {
const button = createButton(null, "Refresh token", clickRefresh);
button.className += "popup-config-col-margin";
button.id = "popup-lyrics-refresh-token";
const textfield = createTextfield(null, defaultVal.token, `Place your ${id} token here`, changeTokenfield);
textfield.className += "popup-config-col-margin";

function clickRefresh() {
button.innerHTML = "Refreshing token...";
button.disabled = true;

Spicetify.CosmosAsync.get("https://apic-desktop.musixmatch.com/ws/1.1/token.get?app_id=web-desktop-app-v1.0", null, {
authority: "apic-desktop.musixmatch.com",
})
.then(({ message: response }) => {
if (response.header.status_code === 200 && response.body.user_token) {
button.innerHTML = "Token refreshed";
textfield.value = response.body.user_token;
textfield.dispatchEvent(new Event("change"));
} else if (response.header.status_code === 401) {
button.innerHTML = "Too many attempts";
} else {
button.innerHTML = "Failed to refresh token";
console.error("Failed to refresh token", response);
}
})
.catch((error) => {
button.innerHTML = "Failed to refresh token";
console.error("Failed to refresh token", error);
});
}

function changeTokenfield(value) {
userConfigs.services.musixmatch.token = value;
LocalStorage.set("popup-lyrics:services:musixmatch:token", value);
updateTrack(true);
}

const container = document.createElement("div");
container.append(button);
container.append(textfield);
return container;
}

function createServiceOption(id, defaultVal, switchCallback, posCallback, tokenCallback) {
function createServiceOption(id, defaultVal, switchCallback, posCallback) {
const name = id.replace(/^./, (c) => c.toUpperCase());

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

if (defaultVal.token !== undefined) {
const input = document.createElement("input");
input.placeholder = `Place your ${id} token here`;
input.value = defaultVal.token;
input.onchange = () => tokenCallback(container, input);
container.append(input);
if (id === "musixmatch") {
container.append(musixmatchTokenElements(defaultVal));
}

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