import { createQuery } from '@tanstack/svelte-query'; import { api } from './client'; import { qk } from './queries'; import type { ActionResult, AdminQuarantineRow, LidarrConfig, LidarrMetadataProfile, LidarrQualityProfile, LidarrQuarantineActionRow, LidarrRequest, LidarrRequestStatus, LidarrRootFolder, LidarrTestResult } from './types'; // Admin Lidarr config ----------------------------------------------------- export async function getLidarrConfig(): Promise { return api.get('/api/admin/lidarr/config'); } export async function putLidarrConfig(cfg: LidarrConfig): Promise { return api.put('/api/admin/lidarr/config', cfg); } // testLidarrConnection always returns 200 with a discriminated-union body — // callers branch on `result.ok`. We do NOT throw on `ok:false`; the SPA wants // to render either branch (e.g. "connected to Lidarr X.Y.Z" vs "auth failed"). export async function testLidarrConnection( body: { base_url?: string; api_key?: string } = {} ): Promise { return api.post('/api/admin/lidarr/test', body); } export async function listQualityProfiles(): Promise { return api.get('/api/admin/lidarr/quality-profiles'); } export async function listMetadataProfiles(): Promise { return api.get('/api/admin/lidarr/metadata-profiles'); } export async function listRootFolders(): Promise { return api.get('/api/admin/lidarr/root-folders'); } // Admin request queue ----------------------------------------------------- export async function listAdminRequests( status?: LidarrRequestStatus, limit?: number ): Promise { const params = new URLSearchParams(); if (status) params.set('status', status); if (limit !== undefined) params.set('limit', String(limit)); const qs = params.toString(); return api.get( qs ? `/api/admin/requests?${qs}` : '/api/admin/requests' ); } export async function approveRequest( id: string, overrides: { quality_profile_id?: number; root_folder_path?: string } = {} ): Promise { return api.post(`/api/admin/requests/${id}/approve`, overrides); } export async function rejectRequest( id: string, notes?: string ): Promise { const body = notes !== undefined ? { notes } : {}; return api.post(`/api/admin/requests/${id}/reject`, body); } // Query factories --------------------------------------------------------- export function createLidarrConfigQuery() { return createQuery({ queryKey: qk.lidarrConfig(), queryFn: getLidarrConfig, staleTime: 60_000 }); } // `enabled` is passed in by the caller — typically derived from // LidarrConfig.enabled — so the query only fires once Lidarr is configured. // Keeping it as a prop (vs. reading config inside this factory) preserves // purity and lets the caller choose its own gating logic. export function createQualityProfilesQuery(enabled: boolean = true) { return createQuery({ queryKey: qk.lidarrQualityProfiles(), queryFn: listQualityProfiles, enabled }); } export function createMetadataProfilesQuery(enabled: boolean = true) { return createQuery({ queryKey: qk.lidarrMetadataProfiles(), queryFn: listMetadataProfiles, enabled }); } export function createRootFoldersQuery(enabled: boolean = true) { return createQuery({ queryKey: qk.lidarrRootFolders(), queryFn: listRootFolders, enabled }); } export function createAdminRequestsQuery(status?: LidarrRequestStatus) { return createQuery({ queryKey: qk.adminRequests(status), queryFn: () => listAdminRequests(status) }); } // Admin quarantine -------------------------------------------------------- export async function listAdminQuarantine(): Promise { return api.get('/api/admin/quarantine'); } export async function resolveQuarantine(trackID: string): Promise { return api.post(`/api/admin/quarantine/${trackID}/resolve`, {}); } export async function deleteQuarantineFile(trackID: string): Promise { return api.post(`/api/admin/quarantine/${trackID}/delete-file`, {}); } export async function deleteQuarantineViaLidarr(trackID: string): Promise { return api.post(`/api/admin/quarantine/${trackID}/delete-via-lidarr`, {}); } export async function listQuarantineActions( limit: number = 50 ): Promise { return api.get( `/api/admin/quarantine/actions?limit=${limit}` ); } export function createAdminQuarantineQuery() { return createQuery({ queryKey: qk.adminQuarantine(), queryFn: listAdminQuarantine, staleTime: 30_000 }); } export function createQuarantineActionsQuery(limit: number = 50) { return createQuery({ queryKey: qk.adminQuarantineActions(limit), queryFn: () => listQuarantineActions(limit), staleTime: 60_000 }); } // Admin cover art --------------------------------------------------------- export type RefetchAlbumCoverResponse = { album_id: string; cover_art_path: string | null; cover_art_source: string | null; }; export async function refetchAlbumCover(albumId: string): Promise { return api.post(`/api/admin/albums/${albumId}/cover/refetch`, {}); } export type RefetchMissingResponse = { queued: number; }; export async function refetchMissingCovers(): Promise { return api.post('/api/admin/covers/refetch-missing', {}); } // Library scan ------------------------------------------------------------- export type ScanStageLibrary = { scanned: number; added: number; updated: number; skipped: number; errored: number; }; export type ScanStageMbidBackfill = { processed: number; healed: number; skipped: number; duplicates?: number; }; export type ScanStageCoverEnrich = { processed: number; succeeded: number; failed: number; }; export type ScanStageArtistArtEnrich = { processed: number; succeeded: number; failed: number; }; export type ScanStatus = { id: string; started_at: string; finished_at: string | null; library?: ScanStageLibrary; mbid_backfill?: ScanStageMbidBackfill; cover_enrich?: ScanStageCoverEnrich; artist_art_enrich?: ScanStageArtistArtEnrich; error_message?: string; in_flight: boolean; }; export async function getScanStatus(): Promise { return api.get('/api/admin/scan/status'); } export type TriggerScanResp = { id?: string }; export async function triggerScan(): Promise { return api.post('/api/admin/scan/run', {}); } // Polls every 3s while a scan is in flight; falls back to a 30s stale-time // when idle so the section auto-updates as workers complete each stage. export function createScanStatusQuery() { return createQuery({ queryKey: qk.scanStatus(), queryFn: getScanStatus, staleTime: 30_000, refetchInterval: 3_000 }); } // Library coverage -------------------------------------------------------- export type CoverageRollup = { total: number; with_art: number; pending: number; settled: number; pending_no_mbid: number; }; export async function getCoverageRollup(): Promise { return api.get('/api/admin/library/coverage'); } // Same pacing as scan-status — 3s while polling, 30s stale-time. The // query is cheap server-side (one aggregate per call) and we want // the gauge to tick during a scan. export function createCoverageQuery() { return createQuery({ queryKey: qk.coverage(), queryFn: getCoverageRollup, staleTime: 30_000, refetchInterval: 3_000 }); } // Cover-art providers ------------------------------------------------------ export type CoverProviderCapability = 'album_cover' | 'artist_thumb' | 'artist_fanart'; export type CoverProvider = { id: string; display_name: string; requires_api_key: boolean; supports: CoverProviderCapability[]; enabled: boolean; api_key_set: boolean; display_order: number; testable: boolean; }; export type CoverProvidersResponse = { providers: CoverProvider[]; sources_version: number; }; export async function getCoverProviders(): Promise { return api.get('/api/admin/cover-sources'); } export type UpdateCoverProviderPatch = { enabled?: boolean; api_key?: string; }; export type UpdateCoverProviderResponse = CoverProvider & { version_bumped: boolean; }; export async function updateCoverProvider( id: string, patch: UpdateCoverProviderPatch ): Promise { return api.patch(`/api/admin/cover-sources/${id}`, patch); } export type TestCoverProviderResponse = { ok: boolean; duration_ms?: number; error?: string; }; export async function testCoverProvider(id: string): Promise { return api.post(`/api/admin/cover-sources/${id}/test`, {}); } // Re-search missing art -------------------------------------------------- export type ResearchMissingArtResponse = { version: number }; // Bumps cover_art_sources_meta.current_version. Every 'none' row becomes // eligible for retry against the current provider chain on the next // enrichment pass. Existing positively-sourced rows are not affected. export async function researchMissingArt(): Promise { return api.post('/api/admin/cover-sources/research', {}); } // Settings rarely change in operator time. 60s staleTime; refetches // on window focus + after any mutation rather than polling. export function createCoverProvidersQuery() { return createQuery({ queryKey: qk.coverProviders(), queryFn: getCoverProviders, staleTime: 60_000 }); } // Scan schedule ----------------------------------------------------------- export type ScanScheduleMode = 'off' | 'interval' | 'daily' | 'weekly'; export type ScanSchedule = { mode: ScanScheduleMode; interval_hours: number | null; time_of_day: string | null; // 'HH:MM' weekly_day: number | null; // 1..7 (Mon..Sun, ISO 8601) next_scheduled_at: string | null; // RFC3339 ISO timestamp; null when mode='off' }; export type ScanScheduleUpdate = { mode: ScanScheduleMode; interval_hours?: number | null; time_of_day?: string | null; weekly_day?: number | null; }; export async function getScanSchedule(): Promise { return api.get('/api/admin/scan/schedule'); } export async function updateScanSchedule(patch: ScanScheduleUpdate): Promise { return api.patch('/api/admin/scan/schedule', patch); } // Settings rarely change in operator time. 60s staleTime; refetches // on window focus + after any PATCH invalidation — no polling. export function createScanScheduleQuery() { return createQuery({ queryKey: qk.scanSchedule(), queryFn: getScanSchedule, staleTime: 60_000 }); } // Admin user-management ------------------------------------------------------ export type AdminUser = { id: string; username: string; display_name: string | null; is_admin: boolean; auto_approve_requests: boolean; created_at: string; }; export type AdminInvite = { token: string; invited_by: string; note: string | null; created_at: string; expires_at: string; redeemed_at: string | null; redeemed_by: string | null; }; export async function listUsers(): Promise { const res = await api.get<{ users: AdminUser[] }>('/api/admin/users'); return res.users; } export async function updateUserAdmin(id: string, isAdmin: boolean): Promise { return api.put(`/api/admin/users/${id}/admin`, { is_admin: isAdmin }); } export async function listInvites(): Promise { const res = await api.get<{ invites: AdminInvite[] }>('/api/admin/invites'); return res.invites; } export async function createInvite(note?: string): Promise { return api.post('/api/admin/invites', note ? { note } : {}); } export async function deleteInvite(token: string): Promise { await api.del(`/api/admin/invites/${token}`); } // U2 user-management actions ----------------------------------------------- export type CreateUserInput = { username: string; password: string; display_name?: string; is_admin?: boolean; }; export async function createUser(input: CreateUserInput): Promise { return api.post('/api/admin/users', input); } export async function deleteUser(id: string): Promise { await api.del(`/api/admin/users/${id}`); } export async function resetUserPassword(id: string, password: string): Promise { await api.post(`/api/admin/users/${id}/reset-password`, { password }); } export async function updateUserAutoApprove(id: string, autoApprove: boolean): Promise { return api.put(`/api/admin/users/${id}/auto-approve`, { auto_approve: autoApprove }); } export function createAdminUsersQuery() { return createQuery({ queryKey: qk.adminUsers(), queryFn: listUsers }); } export function createAdminInvitesQuery() { return createQuery({ queryKey: qk.adminInvites(), queryFn: listInvites }); } // SMTP config (U3) ---------------------------------------------------------- export type SMTPConfig = { enabled: boolean; host: string; port: number; username: string; password: string; // "***" when set, "" when unset from_address: string; from_name: string; use_tls: boolean; }; export async function getSMTPConfig(): Promise { return api.get('/api/admin/smtp-config'); } export async function updateSMTPConfig(input: SMTPConfig): Promise { await api.put('/api/admin/smtp-config', input); } export async function testSMTPConfig(): Promise { await api.post('/api/admin/smtp-config/test', {}); } export function createSMTPConfigQuery() { return createQuery({ queryKey: qk.smtpConfig(), queryFn: getSMTPConfig, staleTime: 60_000 }); }