// Offline catalog service - "browse & queue" for offline mode. // // Two responsibilities: // 1. Full-catalog pre-sync: while online, walk every library and persist all // items to the offline cache so the whole catalog is browsable (greyed out) // offline. Backed by the `syncFullCatalog` Rust command. // 2. Resume-on-reconnect: when the server becomes reachable again, resolve the // stream URLs of downloads that were queued while offline and let the pump // start them. Backed by `resumeQueuedDownloads`. // // It also owns the `showServerCatalog` UI flag (the offline banner toggle that // reveals greyed-out, non-downloaded server media). // // TRACES: UR-002, UR-052 | DR-078 import { writable, type Writable } from "svelte/store"; import { commands } from "$lib/api/bindings"; import { auth } from "$lib/stores/auth"; import { isConnected } from "$lib/stores/connectivity"; /** * When true (and offline), library grids reveal greyed-out versions of media * that exists on the server but isn't downloaded, each with a button to queue * a download for the next reconnect. Toggled from the offline banner. */ export const showServerCatalog: Writable = writable(false); // Keep the backend's offline library queries in sync with the UI toggle. The // offline cache holds the whole synced catalog, so `get_items` would otherwise // return every server item even offline with the toggle off. Include the // non-downloaded catalog only when online (fast browsing reads the same cache) // or when the "Show all server media" toggle is on. let lastIncludeCatalog: boolean | null = null; function pushCatalogVisibility(connected: boolean, showCatalog: boolean): void { const include = connected || showCatalog; if (include === lastIncludeCatalog) return; lastIncludeCatalog = include; commands.setShowServerCatalog(include).catch((err) => { console.warn("[OfflineCatalog] Failed to set catalog visibility:", err); }); } let connectedNow = true; let showCatalogNow = false; isConnected.subscribe((v) => { connectedNow = v; pushCatalogVisibility(connectedNow, showCatalogNow); }); showServerCatalog.subscribe((v) => { showCatalogNow = v; pushCatalogVisibility(connectedNow, showCatalogNow); }); /** Last time a full catalog sync completed, for a UI hint. */ export const lastCatalogSync: Writable = writable(null); // Guard against overlapping syncs (they can be slow on large libraries). let syncInProgress = false; function currentHandle(): string | null { try { return auth.getRepository().getHandle(); } catch { return null; // not connected yet } } /** * Force a full re-index now, ignoring freshness. * * Routine scheduling is the Rust indexer's job (DR-109) — this is the manual * override, for a "re-index now" affordance. It is deliberately *not* called on * startup or reconnect any more: doing so forced a full crawl on every launch * regardless of how fresh the index was. * * The backend refuses overlapping passes, so this is safe to call at any time. */ export async function syncCatalog(): Promise { if (syncInProgress) return; const handle = currentHandle(); if (!handle) return; syncInProgress = true; try { const result = await commands.syncFullCatalog(handle); console.info( `[OfflineCatalog] Synced ${result.itemsCached} items (${result.librariesFailed} libraries failed)` ); await refreshSyncStatus(); } catch (err) { console.warn("[OfflineCatalog] Full catalog sync failed:", err); } finally { syncInProgress = false; } } /** * Resolve URLs for downloads queued while offline and pump them. Call on * reconnect. No-ops if not connected. */ export async function resumeQueued(): Promise { const handle = currentHandle(); if (!handle) return; try { const result = await commands.resumeQueuedDownloads(handle); if (result.resolved > 0 || result.failed > 0) { console.info( `[OfflineCatalog] Resumed queued downloads: ${result.resolved} resolved, ${result.failed} failed` ); } } catch (err) { console.warn("[OfflineCatalog] Failed to resume queued downloads:", err); } } /** Refresh the last-synced timestamp from the backend. */ export async function refreshSyncStatus(): Promise { try { const status = await commands.catalogSyncStatus(); lastCatalogSync.set(status.lastSyncedAt ?? null); } catch (err) { console.debug("[OfflineCatalog] Failed to fetch sync status:", err); } } /** * Called when the server becomes reachable again: resume queued downloads first * (fast, user-visible), then refresh the catalog in the background. */ export async function onReconnected(): Promise { await resumeQueued(); // Re-indexing on reconnect is the Rust indexer's job (DR-109) — it re-checks // staleness every tick, so it picks this up without a nudge from here. Queued // downloads still need resolving from the frontend, which is why this // function remains. }