Skip to content
Open
Show file tree
Hide file tree
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
8 changes: 7 additions & 1 deletion CustomApps/lyrics-plus/OptionsMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmat
);
});

const AdjustmentsMenu = react.memo(({ mode }) => {
const AdjustmentsMenu = react.memo(({ mode, hasPerformer }) => {
return react.createElement(
Spicetify.ReactComponent.TooltipWrapper,
{
Expand Down Expand Up @@ -394,6 +394,12 @@ const AdjustmentsMenu = react.memo(({ mode }) => {
type: ConfigSlider,
when: () => mode === SYNCED || mode === KARAOKE,
},
{
desc: "Show performers",
key: "show-performers",
type: ConfigSlider,
when: () => hasPerformer && (mode === SYNCED || mode === KARAOKE || mode === UNSYNCED),
},
{
desc: "Dual panel",
key: "dual-genius",
Expand Down
63 changes: 55 additions & 8 deletions CustomApps/lyrics-plus/Pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,18 @@ const useTrackPosition = (callback) => {
}, [callbackRef]);
};

const KaraokeLine = ({ text, isActive, position, startTime }) => {
if (!isActive) {
const KaraokeLine = ({ text, isActive, position, startTime, endTime }) => {
if (endTime && position > endTime) {
return text.map(({ word }) => word).join("");
}

return text.map(({ word, time }) => {
return text.map(({ word, time }, i) => {
const isWordActive = position >= startTime;
startTime += time;
return react.createElement(
"span",
{
key: i,
className: `lyrics-lyricsContainer-Karaoke-Word${isWordActive ? " lyrics-lyricsContainer-Karaoke-WordActive" : ""}`,
style: {
"--word-duration": `${time}ms`,
Expand Down Expand Up @@ -138,7 +139,7 @@ const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara
},
key: lyricsId,
},
activeLines.map(({ text, lineNumber, startTime, originalText }, i) => {
activeLines.map(({ text, lineNumber, startTime, endTime, originalText, performer }, i) => {
if (i === 1 && activeLineIndex === 1) {
return react.createElement(IdlingIndicator, {
progress: position / activeLines[2].startTime,
Expand Down Expand Up @@ -207,7 +208,23 @@ const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara
.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
},
},
!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, position, isActive })
(() => {
if (!CONFIG.visual["show-performers"] || !performer) return null;

if (!CONFIG.visual["synced-compact"]) {
const previousLine = lyricWithEmptyLines[lineNumber - 1];
if (previousLine && previousLine.performer === performer) return null;
}

return react.createElement(
"span",
{
className: "lyrics-lyricsContainer-Performer",
},
performer
);
})(),
!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, endTime, position, isActive })
),
belowMode &&
react.createElement(
Expand Down Expand Up @@ -439,7 +456,7 @@ const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKa
react.createElement("p", {
className: "lyrics-lyricsContainer-LyricsUnsyncedPadding",
}),
padded.map(({ text, startTime, originalText }, i) => {
padded.map(({ text, startTime, endTime, originalText, performer }, i) => {
if (i === 0) {
return react.createElement(IdlingIndicator, {
isActive: activeLineIndex === 0,
Expand Down Expand Up @@ -486,7 +503,23 @@ const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKa
.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
},
},
!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, position, isActive })
(() => {
if (!CONFIG.visual["show-performers"] || !performer) return null;

if (!CONFIG.visual["synced-compact"]) {
const previousLine = padded[i - 1];
if (previousLine && previousLine.performer === performer) return null;
}

return react.createElement(
"span",
{
className: "lyrics-lyricsContainer-Performer",
},
performer
);
})(),
!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, endTime, position, isActive })
),
belowMode &&
react.createElement(
Expand Down Expand Up @@ -524,7 +557,7 @@ const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => {
react.createElement("p", {
className: "lyrics-lyricsContainer-LyricsUnsyncedPadding",
}),
lyrics.map(({ text, originalText }, index) => {
lyrics.map(({ text, originalText, performer }, index) => {
const showTranslatedBelow = CONFIG.visual["translate:display-mode"] === "below";
// If we have original text and we are showing translated below, we should show the original text
// Otherwise we should show the translated text
Expand Down Expand Up @@ -553,6 +586,20 @@ const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => {
.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
},
},
(() => {
if (!CONFIG.visual["show-performers"] || !performer) return null;

const previousLine = lyrics[index - 1];
if (previousLine && previousLine.performer === performer) return null;

return react.createElement(
"span",
{
className: "lyrics-lyricsContainer-Performer",
},
performer
);
})(),
lineText
),
belowMode &&
Expand Down
181 changes: 174 additions & 7 deletions CustomApps/lyrics-plus/ProviderMusixmatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const ProviderMusixmatch = (() => {
q_duration: durr,
f_subtitle_length: Math.floor(durr),
usertoken: CONFIG.providers.musixmatch.token,
part: "track_lyrics_translation_status",
part: "track_lyrics_translation_status,track_structure,track_performer_tagging",
};

const finalURL =
Expand Down Expand Up @@ -91,6 +91,130 @@ const ProviderMusixmatch = (() => {
return body;
}

function parsePerformerData(meta) {
if (!meta || !meta.track || !meta.track.performer_tagging) {
return [];
}

const tagging = meta.track.performer_tagging;
const miscTags = meta.track.performer_tagging_misc_tags || {};
let performerMap = [];
if (tagging && tagging.content && tagging.content.length > 0) {
const resources = tagging.resources?.artists || [];
const resourcesList = Array.isArray(resources) ? resources : Object.values(resources);

performerMap = tagging.content
.map((c) => {
if (!c.performers || c.performers.length === 0) return null;

const resolvedPerformers = c.performers
.map((p) => {
let name = "Unknown";
if (p.type === "artist") {
const fqid = p.fqid;
const idFromFqid = fqid ? parseInt(fqid.split(":")[2]) : null;

const artist = resourcesList.find((r) => r.artist_id === idFromFqid);
if (artist) name = artist.artist_name;
} else if (miscTags[p.type]) {
name = miscTags[p.type];
}
return {
fqid: p.fqid,
artist_id: p.fqid ? parseInt(p.fqid.split(":")[2]) : null,
name: name,
};
})
.filter((p) => p.name !== "Unknown");

const names = resolvedPerformers.map((p) => p.name);
if (names.length === 0) return null;

return {
name: names.join(", "),
snippet: c.snippet,
performers: resolvedPerformers,
};
})
.filter(Boolean);
}

const normalizeForMatch = (text) => text.replace(/\s+/g, "").toLowerCase();

const snippetQueue = [];
if (performerMap.length > 0) {
for (const tag of performerMap) {
if (!tag.snippet) continue;
const snippetLines = tag.snippet
.split(/\n+/)
.map((s) => s.trim())
.filter(Boolean);
for (const sLine of snippetLines) {
if (sLine.length < 2 && !/^[\u3131-\uD79D]/.test(sLine)) continue;
snippetQueue.push({
text: normalizeForMatch(sLine),
raw: sLine,
performers: tag.performers,
});
}
}
}
return snippetQueue;
}

function matchSequential(lyricsLines, snippetQueue, getTextCallback = (l) => l.text) {
if (!snippetQueue || snippetQueue.length === 0) return lyricsLines;

const normalizeForMatch = (text) => text.replace(/\s+/g, "").toLowerCase();
let queueCursor = 0;
const LOOKAHEAD = 5;

return lyricsLines.map((line) => {
const lineText = getTextCallback(line) || "♪";
let normalizedLine = normalizeForMatch(lineText);

let matchedPerformers = [];

while (queueCursor < snippetQueue.length) {
let matchFoundAtOffset = -1;

for (let i = 0; i < LOOKAHEAD && queueCursor + i < snippetQueue.length; i++) {
const snippet = snippetQueue[queueCursor + i];

if (normalizedLine.includes(snippet.text) && snippet.text.length > 0) {
matchFoundAtOffset = i;
break;
}
}

if (matchFoundAtOffset !== -1) {
queueCursor += matchFoundAtOffset;
const matchedSnippet = snippetQueue[queueCursor];
matchedPerformers.push(...matchedSnippet.performers);
normalizedLine = normalizedLine.replace(matchedSnippet.text, "");
queueCursor++;
} else {
break;
}
}

const uniquePerformers = [];
const sawMap = new Set();
for (const p of matchedPerformers) {
const key = p.fqid || p.name;
if (!sawMap.has(key)) {
sawMap.add(key);
uniquePerformers.push(p);
}
}

return {
...line,
performers: uniquePerformers,
};
});
}

async function getKaraoke(body) {
const meta = body?.["matcher.track.get"]?.message?.body;
if (!meta) {
Expand Down Expand Up @@ -124,6 +248,8 @@ const ProviderMusixmatch = (() => {

result = result.message.body;

const snippetQueue = parsePerformerData(meta);

const parsedKaraoke = JSON.parse(result.richsync.richsync_body).map((line) => {
const startTime = line.ts * 1000;
const endTime = line.te * 1000;
Expand All @@ -143,11 +269,26 @@ const ProviderMusixmatch = (() => {
});
return {
startTime,
endTime,
text,
};
});

return parsedKaraoke;
return matchSequential(parsedKaraoke, snippetQueue, (line) => {
if (Array.isArray(line.text)) {
return line.text.map((t) => t.word).join("");
}
return line.text;
}).map((line) => {
const performerNames = (line.performers || [])
.map((p) => p.name)
.filter(Boolean)
.join(", ");
return {
...line,
performer: performerNames || null,
};
});
}

function getSynced(body) {
Expand All @@ -169,10 +310,22 @@ const ProviderMusixmatch = (() => {
return null;
}

return JSON.parse(subtitle.subtitle_body).map((line) => ({
text: line.text || "♪",
startTime: line.time.total * 1000,
}));
const snippetQueue = parsePerformerData(meta);
const rawLines = JSON.parse(subtitle.subtitle_body);

return matchSequential(rawLines, snippetQueue, (l) => l.text).map((line) => {
const lineText = line.text || "♪";
const performerNames = (line.performers || [])
.map((p) => p.name)
.filter(Boolean)
.join(", ");

return {
text: lineText,
startTime: line.time.total * 1000,
performer: performerNames || null,
};
});
}

return null;
Expand All @@ -196,7 +349,21 @@ const ProviderMusixmatch = (() => {
if (!lyrics) {
return null;
}
return lyrics.split("\n").map((text) => ({ text }));

const snippetQueue = parsePerformerData(meta);
const rawLines = lyrics.split("\n").map((text) => ({ text }));

return matchSequential(rawLines, snippetQueue, (l) => l.text).map((line) => {
const performerNames = (line.performers || [])
.map((p) => p.name)
.filter(Boolean)
.join(", ");

return {
...line,
performer: performerNames || null,
};
});
}

return null;
Expand Down
4 changes: 3 additions & 1 deletion CustomApps/lyrics-plus/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const CONFIG = {
"musixmatch-translation-language": localStorage.getItem("lyrics-plus:visual:musixmatch-translation-language") || "none",
"fade-blur": getConfig("lyrics-plus:visual:fade-blur"),
"fullscreen-key": localStorage.getItem("lyrics-plus:visual:fullscreen-key") || "f12",
"show-performers": getConfig("lyrics-plus:visual:show-performers", true),
"synced-compact": getConfig("lyrics-plus:visual:synced-compact"),
"dual-genius": getConfig("lyrics-plus:visual:dual-genius"),
"global-delay": Number(localStorage.getItem("lyrics-plus:visual:global-delay")) || 0,
Expand Down Expand Up @@ -1065,6 +1066,7 @@ class LyricsContainer extends react.Component {
const friendlyLanguage = lang && new Intl.DisplayNames(["en"], { type: "language" }).of(lang.split("-")[0])?.toLowerCase();
const hasMusixmatchLanguages = Array.isArray(this.state.musixmatchAvailableTranslations) && this.state.musixmatchAvailableTranslations.length > 0;
const hasTranslation = this.state.neteaseTranslation !== null || this.state.musixmatchTranslation !== null || hasMusixmatchLanguages;
const hasPerformer = !!this.state.currentLyrics?.some((line) => line.performer);

if (mode !== -1) {
showTranslationButton = (friendlyLanguage || hasTranslation) && (mode === SYNCED || mode === UNSYNCED);
Expand Down Expand Up @@ -1160,7 +1162,7 @@ class LyricsContainer extends react.Component {
musixmatchLanguages: this.state.musixmatchAvailableTranslations || [],
musixmatchSelectedLanguage: this.state.musixmatchTranslationLanguage || CONFIG.visual["musixmatch-translation-language"],
}),
react.createElement(AdjustmentsMenu, { mode }),
react.createElement(AdjustmentsMenu, { mode, hasPerformer }),
react.createElement(
Spicetify.ReactComponent.TooltipWrapper,
{
Expand Down
Loading