Synced LRC highlights the playing line, keeps it centred and seeks on click. Plain text scrolls continuously with a nudge knob instead — a highlight on guessed timings turns guaranteed drift into what looks like a bug. Seuraa kappaletta turns following off without losing the highlight, and scrolling by hand turns it off too. Fixes the scroll landing in the wrong place (offsetTop measured from a different coordinate space than the box it was applied to) and the fader shifting the deck sideways at score 100 (auto-sized grid columns plus a readout spanning both).
113 lines
4.1 KiB
Go
113 lines
4.1 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)
|
|
}
|
|
}
|
|
|
|
// Plain text must parse to nil: that is the signal to scroll continuously rather than highlight
|
|
// lines on timings nobody measured.
|
|
func TestParseLRC(t *testing.T) {
|
|
if got := parseLRC("Ihan tavallista tekstiä\ntoinen rivi"); got != nil {
|
|
t.Fatalf("plain text parsed as synced: %v", got)
|
|
}
|
|
|
|
lines := parseLRC("[00:11.74] Ensimmäinen\n[01:02] Toinen\n[00:05.5] Aikaisempi\nrivi ilman aikaa")
|
|
if len(lines) != 3 {
|
|
t.Fatalf("got %d lines, want 3 — untimed lines are dropped", len(lines))
|
|
}
|
|
// Sorted by time, whatever order the file had.
|
|
if lines[0].At != 5.5 || lines[0].Text != "Aikaisempi" {
|
|
t.Fatalf("first line is %+v, want 5.5s Aikaisempi", lines[0])
|
|
}
|
|
if lines[1].At != 11.74 || lines[2].At != 62 {
|
|
t.Fatalf("timestamps parsed as %v and %v, want 11.74 and 62", lines[1].At, lines[2].At)
|
|
}
|
|
|
|
// A refrain can carry several timestamps on one line, and each is its own occurrence.
|
|
rep := parseLRC("[00:10.00][01:10.00] Kertosäe")
|
|
if len(rep) != 2 || rep[0].At != 10 || rep[1].At != 70 {
|
|
t.Fatalf("repeated stamps gave %+v, want two occurrences", rep)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|