Files
Levyraati26_go/templates/song.html
T
Esa Kataja 4f337b6202 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.
2026-08-01 00:21:31 +03:00

183 lines
7.1 KiB
HTML

{{define "content"}}
{{$s := .Data}}
<h1>{{$s.Title}}</h1>
<p class="by">{{$s.Artist}}</p>
<!-- Four different kinds of fact, so four labelled cells rather than one run of text. "Oma
kappale" belongs here as the submitter, not as a badge: it is a fact about you. -->
<dl class="spec">
<div><dt>Genre</dt><dd>{{$s.GenreLabel}}</dd></div>
<div><dt>Kesto</dt><dd>{{$s.Length}}</dd></div>
<div><dt>Lähetti</dt>
<dd>{{if $s.Own}}sinä{{else}}<a href="/profile/{{$s.SubmitterID}}">{{$s.Submitter}}</a>{{end}}</dd>
</div>
<div><dt>Julkaistu</dt><dd>{{fiday $s.CreatedAt}}</dd></div>
</dl>
{{if $s.CanEdit}}
<details class="editbox">
<summary>Muokkaa tietoja</summary>
<form method="post" action="/songs/{{$s.ID}}" class="stack">
<label>Nimi <input name="title" value="{{$s.Title}}" maxlength="100" required></label>
<label>Esittäjä <input name="artist" value="{{$s.Artist}}" maxlength="100" required></label>
<label>Genre
<select name="genre" required>
{{$current := $s.Genre}}
{{range $s.Genres}}
<option value="{{.Code}}" {{if eq .Code $current}}selected{{end}}>{{.Label}}</option>
{{end}}
</select>
</label>
<label>Esittely <textarea name="description" rows="4" maxlength="2000">{{$s.Description}}</textarea></label>
<button type="submit">Tallenna</button>
</form>
<form method="post" action="/songs/{{$s.ID}}/delete"
onsubmit="return confirm('Poistetaanko kappale lopullisesti?')">
<button type="submit" class="ghost danger">Poista kappale</button>
</form>
<p class="muted small">Muokkaus ja poisto ovat mahdollisia vain ennen ensimmäistä arvostelua.</p>
</details>
{{else if $s.Own}}
<p class="muted small">Kappaletta on jo arvosteltu, joten tietoja ei voi enää muuttaa.</p>
{{end}}
{{if $s.CanReview}}
<!-- The channel strip: the fader is the score, the panel beside it is everything else you do
while the track plays. -->
<form method="post" action="/songs/{{$s.ID}}/review" class="strip">
{{template "fader" 50}}
<div class="deck">
{{template "player" $s}}
{{with $s.Description}}<p class="intro">{{.}}</p>{{end}}
<!-- 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>
{{else}}
{{template "player" $s}}
{{with $s.Description}}<p class="intro">{{.}}</p>{{end}}
{{end}}
{{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>
{{with $s.ViewerReview}}
{{if .CanEdit}}
<form method="post" action="/reviews/{{.ID}}" class="strip">
<input type="hidden" name="from" value="/songs/{{$s.ID}}">
{{template "fader" .Score}}
<div class="deck">
<label class="grow">Arvostelu
<textarea name="text" rows="6" maxlength="5000" required>{{.Text}}</textarea>
</label>
<div class="deckfoot">
<button type="submit">Päivitä</button>
<span class="muted small">Muokkausaika päättyy {{fidate .EditableUntil}}.</span>
</div>
</div>
</form>
<form method="post" action="/reviews/{{.ID}}/delete"
onsubmit="return confirm('Poistetaanko arvostelu?')">
<input type="hidden" name="from" value="/songs/{{$s.ID}}">
<button type="submit" class="ghost danger">Poista arvostelu</button>
</form>
{{else}}
<p class="score">{{.Score}}</p>
<p class="reviewtext">{{.Text}}</p>
<p class="muted small">Muokkausaika on päättynyt.</p>
{{end}}
{{end}}
</section>
{{end}}
<section>
<h2>Arvostelut{{if $s.ReviewCount}} ({{$s.ReviewCount}}){{end}}</h2>
{{if $s.Revealed}}
{{if $s.Reviews}}
<!-- One channel per reviewer: the spread across the row is what "divisive" looks like. -->
<div class="channels">
{{range $i, $r := $s.Reviews}}
<div class="chan{{if $r.Own}} own{{end}}" style="--v: {{$r.Score}}; --i: {{$i}}">
<span class="chan-track"><span class="chan-cap"></span></span>
<span class="chan-score">{{$r.Score}}</span>
<span class="chan-who" title="{{$r.Reviewer}}">{{$r.Initials}}</span>
</div>
{{end}}
{{if $s.Average}}
<div class="chan avg" style="--v: {{score $s.Average}}">
<span class="chan-track"><span class="chan-cap"></span></span>
<span class="chan-score">{{score $s.Average}}</span>
<span class="chan-who">ka.</span>
</div>
{{end}}
</div>
{{range $s.Reviews}}
<article class="review{{if .Own}} own{{end}}">
<header>
<span class="avatar small">{{.Initials}}</span>
<span class="who">{{.Reviewer}}</span>
<span class="score">{{.Score}}</span>
<span class="meta">{{fidate .CreatedAt}}</span>
</header>
<p>{{.Text}}</p>
</article>
{{end}}
{{else}}
<p class="muted">Kukaan ei ole vielä arvostellut tätä kappaletta.</p>
{{end}}
{{else}}
<p class="sealed-note">
<span class="sealed" aria-hidden="true"></span>
Muiden pisteet ja arvostelut paljastuvat kun kirjoitat omasi.
{{if $s.ReviewCount}}Arvosteluja on {{$s.ReviewCount}}.{{end}}
</p>
{{end}}
</section>
{{if $s.NextInQueue}}
<p class="nextup"><a href="/songs/{{$s.NextInQueue}}">Seuraava jonossa →</a></p>
{{end}}
{{end}}