diff --git a/core/qobuz_client.py b/core/qobuz_client.py
index 75c12123..61a550d8 100644
--- a/core/qobuz_client.py
+++ b/core/qobuz_client.py
@@ -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
diff --git a/web_server.py b/web_server.py
index 0541f35a..d99476de 100644
--- a/web_server.py
+++ b/web_server.py
@@ -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."""
diff --git a/webui/index.html b/webui/index.html
index 301f3903..e3fb8e3a 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -4232,6 +4232,25 @@
Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source.
+
+
Alternative: Auth Token — If email/password login fails (CAPTCHA), paste your token directly:
+
+
+
+
+
+
+
+
+
+
+
To get your token: log into play.qobuz.com
+ → DevTools (F12) → Network tab → find any request to www.qobuz.com/api.json
+ → look in the request headers for X-User-Auth-Token → copy that value.
+
@@ -4789,6 +4808,23 @@
Requires a paid Qobuz subscription. Streams are DRM-free.
+
+
Alternative: Auth Token — If email/password login fails (CAPTCHA), paste your token directly:
+
+
+
+
+
+
+
+
To get your token: log into play.qobuz.com
+ → DevTools (F12) → Network tab → find any request to www.qobuz.com/api.json
+ → look for X-User-Auth-Token in the request headers → copy that value.