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
This commit is contained in:
parent
e35d84ba96
commit
cc95cfcdf2
4 changed files with 44 additions and 9 deletions
|
|
@ -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…
Reference in a new issue