Merge pull request #274 from kettui/feat/metadata-client-caching
Add caching for metadata clients
This commit is contained in:
commit
8977120ba8
7 changed files with 280 additions and 125 deletions
|
|
@ -61,6 +61,9 @@ main.py
|
|||
ui/
|
||||
requirements.txt
|
||||
|
||||
# Dev-specific files
|
||||
requirements-dev.txt
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
|
|
@ -68,4 +71,4 @@ requirements.txt
|
|||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
|
|
|
|||
|
|
@ -224,6 +224,13 @@ python web_server.py
|
|||
# Open http://localhost:8008
|
||||
```
|
||||
|
||||
For local development and tests:
|
||||
|
||||
```bash
|
||||
pip install -r requirements-dev.txt
|
||||
pytest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup Guide
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ the logic. This prevents bugs where different files have different defaults
|
|||
or auth checks.
|
||||
"""
|
||||
|
||||
import threading
|
||||
from typing import List, Optional, Dict, Any, Literal
|
||||
from core.spotify_client import SpotifyClient
|
||||
from core.itunes_client import iTunesClient
|
||||
|
|
@ -16,6 +17,9 @@ logger = get_logger("metadata_service")
|
|||
|
||||
MetadataProvider = Literal["spotify", "itunes", "auto"]
|
||||
|
||||
_client_cache_lock = threading.RLock()
|
||||
_client_cache: Dict[str, Any] = {}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CANONICAL SOURCE SELECTION — all code should use these two functions
|
||||
|
|
@ -58,8 +62,89 @@ def get_primary_client():
|
|||
|
||||
This is THE single source of truth for "which client should I call?"
|
||||
"""
|
||||
source = get_primary_source()
|
||||
return _get_client_for_source(get_primary_source())
|
||||
|
||||
|
||||
def get_deezer_client():
|
||||
"""Get cached Deezer client.
|
||||
|
||||
Deezer client is safe to reuse across requests because it owns no
|
||||
request-specific state beyond the current access token.
|
||||
"""
|
||||
from core.deezer_client import DeezerClient
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
current_token = config_manager.get('deezer.access_token', None)
|
||||
except Exception:
|
||||
current_token = None
|
||||
|
||||
cache_key = f"deezer::{current_token or ''}"
|
||||
with _client_cache_lock:
|
||||
client = _client_cache.get(cache_key)
|
||||
if client is None:
|
||||
client = DeezerClient()
|
||||
_client_cache[cache_key] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_itunes_client():
|
||||
"""Get cached iTunes client."""
|
||||
with _client_cache_lock:
|
||||
client = _client_cache.get("itunes")
|
||||
if client is None:
|
||||
client = iTunesClient()
|
||||
_client_cache["itunes"] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_discogs_client(token: Optional[str] = None):
|
||||
"""Get cached Discogs client.
|
||||
|
||||
Discogs auth changes are token-driven, so the cache key tracks the
|
||||
current configured token.
|
||||
"""
|
||||
if token is None:
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
current_token = config_manager.get('discogs.token', '') or ''
|
||||
except Exception:
|
||||
current_token = ''
|
||||
else:
|
||||
current_token = token or ''
|
||||
|
||||
cache_key = f"discogs::{current_token}"
|
||||
with _client_cache_lock:
|
||||
client = _client_cache.get(cache_key)
|
||||
if client is None:
|
||||
from core.discogs_client import DiscogsClient
|
||||
client = DiscogsClient(token=current_token or None)
|
||||
_client_cache[cache_key] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_hydrabase_client():
|
||||
"""Return current Hydrabase client if connected, else iTunes fallback."""
|
||||
try:
|
||||
import importlib
|
||||
ws = importlib.import_module('web_server')
|
||||
client = getattr(ws, 'hydrabase_client', None)
|
||||
if client and client.is_connected():
|
||||
return client
|
||||
except Exception:
|
||||
pass
|
||||
return get_itunes_client()
|
||||
|
||||
|
||||
def clear_cached_metadata_clients():
|
||||
"""Clear cached metadata clients.
|
||||
|
||||
Useful for tests and config reload flows.
|
||||
"""
|
||||
with _client_cache_lock:
|
||||
_client_cache.clear()
|
||||
|
||||
|
||||
def _get_client_for_source(source: str):
|
||||
if source == 'spotify':
|
||||
try:
|
||||
import importlib
|
||||
|
|
@ -69,38 +154,18 @@ def get_primary_client():
|
|||
return sc
|
||||
except Exception:
|
||||
pass
|
||||
# Spotify selected but unavailable — fall back to Deezer
|
||||
from core.deezer_client import DeezerClient
|
||||
return DeezerClient()
|
||||
return get_deezer_client()
|
||||
|
||||
if source == 'deezer':
|
||||
from core.deezer_client import DeezerClient
|
||||
return DeezerClient()
|
||||
return get_deezer_client()
|
||||
|
||||
if source == 'discogs':
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
token = config_manager.get('discogs.token', '')
|
||||
if token:
|
||||
from core.discogs_client import DiscogsClient
|
||||
return DiscogsClient(token=token)
|
||||
except Exception:
|
||||
pass
|
||||
return iTunesClient()
|
||||
return get_discogs_client()
|
||||
|
||||
if source == 'hydrabase':
|
||||
try:
|
||||
import importlib
|
||||
ws = importlib.import_module('web_server')
|
||||
client = getattr(ws, 'hydrabase_client', None)
|
||||
if client and client.is_connected():
|
||||
return client
|
||||
except Exception:
|
||||
pass
|
||||
return iTunesClient()
|
||||
return get_hydrabase_client()
|
||||
|
||||
# Default: iTunes
|
||||
return iTunesClient()
|
||||
return get_itunes_client()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -141,7 +206,7 @@ class MetadataService:
|
|||
self.preferred_provider = preferred_provider
|
||||
self.spotify = SpotifyClient()
|
||||
self._fallback_source = get_primary_source()
|
||||
self.itunes = get_primary_client() # May be iTunesClient or DeezerClient
|
||||
self.itunes = _get_client_for_source(self._fallback_source)
|
||||
|
||||
self._log_initialization()
|
||||
|
||||
|
|
@ -311,13 +376,9 @@ class MetadataService:
|
|||
"""Reload configuration for both clients"""
|
||||
logger.info("Reloading metadata service configuration")
|
||||
self.spotify.reload_config()
|
||||
# Re-create fallback client in case the setting changed
|
||||
new_source = get_primary_source()
|
||||
if new_source != self._fallback_source:
|
||||
self._fallback_source = new_source
|
||||
self.itunes = get_primary_client()
|
||||
elif hasattr(self.itunes, 'reload_config'):
|
||||
self.itunes.reload_config()
|
||||
self._fallback_source = new_source
|
||||
self.itunes = _get_client_for_source(new_source)
|
||||
self._log_initialization()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1209,9 +1209,9 @@ class WatchlistScanner:
|
|||
results = client.search_artists(artist_name, limit=5)
|
||||
return self._best_artist_match(results, artist_name)
|
||||
|
||||
# Fallback: create a fresh Deezer client
|
||||
from core.deezer_client import DeezerClient
|
||||
client = DeezerClient()
|
||||
# Fallback: use cached Deezer client
|
||||
from core.metadata_service import get_deezer_client
|
||||
client = get_deezer_client()
|
||||
results = client.search_artists(artist_name, limit=5)
|
||||
return self._best_artist_match(results, artist_name)
|
||||
except Exception as e:
|
||||
|
|
@ -1221,8 +1221,8 @@ class WatchlistScanner:
|
|||
def _match_to_discogs(self, artist_name: str) -> Optional[str]:
|
||||
"""Match artist name to Discogs ID using fuzzy name comparison."""
|
||||
try:
|
||||
from core.discogs_client import DiscogsClient
|
||||
client = DiscogsClient()
|
||||
from core.metadata_service import get_discogs_client
|
||||
client = get_discogs_client()
|
||||
results = client.search_artists(artist_name, limit=5)
|
||||
return self._best_artist_match(results, artist_name)
|
||||
except Exception as e:
|
||||
|
|
|
|||
7
requirements-dev.txt
Normal file
7
requirements-dev.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# SoulSync development requirements
|
||||
# Runtime web dependencies + test runner
|
||||
|
||||
-r requirements-webui.txt
|
||||
|
||||
# Test runner
|
||||
pytest>=9.0.0
|
||||
107
tests/test_metadata_service_cache.py
Normal file
107
tests/test_metadata_service_cache.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
if "spotipy" not in sys.modules:
|
||||
spotipy = types.ModuleType("spotipy")
|
||||
|
||||
class _DummySpotify:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
oauth2 = types.ModuleType("spotipy.oauth2")
|
||||
|
||||
class _DummyOAuth:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
spotipy.Spotify = _DummySpotify
|
||||
oauth2.SpotifyOAuth = _DummyOAuth
|
||||
oauth2.SpotifyClientCredentials = _DummyOAuth
|
||||
spotipy.oauth2 = oauth2
|
||||
sys.modules["spotipy"] = spotipy
|
||||
sys.modules["spotipy.oauth2"] = oauth2
|
||||
|
||||
if "config.settings" not in sys.modules:
|
||||
config_pkg = types.ModuleType("config")
|
||||
settings_mod = types.ModuleType("config.settings")
|
||||
|
||||
class _DummyConfigManager:
|
||||
def get(self, key, default=None):
|
||||
return default
|
||||
|
||||
settings_mod.config_manager = _DummyConfigManager()
|
||||
config_pkg.settings = settings_mod
|
||||
sys.modules["config"] = config_pkg
|
||||
sys.modules["config.settings"] = settings_mod
|
||||
|
||||
from core import metadata_service
|
||||
from config.settings import config_manager
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_metadata_client_cache():
|
||||
metadata_service.clear_cached_metadata_clients()
|
||||
yield
|
||||
metadata_service.clear_cached_metadata_clients()
|
||||
|
||||
|
||||
def test_primary_client_is_cached_for_same_source(monkeypatch):
|
||||
calls = {"deezer": 0}
|
||||
|
||||
class FakeDeezerClient:
|
||||
def __init__(self):
|
||||
calls["deezer"] += 1
|
||||
|
||||
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: "deezer")
|
||||
monkeypatch.setattr("core.deezer_client.DeezerClient", FakeDeezerClient)
|
||||
|
||||
first = metadata_service.get_primary_client()
|
||||
second = metadata_service.get_primary_client()
|
||||
|
||||
assert first is second
|
||||
assert calls["deezer"] == 1
|
||||
|
||||
|
||||
def test_primary_client_switches_cache_by_source(monkeypatch):
|
||||
calls = {"deezer": 0, "itunes": 0}
|
||||
sources = iter(["deezer", "itunes"])
|
||||
|
||||
class FakeDeezerClient:
|
||||
def __init__(self):
|
||||
calls["deezer"] += 1
|
||||
|
||||
class FakeITunesClient:
|
||||
def __init__(self):
|
||||
calls["itunes"] += 1
|
||||
|
||||
monkeypatch.setattr(metadata_service, "get_primary_source", lambda: next(sources))
|
||||
monkeypatch.setattr("core.deezer_client.DeezerClient", FakeDeezerClient)
|
||||
monkeypatch.setattr(metadata_service, "iTunesClient", FakeITunesClient)
|
||||
|
||||
deezer_client = metadata_service.get_primary_client()
|
||||
itunes_client = metadata_service.get_primary_client()
|
||||
|
||||
assert deezer_client is not itunes_client
|
||||
assert calls["deezer"] == 1
|
||||
assert calls["itunes"] == 1
|
||||
|
||||
|
||||
def test_deezer_client_cache_tracks_token(monkeypatch):
|
||||
tokens = iter(["token-a", "token-b"])
|
||||
calls = {"deezer": 0}
|
||||
|
||||
class FakeDeezerClient:
|
||||
def __init__(self):
|
||||
calls["deezer"] += 1
|
||||
|
||||
monkeypatch.setattr("core.deezer_client.DeezerClient", FakeDeezerClient)
|
||||
monkeypatch.setattr(config_manager, "get", lambda key, default=None: next(tokens) if key == "deezer.access_token" else default)
|
||||
|
||||
first = metadata_service.get_deezer_client()
|
||||
second = metadata_service.get_deezer_client()
|
||||
|
||||
assert first is not second
|
||||
assert calls["deezer"] == 2
|
||||
142
web_server.py
142
web_server.py
|
|
@ -702,8 +702,7 @@ def _register_automation_handlers():
|
|||
|
||||
elif source == 'deezer':
|
||||
try:
|
||||
from core.deezer_client import DeezerClient
|
||||
deezer = DeezerClient()
|
||||
deezer = _get_deezer_client()
|
||||
playlist_data = deezer.get_playlist(source_id)
|
||||
if playlist_data and playlist_data.get('tracks'):
|
||||
tracks = []
|
||||
|
|
@ -7125,12 +7124,8 @@ def deezer_callback():
|
|||
config_manager.set('deezer.access_token', access_token)
|
||||
|
||||
# Reload the global deezer client to pick up the token
|
||||
global deezer_client
|
||||
if deezer_client:
|
||||
deezer_client.reload_config()
|
||||
else:
|
||||
from core.deezer_client import DeezerClient
|
||||
deezer_client = DeezerClient()
|
||||
deezer_client = _get_deezer_client()
|
||||
deezer_client.reload_config()
|
||||
|
||||
add_activity_item("✅", "Deezer Auth Complete", "Deezer account connected via OAuth", "Now")
|
||||
logger.info("Deezer OAuth authentication successful")
|
||||
|
|
@ -8024,15 +8019,13 @@ def enhanced_search_source(source_name):
|
|||
else:
|
||||
return jsonify({"artists": [], "albums": [], "tracks": [], "available": False})
|
||||
elif source_name == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
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)
|
||||
client = _get_discogs_client(token)
|
||||
else:
|
||||
return jsonify({"artists": [], "albums": [], "tracks": [], "available": False})
|
||||
elif source_name == 'hydrabase':
|
||||
|
|
@ -10208,8 +10201,7 @@ def get_artist_image(artist_id):
|
|||
source_override = request.args.get('source', '')
|
||||
|
||||
if source_override == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
image_url = client._get_artist_image_from_albums(artist_id)
|
||||
return jsonify({"success": True, "image_url": image_url})
|
||||
elif source_override == 'deezer':
|
||||
|
|
@ -10217,8 +10209,7 @@ def get_artist_image(artist_id):
|
|||
image_url = client._get_artist_image_from_albums(artist_id)
|
||||
return jsonify({"success": True, "image_url": image_url})
|
||||
elif source_override == 'discogs':
|
||||
from core.discogs_client import DiscogsClient
|
||||
client = DiscogsClient()
|
||||
client = _get_discogs_client()
|
||||
image_url = client._get_artist_image_from_albums(artist_id)
|
||||
return jsonify({"success": True, "image_url": image_url})
|
||||
elif source_override == 'hydrabase':
|
||||
|
|
@ -10228,8 +10219,7 @@ def get_artist_image(artist_id):
|
|||
client = _get_deezer_client()
|
||||
image_url = client._get_artist_image_from_albums(artist_id)
|
||||
elif plugin == 'itunes' or artist_id.isdigit():
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
image_url = client._get_artist_image_from_albums(artist_id)
|
||||
else:
|
||||
image_url = None
|
||||
|
|
@ -10283,8 +10273,7 @@ def get_artist_discography(artist_id):
|
|||
if albums:
|
||||
active_source = 'spotify'
|
||||
elif source_override == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_cl = iTunesClient()
|
||||
itunes_cl = _get_itunes_client()
|
||||
albums = itunes_cl.get_artist_albums(artist_id, album_type='album,single', limit=50)
|
||||
if albums:
|
||||
active_source = 'itunes'
|
||||
|
|
@ -10294,8 +10283,7 @@ def get_artist_discography(artist_id):
|
|||
if albums:
|
||||
active_source = 'deezer'
|
||||
elif source_override == 'discogs':
|
||||
from core.discogs_client import DiscogsClient
|
||||
discogs_cl = DiscogsClient()
|
||||
discogs_cl = _get_discogs_client()
|
||||
albums = discogs_cl.get_artist_albums(artist_id, album_type='album,single', limit=50)
|
||||
if albums:
|
||||
active_source = 'discogs'
|
||||
|
|
@ -10304,8 +10292,7 @@ def get_artist_discography(artist_id):
|
|||
if plugin == 'deezer':
|
||||
hb_cl = _get_deezer_client()
|
||||
elif plugin == 'itunes' or artist_id.isdigit():
|
||||
from core.itunes_client import iTunesClient
|
||||
hb_cl = iTunesClient()
|
||||
hb_cl = _get_itunes_client()
|
||||
else:
|
||||
hb_cl = spotify_client
|
||||
albums = hb_cl.get_artist_albums(artist_id, album_type='album,single', limit=50)
|
||||
|
|
@ -10315,20 +10302,17 @@ def get_artist_discography(artist_id):
|
|||
# If direct ID lookup failed but we have artist name, search by name
|
||||
if not albums and artist_name:
|
||||
if source_override == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
cl = iTunesClient()
|
||||
cl = _get_itunes_client()
|
||||
elif source_override == 'hydrabase':
|
||||
plugin = request.args.get('plugin', '').lower()
|
||||
if plugin == 'deezer':
|
||||
cl = _get_deezer_client()
|
||||
else:
|
||||
from core.itunes_client import iTunesClient
|
||||
cl = iTunesClient()
|
||||
cl = _get_itunes_client()
|
||||
elif source_override == 'deezer':
|
||||
cl = _get_deezer_client()
|
||||
elif source_override == 'discogs':
|
||||
from core.discogs_client import DiscogsClient
|
||||
cl = DiscogsClient()
|
||||
cl = _get_discogs_client()
|
||||
elif source_override == 'spotify' and spotify_available:
|
||||
cl = spotify_client
|
||||
else:
|
||||
|
|
@ -10690,20 +10674,17 @@ def get_artist_album_tracks(artist_id, album_id):
|
|||
source_override = request.args.get('source', '')
|
||||
client = spotify_client
|
||||
if source_override == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
elif source_override == 'hydrabase':
|
||||
plugin = request.args.get('plugin', '').lower()
|
||||
if plugin == 'deezer':
|
||||
client = _get_deezer_client()
|
||||
elif plugin == 'itunes' or album_id.isdigit():
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
elif source_override == 'deezer':
|
||||
client = _get_deezer_client()
|
||||
elif source_override == 'discogs':
|
||||
from core.discogs_client import DiscogsClient
|
||||
client = DiscogsClient()
|
||||
client = _get_discogs_client()
|
||||
|
||||
print(f"🎵 Fetching tracks for album: {album_id} by artist: {artist_id} (source: {source_override or 'auto'})")
|
||||
|
||||
|
|
@ -10806,8 +10787,7 @@ def download_discography(artist_id):
|
|||
else:
|
||||
fallback_src = _get_metadata_fallback_source()
|
||||
if fallback_src == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
elif fallback_src == 'deezer':
|
||||
client = _get_deezer_client()
|
||||
|
||||
|
|
@ -14139,8 +14119,7 @@ def redownload_search_metadata(track_id):
|
|||
if spotify_client and spotify_client.is_authenticated():
|
||||
sources_to_search.append(('spotify', spotify_client))
|
||||
try:
|
||||
from core.itunes_client import iTunesClient
|
||||
sources_to_search.append(('itunes', iTunesClient()))
|
||||
sources_to_search.append(('itunes', _get_itunes_client()))
|
||||
except Exception as e:
|
||||
logger.debug(f"iTunes client not available for redownload search: {e}")
|
||||
try:
|
||||
|
|
@ -14387,8 +14366,7 @@ def redownload_start(track_id):
|
|||
if full_track_details and full_track_details.get('album', {}).get('id'):
|
||||
full_album_data = spotify_client.get_album(full_track_details['album']['id'])
|
||||
elif meta_source == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
_it = iTunesClient()
|
||||
_it = _get_itunes_client()
|
||||
results = _it._lookup(id=meta_id, entity='song')
|
||||
if results:
|
||||
for r in results:
|
||||
|
|
@ -17544,8 +17522,7 @@ def _apply_path_template(template: str, context: dict) -> str:
|
|||
itunes_artist_id = context.get('_itunes_artist_id')
|
||||
if itunes_artist_id and (',' in album_artist_value or ' & ' in album_artist_value):
|
||||
try:
|
||||
from core.itunes_client import iTunesClient
|
||||
resolved = iTunesClient().resolve_primary_artist(itunes_artist_id)
|
||||
resolved = _get_itunes_client().resolve_primary_artist(itunes_artist_id)
|
||||
if resolved and resolved != album_artist_value:
|
||||
album_artist_value = resolved
|
||||
except Exception:
|
||||
|
|
@ -18050,8 +18027,7 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
|
|||
_src = original_search.get('_source') or _track_info_ctx.get('_source', '')
|
||||
if _aid.isdigit() and _src != 'deezer':
|
||||
try:
|
||||
from core.itunes_client import iTunesClient
|
||||
resolved = iTunesClient().resolve_primary_artist(_aid)
|
||||
resolved = _get_itunes_client().resolve_primary_artist(_aid)
|
||||
if resolved and resolved != _raw_album_artist:
|
||||
_raw_album_artist = resolved
|
||||
except Exception:
|
||||
|
|
@ -31234,23 +31210,20 @@ def get_album_tracks(album_id):
|
|||
# Use explicit source client when overridden (prevents numeric ID misrouting)
|
||||
client = spotify_client
|
||||
if source_override == 'itunes':
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
elif source_override == 'hydrabase':
|
||||
# Hydrabase IDs originate from whichever plugin the peer runs.
|
||||
# 'plugin' param is authoritative; fall back to ID format detection.
|
||||
plugin = request.args.get('plugin', '').lower()
|
||||
if plugin == 'itunes' or (not plugin and album_id.isdigit()):
|
||||
from core.itunes_client import iTunesClient
|
||||
client = iTunesClient()
|
||||
client = _get_itunes_client()
|
||||
elif plugin == 'deezer':
|
||||
client = _get_deezer_client()
|
||||
# else: spotify (default)
|
||||
elif source_override == 'deezer':
|
||||
client = _get_deezer_client()
|
||||
elif source_override == 'discogs':
|
||||
from core.discogs_client import DiscogsClient
|
||||
client = DiscogsClient()
|
||||
client = _get_discogs_client()
|
||||
|
||||
album_data = client.get_album(album_id)
|
||||
if not album_data:
|
||||
|
|
@ -31665,8 +31638,7 @@ def get_discover_album(source, album_id):
|
|||
fallback_client = _get_deezer_client()
|
||||
fallback_source = 'deezer'
|
||||
else:
|
||||
from core.itunes_client import iTunesClient
|
||||
fallback_client = iTunesClient()
|
||||
fallback_client = _get_itunes_client()
|
||||
fallback_source = 'itunes'
|
||||
|
||||
album_data = fallback_client.get_album(album_id)
|
||||
|
|
@ -33613,19 +33585,20 @@ def cancel_tidal_sync(playlist_id):
|
|||
deezer_discovery_states = {} # Key: playlist_id, Value: discovery state
|
||||
deezer_discovery_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="deezer_discovery")
|
||||
|
||||
# Lazy-initialized global DeezerClient instance
|
||||
_deezer_client_instance = None
|
||||
_deezer_client_lock = threading.Lock()
|
||||
|
||||
def _get_deezer_client():
|
||||
"""Get or create the global DeezerClient instance (thread-safe)."""
|
||||
global _deezer_client_instance
|
||||
if _deezer_client_instance is None:
|
||||
with _deezer_client_lock:
|
||||
if _deezer_client_instance is None:
|
||||
from core.deezer_client import DeezerClient
|
||||
_deezer_client_instance = DeezerClient()
|
||||
return _deezer_client_instance
|
||||
"""Get cached Deezer client."""
|
||||
from core.metadata_service import get_deezer_client
|
||||
return get_deezer_client()
|
||||
|
||||
def _get_itunes_client():
|
||||
"""Get cached iTunes client."""
|
||||
from core.metadata_service import get_itunes_client
|
||||
return get_itunes_client()
|
||||
|
||||
def _get_discogs_client(token=None):
|
||||
"""Get cached Discogs client."""
|
||||
from core.metadata_service import get_discogs_client
|
||||
return get_discogs_client(token)
|
||||
|
||||
def _get_metadata_fallback_source():
|
||||
"""Get the configured primary metadata source.
|
||||
|
|
@ -33650,17 +33623,13 @@ def _get_metadata_fallback_client():
|
|||
if source == 'discogs':
|
||||
token = config_manager.get('discogs.token', '')
|
||||
if token:
|
||||
from core.discogs_client import DiscogsClient
|
||||
return DiscogsClient(token=token)
|
||||
from core.itunes_client import iTunesClient
|
||||
return iTunesClient()
|
||||
return _get_discogs_client(token)
|
||||
return _get_itunes_client()
|
||||
if source == 'hydrabase':
|
||||
if hydrabase_client and hydrabase_client.is_connected():
|
||||
return hydrabase_client
|
||||
from core.itunes_client import iTunesClient
|
||||
return iTunesClient()
|
||||
from core.itunes_client import iTunesClient
|
||||
return iTunesClient()
|
||||
return _get_itunes_client()
|
||||
return _get_itunes_client()
|
||||
|
||||
@app.route('/api/deezer/arl-status', methods=['GET'])
|
||||
def get_deezer_arl_status():
|
||||
|
|
@ -38458,8 +38427,7 @@ def add_to_watchlist():
|
|||
try:
|
||||
if source == 'discogs':
|
||||
# Discogs: fetch artist image from API
|
||||
from core.discogs_client import DiscogsClient
|
||||
dc = DiscogsClient()
|
||||
dc = _get_discogs_client()
|
||||
dc_data = dc.get_artist(artist_id)
|
||||
if dc_data:
|
||||
image_url = dc_data.get('image_url')
|
||||
|
|
@ -42207,17 +42175,15 @@ def _match_liked_artists_to_all_sources(database, profile_id: int):
|
|||
if spotify_client and spotify_client.is_spotify_authenticated():
|
||||
search_clients['spotify'] = spotify_client
|
||||
try:
|
||||
from core.itunes_client import iTunesClient
|
||||
search_clients['itunes'] = iTunesClient()
|
||||
search_clients['itunes'] = _get_itunes_client()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
search_clients['deezer'] = DeezerClient()
|
||||
search_clients['deezer'] = _get_deezer_client()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from core.discogs_client import DiscogsClient
|
||||
dc = DiscogsClient()
|
||||
dc = _get_discogs_client()
|
||||
# Only use Discogs if token is configured
|
||||
from config.settings import config_manager as _cm
|
||||
if _cm.get('discogs.token', ''):
|
||||
|
|
@ -43230,8 +43196,7 @@ def get_artist_map_explore():
|
|||
center_genres = sa.genres if hasattr(sa, 'genres') else []
|
||||
artist_found = True
|
||||
if not artist_found:
|
||||
from core.itunes_client import iTunesClient
|
||||
ic = iTunesClient()
|
||||
ic = _get_itunes_client()
|
||||
results = ic.search_artists(artist_name, limit=1)
|
||||
if results and len(results) > 0:
|
||||
ia = results[0]
|
||||
|
|
@ -48902,15 +48867,20 @@ def start_oauth_callback_servers():
|
|||
def get_spotify_artist_discography(artist_name):
|
||||
"""Get complete artist discography from Spotify using proper matching"""
|
||||
try:
|
||||
from core.spotify_client import SpotifyClient
|
||||
from core.matching_engine import MusicMatchingEngine
|
||||
|
||||
print(f"🎵 Searching Spotify for artist: {artist_name}")
|
||||
|
||||
# Initialize clients
|
||||
spotify_client = SpotifyClient()
|
||||
# Reuse cached profile-aware Spotify client
|
||||
spotify_client = get_spotify_client_for_profile()
|
||||
matching_engine = MusicMatchingEngine()
|
||||
|
||||
if not spotify_client:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Spotify client unavailable'
|
||||
}
|
||||
|
||||
# Search for multiple potential matches (not just 1)
|
||||
artists = spotify_client.search_artists(artist_name, limit=5)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue