Commit graph

3152 commits

Author SHA1 Message Date
BoulderBadgeDad
ea8497b89f
Merge pull request #630 from Nezreka/dev
Dev
2026-05-17 23:27:04 -07:00
Broque Thomas
6af5d191cd Release 2.5.5 — Manual Library Match
Bump version to 2.5.5. Collapse WHATS_NEW to 2.5.5 block (Manual Library
Match entry only). Remove subreddit link from README.
2026-05-17 23:18:59 -07:00
BoulderBadgeDad
ed01a1af8c
Merge pull request #626 from Nezreka/codex/full-release-date-tags
Preserve full release dates in audio tags
2026-05-17 23:11:10 -07:00
Broque Thomas
54dbd150cb Preserve full release dates in audio tags 2026-05-17 23:02:41 -07:00
BoulderBadgeDad
3e67ac6887
Merge pull request #625 from Nezreka/codex/artist-owned-album-matching
Tighten artist discography soundtrack matching
2026-05-17 22:59:40 -07:00
Broque Thomas
025007b97f Tighten artist discography soundtrack matching 2026-05-17 22:51:38 -07:00
BoulderBadgeDad
1cf85987dc
Merge pull request #624 from Nezreka/codex/quarantine-button-escaping
Fix quarantine action button escaping
2026-05-17 22:03:19 -07:00
Broque Thomas
52ee406a6c Fix quarantine action button escaping 2026-05-17 21:47:56 -07:00
BoulderBadgeDad
de560cac85
Merge pull request #623 from Nezreka/codex/manual-library-match
Codex/manual library match
2026-05-17 21:18:30 -07:00
Broque Thomas
0345478361 Skip wishlist adds for manual library matches 2026-05-17 20:50:55 -07:00
Broque Thomas
3e7eeb7c9c Honor manual matches in automatic wishlist cleanup 2026-05-17 20:43:04 -07:00
Broque Thomas
94f6c950cb Polish manual library match tool card 2026-05-17 20:32:18 -07:00
Broque Thomas
42f4aa5eac Add manual library track matching 2026-05-17 20:27:05 -07:00
BoulderBadgeDad
4d882180bb
Merge pull request #622 from Nezreka/codex/missing-track-import
Codex/missing track import
2026-05-17 14:27:12 -07:00
Broque Thomas
f3d5ef6528 Test missing-track existing file imports
Add service-level coverage for the Enhanced Library I Have This flow: copying an existing source file, writing the target album DB row, preserving source audio, inheriting album identity tags, and migrating older track tables that lack disc_number.
2026-05-17 14:18:17 -07:00
Broque Thomas
f9ae0e8d58 Extract missing-track import service
Move the existing-file missing-track import workflow out of web_server.py and into core/library/missing_track_import.py.

Keep the Flask route focused on request wiring and response formatting while the service handles staging copy, post-processing, album identity tag inheritance, DB upsert, and media-server sync.
2026-05-17 14:04:28 -07:00
Broque Thomas
3b62bcab0c Add missing-track import from existing library files
Show actionable missing album tracks in the enhanced library from canonical metadata, with a practical Manage flow for Add to Library or I Have This.

Implement I Have This as a non-destructive copy/import path: copy the chosen existing file, run normal post-processing with the missing track context, insert the real library row, and inherit album identity tags from target siblings so Navidrome does not split albums.

Improve the modal with selectable search results, visible import progress, disabled controls during import, and missing-track row styling.
2026-05-17 13:58:11 -07:00
BoulderBadgeDad
d734f92e19
Merge pull request #621 from Nezreka/codex/slskd-album-preflight
Improve Soulseek album source selection
2026-05-17 10:11:49 -07:00
Broque Thomas
076cf9e516 Improve Soulseek album source selection
Add a conservative Soulseek album preflight scorer so album downloads choose a coherent slskd folder before per-track enqueue. The scorer compares album title, artist, year, track count, tracklist coverage, peer quality, and penalizes unexpected deluxe/remix/live-style folders.

