From d25aa0353635f0daac365c8483bbfe3e74b2b427 Mon Sep 17 00:00:00 2001 From: Lioncat6 <95449321+Lioncat6@users.noreply.github.com> Date: Wed, 19 Aug 2026 22:34:04 -0500 Subject: [PATCH] MusicRRPC: Improve listenbrainz support (#4492) Co-authored-by: Angel Wang Co-authored-by: V --- src/plugins/musicRichPresence/listenbrainz.ts | 122 +++++++++++------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/src/plugins/musicRichPresence/listenbrainz.ts b/src/plugins/musicRichPresence/listenbrainz.ts index e19148620..38950dc7e 100644 --- a/src/plugins/musicRichPresence/listenbrainz.ts +++ b/src/plugins/musicRichPresence/listenbrainz.ts @@ -34,81 +34,107 @@ function fallbackToYoutubeThumbnail(originUrl: string | undefined): string | und return match ? `https://i.ytimg.com/vi/${match[5]}/maxresdefault.jpg` : undefined; } -async function fetchCoverArt(releaseGroupMBID: string, originUrl?: string): Promise { - if (!releaseGroupMBID) return fallbackToYoutubeThumbnail(originUrl); - - if (coverArtCache.has(releaseGroupMBID)) { - return coverArtCache.get(releaseGroupMBID); +async function fetchCoverArtArchive(id: string, path: string) { + if (coverArtCache.has(id)) { + return coverArtCache.get(id); } - const res = await fetch(`https://coverartarchive.org/release-group/${releaseGroupMBID}`); - if (!res.ok) return fallbackToYoutubeThumbnail(originUrl); - - const url = await res.json() - .then(json => json.images[0].thumbnails.large ?? fallbackToYoutubeThumbnail(originUrl)); - coverArtCache.set(releaseGroupMBID, url); + const url = await fetch(`https://coverartarchive.org${path}`) + .then(res => + res.ok + ? res.json() + : Promise.reject(new Error(`${res.status} ${res.statusText}`)) + ) + .then(json => json.images[0].thumbnails.large); + coverArtCache.set(id, url); return url; } -async function getUrls(additionalInfo: Record | undefined, trackName: string, artistName: string, releaseName: string): Promise> { +async function fetchCoverArt(releaseMBID: string, releaseGroupMBID: string, originUrl?: string): Promise { + return ( + (releaseMBID && await fetchCoverArtArchive(releaseMBID, `/release/${releaseMBID}`).catch(() => null)) || + (releaseGroupMBID && await fetchCoverArtArchive(releaseGroupMBID, `/release-group/${releaseGroupMBID}`).catch(() => null)) || + fallbackToYoutubeThumbnail(originUrl) + ); +} + +async function tryLookup(query: string): Promise | undefined> { + const params = new URLSearchParams({ + fmt: "json", + limit: "1" + }); + return await fetch("https://musicbrainz.org/ws/2/recording/?" + params + "&query=" + query, { + headers: { "User-Agent": VENCORD_USER_AGENT } + }) + .then(res => res.ok ? res.json() : Promise.reject(new Error(`${res.status} ${res.statusText}`))) + .then(json => json.recordings?.[0]); +} + +async function getUrls( + additionalInfo: Record | undefined, + trackName: string, + artistName: string, + releaseName: string +): Promise> { // Well tagged music will have MBIDs which we can use directly. These are optional but highly recommended in ListenBrainz scrobbles. // If your music doesn't have these, it's highly recommended to use https://picard.musicbrainz.org/ to automatically add them if (additionalInfo?.recording_mbid) { const { release_group_mbid, release_mbid, recording_mbid, artist_mbids, origin_url } = additionalInfo; return { - imageURL: await fetchCoverArt(release_group_mbid, origin_url), - trackURL: recording_mbid ? url(`/track/${recording_mbid}`) : undefined, - albumURL: release_group_mbid - ? url(`/release-group/${release_group_mbid}`) - : release_mbid - ? url(`/release/${release_mbid}`) + imageURL: await fetchCoverArt(release_mbid, release_group_mbid, origin_url), + trackURL: url(`/track/${recording_mbid}`), + albumURL: release_mbid + ? url(`/release/${release_mbid}`) + : release_group_mbid + ? url(`/release-group/${release_group_mbid}`) : undefined, artistURL: artist_mbids?.length ? url(`/artist/${artist_mbids[0]}`) : undefined, }; } - // If no MBIDs are present, try to search for the track on MusicBrainz + // If no MBIDs are present, try searching MusicBrainz: first by ISRC (if present), then by name. + const nameQuery = encodeURIComponent( + `artist:"${artistName}" AND recording:"${trackName}"${releaseName ? ` AND release:"${releaseName}"` : ""}` + ).replace(/[!()*\-~]/g, "\\$&"); - let rawQuery = `artist:"${artistName}" AND recording:"${trackName}"`; - if (releaseName) - rawQuery += ` AND album:"${releaseName}"`; - const query = encodeURIComponent(rawQuery); + const queries = additionalInfo?.isrc + ? [encodeURIComponent(`isrc:${additionalInfo.isrc}`), nameQuery] + : [nameQuery]; - if (metadataCache.has(query)) { - return metadataCache.get(query) ?? {}; - } + for (const query of queries) { + if (metadataCache.has(query)) { + return metadataCache.get(query) ?? {}; + } - const params = new URLSearchParams({ - fmt: "json", - limit: "1" - }); + const metadata = await tryLookup(query); + if (!metadata) continue; - const metadata = await fetch("https://musicbrainz.org/ws/2/recording/?" + params + "&query=" + query, { - headers: { "User-Agent": VENCORD_USER_AGENT } - }) - .then(res => res.ok ? res.json() : Promise.reject(new Error(`${res.status} ${res.statusText}`))) - .then(json => json.recordings?.[0]); + const artist = metadata["artist-credit"]?.[0]?.artist; + const release = metadata.releases?.find((release: { title: string; }) => release.title === releaseName) || metadata.releases?.[0]; - if (!metadata) { - const data = additionalInfo?.origin_url ? { imageURL: fallbackToYoutubeThumbnail(additionalInfo.origin_url) } : {}; + const data: Partial = { + imageURL: await fetchCoverArt(release?.id, release?.["release-group"]?.id, additionalInfo?.origin_url), + trackURL: url(`/track/${metadata.id}/`), + albumURL: release?.id + ? url(`/release/${release.id}/`) + : release?.["release-group"]?.id + ? url(`/release-group/${release["release-group"].id}/`) + : undefined, + artistURL: artist?.id ? url(`/artist/${artist.id}/`) : undefined, + album: additionalInfo?.release_name ?? release?.title ?? "Unknown", + }; metadataCache.set(query, data); return data; } - const artist = metadata["artist-credit"]?.[0]?.artist; - const release = metadata.releases?.[0]; + const fallback: Partial = additionalInfo?.origin_url + ? { imageURL: fallbackToYoutubeThumbnail(additionalInfo.origin_url) } + : {}; + metadataCache.set(queries.at(-1)!, fallback); - const data: Partial = { - imageURL: await fetchCoverArt(release?.["release-group"]?.id, additionalInfo?.origin_url), - trackURL: url(`/track/${metadata.id}/`), - albumURL: release?.id ? url(`/release/${release.id}/`) : release?.["release-group"]?.id ? url(`/release-group/${release["release-group"].id}/`) : undefined, - artistURL: artist?.id ? url(`/artist/${artist.id}/`) : undefined, - }; - metadataCache.set(query, data); - - return data; + return fallback; } export const ListenBrainzScrobbler: ScrobblerBackend = {