diff --git a/core/amazon_client.py b/core/amazon_client.py index 27bdd824..143f0711 100644 --- a/core/amazon_client.py +++ b/core/amazon_client.py @@ -64,6 +64,10 @@ MIN_API_INTERVAL = 0.5 # seconds — T2Tunes has no published rate limit _last_api_call: float = 0.0 _api_call_lock = threading.Lock() +_META_CACHE_TTL = 300 # seconds +_meta_cache: Dict[str, tuple] = {} # asin -> (fetched_at, meta_dict) +_meta_cache_lock = threading.Lock() + class AmazonClientError(RuntimeError): """Raised on unrecoverable T2Tunes API errors.""" @@ -657,12 +661,21 @@ class AmazonClient: metas: Dict[str, Dict[str, Any]] = {} def _fetch(asin: str) -> None: + now = time.monotonic() + with _meta_cache_lock: + entry = _meta_cache.get(asin) + if entry and (now - entry[0]) < _META_CACHE_TTL: + metas[asin] = entry[1] + return _rate_limit() try: raw = self.album_metadata(asin) lst = raw.get("albumList") if isinstance(lst, list) and lst and isinstance(lst[0], dict): - metas[asin] = lst[0] + meta = lst[0] + with _meta_cache_lock: + _meta_cache[asin] = (time.monotonic(), meta) + metas[asin] = meta except Exception: pass diff --git a/core/library/service_search.py b/core/library/service_search.py index fb47d48a..cb8b1874 100644 --- a/core/library/service_search.py +++ b/core/library/service_search.py @@ -19,6 +19,7 @@ tidal_enrichment_worker = None qobuz_enrichment_worker = None discogs_worker = None audiodb_worker = None +amazon_worker = None def init( @@ -31,11 +32,12 @@ def init( qobuz_worker=None, discogs_worker_obj=None, audiodb_worker_obj=None, + amazon_worker_obj=None, ): """Bind enrichment worker handles so the lifted bodies can use them.""" global spotify_enrichment_worker, itunes_enrichment_worker, mb_worker global lastfm_worker, genius_worker, tidal_enrichment_worker - global qobuz_enrichment_worker, discogs_worker, audiodb_worker + global qobuz_enrichment_worker, discogs_worker, audiodb_worker, amazon_worker spotify_enrichment_worker = spotify_worker itunes_enrichment_worker = itunes_worker mb_worker = musicbrainz_worker @@ -45,6 +47,7 @@ def init( qobuz_enrichment_worker = qobuz_worker discogs_worker = discogs_worker_obj audiodb_worker = audiodb_worker_obj + amazon_worker = amazon_worker_obj def _detect_provider(items, client): @@ -293,4 +296,22 @@ def _search_service(service, entity_type, query): 'image': None, 'extra': f"{result.get('strArtist', '')} · {result.get('strAlbum', '')}"}] return [] + elif service == 'amazon': + if not amazon_worker or not amazon_worker.client: + raise ValueError("Amazon worker not initialized") + client = amazon_worker.client + if entity_type == 'artist': + items = client.search_artists(query, limit=8) + return [{'id': str(a.id), 'name': a.name, 'image': a.image_url, + 'extra': ', '.join(a.genres[:3]) if a.genres else ''} for a in items] + elif entity_type == 'album': + items = client.search_albums(query, limit=8) + return [{'id': str(a.id), 'name': a.name, 'image': a.image_url, + 'extra': f"{', '.join(a.artists)} · {a.release_date or ''}"} for a in items] + elif entity_type == 'track': + items = client.search_tracks(query, limit=8) + return [{'id': str(t.id), 'name': t.name, 'image': t.image_url, + 'extra': f"{', '.join(t.artists)} · {t.album or ''}"} for t in items] + return [] + return [] diff --git a/core/metadata/registry.py b/core/metadata/registry.py index 76a8b212..ef7fbc8f 100644 --- a/core/metadata/registry.py +++ b/core/metadata/registry.py @@ -18,14 +18,13 @@ logger = get_logger("metadata.registry") MetadataClientFactory = Callable[[], Any] -METADATA_SOURCE_PRIORITY = ("deezer", "itunes", "spotify", "discogs", "hydrabase", "amazon") +METADATA_SOURCE_PRIORITY = ("deezer", "itunes", "spotify", "discogs", "hydrabase") METADATA_SOURCE_LABELS = { "spotify": "Spotify", "itunes": "iTunes", "deezer": "Deezer", "discogs": "Discogs", "hydrabase": "Hydrabase", - "amazon": "Amazon Music", } _UNSET = object() diff --git a/database/music_database.py b/database/music_database.py index 1228ec47..a89970c3 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -1634,6 +1634,10 @@ class MusicDatabase: cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN discogs_artist_id TEXT") logger.info("Added discogs_artist_id column to watchlist_artists table for cross-provider support") + if 'amazon_artist_id' not in columns: + cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN amazon_artist_id TEXT") + logger.info("Added amazon_artist_id column to watchlist_artists table for Amazon Music support") + except Exception as e: logger.error(f"Error adding itunes_artist_id column to watchlist_artists: {e}") # Don't raise - this is a migration, database can still function @@ -8955,6 +8959,7 @@ class MusicDatabase: a.tidal_id, a.qobuz_id, a.soul_id, + a.amazon_id, a.server_source FROM artists a WHERE {where_clause} @@ -9054,6 +9059,7 @@ class MusicDatabase: 'tidal_id': row['tidal_id'], 'qobuz_id': row['qobuz_id'], 'soul_id': row['soul_id'], + 'amazon_id': row['amazon_id'], 'album_count': counts_map.get(row['id'], (0, 0))[0], 'track_count': counts_map.get(row['id'], (0, 0))[1], 'is_watched': bool(is_watched) @@ -9112,7 +9118,7 @@ class MusicDatabase: id, name, thumb_url, genres, server_source, musicbrainz_id, deezer_id, audiodb_id, discogs_id, spotify_artist_id, itunes_artist_id, lastfm_url, genius_url, - tidal_id, qobuz_id, soul_id, + tidal_id, qobuz_id, soul_id, amazon_id, lastfm_listeners, lastfm_playcount, lastfm_tags, lastfm_bio FROM artists WHERE id = ? @@ -9270,6 +9276,7 @@ class MusicDatabase: 'tidal_id': artist_row['tidal_id'], 'qobuz_id': artist_row['qobuz_id'], 'soul_id': artist_row['soul_id'], + 'amazon_id': artist_row['amazon_id'], 'lastfm_listeners': artist_row['lastfm_listeners'], 'lastfm_playcount': artist_row['lastfm_playcount'], 'lastfm_tags': artist_row['lastfm_tags'], diff --git a/web_server.py b/web_server.py index 7e328051..aef5b568 100644 --- a/web_server.py +++ b/web_server.py @@ -10655,6 +10655,7 @@ _SERVICE_ID_COLUMNS = { 'genius': {'artist': 'genius_id', 'track': 'genius_id'}, 'tidal': {'artist': 'tidal_id', 'album': 'tidal_id', 'track': 'tidal_id'}, 'qobuz': {'artist': 'qobuz_id', 'album': 'qobuz_id', 'track': 'qobuz_id'}, + 'amazon': {'artist': 'amazon_id', 'album': 'amazon_id', 'track': 'amazon_id'}, } @app.route('/api/library/manual-match', methods=['PUT']) @@ -24372,6 +24373,7 @@ def get_watchlist_artists(): "itunes_artist_id": artist.itunes_artist_id, # For iTunes-only artists "deezer_artist_id": getattr(artist, 'deezer_artist_id', None), "discogs_artist_id": getattr(artist, 'discogs_artist_id', None), + "amazon_artist_id": getattr(artist, 'amazon_artist_id', None), "include_albums": artist.include_albums, "include_eps": artist.include_eps, "include_singles": artist.include_singles, @@ -25171,10 +25173,10 @@ def watchlist_artist_config(artist_id): include_live, include_remixes, include_acoustic, include_compilations, artist_name, image_url, spotify_artist_id, itunes_artist_id, last_scan_timestamp, date_added, include_instrumentals, deezer_artist_id, - lookback_days, discogs_artist_id, preferred_metadata_source + lookback_days, discogs_artist_id, preferred_metadata_source, amazon_artist_id FROM watchlist_artists - WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ? - """, (artist_id, artist_id, artist_id, artist_id)) + WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ? OR amazon_artist_id = ? + """, (artist_id, artist_id, artist_id, artist_id, artist_id)) result = cursor.fetchone() conn.close() @@ -25183,10 +25185,11 @@ def watchlist_artist_config(artist_id): # Determine if this is an iTunes or Spotify artist is_itunes_artist = artist_id.isdigit() - spotify_id = result[9] # spotify_artist_id from query + spotify_id = result[9] # spotify_artist_id from query itunes_id = result[10] # itunes_artist_id from query deezer_id = result[14] # deezer_artist_id from query discogs_id = result[16] # discogs_artist_id from query + amazon_id = result[18] if len(result) > 18 else None # amazon_artist_id from query # Get artist info from Spotify (only for Spotify artists) artist_info = None @@ -25294,6 +25297,7 @@ def watchlist_artist_config(artist_id): "itunes_artist_id": itunes_id, "deezer_artist_id": deezer_id, "discogs_artist_id": discogs_id, + "amazon_artist_id": amazon_id, "watchlist_name": result[7], # Original stored watchlist artist name "global_metadata_source": get_primary_source(), }) @@ -25394,7 +25398,7 @@ def watchlist_artist_link_provider(artist_id): new_provider_id = data.get('provider_id', '').strip() provider = data.get('provider', '').strip() - valid_providers = ('spotify', 'itunes', 'deezer', 'discogs') + valid_providers = ('spotify', 'itunes', 'deezer', 'discogs', 'amazon') if provider not in valid_providers: return jsonify({"success": False, "error": f"Invalid provider. Must be one of: {', '.join(valid_providers)}"}), 400 @@ -25408,8 +25412,8 @@ def watchlist_artist_link_provider(artist_id): cursor.execute(""" SELECT id, artist_name, spotify_artist_id, itunes_artist_id FROM watchlist_artists - WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ? - """, (artist_id, artist_id, artist_id, artist_id)) + WHERE spotify_artist_id = ? OR itunes_artist_id = ? OR deezer_artist_id = ? OR discogs_artist_id = ? OR amazon_artist_id = ? + """, (artist_id, artist_id, artist_id, artist_id, artist_id)) row = cursor.fetchone() if not row: @@ -25420,7 +25424,7 @@ def watchlist_artist_link_provider(artist_id): artist_name = row[1] # Check for duplicate — another watchlist artist already has this provider ID - col_map = {'spotify': 'spotify_artist_id', 'itunes': 'itunes_artist_id', 'deezer': 'deezer_artist_id', 'discogs': 'discogs_artist_id'} + col_map = {'spotify': 'spotify_artist_id', 'itunes': 'itunes_artist_id', 'deezer': 'deezer_artist_id', 'discogs': 'discogs_artist_id', 'amazon': 'amazon_artist_id'} col = col_map[provider] if not is_clear: @@ -32422,6 +32426,7 @@ _init_service_search( qobuz_worker=qobuz_enrichment_worker, discogs_worker_obj=discogs_worker, audiodb_worker_obj=audiodb_worker, + amazon_worker_obj=amazon_worker, ) # Qobuz status / pause / resume routes are now served by the diff --git a/webui/index.html b/webui/index.html index 7ff1b3c9..474b5157 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3655,11 +3655,10 @@ -