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

97 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-download` → `docker/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_progress``Job.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).flac``13 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.