Skip to content

Commit

Permalink
adds: support for synchronized lyrics (as provided by gonic & Navidrome)
Browse files Browse the repository at this point in the history
adds: ability to also write logs to a log file
  • Loading branch information
xxxserxxx committed Dec 18, 2024
1 parent db1c4ab commit 30a5898
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 14 deletions.
31 changes: 31 additions & 0 deletions event_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,38 @@ func (ui *Ui) guiEventLoop() {

ui.app.QueueUpdateDraw(func() {
ui.playerStatus.SetText(formatPlayerStatus(statusData.Volume, statusData.Position, statusData.Duration))
cl := ui.queuePage.currentLyrics.Lines
lcl := len(cl)
if lcl == 0 {
ui.queuePage.lyrics.SetText("\n[::i]No lyrics[-:-:-]")
} else {
p := statusData.Position * 1000
_, _, _, fh := ui.queuePage.lyrics.GetInnerRect()
ui.logger.Printf("field height is %d", fh)
for i := 0; i < lcl-1; i++ {
if p >= cl[i].Start && p < cl[i+1].Start {
txt := ""
if i > 1 {
txt = cl[i-2].Value + "\n"
}
if i > 0 {
txt += "[::b]" + cl[i-1].Value + "[-:-:-]\n"
}
for k := i; k < lcl && k-i < fh; k++ {
txt += cl[k].Value + "\n"
}
ui.queuePage.lyrics.SetText(txt)
break
}
}
}
})

case mpvplayer.EventStopped:
ui.logger.Print("mpvEvent: stopped")
ui.app.QueueUpdateDraw(func() {
ui.startStopStatus.SetText("[red::b]Stopped[::-]")
ui.queuePage.lyrics.SetText("")
ui.queuePage.UpdateQueue()
})

Expand Down Expand Up @@ -115,6 +141,11 @@ func (ui *Ui) guiEventLoop() {
ui.app.QueueUpdateDraw(func() {
ui.startStopStatus.SetText(statusText)
ui.queuePage.UpdateQueue()
if len(ui.queuePage.currentLyrics.Lines) == 0 {
ui.queuePage.lyrics.SetText("\n[::i]No lyrics[-:-:-]")
} else {
ui.queuePage.lyrics.SetText("")
}
})

case mpvplayer.EventPaused:
Expand Down
35 changes: 32 additions & 3 deletions logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,53 @@

package logger

import "fmt"
import (
"fmt"
"io"
"os"
)

type Logger struct {
Prints chan string
fout io.WriteCloser
}

func Init() *Logger {
return &Logger{make(chan string, 100)}
func Init(file string) *Logger {
l := Logger{
Prints: make(chan string, 100),
}
if file != "" {
var err error
l.fout, err = os.Create(file)
if err != nil {
fmt.Printf("error opening requested log file %q\n", file)
}
}
return &l
}

func (l *Logger) Print(s string) {
if l.fout != nil {
fmt.Fprintf(l.fout, "%s\n", s)
}
l.Prints <- s
}

func (l *Logger) Printf(s string, as ...interface{}) {
if l.fout != nil {
fmt.Fprintf(l.fout, s, as...)
fmt.Fprintf(l.fout, "\n")
}
l.Prints <- fmt.Sprintf(s, as...)
}

func (l *Logger) PrintError(source string, err error) {
l.Printf("Error(%s) -> %s", source, err.Error())
}

func (l *Logger) Close() error {
if l.fout != nil {
return l.fout.Close()
}
return nil
}
6 changes: 6 additions & 0 deletions page_playlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ func (p *PlaylistPage) UpdatePlaylists() {
}
p.updatingMutex.Lock()
defer p.updatingMutex.Unlock()
if response == nil {
p.logger.Printf("error: GetPlaylists response is nil")
p.isUpdating = false
stop <- true
return
}
p.ui.playlists = response.Playlists.Playlists
p.ui.app.QueueUpdateDraw(func() {
p.playlistList.Clear()
Expand Down
23 changes: 13 additions & 10 deletions page_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type QueuePage struct {
lyrics *tview.TextView
coverArt *tview.Image

changeLyrics chan string
currentLyrics subsonic.StructuredLyrics

// external refs
ui *Ui
Expand Down Expand Up @@ -157,6 +157,9 @@ func (ui *Ui) createQueuePage() *QueuePage {
queuePage.lyrics.SetTitle(" lyrics ")
queuePage.lyrics.SetTitleAlign(tview.AlignCenter)
queuePage.lyrics.SetDynamicColors(true).SetScrollable(true)
queuePage.lyrics.SetWrap(true)
queuePage.lyrics.SetWordWrap(true)
queuePage.lyrics.SetBorderPadding(1, 1, 1, 1)

queuePage.queueList.SetSelectionChangedFunc(queuePage.changeSelection)

Expand All @@ -180,15 +183,6 @@ func (ui *Ui) createQueuePage() *QueuePage {
starIdList: ui.starIdList,
}

go func() {
for {
select {
case songId := <-queuePage.changeLyrics:
// queuePage.connection.GetLyrics(songId)
}
}
}()

return &queuePage
}

Expand All @@ -212,6 +206,15 @@ func (q *QueuePage) changeSelection(row, column int) {
}
}
q.coverArt.SetImage(art)
lyrics, err := q.ui.connection.GetLyricsBySongId(currentSong.Id)
if err != nil {
q.logger.Printf("error fetching lyrics for %s: %v", currentSong.Title, err)
} else if len(lyrics) > 0 {
q.logger.Printf("got lyrics for %s", currentSong.Title)
q.currentLyrics = lyrics[0]
} else {
q.currentLyrics = subsonic.StructuredLyrics{Lines: []subsonic.LyricsLine{}}
}
_ = q.songInfoTemplate.Execute(q.songInfo, currentSong)
}

Expand Down
4 changes: 3 additions & 1 deletion stmps.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func main() {
memprofile := flag.String("memprofile", "", "write memory profile to `file`")
configFile := flag.String("config", "", "use config `file`")
version := flag.Bool("version", false, "print the stmps version and exit")
logFile := flag.String("log", "", "also write logs to this file")

flag.Parse()
if *help {
Expand Down Expand Up @@ -157,7 +158,8 @@ func main() {
osExit(2)
}

logger := logger.Init()
logger := logger.Init(*logFile)
defer logger.Close()
initCommandHandler(logger)

// init mpv engine
Expand Down
58 changes: 58 additions & 0 deletions subsonic/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ type SubsonicResponse struct {
SearchResults SubsonicResults `json:"searchResult3"`
ScanStatus ScanStatus `json:"scanStatus"`
PlayQueue PlayQueue `json:"playQueue"`
LyricsList LyricsList `json:"lyricsList"`
}

type responseWrapper struct {
Expand Down Expand Up @@ -440,6 +441,48 @@ func (connection *SubsonicConnection) GetCoverArt(id string) (image.Image, error
return art, err
}

// GetLyricsBySongId fetches time synchronized song lyrics. If the server does
// not support this, an error is returned.
func (connection *SubsonicConnection) GetLyricsBySongId(id string) ([]StructuredLyrics, error) {
if id == "" {
return []StructuredLyrics{}, fmt.Errorf("GetLyricsBySongId: no ID provided")
}
query := defaultQuery(connection)
query.Set("id", id)
query.Set("f", "json")
caller := "GetLyricsBySongId"
res, err := http.Get(connection.Host + "/rest/getLyricsBySongId" + "?" + query.Encode())
if err != nil {
return []StructuredLyrics{}, fmt.Errorf("[%s] failed to make GET request: %v", caller, err)
}

if res.Body != nil {
defer res.Body.Close()
} else {
return []StructuredLyrics{}, fmt.Errorf("[%s] response body is nil", caller)
}

if res.StatusCode != http.StatusOK {
return []StructuredLyrics{}, fmt.Errorf("[%s] unexpected status code: %d, status: %s", caller, res.StatusCode, res.Status)
}

if len(res.Header["Content-Type"]) == 0 {
return []StructuredLyrics{}, fmt.Errorf("[%s] unknown image type (no content-type from server)", caller)
}

responseBody, readErr := io.ReadAll(res.Body)
if readErr != nil {
return []StructuredLyrics{}, fmt.Errorf("[%s] failed to read response body: %v", caller, readErr)
}

var decodedBody responseWrapper
err = json.Unmarshal(responseBody, &decodedBody)
if err != nil {
return []StructuredLyrics{}, fmt.Errorf("[%s] failed to unmarshal response body: %v", caller, err)
}
return decodedBody.Response.LyricsList.StructuredLyrics, nil
}

func (connection *SubsonicConnection) GetRandomSongs(Id string, randomType string) (*SubsonicResponse, error) {
query := defaultQuery(connection)

Expand Down Expand Up @@ -688,3 +731,18 @@ func (connection *SubsonicConnection) LoadPlayQueue() (*SubsonicResponse, error)
requestUrl := fmt.Sprintf("%s/rest/getPlayQueue?%s", connection.Host, query.Encode())
return connection.getResponse("GetPlayQueue", requestUrl)
}

type LyricsList struct {
StructuredLyrics []StructuredLyrics `json:"structuredLyrics"`
}

type StructuredLyrics struct {
Lang string `json:"lang"`
Synced bool `json:"synced"`
Lines []LyricsLine `json:"line"`
}

type LyricsLine struct {
Start int64 `json:"start"`
Value string `json:"value"`
}

0 comments on commit 30a5898

Please sign in to comment.