Complete Hydrabase as selectable fallback metadata source

- Remove redundant enable checkbox — fallback dropdown is the enable
- Hydrabase option only appears in dropdown when connected
- Connect/disconnect dynamically adds/removes dropdown option
- _is_hydrabase_active checks fallback_source == hydrabase (not config toggle)
- Fallback client returns hydrabase_client when selected, iTunes if disconnected
- Auto-reconnect respects fallback selection for dev_mode handling
- hydrabase added to settings save service list for persistence
- Status shows green Connected on page load when auto-connected
This commit is contained in:
Broque Thomas 2026-03-20 14:18:10 -07:00
parent 214985482d
commit 10361bb837
4 changed files with 73 additions and 24 deletions

View file

@ -31,6 +31,19 @@ def _create_fallback_client():
if source == 'deezer': if source == 'deezer':
from core.deezer_client import DeezerClient from core.deezer_client import DeezerClient
return DeezerClient() return DeezerClient()
if source == 'hydrabase':
try:
from core.hydrabase_client import HydrabaseClient
# Hydrabase client is managed globally — try to import the running instance
import importlib
ws_module = importlib.import_module('web_server')
client = getattr(ws_module, 'hydrabase_client', None)
if client and client.is_connected():
return client
except Exception:
pass
# Hydrabase not available — fall back to iTunes
return iTunesClient()
return iTunesClient() return iTunesClient()

View file

@ -4481,7 +4481,7 @@ def handle_settings():
if 'active_media_server' in new_settings: if 'active_media_server' in new_settings:
config_manager.set_active_media_server(new_settings['active_media_server']) config_manager.set_active_media_server(new_settings['active_media_server'])
for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'download_source', 'settings', 'database', 'metadata_enhancement', 'file_organization', 'playlist_sync', 'tidal', 'tidal_download', 'qobuz', 'hifi_download', 'listenbrainz', 'acoustid', 'lastfm', 'genius', 'import', 'lossy_copy', 'ui_appearance', 'youtube', 'content_filter', 'itunes', 'm3u_export', 'musicbrainz', 'deezer', 'audiodb', 'metadata']: for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'download_source', 'settings', 'database', 'metadata_enhancement', 'file_organization', 'playlist_sync', 'tidal', 'tidal_download', 'qobuz', 'hifi_download', 'listenbrainz', 'acoustid', 'lastfm', 'genius', 'import', 'lossy_copy', 'ui_appearance', 'youtube', 'content_filter', 'itunes', 'm3u_export', 'musicbrainz', 'deezer', 'audiodb', 'metadata', 'hydrabase']:
if service in new_settings: if service in new_settings:
for key, value in new_settings[service].items(): for key, value in new_settings[service].items():
config_manager.set(f'{service}.{key}', value) config_manager.set(f'{service}.{key}', value)
@ -4560,15 +4560,15 @@ _comparison_lock = threading.Lock()
def _is_hydrabase_active(): def _is_hydrabase_active():
"""Check if Hydrabase should be used as the primary metadata source. """Check if Hydrabase should be used as the primary metadata source.
Active when: (dev_mode OR hydrabase.enabled config) AND client connected.""" Active when: (dev_mode OR selected as fallback source) AND client connected."""
try: try:
if hydrabase_client is None or not hydrabase_client.is_connected(): if hydrabase_client is None or not hydrabase_client.is_connected():
return False return False
# Dev mode always enables Hydrabase (legacy behavior) # Dev mode always enables Hydrabase (legacy behavior)
if dev_mode_enabled: if dev_mode_enabled:
return True return True
# Config toggle: user enabled Hydrabase as metadata source # Selected as the fallback metadata source
return config_manager.get('hydrabase.enabled', False) return _get_metadata_fallback_source() == 'hydrabase'
except (NameError, Exception): except (NameError, Exception):
return False return False
@ -4650,8 +4650,6 @@ def _run_background_comparison(query, hydrabase_counts=None):
def hydrabase_connect(): def hydrabase_connect():
"""Connect to a Hydrabase instance via WebSocket.""" """Connect to a Hydrabase instance via WebSocket."""
global _hydrabase_ws global _hydrabase_ws
if not dev_mode_enabled:
return jsonify({"success": False, "error": "Dev mode not active"}), 403
data = request.get_json() data = request.get_json()
url = data.get('url', '').strip() url = data.get('url', '').strip()
api_key = data.get('api_key', '').strip() api_key = data.get('api_key', '').strip()
@ -4694,8 +4692,10 @@ def hydrabase_disconnect():
pass pass
_hydrabase_ws = None _hydrabase_ws = None
config_manager.set('hydrabase.auto_connect', False) config_manager.set('hydrabase.auto_connect', False)
dev_mode_enabled = False # Only disable dev mode if not using Hydrabase as a regular fallback source
print("🧪 [Hydrabase] Disconnected — dev mode disabled") if _get_metadata_fallback_source() != 'hydrabase':
dev_mode_enabled = False
print("🧪 [Hydrabase] Disconnected")
return jsonify({"success": True}) return jsonify({"success": True})
@app.route('/api/hydrabase/status') @app.route('/api/hydrabase/status')
@ -27441,7 +27441,7 @@ def _get_deezer_client():
return _deezer_client_instance return _deezer_client_instance
def _get_metadata_fallback_source(): def _get_metadata_fallback_source():
"""Get the configured metadata fallback source ('itunes' or 'deezer').""" """Get the configured metadata fallback source ('itunes', 'deezer', or 'hydrabase')."""
try: try:
return config_manager.get('metadata.fallback_source', 'itunes') or 'itunes' return config_manager.get('metadata.fallback_source', 'itunes') or 'itunes'
except Exception: except Exception:
@ -27449,10 +27449,16 @@ def _get_metadata_fallback_source():
def _get_metadata_fallback_client(): def _get_metadata_fallback_client():
"""Get the active metadata fallback client based on settings. """Get the active metadata fallback client based on settings.
Returns an iTunesClient or DeezerClient instance with identical interfaces.""" Returns an iTunesClient, DeezerClient, or HydrabaseClient instance with identical interfaces."""
source = _get_metadata_fallback_source() source = _get_metadata_fallback_source()
if source == 'deezer': if source == 'deezer':
return _get_deezer_client() return _get_deezer_client()
if source == 'hydrabase':
if hydrabase_client and hydrabase_client.is_connected():
return hydrabase_client
# Hydrabase not connected — fall back to iTunes
from core.itunes_client import iTunesClient
return iTunesClient()
from core.itunes_client import iTunesClient from core.itunes_client import iTunesClient
return iTunesClient() return iTunesClient()
@ -40772,9 +40778,8 @@ try:
timeout=10 timeout=10
) )
_hydrabase_ws = _auto_ws _hydrabase_ws = _auto_ws
# Enable dev mode only if Hydrabase was previously in dev mode # Enable dev mode only if not using Hydrabase as a regular fallback source
# The config toggle (hydrabase.enabled) handles non-dev usage if _get_metadata_fallback_source() != 'hydrabase':
if not _hydra_cfg.get('enabled'):
dev_mode_enabled = True dev_mode_enabled = True
print(f"✅ Hydrabase auto-connected to {_hydra_cfg['url']}") print(f"✅ Hydrabase auto-connected to {_hydra_cfg['url']}")
except Exception as e: except Exception as e:

