102 lines
3.3 KiB
JavaScript
102 lines
3.3 KiB
JavaScript
// 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;
|
|
}
|