Move metadata API into package
- add package-owned metadata API, cache, registry, and lookup modules - keep legacy metadata_service and metadata_cache paths as explicit shims - update metadata call sites and tests to use package-owned helpers
This commit is contained in:
parent
e504099439
commit
a759f778b6
17 changed files with 2668 additions and 2305 deletions
|
|
@ -6,7 +6,7 @@ from typing import Dict, List, Optional, Any
|
|||
from functools import wraps
|
||||
from dataclasses import dataclass
|
||||
from utils.logging_config import get_logger
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
from core.metadata.cache import get_metadata_cache
|
||||
|
||||
logger = get_logger("deezer_client")
|
||||
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@ class DeezerDownloadClient:
|
|||
album_ids.add(str(aid))
|
||||
album_release_dates = {}
|
||||
try:
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
from core.metadata.cache import get_metadata_cache
|
||||
cache = get_metadata_cache()
|
||||
except Exception:
|
||||
cache = None
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import re
|
|||
import time
|
||||
import threading
|
||||
import requests
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
from core.metadata.cache import get_metadata_cache
|
||||
from typing import List, Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import threading
|
|||
from functools import wraps
|
||||
from dataclasses import dataclass
|
||||
from utils.logging_config import get_logger
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
from core.metadata.cache import get_metadata_cache
|
||||
|
||||
logger = get_logger("itunes_client")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,76 @@
|
|||
"""Metadata helper package."""
|
||||
"""Metadata package public surface."""
|
||||
|
||||
from core.metadata.api import (
|
||||
MetadataProvider,
|
||||
MetadataService,
|
||||
check_album_completion,
|
||||
check_artist_discography_completion,
|
||||
check_single_completion,
|
||||
clear_cached_metadata_clients,
|
||||
get_album_for_source,
|
||||
get_album_tracks_for_source,
|
||||
get_artist_album_tracks,
|
||||
get_artist_albums_for_source,
|
||||
get_artist_detail_discography,
|
||||
get_artist_discography,
|
||||
get_artist_image_url,
|
||||
get_client_for_source,
|
||||
get_deezer_client,
|
||||
get_discogs_client,
|
||||
get_hydrabase_client,
|
||||
get_itunes_client,
|
||||
get_metadata_service,
|
||||
get_musicmap_similar_artists,
|
||||
get_primary_client,
|
||||
get_primary_source,
|
||||
get_spotify_client,
|
||||
get_source_priority,
|
||||
iter_artist_discography_completion_events,
|
||||
iter_musicmap_similar_artist_events,
|
||||
is_hydrabase_enabled,
|
||||
resolve_album_reference,
|
||||
)
|
||||
from core.metadata.cache import MetadataCache, get_metadata_cache
|
||||
from core.metadata.lookup import MetadataLookupOptions
|
||||
from core.metadata.registry import (
|
||||
METADATA_SOURCE_PRIORITY,
|
||||
get_registered_runtime_client,
|
||||
register_runtime_clients,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"METADATA_SOURCE_PRIORITY",
|
||||
"MetadataCache",
|
||||
"MetadataLookupOptions",
|
||||
"MetadataProvider",
|
||||
"MetadataService",
|
||||
"check_album_completion",
|
||||
"check_artist_discography_completion",
|
||||
"check_single_completion",
|
||||
"clear_cached_metadata_clients",
|
||||
"get_album_for_source",
|
||||
"get_album_tracks_for_source",
|
||||
"get_artist_album_tracks",
|
||||
"get_artist_albums_for_source",
|
||||
"get_artist_detail_discography",
|
||||
"get_artist_discography",
|
||||
"get_artist_image_url",
|
||||
"get_client_for_source",
|
||||
"get_deezer_client",
|
||||
"get_discogs_client",
|
||||
"get_hydrabase_client",
|
||||
"get_itunes_client",
|
||||
"get_metadata_cache",
|
||||
"get_metadata_service",
|
||||
"get_musicmap_similar_artists",
|
||||
"get_primary_client",
|
||||
"get_primary_source",
|
||||
"get_registered_runtime_client",
|
||||
"get_spotify_client",
|
||||
"get_source_priority",
|
||||
"iter_artist_discography_completion_events",
|
||||
"iter_musicmap_similar_artist_events",
|
||||
"is_hydrabase_enabled",
|
||||
"register_runtime_clients",
|
||||
"resolve_album_reference",
|
||||
]
|
||||
|
|
|
|||
2003
core/metadata/api.py
Normal file
2003
core/metadata/api.py
Normal file
File diff suppressed because it is too large
Load diff
22
core/metadata/lookup.py
Normal file
22
core/metadata/lookup.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
"""Shared metadata lookup policy objects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
|
||||
__all__ = ["MetadataLookupOptions"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MetadataLookupOptions:
|
||||
"""Generic metadata lookup policy shared by metadata services."""
|
||||
|
||||
source_override: Optional[str] = None
|
||||
allow_fallback: bool = True
|
||||
skip_cache: bool = False
|
||||
max_pages: int = 0
|
||||
limit: int = 50
|
||||
artist_source_ids: Optional[Dict[str, str]] = None
|
||||
dedup_variants: bool = True
|
||||
|
||||
274
core/metadata/registry.py
Normal file
274
core/metadata/registry.py
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
"""Metadata client registry and source selection.
|
||||
|
||||
Owns shared metadata client singletons, runtime client registration, and
|
||||
canonical source selection. Package-internal code should use this module
|
||||
instead of importing `web_server`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("metadata.registry")
|
||||
|
||||
MetadataClientFactory = Callable[[], Any]
|
||||
|
||||
METADATA_SOURCE_PRIORITY = ("deezer", "itunes", "spotify", "discogs", "hydrabase")
|
||||
|
||||
_UNSET = object()
|
||||
_client_cache_lock = threading.RLock()
|
||||
_client_cache: Dict[str, Any] = {}
|
||||
|
||||
_runtime_clients_lock = threading.RLock()
|
||||
_runtime_clients: Dict[str, Any] = {
|
||||
"spotify": None,
|
||||
"hydrabase": None,
|
||||
}
|
||||
_dev_mode_enabled_provider: Callable[[], bool] = lambda: False
|
||||
|
||||
|
||||
def register_runtime_clients(
|
||||
*,
|
||||
spotify_client: Any = _UNSET,
|
||||
hydrabase_client: Any = _UNSET,
|
||||
dev_mode_enabled_provider: Optional[Callable[[], bool]] = _UNSET,
|
||||
) -> None:
|
||||
"""Register app-owned runtime clients.
|
||||
|
||||
`None` is a valid value and clears the registered client. Omitted
|
||||
arguments leave the current registration unchanged.
|
||||
"""
|
||||
global _dev_mode_enabled_provider
|
||||
with _runtime_clients_lock:
|
||||
if spotify_client is not _UNSET:
|
||||
_runtime_clients["spotify"] = spotify_client
|
||||
if hydrabase_client is not _UNSET:
|
||||
_runtime_clients["hydrabase"] = hydrabase_client
|
||||
if dev_mode_enabled_provider is not _UNSET:
|
||||
_dev_mode_enabled_provider = dev_mode_enabled_provider or (lambda: False)
|
||||
|
||||
|
||||
def get_registered_runtime_client(name: str) -> Any:
|
||||
with _runtime_clients_lock:
|
||||
return _runtime_clients.get(name)
|
||||
|
||||
|
||||
def clear_cached_metadata_clients() -> None:
|
||||
"""Clear lazily-created client singletons.
|
||||
|
||||
Runtime clients registered by the host app stay in place.
|
||||
"""
|
||||
with _client_cache_lock:
|
||||
_client_cache.clear()
|
||||
|
||||
|
||||
def _get_config_value(key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
|
||||
return config_manager.get(key, default)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _get_spotify_factory(client_factory: Optional[MetadataClientFactory]) -> MetadataClientFactory:
|
||||
if client_factory is not None:
|
||||
return client_factory
|
||||
from core.spotify_client import SpotifyClient
|
||||
|
||||
return SpotifyClient
|
||||
|
||||
|
||||
def _get_itunes_factory(client_factory: Optional[MetadataClientFactory]) -> MetadataClientFactory:
|
||||
if client_factory is not None:
|
||||
return client_factory
|
||||
from core.itunes_client import iTunesClient
|
||||
|
||||
return iTunesClient
|
||||
|
||||
|
||||
def _get_deezer_factory(client_factory: Optional[MetadataClientFactory]) -> MetadataClientFactory:
|
||||
if client_factory is not None:
|
||||
return client_factory
|
||||
from core.deezer_client import DeezerClient
|
||||
|
||||
return DeezerClient
|
||||
|
||||
|
||||
def _get_discogs_factory(client_factory: Optional[MetadataClientFactory]) -> MetadataClientFactory:
|
||||
if client_factory is not None:
|
||||
return client_factory
|
||||
from core.discogs_client import DiscogsClient
|
||||
|
||||
return DiscogsClient
|
||||
|
||||
|
||||
def get_spotify_client(client_factory: Optional[MetadataClientFactory] = None):
|
||||
"""Get shared Spotify client.
|
||||
|
||||
Prefers the app-registered runtime client. Falls back to a lazily
|
||||
cached singleton if no runtime client was registered.
|
||||
"""
|
||||
runtime_client = get_registered_runtime_client("spotify")
|
||||
if runtime_client is not None:
|
||||
return runtime_client
|
||||
|
||||
cache_key = "spotify"
|
||||
factory = _get_spotify_factory(client_factory)
|
||||
with _client_cache_lock:
|
||||
client = _client_cache.get(cache_key)
|
||||
if client is None:
|
||||
client = factory()
|
||||
_client_cache[cache_key] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_deezer_client(client_factory: Optional[MetadataClientFactory] = None):
|
||||
"""Get cached Deezer client keyed by current access token."""
|
||||
current_token = _get_config_value("deezer.access_token", None)
|
||||
cache_key = f"deezer::{current_token or ''}"
|
||||
factory = _get_deezer_factory(client_factory)
|
||||
with _client_cache_lock:
|
||||
client = _client_cache.get(cache_key)
|
||||
if client is None:
|
||||
client = factory()
|
||||
_client_cache[cache_key] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_itunes_client(client_factory: Optional[MetadataClientFactory] = None):
|
||||
"""Get cached iTunes client."""
|
||||
cache_key = "itunes"
|
||||
factory = _get_itunes_factory(client_factory)
|
||||
with _client_cache_lock:
|
||||
client = _client_cache.get(cache_key)
|
||||
if client is None:
|
||||
client = factory()
|
||||
_client_cache[cache_key] = client
|
||||
return client
|
||||
|
||||
|
||||
def get_discogs_client(
|
||||
token: Optional[str] = None,
|
||||
client_factory: Optional[MetadataClientFactory] = None,
|
||||
):
|
||||
"""Get cached Discogs client keyed by token."""
|
||||
if token is None:
|
||||
current_token = _get_config_value("discogs.token", "") or ""
|
||||
else:
|
||||
current_token = token or ""
|
||||
|
||||
cache_key = f"discogs::{current_token}"
|
||||
factory = _get_discogs_factory(client_factory)
|
||||
with _client_cache_lock:
|
||||
client = _client_cache.get(cache_key)
|
||||
if client is None:
|
||||
client = factory(token=current_token or None) # type: ignore[misc]
|
||||
_client_cache[cache_key] = client
|
||||
return client
|
||||
|
||||
|
||||
def is_hydrabase_enabled() -> bool:
|
||||
"""Return True when Hydrabase is connected and app-enabled."""
|
||||
try:
|
||||
client = get_registered_runtime_client("hydrabase")
|
||||
if not client or not client.is_connected():
|
||||
return False
|
||||
return bool(_dev_mode_enabled_provider())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_hydrabase_client(allow_fallback: bool = True, require_enabled: bool = True):
|
||||
"""Return registered Hydrabase client or iTunes fallback."""
|
||||
try:
|
||||
client = get_registered_runtime_client("hydrabase")
|
||||
if client and client.is_connected():
|
||||
if not require_enabled or bool(_dev_mode_enabled_provider()):
|
||||
return client
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if allow_fallback:
|
||||
return get_itunes_client()
|
||||
return None
|
||||
|
||||
|
||||
def get_primary_source(spotify_client_factory: Optional[MetadataClientFactory] = None) -> str:
|
||||
"""Return configured primary metadata source."""
|
||||
source = _get_config_value("metadata.fallback_source", "deezer") or "deezer"
|
||||
|
||||
if source == "spotify":
|
||||
try:
|
||||
spotify = get_spotify_client(client_factory=spotify_client_factory)
|
||||
if not spotify or not spotify.is_spotify_authenticated():
|
||||
return "deezer"
|
||||
except Exception:
|
||||
return "deezer"
|
||||
|
||||
return source
|
||||
|
||||
|
||||
def get_source_priority(preferred_source: str):
|
||||
"""Return source priority with preferred source first."""
|
||||
ordered = []
|
||||
if preferred_source in METADATA_SOURCE_PRIORITY:
|
||||
ordered.append(preferred_source)
|
||||
|
||||
for source in METADATA_SOURCE_PRIORITY:
|
||||
if source not in ordered:
|
||||
ordered.append(source)
|
||||
return ordered
|
||||
|
||||
|
||||
def get_primary_client(
|
||||
*,
|
||||
spotify_client_factory: Optional[MetadataClientFactory] = None,
|
||||
itunes_client_factory: Optional[MetadataClientFactory] = None,
|
||||
deezer_client_factory: Optional[MetadataClientFactory] = None,
|
||||
discogs_client_factory: Optional[MetadataClientFactory] = None,
|
||||
):
|
||||
"""Return client for configured primary source."""
|
||||
return get_client_for_source(
|
||||
get_primary_source(spotify_client_factory=spotify_client_factory),
|
||||
spotify_client_factory=spotify_client_factory,
|
||||
itunes_client_factory=itunes_client_factory,
|
||||
deezer_client_factory=deezer_client_factory,
|
||||
discogs_client_factory=discogs_client_factory,
|
||||
)
|
||||
|
||||
|
||||
def get_client_for_source(
|
||||
source: str,
|
||||
*,
|
||||
spotify_client_factory: Optional[MetadataClientFactory] = None,
|
||||
itunes_client_factory: Optional[MetadataClientFactory] = None,
|
||||
deezer_client_factory: Optional[MetadataClientFactory] = None,
|
||||
discogs_client_factory: Optional[MetadataClientFactory] = None,
|
||||
):
|
||||
"""Return exact client for a source, or None if unavailable."""
|
||||
if source == "spotify":
|
||||
try:
|
||||
client = get_spotify_client(client_factory=spotify_client_factory)
|
||||
if client and client.is_spotify_authenticated():
|
||||
return client
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
if source == "deezer":
|
||||
return get_deezer_client(client_factory=deezer_client_factory)
|
||||
|
||||
if source == "discogs":
|
||||
return get_discogs_client(client_factory=discogs_client_factory)
|
||||
|
||||
if source == "hydrabase":
|
||||
return get_hydrabase_client(allow_fallback=False)
|
||||
|
||||
if source == "itunes":
|
||||
return get_itunes_client(client_factory=itunes_client_factory)
|
||||
|
||||
return None
|
||||
174
core/metadata/service.py
Normal file
174
core/metadata/service.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""Compatibility metadata service facade.
|
||||
|
||||
The modern lookup code prefers standalone functions and shared registry
|
||||
helpers, but the legacy `MetadataService` wrapper remains available for
|
||||
call sites that still expect an object.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Literal
|
||||
|
||||
from core.metadata.registry import (
|
||||
get_client_for_source,
|
||||
get_primary_source,
|
||||
get_spotify_client,
|
||||
)
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("metadata_service")
|
||||
|
||||
MetadataProvider = Literal["spotify", "itunes", "auto"]
|
||||
|
||||
|
||||
class MetadataService:
|
||||
"""
|
||||
Unified metadata service that seamlessly switches between Spotify and
|
||||
the configured fallback source.
|
||||
"""
|
||||
|
||||
def __init__(self, preferred_provider: MetadataProvider = "auto"):
|
||||
self.preferred_provider = preferred_provider
|
||||
try:
|
||||
self.spotify = get_spotify_client()
|
||||
except Exception:
|
||||
self.spotify = None
|
||||
self._fallback_source = get_primary_source()
|
||||
try:
|
||||
self.itunes = get_client_for_source(self._fallback_source)
|
||||
except Exception:
|
||||
self.itunes = None
|
||||
self._log_initialization()
|
||||
|
||||
def _log_initialization(self):
|
||||
spotify_status = "Authenticated" if self.spotify and self.spotify.is_spotify_authenticated() else "Not authenticated"
|
||||
fallback_status = "Available" if self.itunes and getattr(self.itunes, "is_authenticated", lambda: False)() else "Not available"
|
||||
|
||||
logger.info(
|
||||
"MetadataService initialized - Spotify: %s, %s: %s",
|
||||
spotify_status,
|
||||
self._fallback_source.capitalize(),
|
||||
fallback_status,
|
||||
)
|
||||
logger.info("Preferred provider: %s", self.preferred_provider)
|
||||
|
||||
def get_active_provider(self) -> str:
|
||||
if self.preferred_provider == "spotify":
|
||||
return "spotify"
|
||||
if self.preferred_provider == "itunes":
|
||||
return self._fallback_source
|
||||
return get_primary_source()
|
||||
|
||||
def _get_client(self):
|
||||
provider = self.get_active_provider()
|
||||
if provider == "spotify":
|
||||
if not self.spotify or not self.spotify.is_spotify_authenticated():
|
||||
logger.warning(
|
||||
"Spotify requested but not authenticated, falling back to %s",
|
||||
self._fallback_source,
|
||||
)
|
||||
return self.itunes
|
||||
return self.spotify
|
||||
return self.itunes
|
||||
|
||||
def search_tracks(self, query: str, limit: int = 20) -> List:
|
||||
client = self._get_client()
|
||||
provider = self.get_active_provider()
|
||||
logger.debug("Searching tracks with %s: %r", provider, query)
|
||||
return client.search_tracks(query, limit)
|
||||
|
||||
def search_artists(self, query: str, limit: int = 20) -> List:
|
||||
client = self._get_client()
|
||||
provider = self.get_active_provider()
|
||||
logger.debug("Searching artists with %s: %r", provider, query)
|
||||
return client.search_artists(query, limit)
|
||||
|
||||
def search_albums(self, query: str, limit: int = 20) -> List:
|
||||
client = self._get_client()
|
||||
provider = self.get_active_provider()
|
||||
logger.debug("Searching albums with %s: %r", provider, query)
|
||||
return client.search_albums(query, limit)
|
||||
|
||||
def get_track_details(self, track_id: str) -> Optional[Dict[str, Any]]:
|
||||
client = self._get_client()
|
||||
return client.get_track_details(track_id)
|
||||
|
||||
def get_album(self, album_id: str) -> Optional[Dict[str, Any]]:
|
||||
client = self._get_client()
|
||||
return client.get_album(album_id)
|
||||
|
||||
def get_album_tracks(self, album_id: str) -> Optional[Dict[str, Any]]:
|
||||
client = self._get_client()
|
||||
provider = self.get_active_provider()
|
||||
logger.debug("Fetching album tracks with %s: %s", provider, album_id)
|
||||
return client.get_album_tracks(album_id)
|
||||
|
||||
def get_artist(self, artist_id: str) -> Optional[Dict[str, Any]]:
|
||||
client = self._get_client()
|
||||
return client.get_artist(artist_id)
|
||||
|
||||
def get_artist_albums(self, artist_id: str, album_type: str = "album,single", limit: int = 50) -> List:
|
||||
client = self._get_client()
|
||||
provider = self.get_active_provider()
|
||||
logger.debug("Fetching artist albums with %s: %s", provider, artist_id)
|
||||
return client.get_artist_albums(artist_id, album_type, limit)
|
||||
|
||||
def get_track_features(self, track_id: str) -> Optional[Dict[str, Any]]:
|
||||
client = self._get_client()
|
||||
return client.get_track_features(track_id)
|
||||
|
||||
def get_user_playlists(self) -> List:
|
||||
if self.spotify and self.spotify.is_spotify_authenticated():
|
||||
return self.spotify.get_user_playlists()
|
||||
logger.warning("User playlists only available with Spotify authentication")
|
||||
return []
|
||||
|
||||
def get_saved_tracks(self) -> List:
|
||||
if self.spotify and self.spotify.is_spotify_authenticated():
|
||||
return self.spotify.get_saved_tracks()
|
||||
logger.warning("Saved tracks only available with Spotify authentication")
|
||||
return []
|
||||
|
||||
def get_saved_tracks_count(self) -> int:
|
||||
if self.spotify and self.spotify.is_spotify_authenticated():
|
||||
return self.spotify.get_saved_tracks_count()
|
||||
return 0
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
return bool(self.spotify and self.spotify.is_spotify_authenticated()) or bool(
|
||||
self.itunes and getattr(self.itunes, "is_authenticated", lambda: False)()
|
||||
)
|
||||
|
||||
def get_provider_info(self) -> Dict[str, Any]:
|
||||
spotify_authenticated = bool(self.spotify and self.spotify.is_spotify_authenticated())
|
||||
itunes_available = bool(self.itunes and getattr(self.itunes, "is_authenticated", lambda: False)())
|
||||
return {
|
||||
"active_provider": self.get_active_provider(),
|
||||
"spotify_authenticated": spotify_authenticated,
|
||||
"itunes_available": itunes_available,
|
||||
"fallback_source": self._fallback_source,
|
||||
"preferred_provider": self.preferred_provider,
|
||||
"can_access_user_data": spotify_authenticated,
|
||||
}
|
||||
|
||||
def reload_config(self):
|
||||
logger.info("Reloading metadata service configuration")
|
||||
if self.spotify and hasattr(self.spotify, "reload_config"):
|
||||
self.spotify.reload_config()
|
||||
new_source = get_primary_source()
|
||||
self._fallback_source = new_source
|
||||
try:
|
||||
self.itunes = get_client_for_source(new_source)
|
||||
except Exception:
|
||||
self.itunes = None
|
||||
self._log_initialization()
|
||||
|
||||
|
||||
_metadata_service_instance: Optional[MetadataService] = None
|
||||
|
||||
|
||||
def get_metadata_service() -> MetadataService:
|
||||
global _metadata_service_instance
|
||||
if _metadata_service_instance is None:
|
||||
_metadata_service_instance = MetadataService()
|
||||
return _metadata_service_instance
|
||||
|
|
@ -23,7 +23,7 @@ from core.imports.context import (
|
|||
get_source_tag_names,
|
||||
normalize_import_context,
|
||||
)
|
||||
from core.metadata_service import get_itunes_client
|
||||
from core.metadata.registry import get_itunes_client
|
||||
from database.music_database import get_database
|
||||
from core.metadata.common import (
|
||||
get_config_manager,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -196,7 +196,7 @@ class RepairWorker:
|
|||
def metadata_cache(self):
|
||||
if self._metadata_cache is None:
|
||||
try:
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
from core.metadata.cache import get_metadata_cache
|
||||
self._metadata_cache = get_metadata_cache()
|
||||
except Exception as e:
|
||||
logger.error("Failed to get metadata cache: %s", e)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from functools import wraps
|
|||
from dataclasses import dataclass
|
||||
from utils.logging_config import get_logger
|
||||
from config.settings import config_manager
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
from core.metadata.cache import get_metadata_cache
|
||||
|
||||
logger = get_logger("spotify_client")
|
||||
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@ class WatchlistScanner:
|
|||
def metadata_service(self):
|
||||
"""Get or create MetadataService instance (lazy loading)"""
|
||||
if self._metadata_service is None:
|
||||
from core.metadata_service import MetadataService
|
||||
from core.metadata.service import MetadataService
|
||||
self._metadata_service = MetadataService()
|
||||
return self._metadata_service
|
||||
|
||||
|
|
@ -1642,7 +1642,7 @@ class WatchlistScanner:
|
|||
return self._best_artist_match(results, artist_name)
|
||||
|
||||
# Fallback: use cached Deezer client
|
||||
from core.metadata_service import get_deezer_client
|
||||
from core.metadata.registry import get_deezer_client
|
||||
client = get_deezer_client()
|
||||
results = client.search_artists(artist_name, limit=5)
|
||||
return self._best_artist_match(results, artist_name)
|
||||
|
|
@ -1653,7 +1653,7 @@ 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.metadata_service import get_discogs_client
|
||||
from core.metadata.registry import get_discogs_client
|
||||
client = get_discogs_client()
|
||||
results = client.search_artists(artist_name, limit=5)
|
||||
return self._best_artist_match(results, artist_name)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from unittest.mock import MagicMock
|
|||
|
||||
import pytest
|
||||
|
||||
from core.metadata_cache import MetadataCache
|
||||
from core.metadata.cache import MetadataCache
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ from core.tidal_client import TidalClient # Added import for Tidal
|
|||
from core.matching_engine import MusicMatchingEngine
|
||||
from core.database_update_worker import DatabaseUpdateWorker
|
||||
from core.web_scan_manager import WebScanManager
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
from core.metadata.cache import get_metadata_cache
|
||||
from core.metadata.registry import register_runtime_clients as register_metadata_runtime_clients
|
||||
from core.imports.context import (
|
||||
get_import_clean_album,
|
||||
get_import_clean_title,
|
||||
|
|
@ -6039,7 +6040,7 @@ _comparison_lock = threading.Lock()
|
|||
def _is_hydrabase_active():
|
||||
"""Check if Hydrabase is connected and enabled for metadata use."""
|
||||
try:
|
||||
from core.metadata_service import is_hydrabase_enabled
|
||||
from core.metadata.registry import is_hydrabase_enabled
|
||||
return is_hydrabase_enabled()
|
||||
except Exception:
|
||||
return False
|
||||
|
|
@ -10274,7 +10275,8 @@ def get_artist_detail(artist_id):
|
|||
# Get source-priority discography for proper categorization and missing releases
|
||||
artist_detail_discography = None
|
||||
try:
|
||||
from core.metadata_service import MetadataLookupOptions, get_artist_detail_discography as _get_artist_detail_discography
|
||||
from core.metadata.lookup import MetadataLookupOptions
|
||||
from core.metadata_service import get_artist_detail_discography as _get_artist_detail_discography
|
||||
|
||||
artist_source_ids = {
|
||||
'spotify': artist_info.get('spotify_artist_id'),
|
||||
|
|
@ -10510,7 +10512,8 @@ def get_artist_discography(artist_id):
|
|||
else:
|
||||
effective_override_source = 'spotify'
|
||||
|
||||
from core.metadata_service import MetadataLookupOptions, get_artist_discography as _get_artist_discography
|
||||
from core.metadata.lookup import MetadataLookupOptions
|
||||
from core.metadata_service import get_artist_discography as _get_artist_discography
|
||||
|
||||
discography = _get_artist_discography(
|
||||
artist_id,
|
||||
|
|
@ -25122,38 +25125,38 @@ deezer_discovery_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix
|
|||
|
||||
def _get_deezer_client():
|
||||
"""Get cached Deezer client."""
|
||||
from core.metadata_service import get_deezer_client
|
||||
from core.metadata.registry import get_deezer_client
|
||||
return get_deezer_client()
|
||||
|
||||
def _get_itunes_client():
|
||||
"""Get cached iTunes client."""
|
||||
from core.metadata_service import get_itunes_client
|
||||
from core.metadata.registry 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
|
||||
from core.metadata.registry import get_discogs_client
|
||||
return get_discogs_client(token)
|
||||
|
||||
def _get_metadata_fallback_source():
|
||||
"""Get the configured primary metadata source.
|
||||
Returns 'spotify', 'itunes', 'deezer', 'discogs', or 'hydrabase'.
|
||||
|
||||
NOTE: This is a thin wrapper — canonical logic lives in core.metadata_service.get_primary_source().
|
||||
NOTE: This is a thin wrapper — canonical logic lives in core.metadata.registry.get_primary_source().
|
||||
Kept as a local function because 70+ callers reference it by name."""
|
||||
from core.metadata_service import get_primary_source
|
||||
from core.metadata.registry import get_primary_source
|
||||
return get_primary_source()
|
||||
|
||||
def _get_metadata_fallback_client():
|
||||
"""Get the active metadata client based on settings.
|
||||
Returns a SpotifyClient, iTunesClient, DeezerClient, DiscogsClient, or HydrabaseClient instance."""
|
||||
source = _get_metadata_fallback_source()
|
||||
from core.metadata.registry import get_client_for_source
|
||||
|
||||
client = get_client_for_source(source)
|
||||
if client is not None:
|
||||
return client
|
||||
if source == 'spotify':
|
||||
if spotify_client and spotify_client.is_spotify_authenticated():
|
||||
return spotify_client
|
||||
# Spotify selected but not authed — fall back to deezer
|
||||
return _get_deezer_client()
|
||||
if source == 'deezer':
|
||||
return _get_deezer_client()
|
||||
if source == 'discogs':
|
||||
token = config_manager.get('discogs.token', '')
|
||||
|
|
@ -29094,7 +29097,7 @@ def start_watchlist_scan():
|
|||
"""Start a watchlist scan for new releases"""
|
||||
try:
|
||||
# Check if MetadataService can provide a working client (Spotify OR fallback)
|
||||
from core.metadata_service import MetadataService
|
||||
from core.metadata.service import MetadataService
|
||||
metadata_service = MetadataService()
|
||||
|
||||
# Get active provider - will be spotify or the configured fallback
|
||||
|
|
@ -29581,7 +29584,7 @@ def watchlist_artist_config(artist_id):
|
|||
'preferred_metadata_source': result[17] if len(result) > 17 else None,
|
||||
}
|
||||
|
||||
from core.metadata_service import get_primary_source
|
||||
from core.metadata.registry import get_primary_source
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"config": config,
|
||||
|
|
@ -30332,7 +30335,7 @@ def _get_active_discovery_source():
|
|||
|
||||
NOTE: Thin wrapper — canonical logic lives in core.metadata_service.get_primary_source().
|
||||
"""
|
||||
from core.metadata_service import get_primary_source
|
||||
from core.metadata.registry import get_primary_source
|
||||
return get_primary_source()
|
||||
|
||||
|
||||
|
|
@ -36121,7 +36124,6 @@ def enrich_beatport_tracks():
|
|||
uncached_tracks = []
|
||||
uncached_indices = []
|
||||
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
mcache = get_metadata_cache()
|
||||
|
||||
for i, track in enumerate(tracks):
|
||||
|
|
@ -39694,6 +39696,12 @@ except Exception as e:
|
|||
hydrabase_worker = None
|
||||
hydrabase_client = None
|
||||
|
||||
register_metadata_runtime_clients(
|
||||
spotify_client=spotify_client,
|
||||
hydrabase_client=hydrabase_client,
|
||||
dev_mode_enabled_provider=lambda: dev_mode_enabled,
|
||||
)
|
||||
|
||||
# --- Hydrabase Auto-Reconnect ---
|
||||
try:
|
||||
_hydra_cfg = config_manager.get_hydrabase_config()
|
||||
|
|
@ -40212,7 +40220,6 @@ def repair_findings_counts():
|
|||
def repair_cache_health():
|
||||
"""Get metadata cache health stats for the repair dashboard"""
|
||||
try:
|
||||
from core.metadata_cache import get_metadata_cache
|
||||
cache = get_metadata_cache()
|
||||
return jsonify(cache.get_health_stats()), 200
|
||||
except Exception as e:
|
||||
|
|
@ -40551,7 +40558,7 @@ def import_search_albums():
|
|||
return jsonify({'success': False, 'error': 'Missing query parameter'}), 400
|
||||
|
||||
limit = min(int(request.args.get('limit', 12)), 50)
|
||||
from core.metadata_service import get_primary_source
|
||||
from core.metadata.registry import get_primary_source
|
||||
|
||||
if get_primary_source() == 'hydrabase' and hydrabase_worker and dev_mode_enabled:
|
||||
hydrabase_worker.enqueue(query, 'albums')
|
||||
|
|
|
|||
Loading…
Reference in a new issue