import { invoke } from '@tauri-apps/api/core'; import { useAuthStore } from '../store/authStore'; import { coverIndexKeyFromRef, coverStorageKeyFromRef } from '../cover/storageKeys'; import { connectBaseUrlForServer } from '../utils/server/serverEndpoint'; import { serverIndexKeyForProfile } from '../utils/server/serverIndexKey'; import { getPlaybackServerId } from '../utils/playback/playbackServer'; import { restBaseFromUrl } from './subsonicClient'; import type { CoverArtRef, CoverArtTier } from '../cover/types'; /** Library SQLite `track.server_id` uses host index keys, not auth profile UUIDs. */ export function librarySqlServerId(profileOrIndexServerId: string): string { const server = useAuthStore.getState().servers.find(s => s.id === profileOrIndexServerId); if (server) return serverIndexKeyForProfile(server); return profileOrIndexServerId; } /** Host root for Rust `build_cover_art_url` (`{host}/rest/getCoverArt.view`). */ export function coverCacheRestHost(serverUrl: string): string { return restBaseFromUrl(serverUrl).replace(/\/rest$/i, ''); } export type CoverCacheEnsureResult = { hit: boolean; path: string; tier: CoverArtTier; }; export type CoverCacheStats = { bytes: number; count: number; pressure: 'ok' | 'pressure' | 'full'; autoDownloadEnabled: boolean; entryCount: number; }; export type CoverPipelineQueueStatsDto = { httpMax: number; httpActive: number; cpuUiMax: number; cpuUiActive: number; cpuBackfillMax: number; cpuBackfillActive: number; libraryBackfillHttpMax: number; libraryBackfillHttpActive: number; libraryBackfillPassRunning: boolean; uiEnsuredTotal: number; }; let coverAutoDownloadEnabled = true; export function setCoverCacheAutoDownloadEnabled(enabled: boolean): void { coverAutoDownloadEnabled = enabled; } function ensureArgsFromRef(ref: CoverArtRef, tier: CoverArtTier) { const { getBaseUrl, getActiveServer } = useAuthStore.getState(); const scope = ref.serverScope; if (scope.kind === 'server') { // scope.url is the index-stable primary; the Rust cover fetcher needs // the runtime connect URL (LAN or public, whichever currently answers). return { serverIndexKey: coverIndexKeyFromRef(ref), cacheKind: ref.cacheKind, cacheEntityId: ref.cacheEntityId, coverArtId: ref.fetchCoverArtId, tier, restBaseUrl: coverCacheRestHost( connectBaseUrlForServer({ id: scope.serverId, url: scope.url }), ), username: scope.username, password: scope.password, }; } const server = scope.kind === 'playback' ? (() => { const playbackServerId = getPlaybackServerId(); if (playbackServerId) { const playbackServer = useAuthStore .getState() .servers.find(s => s.id === playbackServerId); if (playbackServer) return playbackServer; } return getActiveServer(); })() : getActiveServer(); const baseUrl = server ? connectBaseUrlForServer(server) : getBaseUrl(); return { serverIndexKey: coverIndexKeyFromRef(ref), cacheKind: ref.cacheKind, cacheEntityId: ref.cacheEntityId, coverArtId: ref.fetchCoverArtId, tier, restBaseUrl: baseUrl ? coverCacheRestHost(baseUrl) : '', username: server?.username ?? '', password: server?.password ?? '', }; } export type CoverCachePeekItem = { serverIndexKey: string; cacheKind: 'album' | 'artist'; cacheEntityId: string; tier: CoverArtTier; storageKey: string; }; /** Disk-only — no HTTP. Returns map storageKey → absolute .webp path. */ export async function coverCachePeekBatch( refs: CoverArtRef[], tier: CoverArtTier, ): Promise> { if (refs.length === 0) return {}; const items: CoverCachePeekItem[] = refs.map(ref => ({ serverIndexKey: coverIndexKeyFromRef(ref), cacheKind: ref.cacheKind, cacheEntityId: ref.cacheEntityId, tier, storageKey: coverStorageKeyFromRef(ref, tier), })); return invoke>('cover_cache_peek_batch', { items }); } export async function coverCacheEnsure( ref: CoverArtRef, tier: CoverArtTier, _priority?: string, ): Promise { return invoke('cover_cache_ensure', { args: ensureArgsFromRef(ref, tier), }); } export async function coverCacheEnsureBatch( refs: CoverArtRef[], tier: CoverArtTier, _priority?: string, ): Promise { if (refs.length === 0) return; const items = refs.map(ref => ensureArgsFromRef(ref, tier)); await invoke('cover_cache_ensure_batch', { items }); } export async function coverCacheStats(): Promise { const stats = await invoke('cover_cache_stats', {}); setCoverCacheAutoDownloadEnabled(stats.autoDownloadEnabled); return stats; } /** Clears all servers (legacy). Prefer `coverCacheClearServer`. */ export async function coverCacheClear(): Promise { return invoke('cover_cache_clear', {}); } export async function coverCacheClearServer(serverIndexKey: string): Promise { return invoke('cover_cache_clear_server', { serverIndexKey }); } export async function coverCacheStatsServer( serverIndexKey: string, ): Promise> { const stats = await invoke('cover_cache_stats_server', { serverIndexKey }); return { bytes: stats.bytes, entryCount: stats.entryCount }; } export function coverGetPipelineQueueStats(): Promise { return invoke('cover_cache_get_pipeline_queue_stats'); } export async function libraryCoverBackfillBatch( serverIndexKey: string, libraryServerId: string, cursor?: string | null, limit?: number, ): Promise<{ coverIds: string[]; nextCursor: string | null; exhausted: boolean }> { const sqlServerId = librarySqlServerId(libraryServerId); const diskKey = serverIndexKey || sqlServerId; return invoke('library_cover_backfill_batch', { serverIndexKey: diskKey, libraryServerId: sqlServerId, cursor, limit, }); } export async function libraryCoverProgress( serverIndexKey: string, libraryServerId: string, ): Promise<{ totalDistinct: number; pending: number; done: number }> { const sqlServerId = librarySqlServerId(libraryServerId); const diskKey = serverIndexKey || sqlServerId; return invoke('library_cover_progress', { serverIndexKey: diskKey, libraryServerId: sqlServerId, }); } export type LibraryCoverBackfillConfigureArgs = { enabled: boolean; serverIndexKey: string; libraryServerId: string; restBaseUrl: string; username: string; password: string; }; export async function libraryCoverBackfillConfigure( args: LibraryCoverBackfillConfigureArgs, ): Promise { return invoke('library_cover_backfill_configure', args); } /** * Push the current reachable connect URL to the native backfill worker without * rebuilding the session. The worklist is URL-agnostic; each fetch reads this * value live, so a LAN→public flip is honoured by the in-flight pass too. A real * change clears the stale fetch-failed backoff and kicks a retry pass. */ export async function libraryCoverBackfillSetBaseUrl(restBaseUrl: string): Promise { return invoke('library_cover_backfill_set_base_url', { restBaseUrl }); } export type CoverBackfillPulseResult = { scheduled: number; exhausted: boolean; pending: number; done: number; total: number; status: 'idle' | 'active' | 'blocked_sync' | 'blocked_pressure' | 'disabled' | string; }; /** One backfill step (legacy); prefer `libraryCoverBackfillRunFullPass`. */ export async function libraryCoverBackfillPulse(): Promise { return invoke('library_cover_backfill_pulse'); } /** * Start one full-catalog pass on the native runtime (works when the window is inactive). * `force` bypasses the idle gate and clears the fetch-failed backoff so previously * unfetchable (404) covers are retried — used by the manual "Run full pass now". */ export async function libraryCoverBackfillRunFullPass( force = false, ): Promise<{ started: boolean }> { return invoke<{ started: boolean }>('library_cover_backfill_run_full_pass', { force }); } export async function libraryCoverBackfillResetCursor(): Promise { return invoke('library_cover_backfill_reset_cursor'); } /** Yield native library backfill while the user navigates (visible covers first). */ export async function libraryCoverBackfillSetUiPriority(hold: boolean): Promise { return invoke('library_cover_backfill_set_ui_priority', { hold }); } /** Perf-probe only: retune cover backfill threads (download + encode). Returns the clamped value applied. */ export async function libraryCoverBackfillSetParallel(threads: number): Promise { return invoke('library_cover_backfill_set_parallel', { threads }); } export async function libraryCoverClearFetchFailures(serverIndexKey: string): Promise { return invoke('library_cover_clear_fetch_failures', { serverIndexKey }); } export async function libraryCoverCatalogSize(libraryServerId: string): Promise { return invoke('library_cover_catalog_size', { libraryServerId: librarySqlServerId(libraryServerId), }); } export function coverCacheMayBackgroundDownload(): boolean { return coverAutoDownloadEnabled; }