feat: subsonic getAlbum/getRecentAlbums and stream url builder
This commit is contained in:
@@ -75,6 +75,40 @@ export class SubsonicClient {
|
|||||||
await this.request('ping')
|
await this.request('ping')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Raw endpoint URL with auth params — for streaming passthrough. */
|
||||||
|
url(endpoint: string, params: Record<string, string> = {}): string {
|
||||||
|
const u = new URL(`${this.base}/rest/${endpoint}`)
|
||||||
|
const search = { ...this.authParams(), ...params }
|
||||||
|
for (const [k, v] of Object.entries(search)) u.searchParams.set(k, v)
|
||||||
|
return u.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAlbum(albumId: string): Promise<{ id: string; title: string; artist: string; tracks: { id: string; title: string; duration: number | null; track: number | null }[] }> {
|
||||||
|
const envelope = await this.request('getAlbum', { id: albumId })
|
||||||
|
const album = envelope.album ?? {}
|
||||||
|
const songs: any[] = album.song ?? []
|
||||||
|
const tracks = songs
|
||||||
|
.map((s) => ({
|
||||||
|
id: String(s.id),
|
||||||
|
title: String(s.title ?? ''),
|
||||||
|
duration: Number.isFinite(Number(s.duration)) ? Number(s.duration) : null,
|
||||||
|
track: Number.isInteger(Number(s.track)) ? Number(s.track) : null,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => (a.track ?? 9999) - (b.track ?? 9999))
|
||||||
|
return { id: String(album.id ?? albumId), title: album.name ?? album.title ?? '', artist: album.artist ?? '', tracks }
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecentAlbums(size = 500): Promise<{ id: string; title: string; artist: string; playedAt: string | null }[]> {
|
||||||
|
const envelope = await this.request('getAlbumList2', { type: 'recent', size: String(size) })
|
||||||
|
const list: any[] = envelope.albumList2?.album ?? []
|
||||||
|
return list.map((a) => ({
|
||||||
|
id: String(a.id),
|
||||||
|
title: a.name ?? a.title ?? '',
|
||||||
|
artist: a.artist ?? '',
|
||||||
|
playedAt: typeof a.played === 'string' ? a.played : typeof a.playedAt === 'string' ? a.playedAt : null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
async getAllAlbums(
|
async getAllAlbums(
|
||||||
onProgress?: (albums: SubsonicAlbum[], done: number) => void
|
onProgress?: (albums: SubsonicAlbum[], done: number) => void
|
||||||
): Promise<SubsonicAlbum[]> {
|
): Promise<SubsonicAlbum[]> {
|
||||||
|
|||||||
@@ -118,4 +118,76 @@ describe('SubsonicClient', () => {
|
|||||||
{ id: '2', title: 'Album 2', artist: 'Artist 0' },
|
{ id: '2', title: 'Album 2', artist: 'Artist 0' },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('url() builds a raw endpoint URL with auth params', () => {
|
||||||
|
const c = new SubsonicClient({
|
||||||
|
url: 'http://navidrome.local',
|
||||||
|
username: 'sam',
|
||||||
|
password: 'pass',
|
||||||
|
fetchImpl: (async () => subsonicResponse({})) as typeof fetch,
|
||||||
|
})
|
||||||
|
const u = new URL(c.url('stream', { id: 'song-9' }))
|
||||||
|
expect(u.pathname).toBe('/rest/stream')
|
||||||
|
expect(u.searchParams.get('id')).toBe('song-9')
|
||||||
|
expect(u.searchParams.get('u')).toBe('sam')
|
||||||
|
expect(u.searchParams.get('f')).toBe('json')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getAlbum returns ordered tracks', async () => {
|
||||||
|
const c = new SubsonicClient({
|
||||||
|
url: 'http://x',
|
||||||
|
username: 'sam',
|
||||||
|
password: 'pass',
|
||||||
|
fetchImpl: (async () =>
|
||||||
|
subsonicResponse({
|
||||||
|
'subsonic-response': {
|
||||||
|
status: 'ok',
|
||||||
|
album: {
|
||||||
|
id: 'alb-1',
|
||||||
|
name: 'Motion',
|
||||||
|
artist: 'The Cinematic Orchestra',
|
||||||
|
song: [
|
||||||
|
{ id: 's2', title: 'Theme de Yoyo', duration: 300, track: 2 },
|
||||||
|
{ id: 's1', title: 'Overture', duration: 200, track: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})) as typeof fetch,
|
||||||
|
})
|
||||||
|
const album = await c.getAlbum('alb-1')
|
||||||
|
expect(album).toEqual({
|
||||||
|
id: 'alb-1',
|
||||||
|
title: 'Motion',
|
||||||
|
artist: 'The Cinematic Orchestra',
|
||||||
|
tracks: [
|
||||||
|
{ id: 's1', title: 'Overture', duration: 200, track: 1 },
|
||||||
|
{ id: 's2', title: 'Theme de Yoyo', duration: 300, track: 2 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getRecentAlbums returns the recent list raw', async () => {
|
||||||
|
const c = new SubsonicClient({
|
||||||
|
url: 'http://x',
|
||||||
|
username: 'sam',
|
||||||
|
password: 'pass',
|
||||||
|
fetchImpl: (async () =>
|
||||||
|
subsonicResponse({
|
||||||
|
'subsonic-response': {
|
||||||
|
status: 'ok',
|
||||||
|
albumList2: {
|
||||||
|
album: [
|
||||||
|
{ id: 'a1', name: 'Motion', artist: 'TCO', played: '2026-09-01T10:00:00Z' },
|
||||||
|
{ id: 'a2', name: 'Blue Lines', artist: 'Massive Attack' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})) as typeof fetch,
|
||||||
|
})
|
||||||
|
const recent = await c.getRecentAlbums(500)
|
||||||
|
expect(recent).toEqual([
|
||||||
|
{ id: 'a1', title: 'Motion', artist: 'TCO', playedAt: '2026-09-01T10:00:00Z' },
|
||||||
|
{ id: 'a2', title: 'Blue Lines', artist: 'Massive Attack', playedAt: null },
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user