"use client"; import { useEffect, useState } from "react"; import { PageHead } from "../_ui/page-head"; type Loaded = { qobuzEmail: string; qobuzUserId: string; slskdUrl: string; lastfmUsername: string; qobuzPasswordSet: boolean; qobuzTokenSet: boolean; slskdApiKeySet: boolean; lastfmApiKeySet: boolean; }; type Tab = "Qobuz" | "Soulseek" | "Last.fm" | "Library" | "Monitor" | "Discovery"; const TABS: Tab[] = ["Qobuz", "Soulseek", "Last.fm", "Library", "Monitor", "Discovery"]; const DISCOVERY_FIELDS: [string, string][] = [ ["discover.intervalHours", "Interval (hours)"], ["discover.chunkSize", "Seeds per chunk"], ["discover.similarPerSeed", "Similar per seed"], ["discover.albumsPerArtist", "Albums per artist"], ["discover.minScore", "Min score"], ]; const MONITOR_FIELDS: [string, string][] = [ ["monitor.pollIntervalHours", "Poll interval (hours)"], ["monitor.retryIntervalHours", "Retry interval (hours)"], ["monitor.qualityCutoff", "Quality cutoff"], ["monitor.upgradeWindowDays", "Upgrade window (days)"], ]; export function SettingsForm() { const [tab, setTab] = useState("Qobuz"); const [qobuzEmail, setQobuzEmail] = useState(""); const [qobuzPassword, setQobuzPassword] = useState(""); const [qobuzUserId, setQobuzUserId] = useState(""); const [qobuzToken, setQobuzToken] = useState(""); const [slskdUrl, setSlskdUrl] = useState(""); const [slskdApiKey, setSlskdApiKey] = useState(""); const [lastfmUsername, setLastfmUsername] = useState(""); const [lastfmApiKey, setLastfmApiKey] = useState(""); const [pwSet, setPwSet] = useState(false); const [tokenSet, setTokenSet] = useState(false); const [keySet, setKeySet] = useState(false); const [lastfmKeySet, setLastfmKeySet] = useState(false); const [saved, setSaved] = useState(false); const [scanResult, setScanResult] = useState(null); const [scanRequested, setScanRequested] = useState(false); const [discoverCfg, setDiscoverCfg] = useState>({}); const [discoverSaved, setDiscoverSaved] = useState(false); const [monitorCfg, setMonitorCfg] = useState>({}); const [monitorSaved, setMonitorSaved] = useState(false); useEffect(() => { fetch("/api/scan") .then((r) => r.json()) .then((s: { result: string | null; requested: boolean }) => { setScanResult(s.result); setScanRequested(s.requested); }); }, []); async function requestScan() { await fetch("/api/scan", { method: "POST" }); setScanRequested(true); } useEffect(() => { if (!scanRequested) return; const id = setInterval(async () => { const s = await (await fetch("/api/scan")).json(); setScanResult(s.result); if (!s.requested) setScanRequested(false); }, 2000); return () => clearInterval(id); }, [scanRequested]); useEffect(() => { fetch("/api/config") .then((r) => r.json()) .then((c: Loaded) => { setQobuzEmail(c.qobuzEmail); setQobuzUserId(c.qobuzUserId); setSlskdUrl(c.slskdUrl); setLastfmUsername(c.lastfmUsername); setPwSet(c.qobuzPasswordSet); setTokenSet(c.qobuzTokenSet); setKeySet(c.slskdApiKeySet); setLastfmKeySet(c.lastfmApiKeySet); }); }, []); useEffect(() => { fetch("/api/discover/config") .then((r) => r.json()) .then((c: Record) => setDiscoverCfg(c)); }, []); useEffect(() => { fetch("/api/monitor/config") .then((r) => r.json()) .then((c: Record) => setMonitorCfg(c)); }, []); async function saveCredentials(e: React.FormEvent) { e.preventDefault(); await fetch("/api/config", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ qobuzEmail, qobuzPassword, qobuzUserId, qobuzToken, slskdUrl, slskdApiKey, lastfmApiKey, lastfmUsername, }), }); setQobuzPassword(""); setQobuzToken(""); setSlskdApiKey(""); setLastfmApiKey(""); setSaved(true); setTimeout(() => setSaved(false), 1500); setPwSet(pwSet || qobuzPassword !== ""); setTokenSet(tokenSet || qobuzToken !== ""); setKeySet(keySet || slskdApiKey !== ""); setLastfmKeySet(lastfmKeySet || lastfmApiKey !== ""); } function setCfg(key: string, value: string) { setDiscoverCfg((c) => ({ ...c, [key]: value })); } async function saveDiscovery(e: React.FormEvent) { e.preventDefault(); await fetch("/api/discover/config", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(discoverCfg), }); setDiscoverSaved(true); setTimeout(() => setDiscoverSaved(false), 1500); } function setMon(key: string, value: string) { setMonitorCfg((c) => ({ ...c, [key]: value })); } async function saveMonitor(e: React.FormEvent) { e.preventDefault(); await fetch("/api/monitor/config", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(monitorCfg), }); setMonitorSaved(true); setTimeout(() => setMonitorSaved(false), 1500); } const secretHint = (set: boolean) => (set ? · set : null); async function clearField(field: "qobuzPassword" | "qobuzToken" | "slskdApiKey" | "lastfmApiKey") { await fetch(`/api/config?field=${field}`, { method: "DELETE" }); if (field === "qobuzPassword") { setPwSet(false); setQobuzPassword(""); } if (field === "qobuzToken") { setTokenSet(false); setQobuzToken(""); } if (field === "slskdApiKey") { setKeySet(false); setSlskdApiKey(""); } if (field === "lastfmApiKey") { setLastfmKeySet(false); setLastfmApiKey(""); } } return (
{TABS.map((t) => ( ))}
{tab === "Qobuz" ? (
Email / password
Auth token (more reliable; overrides email/password)
{saved ? Saved : null}
) : null} {tab === "Soulseek" ? (
Soulseek (slskd)
{saved ? Saved : null}
) : null} {tab === "Last.fm" ? (
Last.fm
{saved ? Saved : null}
) : null} {tab === "Library" ? (
Music path   /music
Mounted read/write into the worker. To point Lyra at a different folder, set MUSIC_DIR in{" "} docker-compose.yml and rebuild — it can’t be remounted from here.

Scan the library to record what’s already on disk as owned + monitored.

{scanResult ? ( Last scan: {scanResult} ) : null}
) : null} {tab === "Monitor" ? (
{MONITOR_FIELDS.map(([key, label]) => ( ))}
{monitorSaved ? Saved : null}
) : null} {tab === "Discovery" ? (
{DISCOVERY_FIELDS.map(([key, label]) => ( ))}
{discoverSaved ? Saved : null}
) : null}
); }