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.
87 lines
3.0 KiB
Go
87 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Line breaks are the content here — LRC timestamps are per line — so cleanLyrics must not do what
|
|
// clean() does to a title.
|
|
func TestCleanLyrics(t *testing.T) {
|
|
got := cleanLyrics(" [00:11.74] Rivi yksi\r\n[00:13.99] Rivi\x07 kaksi\r\n\n")
|
|
want := "[00:11.74] Rivi yksi\n[00:13.99] Rivi kaksi"
|
|
if got != want {
|
|
t.Fatalf("cleanLyrics gave %q, want %q", got, want)
|
|
}
|
|
if n := len([]rune(cleanLyrics(strings.Repeat("a", maxLyrics+500)))); n != maxLyrics {
|
|
t.Fatalf("truncated to %d runes, want %d", n, maxLyrics)
|
|
}
|
|
}
|
|
|
|
// The lookup is a suggestion, so "nothing found" is a normal answer rather than an error, and a
|
|
// synced hit always beats a plain one.
|
|
func TestFetchLyrics(t *testing.T) {
|
|
var lastPath string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
lastPath = r.URL.Path
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case r.URL.Path == "/get" && r.URL.Query().Get("track_name") == "Paranoid":
|
|
w.Write([]byte(`{"trackName":"Paranoid","artistName":"Black Sabbath","duration":168,
|
|
"plainLyrics":"plain version","syncedLyrics":"[00:11.74] synced version"}`))
|
|
case r.URL.Path == "/get":
|
|
http.Error(w, `{"code":404}`, http.StatusNotFound)
|
|
case r.URL.Path == "/search" && strings.Contains(r.URL.Query().Get("q"), "Soittorasia"):
|
|
// An instrumental and a wrong-length take come first: both must be skipped.
|
|
w.Write([]byte(`[{"trackName":"Soittorasia","duration":200,"instrumental":true,
|
|
"plainLyrics":"","syncedLyrics":"[00:01.00] should be skipped"},
|
|
{"trackName":"Soittorasia","duration":600,
|
|
"plainLyrics":"wrong length take"},
|
|
{"trackName":"Soittorasia","duration":201,
|
|
"plainLyrics":"right one"}]`))
|
|
default:
|
|
w.Write([]byte(`[]`))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
old := lrclibBase
|
|
lrclibBase = srv.URL
|
|
defer func() { lrclibBase = old }()
|
|
|
|
ctx := context.Background()
|
|
|
|
got, err := fetchLyrics(ctx, "Paranoid", "Black Sabbath", 168)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "[00:11.74] synced version" {
|
|
t.Fatalf("exact match returned %q, want the synced version", got)
|
|
}
|
|
|
|
got, err = fetchLyrics(ctx, "Soittorasia", "Joku", 200)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "right one" {
|
|
t.Fatalf("search fallback returned %q — instrumental and wrong-length takes must be skipped", got)
|
|
}
|
|
if lastPath != "/search" {
|
|
t.Fatalf("last request was %s, want the search fallback", lastPath)
|
|
}
|
|
|
|
// Nothing found is not an error: the submitter simply types their own.
|
|
got, err = fetchLyrics(ctx, "Ei olemassa", "Kukaan", 100)
|
|
if err != nil || got != "" {
|
|
t.Fatalf("miss returned %q, %v — want empty and no error", got, err)
|
|
}
|
|
|
|
// No title means nothing to match on, and no request at all.
|
|
if got, err := fetchLyrics(ctx, "", "Artisti", 100); err != nil || got != "" {
|
|
t.Fatalf("empty title returned %q, %v", got, err)
|
|
}
|
|
}
|