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.
This commit is contained in:
parent
e1f7b8f5cc
commit
3d96752087
4 changed files with 196 additions and 0 deletions
|
|
@ -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…
Reference in a new issue