// Background DJ-set listening. A scan OUTLIVES the import modal: starting a // listen closes the modal, progress rides a global chip above the player // bar, the app stays fully usable, and the finished draft waits on the chip // until it is reviewed or dismissed. One scan at a time — the backend's // cancel flag is global by design. import * as api from "./api.js"; export const setlistScan = $state({ active: false, mode: null, // "youtube" | "file" label: "", stage: null, // "tool" | "runtime" | "audio" | "decode" | "listen" window: 0, total: 0, pct: null, hit: null, draft: null, // finished ImportDraft awaiting review error: null, wantsReview: false, // chip clicked; the Playlists view consumes this }); let subscribed = false; function subscribe() { if (subscribed) return; subscribed = true; api.onEvent("playlist:scan-progress", (p) => { // A ready chip must not be mutated by stray late events. if (!setlistScan.active || setlistScan.draft || !p?.stage) return; setlistScan.stage = p.stage; if (p.window != null) setlistScan.window = p.window; if (p.total != null) setlistScan.total = p.total; setlistScan.pct = p.pct ?? null; if (p.hit != null) setlistScan.hit = p.hit; }); } async function run(mode, label, call) { subscribe(); Object.assign(setlistScan, { active: true, mode, label, stage: mode === "youtube" ? "audio" : "decode", window: 0, total: 0, pct: null, hit: null, draft: null, error: null, wantsReview: false, }); try { const draft = await call(); if (draft) { setlistScan.draft = draft; setlistScan.label = draft.name ?? label; } else { // Defensive: a null draft must not wedge the chip. setlistScan.active = false; } } catch (e) { const message = String(e); if (message.includes("canceled")) setlistScan.active = false; else setlistScan.error = message; } } /** Kick a YouTube listen into the background. False = one already runs. */ export function startYoutubeListen(url) { if (setlistScan.active) return false; void run("youtube", "YouTube set", () => api.playlistScanYoutube(url)); return true; } /** Listen to an already-picked set recording in the background, with an * optionally pasted tracklist riding along. False = one already runs. */ export function startFileListen(path, tracklist = null, label = "Set recording") { if (setlistScan.active) return false; void run("file", label, () => api.playlistScanFile(path, tracklist)); return true; } /** Hand the finished draft to the review and clear the chip. */ export function takeScanDraft() { const draft = setlistScan.draft; setlistScan.draft = null; setlistScan.active = false; setlistScan.wantsReview = false; return draft; } /** Cancel a running scan, or drop a finished/failed one unseen. */ export function dismissScan() { if (setlistScan.active && !setlistScan.draft && !setlistScan.error) { void api.playlistScanCancel(); } setlistScan.active = false; setlistScan.draft = null; setlistScan.error = null; setlistScan.wantsReview = false; }