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
+39 -8
View File
@@ -49,14 +49,26 @@
<div class="deck">
{{template "player" $s}}
{{with $s.Description}}<p class="intro">{{.}}</p>{{end}}
<label class="grow">Arvostelu
<textarea name="text" rows="8" maxlength="5000" required
placeholder="Mitä kuulit?"></textarea>
</label>
<div class="deckfoot">
<button type="submit">Tallenna arvostelu</button>
<span class="muted small">Muiden pisteet paljastuvat kun tallennat omasi. Voit muokata
tai poistaa arvostelusi 30 minuutin ajan.</span>
<!-- Two panes when the song has lyrics: read on the left, write on the right, so following
the words costs no scrolling. Without lyrics the pane is absent, not empty. -->
<div class="panes{{if not $s.Lyrics}} solo{{end}}">
{{with $s.Lyrics}}
<div class="lyricspane">
<span class="cap">Sanoitukset</span>
<div class="lyricsbox">{{lyricstext .}}</div>
</div>
{{end}}
<div class="writepane">
<label class="grow">Arvostelu
<textarea name="text" maxlength="5000" required placeholder="Mitä kuulit?"></textarea>
</label>
<div class="deckfoot">
<button type="submit">Tallenna arvostelu</button>
<span class="muted small">Muiden pisteet paljastuvat kun tallennat omasi. Voit muokata
tai poistaa arvostelusi 30 minuutin ajan.</span>
</div>
</div>
</div>
</div>
</form>
@@ -67,6 +79,25 @@
{{with $s.SourceURL}}<p class="muted small"><a href="{{.}}" rel="noreferrer">Kuuntele YouTubessa</a></p>{{end}}
{{if and (not $s.CanReview) (or $s.Lyrics $s.Own)}}
<!-- Only when the review strip is not already showing them: while reviewing, the lyrics live in
the left pane. This panel is for reading afterwards and for the submitter's edits. -->
<details class="lyrics">
<summary>Sanoitukset{{if not $s.Lyrics}} <span class="muted small">— ei vielä lisätty</span>{{end}}</summary>
{{if $s.Lyrics}}<pre class="lyricstext">{{lyricstext $s.Lyrics}}</pre>{{end}}
{{if $s.Own}}
<form method="post" action="/songs/{{$s.ID}}/lyrics" class="stack">
<label>Muokkaa sanoituksia
<textarea name="lyrics" rows="10" maxlength="20000"
placeholder="Liitä sanoitukset tähän.">{{$s.Lyrics}}</textarea>
</label>
<button type="submit">Tallenna sanoitukset</button>
</form>
<p class="muted small">Sanoituksia voi muokata vielä arvostelujenkin jälkeen.</p>
{{end}}
</details>
{{end}}
{{if $s.ViewerReview}}
<section>
<h2>Oma arvostelusi</h2>