From b9865439063b1aa610eed469e236238efa493148 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Thu, 26 Sep 2024 00:37:18 +0300 Subject: [PATCH 1/7] LastFMRichPresence: add listenbrainz support --- src/plugins/lastfm/index.tsx | 116 +++++++++++++++++++++++++++++++---- src/utils/constants.ts | 4 ++ 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 02fd694f86d..7299ecf6114 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -109,18 +109,27 @@ const settings = definePluginSettings({ description: "last.fm api key", type: OptionType.STRING, }, + useListenBrainz: { + description: "use listenbrainz instead of last.fm", + type: OptionType.BOOLEAN, + default: false, + }, + listenBrainzUsername: { + description: "listenbrainz username", + type: OptionType.STRING, + }, shareUsername: { - description: "show link to last.fm profile", + description: "show link to last.fm/listenbrainz profile", type: OptionType.BOOLEAN, default: false, }, shareSong: { - description: "show link to song on last.fm", + description: "show link to song on last.fm/listenbrainz", type: OptionType.BOOLEAN, default: true, }, hideWithSpotify: { - description: "hide last.fm presence if spotify is running", + description: "hide last.fm/listenbrainz presence if spotify is running", type: OptionType.BOOLEAN, default: true, }, @@ -189,8 +198,8 @@ const settings = definePluginSettings({ export default definePlugin({ name: "LastFMRichPresence", - description: "Little plugin for Last.fm rich presence", - authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu], + description: "Little plugin for Last.fm and ListenBrainz rich presence", + authors: [Devs.dzshn, Devs.RuiNtD, Devs.blahajZip, Devs.archeruwu, Devs.ConfiG], settingsAboutComponent: () => ( <> @@ -219,7 +228,7 @@ export default definePlugin({ clearInterval(this.updateInterval); }, - async fetchTrackData(): Promise { + async fetchLastFM(): Promise { if (!settings.store.username || !settings.store.apiKey) return null; @@ -261,6 +270,78 @@ export default definePlugin({ } }, + async fetchListenBrainz(): Promise { + if (!settings.store.listenBrainzUsername) + return null; + + try { + const res = await fetch(`https://api.listenbrainz.org/1/user/${settings.store.listenBrainzUsername}/playing-now`); + if (!res.ok) throw `${res.status} ${res.statusText}`; + + const json = await res.json(); + if (json.error) { + logger.error("Error from ListenBrainz API", `${json.error}: ${json.message}`); + return null; + } + + const trackData = json.payload.listens[0]; + + if (!trackData?.playing_now) + return null; + + let recordingMbid = trackData.track_metadata.additional_info.recording_mbid; + let releaseMbid = trackData.track_metadata.additional_info.release_mbid; + + if (!recordingMbid || !releaseMbid) { + const metadata = await this.lookupListenBrainzMetadata( + trackData.track_metadata.artist_name, + trackData.track_metadata.track_name, + trackData.track_metadata.release_name + ); + + recordingMbid = recordingMbid || metadata.recording_mbid; + releaseMbid = releaseMbid || metadata.release_mbid; + } + + return { + name: trackData.track_metadata.track_name, + album: trackData.track_metadata.release_name, + artist: trackData.track_metadata.artist_name, + url: recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, + imageUrl: releaseMbid && `https://coverartarchive.org/release/${releaseMbid}/front` + }; + } catch (e) { + logger.error("Failed to query ListenBrainz API", e); + // will clear the rich presence if API fails + return null; + } + }, + + async lookupListenBrainzMetadata(artistName: string, recordingName: string, releaseName: string | undefined) { + try { + const params = new URLSearchParams({ + artist_name: artistName, + recording_name: recordingName + }); + if (releaseName) + params.append("release_name", releaseName); + + const res = await fetch(`https://api.listenbrainz.org/1/metadata/lookup/?${params}`); + if (!res.ok) throw `${res.status} ${res.statusText}`; + + const json = await res.json(); + if (json.error) { + logger.error("Error from ListenBrainz API", `${json.error}: ${json.message}`); + return {}; + } + + return json; + } catch (e) { + logger.error("Failed to query ListenBrainz API", e); + return {}; + } + }, + async updatePresence() { setActivity(await this.getActivity()); }, @@ -283,7 +364,7 @@ export default definePlugin({ } } - const trackData = await this.fetchTrackData(); + const trackData = settings.store.useListenBrainz ? await this.fetchListenBrainz() : await this.fetchLastFM(); if (!trackData) return null; const largeImage = this.getLargeImage(trackData); @@ -302,13 +383,22 @@ export default definePlugin({ const buttons: ActivityButton[] = []; - if (settings.store.shareUsername) - buttons.push({ - label: "Last.fm Profile", - url: `https://www.last.fm/user/${settings.store.username}`, - }); + if (settings.store.shareUsername) { + if (settings.store.useListenBrainz) { + buttons.push({ + label: "ListenBrainz Profile", + url: `https://listenbrainz.org/user/${settings.store.listenBrainzUsername}`, + }); + } + else { + buttons.push({ + label: "Last.fm Profile", + url: `https://www.last.fm/user/${settings.store.username}`, + }); + } + } - if (settings.store.shareSong) + if (settings.store.shareSong && trackData.url) buttons.push({ label: "View Song", url: trackData.url, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 6653e630739..9f3a519d82b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -575,6 +575,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "RamziAH", id: 1279957227612147747n, }, + ConfiG: { + name: "ConfiG", + id: 357517231854190592n, + }, } satisfies Record); // iife so #__PURE__ works correctly From 13481ab6f27fea1aed3dd025b4549eb05ca7b704 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Thu, 26 Sep 2024 01:00:29 +0300 Subject: [PATCH 2/7] use origin_url for song url when available --- src/plugins/lastfm/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 7299ecf6114..035088edf57 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -307,7 +307,8 @@ export default definePlugin({ name: trackData.track_metadata.track_name, album: trackData.track_metadata.release_name, artist: trackData.track_metadata.artist_name, - url: recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, + url: trackData.track_metadata.additional_info.origin_url || + recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, imageUrl: releaseMbid && `https://coverartarchive.org/release/${releaseMbid}/front` }; } catch (e) { From f702c4ffd5e3d7e7edbfb3529d830879462fd650 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 27 Sep 2024 23:00:33 +0300 Subject: [PATCH 3/7] send timestamps --- src/plugins/lastfm/index.tsx | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 035088edf57..a2d44ee793e 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -60,6 +60,10 @@ interface TrackData { artist: string; url: string; imageUrl?: string; + timestamps?: { + start: number; + end: number; + }; } // only relevant enum values @@ -193,6 +197,11 @@ const settings = definePluginSettings({ description: "show the Last.fm logo by the album cover", type: OptionType.BOOLEAN, default: true, + }, + sendTimestamps: { + description: "show track duration / listening progress bar (currently only works on listenbrainz), keep in mind that these might not always be 100% accurate", + type: OptionType.BOOLEAN, + default: true, } }); @@ -220,12 +229,17 @@ export default definePlugin({ settings, start() { + this.timestampStuff = { + lastTrack: "", + lastTrackChange: Date.now() + }; this.updatePresence(); this.updateInterval = setInterval(() => { this.updatePresence(); }, 16000); }, stop() { clearInterval(this.updateInterval); + this.timestampStuff = undefined; }, async fetchLastFM(): Promise { @@ -309,7 +323,8 @@ export default definePlugin({ artist: trackData.track_metadata.artist_name, url: trackData.track_metadata.additional_info.origin_url || recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, - imageUrl: releaseMbid && `https://coverartarchive.org/release/${releaseMbid}/front` + imageUrl: releaseMbid && `https://coverartarchive.org/release/${releaseMbid}/front`, + timestamps: settings.store.sendTimestamps ? await this.getListenBrainzTimestamps(trackData) : undefined }; } catch (e) { logger.error("Failed to query ListenBrainz API", e); @@ -343,6 +358,70 @@ export default definePlugin({ } }, + // attempt to get timestamps using some heuristics + // pausing while listening and unpausing before the track would've ended will throw this off + // but other than that it's pretty accurate, at least accurate enough :p + async getListenBrainzTimestamps(trackData: any) { + if (!trackData.track_metadata.additional_info?.duration && !trackData.track_metadata.additional_info?.duration_ms) + return undefined; + + const now = Date.now(); + + const res = await fetch(`https://api.listenbrainz.org/1/user/${settings.store.listenBrainzUsername}/listens?count=1`); + if (!res.ok) throw `${res.status} ${res.statusText}`; + + const json = await res.json(); + if (json.error) { + logger.error("Error from ListenBrainz API", `${json.error}: ${json.message}`); + return undefined; + } + + const duration = trackData.track_metadata.additional_info.duration_ms || + trackData.track_metadata.additional_info.duration * 1000; + + const trackMetadataJson = JSON.stringify(trackData.track_metadata); + // track obviously changed + if (trackMetadataJson !== this.timestampStuff.lastTrack) { + this.timestampStuff.lastTrack = trackMetadataJson; + this.timestampStuff.lastTrackChange = now; + } + // track probably changed because current time exceeded expected track end time + else if (now > this.timestampStuff.lastTrackChange + duration) { + this.timestampStuff.lastTrackChange = now; + } + + const listenAddInfo = json.payload.count >= 1 && json.payload.listens[0].track_metadata.additional_info; + if (listenAddInfo?.duration || listenAddInfo?.duration_ms) { + const listenDuration = listenAddInfo.duration_ms || listenAddInfo.duration * 1000; + const listenStart = json.payload.listens[0].listened_at * 1000; + const listenEnd = listenStart + listenDuration; + + // this listen is current! we have accurate info! + if (now <= listenEnd) { + return { + start: listenStart, + end: listenEnd + }; + } + + // it is Pretty Safe to assume we are listening to music sequentially without stopping Most Of The Time + if (now <= listenEnd + duration) { + return { + start: listenEnd, + end: listenEnd + duration + }; + } + } + + // this technically won't be accurate but good enough + // until we get accurate info halfway through or 4 minutes into the track + // or it's not the first track we are listening to in a row + return { + start: this.timestampStuff.lastTrackChange, + end: this.timestampStuff.lastTrackChange + duration + }; + }, + async updatePresence() { setActivity(await this.getActivity()); }, @@ -435,6 +514,8 @@ export default definePlugin({ button_urls: buttons.map(v => v.url), }, + timestamps: trackData.timestamps, + type: settings.store.useListeningStatus ? ActivityType.LISTENING : ActivityType.PLAYING, flags: ActivityFlag.INSTANCE, }; From 9b80d38f1718da8cf3024f9776c376eae69bd6dd Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 27 Sep 2024 23:16:07 +0300 Subject: [PATCH 4/7] option to use client name as name format --- src/plugins/lastfm/index.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index a2d44ee793e..a87931e4c83 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -64,6 +64,7 @@ interface TrackData { start: number; end: number; }; + client?: string; } // only relevant enum values @@ -82,7 +83,8 @@ const enum NameFormat { SongFirst = "song-first", ArtistOnly = "artist", SongOnly = "song", - AlbumName = "album" + AlbumName = "album", + ClientName = "client" } const applicationId = "1108588077900898414"; @@ -170,6 +172,10 @@ const settings = definePluginSettings({ { label: "Use album name (falls back to custom status text if song has no album)", value: NameFormat.AlbumName + }, + { + label: "Use client name (either the streaming service or the music player, falls back to custom status text if song has no album)", + value: NameFormat.ClientName } ], }, @@ -303,8 +309,10 @@ export default definePlugin({ if (!trackData?.playing_now) return null; - let recordingMbid = trackData.track_metadata.additional_info.recording_mbid; - let releaseMbid = trackData.track_metadata.additional_info.release_mbid; + const trackAddInfo = trackData.track_metadata.additional_info; + + let recordingMbid = trackAddInfo.recording_mbid; + let releaseMbid = trackAddInfo.release_mbid; if (!recordingMbid || !releaseMbid) { const metadata = await this.lookupListenBrainzMetadata( @@ -321,10 +329,11 @@ export default definePlugin({ name: trackData.track_metadata.track_name, album: trackData.track_metadata.release_name, artist: trackData.track_metadata.artist_name, - url: trackData.track_metadata.additional_info.origin_url || + url: trackAddInfo.origin_url || recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, imageUrl: releaseMbid && `https://coverartarchive.org/release/${releaseMbid}/front`, - timestamps: settings.store.sendTimestamps ? await this.getListenBrainzTimestamps(trackData) : undefined + timestamps: settings.store.sendTimestamps ? await this.getListenBrainzTimestamps(trackData) : undefined, + client: trackAddInfo?.music_service_name || trackAddInfo?.music_service || trackAddInfo?.media_player }; } catch (e) { logger.error("Failed to query ListenBrainz API", e); @@ -496,6 +505,8 @@ export default definePlugin({ return trackData.name; case NameFormat.AlbumName: return trackData.album || settings.store.statusName; + case NameFormat.ClientName: + return trackData.client || settings.store.statusName; default: return settings.store.statusName; } From 57c88f1f8e59310976f45be04bc6f6cd55b8931c Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 27 Sep 2024 23:33:09 +0300 Subject: [PATCH 5/7] some fallbacks --- src/plugins/lastfm/index.tsx | 132 ++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index a87931e4c83..cbf0875cb5a 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -304,21 +304,22 @@ export default definePlugin({ return null; } - const trackData = json.payload.listens[0]; + const trackData = json.payload?.listens?.[0]; - if (!trackData?.playing_now) + if (!trackData?.playing_now || !trackData.track_metadata) return null; - const trackAddInfo = trackData.track_metadata.additional_info; + const trackMeta = trackData.track_metadata; + const trackAddInfo = trackMeta.additional_info; - let recordingMbid = trackAddInfo.recording_mbid; - let releaseMbid = trackAddInfo.release_mbid; + let recordingMbid = trackAddInfo?.recording_mbid; + let releaseMbid = trackAddInfo?.release_mbid; if (!recordingMbid || !releaseMbid) { const metadata = await this.lookupListenBrainzMetadata( - trackData.track_metadata.artist_name, - trackData.track_metadata.track_name, - trackData.track_metadata.release_name + trackMeta.artist_name, + trackMeta.track_name, + trackMeta.release_name ); recordingMbid = recordingMbid || metadata.recording_mbid; @@ -326,11 +327,10 @@ export default definePlugin({ } return { - name: trackData.track_metadata.track_name, - album: trackData.track_metadata.release_name, - artist: trackData.track_metadata.artist_name, - url: trackAddInfo.origin_url || - recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, + name: trackMeta.track_name || "Unknown", + album: trackMeta.release_name || "Unknown", + artist: trackMeta.artist_name || "Unknown", + url: trackAddInfo?.origin_url || recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, imageUrl: releaseMbid && `https://coverartarchive.org/release/${releaseMbid}/front`, timestamps: settings.store.sendTimestamps ? await this.getListenBrainzTimestamps(trackData) : undefined, client: trackAddInfo?.music_service_name || trackAddInfo?.music_service || trackAddInfo?.media_player @@ -371,64 +371,68 @@ export default definePlugin({ // pausing while listening and unpausing before the track would've ended will throw this off // but other than that it's pretty accurate, at least accurate enough :p async getListenBrainzTimestamps(trackData: any) { - if (!trackData.track_metadata.additional_info?.duration && !trackData.track_metadata.additional_info?.duration_ms) - return undefined; - - const now = Date.now(); - - const res = await fetch(`https://api.listenbrainz.org/1/user/${settings.store.listenBrainzUsername}/listens?count=1`); - if (!res.ok) throw `${res.status} ${res.statusText}`; + try { + if (!trackData.track_metadata.additional_info?.duration && !trackData.track_metadata.additional_info?.duration_ms) + return undefined; + + const now = Date.now(); + const duration = trackData.track_metadata.additional_info.duration_ms || + trackData.track_metadata.additional_info.duration * 1000; + + const trackMetadataJson = JSON.stringify(trackData.track_metadata); + // track obviously changed + if (trackMetadataJson !== this.timestampStuff.lastTrack) { + this.timestampStuff.lastTrack = trackMetadataJson; + this.timestampStuff.lastTrackChange = now; + } + // track probably changed because current time exceeded expected track end time + else if (now > this.timestampStuff.lastTrackChange + duration) { + this.timestampStuff.lastTrackChange = now; + } - const json = await res.json(); - if (json.error) { - logger.error("Error from ListenBrainz API", `${json.error}: ${json.message}`); - return undefined; - } + const res = await fetch(`https://api.listenbrainz.org/1/user/${settings.store.listenBrainzUsername}/listens?count=1`); + if (!res.ok) throw `${res.status} ${res.statusText}`; - const duration = trackData.track_metadata.additional_info.duration_ms || - trackData.track_metadata.additional_info.duration * 1000; + const json = await res.json(); + if (json.error) { + logger.error("Error from ListenBrainz API", `${json.error}: ${json.message}`); + return undefined; + } - const trackMetadataJson = JSON.stringify(trackData.track_metadata); - // track obviously changed - if (trackMetadataJson !== this.timestampStuff.lastTrack) { - this.timestampStuff.lastTrack = trackMetadataJson; - this.timestampStuff.lastTrackChange = now; - } - // track probably changed because current time exceeded expected track end time - else if (now > this.timestampStuff.lastTrackChange + duration) { - this.timestampStuff.lastTrackChange = now; - } + const listenAddInfo = json.payload.count >= 1 && json.payload.listens[0].track_metadata.additional_info; + if (listenAddInfo?.duration || listenAddInfo?.duration_ms) { + const listenDuration = listenAddInfo.duration_ms || listenAddInfo.duration * 1000; + const listenStart = json.payload.listens[0].listened_at * 1000; + const listenEnd = listenStart + listenDuration; + + // this listen is current! we have accurate info! + if (now <= listenEnd) { + return { + start: listenStart, + end: listenEnd + }; + } - const listenAddInfo = json.payload.count >= 1 && json.payload.listens[0].track_metadata.additional_info; - if (listenAddInfo?.duration || listenAddInfo?.duration_ms) { - const listenDuration = listenAddInfo.duration_ms || listenAddInfo.duration * 1000; - const listenStart = json.payload.listens[0].listened_at * 1000; - const listenEnd = listenStart + listenDuration; - - // this listen is current! we have accurate info! - if (now <= listenEnd) { - return { - start: listenStart, - end: listenEnd - }; + // it is Pretty Safe to assume we are listening to music sequentially without stopping Most Of The Time + if (now <= listenEnd + duration) { + return { + start: listenEnd, + end: listenEnd + duration + }; + } } - // it is Pretty Safe to assume we are listening to music sequentially without stopping Most Of The Time - if (now <= listenEnd + duration) { - return { - start: listenEnd, - end: listenEnd + duration - }; - } + // this technically won't be accurate but good enough + // until we get accurate info halfway through or 4 minutes into the track + // or it's not the first track we are listening to in a row + return { + start: this.timestampStuff.lastTrackChange, + end: this.timestampStuff.lastTrackChange + duration + }; + } catch (e) { + logger.error("Failed to query ListenBrainz API", e); + return undefined; } - - // this technically won't be accurate but good enough - // until we get accurate info halfway through or 4 minutes into the track - // or it's not the first track we are listening to in a row - return { - start: this.timestampStuff.lastTrackChange, - end: this.timestampStuff.lastTrackChange + duration - }; }, async updatePresence() { From b4af3e847b6d6908e1c224fc1888ffe11c2ed8ed Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 27 Sep 2024 23:35:54 +0300 Subject: [PATCH 6/7] change client name option label --- src/plugins/lastfm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index cbf0875cb5a..87f09543c1f 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -174,7 +174,7 @@ const settings = definePluginSettings({ value: NameFormat.AlbumName }, { - label: "Use client name (either the streaming service or the music player, falls back to custom status text if song has no album)", + label: "Use streaming service or music player name (falls back to custom status text if no client info is available)", value: NameFormat.ClientName } ], From 8168ec3f7ea20216d6db4e8ac03790e3ec4bbe51 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Sat, 28 Sep 2024 00:29:52 +0300 Subject: [PATCH 7/7] show client info in small image --- src/plugins/lastfm/index.tsx | 44 ++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/plugins/lastfm/index.tsx b/src/plugins/lastfm/index.tsx index 87f09543c1f..b03ff7799c4 100644 --- a/src/plugins/lastfm/index.tsx +++ b/src/plugins/lastfm/index.tsx @@ -65,6 +65,7 @@ interface TrackData { end: number; }; client?: string; + clientDetails: string; } // only relevant enum values @@ -94,8 +95,9 @@ const logger = new Logger("LastFMRichPresence"); const presenceStore = findByPropsLazy("getLocalPresence"); -async function getApplicationAsset(key: string): Promise { - return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; +async function getApplicationAsset(key: string): Promise { + const ids = await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]); + return ids.length > 0 ? ids[0] : undefined; } function setActivity(activity: Activity | null) { @@ -200,7 +202,7 @@ const settings = definePluginSettings({ ], }, showLastFmLogo: { - description: "show the Last.fm logo by the album cover", + description: "show the Last.fm/ListenBrainz/streaming service/music player logo by the album cover", type: OptionType.BOOLEAN, default: true, }, @@ -281,7 +283,8 @@ export default definePlugin({ album: trackData.album["#text"], artist: trackData.artist["#text"] || "Unknown", url: trackData.url, - imageUrl: trackData.image?.find((x: any) => x.size === "large")?.["#text"] + imageUrl: trackData.image?.find((x: any) => x.size === "large")?.["#text"], + clientDetails: "Last.fm" }; } catch (e) { logger.error("Failed to query Last.fm API", e); @@ -326,6 +329,29 @@ export default definePlugin({ releaseMbid = releaseMbid || metadata.release_mbid; } + let clientDetails = "ListenBrainz"; + if (trackAddInfo) { + const musicService = trackAddInfo.music_service_name || trackAddInfo.music_service; + + let mediaPlayer = trackAddInfo.media_player; + if (mediaPlayer && trackAddInfo.media_player_version) + mediaPlayer = `${mediaPlayer} ${trackAddInfo.media_player_version}`; + + let submissionClient = trackAddInfo.submission_client; + if (submissionClient && trackAddInfo.submission_client_version) + submissionClient = `${submissionClient} ${trackAddInfo.submission_client_version}`; + + if (submissionClient && trackAddInfo.submission_client !== trackAddInfo.media_player) + clientDetails = `${clientDetails} via ${submissionClient}`; + + if (musicService && mediaPlayer) + clientDetails = `${musicService} through ${mediaPlayer} to ${clientDetails}`; + else if (musicService) + clientDetails = `${musicService} to ${clientDetails}`; + else if (mediaPlayer) + clientDetails = `${mediaPlayer} to ${clientDetails}`; + } + return { name: trackMeta.track_name || "Unknown", album: trackMeta.release_name || "Unknown", @@ -333,7 +359,8 @@ export default definePlugin({ url: trackAddInfo?.origin_url || recordingMbid && `https://musicbrainz.org/recording/${recordingMbid}`, imageUrl: releaseMbid && `https://coverartarchive.org/release/${releaseMbid}/front`, timestamps: settings.store.sendTimestamps ? await this.getListenBrainzTimestamps(trackData) : undefined, - client: trackAddInfo?.music_service_name || trackAddInfo?.music_service || trackAddInfo?.media_player + client: trackAddInfo?.music_service_name || trackAddInfo?.music_service || trackAddInfo?.media_player, + clientDetails }; } catch (e) { logger.error("Failed to query ListenBrainz API", e); @@ -466,8 +493,11 @@ export default definePlugin({ large_image: await getApplicationAsset(largeImage), large_text: trackData.album || undefined, ...(settings.store.showLastFmLogo && { - small_image: await getApplicationAsset("lastfm-small"), - small_text: "Last.fm" + small_image: trackData.client && ( + await getApplicationAsset(`client-${trackData.client}-small`) || + trackData.url && await getApplicationAsset(encodeURI(`https://s2.googleusercontent.com/s2/favicons?domain=${trackData.url}`)) + ) || await getApplicationAsset("lastfm-small"), + small_text: trackData.clientDetails }), } : { large_image: await getApplicationAsset("lastfm-large"),