feat(web): Last.fm credentials (api key + username) + Settings tab

This commit is contained in:
Jonathan
2026-07-13 18:53:58 +02:00
parent 5315b240f0
commit 5eb4fbfd55
3 changed files with 85 additions and 5 deletions
+52 -5
View File
@@ -7,13 +7,15 @@ type Loaded = {
qobuzEmail: string;
qobuzUserId: string;
slskdUrl: string;
lastfmUsername: string;
qobuzPasswordSet: boolean;
qobuzTokenSet: boolean;
slskdApiKeySet: boolean;
lastfmApiKeySet: boolean;
};
type Tab = "Qobuz" | "Soulseek" | "Library" | "Monitor" | "Discovery";
const TABS: Tab[] = ["Qobuz", "Soulseek", "Library", "Monitor", "Discovery"];
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)"],
@@ -38,9 +40,12 @@ export function SettingsForm() {
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<string | null>(null);
const [scanRequested, setScanRequested] = useState(false);
@@ -80,9 +85,11 @@ export function SettingsForm() {
setQobuzEmail(c.qobuzEmail);
setQobuzUserId(c.qobuzUserId);
setSlskdUrl(c.slskdUrl);
setLastfmUsername(c.lastfmUsername);
setPwSet(c.qobuzPasswordSet);
setTokenSet(c.qobuzTokenSet);
setKeySet(c.slskdApiKeySet);
setLastfmKeySet(c.lastfmApiKeySet);
});
}, []);
@@ -103,16 +110,27 @@ export function SettingsForm() {
await fetch("/api/config", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ qobuzEmail, qobuzPassword, qobuzUserId, qobuzToken, slskdUrl, slskdApiKey }),
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) {
@@ -147,16 +165,17 @@ export function SettingsForm() {
const secretHint = (set: boolean) => (set ? <span className="set"> · set</span> : null);
async function clearField(field: "qobuzPassword" | "qobuzToken" | "slskdApiKey") {
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 (
<div>
<PageHead title="Settings" eyebrow="Credentials · library · monitor · discovery" />
<PageHead title="Settings" eyebrow="Credentials · library · monitor · discovery · last.fm" />
<div className="tabs">
{TABS.map((t) => (
@@ -241,6 +260,34 @@ export function SettingsForm() {
</form>
) : null}
{tab === "Last.fm" ? (
<form className="settings-section" onSubmit={saveCredentials}>
<div className="subhead">Last.fm</div>
<div className="field-grid">
<label className="field">
<span>Last.fm username</span>
<input aria-label="lastfm username" value={lastfmUsername} onChange={(e) => setLastfmUsername(e.target.value)} />
</label>
<label className="field">
<span>
Last.fm API key{secretHint(lastfmKeySet)}
{lastfmKeySet ? (
<button type="button" className="btn ghost sm" style={{ marginLeft: 8 }}
aria-label="clear lastfm api key" onClick={() => clearField("lastfmApiKey")}>
Clear
</button>
) : null}
</span>
<input aria-label="lastfm api key" type="password" placeholder={lastfmKeySet ? "•••• stored" : ""} value={lastfmApiKey} onChange={(e) => setLastfmApiKey(e.target.value)} />
</label>
</div>
<div className="save-row">
<button type="submit" className="btn accent">Save</button>
{saved ? <span className="saved-note">Saved</span> : null}
</div>
</form>
) : null}
{tab === "Library" ? (
<section className="settings-section">
<div className="info-panel">