yt-dlp declares `dependencies = []` and puts everything in extras, so the
bare `yt-dlp` requirement installed it with no yt-dlp-ejs and no JavaScript
runtime. Without those it cannot solve YouTube's player challenges and falls
back to media URLs that YouTube rejects with HTTP 403, which is the failure
seen when adding a song. Require `yt-dlp[default,curl-cffi,deno]` instead;
the deno extra ships the JS runtime as a wheel, so the image needs no extra
apt packages.
Around that, make the stage survive the next time YouTube changes something:
the download is retried against each player client in `ytdlp_clients`
(default `["default", "tv"]`, the latter not needing a PO token) before
giving up, yt-dlp's retry counts are raised, and its own log lines are routed
into the job log so a PO-token demand can be told apart from a rate limit.
The final error names the running yt-dlp version, since a stale one is the
usual cause.
Cookies, TLS impersonation and a bgutil PO token provider URL are now config
fields, so an operator can work around a block without a code change.
`_classify_downloads` skips .part/.ytdl scratch files, which retries make far
more likely to be lying around.
- DELETE now checks under the shared lock whether another job with the
same slug is queued/running before removing the job entry, so
deleting a done parent can no longer rmtree a rebuild's active
work/output directories.
- _discover_jobs now wraps the iterdir/stat/read_warnings calls in the
same try/except as the JSON parse, so one corrupt or torn sidecar
file (e.g. from a killed mid-write) no longer crashes the whole app
at boot.
- Fixed test_delete_tolerates_a_work_dir_that_is_already_gone, which
had become vacuous since _write_sidecar now eagerly creates the work
dir; it now removes the dir before asserting DELETE tolerates its
absence.
- Added a regression test covering the DELETE slug-collision race.
.review-lane spans the full timeline box height (top:0; bottom:0) to
group each word with its note visually, but as a positioned element it
painted after the unpositioned .review-roll SVG, so the lane's
background covered the note bars for every other word. Give .review-roll
position: relative so DOM order (lanes, then notes) governs paint order
instead.
Replace arbitrary demo pitches with the actual melody transcribed from
Gilbert DeBenedetti's public-domain "Level 4, Flowing" arrangement (E
minor, 6/8), converted to UltraStar semitones, with one note per word
across both demo lines.
The review panel used to show a full line of packed text with one
undifferentiated strip of notes below it, so it was impossible to tell
which notes belonged to which word. The forced-alignment stage already
produces per-word timestamps in alignment.json, but they were never
surfaced past the pipeline.
/api/jobs/{id}/review now includes each line's per-word timing, and the
frontend spaces words out by their real sung timing (karaoke-style)
instead of normal text flow, with the note strip sharing that same time
axis so each note lands directly under its word. Alternating lane
backgrounds make the word/note pairing unambiguous. Dropped lines and
jobs without alignment data keep the old plain-text rendering.
Also fix a secondary ripple in build_notes(): a skipped row (inside a
missing span) no longer advances prev_line/prev_end, so it can't corrupt
line-initial spacing or break timing for its own line's later words.
renderLyricsReview was caching its job ID only in the done path, allowing
stale data to persist when switching between done/running/failed jobs.
Now clears the cache in the failed and not-done branches so a previously-
viewed done job always re-fetches when re-selected.
Regression test: test_lyrics_review_cache_clears_on_job_status_change
verifies both branches clear the cache key before rendering placeholders.
- Added classify_lyrics_match import to wire lyric classification into align stage
- Changed _assign_lines_to_sections signature to accept line_indices list instead of ref
- Modified _run to classify lyrics match, filter dropped lines, and write align_meta.json
- Sort segments by start time before processing for correct matching of non-chronological transcripts
- Added tests for dropped line detection and missing span flagging
- add test_rebuild_inherits_the_parents_cover: covers the cover_path
inheritance mechanism in the rebuild endpoint (Task 2's upload-retention
design relied on it, but nothing exercised it)
- tighten test_lineage_marker_survives_in_the_accessibility_tree to assert
the literal " rebuilt" screen-reader text, not just that rebuilt_from
is referenced somewhere in paintCard
- normalize submit's youtube_url/genre/language the same way rebuild does,
for symmetry, plus a pinning test
- pin acquire's stage outputs to [meta.json, audio.wav] in test_acquire.py,
since web.py's _require_source hardcodes those same filenames
Directly assert STAGE_LINE does not match the [rebuild] line, rather than
inferring it from job.stage, which ok_runner's later [acquire] running
line would overwrite either way.