Skip to content
Merged
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
27 changes: 27 additions & 0 deletions src/datasource/youtube/YouTubeMusicDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ type BetterLyricsResponse = {
was really decided by the order of an array literal. It is now the LYRICS_SOURCES table. */
type LyricsProviderResult = Lyrics;

type SimpMusicResponse = {
data?: Array<{ syncedLyrics?: string | null; vote?: number }>;
};

type RawLikeEndpoint = {
status?: string;
target?: string | {
Expand Down Expand Up @@ -4658,6 +4662,7 @@ export class YouTubeMusicDataSource extends DataSource {
"lrclib-exact": () => this.fetchLrcLibExactLyrics(track),
betterlyrics: () => this.fetchBetterLyrics(track),
"lrclib-search": () => this.fetchLrcLibSearchLyrics(track),
simpmusic: () => this.fetchSimpMusicLyrics(track),
"youtube-transcript": () => this.fetchYouTubeTranscriptLyrics(track),
"youtube-music": () => this.fetchYouTubeMusicLyrics(track),
};
Expand Down Expand Up @@ -4929,6 +4934,28 @@ export class YouTubeMusicDataSource extends DataSource {
return null;
}

// Keyed by video ID, so never another recording; ranked below LRCLIB because entries can be translations (#129).
private async fetchSimpMusicLyrics(track: Track): Promise<LyricsProviderResult | null> {
const response = await tauriFetch(
`https://api-lyrics.simpmusic.org/v1/${encodeURIComponent(track.id)}`,
{ headers: this.getLyricsRequestHeaders(), timeoutMs: 3_500 },
);
if (!response.ok) return null;

const body = await response.json() as SimpMusicResponse;
const best = (body.data ?? [])
.filter((entry) => entry.syncedLyrics)
.sort((left, right) => (right.vote ?? 0) - (left.vote ?? 0))[0];
const lines = best?.syncedLyrics ? this.parseSyncedLyrics(best.syncedLyrics) : [];
if (lines.length === 0) return null;

logInternalInfo("YouTubeMusicDataSource.getLyrics SimpMusic success", {
trackId: track.id,
lineCount: lines.length,
});
return { lines, timing: "synced", sourceLabel: "SimpMusic" };
}

private toLrcLibLyrics(
track: Track,
match: LrcLibTrack,
Expand Down
6 changes: 3 additions & 3 deletions src/datasource/youtube/lyricsSources.check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const synced = (count: number) => ({
timing: "synced" as const,
});

equal(LYRICS_SOURCES.length, 5, "every source is in the table");
equal(LYRICS_SOURCES.length, 6, "every source is in the table");
check(
rankOfSource("lrclib-exact") < rankOfSource("lrclib-search"),
"a duration-exact match outranks a text search of the same corpus",
Expand Down Expand Up @@ -139,7 +139,7 @@ equal(

const defaultWaves = planLyricsWaves();
equal(defaultWaves.length, 2, "by default the expensive sources are held back to a second wave");
equal(defaultWaves[0].length, 3, "three cheap sources race first");
equal(defaultWaves[0].length, 4, "four cheap sources race first");
check(
defaultWaves[1].every((source) => source.wave === 2),
"and only wave two sources are held back",
Expand All @@ -161,7 +161,7 @@ equal(
);
equal(
planLyricsWaves("lrclib-exact")[0].length,
3,
4,
"preferring a source already in wave one does not duplicate it",
);

Expand Down
9 changes: 8 additions & 1 deletion src/datasource/youtube/lyricsSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface LyricsSource {
*
* Declared here rather than left as an early `return null` inside the fetcher, because a
* fetcher that quietly returns nothing is indistinguishable from one that searched and
* found nothing — which is how two of five sources went missing on every track without a
* found nothing — which is how two of the sources went missing on every track without a
* duration while the UI cheerfully reported "No match".
*/
requiresDuration?: boolean;
Expand Down Expand Up @@ -55,6 +55,13 @@ export const LYRICS_SOURCES: LyricsSource[] = [
requiresDuration: true,
note: "Same corpus, matched by text within two seconds of duration — can land on a different master.",
},
{
id: "simpmusic",
label: "SimpMusic",
timeoutMs: 3_500,
wave: 1,
note: "Line-synced and keyed to this exact video, but community entries are sometimes translations.",
},
{
id: "youtube-transcript",
label: "YouTube transcript",
Expand Down
2 changes: 1 addition & 1 deletion src/ui/pages/LyricsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ const SyncedLine = memo(function SyncedLine({
*
* "No lyrics available" is the least useful sentence a music app can show — it gives the
* listener nothing to act on and gives a bug report nothing to go on. This turns it into a
* fact: which of the five ranked sources was asked, how long it took, and why it lost.
* fact: which of the ranked sources was asked, how long it took, and why it lost.
*/
function LyricsSourcePanel({
attempts,
Expand Down
Loading