diff --git a/plugins/radiant-lyrics-luna/src/api.ts b/plugins/radiant-lyrics-luna/src/api.ts index 657f5ca..eaa569a 100644 --- a/plugins/radiant-lyrics-luna/src/api.ts +++ b/plugins/radiant-lyrics-luna/src/api.ts @@ -211,9 +211,10 @@ export interface ArtworkLookup { artwork: AnimatedArtwork | null; /** * Scope the result is valid for, as a dedupe key: the album key only when - * the answer was matched by album. A title-only fallback (or a miss) is - * specific to this track, so its key scopes to the title — the album's - * other tracks must not inherit it. + * the answer was matched by album (by name or via the track's ISRC, which + * the server resolves to the same album). A title-only fallback (or a + * miss) is specific to this track, so its key scopes to the title — the + * album's other tracks must not inherit it. */ key: string; } @@ -222,6 +223,13 @@ export interface ArtworkLookup { export const artworkTitleKey = (artist: string, title: string): string => `${artist}\u0000\u0000${title}`; +/** + * ISRC lookup: the server resolves the exact song by ISRC and answers + * from its album, immune to title/album name mismatches. + */ +const artworkIsrcUrl = (isrc: string): string => + `https://ama.trainswift.net/api/v1/artwork/search?isrc=${encodeURIComponent(isrc)}`; + function artworkSearchUrl( title: string, artist: string, @@ -234,8 +242,9 @@ function artworkSearchUrl( } // Cached including misses; most tracks have none and the answer never changes. -// Only album-matched results go under the album key — a title-only fallback -// must not stand in for the album's other tracks (see ArtworkLookup). +// Only album-matched results (by name or ISRC) go under the album key — a +// title-only fallback must not stand in for the album's other tracks (see +// ArtworkLookup). const animatedArtworkCache = new Map(); const ARTWORK_CACHE_MAX = 200; @@ -243,10 +252,12 @@ export async function fetchAnimatedArtwork( title: string, artist: string, album?: string, + isrc?: string, signal?: AbortSignal, ): Promise { const albumName = album?.trim() || ""; const albumPresent = albumName !== ""; + const isrcTrimmed = isrc?.trim() || ""; // Album key, matching artworkKey() in index.ts; without an album the // answer is only ever matched by title, so key by title directly. const cacheKey = albumPresent @@ -301,7 +312,15 @@ export async function fetchAnimatedArtwork( }; try { - // Exact album first; on a miss retry once without the album (server-side disambiguation is imperfect) + // ISRC first: the exact match. A hit is resolved through the track's + // album, so it is album-scoped (cacheKey); a miss says nothing about + // the album (the ISRC may simply be unknown to the source), so fall + // through to the name flow without caching it. + if (isrcTrimmed) { + const byIsrc = await tryFetch(artworkIsrcUrl(isrcTrimmed)); + if (byIsrc) return { artwork: remember(byIsrc), key: cacheKey }; + } + // Fallback: exact album first; on a miss retry once without the album (server-side disambiguation is imperfect) const withAlbum = await tryFetch( artworkSearchUrl(title, artist, album), ); diff --git a/plugins/radiant-lyrics-luna/src/artwork.ts b/plugins/radiant-lyrics-luna/src/artwork.ts index 4d46912..e53cdb1 100644 --- a/plugins/radiant-lyrics-luna/src/artwork.ts +++ b/plugins/radiant-lyrics-luna/src/artwork.ts @@ -29,7 +29,8 @@ export class AnimatedArtworkLayer { private artwork: { url: string; url_tall: string } | null = null; /** * Scope key of the displayed artwork (see fetchAnimatedArtwork): the album - * key only when the answer was matched by album, else the title key. + * key only when the answer was matched by album (name or ISRC), else the + * title key. */ private artworkKey: string | null = null; private liveSrc: string | null = null; @@ -121,6 +122,7 @@ export class AnimatedArtworkLayer { title: string, artist: string, album: string | undefined, + isrc?: string, ): Promise<{ done: boolean; key: string }> { // Already showing artwork that covers this track (album-matched, or the // same track again) — keep playing, touch nothing. @@ -143,7 +145,13 @@ export class AnimatedArtworkLayer { if (!document.querySelector(ART_TILE_SELECTOR)) return { done: false, key }; - const lookup = await fetchAnimatedArtwork(title, artist, album, signal); + const lookup = await fetchAnimatedArtwork( + title, + artist, + album, + isrc, + signal, + ); if (token !== this.loadToken) return { done: true, key: lookup.key }; if (!this.tileAlive()) return { done: false, key: lookup.key }; diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index f84a5a5..5b205b9 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -1152,6 +1152,7 @@ const updateAnimatedArtwork = async ( target.title, target.artist, target.album, + target.isrc, ); // Adopt the effective scope — a title-matched result (or a miss) must not // stand in for the album's other tracks — unless a newer trigger claimed.