From 1f579cede84947bb67299e057ac75e4a6c67bfb8 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sat, 16 May 2026 13:52:26 -0700 Subject: [PATCH] Add Amazon Music as a primary metadata source Wires AmazonClient into the metadata source registry following the exact same pattern as DeezerClient. No existing source paths touched. - Add get_album_metadata / get_artist_info / get_artist_albums_list aliases to AmazonClient (mirrors DeezerClient interface aliases) - Register amazon in METADATA_SOURCE_PRIORITY and METADATA_SOURCE_LABELS - Add _get_amazon_factory() + get_amazon_client() to registry.py - Add amazon branch to get_client_for_source(); thread amazon_client_factory kwarg through get_primary_client() and get_primary_source_status() - Re-export get_amazon_client from the core.metadata_service shim - Add Amazon Music option to Settings metadata source dropdown - 3530 tests pass --- core/amazon_client.py | 5 +++++ core/metadata/registry.py | 31 ++++++++++++++++++++++++++++++- core/metadata_service.py | 2 ++ webui/index.html | 3 ++- webui/static/helper.js | 1 + 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/core/amazon_client.py b/core/amazon_client.py index 421781ae..9121fd0f 100644 --- a/core/amazon_client.py +++ b/core/amazon_client.py @@ -511,6 +511,11 @@ class AmazonClient: """Not available from Amazon Music — returns None for compatibility.""" return None + # ==================== Interface Aliases (match DeezerClient method names) ==================== + get_album_metadata = get_album + get_artist_info = get_artist + get_artist_albums_list = get_artist_albums + # ------------------------------------------------------------------ # Private helpers # ------------------------------------------------------------------ diff --git a/core/metadata/registry.py b/core/metadata/registry.py index d2dc86f7..76a8b212 100644 --- a/core/metadata/registry.py +++ b/core/metadata/registry.py @@ -18,13 +18,14 @@ logger = get_logger("metadata.registry") MetadataClientFactory = Callable[[], Any] -METADATA_SOURCE_PRIORITY = ("deezer", "itunes", "spotify", "discogs", "hydrabase") +METADATA_SOURCE_PRIORITY = ("deezer", "itunes", "spotify", "discogs", "hydrabase", "amazon") METADATA_SOURCE_LABELS = { "spotify": "Spotify", "itunes": "iTunes", "deezer": "Deezer", "discogs": "Discogs", "hydrabase": "Hydrabase", + "amazon": "Amazon Music", } _UNSET = object() @@ -140,6 +141,14 @@ def _get_discogs_factory(client_factory: Optional[MetadataClientFactory]) -> Met return DiscogsClient +def _get_amazon_factory(client_factory: Optional[MetadataClientFactory]) -> MetadataClientFactory: + if client_factory is not None: + return client_factory + from core.amazon_client import AmazonClient + + return AmazonClient + + def get_spotify_client(client_factory: Optional[MetadataClientFactory] = None): """Get shared Spotify client. @@ -260,6 +269,18 @@ def get_discogs_client( return client +def get_amazon_client(client_factory: Optional[MetadataClientFactory] = None): + """Get cached Amazon Music client.""" + cache_key = "amazon" + factory = _get_amazon_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 is_hydrabase_enabled() -> bool: """Return True when Hydrabase is connected and app-enabled.""" try: @@ -331,6 +352,7 @@ def get_primary_client( itunes_client_factory: Optional[MetadataClientFactory] = None, deezer_client_factory: Optional[MetadataClientFactory] = None, discogs_client_factory: Optional[MetadataClientFactory] = None, + amazon_client_factory: Optional[MetadataClientFactory] = None, ): """Return client for configured primary source.""" return get_client_for_source( @@ -339,6 +361,7 @@ def get_primary_client( itunes_client_factory=itunes_client_factory, deezer_client_factory=deezer_client_factory, discogs_client_factory=discogs_client_factory, + amazon_client_factory=amazon_client_factory, ) @@ -348,6 +371,7 @@ def get_primary_source_status( itunes_client_factory: Optional[MetadataClientFactory] = None, deezer_client_factory: Optional[MetadataClientFactory] = None, discogs_client_factory: Optional[MetadataClientFactory] = None, + amazon_client_factory: Optional[MetadataClientFactory] = None, ) -> Dict[str, Any]: """Return a generic status snapshot for the active primary metadata source.""" source = _get_config_value("metadata.fallback_source", "deezer") or "deezer" @@ -361,6 +385,7 @@ def get_primary_source_status( itunes_client_factory=itunes_client_factory, deezer_client_factory=deezer_client_factory, discogs_client_factory=discogs_client_factory, + amazon_client_factory=amazon_client_factory, ) if source == "spotify": connected = bool(client and client.is_spotify_authenticated()) @@ -387,6 +412,7 @@ def get_client_for_source( itunes_client_factory: Optional[MetadataClientFactory] = None, deezer_client_factory: Optional[MetadataClientFactory] = None, discogs_client_factory: Optional[MetadataClientFactory] = None, + amazon_client_factory: Optional[MetadataClientFactory] = None, ): """Return exact client for a source, or None if unavailable.""" if source == "spotify": @@ -410,4 +436,7 @@ def get_client_for_source( if source == "itunes": return get_itunes_client(client_factory=itunes_client_factory) + if source == "amazon": + return get_amazon_client(client_factory=amazon_client_factory) + return None diff --git a/core/metadata_service.py b/core/metadata_service.py index 5391a80b..748a9cf9 100644 --- a/core/metadata_service.py +++ b/core/metadata_service.py @@ -42,6 +42,7 @@ from core.metadata.registry import ( clear_cached_metadata_client, clear_cached_metadata_clients, clear_cached_profile_spotify_client, + get_amazon_client, get_client_for_source, get_deezer_client, get_discogs_client, @@ -75,6 +76,7 @@ except Exception: # pragma: no cover - optional dependency fallback __all__ = [ "METADATA_SOURCE_PRIORITY", + "get_amazon_client", "MetadataCache", "MetadataLookupOptions", "MetadataProvider", diff --git a/webui/index.html b/webui/index.html index af08e1c0..e0db6b88 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3632,10 +3632,11 @@ +
-
Choose the primary source for artist, album, and track metadata. Spotify can only be selected while an active Spotify session exists. Discogs requires a personal token.
+
Choose the primary source for artist, album, and track metadata. Spotify can only be selected while an active Spotify session exists. Discogs requires a personal token. Amazon Music uses the T2Tunes proxy — no account needed.
diff --git a/webui/static/helper.js b/webui/static/helper.js index 40f4c803..5c2414ef 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3415,6 +3415,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.5.3': [ { unreleased: true }, + { title: 'Amazon Music Metadata Source', desc: 'Amazon Music is now a selectable primary metadata source alongside Spotify, iTunes, Deezer, and Discogs. backed by the same T2Tunes proxy as the download source — no account needed. covers track search, album lookup with cover art, and artist discography. select it under Settings → Connections → Metadata Source.', page: 'settings' }, { title: 'Amazon Music Download Source', desc: 'new download source backed by T2Tunes proxy. searches the Amazon Music catalog, downloads 24-bit/48kHz FLAC (or Opus 320kbps / Dolby Atmos EAC3 fallback). codec waterfall mirrors Tidal/Qobuz — best quality first, auto-fallback. selectable as a standalone or hybrid source from Settings.', page: 'settings' }, ], '2.5.2': [