diff --git a/web/src/lib/api/queries.test.ts b/web/src/lib/api/queries.test.ts index 100cec33..e55b68f7 100644 --- a/web/src/lib/api/queries.test.ts +++ b/web/src/lib/api/queries.test.ts @@ -30,3 +30,11 @@ describe('qk search keys', () => { expect(qk.searchTracks('foo')).toEqual(['searchTracks', { q: 'foo' }]); }); }); + +describe('qk M6a keys', () => { + test('M6a query keys are stable', () => { + expect(qk.home()).toEqual(['home']); + expect(qk.albumsAlpha()).toEqual(['albumsAlpha']); + expect(qk.artistTracks('a1')).toEqual(['artistTracks', 'a1']); + }); +}); diff --git a/web/src/lib/api/queries.ts b/web/src/lib/api/queries.ts index 307a8f90..07d92820 100644 --- a/web/src/lib/api/queries.ts +++ b/web/src/lib/api/queries.ts @@ -34,6 +34,9 @@ export const qk = { ['adminQuarantineActions', { limit: limit ?? 50 }] as const, suggestions: (limit?: number) => ['suggestions', { limit: limit ?? 12 }] as const, + home: () => ['home'] as const, + albumsAlpha: () => ['albumsAlpha'] as const, + artistTracks: (artistId: string) => ['artistTracks', artistId] as const, }; export function createArtistsQuery(sort: ArtistSort) { diff --git a/web/src/lib/api/types.ts b/web/src/lib/api/types.ts index 86674f63..1ee37d68 100644 --- a/web/src/lib/api/types.ts +++ b/web/src/lib/api/types.ts @@ -12,12 +12,15 @@ export type Page = { export type ArtistRef = { id: string; name: string; + sort_name: string; album_count: number; + cover_url: string; // "" when no representative album cover }; export type AlbumRef = { id: string; title: string; + sort_title: string; artist_id: string; artist_name: string; year?: number; @@ -236,3 +239,13 @@ export type ArtistSuggestion = { score: number; attribution: SeedContribution[]; // up to 3 entries, ordered by contribution DESC }; + +// Mirrors internal/api/types.go HomePayload. All slices are non-null +// per the server contract — empty sections render as []. +export type HomePayload = { + recently_added_albums: AlbumRef[]; + rediscover_albums: AlbumRef[]; + rediscover_artists: ArtistRef[]; + most_played_tracks: TrackRef[]; + last_played_artists: ArtistRef[]; +}; diff --git a/web/src/lib/components/AlbumCard.test.ts b/web/src/lib/components/AlbumCard.test.ts index 6a95cbd0..9a29dd54 100644 --- a/web/src/lib/components/AlbumCard.test.ts +++ b/web/src/lib/components/AlbumCard.test.ts @@ -33,6 +33,7 @@ import { enqueueTracks } from '$lib/player/store.svelte'; const album: AlbumRef = { id: 'xyz', title: 'Kind of Blue', + sort_title: 'Kind of Blue', artist_id: 'm-davis', artist_name: 'Miles Davis', year: 1959, diff --git a/web/src/lib/components/ArtistRow.test.ts b/web/src/lib/components/ArtistRow.test.ts index 333b78c2..5ff56b58 100644 --- a/web/src/lib/components/ArtistRow.test.ts +++ b/web/src/lib/components/ArtistRow.test.ts @@ -19,7 +19,7 @@ vi.mock('@tanstack/svelte-query', async (orig) => { return { ...actual, useQueryClient: () => ({}) }; }); -const artist: ArtistRef = { id: 'abc', name: 'alice', album_count: 12 }; +const artist: ArtistRef = { id: 'abc', name: 'alice', sort_name: 'alice', album_count: 12, cover_url: '' }; describe('ArtistRow', () => { test('renders initial, name, album count inside a link to /artists/:id', () => { diff --git a/web/src/routes/albums/[id]/album.test.ts b/web/src/routes/albums/[id]/album.test.ts index fa5673b1..fa39ac20 100644 --- a/web/src/routes/albums/[id]/album.test.ts +++ b/web/src/routes/albums/[id]/album.test.ts @@ -56,7 +56,7 @@ afterEach(() => { describe('album detail page', () => { test('renders hero metadata + one TrackRow per track', () => { const detail: AlbumDetail = { - id: 'xyz', title: 'Kind of Blue', + id: 'xyz', title: 'Kind of Blue', sort_title: 'Kind of Blue', artist_id: 'md', artist_name: 'Miles Davis', year: 1959, track_count: 2, duration_sec: 544 + 565, cover_url: '/api/albums/xyz/cover', @@ -83,7 +83,7 @@ describe('album detail page', () => { test('back link points to /artists/:artistId with the artist name', () => { const detail: AlbumDetail = { - id: 'xyz', title: 'T', + id: 'xyz', title: 'T', sort_title: 'T', artist_id: 'md', artist_name: 'Miles Davis', track_count: 0, duration_sec: 0, cover_url: '/api/albums/xyz/cover', diff --git a/web/src/routes/artists.test.ts b/web/src/routes/artists.test.ts index 25d8beb1..2ce6c39c 100644 --- a/web/src/routes/artists.test.ts +++ b/web/src/routes/artists.test.ts @@ -50,8 +50,8 @@ afterEach(() => { describe('artists list page', () => { test('renders a row per artist', () => { - const alice: ArtistRef = { id: 'a', name: 'Alice', album_count: 3 }; - const bob: ArtistRef = { id: 'b', name: 'Bob', album_count: 1 }; + const alice: ArtistRef = { id: 'a', name: 'Alice', sort_name: 'Alice', album_count: 3, cover_url: '' }; + const bob: ArtistRef = { id: 'b', name: 'Bob', sort_name: 'Bob', album_count: 1, cover_url: '' }; (createArtistsQuery as ReturnType).mockReturnValue( mockInfiniteQuery({ pages: [page([alice, bob], 2)] }) ); @@ -84,7 +84,7 @@ describe('artists list page', () => { test('"End of library" when hasNextPage is false and at least one page loaded', () => { (createArtistsQuery as ReturnType).mockReturnValue( - mockInfiniteQuery({ pages: [page([{ id: 'a', name: 'A', album_count: 1 }], 1)] }) + mockInfiniteQuery({ pages: [page([{ id: 'a', name: 'A', sort_name: 'A', album_count: 1, cover_url: '' }], 1)] }) ); render(ArtistsPage); expect(screen.getByText(/end of library/i)).toBeInTheDocument(); diff --git a/web/src/routes/artists/[id]/artist.test.ts b/web/src/routes/artists/[id]/artist.test.ts index db4a332c..90b61b80 100644 --- a/web/src/routes/artists/[id]/artist.test.ts +++ b/web/src/routes/artists/[id]/artist.test.ts @@ -40,6 +40,7 @@ import { createArtistQuery } from '$lib/api/queries'; function album(id: string, title: string, year?: number): AlbumRef { return { id, title, + sort_title: title, artist_id: 'abc', artist_name: 'Alice', year, track_count: 10, duration_sec: 2400, cover_url: `/api/albums/${id}/cover` @@ -54,7 +55,7 @@ afterEach(() => { describe('artist detail page', () => { test('renders artist name, subtitle, and one AlbumCard per album', () => { const detail: ArtistDetail = { - id: 'abc', name: 'Alice', album_count: 2, + id: 'abc', name: 'Alice', sort_name: 'Alice', album_count: 2, cover_url: '', albums: [album('a1', 'First', 2020), album('a2', 'Second')] }; (createArtistQuery as ReturnType).mockReturnValue(mockQuery({ data: detail })); @@ -67,7 +68,7 @@ describe('artist detail page', () => { test('back link points to Library', () => { (createArtistQuery as ReturnType).mockReturnValue(mockQuery({ - data: { id: 'abc', name: 'Alice', album_count: 0, albums: [] } + data: { id: 'abc', name: 'Alice', sort_name: 'Alice', album_count: 0, cover_url: '', albums: [] } })); render(ArtistPage); const back = screen.getByRole('link', { name: /library/i }); diff --git a/web/src/routes/library/liked/liked.test.ts b/web/src/routes/library/liked/liked.test.ts index bcb2d29b..8dfb625e 100644 --- a/web/src/routes/library/liked/liked.test.ts +++ b/web/src/routes/library/liked/liked.test.ts @@ -42,9 +42,9 @@ afterEach(() => vi.clearAllMocks()); describe('liked library page', () => { test('renders three sections when each has items', () => { - const ar: ArtistRef = { id: 'a1', name: 'X', album_count: 1 }; + const ar: ArtistRef = { id: 'a1', name: 'X', sort_name: 'X', album_count: 1, cover_url: '' }; const al: AlbumRef = { - id: 'al1', title: 'Y', artist_id: 'a1', artist_name: 'X', + id: 'al1', title: 'Y', sort_title: 'Y', artist_id: 'a1', artist_name: 'X', year: 2020, track_count: 1, duration_sec: 100, cover_url: '/x' }; const tr: TrackRef = { diff --git a/web/src/routes/search/albums/albums.test.ts b/web/src/routes/search/albums/albums.test.ts index 32eaed58..78a3e6e2 100644 --- a/web/src/routes/search/albums/albums.test.ts +++ b/web/src/routes/search/albums/albums.test.ts @@ -44,7 +44,7 @@ function page(items: T[], total: number, offset = 0, limit = 50): Page { } const album: AlbumRef = { - id: 'al1', title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis', + id: 'al1', title: 'Kind of Blue', sort_title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis', year: 1959, track_count: 5, duration_sec: 2630, cover_url: '/api/albums/al1/cover' }; diff --git a/web/src/routes/search/artists/artists.test.ts b/web/src/routes/search/artists/artists.test.ts index 0c18d635..19c47e70 100644 --- a/web/src/routes/search/artists/artists.test.ts +++ b/web/src/routes/search/artists/artists.test.ts @@ -43,8 +43,8 @@ afterEach(() => { describe('search artists overflow', () => { test('renders a row per artist', () => { - const a1: ArtistRef = { id: 'a1', name: 'Miles Davis', album_count: 12 }; - const a2: ArtistRef = { id: 'a2', name: 'Miles Mosley', album_count: 1 }; + const a1: ArtistRef = { id: 'a1', name: 'Miles Davis', sort_name: 'Miles Davis', album_count: 12, cover_url: '' }; + const a2: ArtistRef = { id: 'a2', name: 'Miles Mosley', sort_name: 'Miles Mosley', album_count: 1, cover_url: '' }; (createSearchArtistsInfiniteQuery as ReturnType).mockReturnValue( mockInfiniteQuery({ pages: [page([a1, a2], 2)] }) ); @@ -69,7 +69,7 @@ describe('search artists overflow', () => { test('Load more is hidden when hasNextPage is false', () => { (createSearchArtistsInfiniteQuery as ReturnType).mockReturnValue( - mockInfiniteQuery({ pages: [page([{ id: 'a', name: 'A', album_count: 1 }], 1)] }) + mockInfiniteQuery({ pages: [page([{ id: 'a', name: 'A', sort_name: 'A', album_count: 1, cover_url: '' }], 1)] }) ); render(ArtistsOverflow); expect(screen.queryByRole('button', { name: /load more/i })).not.toBeInTheDocument(); diff --git a/web/src/routes/search/search.test.ts b/web/src/routes/search/search.test.ts index 4c4927b6..18bfd292 100644 --- a/web/src/routes/search/search.test.ts +++ b/web/src/routes/search/search.test.ts @@ -53,9 +53,9 @@ function emptyResp(): SearchResponse { }; } -const artist: ArtistRef = { id: 'a1', name: 'Miles Davis', album_count: 12 }; +const artist: ArtistRef = { id: 'a1', name: 'Miles Davis', sort_name: 'Miles Davis', album_count: 12, cover_url: '' }; const album: AlbumRef = { - id: 'al1', title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis', + id: 'al1', title: 'Kind of Blue', sort_title: 'Kind of Blue', artist_id: 'a1', artist_name: 'Miles Davis', year: 1959, track_count: 5, duration_sec: 2630, cover_url: '/api/albums/al1/cover' }; const track: TrackRef = {