Commit graph

2690 commits

Author SHA1 Message Date
Antti Kettunen
b85a05fb88
Move image URL normalization into metadata helpers
- keep existing /api/image-proxy URLs from being wrapped again
- reuse the shared metadata package instead of duplicating URL logic in web_server.py
- add regression coverage for proxy passthrough and internal URL normalization
2026-05-02 22:02:36 +03:00
Antti Kettunen
2b3022f6b0
Fix Spotify source ID fallback
- Prefer real Spotify IDs when importing Spotify contexts
- Skip numeric fallback IDs so Deezer values do not leak into spotify_* columns
- Add regressions for import context and SoulSync library writes
- Keep the route test asserting the Spotify album link
2026-05-02 22:02:01 +03:00
Antti Kettunen
2693640c62
Hide dashboard status placeholders until ready
- Keep the sidebar and dashboard service cards neutral until the first /status payload arrives
- Prevent placeholder source names and card text from flashing on dashboard load
- Reveal the real service status only after the live snapshot populates the UI
2026-05-02 22:02:01 +03:00
Antti Kettunen
a2176af00e
Rename metadata source status selectors
- Switch the dashboard/sidebar service-status card from spotify-branded ids to metadata-source ids
- Update the shared status helpers to target the renamed metadata-source card
- Keep the actual Spotify auth and settings UI unchanged
2026-05-02 22:02:01 +03:00
Antti Kettunen
36131656dd
Make Spotify status updates event-driven
- move Spotify status publishing onto auth, disconnect, and rate-limit transitions
- keep dashboard and debug consumers on the shared cached snapshot
- leave only the initial snapshot seed as a fallback probe
2026-05-02 22:02:01 +03:00
Antti Kettunen
cc13fb8f01
Move metadata status cache into core/metadata
- move metadata-source and Spotify status caching out of web_server.py
- keep the public /status payload unchanged while shrinking server-side glue
- centralize invalidation and TTL handling in core/metadata/status.py
2026-05-02 22:02:00 +03:00
Antti Kettunen
3c7187fb32
Reduce Spotify status polling
- cache Spotify auth and rate-limit status separately from the generic metadata source snapshot
- refresh Spotify status only on explicit auth/disconnect/test paths or after the TTL expires
- keep the legacy OAuth callback paths aligned with the same invalidation helper
2026-05-02 22:02:00 +03:00
Antti Kettunen
e2bd0e1871
Split metadata source and Spotify status
- Keep the primary metadata provider snapshot generic and move Spotify auth/rate-limit details into a separate status object.
- Update the websocket fixture and dashboard/settings consumers to read the two buckets independently.
2026-05-02 22:02:00 +03:00
Antti Kettunen
36267618a3
Rename status cache to metadata_source
Expose the primary metadata provider status under a generic cache key and update the websocket fixture plus frontend readers to match.
2026-05-02 22:02:00 +03:00
Antti Kettunen
1d9d399a2f
Fix dashboard metadata source testing
- Point the dashboard Test Connection button at the active metadata source instead of hardcoded Spotify.
- Populate the response line from the current status payload so the card no longer stays at Response: --.
- Keep the existing Spotify-specific auth handling when Spotify is the configured source.
2026-05-02 22:02:00 +03:00
Antti Kettunen
5ef83cea72
Stop watchlist countdown refetch loop
- Avoid refetching /api/watchlist/count every second when no auto-run is scheduled.
- Keep the timer active only while a next run exists; otherwise leave the label static.
2026-05-02 22:02:00 +03:00
BoulderBadgeDad
7405d04900
Merge pull request #459 from elmerohueso/suppress-duplicate-toasts
add guards to error, complete, and cancelled toasts
2026-05-02 10:07:52 -07:00
elmerohueso
cd19aa0301 revert tidal artist/track id name for hifi downloads
Co-authored-by: Copilot <copilot@github.com>
2026-05-02 07:56:47 -06:00
elmerohueso
a845a3d49d sync up tidal and hifi to get the same tags 2026-05-02 07:50:13 -06:00
elmerohueso
4ddb86522c name tidal and hifi tags the same way 2026-05-02 07:50:13 -06:00
elmerohueso
e78dd7f593 get tidal tags during download, without needing to go through the enrichment pipeline 2026-05-02 07:50:12 -06:00
elmerohueso
1f4e8e5e3b get hifi tags during download, without needing to go through the enrichment pipeline 2026-05-02 07:50:12 -06:00
elmerohueso
02de2fa4e7 add tidal and hifi metdata changes to the UI 2026-05-02 07:50:12 -06:00
elmerohueso
b363afe195 bpm for tidal, copyright and bpm for hifi 2026-05-02 07:50:12 -06:00
elmerohueso
f9f47f978e fix post-download tagging, and enable tagging for hifi 2026-05-02 07:50:12 -06:00
elmerohueso
5880e32a92 add guards to error, complete, and cancelled toasts 2026-05-02 07:50:12 -06:00
BoulderBadgeDad
4ed603713d
Merge pull request #466 from Nezreka/cleanup/enrichment-bubble-old-routes
Drop old per-service enrichment routes after registry cutover
2026-05-01 20:52:54 -07:00
Broque Thomas
7e32618f86 Drop old per-service enrichment routes after registry cutover
Followup to the enrichment-bubble registry consolidation. The
dashboard polling + click handlers all hit
/api/enrichment/<service>/{status,pause,resume} now, so the 30
hand-rolled per-service routes in web_server.py have zero callers
and can come out:

  /api/musicbrainz/{status,pause,resume}
  /api/audiodb/{status,pause,resume}
  /api/discogs/{status,pause,resume}
  /api/deezer/{status,pause,resume}
  /api/spotify-enrichment/{status,pause,resume}
  /api/itunes-enrichment/{status,pause,resume}
  /api/lastfm-enrichment/{status,pause,resume}
  /api/genius-enrichment/{status,pause,resume}
  /api/tidal-enrichment/{status,pause,resume}
  /api/qobuz-enrichment/{status,pause,resume}

Worker init blocks stay (they still construct the workers + persist
pause state). Section comment headers are preserved with a one-line
note pointing readers at the new generic blueprint.

Test fixtures in tests/conftest.py and
tests/metadata/test_enrichment_events.py also updated to use the
new URL paths so they reflect production reality. They were
synthetic stubs that never depended on the production routes —
purely cosmetic alignment.

Net: ~510 lines deleted from web_server.py. Full pytest 1541
passed; ruff clean.
2026-05-01 20:43:18 -07:00
BoulderBadgeDad
3a7a2260b2
Merge pull request #465 from Nezreka/feat/enrichment-bubble-registry
Consolidate enrichment bubble routes behind a service registry
2026-05-01 20:28:17 -07:00
Broque Thomas
98c04cf332 Consolidate enrichment bubble routes behind a service registry
The dashboard's enrichment-status bubbles (MusicBrainz, AudioDB,
Discogs, Deezer, Spotify, iTunes, Last.fm, Genius, Tidal, Qobuz) each
had its own copy-pasted /status, /pause, /resume route in web_server.py
— 30 routes that differed only in the worker reference and a couple
of per-service quirks (Spotify's rate-limit guard, Last.fm/Genius
yield-override behavior, Tidal/Qobuz extra status fields).

Replace them with a registry-driven blueprint:

- core/enrichment/services.py declares an EnrichmentService dataclass
  with worker_getter, config_paused_key, pre_resume_check,
  auto_pause_token, and extra_status_defaults — all variation captured
  as data, no branching on service id.
- core/enrichment/api.py exposes a Flask blueprint with three routes
  (/api/enrichment/<service>/{status,pause,resume}). Per-service
  quirks are honored via the descriptor: Spotify's rate-limit ban
  still returns 429 with `rate_limited: true`, Last.fm/Genius still
  drop the auto-pause token and add the yield override, Tidal/Qobuz
  still merge `authenticated: false` into the fallback payload.
- web_server.py registers all 10 services after their workers
  initialize, wires the host-side hooks (config_manager.set,
  _download_auto_paused.discard, _download_yield_override.add), and
  registers the blueprint.
- webui/static/enrichment.js polling + click handlers now hit the
  generic endpoints. The per-service `update<Service>StatusFromData`
  functions are unchanged — they still process the same payload.

This is the cutover step. Old per-service routes are intentionally
left in place as a fallback during the soak period — they currently
have zero callers in the codebase and will be deleted in a follow-up
patch once production has run on the new pipeline for a few days.

