Add Qobuz auth token login as CAPTCHA bypass alternative

Qobuz added reCAPTCHA to their login endpoint, blocking automated
email/password auth for new users. Token login lets users paste
their X-User-Auth-Token from the browser DevTools after logging in
manually. Added to both Connections and Downloads tabs with
instructions. Existing email/password flow completely unchanged.
Backend validates token via user/get API and saves the session
identically to email/password login.
pull/289/head
Broque Thomas 2 weeks ago
parent e1f7b8f5cc
commit 3d96752087

@ -446,6 +446,64 @@ class QobuzClient:
traceback.print_exc()
return {'status': 'error', 'message': self._auth_error}
def login_with_token(self, token: str) -> Dict[str, Any]:
"""
Login to Qobuz with a user_auth_token pasted from the browser.
Bypasses email/password login (and any CAPTCHA) entirely.
"""
self._auth_error = None
try:
# Step 1: Extract app credentials if we don't have them
if not self.app_id or not self.app_secret:
if not self._extract_app_credentials():
self._auth_error = 'Could not extract Qobuz app credentials. Qobuz may have updated their web player.'
return {'status': 'error', 'message': self._auth_error}
# Step 2: Set the token and validate it
self.user_auth_token = token.strip()
self.session.headers['X-App-Id'] = self.app_id
self.session.headers['X-User-Auth-Token'] = self.user_auth_token
resp = self.session.get(
QOBUZ_API_BASE + 'user/get',
params={'user_id': 'me'},
timeout=15,
)
if resp.status_code != 200:
self.user_auth_token = None
self.session.headers.pop('X-User-Auth-Token', None)
self._auth_error = f'Invalid token (HTTP {resp.status_code})'
return {'status': 'error', 'message': self._auth_error}
data = resp.json()
self.user_info = data
# Check subscription
subscription = data.get('credential', {})
sub_label = subscription.get('label', 'Unknown')
# Save session
self._save_session()
display_name = data.get('display_name', data.get('email', 'unknown'))
logger.info(f"Qobuz token login successful: {display_name} (plan: {sub_label})")
return {
'status': 'success',
'message': f'Logged in as {display_name}',
'user': {
'display_name': display_name,
'subscription': sub_label,
'email': data.get('email', ''),
},
}
except Exception as e:
self._auth_error = str(e)
logger.error(f"Qobuz token login failed: {e}")
return {'status': 'error', 'message': self._auth_error}
def logout(self):
"""Clear Qobuz session."""
self.user_auth_token = None

@ -31465,6 +31465,28 @@ def qobuz_auth_login():
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/qobuz/auth/token', methods=['POST'])
def qobuz_auth_token():
"""Login to Qobuz with a pasted user_auth_token (bypasses CAPTCHA)."""
try:
data = request.get_json()
token = data.get('token', '').strip()
if not token:
return jsonify({"success": False, "error": "Auth token required"}), 400
qobuz = soulseek_client.qobuz
result = qobuz.login_with_token(token)
if result['status'] == 'success':
return jsonify({"success": True, **result})
else:
return jsonify({"success": False, "error": result.get('message', 'Token login failed')}), 400
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/qobuz/auth/status', methods=['GET'])
def qobuz_auth_status():
"""Check if Qobuz client is authenticated."""

