fix(ui): replaygain for Artist Radio and Top Songs (#4328)

* Map replaygain info from getSimilarSongs2

* refactor: rename mapping function

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: Applied code review improvements

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão 2025-07-08 17:41:14 -03:00 committed by GitHub
parent d041cb3249
commit 65961cce4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 72 additions and 14 deletions

View File

@ -49,11 +49,21 @@ describe('ArtistActions', () => {
// Mock console.error to suppress error logging in tests
vi.spyOn(console, 'error').mockImplementation(() => {})
const songWithReplayGain = {
id: 'rec1',
replayGain: {
albumGain: -5,
albumPeak: 1,
trackGain: -6,
trackPeak: 0.8,
},
}
subsonic.getSimilarSongs2.mockResolvedValue({
json: {
'subsonic-response': {
status: 'ok',
similarSongs2: { song: [{ id: 'rec1' }] },
similarSongs2: { song: [songWithReplayGain] },
},
},
})
@ -61,7 +71,7 @@ describe('ArtistActions', () => {
json: {
'subsonic-response': {
status: 'ok',
topSongs: { song: [{ id: 'rec1' }] },
topSongs: { song: [songWithReplayGain] },
},
},
})
@ -93,6 +103,22 @@ describe('ArtistActions', () => {
)
expect(mockDispatch).toHaveBeenCalled()
})
it('maps replaygain info', async () => {
renderArtistActions()
clickActionButton('radio')
await waitFor(() =>
expect(subsonic.getSimilarSongs2).toHaveBeenCalledWith('ar1', 100),
)
const action = mockDispatch.mock.calls[0][0]
expect(action.data.rec1).toMatchObject({
rgAlbumGain: -5,
rgAlbumPeak: 1,
rgTrackGain: -6,
rgTrackPeak: 0.8,
})
})
})
describe('Play action', () => {
@ -106,6 +132,22 @@ describe('ArtistActions', () => {
expect(mockDispatch).toHaveBeenCalled()
})
it('maps replaygain info for top songs', async () => {
renderArtistActions()
clickActionButton('topSongs')
await waitFor(() =>
expect(subsonic.getTopSongs).toHaveBeenCalledWith('Artist', 100),
)
const action = mockDispatch.mock.calls[0][0]
expect(action.data.rec1).toMatchObject({
rgAlbumGain: -5,
rgAlbumPeak: 1,
rgTrackGain: -6,
rgTrackPeak: 0.8,
})
})
it('handles API rejection', async () => {
subsonic.getTopSongs.mockRejectedValue(new Error('Network error'))

View File

@ -1,6 +1,32 @@
import subsonic from '../subsonic/index.js'
import { playTracks } from '../actions/index.js'
const mapReplayGain = (song) => {
const { replayGain: rg } = song
if (!rg) {
return song
}
return {
...song,
...(rg.albumGain !== undefined && { rgAlbumGain: rg.albumGain }),
...(rg.albumPeak !== undefined && { rgAlbumPeak: rg.albumPeak }),
...(rg.trackGain !== undefined && { rgTrackGain: rg.trackGain }),
...(rg.trackPeak !== undefined && { rgTrackPeak: rg.trackPeak }),
}
}
const processSongsForPlayback = (songs) => {
const songData = {}
const ids = []
songs.forEach((s) => {
const song = mapReplayGain(s)
songData[song.id] = song
ids.push(song.id)
})
return { songData, ids }
}
export const playTopSongs = async (dispatch, notify, artistName) => {
const res = await subsonic.getTopSongs(artistName, 100)
const data = res.json['subsonic-response']
@ -17,12 +43,7 @@ export const playTopSongs = async (dispatch, notify, artistName) => {
return
}
const songData = {}
const ids = []
songs.forEach((s) => {
songData[s.id] = s
ids.push(s.id)
})
const { songData, ids } = processSongsForPlayback(songs)
dispatch(playTracks(songData, ids))
}
@ -42,12 +63,7 @@ export const playSimilar = async (dispatch, notify, id) => {
return
}
const songData = {}
const ids = []
songs.forEach((s) => {
songData[s.id] = s
ids.push(s.id)
})
const { songData, ids } = processSongsForPlayback(songs)
dispatch(playTracks(songData, ids))
}