dj sets + lyrics: a mix that names its own tracks, and words pulled off the separated voice
This commit is contained in:
1 parent
4efe85b6bf
commit
1dd8573808
41 files changed
+9288
-647
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user