View file

@ -3673,19 +3673,14 @@
</select> </select>
</div> </div>
<div class="callback-info"> <div class="callback-info">
<div class="callback-help">When Spotify is not connected, this source provides artist, album, and track metadata. Both are free and require no API key.</div> <div class="callback-help">When Spotify is not connected, this source provides artist, album, and track metadata. Hydrabase requires a connection configured above.</div>
</div> </div>
</div> </div>
<!-- Hydrabase P2P Metadata --> <!-- Hydrabase P2P Metadata -->
<div class="api-service-frame" data-stg="connections"> <div class="api-service-frame" data-stg="connections">
<h4 class="service-title" style="color: #00b4d8;">Hydrabase</h4> <h4 class="service-title" style="color: #00b4d8;">Hydrabase</h4>
<div class="form-group" style="margin-bottom: 8px;"> <input type="hidden" id="hydrabase-enabled" value="false">
<label class="checkbox-label" style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 0;">
<input type="checkbox" id="hydrabase-enabled" style="width: 16px; height: 16px;">
<span>Enable Hydrabase as metadata source</span>
</label>
</div>
<div class="form-group"> <div class="form-group">
<label>WebSocket URL:</label> <label>WebSocket URL:</label>
<input type="text" id="hydrabase-url" placeholder="ws://localhost:4545"> <input type="text" id="hydrabase-url" placeholder="ws://localhost:4545">

View file

