Files
vibesonic/internal/database/lyrics_test.go
T
owenandClaude Sonnet 5 e48f063f03 Add structured lyrics storage and indexing (line/word timing, byte offsets)
Adds database.InsertLyrics/GetSongStructuredLyrics storing multiple
lyrics entries per song with per-line start times and, when the
source provides it, word-level cues carrying precomputed UTF-8 byte
offsets. library.IndexLyrics wires this into the scanner: embedded
plain-text tags are stored unsynced, and a same-basename sidecar .lrc
(plain or Enhanced) is parsed and stored synced, with word cues when
present. IndexFile now takes the media file's absolute path so lyrics
indexing can locate sidecar files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
2026-08-23 08:16:42 -05:00

139 lines
3.8 KiB
Go

package database
import (
"database/sql"
"path/filepath"
"testing"
)
func openLyricsTestDB(t *testing.T) (*sql.DB, string) {
t.Helper()
db, err := Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { db.Close() })
folder, err := CreateMusicFolder(db, "Music", "/music", "music")
if err != nil {
t.Fatal(err)
}
mf, err := CreateMediaFile(db, folder.ID, "song.mp3", 100, 0, "h", "mp3", "song", 10, 128, 44100, 2)
if err != nil {
t.Fatal(err)
}
song, err := UpsertSong(db, UpsertSongInput{MediaFileID: mf.ID, Title: "Song"})
if err != nil {
t.Fatal(err)
}
return db, song.ID
}
func TestInsertAndGetPlainLyrics(t *testing.T) {
db, songID := openLyricsTestDB(t)
lines := []LyricsLineInput{{Text: "Line one"}, {Text: "Line two"}}
if _, err := InsertLyrics(db, songID, "embedded", "xxx", false, lines); err != nil {
t.Fatalf("InsertLyrics: %v", err)
}
entries, err := GetSongStructuredLyrics(db, songID)
if err != nil {
t.Fatalf("GetSongStructuredLyrics: %v", err)
}
if len(entries) != 1 {
t.Fatalf("entries = %v, want 1", entries)
}
e := entries[0]
if e.Synced || e.Source != "embedded" || e.Language != "xxx" {
t.Errorf("entry = %+v", e)
}
if len(e.Lines) != 2 || e.Lines[0].Text != "Line one" {
t.Fatalf("lines = %+v", e.Lines)
}
if e.Lines[0].StartMs != nil {
t.Error("plain lyrics line should have nil StartMs")
}
}
func TestInsertAndGetSyncedLyricsWithWordCues(t *testing.T) {
db, songID := openLyricsTestDB(t)
start0 := 1000
end0 := 1500
lines := []LyricsLineInput{
{
StartMs: &start0,
Text: "Hello world",
Words: []LyricsWordInput{
{StartMs: 1000, EndMs: &end0, ByteStart: 0, ByteEnd: 5, Text: "Hello"},
{StartMs: 1500, ByteStart: 6, ByteEnd: 11, Text: "world"},
},
},
}
if _, err := InsertLyrics(db, songID, "external", "en", true, lines); err != nil {
t.Fatalf("InsertLyrics: %v", err)
}
entries, err := GetSongStructuredLyrics(db, songID)
if err != nil {
t.Fatalf("GetSongStructuredLyrics: %v", err)
}
if len(entries) != 1 || !entries[0].Synced {
t.Fatalf("entries = %+v", entries)
}
line := entries[0].Lines[0]
if line.StartMs == nil || *line.StartMs != 1000 {
t.Fatalf("line.StartMs = %v, want 1000", line.StartMs)
}
if len(line.Words) != 2 {
t.Fatalf("words = %+v, want 2", line.Words)
}
if line.Words[0].EndMs == nil || *line.Words[0].EndMs != 1500 {
t.Errorf("Words[0].EndMs = %v, want 1500", line.Words[0].EndMs)
}
if line.Words[1].EndMs != nil {
t.Errorf("Words[1].EndMs = %v, want nil (not provided)", line.Words[1].EndMs)
}
if line.Words[0].ByteStart != 0 || line.Words[0].ByteEnd != 5 {
t.Errorf("Words[0] byte offsets = %d,%d, want 0,5", line.Words[0].ByteStart, line.Words[0].ByteEnd)
}
}
func TestMultipleLyricsEntriesPerSong(t *testing.T) {
db, songID := openLyricsTestDB(t)
if _, err := InsertLyrics(db, songID, "embedded", "xxx", false, []LyricsLineInput{{Text: "Plain"}}); err != nil {
t.Fatal(err)
}
start := 0
if _, err := InsertLyrics(db, songID, "external", "en", true, []LyricsLineInput{{StartMs: &start, Text: "Synced"}}); err != nil {
t.Fatal(err)
}
entries, err := GetSongStructuredLyrics(db, songID)
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 {
t.Fatalf("entries = %v, want 2 (multiple lyric sets preserved)", entries)
}
}
func TestClearSongLyricsCascades(t *testing.T) {
db, songID := openLyricsTestDB(t)
if _, err := InsertLyrics(db, songID, "embedded", "xxx", false, []LyricsLineInput{{Text: "Plain"}}); err != nil {
t.Fatal(err)
}
if err := ClearSongLyrics(db, songID); err != nil {
t.Fatalf("ClearSongLyrics: %v", err)
}
entries, err := GetSongStructuredLyrics(db, songID)
if err != nil {
t.Fatal(err)
}
if len(entries) != 0 {
t.Fatalf("entries = %v, want 0 after clear", entries)
}
}