b9ebd18c2c
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
168 lines
6.1 KiB
TypeScript
168 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
type Loaded = {
|
|
qobuzEmail: string;
|
|
qobuzUserId: string;
|
|
slskdUrl: string;
|
|
qobuzPasswordSet: boolean;
|
|
qobuzTokenSet: boolean;
|
|
slskdApiKeySet: boolean;
|
|
};
|
|
|
|
export function SettingsForm() {
|
|
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 [pwSet, setPwSet] = useState(false);
|
|
const [tokenSet, setTokenSet] = useState(false);
|
|
const [keySet, setKeySet] = useState(false);
|
|
const [saved, setSaved] = useState(false);
|
|
const [scanResult, setScanResult] = useState<string | null>(null);
|
|
const [scanRequested, setScanRequested] = useState(false);
|
|
const [discoverCfg, setDiscoverCfg] = useState<Record<string, string>>({});
|
|
const [discoverSaved, setDiscoverSaved] = 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); // worker finished the scan
|
|
}, 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);
|
|
setPwSet(c.qobuzPasswordSet);
|
|
setTokenSet(c.qobuzTokenSet);
|
|
setKeySet(c.slskdApiKeySet);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetch("/api/discover/config")
|
|
.then((r) => r.json())
|
|
.then((c: Record<string, string>) => setDiscoverCfg(c));
|
|
}, []);
|
|
|
|
async function submit(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 }),
|
|
});
|
|
setQobuzPassword("");
|
|
setQobuzToken("");
|
|
setSlskdApiKey("");
|
|
setSaved(true);
|
|
setPwSet(pwSet || qobuzPassword !== "");
|
|
setTokenSet(tokenSet || qobuzToken !== "");
|
|
setKeySet(keySet || slskdApiKey !== "");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<form onSubmit={submit}>
|
|
<fieldset>
|
|
<legend>Qobuz — email/password</legend>
|
|
<label>Qobuz email <input aria-label="qobuz email" value={qobuzEmail} onChange={(e) => setQobuzEmail(e.target.value)} /></label>
|
|
<label>Qobuz password <input aria-label="qobuz password" type="password" placeholder={pwSet ? "•••• (set)" : ""} value={qobuzPassword} onChange={(e) => setQobuzPassword(e.target.value)} /></label>
|
|
</fieldset>
|
|
<fieldset>
|
|
<legend>Qobuz — auth token (more reliable; overrides email/password)</legend>
|
|
<label>Qobuz user ID <input aria-label="qobuz user id" value={qobuzUserId} onChange={(e) => setQobuzUserId(e.target.value)} /></label>
|
|
<label>Qobuz auth token <input aria-label="qobuz auth token" type="password" placeholder={tokenSet ? "•••• (set)" : ""} value={qobuzToken} onChange={(e) => setQobuzToken(e.target.value)} /></label>
|
|
</fieldset>
|
|
<fieldset>
|
|
<legend>Soulseek (slskd)</legend>
|
|
<label>slskd URL <input aria-label="slskd url" value={slskdUrl} onChange={(e) => setSlskdUrl(e.target.value)} /></label>
|
|
<label>slskd API key <input aria-label="slskd api key" type="password" placeholder={keySet ? "•••• (set)" : ""} value={slskdApiKey} onChange={(e) => setSlskdApiKey(e.target.value)} /></label>
|
|
</fieldset>
|
|
<button type="submit">Save</button>
|
|
{saved ? <span> Saved.</span> : null}
|
|
</form>
|
|
<section>
|
|
<h2>Library</h2>
|
|
<p>Scan <code>/music</code> and record what's already on disk as owned + monitored.</p>
|
|
<button type="button" onClick={requestScan} disabled={scanRequested}>
|
|
{scanRequested ? "Scan requested…" : "Scan library"}
|
|
</button>
|
|
{scanResult ? <p aria-label="scan result">Last scan: {scanResult}</p> : null}
|
|
</section>
|
|
<form onSubmit={saveDiscovery}>
|
|
<h2>Discovery</h2>
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={discoverCfg["discover.enabled"] === "true"}
|
|
onChange={(e) => setCfg("discover.enabled", e.target.checked ? "true" : "false")}
|
|
/>{" "}
|
|
Enable scheduled discovery sweep
|
|
</label>
|
|
{[
|
|
["discover.intervalHours", "Interval (hours)"],
|
|
["discover.maxSeeds", "Max seeds per sweep"],
|
|
["discover.similarPerSeed", "Similar per seed"],
|
|
["discover.albumsPerArtist", "Albums per artist"],
|
|
["discover.minScore", "Min score"],
|
|
].map(([key, label]) => (
|
|
<p key={key}>
|
|
<label>
|
|
{label}:{" "}
|
|
<input
|
|
type="number"
|
|
step="any"
|
|
value={discoverCfg[key] ?? ""}
|
|
onChange={(e) => setCfg(key, e.target.value)}
|
|
/>
|
|
</label>
|
|
</p>
|
|
))}
|
|
<button type="submit">Save discovery settings</button>
|
|
{discoverSaved && <span> Saved.</span>}
|
|
</form>
|
|
</>
|
|
);
|
|
}
|