soulsync/tests
Broque Thomas e577f3cf1f Fix three Lidarr bugs that prevented it from being a real download source
Investigation surfaced that Lidarr was wired into the orchestrator but
the actual download flow had blockers:

1. **Wrong file misfiled.** Lidarr grabs whole albums; SoulSync's
   matched-context post-processing wants the SPECIFIC track the user
   requested. Old code copied every track in the album and reported
   `imported_files[0]` as `file_path` — almost always pointing to
   track 1, not the user's actual track. Post-processing then tagged
   track 1 with the requested track's metadata. Misfiling on every
   real download.

   Fix: parse the wanted track title out of the dispatch display name
   (which `_search_sync` already builds as
   `f"{artist} - {album} - {track_title}"`), look it up against
   Lidarr's `track` API, resolve the matching `trackFileId` to a path,
   and copy ONLY that file. Punctuation-tolerant fuzzy match handles
   the common "m.A.A.d city" vs "maad city" case. Album-level
   dispatches (no track in the display) preserve the old first-file
   fallback so existing album-grab UX is unchanged.

2. **Hardcoded `metadataProfileId=1`.** Required by Lidarr's
   artist-add API. On installs where the user deleted/recreated
   metadata profiles, that id no longer exists and the call fails
   with HTTP 400 — which silently breaks every download flow that
   needs to add an artist. Real-world Lidarr installs do this all
   the time.

   Fix: `_get_metadata_profile_id()` calls Lidarr's `metadataprofile`
   API and returns the first available id. Falls back to 1 only when
   the API call fails entirely (preserves previous behavior so this
   change can't make things worse).

3. **Polling never broke the outer loop on completion.** The inner
   `for item in queue['records']` had `break` statements at status
   transitions, but those only escaped the queue iteration — the
   outer `for poll in range(max_polls)` kept spinning until the
   600-poll timeout even after the album was clearly imported.
   `for/else` semantics didn't apply because completion was detected
   inside the inner loop, not by it running to exhaustion.

   Fix: replaced with an explicit `download_complete` flag set when
   `album/{id}` reports `trackFileCount > 0` (the authoritative
   completion signal — works even when the queue record disappeared
   between polls). Outer loop breaks immediately once the flag flips.

Helper functions added: `_extract_wanted_track_title` (staticmethod,
splits the display name; >=3 parts → track dispatch, 2 parts → album
dispatch), `_normalize_for_match` (lowercase + strip punctuation +
collapse whitespace for fuzzy compare), `_title_similarity` (cheap
score: equal=1.0, substring=0.85, token-overlap-ratio otherwise),
`_pick_track_file_for_wanted` (orchestrates the API calls).

Settings tooltip updated to be honest about Lidarr's natural shape:
album-grabber, no-op for playlist sync, hybrid mode falls through to
other sources for track searches. Sets correct expectations.

Tests: `tests/test_lidarr_download_client.py` — 21 isolated tests
covering pure helpers (title extraction, normalization, similarity)
and the file-picker integration paths (matching path, punctuation
tolerance, below-threshold fallback, missing trackFileId, missing
file on disk, API failures, malformed responses). No live Lidarr
needed — `_api_get` mocked at the client boundary.

Isolation: ONLY touches `core/lidarr_download_client.py`, the Lidarr
settings tooltip in `webui/index.html`, the Lidarr WHATS_NEW entry
in `webui/static/helper.js`, and the new test file. No changes to
the orchestrator, other download clients, the import pipeline,
side_effects, web_server.py, settings.js, or any shared validation /
monitor / task_worker code. Other download sources are not affected
in any way.

Verified: 1753 tests pass (21 new), ruff clean.
2026-05-03 15:49:38 -07:00
..
artists Lift enhance_artist_quality to core/artists/quality.py 2026-04-29 10:05:57 -07:00
automation Surface handler-returned errors in automation last_error 2026-04-30 15:45:28 -07:00
discovery Preserve artwork in quality scanner wishlist handoff 2026-04-30 21:42:16 +03:00
downloads Wire SoundCloud as a first-class download source 2026-05-03 12:54:21 -07:00
imports Reject broken downloads before tagging via universal integrity check 2026-05-03 08:21:01 -07:00
library Fix Album Completeness Auto-Fill on Docker / shared-library setups (#476) 2026-05-03 10:11:06 -07:00
metadata Move image URL normalization into metadata helpers 2026-05-02 22:02:36 +03:00
playlists Lift _prepare_stream_task + playlist_explorer_build_tree to core/ 2026-04-29 11:26:07 -07:00
search Search lift: pre-merge parity polish for cin's review 2026-04-27 15:32:48 -07:00
stats Lift /api/stats/* and /api/listening-stats/* into core/stats/ 2026-04-27 14:27:03 -07:00
streaming Mock streaming prep sleep in tests 2026-04-30 21:26:28 +03:00
watchlist Lift _process_watchlist_scan_automatically to core/watchlist/auto_scan.py 2026-04-29 08:17:41 -07:00
wishlist Coerce wishlist track counts before category checks 2026-04-30 21:42:43 +03:00
conftest.py Split metadata source and Spotify status 2026-05-02 22:02:00 +03:00
test_acoustid_scanner.py Fix null-pointer error in acoustid_scanner 2026-04-17 19:17:24 +03:00
test_album_completeness_job.py Fix DummyConfigManager position in album completeness job test 2026-04-27 19:55:06 +03:00
test_auth_usage_throttle.py test: auth last_used_at write throttle 2026-04-19 15:22:24 -07:00
test_config_save_retry.py Stop config retry tests from writing to the real DB 2026-04-30 14:07:00 -07:00
test_content_filter_regex.py Fix watchlist content filters: live false positives + auto-scan bypass 2026-04-21 19:16:25 -07:00
test_dashboard_events.py Realign test file names 2026-04-17 21:25:01 +03:00
test_discogs_track_count.py Discogs: count rows with empty type_ as real tracks too 2026-04-25 08:27:44 -07:00
test_download_orchestrator_soundcloud.py Wire SoundCloud as a first-class download source 2026-05-03 12:54:21 -07:00
test_downloads_pagination.py Lift shared runtime state into core 2026-04-27 19:54:44 +03:00
test_duplicate_detector_mount_paths.py Filter same-physical-file duplicates from duplicate detector 2026-04-30 14:06:10 -07:00
test_duplicate_detector_slskd_dedup.py Add filename-pass safety: require duration agreement or artist match 2026-05-01 13:15:57 -07:00
test_enrichment_services.py Consolidate enrichment bubble routes behind a service registry 2026-05-01 20:14:30 -07:00
test_featured_artist_completion.py Match featured-artist tracks across discography completion 2026-04-30 19:31:11 -07:00
test_hifi_instance_methods.py validate hifi instance reorder against pre-existing instances 2026-04-28 20:19:01 -06:00
test_hls_parsing.py fix: pick OS-specific ffmpeg binary in hls demux fallback test 2026-04-28 21:13:54 -07:00
test_import_singles_parallel.py Restore soulsync logger state between parallel-imports tests 2026-05-01 08:45:17 -07:00
test_library_reorganize.py Fix CI test failures from incomplete dummy config and encoding 2026-04-19 13:04:55 -07:00
test_library_reorganize_orchestrator.py Reorganize: fix moved-count + status/total UX issues from PR #377 review 2026-04-25 09:07:44 -07:00
test_library_track_identity.py Make extract_external_ids recognize all source-tagging conventions 2026-05-02 18:26:12 -07:00
test_lidarr_download_client.py Fix three Lidarr bugs that prevented it from being a real download source 2026-05-03 15:49:38 -07:00
test_listening_stats_batch_queries.py test: listening stats worker batched query paths 2026-04-19 15:22:25 -07:00
test_missing_cover_art.py Fix CI test failures from incomplete dummy config and encoding 2026-04-19 13:04:55 -07:00
test_provenance_id_persistence.py Persist source IDs at download time + backfill onto tracks on sync 2026-05-02 17:44:10 -07:00
test_qobuz_credential_sync.py Sync Qobuz auth to enrichment worker after login 2026-05-02 14:00:04 -07:00
test_reorganize_db_methods.py Reorganize queue: race + dedupe fixes from kettui review 2026-04-26 08:40:24 -07:00
test_reorganize_queue.py Reorganize queue: race + dedupe fixes from kettui review 2026-04-26 08:40:24 -07:00
test_reorganize_runner.py Library reorganize: FIFO queue with live status panel 2026-04-25 18:01:32 -07:00
test_repair_worker_album_fill.py Fix CI test failures from incomplete dummy config and encoding 2026-04-19 13:04:55 -07:00
test_request_cleanup_timer.py test: api/request periodic cleanup timer 2026-04-19 15:22:25 -07:00
test_runtime_state.py Tighten metadata and import safety 2026-04-27 20:28:05 +03:00
test_script_split_integrity.py Unify artist detail: route source artists to standalone page, retire inline Artists page 2026-04-22 17:00:52 -07:00
test_socketio_cors.py Socket.IO CORS: handle self-review nits 2026-04-26 19:24:43 -07:00
test_soundcloud_client.py Fix three SoundCloud integration gaps surfaced by smoke testing 2026-05-03 13:09:02 -07:00
test_spa_deep_linking.py Rename Search page id from 'downloads' to 'search', bump to 2.43 2026-04-22 13:22:49 -07:00
test_sync_discovery_scan_events.py Realign test file names 2026-04-17 21:25:01 +03:00
test_tidal_auth_instructions.py Show Tidal callback port (not Spotify's) in auth instructions 2026-04-30 20:54:06 -07:00
test_tidal_auth_redirect_uri.py Honor configured Tidal redirect_uri, drop request-host fallback 2026-05-02 19:20:18 -07:00
test_tidal_search_shortening.py Reject Tidal streams that silently downgrade from the requested quality 2026-04-24 13:12:30 -07:00
test_tool_progress_events.py Realign test file names 2026-04-17 21:25:01 +03:00
test_track_artist_matching.py Add fallback negative-case test for track-artist matching 2026-04-30 17:13:15 -07:00
test_track_number_repair.py Fix CI test failures from incomplete dummy config and encoding 2026-04-19 13:04:55 -07:00
test_track_search_single_query.py test: api_search_tracks single-query track search 2026-04-19 15:22:24 -07:00
test_unknown_artist_fixer.py Fix test using old deezer_track_id column name 2026-04-20 21:11:45 -07:00
test_watchlist_album_match.py Stop watchlist re-downloading compilation tracks; catch slskd dedup orphans 2026-05-01 12:57:50 -07:00
test_watchlist_bulk_add.py Bulk watchlist add: fall back through every source ID, not just active 2026-04-30 20:27:42 -07:00
test_watchlist_scanner_scan.py Move the import flows and related post-processing pipelines into separate modules 2026-04-27 19:54:43 +03:00
test_websocket_client_transport.py Repoint websocket transport test to core.js after split 2026-04-22 11:30:39 -07:00
test_websocket_infrastructure.py Split metadata source and Spotify status 2026-05-02 22:02:00 +03:00
test_wishlist_pagination.py test: wishlist SQL pagination and category filter 2026-04-19 15:22:24 -07:00
test_worker_existing_id_marks_matched.py test: enrichment worker re-processing fix and migration backfill 2026-04-19 15:22:24 -07:00
test_worker_utils_album_track_count.py Fix Album Completeness job reporting zero findings for every album 2026-04-24 12:39:41 -07:00