mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
enable notify, simplify use-scrobble with types, remove unused check
This commit is contained in:
parent
b219c900ca
commit
e00aeb2b67
4 changed files with 76 additions and 12 deletions
|
|
@ -171,6 +171,7 @@
|
||||||
"loginRateError": "too many login attempts, please try again in a few seconds",
|
"loginRateError": "too many login attempts, please try again in a few seconds",
|
||||||
"mpvRequired": "MPV required",
|
"mpvRequired": "MPV required",
|
||||||
"networkError": "a network error occurred",
|
"networkError": "a network error occurred",
|
||||||
|
"notificationDenied": "permissions for notifications were denied. this setting has no effect",
|
||||||
"openError": "could not open file",
|
"openError": "could not open file",
|
||||||
"playbackError": "an error occurred when trying to play the media",
|
"playbackError": "an error occurred when trying to play the media",
|
||||||
"remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server",
|
"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",
|
"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": "lyric offset (ms)",
|
||||||
"lyricOffset_description": "offset the lyric by the specified amount of milliseconds",
|
"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": "minimize to tray",
|
||||||
"minimizeToTray_description": "minimize the application to the system tray",
|
"minimizeToTray_description": "minimize the application to the system tray",
|
||||||
"minimumScrobblePercentage": "minimum scrobble duration (percentage)",
|
"minimumScrobblePercentage": "minimum scrobble duration (percentage)",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ Progress Events (Jellyfin only):
|
||||||
- Sends the 'progress' scrobble event on an interval
|
- Sends the 'progress' scrobble event on an interval
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type SongEvent = [QueueSong | undefined, number, 1 | 2];
|
||||||
|
|
||||||
const checkScrobbleConditions = (args: {
|
const checkScrobbleConditions = (args: {
|
||||||
scrobbleAtDurationMs: number;
|
scrobbleAtDurationMs: number;
|
||||||
scrobbleAtPercentage: number;
|
scrobbleAtPercentage: number;
|
||||||
|
|
@ -86,10 +88,21 @@ export const useScrobble = () => {
|
||||||
const progressIntervalId = useRef<null | ReturnType<typeof setInterval>>(null);
|
const progressIntervalId = useRef<null | ReturnType<typeof setInterval>>(null);
|
||||||
const songChangeTimeoutId = useRef<null | ReturnType<typeof setTimeout>>(null);
|
const songChangeTimeoutId = useRef<null | ReturnType<typeof setTimeout>>(null);
|
||||||
const handleScrobbleFromSongChange = useCallback(
|
const handleScrobbleFromSongChange = useCallback(
|
||||||
(
|
(current: SongEvent, previous: SongEvent) => {
|
||||||
current: (number | QueueSong | undefined)[],
|
if (scrobbleSettings?.notify && current[0]) {
|
||||||
previous: (number | QueueSong | undefined)[],
|
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 (!isScrobbleEnabled) return;
|
||||||
|
|
||||||
if (progressIntervalId.current) {
|
if (progressIntervalId.current) {
|
||||||
|
|
@ -98,8 +111,8 @@ export const useScrobble = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// const currentSong = current[0] as QueueSong | undefined;
|
// const currentSong = current[0] as QueueSong | undefined;
|
||||||
const previousSong = previous[0] as QueueSong;
|
const previousSong = previous[0];
|
||||||
const previousSongTimeSec = previous[1] as number;
|
const previousSongTimeSec = previous[1];
|
||||||
|
|
||||||
// Send completion scrobble when song changes and a previous song exists
|
// Send completion scrobble when song changes and a previous song exists
|
||||||
if (previousSong?.id) {
|
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
|
// 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 as ReturnType<typeof setTimeout>);
|
||||||
songChangeTimeoutId.current = 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
|
// Get the current status from the state, not variable. This is because
|
||||||
// of a timing issue where, when playing the first track, the first
|
// of a timing issue where, when playing the first track, the first
|
||||||
// event is song, and then the event is play
|
// event is song, and then the event is play
|
||||||
|
|
@ -169,9 +182,10 @@ export const useScrobble = () => {
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
isScrobbleEnabled,
|
scrobbleSettings?.notify,
|
||||||
scrobbleSettings?.scrobbleAtDuration,
|
scrobbleSettings?.scrobbleAtDuration,
|
||||||
scrobbleSettings?.scrobbleAtPercentage,
|
scrobbleSettings?.scrobbleAtPercentage,
|
||||||
|
isScrobbleEnabled,
|
||||||
isCurrentSongScrobbled,
|
isCurrentSongScrobbled,
|
||||||
sendScrobble,
|
sendScrobble,
|
||||||
handleScrobbleFromSeek,
|
handleScrobbleFromSeek,
|
||||||
|
|
@ -332,7 +346,7 @@ export const useScrobble = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubSongChange = usePlayerStore.subscribe(
|
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,
|
handleScrobbleFromSongChange,
|
||||||
{
|
{
|
||||||
// We need the current time to check the scrobble condition, but we only want to
|
// 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) =>
|
equalityFn: (a, b) =>
|
||||||
// compute whether the song changed
|
// compute whether the song changed
|
||||||
(a[0] as QueueSong)?.uniqueId === (b[0] as QueueSong)?.uniqueId &&
|
(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)
|
// compute whether the same player: relevant for repeat one and repeat all (one track)
|
||||||
a[3] === b[3],
|
a[2] === b[2],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { Slider } from '/@/shared/components/slider/slider';
|
import { Slider } from '/@/shared/components/slider/slider';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
|
|
||||||
export const ScrobbleSettings = () => {
|
export const ScrobbleSettings = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -95,6 +96,52 @@ export const ScrobbleSettings = () => {
|
||||||
}),
|
}),
|
||||||
title: t('setting.minimumScrobbleSeconds', { postProcess: 'sentenceCase' }),
|
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} />;
|
return <SettingsSection options={scrobbleOptions} />;
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,7 @@ export interface SettingsState {
|
||||||
preservePitch: boolean;
|
preservePitch: boolean;
|
||||||
scrobble: {
|
scrobble: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
notify: boolean;
|
||||||
scrobbleAtDuration: number;
|
scrobbleAtDuration: number;
|
||||||
scrobbleAtPercentage: number;
|
scrobbleAtPercentage: number;
|
||||||
};
|
};
|
||||||
|
|
@ -479,6 +480,7 @@ const initialState: SettingsState = {
|
||||||
preservePitch: true,
|
preservePitch: true,
|
||||||
scrobble: {
|
scrobble: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
notify: false,
|
||||||
scrobbleAtDuration: 240,
|
scrobbleAtDuration: 240,
|
||||||
scrobbleAtPercentage: 75,
|
scrobbleAtPercentage: 75,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue