diff --git a/core/downloads/status.py b/core/downloads/status.py index c1599c5a..e9b8d925 100644 --- a/core/downloads/status.py +++ b/core/downloads/status.py @@ -87,7 +87,7 @@ class StatusDeps: # Streaming sources the engine fallback applies to. Soulseek goes through # slskd's live_transfers path and must NOT hit the engine fallback. _STREAMING_SOURCE_NAMES = frozenset(( - 'youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', + 'youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon', )) # Keep these in sync with the engine plugins' state strings. diff --git a/core/downloads/task_worker.py b/core/downloads/task_worker.py index ebe5f716..c5d79d82 100644 --- a/core/downloads/task_worker.py +++ b/core/downloads/task_worker.py @@ -294,7 +294,7 @@ def download_track_worker(task_id: str, batch_id: Optional[str], deps: TaskWorke source_clients = { name: orch.client(name) for name in ('soulseek', 'youtube', 'tidal', 'qobuz', - 'hifi', 'deezer_dl', 'lidarr', 'soundcloud') + 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon') } # The orchestrator tried sources in order but stopped at the first with results. diff --git a/core/downloads/validation.py b/core/downloads/validation.py index 17171535..79131472 100644 --- a/core/downloads/validation.py +++ b/core/downloads/validation.py @@ -79,7 +79,7 @@ def get_valid_candidates(results, spotify_track, query): # Streaming sources (YouTube, Tidal, Qobuz, HiFi, Deezer, SoundCloud) return structured API results # with proper artist/title metadata — score using the same matching engine as Soulseek - _streaming_sources = ("youtube", "tidal", "qobuz", "hifi", "deezer_dl", "soundcloud") + _streaming_sources = ("youtube", "tidal", "qobuz", "hifi", "deezer_dl", "soundcloud", "amazon") if results[0].username in _streaming_sources: source_label = results[0].username.replace('_dl', '').title() expected_artists = spotify_track.artists if spotify_track else [] diff --git a/core/imports/side_effects.py b/core/imports/side_effects.py index 0868d6a2..9b3d9f3a 100644 --- a/core/imports/side_effects.py +++ b/core/imports/side_effects.py @@ -235,7 +235,7 @@ def record_library_history_download(context: Dict[str, Any]) -> None: source_track_id = search_result.get("track_id", "") or search_result.get("id", "") or ti.get("id", "") source_track_title = search_result.get("title", "") or search_result.get("name", "") source_artist = search_result.get("artist", "") - if source_filename and "||" in source_filename and username in ("tidal", "youtube", "qobuz", "hifi", "deezer_dl", "lidarr", "soundcloud"): + if source_filename and "||" in source_filename and username in ("tidal", "youtube", "qobuz", "hifi", "deezer_dl", "lidarr", "soundcloud", "amazon"): stream_id = source_filename.split("||")[0] if stream_id and not source_track_id: source_track_id = stream_id diff --git a/tests/test_download_plugin_conformance.py b/tests/test_download_plugin_conformance.py index 2b0f4e9d..4f380076 100644 --- a/tests/test_download_plugin_conformance.py +++ b/tests/test_download_plugin_conformance.py @@ -59,6 +59,7 @@ def _import_plugin_classes(): from core.deezer_download_client import DeezerDownloadClient from core.lidarr_download_client import LidarrDownloadClient from core.soundcloud_client import SoundcloudClient + from core.amazon_download_client import AmazonDownloadClient return { 'soulseek': SoulseekClient, @@ -69,10 +70,11 @@ def _import_plugin_classes(): 'deezer': DeezerDownloadClient, 'lidarr': LidarrDownloadClient, 'soundcloud': SoundcloudClient, + 'amazon': AmazonDownloadClient, } -def test_default_registry_registers_all_eight_sources(): +def test_default_registry_registers_all_sources(): """Smoke check that the foundation registry knows about every source the orchestrator historically dispatched to. If someone drops a registration here, every other test in this module would @@ -82,7 +84,7 @@ def test_default_registry_registers_all_eight_sources(): registry = build_default_registry() expected = { 'soulseek', 'youtube', 'tidal', 'qobuz', - 'hifi', 'deezer', 'lidarr', 'soundcloud', + 'hifi', 'deezer', 'lidarr', 'soundcloud', 'amazon', } assert set(registry.names()) == expected @@ -102,7 +104,7 @@ def test_deezer_dl_alias_is_registered_against_deezer_spec(): @pytest.mark.parametrize('plugin_name', [ 'soulseek', 'youtube', 'tidal', 'qobuz', - 'hifi', 'deezer', 'lidarr', 'soundcloud', + 'hifi', 'deezer', 'lidarr', 'soundcloud', 'amazon', ]) def test_plugin_class_has_all_required_methods(plugin_name): """Every registered plugin class exposes every protocol method @@ -122,7 +124,7 @@ def test_plugin_class_has_all_required_methods(plugin_name): @pytest.mark.parametrize('plugin_name', [ 'soulseek', 'youtube', 'tidal', 'qobuz', - 'hifi', 'deezer', 'lidarr', 'soundcloud', + 'hifi', 'deezer', 'lidarr', 'soundcloud', 'amazon', ]) def test_plugin_class_async_methods_are_coroutines(plugin_name): """Methods declared async in the protocol must be async on every diff --git a/web_server.py b/web_server.py index 93ec6de1..24f7f773 100644 --- a/web_server.py +++ b/web_server.py @@ -5797,7 +5797,7 @@ def start_download(): if download_id: # Register download for post-processing (simple transfer to /Transfer) context_key = _make_context_key(username, filename) - is_streaming_source = username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud') + is_streaming_source = username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon') with matched_context_lock: matched_downloads_context[context_key] = { 'search_result': { @@ -6179,7 +6179,7 @@ def get_download_status(): all_streaming_downloads = run_async(download_orchestrator.get_all_downloads()) for download in all_streaming_downloads: - if download.username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud'): + if download.username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon'): source_label = download.username.title() # Convert DownloadStatus to transfer format that frontend expects streaming_transfer = { @@ -11106,7 +11106,7 @@ def redownload_search_sources(track_id): quality = ext if ext in ('FLAC', 'MP3', 'OPUS', 'OGG', 'M4A', 'WAV') else candidate.quality or '' svc = source_name if source_name != 'default' else 'hybrid' uname = candidate.username - if uname in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud'): + if uname in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon'): svc = uname source_candidates.append({ 'username': uname, @@ -16412,7 +16412,7 @@ def _try_source_reuse(task_id, batch_id, track): if not source_tracks or not last_source: _sr.info("Skipped — no source_tracks or no last_source") return False - if last_source.get('username') in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud'): + if last_source.get('username') in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon'): _sr.info(f"Skipped — {last_source.get('username')} source (no folder-based reuse)") return False @@ -16514,7 +16514,7 @@ def _store_batch_source(batch_id, username, filename): """Browse the successful download's folder and store results on the batch for reuse.""" _sr = source_reuse_logger _sr.info(f"_store_batch_source called: batch={batch_id}, user={username}, file={filename}") - if not batch_id or username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud'): + if not batch_id or username in ('youtube', 'tidal', 'qobuz', 'hifi', 'deezer_dl', 'lidarr', 'soundcloud', 'amazon'): _sr.info(f"Skipped — no batch_id or streaming source ({username})") return diff --git a/webui/static/helper.js b/webui/static/helper.js index bb609b5e..40f4c803 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3415,7 +3415,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.5.3': [ { unreleased: true }, - { 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 the same pattern as Tidal/Qobuz — best quality first, auto-fallback. not yet wired into the main source selector — coming in a follow-up PR.', 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': [ // --- May 13, 2026 — 2.5.2 release ---