diff --git a/web_server.py b/web_server.py
index 9b5664ae..84a881a2 100644
--- a/web_server.py
+++ b/web_server.py
@@ -19071,6 +19071,15 @@ def get_version_info():
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
+ {
+ "title": "🔧 Add Qobuz to Connections Tab (#218)",
+ "description": "Qobuz credentials now available on the Connections tab for metadata enrichment",
+ "features": [
+ "• New Qobuz section on Settings → Connections tab for enrichment auth",
+ "• Users can connect Qobuz for metadata enrichment regardless of download source",
+ "• Auth status syncs between Connections and Downloads tabs"
+ ]
+ },
{
"title": "🔧 Fix Enrichment Widget Showing 'Running' When Rate Limited",
"description": "Enrichment tooltip now shows Rate Limited or Daily Limit Reached instead of stuck on Running",
diff --git a/webui/index.html b/webui/index.html
index 49903477..7bf055d1 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -3632,6 +3632,38 @@
+
+
+
Qobuz (Metadata & Enrichment)
+
+
+
+
+
+
+ Connects Qobuz for metadata enrichment (ISRC, labels, copyright). Also used for downloads if Qobuz is your download source.
+
+
+
ListenBrainz
diff --git a/webui/static/helper.js b/webui/static/helper.js
index 1f0f0201..9d145aa1 100644
--- a/webui/static/helper.js
+++ b/webui/static/helper.js
@@ -3403,6 +3403,7 @@ function closeHelperSearch() {
const WHATS_NEW = {
'2.1': [
// Newest features first
+ { title: 'Qobuz on Connections Tab', desc: 'Qobuz credentials now on Settings → Connections for metadata enrichment without needing it as download source' },
{ title: 'Fix Enrichment Status Widget', desc: 'Enrichment tooltip now shows Rate Limited or Daily Limit instead of stuck on Running' },
{ title: 'Cache Maintenance', desc: 'Cache evictor now cleans junk entities, orphaned searches, and stale MusicBrainz nulls' },
{ title: 'Fix Wishlist Download Selection', desc: 'Download Selection now only downloads checked tracks instead of the entire category' },
diff --git a/webui/static/script.js b/webui/static/script.js
index 0255d9dc..cb7d1c39 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -7602,24 +7602,83 @@ async function checkQobuzAuthStatus() {
try {
const resp = await fetch('/api/qobuz/auth/status');
const data = await resp.json();
+
+ // Update downloads tab section
const formEl = document.getElementById('qobuz-auth-form');
const loggedInEl = document.getElementById('qobuz-auth-logged-in');
const userInfoEl = document.getElementById('qobuz-auth-user-info');
+ // Update connections tab section
+ const connFormEl = document.getElementById('qobuz-connection-form');
+ const connLoggedInEl = document.getElementById('qobuz-connection-logged-in');
+ const connUserInfoEl = document.getElementById('qobuz-connection-user-info');
+
if (data.authenticated) {
const user = data.user || {};
- userInfoEl.textContent = `Connected: ${user.display_name || 'Qobuz User'} (${user.subscription || 'Active'})`;
- loggedInEl.style.display = 'flex';
- formEl.style.display = 'none';
+ const label = `Connected: ${user.display_name || 'Qobuz User'} (${user.subscription || 'Active'})`;
+
+ if (userInfoEl) { userInfoEl.textContent = label; }
+ if (loggedInEl) loggedInEl.style.display = 'flex';
+ if (formEl) formEl.style.display = 'none';
+
+ if (connUserInfoEl) { connUserInfoEl.textContent = label; }
+ if (connLoggedInEl) connLoggedInEl.style.display = 'flex';
+ if (connFormEl) connFormEl.style.display = 'none';
} else {
- loggedInEl.style.display = 'none';
- formEl.style.display = 'block';
+ if (loggedInEl) loggedInEl.style.display = 'none';
+ if (formEl) formEl.style.display = 'block';
+
+ if (connLoggedInEl) connLoggedInEl.style.display = 'none';
+ if (connFormEl) connFormEl.style.display = 'block';
}
} catch (e) {
console.error('Qobuz auth status check failed:', e);
}
}
+async function loginQobuzFromConnections() {
+ const btn = document.getElementById('qobuz-connection-login-btn');
+ const statusEl = document.getElementById('qobuz-connection-status');
+ const email = document.getElementById('qobuz-connection-email').value.trim();
+ const password = document.getElementById('qobuz-connection-password').value;
+
+ if (!email || !password) {
+ showToast('Please enter your Qobuz email and password', 'warning');
+ return;
+ }
+
+ btn.disabled = true;
+ btn.textContent = 'Connecting...';
+ statusEl.textContent = '';
+
+ try {
+ const resp = await fetch('/api/qobuz/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ });
+ const data = await resp.json();
+
+ if (data.success) {
+ showToast('Qobuz connected successfully!', 'success');
+ document.getElementById('qobuz-connection-password').value = '';
+ checkQobuzAuthStatus();
+ } else {
+ statusEl.textContent = data.error || 'Login failed';
+ statusEl.style.color = '#ff5555';
+ showToast(data.error || 'Qobuz login failed', 'error');
+ }
+ } catch (error) {
+ console.error('Qobuz login error:', error);
+ statusEl.textContent = 'Connection error';
+ statusEl.style.color = '#ff5555';
+ showToast('Failed to connect to Qobuz', 'error');
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Connect Qobuz';
+ }
+}
+
async function loginQobuz() {
const btn = document.getElementById('qobuz-login-btn');
const statusEl = document.getElementById('qobuz-auth-status');