@ -4232,6 +4232,25 @@
<div class="setting-help-text" style="margin-top: 6px;">
Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source.
</div>
<div class="callback-info" style="margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<div class="callback-help" style="margin-bottom: 8px;"><strong>Alternative: Auth Token</strong> — If email/password login fails (CAPTCHA), paste your token directly:</div>
</div>
<div class="form-group">
<label>Auth Token:</label>
<input type="password" id="qobuz-connection-token" class="form-input"
placeholder="Paste your Qobuz auth token here">
</div>
<div class="form-actions">
<button class="auth-button" id="qobuz-token-login-btn" onclick="loginQobuzWithToken()">
Connect with Token
</button>
<span id="qobuz-token-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div class="callback-info">
<div class="callback-help">To get your token: log into <a href="https://play.qobuz.com" target="_blank" style="color: #4285f4;">play.qobuz.com</a>
→ DevTools (F12) → Network tab → find any request to <code>www.qobuz.com/api.json</code>
→ look in the request headers for <code>X-User-Auth-Token</code> → copy that value.</div>
</div>
</div>
</div>
@ -4789,6 +4808,23 @@
<div class="setting-help-text" style="margin-top: 6px;">
Requires a paid Qobuz subscription. Streams are DRM-free.
</div>
<div class="callback-info" style="margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<div class="callback-help" style="margin-bottom: 8px;"><strong>Alternative: Auth Token</strong> — If email/password login fails (CAPTCHA), paste your token directly:</div>
</div>
<input type="password" id="qobuz-download-token" class="form-input"
placeholder="Paste your Qobuz auth token here"
style="margin-bottom: 8px;">
<div class="form-actions">
<button class="auth-button" id="qobuz-download-token-btn" onclick="loginQobuzWithTokenFromDownloads()">
Connect with Token
</button>
<span id="qobuz-download-token-status" class="setting-help-text" style="margin-left: 8px;"></span>
</div>
<div class="callback-info">
<div class="callback-help">To get your token: log into <a href="https://play.qobuz.com" target="_blank" style="color: #4285f4;">play.qobuz.com</a>
→ DevTools (F12) → Network tab → find any request to <code>www.qobuz.com/api.json</code>
→ look for <code>X-User-Auth-Token</code> in the request headers → copy that value.</div>
</div>
</div>
</div>

@ -7983,6 +7983,86 @@ async function loginQobuzFromConnections() {
}
}
async function loginQobuzWithToken() {
const btn = document.getElementById('qobuz-token-login-btn');
const statusEl = document.getElementById('qobuz-token-status');
const token = document.getElementById('qobuz-connection-token').value.trim();
if (!token) {
showToast('Please paste your Qobuz auth token', 'warning');
return;
}
btn.disabled = true;
btn.textContent = 'Connecting...';
if (statusEl) statusEl.textContent = '';
try {
const resp = await fetch('/api/qobuz/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const data = await resp.json();
if (data.success) {
showToast('Qobuz connected via token!', 'success');
document.getElementById('qobuz-connection-token').value = '';
checkQobuzAuthStatus();
} else {
if (statusEl) { statusEl.textContent = data.error || 'Token login failed'; statusEl.style.color = '#ff5555'; }
showToast(data.error || 'Qobuz token login failed', 'error');
}
} catch (error) {
console.error('Qobuz token login error:', error);
if (statusEl) { statusEl.textContent = 'Connection error'; statusEl.style.color = '#ff5555'; }
showToast('Failed to connect to Qobuz', 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Connect with Token';
}
}
async function loginQobuzWithTokenFromDownloads() {
const btn = document.getElementById('qobuz-download-token-btn');
const statusEl = document.getElementById('qobuz-download-token-status');
const token = document.getElementById('qobuz-download-token').value.trim();
if (!token) {
showToast('Please paste your Qobuz auth token', 'warning');
return;
}
btn.disabled = true;
btn.textContent = 'Connecting...';
if (statusEl) statusEl.textContent = '';
try {
const resp = await fetch('/api/qobuz/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
const data = await resp.json();
if (data.success) {
showToast('Qobuz connected via token!', 'success');
document.getElementById('qobuz-download-token').value = '';
checkQobuzAuthStatus();
} else {
if (statusEl) { statusEl.textContent = data.error || 'Token login failed'; statusEl.style.color = '#ff5555'; }
showToast(data.error || 'Qobuz token login failed', 'error');
}
} catch (error) {
console.error('Qobuz token login error:', error);
if (statusEl) { statusEl.textContent = 'Connection error'; statusEl.style.color = '#ff5555'; }
showToast('Failed to connect to Qobuz', 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Connect with Token';
}
}
async function loginQobuz() {
const btn = document.getElementById('qobuz-login-btn');
const statusEl = document.getElementById('qobuz-auth-status');

Loading…
Cancel
Save