feat: web api client additions for wave 1
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import type {
|
||||
AlbumTracks,
|
||||
BackupFile,
|
||||
Candidate,
|
||||
CollectionResponse,
|
||||
DigitalAlbum,
|
||||
Item,
|
||||
ItemDetail,
|
||||
Loan,
|
||||
LoansResponse,
|
||||
ReleasePreview,
|
||||
SettingsView,
|
||||
Stats,
|
||||
SyncState,
|
||||
User,
|
||||
} from './types'
|
||||
@@ -73,17 +79,18 @@ export const api = {
|
||||
),
|
||||
getReleasePreview: (id: number) => request<ReleasePreview>(`/api/lookup/release/${id}`),
|
||||
|
||||
listCollection: (params: { format?: string; ripped?: string; q?: string } = {}) => {
|
||||
listCollection: (params: { format?: string; ripped?: string; q?: string; onLoan?: string } = {}) => {
|
||||
const usp = new URLSearchParams()
|
||||
if (params.format) usp.set('format', params.format)
|
||||
if (params.ripped) usp.set('ripped', params.ripped)
|
||||
if (params.q) usp.set('q', params.q)
|
||||
if (params.onLoan) usp.set('onLoan', params.onLoan)
|
||||
const qs = usp.toString()
|
||||
return request<CollectionResponse>(`/api/collection${qs ? `?${qs}` : ''}`)
|
||||
},
|
||||
addToCollection: (body: { releaseId: number; barcode?: string; matchAlbumId?: number }) =>
|
||||
post<Item>('/api/collection', body),
|
||||
getItem: (id: number) => request<Item>(`/api/collection/${id}`),
|
||||
getItem: (id: number) => request<ItemDetail>(`/api/collection/${id}`),
|
||||
deleteItem: (id: number) => request<{ ok: boolean }>(`/api/collection/${id}`, { method: 'DELETE' }),
|
||||
setRip: (id: number, ripped: boolean | null) =>
|
||||
request<Item>(`/api/collection/${id}/rip`, {
|
||||
@@ -97,4 +104,18 @@ export const api = {
|
||||
syncStatus: () => request<SyncState>('/api/library/sync'),
|
||||
startSync: () => request<SyncState>('/api/library/sync', { method: 'POST' }),
|
||||
searchAlbums: (q: string) => request<{ albums: DigitalAlbum[] }>(`/api/library/albums?q=${encodeURIComponent(q)}`),
|
||||
|
||||
getAlbumTracks: (subsonicId: string) => request<AlbumTracks>(`/api/album/${encodeURIComponent(subsonicId)}/tracks`),
|
||||
markPlayed: (subsonicId: string) => post<{ ok: boolean }>(`/api/album/${encodeURIComponent(subsonicId)}/played`),
|
||||
streamUrl: (songId: string) => `/api/stream/${encodeURIComponent(songId)}`,
|
||||
|
||||
getStats: () => request<Stats>('/api/stats'),
|
||||
exportUrl: () => '/api/export',
|
||||
|
||||
lendItem: (id: number, borrower: string) => post<Loan>(`/api/collection/${id}/loan`, { borrower }),
|
||||
getLoans: () => request<LoansResponse>('/api/loans'),
|
||||
returnLoan: (id: number) => post<{ ok: boolean }>(`/api/loans/${id}/return`),
|
||||
|
||||
triggerBackup: () => post<{ file: string }>('/api/backup'),
|
||||
getBackups: () => request<{ backups: BackupFile[] }>('/api/backups'),
|
||||
}
|
||||
|
||||
@@ -68,3 +68,49 @@ export interface DigitalAlbum {
|
||||
title: string
|
||||
artist: string
|
||||
}
|
||||
export interface Track {
|
||||
id: string
|
||||
title: string
|
||||
duration: number | null
|
||||
track: number | null
|
||||
}
|
||||
export interface AlbumTracks {
|
||||
id: string
|
||||
title: string
|
||||
artist: string
|
||||
tracks: Track[]
|
||||
}
|
||||
export interface MatchedAlbum {
|
||||
id: number
|
||||
subsonicId: string
|
||||
lastPlayedAt: string | null
|
||||
}
|
||||
export interface Stats {
|
||||
totals: { items: number; ripped: number; notRipped: number; onLoan: number }
|
||||
ripRatio: number
|
||||
formats: { name: string; count: number }[]
|
||||
decades: { name: string; count: number }[]
|
||||
topGenres: { name: string; count: number }[]
|
||||
topArtists: { name: string; count: number }[]
|
||||
addedByMonth: { month: string; count: number }[]
|
||||
}
|
||||
export interface Loan {
|
||||
id: number
|
||||
itemId: number
|
||||
borrower: string
|
||||
lentAt: string
|
||||
returnedAt: string | null
|
||||
}
|
||||
export interface LoansResponse {
|
||||
active: Loan[]
|
||||
history: Loan[]
|
||||
}
|
||||
export interface BackupFile {
|
||||
file: string
|
||||
sizeBytes: number
|
||||
createdAt: string
|
||||
}
|
||||
export interface ItemDetail extends Item {
|
||||
matchedAlbum: MatchedAlbum | null
|
||||
loan: { id: number; borrower: string; lentAt: string } | null
|
||||
}
|
||||
|
||||
43
web/test/api.test.ts
Normal file
43
web/test/api.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { api, ApiError } from '../src/api'
|
||||
|
||||
const fetchMock = vi.fn()
|
||||
beforeEach(() => vi.stubGlobal('fetch', fetchMock))
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
function jsonOk(body: unknown) {
|
||||
return Promise.resolve(new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } }))
|
||||
}
|
||||
|
||||
describe('api additions', () => {
|
||||
it('album tracks + played + stream url', async () => {
|
||||
fetchMock.mockImplementation(() => jsonOk({ id: 'a1', title: 'Motion', artist: 'TCO', tracks: [] }))
|
||||
await api.getAlbumTracks('a1')
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/album/a1/tracks', expect.anything())
|
||||
await api.markPlayed('a1')
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/album/a1/played', expect.objectContaining({ method: 'POST' }))
|
||||
expect(api.streamUrl('s 1')).toBe('/api/stream/s%201')
|
||||
})
|
||||
|
||||
it('stats, loans, backups urls', async () => {
|
||||
fetchMock.mockImplementation(() => jsonOk({}))
|
||||
await api.getStats()
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/stats', expect.anything())
|
||||
await api.lendItem(5, 'Bob')
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/collection/5/loan', expect.objectContaining({ method: 'POST' }))
|
||||
await api.returnLoan(7)
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/loans/7/return', expect.objectContaining({ method: 'POST' }))
|
||||
await api.getBackups()
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/backups', expect.anything())
|
||||
expect(api.exportUrl()).toBe('/api/export')
|
||||
})
|
||||
|
||||
it('surfaces subsonic errors as ApiError', async () => {
|
||||
fetchMock.mockReturnValue(
|
||||
Promise.resolve(new Response(JSON.stringify({ error: 'no_subsonic_config' }), { status: 409 }))
|
||||
)
|
||||
const err = await api.getAlbumTracks('a1').catch((e) => e)
|
||||
expect(err).toBeInstanceOf(ApiError)
|
||||
expect((err as ApiError).code).toBe('no_subsonic_config')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user