From e674869e06c47604f5d1abeca4d9633071e8dc6b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 1 May 2026 20:13:49 -0400 Subject: [PATCH] feat(web): add home/albums/artists API clients for M6a Co-Authored-By: Claude Sonnet 4.6 --- web/src/lib/api/albums.test.ts | 33 +++++++++++++++++++++++++++++++++ web/src/lib/api/albums.ts | 22 ++++++++++++++++++++++ web/src/lib/api/artists.test.ts | 23 +++++++++++++++++++++++ web/src/lib/api/artists.ts | 16 ++++++++++++++++ web/src/lib/api/home.test.ts | 32 ++++++++++++++++++++++++++++++++ web/src/lib/api/home.ts | 19 +++++++++++++++++++ 6 files changed, 145 insertions(+) create mode 100644 web/src/lib/api/albums.test.ts create mode 100644 web/src/lib/api/albums.ts create mode 100644 web/src/lib/api/artists.test.ts create mode 100644 web/src/lib/api/artists.ts create mode 100644 web/src/lib/api/home.test.ts create mode 100644 web/src/lib/api/home.ts diff --git a/web/src/lib/api/albums.test.ts b/web/src/lib/api/albums.test.ts new file mode 100644 index 00000000..b5dbef43 --- /dev/null +++ b/web/src/lib/api/albums.test.ts @@ -0,0 +1,33 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('./client', () => ({ + api: { get: vi.fn() } +})); + +import { listAlbumsAlpha } from './albums'; +import { qk } from './queries'; +import { api } from './client'; + +afterEach(() => vi.clearAllMocks()); + +describe('albums client', () => { + test('listAlbumsAlpha hits /api/library/albums with paging', async () => { + (api.get as ReturnType).mockResolvedValueOnce({ + items: [], total: 0, limit: 50, offset: 0 + }); + await listAlbumsAlpha(50, 0); + expect(api.get).toHaveBeenCalledWith('/api/library/albums?limit=50&offset=0'); + }); + + test('listAlbumsAlpha honors custom limit and offset', async () => { + (api.get as ReturnType).mockResolvedValueOnce({ + items: [], total: 0, limit: 25, offset: 100 + }); + await listAlbumsAlpha(25, 100); + expect(api.get).toHaveBeenCalledWith('/api/library/albums?limit=25&offset=100'); + }); + + test('qk.albumsAlpha key is stable', () => { + expect(qk.albumsAlpha()).toEqual(['albumsAlpha']); + }); +}); diff --git a/web/src/lib/api/albums.ts b/web/src/lib/api/albums.ts new file mode 100644 index 00000000..a40cc8de --- /dev/null +++ b/web/src/lib/api/albums.ts @@ -0,0 +1,22 @@ +import { createInfiniteQuery } from '@tanstack/svelte-query'; +import { api } from './client'; +import { qk } from './queries'; +import type { AlbumRef, Page } from './types'; + +export const ALBUM_PAGE_SIZE = 50; + +export async function listAlbumsAlpha(limit: number, offset: number): Promise> { + return api.get>(`/api/library/albums?limit=${limit}&offset=${offset}`); +} + +export function createAlbumsAlphaInfiniteQuery() { + return createInfiniteQuery({ + queryKey: qk.albumsAlpha(), + queryFn: ({ pageParam = 0 }) => listAlbumsAlpha(ALBUM_PAGE_SIZE, pageParam), + initialPageParam: 0, + getNextPageParam: (last) => { + const loaded = last.offset + last.items.length; + return loaded >= last.total ? undefined : loaded; + } + }); +} diff --git a/web/src/lib/api/artists.test.ts b/web/src/lib/api/artists.test.ts new file mode 100644 index 00000000..a2eba5b0 --- /dev/null +++ b/web/src/lib/api/artists.test.ts @@ -0,0 +1,23 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('./client', () => ({ + api: { get: vi.fn() } +})); + +import { listArtistTracks } from './artists'; +import { qk } from './queries'; +import { api } from './client'; + +afterEach(() => vi.clearAllMocks()); + +describe('artists client', () => { + test('listArtistTracks hits /api/artists/:id/tracks', async () => { + (api.get as ReturnType).mockResolvedValueOnce([]); + await listArtistTracks('art-1'); + expect(api.get).toHaveBeenCalledWith('/api/artists/art-1/tracks'); + }); + + test('qk.artistTracks key includes the artist id', () => { + expect(qk.artistTracks('art-1')).toEqual(['artistTracks', 'art-1']); + }); +}); diff --git a/web/src/lib/api/artists.ts b/web/src/lib/api/artists.ts new file mode 100644 index 00000000..05b14f4c --- /dev/null +++ b/web/src/lib/api/artists.ts @@ -0,0 +1,16 @@ +import { createQuery } from '@tanstack/svelte-query'; +import { api } from './client'; +import { qk } from './queries'; +import type { TrackRef } from './types'; + +export async function listArtistTracks(artistId: string): Promise { + return api.get(`/api/artists/${artistId}/tracks`); +} + +export function createArtistTracksQuery(artistId: string) { + return createQuery({ + queryKey: qk.artistTracks(artistId), + queryFn: () => listArtistTracks(artistId), + enabled: artistId.length > 0 + }); +} diff --git a/web/src/lib/api/home.test.ts b/web/src/lib/api/home.test.ts new file mode 100644 index 00000000..6bfac118 --- /dev/null +++ b/web/src/lib/api/home.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('./client', () => ({ + api: { get: vi.fn() } +})); + +import { listHome } from './home'; +import { qk } from './queries'; +import { api } from './client'; +import type { HomePayload } from './types'; + +afterEach(() => vi.clearAllMocks()); + +describe('home client', () => { + test('listHome hits /api/home', async () => { + const fixture: HomePayload = { + recently_added_albums: [], + rediscover_albums: [], + rediscover_artists: [], + most_played_tracks: [], + last_played_artists: [] + }; + (api.get as ReturnType).mockResolvedValueOnce(fixture); + const got = await listHome(); + expect(api.get).toHaveBeenCalledWith('/api/home'); + expect(got).toEqual(fixture); + }); + + test('qk.home key is stable', () => { + expect(qk.home()).toEqual(['home']); + }); +}); diff --git a/web/src/lib/api/home.ts b/web/src/lib/api/home.ts new file mode 100644 index 00000000..497422c4 --- /dev/null +++ b/web/src/lib/api/home.ts @@ -0,0 +1,19 @@ +import { createQuery } from '@tanstack/svelte-query'; +import { api } from './client'; +import { qk } from './queries'; +import type { HomePayload } from './types'; + +export async function listHome(): Promise { + return api.get('/api/home'); +} + +// staleTime = 60s — repeat home-page mounts within a minute reuse cached +// data. Like-state changes, new plays, freshly-added albums surface on +// the next refetch. +export function createHomeQuery() { + return createQuery({ + queryKey: qk.home(), + queryFn: listHome, + staleTime: 60_000 + }); +}