From d522b47453d915075bf757570d23a65475bdaa93 Mon Sep 17 00:00:00 2001 From: Sora <34794115+sorae42@users.noreply.github.com> Date: Wed, 19 Jul 2023 21:11:29 +0700 Subject: [PATCH] MusicBrainz as Artwork Supplier (#66) * getting artwork from musicbrainz * replace parameter * musicbrainz is quirky sometime * fix indentation * i forgot * fix a bit * fix ts errors * rename things, mb lookup typing, bump cache ver * musicbrainz: escape query, parentheses cleaning --------- Co-authored-by: NextFire --- music-rpc.ts | 126 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 29 deletions(-) diff --git a/music-rpc.ts b/music-rpc.ts index 5ea77b6..5e4a631 100755 --- a/music-rpc.ts +++ b/music-rpc.ts @@ -9,15 +9,15 @@ import type { iTunes } from "https://raw.githubusercontent.com/NextFire/jxa/v0.0 // Cache class Cache { - static VERSION = 4; + static VERSION = 5; static CACHE_FILE = "cache.json"; - static #data: Map = new Map(); + static #data: Map = new Map(); static get(key: string) { return this.#data.get(key); } - static set(key: string, value: iTunesInfos) { + static set(key: string, value: TrackExtras) { this.#data.set(key, value); this.saveCache(); } @@ -118,26 +118,26 @@ function getProps(): Promise { }, APP_NAME); } -// iTunes Search API - -async function iTunesSearch(props: iTunesProps): Promise { +async function getTrackExtras(props: iTunesProps): Promise { const { name, artist, album } = props; const cacheIndex = `${name} ${artist} ${album}`; let infos = Cache.get(cacheIndex); if (!infos) { - infos = await _iTunesSearch(name, artist, album); + infos = await _getTrackExtras(name, artist, album); Cache.set(cacheIndex, infos); } return infos; } -async function _iTunesSearch( +// iTunes Search API + +async function _getTrackExtras( song: string, artist: string, album: string -): Promise { +): Promise { // Asterisks tend to result in no songs found, and songs are usually able to be found without it const query = `${song} ${artist} ${album}`.replace("*", ""); const params = new URLSearchParams({ @@ -163,16 +163,69 @@ async function _iTunesSearch( } else if (album.match(/\(.*\)$/)) { // If there are no results, try to remove the part // of the album name in parentheses (e.g. "Album (Deluxe Edition)") - return await _iTunesSearch( + return await _getTrackExtras( song, artist, album.replace(/\(.*\)$/, "").trim() ); } - const artwork = result?.artworkUrl100 ?? null; - const url = result?.trackViewUrl ?? null; - return { artwork, url }; + const artworkUrl = + result?.artworkUrl100 ?? (await _getMBArtwork(artist, song, album)) ?? null; + + const iTunesUrl = result?.trackViewUrl ?? null; + return { artworkUrl, iTunesUrl }; +} + +// MusicBrainz Artwork Getter + +const MB_EXCLUDED_NAMES = ["", "Various Artist"]; +const luceneEscape = (term: string) => + term.replace(/([+\-&|!(){}\[\]^"~*?:\\])/g, "\\$1"); +const removeParenthesesContent = (term: string) => + term.replace(/\([^)]*\)/g, "").trim(); + +async function _getMBArtwork( + artist: string, + song: string, + album: string +): Promise { + const queryTerms = []; + if (!MB_EXCLUDED_NAMES.every((elem) => artist.includes(elem))) { + queryTerms.push( + `artist:"${luceneEscape(removeParenthesesContent(artist))}"` + ); + } + if (!MB_EXCLUDED_NAMES.every((elem) => album.includes(elem))) { + queryTerms.push(`release:"${luceneEscape(album)}"`); + } else { + queryTerms.push(`recording:"${luceneEscape(song)}"`); + } + const query = queryTerms.join(" "); + + const params = new URLSearchParams({ + fmt: "json", + limit: "10", + query, + }); + + let resp: Response; + let result: string | undefined; + + resp = await fetch(`https://musicbrainz.org/ws/2/release?${params}`); + const json: MBReleaseLookupResponse = await resp.json(); + + for (const release of json.releases) { + resp = await fetch( + `https://coverartarchive.org/release/${release.id}/front` + ); + if (resp.ok) { + result = resp.url; + break; + } + } + + return result; } // Activity setter @@ -211,30 +264,37 @@ async function setActivity(rpc: Client) { activity.state = formatStr(props.artist); } - const query = `artist:${props.artist} track:${props.name}`; - activity.buttons = [ - { - label: "Search on Spotify", - url: encodeURI(`https://open.spotify.com/search/${query}?si`), - }, - ]; - // album.length == 0 for radios if (props.album.length > 0) { - const infos = await iTunesSearch(props); + const buttons = []; + + const infos = await getTrackExtras(props); console.log("infos:", infos); activity.assets = { - large_image: infos.artwork ?? "appicon", + large_image: infos.artworkUrl ?? "appicon", large_text: formatStr(props.album), }; - if (infos.url) { - activity.buttons.unshift({ + if (infos.iTunesUrl) { + buttons.push({ label: "Play on Apple Music", - url: infos.url, + url: infos.iTunesUrl, }); } + + const query = `artist:${props.artist} track:${props.name}`; + const spotifyUrl = encodeURI( + `https://open.spotify.com/search/${query}?si` + ); + if (spotifyUrl.length <= 512) { + buttons.push({ + label: "Search on Spotify", + url: spotifyUrl, + }); + } + + if (buttons.length > 0) activity.buttons = buttons; } await rpc.setActivity(activity); @@ -277,9 +337,9 @@ interface iTunesProps { playerPosition: number; } -interface iTunesInfos { - artwork: string | null; - url: string | null; +interface TrackExtras { + artworkUrl: string | null; + iTunesUrl: string | null; } interface iTunesSearchResponse { @@ -293,3 +353,11 @@ interface iTunesSearchResult { artworkUrl100: string; trackViewUrl: string; } + +interface MBReleaseLookupResponse { + releases: MBRelease[]; +} + +interface MBRelease { + id: string; +}