enable notify, simplify use-scrobble with types, remove unused check

This commit is contained in:
Kendall Garner 2025-07-07 19:25:25 -07:00
parent b219c900ca
commit e00aeb2b67
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
4 changed files with 76 additions and 12 deletions

View file

@ -171,6 +171,7 @@
"loginRateError": "too many login attempts, please try again in a few seconds",
"mpvRequired": "MPV required",
"networkError": "a network error occurred",
"notificationDenied": "permissions for notifications were denied. this setting has no effect",
"openError": "could not open file",
"playbackError": "an error occurred when trying to play the media",
"remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server",
@ -605,6 +606,8 @@
"lyricFetchProvider_description": "select the providers to fetch lyrics from. the order of the providers is the order in which they will be queried",
"lyricOffset": "lyric offset (ms)",
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
"notify": "enable song notifications",
"notify_description": "show notifications when changing the current song",
"minimizeToTray": "minimize to tray",
"minimizeToTray_description": "minimize the application to the system tray",
"minimumScrobblePercentage": "minimum scrobble duration (percentage)",

View file

@ -34,6 +34,8 @@ Progress Events (Jellyfin only):
- Sends the 'progress' scrobble event on an interval
*/
type SongEvent = [QueueSong | undefined, number, 1 | 2];
const checkScrobbleConditions = (args: {
scrobbleAtDurationMs: number;
scrobbleAtPercentage: number;
@ -86,10 +88,21 @@ export const useScrobble = () => {
const progressIntervalId = useRef<null | ReturnType<typeof setInterval>>(null);
const songChangeTimeoutId = useRef<null | ReturnType<typeof setTimeout>>(null);
const handleScrobbleFromSongChange = useCallback(
(
current: (number | QueueSong | undefined)[],
previous: (number | QueueSong | undefined)[],
) => {
(current: SongEvent, previous: SongEvent) => {
if (scrobbleSettings?.notify && current[0]) {
const currentSong = current[0];
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,
});
}
if (!isScrobbleEnabled) return;
if (progressIntervalId.current) {
@ -98,8 +111,8 @@ export const useScrobble = () => {
}
// const currentSong = current[0] as QueueSong | undefined;
const previousSong = previous[0] as QueueSong;
const previousSongTimeSec = previous[1] as number;
const previousSong = previous[0];
const previousSongTimeSec = previous[1];
// Send completion scrobble when song changes and a previous song exists
if (previousSong?.id) {
@ -135,7 +148,7 @@ export const useScrobble = () => {
// 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;
const currentSong = current[0];
// Get the current status from the state, not variable. This is because
// of a timing issue where, when playing the first track, the first
// event is song, and then the event is play
@ -169,9 +182,10 @@ export const useScrobble = () => {
}, 2000);
},
[
isScrobbleEnabled,
scrobbleSettings?.notify,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isScrobbleEnabled,
isCurrentSongScrobbled,
sendScrobble,
handleScrobbleFromSeek,
@ -332,7 +346,7 @@ export const useScrobble = () => {
useEffect(() => {
const unsubSongChange = usePlayerStore.subscribe(
(state) => [state.current.song, state.current.time, state.current.player],
(state): SongEvent => [state.current.song, state.current.time, state.current.player],
handleScrobbleFromSongChange,
{
// We need the current time to check the scrobble condition, but we only want to
@ -345,10 +359,8 @@ export const useScrobble = () => {
equalityFn: (a, b) =>
// compute whether the song changed
(a[0] as QueueSong)?.uniqueId === (b[0] as QueueSong)?.uniqueId &&
// compute whether the position changed. This should imply 1
a[2] === b[2] &&
// compute whether the same player: relevant for repeat one and repeat all (one track)
a[3] === b[3],
a[2] === b[2],
},
);

View file

@ -8,6 +8,7 @@ import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { toast } from '/@/shared/components/toast/toast';
export const ScrobbleSettings = () => {
const { t } = useTranslation();
@ -95,6 +96,52 @@ export const ScrobbleSettings = () => {
}),
title: t('setting.minimumScrobbleSeconds', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Toggle notify"
defaultChecked={settings.scrobble.notify}
onChange={async (e) => {
if (Notification.permission === 'denied') {
toast.error({
message: t('error.notificationDenied', {
postProcess: 'sentenceCase',
}),
});
return;
}
if (Notification.permission !== 'granted') {
const permissions = await Notification.requestPermission();
if (permissions !== 'granted') {
toast.error({
message: t('error.notificationDenied', {
postProcess: 'sentenceCase',
}),
});
return;
}
}
setSettings({
playback: {
...settings,
scrobble: {
...settings.scrobble,
notify: e.currentTarget.checked,
},
},
});
}}
/>
),
description: t('setting.notify', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !('Notification' in window),
title: t('setting.notify', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection options={scrobbleOptions} />;

View file

@ -285,6 +285,7 @@ export interface SettingsState {
preservePitch: boolean;
scrobble: {
enabled: boolean;
notify: boolean;
scrobbleAtDuration: number;
scrobbleAtPercentage: number;
};
@ -479,6 +480,7 @@ const initialState: SettingsState = {
preservePitch: true,
scrobble: {
enabled: true,
notify: false,
scrobbleAtDuration: 240,
scrobbleAtPercentage: 75,
},