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.user_id: Optional[str] = None
|
||||||
self._itunes_client = None # Lazy-loaded iTunes fallback
|
self._itunes_client = None # Lazy-loaded iTunes fallback
|
||||||
self._deezer_client = None # Lazy-loaded Deezer 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_cache_lock = threading.Lock()
|
||||||
self._auth_cached_result: Optional[bool] = None
|
self._auth_cached_result: Optional[bool] = None
|
||||||
self._auth_cache_time: float = 0
|
self._auth_cache_time: float = 0
|
||||||
|
|
@ -454,9 +455,18 @@ class SpotifyClient:
|
||||||
logger.info("Deezer fallback client initialized")
|
logger.info("Deezer fallback client initialized")
|
||||||
return self._deezer_client
|
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
|
@property
|
||||||
def _fallback_source(self) -> str:
|
def _fallback_source(self) -> str:
|
||||||
"""Get configured metadata fallback source ('itunes' or 'deezer')"""
|
"""Get configured metadata fallback source ('itunes', 'deezer', or 'discogs')"""
|
||||||
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:
|
||||||
|
|
@ -467,6 +477,12 @@ class SpotifyClient:
|
||||||
"""Get the active fallback metadata client based on settings"""
|
"""Get the active fallback metadata client based on settings"""
|
||||||
if self._fallback_source == 'deezer':
|
if self._fallback_source == 'deezer':
|
||||||
return self._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
|
return self._itunes
|
||||||
|
|
||||||
def reload_config(self):
|
def reload_config(self):
|
||||||
|
|
|
||||||
|
|
@ -7362,6 +7362,7 @@ def enhanced_search():
|
||||||
# Determine which alternate sources are available (for frontend to fetch async)
|
# Determine which alternate sources are available (for frontend to fetch async)
|
||||||
spotify_available = bool(spotify_client and spotify_client.is_spotify_authenticated())
|
spotify_available = bool(spotify_client and spotify_client.is_spotify_authenticated())
|
||||||
hydrabase_available = bool(hydrabase_client and hydrabase_client.is_connected())
|
hydrabase_available = bool(hydrabase_client and hydrabase_client.is_connected())
|
||||||
|
discogs_available = bool(config_manager.get('discogs.token', ''))
|
||||||
alternate_sources = []
|
alternate_sources = []
|
||||||
if primary_source != 'spotify' and spotify_available:
|
if primary_source != 'spotify' and spotify_available:
|
||||||
alternate_sources.append('spotify')
|
alternate_sources.append('spotify')
|
||||||
|
|
@ -7369,6 +7370,8 @@ def enhanced_search():
|
||||||
alternate_sources.append('itunes')
|
alternate_sources.append('itunes')
|
||||||
if primary_source != 'deezer':
|
if primary_source != 'deezer':
|
||||||
alternate_sources.append('deezer')
|
alternate_sources.append('deezer')
|
||||||
|
if primary_source != 'discogs' and discogs_available:
|
||||||
|
alternate_sources.append('discogs')
|
||||||
if primary_source != 'hydrabase' and hydrabase_available:
|
if primary_source != 'hydrabase' and hydrabase_available:
|
||||||
alternate_sources.append('hydrabase')
|
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.
|
This prevents slow sources (iTunes with 3s rate limit) from blocking the UI.
|
||||||
Falls back to single JSON response if streaming not supported.
|
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
|
return jsonify({"error": f"Unknown source: {source_name}"}), 400
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
@ -7477,6 +7480,13 @@ def enhanced_search_source(source_name):
|
||||||
client = iTunesClient()
|
client = iTunesClient()
|
||||||
elif source_name == 'deezer':
|
elif source_name == 'deezer':
|
||||||
client = _get_deezer_client()
|
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':
|
elif source_name == 'hydrabase':
|
||||||
if hydrabase_client and hydrabase_client.is_connected():
|
if hydrabase_client and hydrabase_client.is_connected():
|
||||||
client = hydrabase_client
|
client = hydrabase_client
|
||||||
|
|
@ -31896,7 +31906,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', 'deezer', or 'hydrabase')."""
|
"""Get the configured metadata fallback source ('itunes', 'deezer', 'discogs', or 'hydrabase')."""
|
||||||
try:
|
try:
|
||||||
return config_manager.get('metadata.fallback_source', 'deezer') or 'deezer'
|
return config_manager.get('metadata.fallback_source', 'deezer') or 'deezer'
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -31904,14 +31914,21 @@ 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, DeezerClient, or HydrabaseClient instance with identical interfaces."""
|
Returns an iTunesClient, DeezerClient, DiscogsClient, 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 == '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 source == 'hydrabase':
|
||||||
if hydrabase_client and hydrabase_client.is_connected():
|
if hydrabase_client and hydrabase_client.is_connected():
|
||||||
return hydrabase_client
|
return hydrabase_client
|
||||||
# Hydrabase not connected — fall back to iTunes
|
|
||||||
from core.itunes_client import iTunesClient
|
from core.itunes_client import iTunesClient
|
||||||
return iTunesClient()
|
return iTunesClient()
|
||||||
from core.itunes_client import iTunesClient
|
from core.itunes_client import iTunesClient
|
||||||
|
|
|
||||||
|
|
@ -7922,6 +7922,7 @@ function initializeSearchModeToggle() {
|
||||||
spotify: { text: 'Spotify', tabClass: 'enh-tab-spotify', badgeClass: 'enh-badge-spotify' },
|
spotify: { text: 'Spotify', tabClass: 'enh-tab-spotify', badgeClass: 'enh-badge-spotify' },
|
||||||
itunes: { text: 'Apple Music', tabClass: 'enh-tab-itunes', badgeClass: 'enh-badge-itunes' },
|
itunes: { text: 'Apple Music', tabClass: 'enh-tab-itunes', badgeClass: 'enh-badge-itunes' },
|
||||||
deezer: { text: 'Deezer', tabClass: 'enh-tab-deezer', badgeClass: 'enh-badge-deezer' },
|
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' },
|
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.
|
// Fire ALL source fetches immediately in parallel with the primary endpoint.
|
||||||
// Don't guess which is primary — the main endpoint response will tell us.
|
// 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.
|
// 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);
|
_fetchAlternateSource(srcName, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17120,7 +17121,7 @@ function _gsRender(data) {
|
||||||
return;
|
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 || '';
|
const srcLabel = sourceLabels[_gsState.activeSource] || _gsState.activeSource || '';
|
||||||
|
|
||||||
let h = '';
|
let h = '';
|
||||||
|
|
@ -17217,7 +17218,7 @@ function _gsRenderTabs() {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const sources = Object.keys(_gsState.sources);
|
const sources = Object.keys(_gsState.sources);
|
||||||
if (sources.length < 2) { el.style.display = 'none'; return; }
|
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.style.display = 'flex';
|
||||||
el.innerHTML = sources.map(s => {
|
el.innerHTML = sources.map(s => {
|
||||||
const d = _gsState.sources[s];
|
const d = _gsState.sources[s];
|
||||||
|
|
@ -44811,7 +44812,7 @@ function _renderRedownloadStep1(overlay, track, data) {
|
||||||
|
|
||||||
const bestSource = data.best_match?.source || sources[0];
|
const bestSource = data.best_match?.source || sources[0];
|
||||||
const sourceIcons = { spotify: '🟢', itunes: '🍎', deezer: '🟣', hydrabase: '🔷' };
|
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
|
// Build columns — one per source, side by side
|
||||||
const columnsHtml = sources.map(source => {
|
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-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-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-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-source-tab.enh-tab-hydrabase.active { background: rgba(0, 180, 216, 0.2); color: #00b4d8; }
|
||||||
|
|
||||||
.enh-dropdown-section {
|
.enh-dropdown-section {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue