Groundwork for unifying Discover-page playlists (ListenBrainz, Last.fm radio, SoulSync Discovery) with Sync-page playlists (Spotify, Tidal, Qobuz, YouTube, Spotify public, iTunes link). All nine sources now expose the same `PlaylistSource` Protocol so callers stop having to branch per-source. This commit only adds the abstraction — no dispatch sites collapse to the registry yet, no DB or UI changes. Adapters wrap existing clients via injected getter callables to avoid eager imports of web_server.py globals. - core/playlists/sources/base.py — PlaylistMeta, NormalizedTrack, PlaylistDetail dataclasses + PlaylistSource Protocol with supports_listing / supports_refresh / requires_auth capability flags. needs_discovery flag on NormalizedTrack marks tracks that carry raw MB metadata (LB, Last.fm) vs tracks already matched to a provider ID (everything else). - core/playlists/sources/registry.py — thread-safe lazy-factory registry with instance caching + re-register invalidation. - nine adapters in core/playlists/sources/ wrapping SpotifyClient, TidalClient, QobuzClient, spotify_public_scraper, the YouTube + iTunes-link parsers (via injected callables), ListenBrainzManager, Last.fm radio rows in the ListenBrainz cache, and PersonalizedPlaylistManager. - tests/test_playlist_sources_adapters.py — 18 tests covering each adapter's field projection with fake backing clients, plus registry lazy-construct + cache + re-register invalidation. Phase 1 will collapse refresh_mirrored.py's per-source if/elif chain to a registry lookup and surface ListenBrainz as a Sync-page tab.
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
"""Registry for playlist source adapters.
|
|
|
|
Adapters are registered as zero-arg factories so we can lazy-construct
|
|
them. This matters because some adapters need late-binding to globals
|
|
that aren't ready at import time (e.g. the YouTube adapter wraps a
|
|
parser defined in ``web_server.py`` — importing it eagerly would cause
|
|
a circular import).
|
|
|
|
Usage::
|
|
|
|
registry = get_registry()
|
|
registry.register("spotify", lambda: SpotifyPlaylistSource(...))
|
|
source = registry.get_source("spotify")
|
|
|
|
In Phase 0 the registry is set up but not yet consumed by the dispatch
|
|
sites. Phase 1+ wires it in.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from threading import Lock
|
|
from typing import Callable, Dict, List, Optional
|
|
|
|
from core.playlists.sources.base import PlaylistSource
|
|
|
|
|
|
class PlaylistSourceRegistry:
|
|
"""Thread-safe registry mapping source name → cached adapter instance."""
|
|
|
|
def __init__(self) -> None:
|
|
self._factories: Dict[str, Callable[[], PlaylistSource]] = {}
|
|
self._instances: Dict[str, PlaylistSource] = {}
|
|
self._lock = Lock()
|
|
|
|
def register(self, name: str, factory: Callable[[], PlaylistSource]) -> None:
|
|
"""Register an adapter factory under ``name``.
|
|
|
|
Re-registering replaces the previous factory and invalidates the
|
|
cached instance. Used by tests to swap in stubs."""
|
|
with self._lock:
|
|
self._factories[name] = factory
|
|
self._instances.pop(name, None)
|
|
|
|
def unregister(self, name: str) -> None:
|
|
with self._lock:
|
|
self._factories.pop(name, None)
|
|
self._instances.pop(name, None)
|
|
|
|
def get_source(self, name: str) -> Optional[PlaylistSource]:
|
|
"""Return the adapter for ``name``, building it on first access."""
|
|
with self._lock:
|
|
if name in self._instances:
|
|
return self._instances[name]
|
|
factory = self._factories.get(name)
|
|
if factory is None:
|
|
return None
|
|
instance = factory()
|
|
self._instances[name] = instance
|
|
return instance
|
|
|
|
def known_names(self) -> List[str]:
|
|
with self._lock:
|
|
return sorted(self._factories.keys())
|
|
|
|
def reset(self) -> None:
|
|
"""Drop all registrations + cached instances. Test-only."""
|
|
with self._lock:
|
|
self._factories.clear()
|
|
self._instances.clear()
|
|
|
|
|
|
_default_registry = PlaylistSourceRegistry()
|
|
|
|
|
|
def get_registry() -> PlaylistSourceRegistry:
|
|
"""Return the process-wide default registry."""
|
|
return _default_registry
|