diff --git a/src/renderer/features/player/hooks/use-scrobble.ts b/src/renderer/features/player/hooks/use-scrobble.ts index 88d798b9..3df9ab01 100644 --- a/src/renderer/features/player/hooks/use-scrobble.ts +++ b/src/renderer/features/player/hooks/use-scrobble.ts @@ -34,6 +34,8 @@ Progress Events (Jellyfin only): - Sends the 'progress' scrobble event on an interval */ +type PlayerEvent = [PlayerStatus, number]; + type SongEvent = [QueueSong | undefined, number, 1 | 2]; const checkScrobbleConditions = (args: { @@ -86,21 +88,35 @@ export const useScrobble = () => { ); const progressIntervalId = useRef>(null); - const songChangeTimeoutId = useRef>(null); + const songChangeTimeoutId = useRef | undefined>(undefined); + const notifyTimeoutId = useRef | undefined>(undefined); + const handleScrobbleFromSongChange = useCallback( (current: SongEvent, previous: SongEvent) => { - if (scrobbleSettings?.notify && current[0]) { + if (scrobbleSettings?.notify && current[0]?.id) { + clearTimeout(notifyTimeoutId.current); const currentSong = current[0]; - const artists = - currentSong.artists?.length > 0 - ? currentSong.artists.map((artist) => artist.name).join(', ') - : currentSong.artistName; + // Set a delay so that quickly (within a second) switching songs doesn't trigger multiple + // notifications + notifyTimeoutId.current = setTimeout(() => { + // Only trigger if the song changed, or the player changed. This should be the case + // anyways, but who knows + if ( + currentSong.uniqueId !== previous[0]?.uniqueId || + current[2] !== previous[2] + ) { + const artists = + currentSong.artists?.length > 0 + ? currentSong.artists.map((artist) => artist.name).join(', ') + : currentSong.artistName; - new Notification(`Now playing ${currentSong.name}`, { - body: `by ${artists} on ${currentSong.album}`, - icon: currentSong.imageUrl || undefined, - }); + new Notification(`Now playing ${currentSong.name}`, { + body: `by ${artists} on ${currentSong.album}`, + icon: currentSong.imageUrl || undefined, + }); + } + }, 1000); } if (!isScrobbleEnabled) return; @@ -110,7 +126,6 @@ export const useScrobble = () => { progressIntervalId.current = null; } - // const currentSong = current[0] as QueueSong | undefined; const previousSong = previous[0]; const previousSongTimeSec = previous[1]; @@ -146,7 +161,7 @@ export const useScrobble = () => { setIsCurrentSongScrobbled(false); // Use a timeout to prevent spamming the server with scrobble events when switching through songs quickly - clearTimeout(songChangeTimeoutId.current as ReturnType); + clearTimeout(songChangeTimeoutId.current); songChangeTimeoutId.current = setTimeout(() => { const currentSong = current[0]; // Get the current status from the state, not variable. This is because @@ -193,10 +208,7 @@ export const useScrobble = () => { ); const handleScrobbleFromStatusChange = useCallback( - ( - current: (number | PlayerStatus | undefined)[], - previous: (number | PlayerStatus | undefined)[], - ) => { + (current: PlayerEvent, previous: PlayerEvent) => { if (!isScrobbleEnabled) return; const currentSong = usePlayerStore.getState().current.song; @@ -208,8 +220,8 @@ export const useScrobble = () => { ? usePlayerStore.getState().current.time * 1e7 : undefined; - const currentStatus = current[0] as PlayerStatus; - const currentTimeSec = current[1] as number; + const currentStatus = current[0]; + const currentTimeSec = current[1]; // Whenever the player is restarted, send a 'start' scrobble if (currentStatus === PlayerStatus.PLAYING) { @@ -249,12 +261,12 @@ export const useScrobble = () => { }); if (progressIntervalId.current) { - clearInterval(progressIntervalId.current as ReturnType); + clearInterval(progressIntervalId.current); progressIntervalId.current = null; } } else { const isLastTrackInQueue = usePlayerStore.getState().actions.checkIsLastTrack(); - const previousTimeSec = previous[1] as number; + const previousTimeSec = previous[1]; // If not already scrobbled, send a 'submission' scrobble if conditions are met const shouldSubmitScrobble = checkScrobbleConditions({ @@ -358,17 +370,17 @@ export const useScrobble = () => { // multiple times in a row and playback goes normally (no next/previous) equalityFn: (a, b) => // compute whether the song changed - (a[0] as QueueSong)?.uniqueId === (b[0] as QueueSong)?.uniqueId && + a[0]?.uniqueId === b[0]?.uniqueId && // compute whether the same player: relevant for repeat one and repeat all (one track) a[2] === b[2], }, ); const unsubStatusChange = usePlayerStore.subscribe( - (state) => [state.current.status, state.current.time], + (state): PlayerEvent => [state.current.status, state.current.time], handleScrobbleFromStatusChange, { - equalityFn: (a, b) => (a[0] as PlayerStatus) === (b[0] as PlayerStatus), + equalityFn: (a, b) => a[0] === b[0], }, );