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:
Esa Kataja
2026-08-01 00:21:31 +03:00
parent ac2cfaebac
commit 4f337b6202
13 changed files with 593 additions and 38 deletions
+23
View File
@@ -31,6 +31,28 @@
{{define "saved"}}<span id="saved" class="saved">{{if .}}Tallennettu {{.}}{{end}}</span>{{end}}
<!-- Included by the page and returned alone by the suggestion button, so the markup exists once.
hx-include sends the current title and artist, which is the whole point: the lookup uses what
the submitter just fixed, not what the tags claimed. -->
{{define "lyricsfield"}}
<div id="lyricsfield">
<label>Sanoitukset <span class="muted small">(vapaaehtoinen)</span>
<textarea name="lyrics" rows="8" maxlength="20000"
placeholder="Liitä sanoitukset tähän tai hae ne alta.">{{.Lyrics}}</textarea>
</label>
<div class="lyricsbar">
<button type="button" class="ghost"
hx-post="/submit/{{.ID}}/lyrics"
hx-include="[name='title'], [name='artist']"
hx-target="#lyricsfield" hx-swap="outerHTML">Hae sanoitukset</button>
{{if .Found}}<span class="muted small">Löytyi — tarkista ja muokkaa tarvittaessa.</span>{{end}}
{{if .Searched}}{{if not .Found}}
<span class="muted small">Ei löytynyt. Tarkista nimi ja esittäjä tai liitä sanoitukset itse.</span>
{{end}}{{end}}
</div>
</div>
{{end}}
{{define "content"}}
<h1>Lähetys</h1>
@@ -58,6 +80,7 @@
<label>Esittely <span class="muted small">(vapaaehtoinen)</span>
<textarea name="description" rows="5" maxlength="2000">{{.Data.Description}}</textarea>
</label>
{{template "lyricsfield" dict "ID" .Data.ID "Lyrics" .Data.Lyrics}}
{{template "saved" ""}}
</form>