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
95 lines
2.3 KiB
Go
95 lines
2.3 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
|
)
|
|
|
|
func init() {
|
|
registerExtension("songLyrics", 1, 2)
|
|
register("getLyricsBySongId", handleGetLyricsBySongID)
|
|
}
|
|
|
|
// handleGetLyricsBySongID implements the songLyrics extension. It never
|
|
// fakes cue-level timing: cueLine data is only emitted for entries that
|
|
// were actually parsed from an Enhanced-LRC-style source with word/
|
|
// syllable cues, and only when the client asked for enhanced=true.
|
|
func handleGetLyricsBySongID(rc *Context) *apiError {
|
|
id, aerr := rc.Params.Required("id")
|
|
if aerr != nil {
|
|
return aerr
|
|
}
|
|
enhanced := rc.Params.Bool("enhanced", false)
|
|
|
|
db := rc.Server.DB
|
|
if _, err := database.GetSong(db, id); err != nil {
|
|
return errNotFound("Song")
|
|
}
|
|
|
|
rows, err := database.GetSongStructuredLyrics(db, id)
|
|
if err != nil {
|
|
return newError(ErrGeneric, err.Error())
|
|
}
|
|
|
|
list := &LyricsList{}
|
|
for _, row := range rows {
|
|
kind := "main" // every entry VibeSonic currently stores/ingests is a primary lyric layer, never a translation or pronunciation.
|
|
if !enhanced && kind != "main" {
|
|
continue
|
|
}
|
|
|
|
sl := StructuredLyrics{
|
|
Lang: row.Language,
|
|
Synced: row.Synced,
|
|
Line: make([]Line, 0, len(row.Lines)),
|
|
}
|
|
if enhanced {
|
|
sl.Kind = kind
|
|
}
|
|
|
|
hasCues := false
|
|
for _, l := range row.Lines {
|
|
line := Line{Value: l.Text}
|
|
if row.Synced && l.StartMs != nil {
|
|
start := *l.StartMs
|
|
line.Start = &start
|
|
}
|
|
sl.Line = append(sl.Line, line)
|
|
if len(l.Words) > 0 {
|
|
hasCues = true
|
|
}
|
|
}
|
|
|
|
if enhanced && row.Synced && hasCues {
|
|
sl.CueLine = make([]CueLine, 0, len(row.Lines))
|
|
for i, l := range row.Lines {
|
|
if len(l.Words) == 0 {
|
|
continue
|
|
}
|
|
cl := CueLine{Index: i, Value: l.Text}
|
|
if l.StartMs != nil {
|
|
cl.Start = *l.StartMs
|
|
}
|
|
cl.Cue = make([]Cue, 0, len(l.Words))
|
|
for _, w := range l.Words {
|
|
cue := Cue{Start: w.StartMs, ByteStart: w.ByteStart, ByteEnd: w.ByteEnd, Value: w.Text}
|
|
if w.EndMs != nil {
|
|
end := *w.EndMs
|
|
cue.End = &end
|
|
}
|
|
cl.Cue = append(cl.Cue, cue)
|
|
}
|
|
if last := l.Words[len(l.Words)-1]; last.EndMs != nil {
|
|
cl.End = *last.EndMs
|
|
}
|
|
sl.CueLine = append(sl.CueLine, cl)
|
|
}
|
|
}
|
|
|
|
list.StructuredLyrics = append(list.StructuredLyrics, sl)
|
|
}
|
|
|
|
rc.Response = newResponse()
|
|
rc.Response.LyricsList = list
|
|
return nil
|
|
}
|