Preserve hybrid source priority by only running Soulseek album preflight when Soulseek is the selected source or first in the hybrid order. If Soulseek is only a fallback behind another source, the normal hybrid flow is left alone.

Reuse the richest wishlist album context across tracks in the same album group so release date, artwork, album type, and album artist stay consistent for path generation. Also preserve peer-quality tie breakers when attempting equal-confidence candidates.

Tests cover correct-folder selection over larger wrong editions, Soulseek primary vs fallback hybrid behavior, shared wishlist album context, and peer-quality candidate ordering.
2026-05-17 10:08:43 -07:00
BoulderBadgeDad
e37acf2463
Merge pull request #618 from Nezreka/dev
Dev
2026-05-16 23:20:01 -07:00
Broque Thomas
57cbdc2ed4 Update docker-publish.yml 2026-05-16 23:10:44 -07:00
Broque Thomas
e132f1e295 chore: bump version to 2.5.4
- _SOULSYNC_BASE_VERSION in web_server.py
- WHATS_NEW key + date in helper.js (strips unreleased flag from Amazon entries)
- fallback version string in helper.js
2026-05-16 23:09:23 -07:00
BoulderBadgeDad
46f27583f5
Merge pull request #617 from Nezreka/feature/amazon-music-metadata
Feature/amazon music metadata
2026-05-16 23:03:43 -07:00
Broque Thomas
35db1ac06d fix(lint): log S110 bare except-pass in amazon client and worker
Three ruff S110 violations replaced with logger.debug calls:
- amazon_client.py:527 duration backfill ASIN search
- amazon_client.py:679 album metadata fetch in _fetch_album_metas
- amazon_worker.py:401 artist image backfill from albums
2026-05-16 22:58:59 -07:00
Broque Thomas
42a833fcb2 Amazon Music: UI badges, enrichment match chips, watchlist linking, metadata cache
- Artist cards, hero section, and enhanced view now show Amazon Music badges
  when amazon_id is populated (AMAZON_LOGO_URL constant, orange #FF9900 brand)
- Enhanced view artist and album match status rows include amazon_match_status
  chip with click-to-rematch via openManualMatchModal
- getServiceUrl: added amazon (album/track ASIN → music.amazon.com) and fixed
  missing discogs entries; serviceLabels adds tidal/qobuz/amazon
- Enhanced view enhanced-artist-id-badges includes amazon_id entry
- DB SELECTs for library artists list and artist detail now return amazon_id;
  both response dicts include the field
- watchlist_artists migration adds amazon_artist_id column
- Watchlist config GET: amazon_artist_id in SELECT/WHERE/response (index 18)
- Watchlist artists list response includes amazon_artist_id
- link-provider endpoint: amazon added to valid_providers and col_map
- _populateLinkedProviderSection: amazonId param + Amazon Music source row
- Watchlist card source badges render Amazon pill (watchlist-source-amazon CSS)
- _openSourceSearch labels map includes amazon
- service_search: amazon_worker injected via init(); _search_service amazon branch
  uses search_artists/albums/tracks, same {id,name,image,extra} return shape
- _SERVICE_ID_COLUMNS: amazon → amazon_id for artist/album/track
- _init_service_search call passes amazon_worker_obj
- amazon_client._fetch_album_metas: 5-minute TTL cache per ASIN — cached hits
  skip _rate_limit() and HTTP call entirely; fixes ~10s artist detail load
- registry.py: removed amazon from METADATA_SOURCE_PRIORITY and
  METADATA_SOURCE_LABELS — T2Tunes has no discography API, cannot serve as a
  primary metadata source; Amazon remains a download source + ASIN enricher
- Settings metadata source dropdown and help text updated accordingly
2026-05-16 22:52:27 -07:00
Broque Thomas
1f6edbb1da Remove arbitrary 10-album cap in get_artist_albums meta fetch
The cap caused albums beyond position 10 to load without art on the
artist detail discography. T2Tunes search_raw naturally returns ~20
results per query, so album_candidates is already bounded — no explicit
cap needed.
2026-05-16 19:54:36 -07:00
Broque Thomas
376aaa4cc9 Fix Amazon artist detail: album art and singles missing
Two bugs in the library artist detail page when Amazon is the source:

1. No album art: get_artist_albums returned Album dataclasses with
   image_url=None — it collected ASINs but never called _fetch_album_metas.
   Now fetches metas for up to 10 albums (same cap as search_albums),
   populating image_url, release_date, and total_tracks on each Album.

2. No singles: Album.from_search_hit hardcodes album_type="album" and
   T2Tunes exposes no release type in search results. Added inference:
   total_tracks==1 → album_type="single", which routes them to the
   singles bucket in the discography categorizer.

Also passes album_name through _strip_edition and artist through
_primary_artist in get_artist_albums (parity with search_albums).

3. amazon_id missing from artist_source_ids in get_artist_detail:
   the discography lookup never received the stored Amazon slug so
   it always fell back to name search. Added 'amazon': artist_info.
   get('amazon_id') to the dict alongside spotify/deezer/itunes/etc.
2026-05-16 19:19:12 -07:00
Broque Thomas
786c576b80 Fix Amazon missing from _get_enrichment_status workers_info
_get_enrichment_status had a hardcoded workers_info list. Amazon was
registered in the generic enrichment blueprint but never added here,
so the rate-monitor speedometer overlay and status API omitted it.

Adds ('amazon_enrichment', 'Amazon Music', lambda: amazon_worker)
to workers_info — same pattern as Deezer, Discogs, Tidal, Qobuz.
2026-05-16 18:39:34 -07:00
Broque Thomas
5450f4ac5e Wire Amazon Music enrichment worker into dashboard UI
Adds full parity with Deezer/Qobuz/Tidal/Discogs in every dashboard
UI layer — orb button, live tooltip, WebSocket push, rate speedometer.

- webui/index.html: Amazon enrichment orb button after Discogs
- webui/static/amazon.svg: local icon (a + smile, same pattern as
  hydrabase.png — avoids external URL dependency)
- webui/static/style.css: Amazon button/spinner/tooltip CSS with
  FF9900 brand color; added to mobile tooltip suppress list
- webui/static/worker-orbs.js: Amazon orb in WORKER_DEFS [255,153,0]
- webui/static/api-monitor.js: Amazon in rate gauge services list,
  label, and color map
- webui/static/enrichment.js: updateAmazonEnrichmentStatusFromData,
  toggleAmazonEnrichment, DOMContentLoaded init + 2s poll
- webui/static/core.js: socket.on enrichment:amazon-enrichment listener
- web_server.py: amazon-enrichment added to _emit_enrichment_status_loop
  workers dict so WebSocket pushes fire every 2s
2026-05-16 17:43:38 -07:00
Broque Thomas
4fce832ae1 Add Amazon Music enrichment worker
Background worker matching library artists/albums/tracks to Amazon ASINs
via T2Tunes search. Follows same 6-tier priority queue as Deezer/iTunes/
Spotify/Qobuz/Tidal workers. Backfills artist thumbnails from album cover
stand-ins (T2Tunes exposes no direct artist images).

- core/amazon_worker.py: new AmazonWorker class with full parity
- database/music_database.py: expand _add_amazon_columns to cover
  amazon_id/amazon_match_status/amazon_last_attempted on artists,
  albums, and tracks (was artists-only)
- web_server.py: import, init, register in enrichment panel, add to
  scan pause/resume dicts and rate monitor key map
- helper.js: WHATS_NEW 2.5.3 entry for enrichment worker
2026-05-16 17:30:48 -07:00
Broque Thomas
121651da2c Add amazon_id column to artists table for full source parity
Schema: ALTER TABLE artists ADD COLUMN amazon_id TEXT with index, added via
_add_amazon_columns migration called after Discogs in _run_migrations.

SOURCE_ID_FIELD: add "amazon" -> "amazon_id" entry. find_library_artist_for_
source now looks up Amazon artists by slug before falling back to name match,
same as every other source. artist_source_detail already stamps artist_info
[source_id_field] = artist_id so the amazon_id is set on source-only payloads.

Tests: add "amazon": "amazon_id" to EXPECTED_SOURCE_ID_FIELD; revert test
assertion back to strict equality (SOURCE_ONLY_ARTIST_SOURCES == SOURCE_ID_
FIELD.keys() holds again now that amazon has a column).
2026-05-16 17:06:54 -07:00
Broque Thomas
265fe5233e Fix Amazon artist detail: library upgrade lookup and artist images
Library upgrade: find_library_artist_for_source returned None immediately for
Amazon because SOURCE_ID_FIELD has no 'amazon' entry (no DB column for Amazon
artist IDs). The name-based fallback was unreachable. Fix: only skip the column
query when column is None, not the whole function — name lookup now runs for
any source when artist_name + active_server are provided.

Artist images: add AmazonClient._get_artist_image_from_albums so the standard
_get_artist_image_from_source path in metadata/artist_image.py can call it as
a fallback (same hook iTunes/Deezer/Discogs expose). Searches by unslugified
artist name, matches primary artist, fetches album cover from album_metadata.

Test: updated test_source_only_set_matches_mapping_keys → _contains_all_mapped_
sources to assert subset (not equality) — SOURCE_ONLY_ARTIST_SOURCES intentionally
includes sources without a DB column that rely on name-only lookup.
2026-05-16 17:01:37 -07:00
Broque Thomas
d944884ab4 Backfill album release_date from stream tags when T2Tunes metadata omits it
T2Tunes albumList entries may not include a release_date field, leaving the
$year path template empty. get_album() now falls back to the first track's
release_date (populated from the FLAC date tag via get_album_tracks) when
album metadata has none. Also try camelCase releaseDate key at all albumList
read sites (Album.from_metadata, get_album, _fetch_album_metas consumers).

1 new test: release_date backfilled from stream date tag when absent from
album metadata. date tag "2024-11-22" added to MEDIA_RESPONSE_FLAC fixture.
2026-05-16 16:32:54 -07:00
Broque Thomas
96a1c8b7b8 Enrich Amazon album track durations via search results
media_from_asin returns no duration data. get_album_tracks now does one
search_raw call using the album name + primary artist from stream tags,
filters hits by albumAsin == requested asin, and builds a duration_map
(track asin → duration_ms). Search failures are swallowed — duration_ms
falls back to 0 so the existing behaviour is preserved on error.

2 new tests: duration populated when search returns matching hit; duration
stays 0 when search endpoint returns an error.
2026-05-16 16:21:12 -07:00
Broque Thomas
c2fcef45c2 Fix $year missing and disc_number crash on Amazon album downloads
release_date: T2Tunes album metadata may use camelCase releaseDate — try both
keys at all read sites (get_album, get_track_details, Album.from_metadata,
_fetch_album_metas consumers). Final fallback: s.date from stream tags, which
T2Tunes always populates from embedded FLAC/MP4 date tag. Wire s.date into
get_album_tracks items and get_track_details album.release_date so the $year
path template resolves correctly.

disc_number crash: .get('disc_number', 1) returns None when key is present but
value is None (Amazon stream info has Optional[int] for disc_number). Switch all
max() call sites and disc_num assignments to `or 1` guard:
- master.py: run_full_missing_tracks_process max() and disc_num read
- candidates.py: track_info and detailed_track disc_number reads
- web_server.py: enhanced and standard album download max() calls
2026-05-16 16:11:57 -07:00
Broque Thomas
8a3bb88678 Fix AcoustID quarantine and disc_number crash on Amazon album downloads
AcoustID verification was quarantining every Amazon track because T2Tunes
embeds [Explicit] and [feat. X] in stream tag titles/artists, but AcoustID
returns bare titles — triggering version-mismatch rejection on every track.

- get_track_details: apply _strip_edition to name/album, _primary_artist to
  artist; wire s.track_number / s.disc_number instead of hardcoded None
- get_album_tracks: apply _strip_edition to name, _primary_artist to artist

Also fix TypeError crash in album download paths when disc_number is None
(present in dict but explicitly None, so .get('disc_number', 1) returns None):
- master.py run_full_missing_tracks_process: or 1 guard on both max() and disc_num
- candidates.py track_info extraction: or 1 guard on both disc_number reads
- web_server.py enhanced + standard album download max() calls: or 1 guard
2026-05-16 16:01:11 -07:00
Broque Thomas
51e00d4ebf Fix Amazon Music search quality: images, dedup, explicit stripping, album/artist clicks
- All search_raw calls switched from single-type to types="track,album" — T2Tunes only
  returns results when both types are requested together
- _fetch_album_metas: parallel fetch (up to 5 workers) of album cover art via
  album_metadata(asin) — T2Tunes search results carry no image URLs
- search_tracks: populates image_url, release_date, total_tracks from album meta
- search_artists: strips feat. credits via _primary_artist() so "Artist feat. X" and
  "Artist ft. Y" collapse to one "Artist" entry; uses album cover as artist image
  stand-in (same approach as iTunes — T2Tunes has no artist images)
- search_albums: name-based dedup (display_name + artist key) instead of ASIN-based;
  populates image_url, release_date, total_tracks from album meta (cap 10 ASIN fetches)
- _strip_edition(): strips [Explicit]/(Explicit) from track/album names — explicit is
  the default version; Clean/Edited/Censored labels kept as-is so they stay distinct
- get_album(): applies _strip_edition to name and _primary_artist to artist so
  MusicBrainz preflight matching doesn't fail on "[Explicit]" album names
- get_album_tracks(): populates track_number and disc_number from T2TunesStreamInfo
  instead of hardcoding None — fixes track ordering in multi-track album downloads
- get_artist() / get_artist_albums(): _unslugify() converts slug artist IDs back to
  search names; _primary_artist() in comparison handles feat-annotated results
- SOURCE_ONLY_ARTIST_SOURCES: added "amazon" so artist detail page doesn't 404
- build_source_only_artist_detail: added amazon_client param + dispatch branch
- web_server.py: resolve amazon_client in _build_source_only_artist_detail wrapper;
  add source_override=="amazon" branch in get_spotify_album_tracks endpoint
- 77 tests covering all above paths; all pass
2026-05-16 15:55:15 -07:00
Broque Thomas
d39679951b Wire Amazon Music into enhanced search and global search source picker
- Add 'amazon' to VALID_SOURCES (and transitively VALID_STREAM_SOURCES)
  in core/search/orchestrator.py so the backend accepts it as a
  requested source without returning 400
- Add resolve_client('amazon') case — mirrors musicbrainz pattern,
  gets the cached AmazonClient from the metadata registry
- Add 'amazon' to _alternate_sources() so it appears as a tab when
  another source is primary (always available, no credentials)
- Add SERVICE_CONFIG_REGISTRY entry 'amazon': {'always': True} so
  /api/settings/config-status reports it as configured
- Add SOURCE_LABELS['amazon'] and SOURCE_ORDER entry in
  shared-helpers.js so both enhanced search and global search show
  the Amazon Music tab
- Add 'amazon' to _ALWAYS_CONFIGURED_SOURCES so the picker never
  dims the tab (no credentials required)
- Add .enh-tab-amazon.active CSS (Amazon orange #FF9900)
- 3530 tests pass
2026-05-16 14:18:18 -07:00
Broque Thomas
1f579cede8 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
2026-05-16 13:52:26 -07:00
BoulderBadgeDad
e8b9f80597
Merge pull request #615 from Nezreka/feature/amazon-music-download
Feature/amazon music download
2026-05-16 12:56:09 -07:00
Broque Thomas
14a99f47ab fix(tests): use asyncio.run() instead of get_event_loop() in amazon test helper
get_event_loop() raises RuntimeError on Python 3.11+ Linux when no loop
exists. asyncio.run() creates its own loop per call — no deprecation warning,
works across all supported Python versions.
2026-05-16 12:48:36 -07:00
Broque Thomas
5d8ca70fe5 Add T2Tunes probe unit tests
Tests for tools/t2tunes_probe.py: status endpoint, nested search
response flattening, streamable typo tolerance + decryption key flag,
non-JSON error handling, and HEAD→range-GET fallback for stream probing.
2026-05-16 11:49:03 -07:00
Broque Thomas
ff27effdae Amazon download client: write final size==transferred before returning file path
The download monitor blocks post-processing with a bytes-incomplete guard:
if size > 0 and transferred < size: continue

_stream_to_file throttles engine updates to every 0.5s. The last tick before
the file finishes typically leaves transferred slightly below the Content-Length
size in the engine record. Other streaming clients (YouTube, Tidal, HiFi, etc.)
use their own download threads and don't track bytes at all, so size stays 0
and the guard is always skipped. Amazon was the only client hitting it.

Fix: just before returning the file path from _download_sync, write a final
engine record update setting size == transferred == out_path.stat().st_size
(the decrypted output size). The bytes-incomplete guard then sees
transferred == size and falls through to trigger post-processing normally.
2026-05-16 11:09:04 -07:00
Broque Thomas
b4403ed393 Amazon download client: fix engine API calls in status methods
`get_all_downloads` was calling `engine.get_all_records()` — a method that
doesn't exist on DownloadEngine. Same story for `cancel_record` and
`clear_completed`. The engine exposes `iter_records_for_source`, `get_record`,
`update_record`, and `remove_record` — matching what every other streaming
client (Deezer, HiFi, Qobuz, SoundCloud, Tidal, YouTube) already uses.

With `get_all_downloads` silently returning `[]` on every call (the missing
method raised, `except Exception: return []` swallowed it), the download monitor
never saw Amazon records as complete — tasks stayed stuck at 0% even after the
file had fully downloaded.

Changes:
- `get_all_downloads` → `iter_records_for_source('amazon')`
- `get_download_status` → `get_record('amazon', id)`, no try/except
- `cancel_download` → `get_record` check + `update_record` (Cancelled) +
  optional `remove_record` — same pattern as deezer/hifi/etc
- `clear_all_completed_downloads` → iterate + `remove_record` for terminal
  states; returns True on no-engine (nothing to clear = success)
- `_record_to_status` drops the `download_id` argument; reads `rec['id']`
  instead (worker stores `'id'` in every record — `iter_records_for_source`
  returns the full record dict)

Tests updated to match: `iter_records_for_source` mock replaces
`get_all_records`, cancel test verifies `update_record`+`remove_record`,
clear test verifies only terminal-state records are removed, graceful-error
test replaced with no-records boundary test (exception propagation is handled
at the engine aggregator layer, not per-plugin).
2026-05-16 10:49:11 -07:00
Broque Thomas
ebda0b8613 fix(amazon): _record_to_status read 'filename' not 'original_filename'
The engine worker stores the encoded filename under the key 'filename'
(see worker.py dispatch). _record_to_status was reading 'original_filename',
which always returns "" — so every DownloadStatus emitted by
get_all_downloads/get_download_status had an empty filename string.

The download monitor builds lookup keys as
_make_context_key(download.username, download.filename). With filename=""
the key was always "amazon::" which never matched the task's
"amazon::B0B1234||Artist - Title" key. Monitor never detected Amazon
download completions, so tasks sat stuck at Downloading 0% forever even
though the files had actually downloaded.

Also fixes tests that had the same wrong key.
2026-05-16 10:37:29 -07:00
Broque Thomas
9fb63ff86d fix(amazon): add set_engine/set_shutdown_check so _engine gets wired
AmazonDownloadClient was missing set_engine() and set_shutdown_check().
The download engine auto-wires plugins by calling set_engine(self) at
registration time if the method exists (engine.py:136). Without it,
_engine stayed None forever, causing every download() call to raise
RuntimeError("_engine is not set") — silently failing and marking all
tracks not found.

All other streaming clients (Deezer, Qobuz, Tidal, HiFi, SoundCloud)
expose set_engine(); Amazon now matches the pattern.

Tests added: set_engine wires _engine, set_shutdown_check wires callback,
set_engine unblocks download dispatch (the exact live failure mode).
2026-05-16 10:31:17 -07:00
Broque Thomas
791e3630ff 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.
2026-05-16 10:24:13 -07:00
Broque Thomas
dcbe09c7aa Add Amazon Music to download source mode dropdown 2026-05-16 10:04:14 -07:00
Broque Thomas
fa73c41ef6 Wire Amazon Music as a first-class download source
Follows the exact same standard as Tidal, Qobuz, HiFi, and Deezer.

registry.py — import + register AmazonDownloadClient as 'amazon'.

amazon_download_client.py — read amazon_download.quality / allow_fallback
from config on init; pass quality as preferred_codec to AmazonClient;
_download_sync codec waterfall respects allow_fallback flag.

download_orchestrator.py — reload_settings() updates preferred_codec +
allow_fallback on the live client after a settings save. 'amazon' added
to _streaming_sources so search_and_download_best routes it correctly.

api_call_tracker.py — 'amazon' registered in RATE_LIMITS (120/min),
SERVICE_LABELS, and SERVICE_ORDER so API call monitoring shows Amazon.

web_server.py — 'amazon_download' added to the settings service loop.
'amazon' added to serverless_sources (no slskd probe needed). Streaming
file-finder extended to handle amazon username + ||asin||title encoding
(extension-less fuzzy match, same as Tidal/Qobuz/HiFi). New endpoint:
GET /api/amazon/test-connection → checks T2Tunes proxy status.

webui/index.html — amazon-download-settings-container: quality dropdown
(flac/opus/eac3), allow-fallback checkbox, test-connection button.

webui/static/settings.js — 'Amazon Music' added to HYBRID_SOURCES,
_hybridSourceEnabled, allSources mode list, loadSettings(), saveSettings()
payload, updateDownloadSourceUI() show/hide + auto-test. New
testAmazonConnection() function.
2026-05-16 09:40:50 -07:00
Broque Thomas
85984d4174 Amazon Music provider: metadata client + download source (T2Tunes)
core/amazon_client.py — T2Tunes-backed metadata client following the
DeezerClient/iTunesClient contract. Exposes search_tracks, search_artists,
search_albums, get_track_details, get_album, get_album_tracks, get_artist,
get_artist_albums, get_track_features. T2TunesStreamInfo dataclass captures
the hex decryption key returned by the proxy (CENC/AES-128). Handles the
"stremeable" API typo. 0.5 s rate-limit guard + api_call_tracker.

core/amazon_download_client.py — DownloadSourcePlugin backed by the above
client. Codec waterfall: FLAC → Opus → EAC3. Downloads the encrypted MP4
container, decrypts with ffmpeg -decryption_key, yields the native audio
file (.flac / .opus / .eac3). Not yet wired into the app source registry —
validated in isolation only; see tests/tools/.

tools/t2tunes_probe.py + tools/t2tunes_media_plan.py — standalone CLI tools
used for live API exploration during development.

tests/tools/test_amazon_client.py — 72 unit tests (all mocked).
tests/tools/test_amazon_download_client.py — 52 unit tests (all mocked).
124 tests pass.
2026-05-16 07:46:41 -07:00