More typechecks on scrobble, use timeout on notification (#1004)

This commit is contained in:
Kendall Garner 2025-07-10 13:53:40 +00:00 committed by GitHub
parent 56130d8503
commit 69341f4492
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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 | ReturnType<typeof setInterval>>(null);
const songChangeTimeoutId = useRef<null | ReturnType<typeof setTimeout>>(null);
const songChangeTimeoutId = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const notifyTimeoutId = useRef<ReturnType<typeof setTimeout> | 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<typeof setTimeout>);
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<typeof setInterval>);
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],
},
);