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':
from core.deezer_client import 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()

View file

@ -4481,7 +4481,7 @@ def handle_settings():
if 'active_media_server' in new_settings:
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:
for key, value in new_settings[service].items():
config_manager.set(f'{service}.{key}', value)
@ -4560,15 +4560,15 @@ _comparison_lock = threading.Lock()
def _is_hydrabase_active():
"""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:
if hydrabase_client is None or not hydrabase_client.is_connected():
return False
# Dev mode always enables Hydrabase (legacy behavior)
if dev_mode_enabled:
return True
# Config toggle: user enabled Hydrabase as metadata source
return config_manager.get('hydrabase.enabled', False)
# Selected as the fallback metadata source
return _get_metadata_fallback_source() == 'hydrabase'
except (NameError, Exception):
return False
@ -4650,8 +4650,6 @@ def _run_background_comparison(query, hydrabase_counts=None):
def hydrabase_connect():
"""Connect to a Hydrabase instance via WebSocket."""
global _hydrabase_ws
if not dev_mode_enabled:
return jsonify({"success": False, "error": "Dev mode not active"}), 403
data = request.get_json()
url = data.get('url', '').strip()
api_key = data.get('api_key', '').strip()
@ -4694,8 +4692,10 @@ def hydrabase_disconnect():
pass
_hydrabase_ws = None
config_manager.set('hydrabase.auto_connect', False)
dev_mode_enabled = False
print("🧪 [Hydrabase] Disconnected — dev mode disabled")
# Only disable dev mode if not using Hydrabase as a regular fallback source
if _get_metadata_fallback_source() != 'hydrabase':
dev_mode_enabled = False
print("🧪 [Hydrabase] Disconnected")
return jsonify({"success": True})
@app.route('/api/hydrabase/status')
@ -27441,7 +27441,7 @@ def _get_deezer_client():
return _deezer_client_instance
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:
return config_manager.get('metadata.fallback_source', 'itunes') or 'itunes'
except Exception:
@ -27449,10 +27449,16 @@ def _get_metadata_fallback_source():
def _get_metadata_fallback_client():
"""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()
if source == 'deezer':
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
return iTunesClient()
@ -40772,9 +40778,8 @@ try:
timeout=10
)
_hydrabase_ws = _auto_ws
# Enable dev mode only if Hydrabase was previously in dev mode
# The config toggle (hydrabase.enabled) handles non-dev usage
if not _hydra_cfg.get('enabled'):
# Enable dev mode only if not using Hydrabase as a regular fallback source
if _get_metadata_fallback_source() != 'hydrabase':
dev_mode_enabled = True
print(f"✅ Hydrabase auto-connected to {_hydra_cfg['url']}")
except Exception as e:

View file

@ -3673,19 +3673,14 @@
</select>
</div>
<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>
<!-- Hydrabase P2P Metadata -->
<div class="api-service-frame" data-stg="connections">
<h4 class="service-title" style="color: #00b4d8;">Hydrabase</h4>
<div class="form-group" style="margin-bottom: 8px;">
<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>
<input type="hidden" id="hydrabase-enabled" value="false">
<div class="form-group">
<label>WebSocket URL:</label>
<input type="text" id="hydrabase-url" placeholder="ws://localhost:4545">

View file

@ -5469,10 +5469,30 @@ async function loadSettingsData() {
// Populate Hydrabase settings
const hbConfig = settings.hydrabase || {};
document.getElementById('hydrabase-enabled').checked = hbConfig.enabled || false;
document.getElementById('hydrabase-url').value = hbConfig.url || '';
document.getElementById('hydrabase-api-key').value = hbConfig.api_key || '';
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)
document.getElementById('download-path').value = settings.soulseek?.download_path || './downloads';
@ -6107,7 +6127,16 @@ async function toggleHydrabaseFromSettings() {
// Disconnect
await fetch('/api/hydrabase/disconnect', { method: 'POST' });
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');
} else {
// Connect
@ -6119,7 +6148,15 @@ async function toggleHydrabaseFromSettings() {
const data = await res.json();
if (data.success) {
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');
} else {
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'
},
hydrabase: {
enabled: document.getElementById('hydrabase-enabled').checked,
url: document.getElementById('hydrabase-url').value,
api_key: document.getElementById('hydrabase-api-key').value,
auto_connect: document.getElementById('hydrabase-auto-connect').checked