27 new tests in tests/test_enrichment_services.py cover the registry
behavior + every quirk path through the generic blueprint (rate-limit
guard, auto-pause token cleanup, persisted-pause config keys, extra
default fields, worker-not-initialized fallback, exceptions). Full
suite 1541 passed; ruff clean.
2026-05-01 20:14:30 -07:00
BoulderBadgeDad
30c506cd12
Merge pull request #464 from Nezreka/feat/sidebar-library-artist-breadcrumb
Show artist breadcrumb on sidebar Library button when on artist-detai…
2026-05-01 18:48:38 -07:00
Broque Thomas
0c4fad033d Show artist breadcrumb on sidebar Library button when on artist-detail page
Artist-detail is a "pseudo-page" reachable from Library, the unified
Search page, and the global search popover. It has no [data-page]
match in the sidebar, so navigateToPage's bulk-active-removal left
every nav button unhighlighted while the user was viewing an artist —
the sidebar offered no visual anchor for where they were.

Now:
- navigateToPage('artist-detail') falls back to highlighting the
  Library button when no [data-page] match exists, anchoring the
  sidebar to the canonical home for artist detail views.
- A new _updateSidebarLibraryBreadcrumb() helper rewrites the Library
  button label between plain "Library" and a "Library / Artist Name"
  breadcrumb based on currentPage + artistDetailPageState. Long names
  (>14 chars) truncate with an ellipsis; the full name shows on hover
  via the title attribute.
- Called from navigateToPage (entering / leaving the page) and from
  loadArtistDetailData (covers same-page artist switches in the
  similar-artist chain where currentPage stays 'artist-detail').

CSS adds .nav-text-root / .nav-text-sep / .nav-text-context selectors
so the "Library" anchor word stays visually dominant while the
separator and artist name dim to a secondary tier — readable but not
competing for attention.

Pure visual change. No backend touched. No new tests (DOM-only).
2026-05-01 17:49:34 -07:00
BoulderBadgeDad
6db2cbe2a7
Merge pull request #462 from Nezreka/release/2.4.1
Bump version to 2.4.1
2026-05-01 15:07:54 -07:00
Broque Thomas
84810b4de4 Bump version to 2.4.1
Patch release wrapping up the 2.4.1 dev cycle. Highlights:
- Watchlist no longer re-downloads compilation/soundtrack tracks
  (#458 dedup orphan cleanup + the album-match fix work in tandem
  to stop the loop).
- Duplicate detector catches slskd dedup orphans via a second
  filename-bucket pass.
- Beatport tab hidden temporarily — Cloudflare Turnstile blocks the
  scraper and the official OAuth API is closed to public devs.
- Service worker for cover art + installable PWA manifest.
- Browser caching for static assets (1y) and discover pages (5min).
- Socket.IO same-origin default + admin-only /api/settings.

Files updated:
- web_server.py: _SOULSYNC_BASE_VERSION 2.4.0 -> 2.4.1
- webui/index.html: sidebar version button + modal subtitle
- webui/static/helper.js: WHATS_NEW dev-cycle marker -> release date,
  fallback version in _getLatestWhatsNewVersion, 8 new
  VERSION_MODAL_SECTIONS entries promoted from this cycle
- .github/workflows/docker-publish.yml: workflow_dispatch default
  version_tag updated to 2.4.1
2026-05-01 15:04:33 -07:00
BoulderBadgeDad
bec5aa7d67
Merge pull request #461 from Nezreka/fix/watchlist-redownload-and-duplicate-detection
Fix/watchlist redownload and duplicate detection
2026-05-01 14:04:06 -07:00
Broque Thomas
0e68109b68 Add filename-pass safety: require duration agreement or artist match
Self-review of the previous commit found a real false-positive risk in
the new filename-bucket pass: two unrelated songs that happen to share
a canonical filename (e.g. ``Yellow.mp3`` by Coldplay vs by some other
artist) would be grouped because all metadata gates were dropped.

The filename pass now layers a safety net under ``require_metadata_match=False``:

- If both rows carry a duration: must agree within 3 seconds. Same
  source download = identical duration; a 3+ second gap means
  different recordings.
- Else if both rows carry an artist: relaxed 0.6 similarity check —
  catches dedup orphans that share an artist tag while rejecting
  strangers-with-same-filename.
- Else (no duration AND at least one artist blank): skip — too little
  signal to safely group.

5 additional regression tests cover the false-positive prevention
paths plus the genuine dedup-orphan scenarios that must still be
caught after the safety net.
2026-05-01 13:15:57 -07:00
Broque Thomas
6e61890551 Stop watchlist re-downloading compilation tracks; catch slskd dedup orphans
Two related bugs reported on Discord by Mushy.

