Add lyrics: paste, fetch, and read them while reviewing
Lyrics are suggested at submission and never imposed. The conversion worker makes one LRCLIB lookup with whatever metadata exists, and the waiting page has a Hae sanoitukset button that re-queries with whatever title and artist are currently typed — which is the case that matters, since our metadata comes from ID3 tags and YouTube uploaders. Neither path overwrites typed text. - lyrics text on both submissions and songs, copied across at publish. Nothing has launched, so the column goes into 001_init.sql rather than a migration - The lock does not cover lyrics: it freezes what the song claims to be, and nobody reviewed the lyrics. So the submitter can still fix them afterwards, or paste them for an old song a year later - The review strip gained a second pane: lyrics on the left, review on the right, so following the words costs no scrolling. No lyrics means no pane, not an empty one. Below 1024px the panes stack - LRC timestamps are stored but stripped for reading — they belong to the player, not the reader - The lyrics box is capped and scrolls inside itself, so a long song cannot stretch the strip past the screen Fixes a real bug found on the way: saveMetadata cleared any field the request did not carry, so publishing wiped the lyrics the worker had just fetched. Fields absent from a request now keep their stored value. The client identifies itself to LRCLIB as "levyraati" and nothing more. Tests cover cleanLyrics keeping line breaks, and fetchLyrics against a local server: synced beats plain, instrumentals and wrong-length takes are skipped, and a miss is empty with no error.
This commit is contained in:
@@ -191,3 +191,21 @@ says so.
|
||||
starting at 1, for the second attempt at a release. The string lives in a git tag and reaches
|
||||
the binary through `-ldflags`, so no file in the repo has to be bumped and a local build
|
||||
honestly reports `dev`. It surfaces in the footer, the startup log and `/healthz`.
|
||||
45. **Lyrics are suggested at submission, and stay editable forever.** Four decisions in one, taken
|
||||
2026-07-31 while scoping the feature in [later.md](./later.md):
|
||||
- **Suggested, never imposed.** The worker attempts one LRCLIB lookup after conversion, and the
|
||||
waiting page carries a *Hae sanoitukset* button that re-queries with whatever title and artist
|
||||
are currently typed. The button exists because our metadata comes from ID3 tags and YouTube
|
||||
uploaders, so the automatic attempt misses exactly the songs with messy names — and would look
|
||||
broken rather than absent. Neither path overwrites text the submitter has typed.
|
||||
- **Lyrics live on the submission, not just the song**, and are copied across at publish, because
|
||||
they are part of preparing a song rather than something bolted on afterwards.
|
||||
- **No migration 002.** Nothing has launched, so the column goes into `001_init.sql` and the
|
||||
database is recreated. The schema has no legacy to respect until there is data worth keeping.
|
||||
- **The lock does not cover lyrics.** It exists so the thing people reviewed stops changing under
|
||||
them, and nobody reviewed the lyrics — the rule is now *the lock freezes what the song claims
|
||||
to be; lyrics are an attachment to it.* This also allows pasting lyrics for an old song, which
|
||||
is when the feature is worth most.
|
||||
|
||||
The coverage assumption that shaped the earlier sketch was wrong and is corrected in `later.md`:
|
||||
LRCLIB has synced lyrics for a good share of Finnish rock, not almost none.
|
||||
|
||||
+48
-16
@@ -21,28 +21,60 @@ Two things to remember when it happens:
|
||||
|
||||
## Lyrics with scaled autoscroll
|
||||
|
||||
Fetch lyrics and scroll them in time with the audio.
|
||||
Fetch lyrics and scroll them in time with the audio. Designed and decided (decisions 45), not built.
|
||||
|
||||
- **LRCLIB** (`lrclib.net`) is a community database with no API key, and its responses include
|
||||
`syncedLyrics` — real LRC with `[mm:ss.xx]` per-line timestamps — alongside `plainLyrics`. Query by
|
||||
track, artist and duration, all of which are already on the song row. So a decent share of songs
|
||||
need no faked timing at all.
|
||||
- **Synced hit** → highlight the current line properly. **Plain hit or manual paste** → distribute
|
||||
lines evenly across `duration_seconds` and scroll the block *continuously without highlighting a
|
||||
line*. Highlighting makes every second of drift read as a bug, and drift is guaranteed — intros and
|
||||
outros alone break a uniform mapping.
|
||||
**Coverage, measured 2026-07-31** rather than assumed. An earlier version of this page guessed
|
||||
LRCLIB would miss nearly all Finnish music. It does not:
|
||||
|
||||
| Search | Results | With `syncedLyrics` |
|
||||
|---|---|---|
|
||||
| Nightwish | 20 | 20 |
|
||||
| Eppu Normaali | 20 | 13 |
|
||||
| CMX | 15 | 12 |
|
||||
| Popeda | 20 | 8 |
|
||||
|
||||
**LRCLIB** (`lrclib.net`) needs no API key. `/api/get` matches on artist, track and duration within
|
||||
±2 s and returns `syncedLyrics` — real LRC with `[mm:ss.xx]` per line — alongside `plainLyrics`;
|
||||
`/api/search?q=` is the looser fallback. Go's side is `net/http` and `encoding/json`, so the
|
||||
dependency budget survives, and it is treated exactly like ffmpeg and yt-dlp: a timeout, allowed to
|
||||
fail, never blocking anything.
|
||||
|
||||
**Where it happens: at submission, as a suggestion.**
|
||||
|
||||
- The worker attempts one automatic lookup after conversion, using whatever metadata exists.
|
||||
- The waiting page has a **Hae sanoitukset** button that re-queries with whatever is currently typed
|
||||
in the title and artist fields. That is the answer for messy tags — `Sentenced Noose` from a
|
||||
YouTube upload will not match until the submitter fixes it, and the automatic attempt would
|
||||
otherwise just look broken.
|
||||
- Neither ever overwrites text the submitter has typed. They can accept the suggestion, edit it, or
|
||||
leave the field empty.
|
||||
|
||||
**Storage:** one nullable `lyrics text` column on both `submissions` and `songs`, copied across at
|
||||
publish. LRC or plain is told apart by whether the first line starts with `[`, so no second column
|
||||
and no flag. **Nothing has launched, so this goes into `001_init.sql` rather than a migration 002.**
|
||||
|
||||
**Lyrics stay editable after the song locks** — the lock exists so the thing people reviewed stops
|
||||
changing, and nobody reviewed the lyrics. It also means someone can paste them for an old song a
|
||||
year later, which is when this feature is most useful.
|
||||
|
||||
**Playback:**
|
||||
|
||||
- **Synced hit** → highlight the current line properly, driven by the transport's `timeupdate`.
|
||||
- **Plain hit or manual paste** → distribute lines evenly across `duration_seconds` and scroll the
|
||||
block *continuously without highlighting a line*. Highlighting makes every second of drift read as
|
||||
a bug, and drift is guaranteed — intros and outros alone break a uniform mapping.
|
||||
- The Web Animations API does the whole thing including seeking: build the scroll animation with
|
||||
`duration_seconds`, `pause()` it, and bind `play`/`pause`/`seeked` on the audio element. No timers,
|
||||
no drift accumulation.
|
||||
- **Leave a nudge knob** — a ±10 s offset slider, remembered per song in `localStorage`. Uniform
|
||||
distribution models a song no real song obeys, and one drag while listening beats any heuristic.
|
||||
- Storage: one nullable `lyrics text` column. LRC or plain — tell them apart by whether the first
|
||||
line starts with `[`, so no second column and no flag. Fetched best-effort in the publish worker.
|
||||
- **Add a paste box to the submitter's edit form.** The genre list contains *Finnish*,
|
||||
*Experimental* and *Just Plain Weird*; LRCLIB will miss nearly all of it, and for those songs the
|
||||
textarea is the entire feature.
|
||||
- Copyright posture is the same as the YouTube note: private app, ten people, written down
|
||||
deliberately.
|
||||
|
||||
**Still open:** where the panel lives on the song page. That page's job is now *listen and write*,
|
||||
and a scrolling lyrics panel competes with the review textarea for both space and attention — a
|
||||
collapsed panel under the player is the starting guess, not a decision.
|
||||
|
||||
Copyright posture is the same as the YouTube note: private app, ten people, written down
|
||||
deliberately.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user