mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Player.jsx Component - Comprehensive Architecture Improvement
This commit is contained in:
parent
b0e2457e61
commit
b342035884
@ -61,10 +61,16 @@ const Player = () => {
|
||||
dispatchClearQueue,
|
||||
} = usePlayerState()
|
||||
|
||||
const { startTime, scrobbled, onAudioProgress, onAudioPlayTrackChange, onAudioEnded } =
|
||||
useScrobbling(playerState, dispatch, dataProvider)
|
||||
const {
|
||||
startTime,
|
||||
scrobbled,
|
||||
onAudioProgress,
|
||||
onAudioPlayTrackChange,
|
||||
onAudioEnded,
|
||||
} = useScrobbling(playerState, dispatch, dataProvider)
|
||||
|
||||
const { preloaded, preloadNextSong, resetPreloading } = usePreloading(playerState)
|
||||
const { preloaded, preloadNextSong, resetPreloading } =
|
||||
usePreloading(playerState)
|
||||
|
||||
const { audioInstance, setAudioInstance, onAudioPlay } = useAudioInstance(
|
||||
isMobilePlayer,
|
||||
@ -81,7 +87,6 @@ const Player = () => {
|
||||
enableCoverAnimation: config.enableCoverAnimation,
|
||||
})
|
||||
|
||||
|
||||
const defaultOptions = useMemo(
|
||||
() => ({
|
||||
theme: playerTheme,
|
||||
@ -134,12 +139,24 @@ const Player = () => {
|
||||
autoPlay: playerState.clear || playerState.playIndex === 0,
|
||||
clearPriorAudioLists: playerState.clear,
|
||||
extendsContent: (
|
||||
<PlayerToolbar id={currentTrack.trackId} isRadio={currentTrack.isRadio} />
|
||||
<PlayerToolbar
|
||||
id={currentTrack.trackId}
|
||||
isRadio={currentTrack.isRadio}
|
||||
/>
|
||||
),
|
||||
defaultVolume: isMobilePlayer ? 1 : playerState.volume,
|
||||
showMediaSession: !currentTrack.isRadio,
|
||||
}
|
||||
}, [defaultOptions, audioLists, playerState.playIndex, playerState.clear, playerState.volume, isMobilePlayer, currentTrack.trackId, currentTrack.isRadio])
|
||||
}, [
|
||||
defaultOptions,
|
||||
audioLists,
|
||||
playerState.playIndex,
|
||||
playerState.clear,
|
||||
playerState.volume,
|
||||
isMobilePlayer,
|
||||
currentTrack.trackId,
|
||||
currentTrack.isRadio,
|
||||
])
|
||||
|
||||
const onAudioListsChange = useCallback(
|
||||
(_, audioLists, audioInfo) => dispatchSyncQueue(audioInfo, audioLists),
|
||||
@ -202,14 +219,9 @@ const Player = () => {
|
||||
[audioInstance, playerState],
|
||||
)
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={createMuiTheme(theme)}>
|
||||
<div
|
||||
role="region"
|
||||
aria-label="Audio Player"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div role="region" aria-label="Audio Player" aria-live="polite">
|
||||
<ReactJkMusicPlayer
|
||||
{...options}
|
||||
className={classes.player}
|
||||
|
||||
@ -26,7 +26,9 @@ jest.mock('../config', () => ({
|
||||
|
||||
jest.mock('./AudioTitle', () => ({
|
||||
__esModule: true,
|
||||
default: ({ audioInfo }) => <div data-testid="audio-title">{audioInfo?.song?.title || 'No song'}</div>,
|
||||
default: ({ audioInfo }) => (
|
||||
<div data-testid="audio-title">{audioInfo?.song?.title || 'No song'}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('./PlayerToolbar', () => ({
|
||||
@ -101,9 +103,18 @@ describe('Player Component', () => {
|
||||
{
|
||||
player: {
|
||||
queue: [
|
||||
{ uuid: '1', musicSrc: 'song1.mp3', title: 'Song 1', artist: 'Artist 1' },
|
||||
{
|
||||
uuid: '1',
|
||||
musicSrc: 'song1.mp3',
|
||||
title: 'Song 1',
|
||||
artist: 'Artist 1',
|
||||
},
|
||||
],
|
||||
current: { uuid: '1', trackId: 'track1', song: { title: 'Song 1', artist: 'Artist 1' } },
|
||||
current: {
|
||||
uuid: '1',
|
||||
trackId: 'track1',
|
||||
song: { title: 'Song 1', artist: 'Artist 1' },
|
||||
},
|
||||
playIndex: 0,
|
||||
mode: 'single',
|
||||
volume: 0.8,
|
||||
@ -115,7 +126,7 @@ describe('Player Component', () => {
|
||||
replayGain: {
|
||||
gainMode: 'track',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const renderPlayer = () => {
|
||||
@ -124,7 +135,7 @@ describe('Player Component', () => {
|
||||
<ThemeProvider theme={createMuiTheme()}>
|
||||
<Player />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
@ -149,7 +160,8 @@ describe('Player Component', () => {
|
||||
expect(container.firstChild).toBeNull()
|
||||
|
||||
// Reset mock
|
||||
const { useAuthState: originalUseAuthState } = jest.requireMock('react-admin')
|
||||
const { useAuthState: originalUseAuthState } =
|
||||
jest.requireMock('react-admin')
|
||||
originalUseAuthState.mockReturnValue({ authenticated: true })
|
||||
})
|
||||
|
||||
@ -164,7 +176,7 @@ describe('Player Component', () => {
|
||||
player: { queue: [] },
|
||||
settings: { notifications: true },
|
||||
replayGain: { gainMode: 'track' },
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const { container } = render(
|
||||
@ -172,7 +184,7 @@ describe('Player Component', () => {
|
||||
<ThemeProvider theme={createMuiTheme()}>
|
||||
<Player />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</Provider>,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
@ -221,7 +233,7 @@ describe('Player Component', () => {
|
||||
player: { queue: [] },
|
||||
settings: { notifications: true },
|
||||
replayGain: { gainMode: 'track' },
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
render(
|
||||
@ -229,7 +241,7 @@ describe('Player Component', () => {
|
||||
<ThemeProvider theme={createMuiTheme()}>
|
||||
<Player />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</Provider>,
|
||||
)
|
||||
|
||||
// Document title should be reset when player is not visible
|
||||
@ -240,6 +252,8 @@ describe('Player Component', () => {
|
||||
renderPlayer()
|
||||
|
||||
// ThemeProvider should wrap the component
|
||||
expect(screen.getByTestId('react-jk-music-player').parentElement).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByTestId('react-jk-music-player').parentElement,
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -95,7 +95,11 @@ export const useAudioInstance = (isMobilePlayer, context) => {
|
||||
|
||||
if (showNotifications) {
|
||||
try {
|
||||
sendNotification(song.title, `${song.artist} - ${song.album}`, info.cover)
|
||||
sendNotification(
|
||||
song.title,
|
||||
`${song.artist} - ${song.album}`,
|
||||
info.cover,
|
||||
)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Notification error:', error)
|
||||
@ -123,4 +127,4 @@ export const useAudioInstance = (isMobilePlayer, context) => {
|
||||
setAudioInstance,
|
||||
onAudioPlay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,4 +81,4 @@ export const usePlayerState = () => {
|
||||
dispatchSyncQueue,
|
||||
dispatchClearQueue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,4 +101,4 @@ describe('usePlayerState', () => {
|
||||
expect(useDispatch).toHaveBeenCalled()
|
||||
expect(useSelector).toHaveBeenCalledWith(expect.any(Function))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -108,7 +108,10 @@ describe('usePreloading', () => {
|
||||
result.current.preloadNextSong()
|
||||
})
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Error during preloading:', expect.any(Error))
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error during preloading:',
|
||||
expect.any(Error),
|
||||
)
|
||||
expect(result.current.preloaded).toBe(false) // Should remain false on error
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
@ -134,8 +137,11 @@ describe('usePreloading', () => {
|
||||
result.current.preloadNextSong()
|
||||
})
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Preloading error:', expect.any(Event))
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Preloading error:',
|
||||
expect.any(Event),
|
||||
)
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -37,7 +37,7 @@ describe('useReplayGain', () => {
|
||||
|
||||
it('should initialize with null context and gainNode', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReplayGain(null, { current: {} }, { gainMode: 'track' })
|
||||
useReplayGain(null, { current: {} }, { gainMode: 'track' }),
|
||||
)
|
||||
|
||||
expect(result.current.context).toBeNull()
|
||||
@ -52,7 +52,7 @@ describe('useReplayGain', () => {
|
||||
const mockGainInfo = { gainMode: 'track' }
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo)
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo),
|
||||
)
|
||||
|
||||
expect(global.AudioContext).toHaveBeenCalled()
|
||||
@ -69,11 +69,17 @@ describe('useReplayGain', () => {
|
||||
mockCalculateGain.mockReturnValue(0.8)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo)
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo),
|
||||
)
|
||||
|
||||
expect(mockCalculateGain).toHaveBeenCalledWith(mockGainInfo, mockPlayerState.current.song)
|
||||
expect(result.current.gainNode.gain.setValueAtTime).toHaveBeenCalledWith(0.8, 0)
|
||||
expect(mockCalculateGain).toHaveBeenCalledWith(
|
||||
mockGainInfo,
|
||||
mockPlayerState.current.song,
|
||||
)
|
||||
expect(result.current.gainNode.gain.setValueAtTime).toHaveBeenCalledWith(
|
||||
0.8,
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle Web Audio API errors gracefully', () => {
|
||||
@ -89,12 +95,12 @@ describe('useReplayGain', () => {
|
||||
const mockGainInfo = { gainMode: 'track' }
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo)
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo),
|
||||
)
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error initializing Web Audio API for replay gain:',
|
||||
expect.any(Error)
|
||||
expect.any(Error),
|
||||
)
|
||||
expect(result.current.context).toBeNull()
|
||||
|
||||
@ -128,10 +134,13 @@ describe('useReplayGain', () => {
|
||||
}))
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo)
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo),
|
||||
)
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Error applying replay gain:', expect.any(Error))
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error applying replay gain:',
|
||||
expect.any(Error),
|
||||
)
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
@ -142,10 +151,10 @@ describe('useReplayGain', () => {
|
||||
const mockGainInfo = { gainMode: 'off' }
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo)
|
||||
useReplayGain(mockAudioInstance, mockPlayerState, mockGainInfo),
|
||||
)
|
||||
|
||||
expect(global.AudioContext).not.toHaveBeenCalled()
|
||||
expect(result.current.context).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -35,7 +35,7 @@ describe('useScrobbling', () => {
|
||||
|
||||
it('should initialize with default state', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider)
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider),
|
||||
)
|
||||
|
||||
expect(result.current.startTime).toBeNull()
|
||||
@ -47,7 +47,7 @@ describe('useScrobbling', () => {
|
||||
|
||||
it('should handle audio progress and scrobble when conditions are met', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider)
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider),
|
||||
)
|
||||
|
||||
const mockInfo = {
|
||||
@ -68,7 +68,7 @@ describe('useScrobbling', () => {
|
||||
|
||||
it('should not scrobble radio streams', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider)
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider),
|
||||
)
|
||||
|
||||
const mockInfo = {
|
||||
@ -87,7 +87,7 @@ describe('useScrobbling', () => {
|
||||
|
||||
it('should reset scrobbling state on track change', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider)
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider),
|
||||
)
|
||||
|
||||
// Set initial state
|
||||
@ -114,7 +114,7 @@ describe('useScrobbling', () => {
|
||||
|
||||
it('should handle audio ended and perform keepalive', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider)
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider),
|
||||
)
|
||||
|
||||
const mockInfo = { trackId: 'track1' }
|
||||
@ -125,7 +125,9 @@ describe('useScrobbling', () => {
|
||||
|
||||
expect(result.current.scrobbled).toBe(false)
|
||||
expect(result.current.startTime).toBeNull()
|
||||
expect(mockDataProvider.getOne).toHaveBeenCalledWith('keepalive', { id: 'track1' })
|
||||
expect(mockDataProvider.getOne).toHaveBeenCalledWith('keepalive', {
|
||||
id: 'track1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle scrobbling errors gracefully', () => {
|
||||
@ -136,7 +138,7 @@ describe('useScrobbling', () => {
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider)
|
||||
useScrobbling(mockPlayerState, mockDispatch, mockDataProvider),
|
||||
)
|
||||
|
||||
const mockInfo = {
|
||||
@ -150,9 +152,12 @@ describe('useScrobbling', () => {
|
||||
result.current.onAudioProgress(mockInfo)
|
||||
})
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Scrobbling error:', expect.any(Error))
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Scrobbling error:',
|
||||
expect.any(Error),
|
||||
)
|
||||
expect(result.current.scrobbled).toBe(true) // Still sets to true despite error
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user