mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-14 22:28:41 +01:00
Song coverArt ids emitted to Subsonic clients embedded no timestamp for the disc/album fallback paths (always `_0`), so clients that cache by coverArt id kept stale art after an album cover upload or delete. Fold the song's CoverArtUpdatedAt into DiscCoverArtID and AlbumCoverArtID; ids are unchanged when no cover was ever uploaded. The web UI disc-header thumbnails had the same gap: getDiscCoverArtUrl only used the song's updatedAt. It now takes the record and picks the newest of updatedAt/coverArtUpdatedAt, sharing the cache-key helper with getCoverArtUrl.
259 lines
6.9 KiB
JavaScript
259 lines
6.9 KiB
JavaScript
import { vi } from 'vitest'
|
|
import config from '../config'
|
|
import subsonic from './index'
|
|
|
|
describe('getCoverArtUrl', () => {
|
|
beforeEach(() => {
|
|
// Mock window.location
|
|
delete window.location
|
|
window.location = { href: 'http://localhost:3000/app' }
|
|
|
|
// Mock localStorage values required by subsonic
|
|
const localStorageMock = {
|
|
getItem: vi.fn((key) => {
|
|
const values = {
|
|
username: 'testuser',
|
|
'subsonic-token': 'testtoken',
|
|
'subsonic-salt': 'testsalt',
|
|
}
|
|
return values[key] || null
|
|
}),
|
|
setItem: vi.fn(),
|
|
clear: vi.fn(),
|
|
}
|
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
|
|
})
|
|
|
|
it('should return playlist cover art URL for records with sync property', () => {
|
|
const playlistRecord = {
|
|
id: 'playlist-123',
|
|
sync: true,
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
}
|
|
|
|
const url = subsonic.getCoverArtUrl(
|
|
playlistRecord,
|
|
config.uiCoverArtSize,
|
|
true,
|
|
)
|
|
|
|
expect(url).toContain('pl-playlist-123')
|
|
expect(url).toContain('size=600')
|
|
expect(url).toContain('square=true')
|
|
expect(url).toContain('_=2023-01-01T00%3A00%3A00Z')
|
|
})
|
|
|
|
it('should add timestamp for playlists without updatedAt', () => {
|
|
const playlistRecord = {
|
|
id: 'playlist-123',
|
|
sync: true,
|
|
}
|
|
|
|
const url = subsonic.getCoverArtUrl(
|
|
playlistRecord,
|
|
config.uiCoverArtSize,
|
|
true,
|
|
)
|
|
|
|
expect(url).toContain('pl-playlist-123')
|
|
expect(url).toContain('size=600')
|
|
expect(url).toContain('square=true')
|
|
expect(url).not.toContain('_=')
|
|
})
|
|
|
|
it('should return album cover art URL for records with albumArtist', () => {
|
|
const albumRecord = {
|
|
id: 'album-123',
|
|
albumArtist: 'Test Artist',
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
}
|
|
|
|
const url = subsonic.getCoverArtUrl(
|
|
albumRecord,
|
|
config.uiCoverArtSize,
|
|
true,
|
|
)
|
|
|
|
expect(url).toContain('al-album-123')
|
|
expect(url).toContain('size=600')
|
|
expect(url).toContain('square=true')
|
|
})
|
|
|
|
it('should bust the cache on coverArtUpdatedAt when it is newer', () => {
|
|
const albumRecord = {
|
|
id: 'album-123',
|
|
albumArtist: 'Test Artist',
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
coverArtUpdatedAt: '2024-06-01T00:00:00Z',
|
|
}
|
|
|
|
const url = subsonic.getCoverArtUrl(albumRecord)
|
|
|
|
expect(url).toContain('al-album-123')
|
|
expect(url).toContain('_=2024-06-01T00%3A00%3A00Z')
|
|
expect(url).not.toContain('_=2023-01-01')
|
|
})
|
|
|
|
it('should return media file cover art URL for records with album', () => {
|
|
const songRecord = {
|
|
id: 'song-123',
|
|
album: 'Test Album',
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
}
|
|
|
|
const url = subsonic.getCoverArtUrl(songRecord, config.uiCoverArtSize, true)
|
|
|
|
expect(url).toContain('mf-song-123')
|
|
expect(url).toContain('size=600')
|
|
expect(url).toContain('square=true')
|
|
})
|
|
|
|
it('should return artist cover art URL for other records', () => {
|
|
const artistRecord = {
|
|
id: 'artist-123',
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
}
|
|
|
|
const url = subsonic.getCoverArtUrl(
|
|
artistRecord,
|
|
config.uiCoverArtSize,
|
|
true,
|
|
)
|
|
|
|
expect(url).toContain('ar-artist-123')
|
|
expect(url).toContain('size=600')
|
|
expect(url).toContain('square=true')
|
|
})
|
|
|
|
it('should handle records without updatedAt', () => {
|
|
const record = {
|
|
id: 'test-123',
|
|
}
|
|
|
|
const url = subsonic.getCoverArtUrl(record)
|
|
|
|
expect(url).toContain('ar-test-123')
|
|
expect(url).not.toContain('_=')
|
|
})
|
|
})
|
|
|
|
describe('getDiscCoverArtUrl', () => {
|
|
beforeEach(() => {
|
|
const localStorageMock = {
|
|
getItem: vi.fn((key) => {
|
|
const values = {
|
|
username: 'testuser',
|
|
'subsonic-token': 'testtoken',
|
|
'subsonic-salt': 'testsalt',
|
|
}
|
|
return values[key] || null
|
|
}),
|
|
}
|
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
|
|
})
|
|
|
|
it('should construct URL with dc-albumId:discNumber format, size, and cache param', () => {
|
|
const url = subsonic.getDiscCoverArtUrl(
|
|
{
|
|
albumId: 'album-123',
|
|
discNumber: 2,
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
},
|
|
48,
|
|
)
|
|
|
|
expect(url).toContain('getCoverArt')
|
|
expect(url).toContain('id=dc-album-123%3A2')
|
|
expect(url).toContain('size=48')
|
|
expect(url).toContain('_=2023-01-01T00%3A00%3A00Z')
|
|
})
|
|
|
|
it('should handle missing updatedAt', () => {
|
|
const url = subsonic.getDiscCoverArtUrl(
|
|
{ albumId: 'album-123', discNumber: 1 },
|
|
48,
|
|
)
|
|
|
|
expect(url).toContain('id=dc-album-123%3A1')
|
|
expect(url).toContain('size=48')
|
|
expect(url).not.toContain('_=')
|
|
})
|
|
|
|
it('should handle missing size', () => {
|
|
const url = subsonic.getDiscCoverArtUrl({
|
|
albumId: 'album-123',
|
|
discNumber: 1,
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
})
|
|
|
|
expect(url).toContain('id=dc-album-123%3A1')
|
|
expect(url).toContain('_=2023-01-01T00%3A00%3A00Z')
|
|
expect(url).not.toContain('size=')
|
|
})
|
|
|
|
it('should bust the cache on coverArtUpdatedAt when it is newer', () => {
|
|
const url = subsonic.getDiscCoverArtUrl({
|
|
albumId: 'album-123',
|
|
discNumber: 1,
|
|
updatedAt: '2023-01-01T00:00:00Z',
|
|
coverArtUpdatedAt: '2024-06-01T00:00:00Z',
|
|
})
|
|
|
|
expect(url).toContain('id=dc-album-123%3A1')
|
|
expect(url).toContain('_=2024-06-01T00%3A00%3A00Z')
|
|
expect(url).not.toContain('_=2023-01-01')
|
|
})
|
|
})
|
|
|
|
describe('getAvatarUrl', () => {
|
|
beforeEach(() => {
|
|
// Mock localStorage values required by subsonic
|
|
const localStorageMock = {
|
|
getItem: vi.fn((key) => {
|
|
const values = {
|
|
username: 'testuser',
|
|
'subsonic-token': 'testtoken',
|
|
'subsonic-salt': 'testsalt',
|
|
}
|
|
return values[key] || null
|
|
}),
|
|
}
|
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
|
|
})
|
|
|
|
it('should include username parameter', () => {
|
|
const url = subsonic.getAvatarUrl('john')
|
|
expect(url).toContain('getAvatar')
|
|
expect(url).toContain('username=john')
|
|
})
|
|
})
|
|
|
|
describe('reportPlayback', () => {
|
|
beforeEach(() => {
|
|
const localStorageMock = {
|
|
getItem: vi.fn((key) => {
|
|
const values = {
|
|
username: 'testuser',
|
|
'subsonic-token': 'testtoken',
|
|
'subsonic-salt': 'testsalt',
|
|
}
|
|
return values[key] || null
|
|
}),
|
|
}
|
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock })
|
|
})
|
|
|
|
it('should construct reportPlayback URL with correct parameters', () => {
|
|
const url = subsonic.url('reportPlayback', null, {
|
|
mediaId: 'song-123',
|
|
mediaType: 'song',
|
|
positionMs: 5000,
|
|
state: 'playing',
|
|
})
|
|
expect(url).toContain('reportPlayback')
|
|
expect(url).toContain('mediaId=song-123')
|
|
expect(url).toContain('mediaType=song')
|
|
expect(url).toContain('positionMs=5000')
|
|
expect(url).toContain('state=playing')
|
|
})
|
|
})
|