// Shared, DOM-free job/library helpers used by both the mobile UI and // (incrementally) the desktop catalog. Anything in here must stay free of // document/window-element access so it can be imported from either entry // point. The API (`GET /api/jobs`) is the canonical data source. import { fmtTime } from "../utils.js"; // Mirror of catalog.js PROCESSING_STATUSES — keep in sync with the backend // job lifecycle. These map to the amber "working" dot. const PROCESSING_STATUSES = new Set(["queued", "downloading", "analyzing", "separating", "processing"]); export async function fetchJobs() { const res = await fetch("/api/jobs", { cache: "no-store" }); if (!res.ok) throw new Error(`GET /api/jobs -> ${res.status}`); return res.json(); } export function deriveSource(sourceUrl) { if (!sourceUrl) return "Local file"; if (sourceUrl.startsWith("local:")) return "Local file"; if (sourceUrl.includes("youtube.com") || sourceUrl.includes("youtu.be")) return "YouTube"; if (sourceUrl.includes("soundcloud.com")) return "SoundCloud"; return "Web"; } // One of: "done" (green), "processing" (amber), "unavailable" (grey/red). export function statusKind(status) { if (PROCESSING_STATUSES.has(status)) return "processing"; if (status === "done") return "done"; return "unavailable"; // unavailable / error / failed / unknown } export function coverInitial(title) { const t = (title || "").trim(); return t ? t[0].toUpperCase() : "♪"; } // Deterministic cover gradient: the same track always gets the same color. // Palette matches the mobile design's aesthetic (design/mobile prototype). const GRADIENTS = [ "linear-gradient(150deg,#7b46f0,#3f6ef0 55%,#1b245f)", "linear-gradient(140deg,#2bd4c4,#1a6d9e)", "linear-gradient(150deg,#c44ad0,#5a2b9e)", "linear-gradient(150deg,#f06a6a,#7a2b4e)", "linear-gradient(150deg,#4a9bf5,#2b3a7a)", "linear-gradient(150deg,#f5a516,#d2541f)", "linear-gradient(150deg,#3fcf6e,#1a6d4e)", ]; export function coverGradient(seed) { const s = String(seed || ""); let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return GRADIENTS[h % GRADIENTS.length]; } // Normalize an /api/jobs state object into the shape the mobile UI renders. export function jobToCard(state) { const id = state.job_id; const title = state.title || "Untitled"; const stemCount = (state.stems || state.selected_stems || []).length; const parts = []; if (state.duration) parts.push(fmtTime(state.duration)); if (stemCount) parts.push(`${stemCount} stem${stemCount === 1 ? "" : "s"}`); return { id, title, sub: deriveSource(state.source_url), meta: parts.join(" · "), stemCount, status: statusKind(state.status), createdAt: state.created_at || 0, initial: coverInitial(title), gradient: coverGradient(id || title), thumb: typeof state.thumbnail === "string" ? state.thumbnail : "", }; }