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>
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user