@ -5469,10 +5469,30 @@ async function loadSettingsData() {
// Populate Hydrabase settings // Populate Hydrabase settings
const hbConfig = settings.hydrabase || {}; const hbConfig = settings.hydrabase || {};
document.getElementById('hydrabase-enabled').checked = hbConfig.enabled || false;
document.getElementById('hydrabase-url').value = hbConfig.url || ''; document.getElementById('hydrabase-url').value = hbConfig.url || '';
document.getElementById('hydrabase-api-key').value = hbConfig.api_key || ''; document.getElementById('hydrabase-api-key').value = hbConfig.api_key || '';
document.getElementById('hydrabase-auto-connect').checked = hbConfig.auto_connect || false; document.getElementById('hydrabase-auto-connect').checked = hbConfig.auto_connect || false;
// Check live connection status + add Hydrabase to fallback dropdown if connected
fetch('/api/hydrabase/status').then(r => r.json()).then(s => {
const btn = document.getElementById('hydrabase-connect-btn');
const statusEl = document.getElementById('hydrabase-settings-status');
if (s.connected) {
if (btn) btn.textContent = 'Disconnect';
if (statusEl) { statusEl.textContent = 'Connected'; statusEl.style.color = '#4caf50'; }
// Add Hydrabase to fallback source dropdown
const fbSelect = document.getElementById('metadata-fallback-source');
if (fbSelect && !fbSelect.querySelector('option[value="hydrabase"]')) {
const opt = document.createElement('option');
opt.value = 'hydrabase';
opt.textContent = 'Hydrabase (P2P)';
fbSelect.appendChild(opt);
}
// Restore selection if it was hydrabase
if ((settings.metadata?.fallback_source) === 'hydrabase') {
fbSelect.value = 'hydrabase';
}
}
}).catch(() => {});
// Populate Download settings (right column) // Populate Download settings (right column)
document.getElementById('download-path').value = settings.soulseek?.download_path || './downloads'; document.getElementById('download-path').value = settings.soulseek?.download_path || './downloads';
@ -6107,7 +6127,16 @@ async function toggleHydrabaseFromSettings() {
// Disconnect // Disconnect
await fetch('/api/hydrabase/disconnect', { method: 'POST' }); await fetch('/api/hydrabase/disconnect', { method: 'POST' });
if (btn) btn.textContent = 'Connect'; if (btn) btn.textContent = 'Connect';
if (statusEl) statusEl.textContent = 'Disconnected'; if (statusEl) { statusEl.textContent = 'Disconnected'; statusEl.style.color = 'rgba(255,255,255,0.4)'; }
// Remove from fallback dropdown + reset to iTunes if was selected
const fbSel2 = document.getElementById('metadata-fallback-source');
if (fbSel2) {
const hbOpt = fbSel2.querySelector('option[value="hydrabase"]');
if (hbOpt) {
if (fbSel2.value === 'hydrabase') fbSel2.value = 'itunes';
hbOpt.remove();
}
}
showToast('Hydrabase disconnected', 'info'); showToast('Hydrabase disconnected', 'info');
} else { } else {
// Connect // Connect
@ -6119,7 +6148,15 @@ async function toggleHydrabaseFromSettings() {
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
if (btn) btn.textContent = 'Disconnect'; if (btn) btn.textContent = 'Disconnect';
if (statusEl) statusEl.textContent = 'Connected'; if (statusEl) { statusEl.textContent = 'Connected'; statusEl.style.color = '#4caf50'; }
// Add to fallback dropdown
const fbSel = document.getElementById('metadata-fallback-source');
if (fbSel && !fbSel.querySelector('option[value="hydrabase"]')) {
const opt = document.createElement('option');
opt.value = 'hydrabase';
opt.textContent = 'Hydrabase (P2P)';
fbSel.appendChild(opt);
}
showToast('Hydrabase connected', 'success'); showToast('Hydrabase connected', 'success');
} else { } else {
if (statusEl) statusEl.textContent = data.error || 'Connection failed'; if (statusEl) statusEl.textContent = data.error || 'Connection failed';
@ -6440,7 +6477,6 @@ async function saveSettings(quiet = false) {
fallback_source: document.getElementById('metadata-fallback-source').value || 'itunes' fallback_source: document.getElementById('metadata-fallback-source').value || 'itunes'
}, },
hydrabase: { hydrabase: {
enabled: document.getElementById('hydrabase-enabled').checked,
url: document.getElementById('hydrabase-url').value, url: document.getElementById('hydrabase-url').value,
api_key: document.getElementById('hydrabase-api-key').value, api_key: document.getElementById('hydrabase-api-key').value,
auto_connect: document.getElementById('hydrabase-auto-connect').checked auto_connect: document.getElementById('hydrabase-auto-connect').checked