1. The watchlist re-downloaded the same OST track up to 7 times.

   ``is_track_missing_from_library`` compared Spotify's album name and
   the media-server scan's album name with a raw SequenceMatcher at a
   strict 0.85 threshold. Compilations and soundtracks routinely fail
   this — Spotify reports
   ``"Napoleon Dynamite (Music From The Motion Picture)"`` while the
   Plex / Navidrome / Jellyfin tag scan saves it as
   ``"Napoleon Dynamite OST"``. Raw similarity ≈ 0.49, so the scanner
   declared the track missing on every 30-minute scan and added it back
   to the wishlist. The wishlist then issued a fresh download. slskd
   appended ``_<19-digit-ns-timestamp>`` to each new copy because the
   target file already existed, and the user ended up with seven copies
   of one song in one folder.

   Fix: extract two pure helpers — ``_normalize_album_for_match``
   strips qualifier parentheticals (Music From X, OST, Deluxe Edition,
   Remastered, Anniversary, etc.) and trailing dash-clauses;
   ``_albums_likely_match`` checks equality after normalization,
   substring containment, and a relaxed 0.6 fuzzy ratio. A volume /
   part / disc / standalone-trailing-number guard rejects pairs like
   ``"Greatest Hits Vol. 1"`` vs ``"Greatest Hits Vol. 2"`` so the
   relaxed threshold doesn't introduce false positives on serialized
   releases. After this change the Napoleon Dynamite case collapses
   to ``"napoleon dynamite" == "napoleon dynamite"`` via the equality
   short-circuit and the redownload loop dies.

2. The duplicate detector found only one of the seven dupe files.

   The detector buckets tracks by the first 4 chars of their normalized
   tag title. Files written by slskd directly into a library folder
   often get inconsistent (or blank) tags from the media-server rescan,
   so the seven copies were bucketed apart by parsed title and never
   compared.

   Fix: refactor the per-bucket comparison into ``_scan_bucket``, then
   add a second pass — ``_build_filename_buckets`` re-buckets leftover
   tracks by canonical filename stem (slskd dedup tail stripped via
   ``_strip_slskd_dedup_suffix``, same regex the import-cleanup PR uses)
   plus extension. Filename agreement is itself strong evidence the
   files came from the same source download, so the second pass calls
   ``_scan_bucket`` with ``require_metadata_match=False`` to skip the
   title / artist / cross-album gates. The same-physical-file guard
   still runs so bind-mount duplicates aren't flagged.

72 new regression tests across two files cover the album-match
helpers (28 tests including the Napoleon Dynamite scenario, 7 volume
disagreements, 8 positive/negative pairs, 5 defensive cases) and the
new filename-bucket pass (16 tests across bucket construction, scan
integration, and existing title-pass behavior). Full pytest 1509
passed; ruff clean.

