Files
Lyra/docs/superpowers/plans/2026-07-15-post-deploy-ux-fixes.md
Jonathan c7bab8e997 docs: handoff plan for 3 post-deploy UX fixes
Qobuz progress smoothing, parallel source searches, bonus-track filename
canonicalization. Lyra is live on the VM; these are worker-only polish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 02:24:56 +02:00

7.0 KiB
Raw Permalink Blame History

Post-deploy UX fixes (3 items) — handoff plan

Context: Lyra is DEPLOYED + LIVE on the media VM (ssh alias download = jonathan@10.10.0.103, hostname servarr, x86_64). Pre-built images from git.jger.nl/jonathan/lyra-{web,worker}, whole stack behind its own gluetun VPN, deploy repo Jonathan/vm-downloaddocker/lyra/. Library was wiped to a blank slate; user rebuilds via Last.fm. Two test presses succeeded end-to-end (John Mayer Continuum q3 hi-res, Room for Squares q2) — pipeline is solid. These 3 are UX polish the user hit in real use. See MEMORY.md → project-state.md "VM DEPLOYMENT" for full deploy details.

Workflow for EACH fix: implement in worker/ → run worker tests (cd worker && source .venv/bin/activate && python -m pytest) → on the DEV box ./scripts/build-push.sh (needs docker login git.jger.nl already done this session; a fresh session may need re-login — a real TTY, not the !-prefix) → on VM ssh download 'cd /opt/git/vm-download/docker/lyra && docker compose pull && docker compose up -d worker' → verify with a test press (queue via SQL, see below) and watch Job.downloadProgress/on-disk result. Commit per fix; branch feat/post-deploy-ux, ff-merge to main, push.

Queue a test press on the VM (mimics the Wanted "Search now"): pick a MonitoredRelease id from SELECT id,"artistName",album FROM "MonitoredRelease" WHERE monitored AND NOT ignored AND state<>'fulfilled', then insert a Request+Job (use a /tmp/*.sql file + docker cp + psql -f to avoid ssh quoting hell): INSERT INTO "Request"(id,artist,album,"monitoredReleaseId",status,"createdAt") SELECT gen_random_uuid()::text,"artistName",album,id,'pending',now() FROM "MonitoredRelease" WHERE id='<mrid>' RETURNING id → capture into a Job insert (id,"requestId",state,"currentStage",attempts,"downloadProgress","createdAt","updatedAt") VALUES (gen_random_uuid()::text,rid,'requested','intake',0,0,now(),now()).


Fix 1 — Smooth Qobuz download progress (byte-level)

Problem: the Floor progress bar sits at 0% for the whole Qobuz download then jumps to 100%. Qobuz/streamrip's per-track byte callback is NOT hooked, so Job.downloadProgress comes only from the file-count poller (pipeline._poll_download_progress) — and streamrip downloads tracks concurrently, so 0 files are "complete" until they all land at once. yt-dlp + slskd already report bytes via on_progress (smooth); only Qobuz doesn't.

Files: worker/lyra_worker/adapters/_streamrip.py (the Qobuz download), worker/lyra_worker/pipeline.py (threads on_progress_make_on_progressJob.downloadProgress; poller is the fallback when the per-attempt reports Event is set).

Approach: hook streamrip's progress. streamrip drives progress via a callback/console; investigate its API (it uses a rip.utils/Downloadable byte-progress hook or a tqdm callback). Aggregate downloaded bytes / total bytes summed across the album's concurrent tracks → a single fraction → call the pipeline's on_progress(frac) (which sets reports and writes throttled Job.downloadProgress). Keep the file-count poller as the fallback (adapters that don't report). Match the throttle already in _make_on_progress (≥1% move or every 0.5s).

Test: hard to unit-test streamrip internals — add a unit test that a fake byte-callback aggregates to a monotonic 0→1 fraction, plus live-verify with a test press: Job.downloadProgress climbs mid-download instead of 0→100. (Known caveat if streamrip exposes no usable byte hook: fall back to a smoother file-count cadence, or accept the jump — document the finding.)

Fix 2 — Parallelize the source searches (Soulseek adds ~2 min)

Problem: every acquisition spends up to ~2 min in the Soulseek search. SlskdClient.search_album polls _SEARCH_POLLS=40 × _POLL_SECONDS=3s = 120s (early-exits on isComplete, but slskd searches genuinely run long). The pipeline searches adapters SEQUENTIALLY (for adapter in adapters: adapter.search(target)), so Soulseek's 2 min is added on top of Qobuz's ~instant search.

Files: worker/lyra_worker/pipeline.py (the match phase, for adapter in adapters loop); optionally worker/lyra_worker/adapters/_slskd.py (_SEARCH_POLLS).

Approach: run the per-adapter searches concurrently (e.g. ThreadPoolExecutor, one thread per adapter), collect all candidates when the last returns → total match wall-clock = slowest single search, not the sum. So Qobuz returns instantly and only Soulseek's ~2 min gates. Keep the existing per-adapter try/except (a failing source contributes nothing). Thread-safety caveat: streamrip uses a module-level persistent asyncio loop (_streamrip._run) — verify concurrent Qobuz search from a worker thread is safe (it runs coroutines on that shared loop; may need a lock or to keep Qobuz on the calling thread while Soulseek/YouTube go to threads). Optionally also drop _SEARCH_POLLS to ~20 (60s) with the early-exit.

Test: a pipeline test asserting all adapters' candidates are collected (order-independent) when searched concurrently; existing match/ranking tests stay green. Live-verify a press: match phase finishes in ~the Soulseek time, not Soulseek+Qobuz.

Fix 3 — Canonicalize bonus-track filenames on import

Problem: tracks BEYOND the MusicBrainz tracklist keep streamrip's raw name. Real example: Room for Squares imported 12 clean ## Title.flac + one 13. John Mayer - St. Patrick's Day (Album Version).flac (MB lists 12; Qobuz had a 13th bonus). The main tracklist gets ## Title but extras don't.

Files: worker/lyra_worker/library.py (import_album — how it names/moves audio into Artist/Album (Year)/## Title.ext), and/or the tagger worker/lyra_worker/_mutagen.py.

Approach: find where files are renamed to ## Title.ext. For a track with no MB position/title (an extra), derive a clean name from its own embedded tags (track number + title) or by stripping the ^\d+\.?\s*(Artist\s*-\s*)? prefix from the streamrip filename → ## Title.ext. Ensure the position keeps counting past the MB list (13, 14, …) and the on-disk trackNames capture stays consistent.

Test: unit-test the filename-canonicalizer on inputs like 13. John Mayer - St. Patrick's Day (Album Version).flac13 St. Patrick's Day (Album Version).flac; live-verify by re-pressing an album with a bonus track (or docker compose exec worker a rename dry-run). NOTE: the already-imported Room for Squares track 13 can be fixed by re-pressing (force upgrade) or a one-off rename — mention to the user.


Self-review / notes

  • All three are worker-only → only the worker image rebuilds/redeploys (docker compose up -d worker).
  • Don't disrupt the live stack: the VM is in use; deploy the worker, verify one press, done.
  • If context/time is tight, do them in priority order: 2 (search speed) and 3 (filenames) are self-contained and low-risk; 1 (streamrip progress) is the deepest (streamrip internals) — timebox it and fall back to documenting if streamrip has no clean byte hook.