dj sets + lyrics: a mix that names its own tracks, and words pulled off the separated voice

This commit is contained in:
KeelrLeek committed 2026-08-24 10:49:10 -05:00
1 parent 4efe85b6bf
commit 1dd8573808
41 files changed
+9288 -647

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
// 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;
}