From a3449d22ff97d91f46c3a9e56237f80a04a42ed4 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 5 Mar 2024 08:18:19 -0800 Subject: [PATCH 1/5] update go-subsonic --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6e39c67..6ab782c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee - github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b + github.com/dweymouth/go-subsonic v0.0.0-20240305034202-6193dca1c9de github.com/fsnotify/fsnotify v1.6.0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 diff --git a/go.sum b/go.sum index feb47c5..5cec0b5 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc h1:wJy4U12Ys github.com/dweymouth/go-jellyfin v0.0.0-20231116161116-e800860bdacc/go.mod h1:BMwS4vdjEYf1gmjPGSKCzWP/I6YlI6fkefJ9nsjBjaU= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= -github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b h1:LGUWXRcndcWwBN0HTFsCUlIjr8aakRsi9FlUX1CPSs8= -github.com/dweymouth/go-subsonic v0.0.0-20240225165422-3617754d751b/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= +github.com/dweymouth/go-subsonic v0.0.0-20240305034202-6193dca1c9de h1:RVYHvjT01YSLphWlTA/uaGtfVp2x6/4UfOIYYp9vMis= +github.com/dweymouth/go-subsonic v0.0.0-20240305034202-6193dca1c9de/go.mod h1:OWtcumdQsan8uM6wmx6PqKhldaCthH10CQ+vb+94kzo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= From ea1d78ed589a7e21e81f52c510922534a61d20bf Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Tue, 5 Mar 2024 08:53:57 -0800 Subject: [PATCH 2/5] add LyricsProvider / GetLyrics --- backend/mediaprovider/mediaprovider.go | 4 ++ backend/mediaprovider/model.go | 12 +++++ .../subsonic/subsonicmediaprovider.go | 48 +++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index f7fe0b1..fde5fb7 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -154,6 +154,10 @@ type SupportsSharing interface { CanShareArtists() bool } +type LyricsProvider interface { + GetLyrics(track *Track) (*Lyrics, error) +} + type JukeboxProvider interface { JukeboxStart() error JukeboxStop() error diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index ac4201a..d7a820f 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -120,6 +120,18 @@ type PlaylistWithTracks struct { Tracks []*Track } +type Lyrics struct { + Title string + Artist string + Synced bool + Lines []LyricLine +} + +type LyricLine struct { + Text string + Start float64 // seconds +} + type ContentType int const ( diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 5c858eb..4470bf7 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -6,6 +6,7 @@ import ( "io" "math" "net/url" + "slices" "strconv" "strings" "sync" @@ -345,6 +346,53 @@ func (s *subsonicMediaProvider) RescanLibrary() error { return err } +// LyricsProvider interface +var _ mediaprovider.LyricsProvider = (*subsonicMediaProvider)(nil) + +func (s *subsonicMediaProvider) GetLyrics(track *mediaprovider.Track) (*mediaprovider.Lyrics, error) { + ext, err := s.client.GetOpenSubsonicExtensions() + supportsSynced := err == nil && + slices.ContainsFunc(ext, func(ext *subsonic.OpenSubsonicExtension) bool { + return ext.Name == subsonic.SongLyricsExtension + }) + if supportsSynced { + lyrics, err := s.client.GetLyricsBySongId(track.ID) + if err != nil || len(lyrics.StructuredLyrics) == 0 { + return nil, err + } + lyric := lyrics.StructuredLyrics[0] + mpLyrics := &mediaprovider.Lyrics{ + Title: lyric.DisplayTitle, + Artist: lyric.DisplayArtist, + Synced: lyric.Synced, + } + for _, line := range lyric.Lines { + mpLyrics.Lines = append(mpLyrics.Lines, mediaprovider.LyricLine{ + Text: line.Text, + Start: float64(line.Start) / 1000, + }) + } + return mpLyrics, nil + } + // fallback to legacy getLyrics endpoint + lyrics, err := s.client.GetLyrics(track.Name, track.ArtistNames[0]) + if err != nil || lyrics == nil || lyrics.Text == "" { + return nil, err + } + mpLyrics := &mediaprovider.Lyrics{ + Title: lyrics.Title, + Artist: lyrics.Artist, + Synced: false, + } + lines := strings.Split(lyrics.Text, "\n") + for _, line := range lines { + mpLyrics.Lines = append(mpLyrics.Lines, mediaprovider.LyricLine{ + Text: line, + }) + } + return mpLyrics, nil +} + func toTrack(ch *subsonic.Child) *mediaprovider.Track { if ch == nil { return nil From 3fa52c73df4c6a4a0ad9a8d796acac6d0d110b37 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 6 Mar 2024 08:05:53 -0800 Subject: [PATCH 3/5] add unsynced lyrics view (not tested yet) --- ui/browsing/nowplayingpage.go | 14 ++++++++++-- ui/browsing/router.go | 4 +--- ui/controller/routes.go | 1 - ui/widgets/lyricsviewer.go | 42 ++++++++++++++++++++++++++++++++--- 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 8f9d0ab..b1757b6 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -46,6 +46,7 @@ type NowPlayingPage struct { type nowPlayingPageState struct { contr *controller.Controller pool *util.WidgetPool + sm *backend.ServerManager pm *backend.PlaybackManager im *backend.ImageManager canRate bool @@ -55,13 +56,14 @@ type nowPlayingPageState struct { func NewNowPlayingPage( contr *controller.Controller, pool *util.WidgetPool, + sm *backend.ServerManager, im *backend.ImageManager, pm *backend.PlaybackManager, canRate bool, canShare bool, ) *NowPlayingPage { a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ - contr: contr, pool: pool, im: im, pm: pm, canRate: canRate, canShare: canShare, + contr: contr, pool: pool, sm: sm, im: im, pm: pm, canRate: canRate, canShare: canShare, }} a.ExtendBaseWidget(a) @@ -181,6 +183,14 @@ func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr a.card.SetCoverImage(nil) return } + if lp, ok := a.sm.Server.(mediaprovider.LyricsProvider); ok { + lyrics, err := lp.GetLyrics(song) + if err == nil { + a.lyricsViewer.SetLyrics(lyrics) + } else { + a.lyricsViewer.SetLyrics(nil) + } + } a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.CoverArtID, func(img image.Image, err error) { if err != nil { log.Printf("error loading cover art: %v\n", err) @@ -209,7 +219,7 @@ func (s *nowPlayingPageState) Restore() Page { page.Reload() return page } - return NewNowPlayingPage(s.contr, s.pool, s.im, s.pm, s.canRate, s.canShare) + return NewNowPlayingPage(s.contr, s.pool, s.sm, s.im, s.pm, s.canRate, s.canShare) } var _ CanShowPlayTime = (*NowPlayingPage)(nil) diff --git a/ui/browsing/router.go b/ui/browsing/router.go index c92d2e9..65aa5ed 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -43,14 +43,12 @@ func (r Router) CreatePage(rte controller.Route) Page { return NewArtistsPage(r.Controller, r.widgetPool, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Favorites: return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) - case controller.Fullscreen: - return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate, canShare) case controller.Genre: return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: - return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ImageManager, r.App.PlaybackManager, canRate, canShare) + return NewNowPlayingPage(r.Controller, r.widgetPool, r.App.ServerManager, r.App.ImageManager, r.App.PlaybackManager, canRate, canShare) case controller.Playlist: return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) case controller.Playlists: diff --git a/ui/controller/routes.go b/ui/controller/routes.go index a351b8f..dc2255a 100644 --- a/ui/controller/routes.go +++ b/ui/controller/routes.go @@ -8,7 +8,6 @@ const ( Albums Artist Artists - Fullscreen Genre Genres Favorites diff --git a/ui/widgets/lyricsviewer.go b/ui/widgets/lyricsviewer.go index 683771f..d40ea8c 100644 --- a/ui/widgets/lyricsviewer.go +++ b/ui/widgets/lyricsviewer.go @@ -1,15 +1,51 @@ package widgets -import "fyne.io/fyne/v2/widget" +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" + "github.com/dweymouth/supersonic/backend/mediaprovider" +) type LyricsViewer struct { - widget.Label + widget.BaseWidget + + noLyricsLabel widget.Label + unsyncedViewer *widget.RichText + + container *container.Scroll } func NewLyricsViewer() *LyricsViewer { - l := &LyricsViewer{Label: widget.Label{ + l := &LyricsViewer{noLyricsLabel: widget.Label{ Text: "Lyrics not available", }} l.ExtendBaseWidget(l) + l.container = container.NewVScroll(&l.noLyricsLabel) return l } + +func (l *LyricsViewer) SetLyrics(lyrics *mediaprovider.Lyrics) { + if lyrics == nil { + l.container.Content = &l.noLyricsLabel + l.Refresh() + return + } + + if l.unsyncedViewer == nil { + l.unsyncedViewer = widget.NewRichText() + } + l.unsyncedViewer.Segments = l.unsyncedViewer.Segments[:0] + for _, line := range lyrics.Lines { + ts := &widget.TextSegment{Text: line.Text} + ts.Style.Alignment = fyne.TextAlignCenter + ts.Style.Inline = false + l.unsyncedViewer.Segments = append(l.unsyncedViewer.Segments, ts) + } + l.container.Content = l.unsyncedViewer + l.Refresh() +} + +func (l *LyricsViewer) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(l.container) +} From b3bec2694b14f53d04f59f0e2f877ee8ed30e491 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 6 Mar 2024 08:13:02 -0800 Subject: [PATCH 4/5] fetch lyrics async --- ui/browsing/nowplayingpage.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index b1757b6..2644ad3 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -183,14 +183,7 @@ func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr a.card.SetCoverImage(nil) return } - if lp, ok := a.sm.Server.(mediaprovider.LyricsProvider); ok { - lyrics, err := lp.GetLyrics(song) - if err == nil { - a.lyricsViewer.SetLyrics(lyrics) - } else { - a.lyricsViewer.SetLyrics(nil) - } - } + a.imageLoadCancel = a.im.GetFullSizeCoverArtAsync(song.CoverArtID, func(img image.Image, err error) { if err != nil { log.Printf("error loading cover art: %v\n", err) @@ -198,6 +191,17 @@ func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr a.card.SetCoverImage(img) } }) + + go func() { + if lp, ok := a.sm.Server.(mediaprovider.LyricsProvider); ok { + lyrics, err := lp.GetLyrics(song) + if err == nil { + a.lyricsViewer.SetLyrics(lyrics) + } else { + a.lyricsViewer.SetLyrics(nil) + } + } + }() } func (a *NowPlayingPage) OnPlayQueueChange() { From 129afcf4a8d6c7ff125d37c3730005768f0d40a6 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 6 Mar 2024 09:44:52 -0800 Subject: [PATCH 5/5] update lyrics text style --- ui/browsing/nowplayingpage.go | 1 + ui/widgets/lyricsviewer.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 2644ad3..daf1f92 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -198,6 +198,7 @@ func (a *NowPlayingPage) OnSongChange(song, lastScrobbledIfAny *mediaprovider.Tr if err == nil { a.lyricsViewer.SetLyrics(lyrics) } else { + log.Printf("Error fetching lyrics: %v", err) a.lyricsViewer.SetLyrics(nil) } } diff --git a/ui/widgets/lyricsviewer.go b/ui/widgets/lyricsviewer.go index d40ea8c..daffc61 100644 --- a/ui/widgets/lyricsviewer.go +++ b/ui/widgets/lyricsviewer.go @@ -26,7 +26,7 @@ func NewLyricsViewer() *LyricsViewer { } func (l *LyricsViewer) SetLyrics(lyrics *mediaprovider.Lyrics) { - if lyrics == nil { + if lyrics == nil || len(lyrics.Lines) == 0 { l.container.Content = &l.noLyricsLabel l.Refresh() return @@ -34,11 +34,13 @@ func (l *LyricsViewer) SetLyrics(lyrics *mediaprovider.Lyrics) { if l.unsyncedViewer == nil { l.unsyncedViewer = widget.NewRichText() + l.unsyncedViewer.Wrapping = fyne.TextWrapWord } l.unsyncedViewer.Segments = l.unsyncedViewer.Segments[:0] for _, line := range lyrics.Lines { ts := &widget.TextSegment{Text: line.Text} ts.Style.Alignment = fyne.TextAlignCenter + ts.Style.SizeName = widget.RichTextStyleSubHeading.SizeName ts.Style.Inline = false l.unsyncedViewer.Segments = append(l.unsyncedViewer.Segments, ts) }