Wire Discogs as fully featured fallback metadata source

- SpotifyClient: add _discogs lazy-load property, route _fallback to
  DiscogsClient when configured (requires token, falls back to iTunes)
- web_server: _get_metadata_fallback_client returns DiscogsClient when
  selected and token present
- Enhanced search: Discogs added as source tab with NDJSON streaming,
  only available when token configured
- Alternate sources list includes Discogs when token is set
- Frontend: source labels, tab styling, fetch list all include Discogs
- Consistent with iTunes/Deezer pattern — same interfaces, same routing
pull/253/head
Broque Thomas 2 months ago
parent e35d84ba96
commit cc95cfcdf2

@ -417,6 +417,7 @@ class SpotifyClient:
self.user_id: Optional[str] = None
self._itunes_client = None # Lazy-loaded iTunes fallback
self._deezer_client = None # Lazy-loaded Deezer fallback
self._discogs_client = None # Lazy-loaded Discogs fallback
self._auth_cache_lock = threading.Lock()
self._auth_cached_result: Optional[bool] = None
self._auth_cache_time: float = 0
@ -454,9 +455,18 @@ class SpotifyClient:
logger.info("Deezer fallback client initialized")
return self._deezer_client
@property
def _discogs(self):
"""Lazy-load Discogs client for metadata fallback"""
if self._discogs_client is None:
from core.discogs_client import DiscogsClient
self._discogs_client = DiscogsClient()
logger.info("Discogs fallback client initialized")
return self._discogs_client
@property
def _fallback_source(self) -> str:
"""Get configured metadata fallback source ('itunes' or 'deezer')"""
"""Get configured metadata fallback source ('itunes', 'deezer', or 'discogs')"""
try:
return config_manager.get('metadata.fallback_source', 'itunes') or 'itunes'
except Exception:
@ -467,6 +477,12 @@ class SpotifyClient:
"""Get the active fallback metadata client based on settings"""
if self._fallback_source == 'deezer':
return self._deezer
if self._fallback_source == 'discogs':
# Only use Discogs if token is configured
token = config_manager.get('discogs.token', '')
if token:
return self._discogs
return self._itunes # Fall back to iTunes if no Discogs token
return self._itunes
def reload_config(self):

@ -7362,6 +7362,7 @@ def enhanced_search():
# Determine which alternate sources are available (for frontend to fetch async)
spotify_available = bool(spotify_client and spotify_client.is_spotify_authenticated())
hydrabase_available = bool(hydrabase_client and hydrabase_client.is_connected())
discogs_available = bool(config_manager.get('discogs.token', ''))
alternate_sources = []
if primary_source != 'spotify' and spotify_available:
alternate_sources.append('spotify')
@ -7369,6 +7370,8 @@ def enhanced_search():
alternate_sources.append('itunes')
if primary_source != 'deezer':
alternate_sources.append('deezer')
if primary_source != 'discogs' and discogs_available:
alternate_sources.append('discogs')
if primary_source != 'hydrabase' and hydrabase_available:
alternate_sources.append('hydrabase')
@ -7457,7 +7460,7 @@ def enhanced_search_source(source_name):
This prevents slow sources (iTunes with 3s rate limit) from blocking the UI.
Falls back to single JSON response if streaming not supported.
"""
if source_name not in ('spotify', 'itunes', 'deezer', 'hydrabase'):
if source_name not in ('spotify', 'itunes', 'deezer', 'discogs', 'hydrabase'):
return jsonify({"error": f"Unknown source: {source_name}"}), 400
data = request.get_json()
@ -7477,6 +7480,13 @@ def enhanced_search_source(source_name):
client = iTunesClient()
elif source_name == 'deezer':
client = _get_deezer_client()
elif source_name == 'discogs':
token = config_manager.get('discogs.token', '')
if token:
from core.discogs_client import DiscogsClient
client = DiscogsClient(token=token)
else:
return jsonify({"artists": [], "albums": [], "tracks": [], "available": False})
elif source_name == 'hydrabase':
if hydrabase_client and hydrabase_client.is_connected():
client = hydrabase_client
@ -31896,7 +31906,7 @@ def _get_deezer_client():
return _deezer_client_instance
def _get_metadata_fallback_source():
"""Get the configured metadata fallback source ('itunes', 'deezer', or 'hydrabase')."""
"""Get the configured metadata fallback source ('itunes', 'deezer', 'discogs', or 'hydrabase')."""
try:
return config_manager.get('metadata.fallback_source', 'deezer') or 'deezer'
except Exception:
@ -31904,14 +31914,21 @@ def _get_metadata_fallback_source():
def _get_metadata_fallback_client():
"""Get the active metadata fallback client based on settings.
Returns an iTunesClient, DeezerClient, or HydrabaseClient instance with identical interfaces."""
Returns an iTunesClient, DeezerClient, DiscogsClient, or HydrabaseClient instance with identical interfaces."""
source = _get_metadata_fallback_source()
if source == 'deezer':
return _get_deezer_client()
if source == 'discogs':
token = config_manager.get('discogs.token', '')
if token:
from core.discogs_client import DiscogsClient
return DiscogsClient(token=token)
# No token — fall back to iTunes
from core.itunes_client import iTunesClient
return iTunesClient()
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

