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()