Lint all files

This commit is contained in:
jeffvli 2023-07-01 19:10:05 -07:00
parent 22af76b4d6
commit 30e52ebb54
334 changed files with 76519 additions and 75932 deletions

File diff suppressed because it is too large Load diff

View file

@ -7,43 +7,43 @@ import { toast } from '/@/renderer/components/toast/index';
import isElectron from 'is-electron';
import { nanoid } from 'nanoid/non-secure';
import {
LibraryItem,
QueueSong,
Song,
SongListResponse,
instanceOfCancellationError,
LibraryItem,
QueueSong,
Song,
SongListResponse,
instanceOfCancellationError,
} from '/@/renderer/api/types';
import {
getPlaylistSongsById,
getSongById,
getAlbumSongsById,
getAlbumArtistSongsById,
getSongsByQuery,
getPlaylistSongsById,
getSongById,
getAlbumSongsById,
getAlbumArtistSongsById,
getSongsByQuery,
} from '/@/renderer/features/player/utils';
import { queryKeys } from '/@/renderer/api/query-keys';
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
let queryKey;
let queryKey;
switch (itemType) {
case LibraryItem.ALBUM:
queryKey = queryKeys.songs.list(serverId);
break;
case LibraryItem.ALBUM_ARTIST:
queryKey = queryKeys.songs.list(serverId);
break;
case LibraryItem.PLAYLIST:
queryKey = queryKeys.playlists.songList(serverId);
break;
case LibraryItem.SONG:
queryKey = queryKeys.songs.list(serverId);
break;
default:
queryKey = queryKeys.songs.list(serverId);
break;
}
switch (itemType) {
case LibraryItem.ALBUM:
queryKey = queryKeys.songs.list(serverId);
break;
case LibraryItem.ALBUM_ARTIST:
queryKey = queryKeys.songs.list(serverId);
break;
case LibraryItem.PLAYLIST:
queryKey = queryKeys.playlists.songList(serverId);
break;
case LibraryItem.SONG:
queryKey = queryKeys.songs.list(serverId);
break;
default:
queryKey = queryKeys.songs.list(serverId);
break;
}
return queryKey;
return queryKey;
};
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
@ -53,118 +53,133 @@ const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
const addToQueue = usePlayerStore.getState().actions.addToQueue;
export const useHandlePlayQueueAdd = () => {
const queryClient = useQueryClient();
const playerType = usePlayerType();
const server = useCurrentServer();
const { play } = usePlayerControls();
const timeoutIds = useRef<Record<string, ReturnType<typeof setTimeout>> | null>({});
const queryClient = useQueryClient();
const playerType = usePlayerType();
const server = useCurrentServer();
const { play } = usePlayerControls();
const timeoutIds = useRef<Record<string, ReturnType<typeof setTimeout>> | null>({});
const handlePlayQueueAdd = useCallback(
async (options: PlayQueueAddOptions) => {
if (!server) return toast.error({ message: 'No server selected', type: 'error' });
const { initialIndex, initialSongId, playType, byData, byItemType, query } = options;
let songs: QueueSong[] | null = null;
let initialSongIndex = 0;
const handlePlayQueueAdd = useCallback(
async (options: PlayQueueAddOptions) => {
if (!server) return toast.error({ message: 'No server selected', type: 'error' });
const { initialIndex, initialSongId, playType, byData, byItemType, query } = options;
let songs: QueueSong[] | null = null;
let initialSongIndex = 0;
if (byItemType) {
let songList: SongListResponse | undefined;
const { type: itemType, id } = byItemType;
if (byItemType) {
let songList: SongListResponse | undefined;
const { type: itemType, id } = byItemType;
const fetchId = nanoid();
timeoutIds.current = {
...timeoutIds.current,
[fetchId]: setTimeout(() => {
toast.info({
autoClose: false,
id: fetchId,
message: 'This is taking a while... close the notification to cancel the request',
onClose: () => {
queryClient.cancelQueries({
exact: false,
queryKey: getRootQueryKey(itemType, server?.id),
});
},
title: 'Adding to queue',
});
}, 2000),
};
const fetchId = nanoid();
timeoutIds.current = {
...timeoutIds.current,
[fetchId]: setTimeout(() => {
toast.info({
autoClose: false,
id: fetchId,
message:
'This is taking a while... close the notification to cancel the request',
onClose: () => {
queryClient.cancelQueries({
exact: false,
queryKey: getRootQueryKey(itemType, server?.id),
});
},
title: 'Adding to queue',
});
}, 2000),
};
try {
if (itemType === LibraryItem.PLAYLIST) {
songList = await getPlaylistSongsById({ id: id?.[0], query, queryClient, server });
} else if (itemType === LibraryItem.ALBUM) {
songList = await getAlbumSongsById({ id, query, queryClient, server });
} else if (itemType === LibraryItem.ALBUM_ARTIST) {
songList = await getAlbumArtistSongsById({ id, query, queryClient, server });
} else if (itemType === LibraryItem.SONG) {
if (id?.length === 1) {
songList = await getSongById({ id: id?.[0], queryClient, server });
} else {
songList = await getSongsByQuery({ query, queryClient, server });
try {
if (itemType === LibraryItem.PLAYLIST) {
songList = await getPlaylistSongsById({
id: id?.[0],
query,
queryClient,
server,
});
} else if (itemType === LibraryItem.ALBUM) {
songList = await getAlbumSongsById({ id, query, queryClient, server });
} else if (itemType === LibraryItem.ALBUM_ARTIST) {
songList = await getAlbumArtistSongsById({
id,
query,
queryClient,
server,
});
} else if (itemType === LibraryItem.SONG) {
if (id?.length === 1) {
songList = await getSongById({ id: id?.[0], queryClient, server });
} else {
songList = await getSongsByQuery({ query, queryClient, server });
}
}
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
delete timeoutIds.current[fetchId];
toast.hide(fetchId);
} catch (err: any) {
if (instanceOfCancellationError(err)) {
return null;
}
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
delete timeoutIds.current[fetchId];
toast.hide(fetchId);
return toast.error({
message: err.message,
title: 'Play queue add failed',
});
}
songs =
songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
} else if (byData) {
songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
}
}
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
delete timeoutIds.current[fetchId];
toast.hide(fetchId);
} catch (err: any) {
if (instanceOfCancellationError(err)) {
if (!songs || songs?.length === 0)
return toast.warn({
message: 'The query returned no results',
title: 'No tracks added',
});
if (initialIndex) {
initialSongIndex = initialIndex;
} else if (initialSongId) {
initialSongIndex = songs.findIndex((song) => song.id === initialSongId);
}
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
if (playerType === PlaybackType.LOCAL) {
mpvPlayer?.volume(usePlayerStore.getState().volume);
if (playType === Play.NEXT || playType === Play.LAST) {
mpvPlayer?.setQueueNext(playerData);
}
if (playType === Play.NOW) {
mpvPlayer?.setQueue(playerData);
mpvPlayer?.play();
}
}
play();
mpris?.updateSong({
currentTime: usePlayerStore.getState().current.time,
repeat: usePlayerStore.getState().repeat,
shuffle: usePlayerStore.getState().shuffle,
song: playerData.current.song,
status: 'Playing',
});
return null;
}
},
[play, playerType, queryClient, server],
);
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
delete timeoutIds.current[fetchId];
toast.hide(fetchId);
return toast.error({
message: err.message,
title: 'Play queue add failed',
});
}
songs = songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
} else if (byData) {
songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
}
if (!songs || songs?.length === 0)
return toast.warn({ message: 'The query returned no results', title: 'No tracks added' });
if (initialIndex) {
initialSongIndex = initialIndex;
} else if (initialSongId) {
initialSongIndex = songs.findIndex((song) => song.id === initialSongId);
}
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
if (playerType === PlaybackType.LOCAL) {
mpvPlayer?.volume(usePlayerStore.getState().volume);
if (playType === Play.NEXT || playType === Play.LAST) {
mpvPlayer?.setQueueNext(playerData);
}
if (playType === Play.NOW) {
mpvPlayer?.setQueue(playerData);
mpvPlayer?.play();
}
}
play();
mpris?.updateSong({
currentTime: usePlayerStore.getState().current.time,
repeat: usePlayerStore.getState().repeat,
shuffle: usePlayerStore.getState().shuffle,
song: playerData.current.song,
status: 'Playing',
});
return null;
},
[play, playerType, queryClient, server],
);
return handlePlayQueueAdd;
return handlePlayQueueAdd;
};

View file

@ -2,6 +2,6 @@ import { useContext } from 'react';
import { PlayQueueHandlerContext } from '/@/renderer/features/player/context/play-queue-handler-context';
export const usePlayQueueAdd = () => {
const { handlePlayQueueAdd } = useContext(PlayQueueHandlerContext);
return handlePlayQueueAdd;
const { handlePlayQueueAdd } = useContext(PlayQueueHandlerContext);
return handlePlayQueueAdd;
};

View file

@ -10,123 +10,123 @@ const utils = isElectron() ? window.electron.utils : null;
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
let volumeToSet;
const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100;
if (newVolumeGreaterThanHundred) {
volumeToSet = 100;
} else {
volumeToSet = volume + volumeWheelStep;
}
let volumeToSet;
const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100;
if (newVolumeGreaterThanHundred) {
volumeToSet = 100;
} else {
volumeToSet = volume + volumeWheelStep;
}
return volumeToSet;
return volumeToSet;
};
const calculateVolumeDown = (volume: number, volumeWheelStep: number) => {
let volumeToSet;
const newVolumeLessThanZero = volume - volumeWheelStep < 0;
if (newVolumeLessThanZero) {
volumeToSet = 0;
} else {
volumeToSet = volume - volumeWheelStep;
}
let volumeToSet;
const newVolumeLessThanZero = volume - volumeWheelStep < 0;
if (newVolumeLessThanZero) {
volumeToSet = 0;
} else {
volumeToSet = volume - volumeWheelStep;
}
return volumeToSet;
return volumeToSet;
};
export const useRightControls = () => {
const { setVolume, setMuted } = usePlayerControls();
const volume = useVolume();
const muted = useMuted();
const { volumeWheelStep } = useGeneralSettings();
const { setVolume, setMuted } = usePlayerControls();
const volume = useVolume();
const muted = useMuted();
const { volumeWheelStep } = useGeneralSettings();
// Ensure that the mpv player volume is set on startup
useEffect(() => {
if (isElectron()) {
mpvPlayer.volume(volume);
mpris?.updateVolume(volume / 100);
// Ensure that the mpv player volume is set on startup
useEffect(() => {
if (isElectron()) {
mpvPlayer.volume(volume);
mpris?.updateVolume(volume / 100);
if (muted) {
mpvPlayer.mute();
}
}
if (muted) {
mpvPlayer.mute();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleVolumeSlider = (e: number) => {
mpvPlayer?.volume(e);
mpris?.updateVolume(e / 100);
setVolume(e);
};
const handleVolumeSliderState = (e: number) => {
mpris?.updateVolume(e / 100);
setVolume(e);
};
const handleVolumeDown = useCallback(() => {
const volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
mpvPlayer?.volume(volumeToSet);
mpris?.updateVolume(volumeToSet / 100);
setVolume(volumeToSet);
}, [setVolume, volume, volumeWheelStep]);
const handleVolumeUp = useCallback(() => {
const volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
mpvPlayer?.volume(volumeToSet);
mpris?.updateVolume(volumeToSet / 100);
setVolume(volumeToSet);
}, [setVolume, volume, volumeWheelStep]);
const handleVolumeWheel = useCallback(
(e: WheelEvent<HTMLDivElement | HTMLButtonElement>) => {
let volumeToSet;
if (e.deltaY > 0) {
volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
} else {
volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
}
mpvPlayer?.volume(volumeToSet);
mpris?.updateVolume(volumeToSet / 100);
setVolume(volumeToSet);
},
[setVolume, volume, volumeWheelStep],
);
const handleMute = useCallback(() => {
setMuted(!muted);
mpvPlayer?.mute();
}, [muted, setMuted]);
useEffect(() => {
if (isElectron()) {
mpvPlayerListener?.rendererVolumeMute(() => {
handleMute();
});
mpvPlayerListener?.rendererVolumeUp(() => {
handleVolumeUp();
});
mpvPlayerListener?.rendererVolumeDown(() => {
handleVolumeDown();
});
}
return () => {
ipc?.removeAllListeners('renderer-player-volume-mute');
ipc?.removeAllListeners('renderer-player-volume-up');
ipc?.removeAllListeners('renderer-player-volume-down');
const handleVolumeSlider = (e: number) => {
mpvPlayer?.volume(e);
mpris?.updateVolume(e / 100);
setVolume(e);
};
}, [handleMute, handleVolumeDown, handleVolumeUp]);
return {
handleMute,
handleVolumeDown,
handleVolumeSlider,
handleVolumeSliderState,
handleVolumeUp,
handleVolumeWheel,
};
const handleVolumeSliderState = (e: number) => {
mpris?.updateVolume(e / 100);
setVolume(e);
};
const handleVolumeDown = useCallback(() => {
const volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
mpvPlayer?.volume(volumeToSet);
mpris?.updateVolume(volumeToSet / 100);
setVolume(volumeToSet);
}, [setVolume, volume, volumeWheelStep]);
const handleVolumeUp = useCallback(() => {
const volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
mpvPlayer?.volume(volumeToSet);
mpris?.updateVolume(volumeToSet / 100);
setVolume(volumeToSet);
}, [setVolume, volume, volumeWheelStep]);
const handleVolumeWheel = useCallback(
(e: WheelEvent<HTMLDivElement | HTMLButtonElement>) => {
let volumeToSet;
if (e.deltaY > 0) {
volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
} else {
volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
}
mpvPlayer?.volume(volumeToSet);
mpris?.updateVolume(volumeToSet / 100);
setVolume(volumeToSet);
},
[setVolume, volume, volumeWheelStep],
);
const handleMute = useCallback(() => {
setMuted(!muted);
mpvPlayer?.mute();
}, [muted, setMuted]);
useEffect(() => {
if (isElectron()) {
mpvPlayerListener?.rendererVolumeMute(() => {
handleMute();
});
mpvPlayerListener?.rendererVolumeUp(() => {
handleVolumeUp();
});
mpvPlayerListener?.rendererVolumeDown(() => {
handleVolumeDown();
});
}
return () => {
ipc?.removeAllListeners('renderer-player-volume-mute');
ipc?.removeAllListeners('renderer-player-volume-up');
ipc?.removeAllListeners('renderer-player-volume-down');
};
}, [handleMute, handleVolumeDown, handleVolumeUp]);
return {
handleMute,
handleVolumeDown,
handleVolumeSlider,
handleVolumeSliderState,
handleVolumeUp,
handleVolumeWheel,
};
};

View file

@ -34,286 +34,294 @@ Progress Events (Jellyfin only):
*/
const checkScrobbleConditions = (args: {
scrobbleAtDuration: number;
scrobbleAtPercentage: number;
songCompletedDuration: number;
songDuration: number;
scrobbleAtDuration: number;
scrobbleAtPercentage: number;
songCompletedDuration: number;
songDuration: number;
}) => {
const { scrobbleAtDuration, scrobbleAtPercentage, songCompletedDuration, songDuration } = args;
const percentageOfSongCompleted = songDuration ? (songCompletedDuration / songDuration) * 100 : 0;
const { scrobbleAtDuration, scrobbleAtPercentage, songCompletedDuration, songDuration } = args;
const percentageOfSongCompleted = songDuration
? (songCompletedDuration / songDuration) * 100
: 0;
return (
percentageOfSongCompleted >= scrobbleAtPercentage || songCompletedDuration >= scrobbleAtDuration
);
return (
percentageOfSongCompleted >= scrobbleAtPercentage ||
songCompletedDuration >= scrobbleAtDuration
);
};
export const useScrobble = () => {
const status = useCurrentStatus();
const scrobbleSettings = usePlaybackSettings().scrobble;
const isScrobbleEnabled = scrobbleSettings?.enabled;
const sendScrobble = useSendScrobble();
const status = useCurrentStatus();
const scrobbleSettings = usePlaybackSettings().scrobble;
const isScrobbleEnabled = scrobbleSettings?.enabled;
const sendScrobble = useSendScrobble();
const [isCurrentSongScrobbled, setIsCurrentSongScrobbled] = useState(false);
const [isCurrentSongScrobbled, setIsCurrentSongScrobbled] = useState(false);
const handleScrobbleFromSeek = useCallback(
(currentTime: number) => {
if (!isScrobbleEnabled) return;
const handleScrobbleFromSeek = useCallback(
(currentTime: number) => {
if (!isScrobbleEnabled) return;
const currentSong = usePlayerStore.getState().current.song;
const currentSong = usePlayerStore.getState().current.song;
if (!currentSong?.id || currentSong?.serverType !== ServerType.JELLYFIN) return;
if (!currentSong?.id || currentSong?.serverType !== ServerType.JELLYFIN) return;
const position =
currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined;
const position =
currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined;
sendScrobble.mutate({
query: {
event: 'timeupdate',
id: currentSong.id,
position,
submission: false,
sendScrobble.mutate({
query: {
event: 'timeupdate',
id: currentSong.id,
position,
submission: false,
},
serverId: currentSong?.serverId,
});
},
serverId: currentSong?.serverId,
});
},
[isScrobbleEnabled, sendScrobble],
);
const progressIntervalId = useRef<ReturnType<typeof setInterval> | null>(null);
const songChangeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleScrobbleFromSongChange = useCallback(
(current: (QueueSong | number | undefined)[], previous: (QueueSong | number | undefined)[]) => {
if (!isScrobbleEnabled) return;
if (progressIntervalId.current) {
clearInterval(progressIntervalId.current);
}
// const currentSong = current[0] as QueueSong | undefined;
const previousSong = previous[0] as QueueSong;
const previousSongTime = previous[1] as number;
// Send completion scrobble when song changes and a previous song exists
if (previousSong?.id) {
const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: previousSongTime,
songDuration: previousSong.duration,
});
if (
(!isCurrentSongScrobbled && shouldSubmitScrobble) ||
previousSong?.serverType === ServerType.JELLYFIN
) {
const position =
previousSong?.serverType === ServerType.JELLYFIN ? previousSongTime * 1e7 : undefined;
sendScrobble.mutate({
query: {
id: previousSong.id,
position,
submission: true,
},
serverId: previousSong?.serverId,
});
}
}
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>);
songChangeTimeoutId.current = setTimeout(() => {
const currentSong = current[0] as QueueSong | undefined;
// Send start scrobble when song changes and the new song is playing
if (status === PlayerStatus.PLAYING && currentSong?.id) {
sendScrobble.mutate({
query: {
event: 'start',
id: currentSong.id,
position: 0,
submission: false,
},
serverId: currentSong?.serverId,
});
if (currentSong?.serverType === ServerType.JELLYFIN) {
progressIntervalId.current = setInterval(() => {
const currentTime = usePlayerStore.getState().current.time;
handleScrobbleFromSeek(currentTime);
}, 10000);
}
}
}, 2000);
},
[
isScrobbleEnabled,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
sendScrobble,
status,
handleScrobbleFromSeek,
],
);
const handleScrobbleFromStatusChange = useCallback(
(status: PlayerStatus | undefined) => {
if (!isScrobbleEnabled) return;
const currentSong = usePlayerStore.getState().current.song;
if (!currentSong?.id) return;
const position =
currentSong?.serverType === ServerType.JELLYFIN
? usePlayerStore.getState().current.time * 1e7
: undefined;
// Whenever the player is restarted, send a 'start' scrobble
if (status === PlayerStatus.PLAYING) {
sendScrobble.mutate({
query: {
event: 'unpause',
id: currentSong.id,
position,
submission: false,
},
serverId: currentSong?.serverId,
});
if (currentSong?.serverType === ServerType.JELLYFIN) {
progressIntervalId.current = setInterval(() => {
const currentTime = usePlayerStore.getState().current.time;
handleScrobbleFromSeek(currentTime);
}, 10000);
}
// Jellyfin is the only one that needs to send a 'pause' event to the server
} else if (currentSong?.serverType === ServerType.JELLYFIN) {
sendScrobble.mutate({
query: {
event: 'pause',
id: currentSong.id,
position,
submission: false,
},
serverId: currentSong?.serverId,
});
if (progressIntervalId.current) {
clearInterval(progressIntervalId.current as ReturnType<typeof setInterval>);
}
} else {
// If not already scrobbled, send a 'submission' scrobble if conditions are met
const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: usePlayerStore.getState().current.time,
songDuration: currentSong.duration,
});
if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
sendScrobble.mutate({
query: {
id: currentSong.id,
submission: true,
},
serverId: currentSong?.serverId,
});
setIsCurrentSongScrobbled(true);
}
}
},
[
isScrobbleEnabled,
sendScrobble,
handleScrobbleFromSeek,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
],
);
// When pressing the "Previous Track" button, the player will restart the current song if the
// currentTime is >= 10 seconds. Since the song / status change events are not triggered, we will
// need to perform another check to see if the scrobble conditions are met
const handleScrobbleFromSongRestart = useCallback(
(currentTime: number) => {
if (!isScrobbleEnabled) return;
const currentSong = usePlayerStore.getState().current.song;
if (!currentSong?.id) return;
const position =
currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined;
const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: currentTime,
songDuration: currentSong.duration,
});
if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
sendScrobble.mutate({
query: {
id: currentSong.id,
position,
submission: true,
},
serverId: currentSong?.serverId,
});
}
if (currentSong?.serverType === ServerType.JELLYFIN) {
sendScrobble.mutate({
query: {
event: 'start',
id: currentSong.id,
position: 0,
submission: false,
},
serverId: currentSong?.serverId,
});
}
setIsCurrentSongScrobbled(false);
},
[
isScrobbleEnabled,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
sendScrobble,
],
);
useEffect(() => {
const unsubSongChange = usePlayerStore.subscribe(
(state) => [state.current.song, state.current.time],
handleScrobbleFromSongChange,
{
// We need the current time to check the scrobble condition, but we only want to
// trigger the callback when the song changes
equalityFn: (a, b) => (a[0] as QueueSong)?.id === (b[0] as QueueSong)?.id,
},
[isScrobbleEnabled, sendScrobble],
);
const unsubStatusChange = usePlayerStore.subscribe(
(state) => state.current.status,
handleScrobbleFromStatusChange,
const progressIntervalId = useRef<ReturnType<typeof setInterval> | null>(null);
const songChangeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleScrobbleFromSongChange = useCallback(
(
current: (QueueSong | number | undefined)[],
previous: (QueueSong | number | undefined)[],
) => {
if (!isScrobbleEnabled) return;
if (progressIntervalId.current) {
clearInterval(progressIntervalId.current);
}
// const currentSong = current[0] as QueueSong | undefined;
const previousSong = previous[0] as QueueSong;
const previousSongTime = previous[1] as number;
// Send completion scrobble when song changes and a previous song exists
if (previousSong?.id) {
const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: previousSongTime,
songDuration: previousSong.duration,
});
if (
(!isCurrentSongScrobbled && shouldSubmitScrobble) ||
previousSong?.serverType === ServerType.JELLYFIN
) {
const position =
previousSong?.serverType === ServerType.JELLYFIN
? previousSongTime * 1e7
: undefined;
sendScrobble.mutate({
query: {
id: previousSong.id,
position,
submission: true,
},
serverId: previousSong?.serverId,
});
}
}
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>);
songChangeTimeoutId.current = setTimeout(() => {
const currentSong = current[0] as QueueSong | undefined;
// Send start scrobble when song changes and the new song is playing
if (status === PlayerStatus.PLAYING && currentSong?.id) {
sendScrobble.mutate({
query: {
event: 'start',
id: currentSong.id,
position: 0,
submission: false,
},
serverId: currentSong?.serverId,
});
if (currentSong?.serverType === ServerType.JELLYFIN) {
progressIntervalId.current = setInterval(() => {
const currentTime = usePlayerStore.getState().current.time;
handleScrobbleFromSeek(currentTime);
}, 10000);
}
}
}, 2000);
},
[
isScrobbleEnabled,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
sendScrobble,
status,
handleScrobbleFromSeek,
],
);
return () => {
unsubSongChange();
unsubStatusChange();
};
}, [handleScrobbleFromSongChange, handleScrobbleFromStatusChange]);
const handleScrobbleFromStatusChange = useCallback(
(status: PlayerStatus | undefined) => {
if (!isScrobbleEnabled) return;
return { handleScrobbleFromSeek, handleScrobbleFromSongRestart };
const currentSong = usePlayerStore.getState().current.song;
if (!currentSong?.id) return;
const position =
currentSong?.serverType === ServerType.JELLYFIN
? usePlayerStore.getState().current.time * 1e7
: undefined;
// Whenever the player is restarted, send a 'start' scrobble
if (status === PlayerStatus.PLAYING) {
sendScrobble.mutate({
query: {
event: 'unpause',
id: currentSong.id,
position,
submission: false,
},
serverId: currentSong?.serverId,
});
if (currentSong?.serverType === ServerType.JELLYFIN) {
progressIntervalId.current = setInterval(() => {
const currentTime = usePlayerStore.getState().current.time;
handleScrobbleFromSeek(currentTime);
}, 10000);
}
// Jellyfin is the only one that needs to send a 'pause' event to the server
} else if (currentSong?.serverType === ServerType.JELLYFIN) {
sendScrobble.mutate({
query: {
event: 'pause',
id: currentSong.id,
position,
submission: false,
},
serverId: currentSong?.serverId,
});
if (progressIntervalId.current) {
clearInterval(progressIntervalId.current as ReturnType<typeof setInterval>);
}
} else {
// If not already scrobbled, send a 'submission' scrobble if conditions are met
const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: usePlayerStore.getState().current.time,
songDuration: currentSong.duration,
});
if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
sendScrobble.mutate({
query: {
id: currentSong.id,
submission: true,
},
serverId: currentSong?.serverId,
});
setIsCurrentSongScrobbled(true);
}
}
},
[
isScrobbleEnabled,
sendScrobble,
handleScrobbleFromSeek,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
],
);
// When pressing the "Previous Track" button, the player will restart the current song if the
// currentTime is >= 10 seconds. Since the song / status change events are not triggered, we will
// need to perform another check to see if the scrobble conditions are met
const handleScrobbleFromSongRestart = useCallback(
(currentTime: number) => {
if (!isScrobbleEnabled) return;
const currentSong = usePlayerStore.getState().current.song;
if (!currentSong?.id) return;
const position =
currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined;
const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDuration: scrobbleSettings?.scrobbleAtDuration,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDuration: currentTime,
songDuration: currentSong.duration,
});
if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
sendScrobble.mutate({
query: {
id: currentSong.id,
position,
submission: true,
},
serverId: currentSong?.serverId,
});
}
if (currentSong?.serverType === ServerType.JELLYFIN) {
sendScrobble.mutate({
query: {
event: 'start',
id: currentSong.id,
position: 0,
submission: false,
},
serverId: currentSong?.serverId,
});
}
setIsCurrentSongScrobbled(false);
},
[
isScrobbleEnabled,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
sendScrobble,
],
);
useEffect(() => {
const unsubSongChange = usePlayerStore.subscribe(
(state) => [state.current.song, state.current.time],
handleScrobbleFromSongChange,
{
// We need the current time to check the scrobble condition, but we only want to
// trigger the callback when the song changes
equalityFn: (a, b) => (a[0] as QueueSong)?.id === (b[0] as QueueSong)?.id,
},
);
const unsubStatusChange = usePlayerStore.subscribe(
(state) => state.current.status,
handleScrobbleFromStatusChange,
);
return () => {
unsubSongChange();
unsubStatusChange();
};
}, [handleScrobbleFromSongChange, handleScrobbleFromStatusChange]);
return { handleScrobbleFromSeek, handleScrobbleFromSongRestart };
};