@ -7922,6 +7922,7 @@ function initializeSearchModeToggle() {
spotify: { text: 'Spotify', tabClass: 'enh-tab-spotify', badgeClass: 'enh-badge-spotify' },
itunes: { text: 'Apple Music', tabClass: 'enh-tab-itunes', badgeClass: 'enh-badge-itunes' },
deezer: { text: 'Deezer', tabClass: 'enh-tab-deezer', badgeClass: 'enh-badge-deezer' },
discogs: { text: 'Discogs', tabClass: 'enh-tab-discogs', badgeClass: 'enh-badge-discogs' },
hydrabase: { text: 'Hydrabase', tabClass: 'enh-tab-hydrabase', badgeClass: 'enh-badge-hydrabase' },
};
@ -8047,7 +8048,7 @@ function initializeSearchModeToggle() {
// Fire ALL source fetches immediately in parallel with the primary endpoint.
// Don't guess which is primary — the main endpoint response will tell us.
// If an alternate duplicates the primary, it just overwrites with same data.
for (const srcName of ['spotify', 'itunes', 'deezer', 'hydrabase']) {
for (const srcName of ['spotify', 'itunes', 'deezer', 'discogs', 'hydrabase']) {
_fetchAlternateSource(srcName, query);
}
@ -17120,7 +17121,7 @@ function _gsRender(data) {
return;
}
const sourceLabels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', hydrabase: 'Hydrabase' };
const sourceLabels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', discogs: 'Discogs', hydrabase: 'Hydrabase' };
const srcLabel = sourceLabels[_gsState.activeSource] || _gsState.activeSource || '';
let h = '';
@ -17217,7 +17218,7 @@ function _gsRenderTabs() {
if (!el) return;
const sources = Object.keys(_gsState.sources);
if (sources.length < 2) { el.style.display = 'none'; return; }
const labels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', hydrabase: 'Hydrabase' };
const labels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', discogs: 'Discogs', hydrabase: 'Hydrabase' };
el.style.display = 'flex';
el.innerHTML = sources.map(s => {
const d = _gsState.sources[s];
@ -44811,7 +44812,7 @@ function _renderRedownloadStep1(overlay, track, data) {
const bestSource = data.best_match?.source || sources[0];
const sourceIcons = { spotify: '🟢', itunes: '🍎', deezer: '🟣', hydrabase: '🔷' };
const sourceLabels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', hydrabase: 'Hydrabase' };
const sourceLabels = { spotify: 'Spotify', itunes: 'Apple Music', deezer: 'Deezer', discogs: 'Discogs', hydrabase: 'Hydrabase' };
// Build columns — one per source, side by side
const columnsHtml = sources.map(source => {

@ -32143,6 +32143,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
.enh-source-tab.enh-tab-spotify.active { background: rgba(29, 185, 84, 0.2); color: #1db954; }
.enh-source-tab.enh-tab-itunes.active { background: rgba(252, 60, 68, 0.2); color: #fc3c44; }
.enh-source-tab.enh-tab-deezer.active { background: rgba(162, 56, 255, 0.2); color: #a238ff; }
.enh-source-tab.enh-tab-discogs.active { background: rgba(212, 165, 116, 0.2); color: #D4A574; }
.enh-source-tab.enh-tab-hydrabase.active { background: rgba(0, 180, 216, 0.2); color: #00b4d8; }
.enh-dropdown-section {

Loading…
Cancel
Save