test-web / test (push) Failing after 32s
Renders GET /api/admin/library/missing under Admin -> Missing files. Folder-grouped, because that is the unit an operator decides about: the case behind #2523 was three reorganised albums, and forty individual rows hides that it is really three decisions. Each row leads with the fact that settles whether a missing file is worth chasing -- "last played 2d ago" against "never played". The group header carries how many tracks and how long they have been gone. Read-only. No remove button anywhere: the row, its play history and its likes survive a file going missing, and the scanner clears the mark by itself when the file returns (or adopts the row if it returns renamed, #2528). The page says so in its own copy rather than leaving the operator to infer it. Empty state explains the feature instead of the emptiness -- what puts a row here (moved outside Minstrel, deleted, a drive that didn't mount) and that rows leave on their own. Someone who has never seen this page should not have to guess. Paging follows the house pattern -- plain offset into the factory, wrapped in $derived so a page change re-creates the query with a new key. Passing a getter instead would capture the key once and paging would silently not refetch. The pager only renders when it can do something.
171 lines
6.7 KiB
TypeScript
171 lines
6.7 KiB
TypeScript
import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query';
|
|
import { api } from './client';
|
|
import { pageGetNextPageParam } from './paging';
|
|
import type {
|
|
ArtistRef,
|
|
ArtistDetail,
|
|
AlbumRef,
|
|
AlbumDetail,
|
|
Page,
|
|
SearchResponse,
|
|
TrackRef
|
|
} from './types';
|
|
|
|
export type ArtistSort = 'alpha' | 'newest';
|
|
export const ARTIST_PAGE_SIZE = 50;
|
|
export const SEARCH_SUMMARY_LIMIT = 10;
|
|
export const SEARCH_FACET_PAGE_SIZE = 50;
|
|
|
|
export const qk = {
|
|
artists: (sort: ArtistSort) => ['artists', { sort }] as const,
|
|
artist: (id: string) => ['artist', id] as const,
|
|
album: (id: string) => ['album', id] as const,
|
|
search: (q: string) => ['search', { q }] as const,
|
|
searchArtists: (q: string) => ['searchArtists', { q }] as const,
|
|
searchAlbums: (q: string) => ['searchAlbums', { q }] as const,
|
|
searchTracks: (q: string) => ['searchTracks', { q }] as const,
|
|
likedIds: () => ['likedIds'] as const,
|
|
likedTracks: () => ['likedTracks'] as const,
|
|
likedAlbums: () => ['likedAlbums'] as const,
|
|
likedArtists: () => ['likedArtists'] as const,
|
|
history: () => ['history'] as const,
|
|
// Lidarr / requests / admin.
|
|
lidarrSearch: (q: string, kind: string) =>
|
|
['lidarrSearch', { q, kind }] as const,
|
|
myRequests: () => ['myRequests'] as const,
|
|
lidarrConfig: () => ['lidarrConfig'] as const,
|
|
lidarrQualityProfiles: () => ['lidarrQualityProfiles'] as const,
|
|
lidarrMetadataProfiles: () => ['lidarrMetadataProfiles'] as const,
|
|
lidarrRootFolders: () => ['lidarrRootFolders'] as const,
|
|
adminRequests: (status?: string) =>
|
|
['adminRequests', { status: status ?? 'all' }] as const,
|
|
myQuarantine: () => ['myQuarantine'] as const,
|
|
adminQuarantine: () => ['adminQuarantine'] as const,
|
|
adminQuarantineActions: (limit?: number) =>
|
|
['adminQuarantineActions', { limit: limit ?? 50 }] as const,
|
|
adminPlaybackErrors: (resolved?: boolean) =>
|
|
['adminPlaybackErrors', { resolved: resolved ?? false }] as const,
|
|
scanStatus: () => ['scanStatus'] as const,
|
|
coverage: () => ['coverage'] as const,
|
|
coverProviders: () => ['coverProviders'] as const,
|
|
tagProviders: () => ['tagProviders'] as const,
|
|
adminUsers: () => ['adminUsers'] as const,
|
|
adminInvites: () => ['adminInvites'] as const,
|
|
adminDiagnostics: (f: Record<string, string | number | undefined>) =>
|
|
['adminDiagnostics', f] as const,
|
|
adminMissingFiles: (offset?: number) =>
|
|
['adminMissingFiles', { offset: offset ?? 0 }] as const,
|
|
adminDiagnosticDevices: (userId?: string) =>
|
|
['adminDiagnosticDevices', { userId: userId ?? 'all' }] as const,
|
|
smtpConfig: () => ['smtpConfig'] as const,
|
|
suggestions: (limit?: number) =>
|
|
['suggestions', { limit: limit ?? 12 }] as const,
|
|
suggestionSnoozes: () => ['suggestionSnoozes'] as const,
|
|
home: () => ['home'] as const,
|
|
albumsAlpha: () => ['albumsAlpha'] as const,
|
|
artistTracks: (artistId: string) => ['artistTracks', artistId] as const,
|
|
similarArtists: (artistId: string) => ['similarArtists', artistId] as const,
|
|
artistTopTracks: (artistId: string) => ['artistTopTracks', artistId] as const,
|
|
playlists: (kind?: 'user' | 'system' | 'all') =>
|
|
['playlists', { kind: kind ?? 'user' }] as const,
|
|
playlist: (id: string) => ['playlist', id] as const,
|
|
systemPlaylistsStatus: () => ['systemPlaylistsStatus'] as const,
|
|
// Browse indexes (#367). Keys carry no arguments — both are whole-library
|
|
// indexes, and the per-genre / per-year album lists are fetched outside
|
|
// svelte-query because their selection comes from the URL.
|
|
genres: () => ['genres'] as const,
|
|
albumYears: () => ['albumYears'] as const,
|
|
};
|
|
|
|
export function createArtistsQuery(sort: ArtistSort) {
|
|
return createInfiniteQuery({
|
|
queryKey: qk.artists(sort),
|
|
queryFn: ({ pageParam = 0 }) =>
|
|
api.get<Page<ArtistRef>>(
|
|
`/api/artists?sort=${sort}&limit=${ARTIST_PAGE_SIZE}&offset=${pageParam}`
|
|
),
|
|
initialPageParam: 0,
|
|
getNextPageParam: pageGetNextPageParam<ArtistRef>(),
|
|
});
|
|
}
|
|
|
|
export function createArtistQuery(id: string) {
|
|
return createQuery({
|
|
queryKey: qk.artist(id),
|
|
queryFn: () => api.get<ArtistDetail>(`/api/artists/${id}`),
|
|
});
|
|
}
|
|
|
|
export function createAlbumQuery(id: string) {
|
|
return createQuery({
|
|
queryKey: qk.album(id),
|
|
queryFn: () => api.get<AlbumDetail>(`/api/albums/${id}`),
|
|
});
|
|
}
|
|
|
|
export function createSimilarArtistsQuery(id: string) {
|
|
return createQuery({
|
|
queryKey: qk.similarArtists(id),
|
|
queryFn: () => api.get<ArtistRef[]>(`/api/artists/${id}/similar`),
|
|
enabled: id.length > 0,
|
|
});
|
|
}
|
|
|
|
export function createArtistTopTracksQuery(id: string) {
|
|
return createQuery({
|
|
queryKey: qk.artistTopTracks(id),
|
|
queryFn: () => api.get<TrackRef[]>(`/api/artists/${id}/top-tracks`),
|
|
enabled: id.length > 0,
|
|
});
|
|
}
|
|
|
|
export function createSearchQuery(q: string) {
|
|
return createQuery({
|
|
queryKey: qk.search(q),
|
|
queryFn: () =>
|
|
api.get<SearchResponse>(
|
|
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_SUMMARY_LIMIT}`
|
|
),
|
|
enabled: q.length > 0
|
|
});
|
|
}
|
|
|
|
export function createSearchArtistsInfiniteQuery(q: string) {
|
|
return createInfiniteQuery({
|
|
queryKey: qk.searchArtists(q),
|
|
queryFn: ({ pageParam = 0 }) =>
|
|
api.get<SearchResponse>(
|
|
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
|
|
).then((r) => r.artists),
|
|
initialPageParam: 0,
|
|
getNextPageParam: pageGetNextPageParam<ArtistRef>(),
|
|
enabled: q.length > 0
|
|
});
|
|
}
|
|
|
|
export function createSearchAlbumsInfiniteQuery(q: string) {
|
|
return createInfiniteQuery({
|
|
queryKey: qk.searchAlbums(q),
|
|
queryFn: ({ pageParam = 0 }) =>
|
|
api.get<SearchResponse>(
|
|
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
|
|
).then((r) => r.albums),
|
|
initialPageParam: 0,
|
|
getNextPageParam: pageGetNextPageParam<AlbumRef>(),
|
|
enabled: q.length > 0
|
|
});
|
|
}
|
|
|
|
export function createSearchTracksInfiniteQuery(q: string) {
|
|
return createInfiniteQuery({
|
|
queryKey: qk.searchTracks(q),
|
|
queryFn: ({ pageParam = 0 }) =>
|
|
api.get<SearchResponse>(
|
|
`/api/search?q=${encodeURIComponent(q)}&limit=${SEARCH_FACET_PAGE_SIZE}&offset=${pageParam}`
|
|
).then((r) => r.tracks),
|
|
initialPageParam: 0,
|
|
getNextPageParam: pageGetNextPageParam<TrackRef>(),
|
|
enabled: q.length > 0
|
|
});
|
|
}
|