feat: Qobuz auth-token (user_id + token) login as a reliable alternative to email/password

Qobuz's email/password API login returns 401 even with valid credentials
(a known Qobuz limitation, unaffected by streamrip version). Add settings
fields for a Qobuz user ID + auth token (token stored encrypted); when both
are present StreamripClient uses use_auth_token=True, else falls back to
email/password.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-11 00:46:17 +02:00
parent caa0d542ea
commit 00b3dddca6
5 changed files with 73 additions and 11 deletions
+31 -6
View File
@@ -2,14 +2,24 @@
import { useEffect, useState } from "react";
type Loaded = { qobuzEmail: string; slskdUrl: string; qobuzPasswordSet: boolean; slskdApiKeySet: boolean };
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);
@@ -18,8 +28,10 @@ export function SettingsForm() {
.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);
});
}, []);
@@ -29,21 +41,34 @@ export function SettingsForm() {
await fetch("/api/config", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ qobuzEmail, qobuzPassword, slskdUrl, slskdApiKey }),
body: JSON.stringify({ qobuzEmail, qobuzPassword, qobuzUserId, qobuzToken, slskdUrl, slskdApiKey }),
});
setQobuzPassword("");
setQobuzToken("");
setSlskdApiKey("");
setSaved(true);
setPwSet(pwSet || qobuzPassword !== "");
setTokenSet(tokenSet || qobuzToken !== "");
setKeySet(keySet || slskdApiKey !== "");
}
return (
<form onSubmit={submit}>
<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>
<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>
<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>