Files
vibesonic/internal/web/lyrics_handler.go
T
owenandClaude Sonnet 5 933ed31550 Add a web player lyrics panel with active-line highlighting and click-to-seek
New GET /player/lyrics/{id} (auth-required) returns the best available
stored structuredLyrics entry for a song — preferring a synced one —
as plain JSON lines with a start time only where the source actually
has one.

The player bar gains a lyrics toggle; the panel fetches lyrics on
every song change, renders each line, and — only for lines that
actually carry a startMs — makes them clickable (seeks + plays) and
highlights the currently active line as playback progresses. Plain
(unsynced) lyrics render as static text with no fabricated timing or
highlighting.

Note: verified via Go template parsing/rendering and the new handler
tests, plus a JS syntax check; not manually exercised in a browser in
this session.

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

51 lines
1.4 KiB
Go

package web
import (
"encoding/json"
"net/http"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
)
type lyricsLineJSON struct {
Text string `json:"text"`
StartMs *int `json:"startMs,omitempty"`
}
type lyricsResponseJSON struct {
Synced bool `json:"synced"`
Lines []lyricsLineJSON `json:"lines"`
}
// handleLyrics serves the web player's lyrics panel: the best available
// stored structuredLyrics entry for a song, as line text plus a start
// time when the source actually has one. Prefers a synced entry over
// an unsynced one so the panel can highlight/click-to-seek whenever
// that data exists, but never fabricates timing for an unsynced entry.
func (h *Web) handleLyrics(w http.ResponseWriter, r *http.Request) {
songID := r.PathValue("id")
entries, err := database.GetSongStructuredLyrics(h.DB, songID)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
var best *database.StructuredLyricsRow
for i := range entries {
if best == nil || (entries[i].Synced && !best.Synced) {
best = &entries[i]
}
}
out := lyricsResponseJSON{Lines: []lyricsLineJSON{}}
if best != nil {
out.Synced = best.Synced
for _, l := range best.Lines {
out.Lines = append(out.Lines, lyricsLineJSON{Text: l.Text, StartMs: l.StartMs})
}
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
}