From 298f4f6602e24e4606815e6bbf05cd526e21f10c Mon Sep 17 00:00:00 2001 From: natilou Date: Sun, 24 Mar 2024 18:38:41 -0300 Subject: [PATCH 1/3] feat: Add Song radio option in tracklist. At this moment this feature is only available on the Subsonic media provider. Using a Jellyfin server the "Play song radio" option will be disabled. --- backend/mediaprovider/mediaprovider.go | 4 +++ .../subsonic/subsonicmediaprovider.go | 8 +++++ res/bundled.go | 5 +++ res/bundled_gen.sh | 1 + res/icons/remix_design/broadcast.svg | 4 +++ ui/browsing/albumpage.go | 2 ++ ui/browsing/artistpage.go | 2 ++ ui/browsing/favoritespage.go | 2 ++ ui/browsing/playlistpage.go | 2 ++ ui/browsing/trackspage.go | 4 +++ ui/controller/controller.go | 34 +++++++++++++++++++ ui/theme/theme.go | 1 + ui/widgets/tracklist.go | 20 ++++++++++- 13 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 res/icons/remix_design/broadcast.svg diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index 2bdcccb..8bfec98 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -200,6 +200,10 @@ type SupportsSharing interface { CanShareArtists() bool } +type SupportsSongRadio interface { + GetSongRadio(trackID string, count int) ([]*Track, error) +} + type LyricsProvider interface { GetLyrics(track *Track) (*Lyrics, error) } diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index f18f649..b6ab679 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -543,3 +543,11 @@ func fillPlaylist(pl *subsonic.Playlist, playlist *mediaprovider.Playlist) { playlist.TrackCount = pl.SongCount playlist.Duration = pl.Duration } + +func (s *subsonicMediaProvider) GetSongRadio(trackID string, count int) ([]*mediaprovider.Track, error) { + tr, err := s.client.GetSimilarSongs(trackID, map[string]string{"count": strconv.Itoa(count)}) + if err != nil { + return nil, err + } + return sharedutil.MapSlice(tr, toTrack), nil +} diff --git a/res/bundled.go b/res/bundled.go index d34c469..b63a2d8 100644 --- a/res/bundled.go +++ b/res/bundled.go @@ -80,6 +80,11 @@ var ResFilterSvg = &fyne.StaticResource{ StaticContent: []byte( "\r\n\r\n\t\r\n\r\n\r\n"), } +var ResBroadcastSvg = &fyne.StaticResource{ + StaticName: "broadcast.svg", + StaticContent: []byte( + "\n\n \n"), +} var ResRepeatSvg = &fyne.StaticResource{ StaticName: "repeat.svg", StaticContent: []byte( diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh index c0bdb70..bcd7a94 100755 --- a/res/bundled_gen.sh +++ b/res/bundled_gen.sh @@ -15,6 +15,7 @@ fyne bundle -append -prefix Res icons/publicdomain/theatermasks.svg >> bundled.g fyne bundle -append -prefix Res icons/publicdomain/grid.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/list.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/filter.svg >> bundled.go +fyne bundle -append -prefix Res icons/remix_design/broadcast.svg >> bundled.go fyne bundle -append -prefix Res icons/remix_design/repeat.svg >> bundled.go fyne bundle -append -prefix Res icons/remix_design/repeatone.svg >> bundled.go fyne bundle -append -prefix Res icons/remix_design/shuffle.svg >> bundled.go diff --git a/res/icons/remix_design/broadcast.svg b/res/icons/remix_design/broadcast.svg new file mode 100644 index 0000000..ff53661 --- /dev/null +++ b/res/icons/remix_design/broadcast.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 13078dd..62e8134 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -95,8 +95,10 @@ func newAlbumPage( a.tracklist.SetSorting(sort) _, canRate := a.mp.(mediaprovider.SupportsRating) _, canShare := a.mp.(mediaprovider.SupportsSharing) + _, canSongRadio := a.mp.(mediaprovider.SupportsSongRadio) a.tracklist.Options.DisableRating = !canRate a.tracklist.Options.DisableSharing = !canShare + a.tracklist.Options.DisableSongRadio = !canSongRadio a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.cfg.TracklistColumns = cols } diff --git a/ui/browsing/artistpage.go b/ui/browsing/artistpage.go index e942a7f..cec5037 100644 --- a/ui/browsing/artistpage.go +++ b/ui/browsing/artistpage.go @@ -238,8 +238,10 @@ func (a *ArtistPage) showTopTracks() { tl.Options = widgets.TracklistOptions{AutoNumber: true} _, canRate := a.mp.(mediaprovider.SupportsRating) _, canShare := a.mp.(mediaprovider.SupportsSharing) + _, canSongRadio := a.mp.(mediaprovider.SupportsSongRadio) tl.Options.DisableRating = !canRate tl.Options.DisableSharing = !canShare + tl.Options.DisableSongRadio = !canSongRadio tl.SetVisibleColumns(a.cfg.TracklistColumns) tl.SetSorting(a.trackSort) tl.OnVisibleColumnsChanged = func(cols []string) { diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 9e78fae..b60ee87 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -403,8 +403,10 @@ func (a *FavoritesPage) onShowFavoriteSongs() { tracklist.Options = widgets.TracklistOptions{AutoNumber: true} _, canRate := a.mp.(mediaprovider.SupportsRating) _, canShare := a.mp.(mediaprovider.SupportsSharing) + _, canSongRadio := a.mp.(mediaprovider.SupportsSongRadio) tracklist.Options.DisableRating = !canRate tracklist.Options.DisableSharing = !canShare + tracklist.Options.DisableSongRadio = !canSongRadio tracklist.SetVisibleColumns(a.cfg.TracklistColumns) tracklist.SetSorting(a.trackSort) tracklist.OnVisibleColumnsChanged = func(cols []string) { diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index a4d6adc..7022996 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -88,11 +88,13 @@ func newPlaylistPage( } _, canRate := a.sm.Server.(mediaprovider.SupportsRating) _, canShare := a.sm.Server.(mediaprovider.SupportsSharing) + _, canSongRadio := a.sm.Server.(mediaprovider.SupportsSongRadio) remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist) remove.Icon = theme.ContentClearIcon() a.tracklist.Options = widgets.TracklistOptions{ DisableRating: !canRate, DisableSharing: !canShare, + DisableSongRadio: !canSongRadio, AuxiliaryMenuItems: []*fyne.MenuItem{ util.NewReorderTracksSubmenu(a.doSetNewTrackOrder), remove, diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index 4b6884a..ef1f341 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -41,6 +41,7 @@ type tracksPageState struct { mp mediaprovider.MediaProvider canRate bool canShare bool + canSongRadio bool } func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage { @@ -50,10 +51,12 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, t.tracklist = t.obtainTracklist() _, t.canRate = mp.(mediaprovider.SupportsRating) _, t.canShare = mp.(mediaprovider.SupportsSharing) + _, t.canSongRadio = mp.(mediaprovider.SupportsSongRadio) t.tracklist.Options = widgets.TracklistOptions{ DisableSorting: true, DisableRating: !t.canRate, DisableSharing: !t.canShare, + DisableSongRadio: !t.canSongRadio, AutoNumber: true, } t.tracklist.SetVisibleColumns(conf.TracklistColumns) @@ -141,6 +144,7 @@ func (t *TracksPage) doSearch(query string) { DisableSorting: true, DisableRating: !t.canRate, DisableSharing: !t.canShare, + DisableSongRadio: !t.canSongRadio, } t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns) t.searchTracklist.SetNowPlaying(t.nowPlayingID) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index cb14e23..bcc9359 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -145,6 +145,20 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget tracklist.OnShare = func(trackID string) { go m.ShowShareDialog(trackID) } + tracklist.OnPlaySongRadio = func (track *mediaprovider.Track) { + go func() { + tracks, err := m.GetSongRadioTracks(track) + if err != nil { + log.Printf("Error getting song radio: ", err) + return + } + m.App.PlaybackManager.LoadTracks(tracks, false, false) + if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { + m.App.PlaybackManager.SetReplayGainMode(mode) + } + m.App.PlaybackManager.PlayFromBeginning() + }() + } } func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) { @@ -868,3 +882,23 @@ func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover i pop.Show() }() } + +func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*mediaprovider.Track, error) { + r, ok := c.App.ServerManager.Server.(mediaprovider.SupportsSongRadio) + if !ok { + return nil, fmt.Errorf("Server does not support song radio") + } + + radioTracks, err := r.GetSongRadio(sourceTrack.ID, 100) + if err != nil { + return nil, fmt.Errorf("Error getting song radio: ", err) + } + + // The goal of this implementation is to place the source track first in the queue. + filteredTracks := sharedutil.FilterSlice(radioTracks, func(track *mediaprovider.Track) bool{ + return track.ID != sourceTrack.ID + }) + tracks := []*mediaprovider.Track{sourceTrack} + tracks = append(tracks, filteredTracks...) + return tracks, nil +} \ No newline at end of file diff --git a/ui/theme/theme.go b/ui/theme/theme.go index 64f0296..2755d63 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -27,6 +27,7 @@ const ( var ( AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg) ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg) + BroadcastIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg) FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg) NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg) NowPlayingIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg) diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 03eb592..7806f26 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -76,6 +76,9 @@ type TracklistOptions struct { // Disables the sharing option. DisableSharing bool + + // Disables the song radio option. + DisableSongRadio bool } type Tracklist struct { @@ -92,6 +95,7 @@ type Tracklist struct { OnSetRating func(trackIDs []string, rating int) OnDownload func(tracks []*mediaprovider.Track, downloadName string) OnShare func(trackID string) + OnPlaySongRadio func(track *mediaprovider.Track) OnShowArtistPage func(artistID string) OnShowAlbumPage func(albumID string) @@ -114,6 +118,7 @@ type Tracklist struct { ctxMenu *fyne.Menu ratingSubmenu *fyne.MenuItem shareMenuItem *fyne.MenuItem + songRadioMenuItem *fyne.MenuItem container *fyne.Container } @@ -539,8 +544,12 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { } }) add.Icon = theme.ContentAddIcon() + t.songRadioMenuItem = fyne.NewMenuItem("Play song radio", func() { + t.onPlaySongRadio(t.selectedTracks()) + }) + t.songRadioMenuItem.Icon = myTheme.BroadcastIcon t.ctxMenu.Items = append(t.ctxMenu.Items, - play, shuffle, add) + play, shuffle, add, t.songRadioMenuItem) } playlist := fyne.NewMenuItem("Add to playlist...", func() { if t.OnAddToPlaylist != nil { @@ -579,6 +588,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) { } t.ratingSubmenu.Disabled = t.Options.DisableRating t.shareMenuItem.Disabled = t.Options.DisableSharing || len(t.selectedTracks()) != 1 + t.songRadioMenuItem.Disabled = t.Options.DisableSongRadio || len(t.selectedTracks()) != 1 widget.ShowPopUpMenuAtPosition(t.ctxMenu, fyne.CurrentApp().Driver().CanvasForObject(t), e.AbsolutePosition) } @@ -649,6 +659,14 @@ func (t *Tracklist) onShare(tracks []*mediaprovider.Track) { } } +func (t *Tracklist) onPlaySongRadio(tracks []*mediaprovider.Track) { + if t.OnPlaySongRadio != nil { + if len(tracks) > 0 { + t.OnPlaySongRadio(tracks[0]) + } + } +} + func (t *Tracklist) selectedTracks() []*mediaprovider.Track { t.tracksMutex.RLock() defer t.tracksMutex.RUnlock() From c1bf78943292b627b3386cc57923479c99252b11 Mon Sep 17 00:00:00 2001 From: natilou Date: Sun, 24 Mar 2024 19:00:22 -0300 Subject: [PATCH 2/3] fix fmt --- ui/browsing/playlistpage.go | 4 ++-- ui/browsing/trackspage.go | 30 +++++++++++++++--------------- ui/controller/controller.go | 10 +++++----- ui/widgets/tracklist.go | 16 ++++++++-------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 7022996..4d340ad 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -92,8 +92,8 @@ func newPlaylistPage( remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist) remove.Icon = theme.ContentClearIcon() a.tracklist.Options = widgets.TracklistOptions{ - DisableRating: !canRate, - DisableSharing: !canShare, + DisableRating: !canRate, + DisableSharing: !canShare, DisableSongRadio: !canSongRadio, AuxiliaryMenuItems: []*fyne.MenuItem{ util.NewReorderTracksSubmenu(a.doSetNewTrackOrder), diff --git a/ui/browsing/trackspage.go b/ui/browsing/trackspage.go index ef1f341..becad43 100644 --- a/ui/browsing/trackspage.go +++ b/ui/browsing/trackspage.go @@ -34,13 +34,13 @@ type TracksPage struct { } type tracksPageState struct { - searchText string - widgetPool *util.WidgetPool - contr *controller.Controller - conf *backend.TracksPageConfig - mp mediaprovider.MediaProvider - canRate bool - canShare bool + searchText string + widgetPool *util.WidgetPool + contr *controller.Controller + conf *backend.TracksPageConfig + mp mediaprovider.MediaProvider + canRate bool + canShare bool canSongRadio bool } @@ -53,11 +53,11 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, _, t.canShare = mp.(mediaprovider.SupportsSharing) _, t.canSongRadio = mp.(mediaprovider.SupportsSongRadio) t.tracklist.Options = widgets.TracklistOptions{ - DisableSorting: true, - DisableRating: !t.canRate, - DisableSharing: !t.canShare, + DisableSorting: true, + DisableRating: !t.canRate, + DisableSharing: !t.canShare, DisableSongRadio: !t.canSongRadio, - AutoNumber: true, + AutoNumber: true, } t.tracklist.SetVisibleColumns(conf.TracklistColumns) t.tracklist.OnVisibleColumnsChanged = func(cols []string) { @@ -140,10 +140,10 @@ func (t *TracksPage) doSearch(query string) { if t.searchTracklist == nil { t.searchTracklist = t.obtainTracklist() t.searchTracklist.Options = widgets.TracklistOptions{ - AutoNumber: true, - DisableSorting: true, - DisableRating: !t.canRate, - DisableSharing: !t.canShare, + AutoNumber: true, + DisableSorting: true, + DisableRating: !t.canRate, + DisableSharing: !t.canShare, DisableSongRadio: !t.canSongRadio, } t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index bcc9359..07f9a20 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -145,12 +145,12 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget tracklist.OnShare = func(trackID string) { go m.ShowShareDialog(trackID) } - tracklist.OnPlaySongRadio = func (track *mediaprovider.Track) { + tracklist.OnPlaySongRadio = func(track *mediaprovider.Track) { go func() { tracks, err := m.GetSongRadioTracks(track) if err != nil { log.Printf("Error getting song radio: ", err) - return + return } m.App.PlaybackManager.LoadTracks(tracks, false, false) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { @@ -893,12 +893,12 @@ func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*me if err != nil { return nil, fmt.Errorf("Error getting song radio: ", err) } - + // The goal of this implementation is to place the source track first in the queue. - filteredTracks := sharedutil.FilterSlice(radioTracks, func(track *mediaprovider.Track) bool{ + filteredTracks := sharedutil.FilterSlice(radioTracks, func(track *mediaprovider.Track) bool { return track.ID != sourceTrack.ID }) tracks := []*mediaprovider.Track{sourceTrack} tracks = append(tracks, filteredTracks...) return tracks, nil -} \ No newline at end of file +} diff --git a/ui/widgets/tracklist.go b/ui/widgets/tracklist.go index 7806f26..a025be2 100644 --- a/ui/widgets/tracklist.go +++ b/ui/widgets/tracklist.go @@ -111,15 +111,15 @@ type Tracklist struct { tracks []*util.TrackListModel tracksOrigOrder []*util.TrackListModel - nowPlayingID string - colLayout *layouts.ColumnsLayout - hdr *ListHeader - list *FocusList - ctxMenu *fyne.Menu - ratingSubmenu *fyne.MenuItem - shareMenuItem *fyne.MenuItem + nowPlayingID string + colLayout *layouts.ColumnsLayout + hdr *ListHeader + list *FocusList + ctxMenu *fyne.Menu + ratingSubmenu *fyne.MenuItem + shareMenuItem *fyne.MenuItem songRadioMenuItem *fyne.MenuItem - container *fyne.Container + container *fyne.Container } func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { From 0bd0ea891951c7e34db88bdb614d218fbf21caa1 Mon Sep 17 00:00:00 2001 From: natilou Date: Fri, 29 Mar 2024 12:17:03 -0300 Subject: [PATCH 3/3] adding get son radio support to Jellyfin server --- backend/mediaprovider/jellyfin/jellyfinmediaprovider.go | 8 ++++++++ ui/controller/controller.go | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index c4b40f6..882c885 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -452,3 +452,11 @@ func (j *jellyfinMediaProvider) fillPlaylist(p *jellyfin.Playlist, pl *mediaprov pl.Owner = j.client.LoggedInUser() pl.Public = false } + +func (j *jellyfinMediaProvider) GetSongRadio(trackID string, count int) ([]*mediaprovider.Track, error) { + tr, err := j.client.GetInstantMix(trackID, jellyfin.TypeSong, count) + if err != nil { + return nil, err + } + return sharedutil.MapSlice(tr, toTrack), nil +} diff --git a/ui/controller/controller.go b/ui/controller/controller.go index 07f9a20..d64bc61 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -149,7 +149,7 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget go func() { tracks, err := m.GetSongRadioTracks(track) if err != nil { - log.Printf("Error getting song radio: ", err) + log.Println("Error getting song radio: ", err) return } m.App.PlaybackManager.LoadTracks(tracks, false, false) @@ -886,12 +886,12 @@ func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover i func (c *Controller) GetSongRadioTracks(sourceTrack *mediaprovider.Track) ([]*mediaprovider.Track, error) { r, ok := c.App.ServerManager.Server.(mediaprovider.SupportsSongRadio) if !ok { - return nil, fmt.Errorf("Server does not support song radio") + return nil, fmt.Errorf("server does not support song radio") } radioTracks, err := r.GetSongRadio(sourceTrack.ID, 100) if err != nil { - return nil, fmt.Errorf("Error getting song radio: ", err) + return nil, fmt.Errorf("error getting song radio: %s", err.Error()) } // The goal of this implementation is to place the source track first in the queue.