release / govulncheck (push) Successful in 21s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m54s
release / integration (push) Successful in 5m33s
release / android (push) Successful in 5m40s
release / Build signed APK (releases and dev) (push) Successful in 5m56s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
The section opens on a catalogue of every artist on the list, A–Z with a letter rail, each card showing how many of their albums are held. An artist opens on their page: every album of theirs, held or not, with its status, search, releases and tracks. Wanted, Missing files, Activity, Following and Blocked sit down a left rail as the sections that show state; Held is gone, the catalogue shows it. The section moves from /wanted to /collection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
8.4 KiB
TypeScript
196 lines
8.4 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.
|
|
catalogueSearch: (q: string, kind: string) =>
|
|
['catalogueSearch', { 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,
|
|
// Wanted list (M502 #5520). Wants sit under ['adminWants'] so one prefix
|
|
// invalidation refreshes every tab and its counts.
|
|
adminWants: (state: string, q: string = '', artist: string = '') =>
|
|
['adminWants', { state, q, artist }] as const,
|
|
wantArtists: (q: string = '') => ['adminWants', 'artists', { q }] as const,
|
|
wantTracks: (id: string) => ['wantTracks', id] as const,
|
|
adminFollows: () => ['adminFollows'] as const,
|
|
adminFollow: (artistMbid: string) => ['adminFollow', artistMbid] as const,
|
|
releaseProfiles: () => ['releaseProfiles'] as const,
|
|
// Search and grab (M502 #5521).
|
|
wantCandidates: (wantId: string) => ['wantCandidates', wantId] as const,
|
|
searchSettings: () => ['searchSettings'] as const,
|
|
// The Wanted view's Activity and Blocked tabs (#5522).
|
|
downloads: () => ['adminWants', 'downloads'] as const,
|
|
releaseBlocks: () => ['adminWants', 'releaseBlocks'] 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,
|
|
fingerprintCoverage: () => ['fingerprintCoverage'] 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,
|
|
adminDuplicates: (view: string = 'review') =>
|
|
['adminDuplicates', { view }] as const,
|
|
adminResolvedDuplicates: () => ['adminResolvedDuplicates'] as const,
|
|
adminSuspectSources: () => ['adminSuspectSources'] as const,
|
|
adminDiagnosticDevices: (userId?: string) =>
|
|
['adminDiagnosticDevices', { userId: userId ?? 'all' }] as const,
|
|
smtpConfig: () => ['smtpConfig'] as const,
|
|
// Notifications inbox (M489). Both keys sit under ['notifications'] so one
|
|
// prefix invalidation refreshes the list and the badge together.
|
|
notifications: () => ['notifications', 'list'] as const,
|
|
notificationsUnread: () => ['notifications', 'unread'] as const,
|
|
notificationSettings: () => ['notificationSettings'] 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
|
|
});
|
|
}
|