MusicRRPC: Improve listenbrainz support (#4492)

Co-authored-by: Angel Wang <angel@angelcube.dev>
Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
Lioncat6 2026-08-19 22:34:04 -05:00 • committed by GitHub
parent 305eac5d26
commit d25aa03536
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<string | undefined> {
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<string, string> | undefined, trackName: string, artistName: string, releaseName: string): Promise<Partial<TrackData>> {
async function fetchCoverArt(releaseMBID: string, releaseGroupMBID: string, originUrl?: string): Promise<string | undefined> {
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<Record<string, any> | 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<string, string> | undefined,
trackName: string,
artistName: string,
releaseName: string
): Promise<Partial<TrackData>> {
// 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<TrackData> = {
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<TrackData> = additionalInfo?.origin_url
? { imageURL: fallbackToYoutubeThumbnail(additionalInfo.origin_url) }
: {};
metadataCache.set(queries.at(-1)!, fallback);
const data: Partial<TrackData> = {
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 = {