diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index e5308e5..e4d5137 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -44,6 +44,7 @@ const playQueueState: PlayQueue = { playerUpdated: 0, autoIncremented: false, volume: 0.5, + scrobble: false, isLoading: false, repeat: 'all', shuffle: false, diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx index 951faf4..cca0f12 100644 --- a/src/components/player/Player.tsx +++ b/src/components/player/Player.tsx @@ -23,7 +23,7 @@ import { setFadeData, setPlayerSrc, } from '../../redux/playQueueSlice'; -import { setCurrentSeek, setScrobbled } from '../../redux/playerSlice'; +import { setCurrentSeek } from '../../redux/playerSlice'; import cacheSong from '../shared/cacheSong'; import { getSongCachePath, isCached } from '../../shared/utils'; import { scrobble } from '../../api/api'; @@ -35,7 +35,9 @@ const gaplessListenHandler = ( currentPlayer: number, dispatch: any, pollingInterval: number, - scrobbled: boolean + shouldScrobble: boolean, + scrobbled: boolean, + setScrobbled: any ) => { const currentSeek = currentPlayerRef.current?.audioEl.current?.currentTime || 0; const duration = currentPlayerRef.current?.audioEl.current?.duration; @@ -55,8 +57,19 @@ const gaplessListenHandler = ( nextPlayerRef.current.audioEl.current.play(); } - if ((currentSeek >= 240 || currentSeek >= duration - 5) && !scrobbled) { - dispatch(setScrobbled(true)); + // Conditions for scrobbling gapless track + // 1. Scrobble enabled in settings + // 2. Not already scrobbled + // 3. Track reached past 4 minutes or past the 90% mark + // 4. Not in the last 2 seconds of the track (gapless player starts second track before first ends) + // Step 4 sets the scrobbled value to false again which would trigger a second scrobble + if ( + shouldScrobble && + !scrobbled && + (currentSeek >= 240 || currentSeek >= duration * 0.9) && + currentSeek <= duration - 2 + ) { + setScrobbled(true); scrobble({ id: playQueue.currentSongId, submission: true }); } }; @@ -72,7 +85,9 @@ const listenHandler = ( fadeType: string, volumeFade: boolean, debug: boolean, - scrobbled: boolean + shouldScrobble: boolean, + scrobbled: boolean, + setScrobbled: any ) => { const currentSeek = currentPlayerRef.current?.audioEl.current?.currentTime || 0; const duration = currentPlayerRef.current?.audioEl.current?.duration; @@ -203,12 +218,18 @@ const listenHandler = ( dispatch(setCurrentSeek({ seek: currentSeek })); } + // Conditions for scrobbling fading track + // 1. Scrobble enabled in settings + // 2. Not already scrobbled + // 3. Track reached past 4 minutes or past the fadeAtTime - 15 seconds + // 4. The track is not fading if ( - (currentSeek >= 240 || currentSeek >= duration - fadeAtTime + 1) && + shouldScrobble && !scrobbled && + (currentSeek >= 240 || currentSeek >= fadeAtTime - 15) && currentSeek <= fadeAtTime ) { - dispatch(setScrobbled(true)); + setScrobbled(true); scrobble({ id: playQueue.currentSongId, submission: true }); } }; @@ -227,6 +248,7 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { const [fadeType, setFadeType] = useState(playQueue.fadeType); const [volumeFade, setVolumeFade] = useState(playQueue.volumeFade); const [pollingInterval, setPollingInterval] = useState(playQueue.pollingInterval); + const [scrobbled, setScrobbled] = useState(false); const getSrc1 = useCallback(() => { const cachedSongPath = `${cachePath}/${ @@ -347,18 +369,11 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { fadeType, volumeFade, debug, - player.scrobbled + playQueue.scrobble, + scrobbled, + setScrobbled ); - }, [ - currentEntryList, - debug, - dispatch, - fadeDuration, - fadeType, - playQueue, - player.scrobbled, - volumeFade, - ]); + }, [currentEntryList, debug, dispatch, fadeDuration, fadeType, playQueue, scrobbled, volumeFade]); const handleListenPlayer2 = useCallback(() => { listenHandler( @@ -372,18 +387,11 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { fadeType, volumeFade, debug, - player.scrobbled + playQueue.scrobble, + scrobbled, + setScrobbled ); - }, [ - currentEntryList, - debug, - dispatch, - fadeDuration, - fadeType, - playQueue, - player.scrobbled, - volumeFade, - ]); + }, [currentEntryList, debug, dispatch, fadeDuration, fadeType, playQueue, scrobbled, volumeFade]); const handleOnEndedPlayer1 = () => { player1Ref.current.audioEl.current.currentTime = 0; @@ -452,7 +460,7 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { } }; - const handleGaplessPlayer1 = () => { + const handleGaplessPlayer1 = useCallback(() => { gaplessListenHandler( player1Ref, player2Ref, @@ -460,11 +468,13 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { 1, dispatch, pollingInterval, - player.scrobbled + playQueue.scrobble, + scrobbled, + setScrobbled ); - }; + }, [dispatch, playQueue, scrobbled, pollingInterval]); - const handleGaplessPlayer2 = () => { + const handleGaplessPlayer2 = useCallback(() => { gaplessListenHandler( player2Ref, player1Ref, @@ -472,13 +482,27 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { 2, dispatch, pollingInterval, - player.scrobbled + playQueue.scrobble, + scrobbled, + setScrobbled ); - }; + }, [dispatch, playQueue, scrobbled, pollingInterval]); - const handleOnPlay = () => { - dispatch(setScrobbled(false)); - scrobble({ id: playQueue.currentSongId, submission: false }); + const handleOnPlay = (playerNumber: 1 | 2) => { + setScrobbled(false); + if (playQueue.scrobble) { + if (playerNumber === 1) { + scrobble({ + id: playQueue[currentEntryList][playQueue.player1.index].id, + submission: false, + }); + } else { + scrobble({ + id: playQueue[currentEntryList][playQueue.player2.index].id, + submission: false, + }); + } + } }; return ( @@ -490,7 +514,7 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { handleOnPlay(1)} listenInterval={pollingInterval} preload="auto" onListen={fadeDuration === 0 ? handleGaplessPlayer1 : handleListenPlayer1} @@ -512,7 +536,7 @@ const Player = ({ currentEntryList, children }: any, ref: any) => { handleOnPlay(2)} listenInterval={pollingInterval} preload="auto" onListen={fadeDuration === 0 ? handleGaplessPlayer2 : handleListenPlayer2} diff --git a/src/components/settings/ConfigPanels/PlayerConfig.tsx b/src/components/settings/ConfigPanels/PlayerConfig.tsx index 49e8016..0566c59 100644 --- a/src/components/settings/ConfigPanels/PlayerConfig.tsx +++ b/src/components/settings/ConfigPanels/PlayerConfig.tsx @@ -3,11 +3,15 @@ import settings from 'electron-settings'; import { ControlLabel } from 'rsuite'; import { ConfigPanel } from '../styled'; import { StyledCheckbox, StyledInputNumber } from '../../shared/styled'; +import { useAppDispatch } from '../../../redux/hooks'; +import { setPlaybackSetting } from '../../../redux/playQueueSlice'; const PlayerConfig = () => { + const dispatch = useAppDispatch(); const [globalMediaHotkeys, setGlobalMediaHotkeys] = useState( Boolean(settings.getSync('globalMediaHotkeys')) ); + const [scrobble, setScrobble] = useState(Boolean(settings.getSync('scrobble'))); return (

@@ -48,6 +52,16 @@ const PlayerConfig = () => { > Enable global media hotkeys (requires app restart) + { + settings.setSync('scrobble', !scrobble); + dispatch(setPlaybackSetting({ setting: 'scrobble', value: !scrobble })); + setScrobble(!scrobble); + }} + > + Enable scrobbling + ); }; diff --git a/src/components/shared/setDefaultSettings.ts b/src/components/shared/setDefaultSettings.ts index ff00779..9749b9d 100644 --- a/src/components/shared/setDefaultSettings.ts +++ b/src/components/shared/setDefaultSettings.ts @@ -26,6 +26,10 @@ const setDefaultSettings = (force: boolean) => { settings.setSync('cachePath', path.join(path.dirname(settings.file()))); } + if (force || !settings.hasSync('scrobble')) { + settings.setSync('scrobble', false); + } + if (force || !settings.hasSync('volume')) { settings.setSync('volume', 0.3); } diff --git a/src/redux/playQueueSlice.ts b/src/redux/playQueueSlice.ts index 3357567..2ac5d24 100644 --- a/src/redux/playQueueSlice.ts +++ b/src/redux/playQueueSlice.ts @@ -75,6 +75,7 @@ export interface PlayQueue { playerUpdated: number; autoIncremented: boolean; volume: number; + scrobble: boolean; isLoading: boolean; repeat: string; shuffle: boolean; @@ -119,6 +120,7 @@ const initialState: PlayQueue = { playerUpdated: 0, autoIncremented: false, volume: Number(parsedSettings.volume), + scrobble: Boolean(parsedSettings.scrobble), isLoading: Boolean(false), repeat: String(parsedSettings.repeat), shuffle: Boolean(parsedSettings.shuffle), @@ -302,6 +304,9 @@ const playQueueSlice = createSlice({ case 'scrollWithCurrentSong': state.scrollWithCurrentSong = action.payload.value; break; + case 'scrobble': + state.scrobble = action.payload.value; + break; default: break; } diff --git a/src/shared/mockSettings.ts b/src/shared/mockSettings.ts index d67c2d3..0a8e409 100644 --- a/src/shared/mockSettings.ts +++ b/src/shared/mockSettings.ts @@ -15,6 +15,7 @@ export const mockSettings = { pollingInterval: 20, fadeDuration: 9, fadeType: 'equalPower', + scrobble: false, gridCardSize: 200, playlistViewType: 'grid', albumViewType: 'grid',