From f702c4ffd5e3d7e7edbfb3529d830879462fd650 Mon Sep 17 00:00:00 2001 From: ConfiG Date: Fri, 27 Sep 2024 23:00:33 +0300 Subject: [PATCH] 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 035088edf..a2d44ee79 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, };