diff --git a/core/amazon_download_client.py b/core/amazon_download_client.py index 81477b13..0273f81f 100644 --- a/core/amazon_download_client.py +++ b/core/amazon_download_client.py @@ -78,7 +78,10 @@ class AmazonDownloadClient(DownloadSourcePlugin): self.download_path = Path(download_path) self.download_path.mkdir(parents=True, exist_ok=True) - self._client = AmazonClient() + self._quality = config_manager.get("amazon_download.quality", "flac") + self._allow_fallback = config_manager.get("amazon_download.allow_fallback", True) + + self._client = AmazonClient(preferred_codec=self._quality) self.session = http_requests.Session() self.session.headers.update({ "User-Agent": "SoulSync/1.0", @@ -213,7 +216,8 @@ class AmazonDownloadClient(DownloadSourcePlugin): display_name: str, ) -> Optional[str]: asin = str(target_id) - for codec in CODEC_PREFERENCE: + codecs = CODEC_PREFERENCE if self._allow_fallback else [self._quality] + for codec in codecs: try: streams = self._client.media_from_asin(asin, codec=codec) except AmazonClientError as exc: diff --git a/core/api_call_tracker.py b/core/api_call_tracker.py index 7fb561e1..95e1e18e 100644 --- a/core/api_call_tracker.py +++ b/core/api_call_tracker.py @@ -30,6 +30,7 @@ RATE_LIMITS = { 'tidal': 120, # MIN_API_INTERVAL=0.5s → ~120/min 'qobuz': 60, # Variable throttle, ~60/min estimate 'discogs': 60, # MIN_API_INTERVAL=1.0s with auth → ~60/min + 'amazon': 120, # MIN_API_INTERVAL=0.5s → ~120/min (T2Tunes proxy) } # Display names for UI @@ -44,12 +45,13 @@ SERVICE_LABELS = { 'tidal': 'Tidal', 'qobuz': 'Qobuz', 'discogs': 'Discogs', + 'amazon': 'Amazon Music', } # Display order SERVICE_ORDER = [ 'spotify', 'itunes', 'deezer', 'lastfm', 'genius', - 'musicbrainz', 'audiodb', 'tidal', 'qobuz', 'discogs', + 'musicbrainz', 'audiodb', 'tidal', 'qobuz', 'discogs', 'amazon', ] diff --git a/core/download_orchestrator.py b/core/download_orchestrator.py index e8231e09..480ce7d6 100644 --- a/core/download_orchestrator.py +++ b/core/download_orchestrator.py @@ -104,6 +104,15 @@ class DownloadOrchestrator: deezer_dl.reconnect(deezer_arl) deezer_dl._quality = config_manager.get('deezer_download.quality', 'flac') + # Reload Amazon quality preference (T2Tunes needs no reconnect — public proxy) + amazon = self.client('amazon') + if amazon: + quality = config_manager.get('amazon_download.quality', 'flac') + amazon._quality = quality + amazon._allow_fallback = config_manager.get('amazon_download.allow_fallback', True) + if hasattr(amazon, '_client') and amazon._client: + amazon._client.preferred_codec = quality + # Reload download path for all clients that cache it. # Soulseek owns the path config and is reloaded above; every # other source mirrors that path so files all land in one @@ -319,7 +328,7 @@ class DownloadOrchestrator: return None # 2. Filter and validate results - _streaming_sources = ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud') + _streaming_sources = ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon') is_streaming = tracks[0].username in _streaming_sources if tracks else False if is_streaming and expected_track: diff --git a/core/download_plugins/registry.py b/core/download_plugins/registry.py index ed62e899..ad01c2e9 100644 --- a/core/download_plugins/registry.py +++ b/core/download_plugins/registry.py @@ -38,6 +38,7 @@ from core.download_plugins.base import DownloadSourcePlugin # than the legacy module-top imports here. Importing everything at # registry-load time pins the bindings the same way the legacy # orchestrator did. +from core.amazon_download_client import AmazonDownloadClient from core.deezer_download_client import DeezerDownloadClient from core.hifi_client import HiFiClient from core.lidarr_download_client import LidarrDownloadClient @@ -176,6 +177,7 @@ def build_default_registry() -> DownloadPluginRegistry: """ registry = DownloadPluginRegistry() + registry.register(PluginSpec(name='amazon', factory=AmazonDownloadClient, display_name='Amazon Music')) registry.register(PluginSpec(name='soulseek', factory=SoulseekClient, display_name='Soulseek')) registry.register(PluginSpec(name='youtube', factory=YouTubeClient, display_name='YouTube')) registry.register(PluginSpec(name='tidal', factory=TidalDownloadClient, display_name='Tidal')) diff --git a/web_server.py b/web_server.py index a29ff3bd..93ec6de1 100644 --- a/web_server.py +++ b/web_server.py @@ -1750,21 +1750,22 @@ def _find_downloaded_file(download_path, track_data): audio_extensions = {'.mp3', '.flac', '.ogg', '.aac', '.wma', '.wav', '.m4a'} target_filename = extract_filename(track_data.get('filename', '')) - # YOUTUBE/TIDAL/QOBUZ/HIFI SUPPORT: Handle encoded filename format "id||title" + # YOUTUBE/TIDAL/QOBUZ/HIFI/AMAZON SUPPORT: Handle encoded filename format "id||title" # The file on disk will be "title.ext", not "id||title" is_youtube = track_data.get('username') == 'youtube' is_tidal = track_data.get('username') == 'tidal' is_qobuz = track_data.get('username') == 'qobuz' is_hifi = track_data.get('username') == 'hifi' - is_streaming_source = is_youtube or is_tidal or is_qobuz or is_hifi + is_amazon = track_data.get('username') == 'amazon' + is_streaming_source = is_youtube or is_tidal or is_qobuz or is_hifi or is_amazon target_filename_youtube = None if is_streaming_source and '||' in target_filename: _, title = target_filename.split('||', 1) - if is_tidal or is_qobuz or is_hifi: - # Tidal/Qobuz/HiFi files can be flac or m4a — match any audio extension + if is_tidal or is_qobuz or is_hifi or is_amazon: + # Tidal/Qobuz/HiFi/Amazon files can be flac, opus, eac3, or m4a — match any audio extension safe_title = re.sub(r'[<>:"/\\|?*]', '_', title) target_filename_youtube = safe_title # Extension-less for flexible matching - source_name = 'HiFi' if is_hifi else ('Qobuz' if is_qobuz else 'Tidal') + source_name = 'HiFi' if is_hifi else ('Qobuz' if is_qobuz else ('Amazon' if is_amazon else 'Tidal')) logger.debug(f"[{source_name} Stream] Looking for file starting with: {target_filename_youtube}") else: # yt-dlp will create "Title.mp3" from "Title" @@ -1802,11 +1803,11 @@ def _find_downloaded_file(download_path, track_data): # For Tidal, compare without extension (file could be .flac or .m4a) compare_target = target_filename_youtube.lower() compare_file = file.lower() - if is_tidal or is_qobuz or is_hifi: + if is_tidal or is_qobuz or is_hifi or is_amazon: compare_file = os.path.splitext(compare_file)[0] similarity = SequenceMatcher(None, compare_file, compare_target).ratio() - source_label = 'HiFi' if is_hifi else ('Qobuz' if is_qobuz else ('Tidal' if is_tidal else 'YouTube')) + source_label = 'HiFi' if is_hifi else ('Qobuz' if is_qobuz else ('Amazon' if is_amazon else ('Tidal' if is_tidal else 'YouTube'))) logger.debug(f"[{source_label} Stream] Comparing: '{file}' vs '{target_filename_youtube}' = {similarity:.2f}") # Keep track of best match @@ -1826,7 +1827,7 @@ def _find_downloaded_file(download_path, track_data): # For YouTube/Tidal, if we found a good enough match (80%+), use it if is_streaming_source and best_match and best_similarity >= 0.80: - source_label = 'Qobuz' if is_qobuz else ('Tidal' if is_tidal else 'YouTube') + source_label = 'Qobuz' if is_qobuz else ('Amazon' if is_amazon else ('Tidal' if is_tidal else 'YouTube')) logger.debug(f"Found good match ({best_similarity:.2f}) for {source_label} streaming file: {best_match}") return best_match @@ -2121,7 +2122,7 @@ def get_status(): # don't depend on slskd being reachable — when one of these is the # active source, surface "connected" without probing slskd so the # dashboard / sidebar indicator stays green. - serverless_sources = ('youtube', 'hifi', 'qobuz', 'tidal', 'deezer_dl', 'lidarr', 'soundcloud') + serverless_sources = ('youtube', 'hifi', 'qobuz', 'tidal', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon') is_serverless = (download_mode in serverless_sources or (download_mode == 'hybrid' and hybrid_order and any(s in serverless_sources for s in hybrid_order))) @@ -2748,7 +2749,7 @@ def handle_settings(): if 'active_media_server' in new_settings: config_manager.set_active_media_server(new_settings['active_media_server']) - for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'download_source', 'settings', 'database', 'metadata_enhancement', 'file_organization', 'playlist_sync', 'tidal', 'tidal_download', 'qobuz', 'hifi_download', 'deezer_download', 'lidarr_download', 'listenbrainz', 'acoustid', 'lastfm', 'genius', 'import', 'lossy_copy', 'listening_stats', 'ui_appearance', 'youtube', 'content_filter', 'itunes', 'm3u_export', 'musicbrainz', 'deezer', 'audiodb', 'metadata', 'hydrabase', 'security', 'discogs', 'library', 'discover', 'wishlist', 'genre_whitelist', 'post_processing']: + for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'download_source', 'settings', 'database', 'metadata_enhancement', 'file_organization', 'playlist_sync', 'tidal', 'tidal_download', 'qobuz', 'hifi_download', 'deezer_download', 'amazon_download', 'lidarr_download', 'listenbrainz', 'acoustid', 'lastfm', 'genius', 'import', 'lossy_copy', 'listening_stats', 'ui_appearance', 'youtube', 'content_filter', 'itunes', 'm3u_export', 'musicbrainz', 'deezer', 'audiodb', 'metadata', 'hydrabase', 'security', 'discogs', 'library', 'discover', 'wishlist', 'genre_whitelist', 'post_processing']: if service in new_settings: for key, value in new_settings[service].items(): config_manager.set(f'{service}.{key}', value) @@ -19683,6 +19684,26 @@ def deezer_download_test_download(): # =================================================================== +# AMAZON DOWNLOAD ENDPOINTS +# =================================================================== + +@app.route('/api/amazon/test-connection', methods=['GET']) +@admin_only +def amazon_test_connection(): + """Check whether the T2Tunes proxy is up and Amazon Music is reachable.""" + try: + from core.amazon_client import AmazonClient + c = AmazonClient() + status = c.status() + amazon_up = str(status.get('amazonMusic', '')).lower() == 'up' + return jsonify({ + 'connected': amazon_up, + 'status': status, + }) + except Exception as e: + return jsonify({'connected': False, 'error': str(e)}), 200 + + # TIDAL DOWNLOAD AUTH ENDPOINTS # =================================================================== diff --git a/webui/index.html b/webui/index.html index 1a0530b5..6f312f04 100644 --- a/webui/index.html +++ b/webui/index.html @@ -4616,6 +4616,39 @@ + +
+