fix(amazon): wire amazon into all streaming-source guards
`validation.py` had amazon absent from `_streaming_sources`, causing Amazon TrackResult objects (bitrate=None, size=0) to fall through to the Soulseek P2P code path and get rejected by `filter_results_by_quality_preference`. Every album track was marked not found. Fix: add 'amazon' to every streaming-source guard tuple/set that was previously missing it: - core/downloads/validation.py — primary bug fix (quality-filter bypass) - core/downloads/status.py — _STREAMING_SOURCE_NAMES frozenset - core/downloads/task_worker.py — hybrid fallback client map - core/imports/side_effects.py — || filename→stream-id extraction - web_server.py — is_streaming_source, transfer list display, candidate source label, _try_source_reuse, _store_batch_source - tests/test_download_plugin_conformance.py — registry count + parametrize Also updates the 2.5.3 What's New entry to drop the stale "not yet wired" disclaimer.
This commit is contained in:
parent
dcbe09c7aa
commit
791e3630ff
7 changed files with 16 additions and 14 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
Loading…
Reference in a new issue