import { html, render as litRender } from "lit-html"; import * as Output from "~/common/output.js"; import foundation from "~/common/foundation.js"; import { effect, signal } from "~/common/signal.js"; import { SCHEME as SCHEME_DROPBOX } from "~/components/input/dropbox/constants.js"; import { SCHEME as SCHEME_EPHEMERAL_CACHE } from "~/components/input/ephemeral-cache/constants.js"; import { SCHEME as SCHEME_HTTPS } from "~/components/input/https/constants.js"; import { SCHEME as SCHEME_ICECAST } from "~/components/input/icecast/constants.js"; import { SCHEME as SCHEME_LOCAL } from "~/components/input/local/constants.js"; import { SCHEME as SCHEME_OPENSUBSONIC } from "~/components/input/opensubsonic/constants.js"; import { SCHEME as SCHEME_S3 } from "~/components/input/s3/constants.js"; /** @type {Record} */ const SCHEME_NAMES = { [SCHEME_DROPBOX]: "Dropbox", [SCHEME_EPHEMERAL_CACHE]: "Browser storage", [SCHEME_HTTPS]: "HTTPS", [SCHEME_ICECAST]: "Icecast", [SCHEME_LOCAL]: "Local directories & files", [SCHEME_OPENSUBSONIC]: "OpenSubsonic", [SCHEME_S3]: "S3", }; foundation.setup({ title: "Sources | Diffuse" }); //////////////////////////////////////////// // SETUP //////////////////////////////////////////// const [ inputConfigurator, sourcesOrchestrator, outputOrchestrator, processOrchestrator, ] = await Promise.all([ foundation.configurator.input(), foundation.orchestrator.sources(), foundation.orchestrator.output(), foundation.orchestrator.processTracks({ disableWhenReady: true }), ]); await Promise.all([ customElements.whenDefined(inputConfigurator.localName), customElements.whenDefined(sourcesOrchestrator.localName), customElements.whenDefined(outputOrchestrator.localName), ]); //////////////////////////////////////////// // PROCESS BUTTON //////////////////////////////////////////// const processBtn = /** @type {HTMLButtonElement} */ (document.querySelector("#process-btn")); const processIcon = /** @type {HTMLElement} */ (document.querySelector("#process-icon")); const processLabel = /** @type {HTMLElement} */ (document.querySelector("#process-label")); effect(() => { const isProcessing = processOrchestrator.isProcessing(); const { processed, total } = processOrchestrator.progress(); const pct = total > 0 ? Math.round((processed / total) * 100) : null; processBtn.disabled = isProcessing; processIcon.className = isProcessing ? "ph-fill ph-arrows-clockwise animate-spin" : "ph-fill ph-arrows-clockwise"; processLabel.textContent = isProcessing ? (pct !== null ? `Processing (${pct}%)` : "Listing") : "Process"; }); processBtn.addEventListener("click", async () => { const output = await foundation.orchestrator.output(); await Output.data(output.tracks); await processOrchestrator.process(); }); //////////////////////////////////////////// // UI //////////////////////////////////////////// const list = /** @type {HTMLElement} */ (document.querySelector("#sources-list")); const empty = /** @type {HTMLElement} */ (document.querySelector("#sources-empty")); /** @param {string} uri */ const trackPrefix = (uri) => { const q = uri.indexOf("?"); return q === -1 ? uri : uri.slice(0, q); }; //////////////////////////////////////////// // ONLINE STATUS //////////////////////////////////////////// const onlineMap = signal(/** @type {Record} */ ({})); /** @param {{ [scheme: string]: import("~/components/input/types.d.ts").Source[] }} sourcesRecord */ async function checkOnlineStatus(sourcesRecord) { const sources = Object.values(sourcesRecord).flat(); const entries = await Promise.all( sources.map(async ({ uri }) => { const result = await inputConfigurator.consult(uri); const online = result.supported && result.consult !== "undetermined" ? result.consult : null; return /** @type {[string, boolean | null]} */ ([trackPrefix(uri), online]); }), ); onlineMap.value = Object.fromEntries(entries); } effect(() => { checkOnlineStatus(sourcesOrchestrator.sources()); }); effect(() => { const sourcesRecord = sourcesOrchestrator.sources(); const statusMap = onlineMap.get(); const tracksCol = outputOrchestrator.tracks.collection(); const tracks = tracksCol.state === "loaded" ? tracksCol.data : []; /** @param {string} uri */ const statusClass = (uri) => { const status = statusMap[trackPrefix(uri)]; if (status === true) return "sources-item__status--online"; if (status === false) return "sources-item__status--offline"; return "sources-item__status--unknown"; }; /** @param {string} uri */ const statusTitle = (uri) => { const status = statusMap[trackPrefix(uri)]; if (status === true) return "Online"; if (status === false) return "Offline"; return "Status unknown"; }; const entries = Object.entries(sourcesRecord).filter( ([, sources]) => sources.length > 0, ); list.hidden = entries.length === 0; empty.hidden = entries.length > 0; litRender( html` ${entries.map(([scheme, sources]) => { if (scheme === SCHEME_EPHEMERAL_CACHE) { const uri = `${SCHEME_EPHEMERAL_CACHE}://`; const isDisabled = sourcesOrchestrator.isDisabled(uri); const trackCount = tracks.filter((t) => t.uri.startsWith(uri)).length; return html`
  • ${SCHEME_NAMES[scheme] ?? scheme}
  • Files stored in the browser ${trackCount} track${trackCount === 1 ? "" : "s"}
  • `; } return html`
  • ${SCHEME_NAMES[scheme] ?? scheme}
  • ${sources.map(({ label, uri }) => { const isDisabled = sourcesOrchestrator.isDisabled(uri); const trackCount = tracks.filter((t) => t.uri.startsWith(trackPrefix(uri)) ).length; return html`
  • ${label} ${trackCount} track${trackCount === 1 ? "" : "s"}
  • `; })} `; })} `, list, ); }); //////////////////////////////////////////// // ACTIONS //////////////////////////////////////////// async function removeEphemeralSources() { return removeSource(SCHEME_EPHEMERAL_CACHE); } /** @param {string} uri */ async function removeSource(uri) { const tracks = await Output.data(outputOrchestrator.tracks); const detachedTracks = await inputConfigurator.detach({ fileUriOrScheme: uri, tracks, }); if (detachedTracks) await outputOrchestrator.tracks.save(detachedTracks); } //////////////////////////////////////////// // 🚀 //////////////////////////////////////////// foundation.ready();