Implement songLyrics extension: getLyricsBySongId with real word/syllable cues

Adds getLyricsBySongId (versions 1 and 2 of the songLyrics extension,
gated by the enhanced param): returns every stored structuredLyrics
entry for a song, including line-level start times for synced lyrics
and, only when a source actually provided word/syllable timing (an
Enhanced LRC sidecar) and enhanced=true was requested, cueLine/cue
data with the precomputed UTF-8 byte offsets. Plain and non-enhanced
synced lyrics never get fabricated cue data.

Removes the now-dead SetSongLyrics writer (superseded by
database.InsertLyrics); SongLyricsPlainText keeps serving the legacy
getLyrics endpoint unchanged.

Adds a scanner integration test proving a sidecar .lrc (with an
Enhanced LRC line) is discovered and indexed during a real scan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
This commit is contained in:
2026-08-23 08:19:41 -05:00
co-authored by Claude Sonnet 5
parent e48f063f03
commit ec28053d36
5 changed files with 363 additions and 53 deletions
-52
View File
@@ -143,58 +143,6 @@ func AlbumsByRecentlyPlayed(db *sql.DB, userID string, limit, offset int) ([]*Al
return scanAlbums(rows)
}
// SetSongLyrics replaces a song's stored (unsynced) lyrics with
// plainText, split into lines. Called by the scanner when a file has
// embedded lyrics.
func SetSongLyrics(db *sql.DB, songID, language, plainText string) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`DELETE FROM lyrics WHERE song_id = ?`, songID); err != nil {
return err
}
if plainText == "" {
return tx.Commit()
}
lyricsID := idgen.New("ly")
if _, err := tx.Exec(`INSERT INTO lyrics (id, song_id, language, source, synced) VALUES (?, ?, ?, 'embedded', 0)`,
lyricsID, songID, language); err != nil {
return err
}
for i, line := range splitLines(plainText) {
if _, err := tx.Exec(`INSERT INTO lyrics_lines (id, lyrics_id, line_index, text) VALUES (?, ?, ?, ?)`,
idgen.New("ll"), lyricsID, i, line); err != nil {
return err
}
}
return tx.Commit()
}
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
line := s[start:i]
line = trimCR(line)
lines = append(lines, line)
start = i + 1
}
}
lines = append(lines, trimCR(s[start:]))
return lines
}
func trimCR(s string) string {
if len(s) > 0 && s[len(s)-1] == '\r' {
return s[:len(s)-1]
}
return s
}
// SongLyricsPlainText returns a song's stored lyrics as plain text
// (lines joined with "\n"), and whether any were found.
func SongLyricsPlainText(db *sql.DB, songID string) (string, bool, error) {