Phase 1a of the Discover-to-Sync unification. The mirrored-playlist refresh handler used to branch per-source through a ~190-line if/elif chain (Spotify, Spotify public, Deezer, Tidal, YouTube). Each branch hand-built its own ``extra_data`` JSON for the matched- data block. With every new source we considered for Sync-page mirror support (ListenBrainz, Last.fm radio, SoulSync Discovery, iTunes link), that chain would have grown a new elif. This commit lifts the per-source logic into the existing adapter layer and collapses the dispatch to a registry lookup: - ``core/playlists/sources/deezer.py`` — new adapter so the registry covers every source the refresh handler previously branched on. - ``core/playlists/sources/bootstrap.py`` — single helper that builds a populated registry from injected getter callables. Both ``web_server.py`` boot and the automation test fixtures call it, so the two construction paths can't drift. - ``core/playlists/sources/base.py`` — ``to_mirror_track_dict`` projection helper centralises the NormalizedTrack → DB-row conversion (including the discovered/matched_data and spotify_hint extra_data shapes the downstream sync + wishlist consumers already expect). - Spotify adapter now populates ``extra['discovered']`` + an ``extra['matched_data']`` block when fetching via the authed API, so Spotify mirrors keep landing pre-discovered (matches the pre-refactor contract pinned by ``test_spotify_refresh_writes_to_db``). - Spotify-public adapter populates ``extra['spotify_hint']`` so the discovery worker can skip its search step and jump straight to enrichment for the known track ID. - All artist-name fields now project to first-artist-only across every adapter — matches the pre-refactor mirror_playlist DB shape (``t.artists[0]``). ``refresh_mirrored.py`` shrinks ~190 → ~80 lines and keeps: - the file/beatport unrefreshable-source filter, - URL extraction from ``description`` via ``require_refresh_url`` for spotify_public + youtube, - the Spotify-public → authed-Spotify fallback when the user is signed in (handler-level branch, not in any adapter), - the Tidal-not-authenticated soft-skip log (skip, not error), - existing-extra_data preservation across refreshes, - the ``playlist_changed`` automation event emit on track-set delta. Test scaffolding: - ``_build_deps`` in ``tests/automation/test_handlers_playlist.py`` now builds a default registry from the passed clients via ``build_playlist_source_registry``, so existing refresh tests exercise the same path without per-test changes. New tests cover Tidal-not-authed soft-skip, Deezer refresh writes plain tracks, YouTube refresh reads URL from description, and Spotify-public uses authed Spotify when signed in. - 4 new adapter tests for Deezer projection + ``to_mirror_track_dict`` (minimal track, Spotify matched_data, Spotify-public spotify_hint). - ``playlist_source_registry`` field on ``AutomationDeps`` defaults to ``None`` so the other 5 automation test files (which don't exercise refresh_mirrored) keep working unchanged. 220 tests across automation + adapter suites green.
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""Helper for constructing + populating a PlaylistSourceRegistry.
|
|
|
|
Both ``web_server.py`` (at app boot) and the automation test fixtures
|
|
build a registry the same way: take the client / parser / manager
|
|
getters that already exist as module globals, wire them into the
|
|
adapter constructors, register each adapter under its canonical name.
|
|
|
|
This module owns that wiring so the two call sites can't drift.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Callable, Optional
|
|
|
|
from core.playlists.sources.base import (
|
|
SOURCE_DEEZER,
|
|
SOURCE_ITUNES_LINK,
|
|
SOURCE_LASTFM,
|
|
SOURCE_LISTENBRAINZ,
|
|
SOURCE_QOBUZ,
|
|
SOURCE_SOULSYNC_DISCOVERY,
|
|
SOURCE_SPOTIFY,
|
|
SOURCE_SPOTIFY_PUBLIC,
|
|
SOURCE_TIDAL,
|
|
SOURCE_YOUTUBE,
|
|
)
|
|
from core.playlists.sources.deezer import DeezerPlaylistSource
|
|
from core.playlists.sources.itunes_link import ITunesLinkPlaylistSource
|
|
from core.playlists.sources.lastfm import LastFMPlaylistSource
|
|
from core.playlists.sources.listenbrainz import ListenBrainzPlaylistSource
|
|
from core.playlists.sources.qobuz import QobuzPlaylistSource
|
|
from core.playlists.sources.registry import PlaylistSourceRegistry
|
|
from core.playlists.sources.soulsync_discovery import (
|
|
SoulSyncDiscoveryPlaylistSource,
|
|
)
|
|
from core.playlists.sources.spotify import SpotifyPlaylistSource
|
|
from core.playlists.sources.spotify_public import SpotifyPublicPlaylistSource
|
|
from core.playlists.sources.tidal import TidalPlaylistSource
|
|
from core.playlists.sources.youtube import YouTubePlaylistSource
|
|
|
|
|
|
def build_playlist_source_registry(
|
|
*,
|
|
spotify_client_getter: Callable[[], Any],
|
|
tidal_client_getter: Callable[[], Any],
|
|
qobuz_client_getter: Callable[[], Any],
|
|
deezer_client_getter: Callable[[], Any],
|
|
itunes_link_parser: Optional[Callable[[str], Optional[dict]]] = None,
|
|
youtube_parser: Optional[Callable[[str], Optional[dict]]] = None,
|
|
listenbrainz_manager_getter: Optional[Callable[[], Any]] = None,
|
|
lastfm_manager_getter: Optional[Callable[[], Any]] = None,
|
|
personalized_manager_getter: Optional[Callable[[], Any]] = None,
|
|
profile_id_getter: Optional[Callable[[], int]] = None,
|
|
) -> PlaylistSourceRegistry:
|
|
"""Build a fresh registry with every default adapter registered.
|
|
|
|
Each parameter is the getter the corresponding adapter needs. Pass
|
|
``lambda: None`` (or omit) for sources you don't want to expose —
|
|
the adapter will simply degrade to empty results when its backing
|
|
client is None / its parser is unset.
|
|
"""
|
|
reg = PlaylistSourceRegistry()
|
|
|
|
reg.register(SOURCE_SPOTIFY, lambda: SpotifyPlaylistSource(spotify_client_getter))
|
|
reg.register(SOURCE_SPOTIFY_PUBLIC, lambda: SpotifyPublicPlaylistSource())
|
|
reg.register(SOURCE_DEEZER, lambda: DeezerPlaylistSource(deezer_client_getter))
|
|
reg.register(SOURCE_TIDAL, lambda: TidalPlaylistSource(tidal_client_getter))
|
|
reg.register(SOURCE_QOBUZ, lambda: QobuzPlaylistSource(qobuz_client_getter))
|
|
|
|
_no_url_parser = lambda url: None
|
|
reg.register(
|
|
SOURCE_YOUTUBE,
|
|
lambda: YouTubePlaylistSource(youtube_parser or _no_url_parser),
|
|
)
|
|
reg.register(
|
|
SOURCE_ITUNES_LINK,
|
|
lambda: ITunesLinkPlaylistSource(itunes_link_parser or _no_url_parser),
|
|
)
|
|
|
|
_no_manager = lambda: None
|
|
reg.register(
|
|
SOURCE_LISTENBRAINZ,
|
|
lambda: ListenBrainzPlaylistSource(listenbrainz_manager_getter or _no_manager),
|
|
)
|
|
reg.register(
|
|
SOURCE_LASTFM,
|
|
lambda: LastFMPlaylistSource(lastfm_manager_getter or _no_manager),
|
|
)
|
|
reg.register(
|
|
SOURCE_SOULSYNC_DISCOVERY,
|
|
lambda: SoulSyncDiscoveryPlaylistSource(
|
|
personalized_manager_getter or _no_manager,
|
|
profile_id_getter=profile_id_getter,
|
|
),
|
|
)
|
|
|
|
return reg
|