Reported by Mushy in Discord.
2026-05-01 12:57:50 -07:00
BoulderBadgeDad
8c8a1c05eb
Merge pull request #460 from Nezreka/fix/disable-beatport-features
Hide Beatport tab temporarily
2026-05-01 11:57:58 -07:00
Broque Thomas
ab884292d1 Hide Beatport tab temporarily
Some checks failed
Compile the app and run tests / sanity-check (push) Has been cancelled
Beatport added Cloudflare Turnstile to every public page on
beatport.com. The unified scraper now receives bot-challenge HTML
instead of real content, so all /api/beatport/* endpoints return
500 with "Could not fetch Beatport homepage".

The official Beatport v4 API is locked behind OAuth application
registration that isn't open to the public — confirmed via the
docs at api.beatport.com/v4/docs and community projects
(beets-beatport4). The public docs SPA client_id only accepts
browser-based flows (post-message redirect URI), which can't be
driven server-side.

Hide the Beatport tab on the Sync page so users stop hitting the
broken endpoints. Backend routes and beatport_unified_scraper.py
stay in code — revival is a one-attribute HTML change once
Cloudflare relaxes or a workaround is found.

Reported via the homepage 500 spam in user logs.
2026-05-01 11:44:06 -07:00
BoulderBadgeDad
8bac5a5109
Merge pull request #458 from Nezreka/fix/cleanup-slskd-dedup-orphans
Prune slskd dedup orphans after import
2026-05-01 09:47:36 -07:00
Broque Thomas
46d8e15674 Prune slskd dedup orphans after import
slskd appends "_<19-digit unix-nanosecond timestamp>" to a downloaded
filename when the destination already contains a same-named file
(concurrent downloads of the same track, partial-file retries after a
connection drop, cancelled-then-redownloaded files, the same track
surfacing in multiple synced playlists). The file-finder code already
recognized the suffix when matching a download to its source — but
after the canonical file moved into the library, the leftover
"_<timestamp>" siblings sat orphaned in the downloads folder forever.

Reported on Discord by Shdjfgatdif.

cleanup_slskd_dedup_siblings() runs at the end of each successful
import (3 safe_move_file sites in pipeline.py) and prunes any
remaining siblings that strip down to the canonical stem with the
same extension. Conservative match (>= 18 trailing digits) keeps
legitimate filenames like "Track 5" and "Album 1995" untouched. Per-
file unlink failures are swallowed so a single locked file doesn't
block the rest.

17 regression tests cover the suffix-strip primitive, orphan removal,
no-op cases, mismatched extensions, subdirectories, and partial-failure
recovery.
2026-05-01 09:35:08 -07:00
BoulderBadgeDad
94b08bbb49
Merge pull request #457 from kettui/refactor/spotify-auth-flow
Clarify metadata source gating and Spotify auth flow
2026-05-01 08:55:41 -07:00
BoulderBadgeDad
57d0d2361c
Merge pull request #456 from Nezreka/feat/parallel-singles-import
Parallelize singles-import processing with a 3-worker executor
2026-05-01 08:48:04 -07:00
Broque Thomas
ab85c45785 Restore soulsync logger state between parallel-imports tests
Importing web_server fires utils.logging_config.setup_logging at
module-init, which clears + re-installs handlers on the shared
'soulsync' logger and pins its level to the user's configured
value. That mutation leaks across tests in the same pytest process.

This file runs alphabetically before test_library_reorganize_orchestrator,
so the leak broke test_watchdog_warns_about_stuck_workers downstream
— it relies on caplog capturing soulsync.library_reorganize warnings
via root-logger propagation, and the reconfigured logger's new
handler chain swallowed those records before they reached caplog
(caplog.records came back empty even though pytest's live-log
capture clearly showed the warning fired).

Adds an autouse fixture that snapshots the soulsync logger's
handlers, level, and propagate flag before each test in this file
and restores them afterwards. Pollution stays scoped to this file.

tests/test_tidal_auth_instructions.py also imports web_server but
runs alphabetically AFTER test_library_reorganize_orchestrator so
it never tripped this — fix is scoped here, not a project-wide
conftest, so we don't change behaviour for unrelated test files.
2026-05-01 08:45:17 -07:00
Antti Kettunen
4e40bce3e9
Gate Discogs primary source by token
- Show Discogs with a lock icon until a personal access token is present.
- Prevent selecting locked Discogs and steer users to the Discogs settings section.
- Keep metadata-source availability and selection state synced as the token changes.
2026-05-01 12:59:38 +03:00
Antti Kettunen
5ff20fbfec
Polish Spotify source selection
- Show Spotify with a lock icon when it is not currently selectable.
- Keep the explanation in the hover title instead of cluttering the dropdown label.
- Redirect users to the Spotify settings section when they try to pick a locked source.
2026-05-01 12:51:51 +03:00
Antti Kettunen
287c9601fc
Mark Spotify settings as needing auth
- Drive the Spotify settings accordion from live auth state instead of treating it as configured/healthy when the session is missing.
- Reuse the existing yellow missing-state styling so unauthenticated Spotify is visually distinct from active Spotify.
- Keep the shared status refresh path updating the settings view immediately after auth changes.
2026-05-01 12:41:22 +03:00
Antti Kettunen
e615e407e6
Handle Spotify auth completion failures
- Return a distinct post-auth warning page when Spotify OAuth completes but the client still does not report an authenticated session.
- Send the completion signal back to the opener so the settings UI can refresh and show the warning state immediately.
- Keep the standalone callback server and the main Flask callback path aligned on the same result-page helper.
2026-05-01 12:29:06 +03:00
Antti Kettunen
f733744f91
Fix Spotify auth completion sync
- Make the Spotify auth completion popup notify the opener across callback origins.
- Refresh service status in the settings UI after auth completes so the button flips to Disconnect immediately.
- Keep the standalone callback instruction page and the main app flow working with the same completion signal.
2026-05-01 12:14:13 +03:00
Antti Kettunen
74e3cc460c
Simplify service status and labels
- Flatten the Spotify service-status rendering so it shows rate-limit and recovery states explicitly, while otherwise displaying the active metadata provider directly.
- Keep the Spotify auth controls and metadata-source picker aligned with the real session state after authenticate and disconnect flows.
- Return "Unmapped" for unknown metadata source labels instead of implying iTunes.
- Update the metadata registry tests to cover the new label fallback.
2026-05-01 12:06:58 +03:00
Antti Kettunen
55603be14c
Clarify Spotify auth flow and sync UI
- Send Spotify auth completion back to the opener so the settings page refreshes immediately
- Make the local auth flow go straight through to Spotify instead of showing the temporary instruction page
- Keep the remote/docker instruction page available for manual callback setups
- Sync Spotify status, connect/disconnect buttons, and metadata source selection after auth and disconnect
- Keep the disconnect behavior aligned with the active primary metadata source
2026-05-01 11:25:12 +03:00
Antti Kettunen
9646f6ca7f
Clarify Spotify auth actions
- Hide the auth button when a Spotify session is active
- Treat disconnect as a session change, not a provider swap
- Share metadata source labels in the registry
- Tighten rate-limit copy around Spotify-specific behavior
2026-05-01 10:36:50 +03:00
Broque Thomas
1aa565a330 Silence shutdown-time logger errors so CI stderr stays clean
Pytest tears down its log file handles before atexit runs. Every
"Shutting down ..." line a worker emits while stopping then crashes
Python's logger with "I/O operation on closed file" and floods CI
stderr with --- Logging error --- traceback blocks. The CI sanity
check workflow noticed once tests started importing web_server (the
Tidal-auth integration test PR + this parallel-imports PR are the
first two test files that boot the full module).

Adds a tiny atexit handler that flips ``logging.raiseExceptions =
False`` BEFORE the other shutdown handlers run. atexit's LIFO order
makes "registered last" mean "runs first", so this fires ahead of
cleanup_monitor / _atexit_shutdown / _atexit_save_history and any
log calls those make can't bubble the closed-stream traceback.

The shutdown messages themselves are best-effort debug
breadcrumbs, not data we need to preserve at process exit, so
silencing the internal handler errors costs nothing.
2026-04-30 22:58:26 -07:00
Broque Thomas
f339211654 Parallelize singles-import processing with a 3-worker executor
Discord-reported (fresh.dumbledore + maintainer ack): the
/api/import/singles/process route iterated staging files through a
plain Python for loop. Per-file work is dominated by metadata
search round-trips (Spotify/iTunes/Deezer/Discogs), so a multi-
track manual import on a typical home network was painfully slow.

Adds a dedicated import_singles_executor (3 workers) alongside the
existing executor pool, and refactors the route to submit every
file at once and aggregate results via as_completed. Worker count
balances throughput against any single provider's per-source rate
limits — the same shape used by missing_download_executor.

Extracts the per-file pipeline into _process_single_import_file
which returns a typed (status, payload) outcome:
- ("ok", final_title) on success
- ("error", message) for missing/malformed input or pipeline failure
The worker wraps its own exceptions so a single bad file can't
crash the batch; the route adds a belt-and-suspenders try/except
around future.result() for any worker-level surprises.

Pipeline thread-safety verified: post_process_matched_download
already serializes per-file via post_process_locks (one lock per
context_key — and each import gets a unique UUID context_key), DB
writes serialize through SQLite's WAL + busy_timeout, metadata
registry uses RLocks, no bare module-level mutable state.

Adds 9 regression tests:
- 4 worker-contract tests (missing file, malformed match, pipeline
  exception wrapping, happy-path return shape)
- 2 executor-config tests (worker count, thread name prefix)
- 1 integration test that proves the route actually parallelizes
  by checking wall-clock duration is well under sequential cost
- 1 mixed-outcome aggregation test
- 1 worker-crash recovery test

Doesn't address the related "stops on tab close" complaint —
that's a separate request-lifecycle issue that needs job_id +
polling, not just parallelism.
2026-04-30 22:41:04 -07:00
BoulderBadgeDad
7f191be7af
Merge pull request #455 from Nezreka/fix/import-route-singles-through-album-path
Route imported singles/EPs through album_path template
2026-04-30 21:40:33 -07:00