Merge pull request #352 from natilou/similar-songs

feat: Add Song radio option in tracklist.
This commit is contained in:
Drew Weymouth
2024-03-30 09:56:45 -07:00
committed by GitHub
14 changed files with 121 additions and 26 deletions
@@ -452,3 +452,11 @@ func (j *jellyfinMediaProvider) fillPlaylist(p *jellyfin.Playlist, pl *mediaprov
pl.Owner = j.client.LoggedInUser() pl.Owner = j.client.LoggedInUser()
pl.Public = false 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
}
+4
View File
@@ -200,6 +200,10 @@ type SupportsSharing interface {
CanShareArtists() bool CanShareArtists() bool
} }
type SupportsSongRadio interface {
GetSongRadio(trackID string, count int) ([]*Track, error)
}
type LyricsProvider interface { type LyricsProvider interface {
GetLyrics(track *Track) (*Lyrics, error) GetLyrics(track *Track) (*Lyrics, error)
} }
@@ -543,3 +543,11 @@ func fillPlaylist(pl *subsonic.Playlist, playlist *mediaprovider.Playlist) {
playlist.TrackCount = pl.SongCount playlist.TrackCount = pl.SongCount
playlist.Duration = pl.Duration 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
}
+5
View File
@@ -80,6 +80,11 @@ var ResFilterSvg = &fyne.StaticResource{
StaticContent: []byte( StaticContent: []byte(
"<svg fill=\"#000000\" version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" \r\n\t width=\"800px\" height=\"800px\" viewBox=\"0 0 971.986 971.986\"\r\n\t xml:space=\"preserve\">\r\n<g>\r\n\t<path d=\"M370.216,459.3c10.2,11.1,15.8,25.6,15.8,40.6v442c0,26.601,32.1,40.101,51.1,21.4l123.3-141.3\r\n\t\tc16.5-19.8,25.6-29.601,25.6-49.2V500c0-15,5.7-29.5,15.8-40.601L955.615,75.5c26.5-28.8,6.101-75.5-33.1-75.5h-873\r\n\t\tc-39.2,0-59.7,46.6-33.1,75.5L370.216,459.3z\"/>\r\n</g>\r\n</svg>\r\n"), "<svg fill=\"#000000\" version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" \r\n\t width=\"800px\" height=\"800px\" viewBox=\"0 0 971.986 971.986\"\r\n\t xml:space=\"preserve\">\r\n<g>\r\n\t<path d=\"M370.216,459.3c10.2,11.1,15.8,25.6,15.8,40.6v442c0,26.601,32.1,40.101,51.1,21.4l123.3-141.3\r\n\t\tc16.5-19.8,25.6-29.601,25.6-49.2V500c0-15,5.7-29.5,15.8-40.601L955.615,75.5c26.5-28.8,6.101-75.5-33.1-75.5h-873\r\n\t\tc-39.2,0-59.7,46.6-33.1,75.5L370.216,459.3z\"/>\r\n</g>\r\n</svg>\r\n"),
} }
var ResBroadcastSvg = &fyne.StaticResource{
StaticName: "broadcast.svg",
StaticContent: []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M4.92893 2.92896L6.34315 4.34317C4.89543 5.79088 4 7.79088 4 10C4 12.2092 4.89543 14.2092 6.34315 15.6569L4.92893 17.0711C3.11929 15.2614 2 12.7614 2 10C2 7.2386 3.11929 4.7386 4.92893 2.92896ZM19.0711 2.92896C20.8807 4.7386 22 7.2386 22 10C22 12.7614 20.8807 15.2614 19.0711 17.0711L17.6569 15.6569C19.1046 14.2092 20 12.2092 20 10C20 7.79088 19.1046 5.79088 17.6569 4.34317L19.0711 2.92896ZM7.75736 5.75738L9.17157 7.1716C8.44771 7.89545 8 8.89545 8 10C8 11.1046 8.44771 12.1046 9.17157 12.8285L7.75736 14.2427C6.67157 13.1569 6 11.6569 6 10C6 8.34317 6.67157 6.84317 7.75736 5.75738ZM16.2426 5.75738C17.3284 6.84317 18 8.34317 18 10C18 11.6569 17.3284 13.1569 16.2426 14.2427L14.8284 12.8285C15.5523 12.1046 16 11.1046 16 10C16 8.89545 15.5523 7.89545 14.8284 7.1716L16.2426 5.75738ZM12 12C10.8954 12 10 11.1046 10 10C10 8.89545 10.8954 8.00002 12 8.00002C13.1046 8.00002 14 8.89545 14 10C14 11.1046 13.1046 12 12 12ZM12 14C12.5798 14 13.0774 14.413 13.1843 14.9829L14.5 22H9.5L10.8157 14.9829C10.9226 14.413 11.4202 14 12 14Z\"/>\n</svg>"),
}
var ResRepeatSvg = &fyne.StaticResource{ var ResRepeatSvg = &fyne.StaticResource{
StaticName: "repeat.svg", StaticName: "repeat.svg",
StaticContent: []byte( StaticContent: []byte(
+1
View File
@@ -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/grid.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/list.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/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/repeat.svg >> bundled.go
fyne bundle -append -prefix Res icons/remix_design/repeatone.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 fyne bundle -append -prefix Res icons/remix_design/shuffle.svg >> bundled.go
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M4.92893 2.92896L6.34315 4.34317C4.89543 5.79088 4 7.79088 4 10C4 12.2092 4.89543 14.2092 6.34315 15.6569L4.92893 17.0711C3.11929 15.2614 2 12.7614 2 10C2 7.2386 3.11929 4.7386 4.92893 2.92896ZM19.0711 2.92896C20.8807 4.7386 22 7.2386 22 10C22 12.7614 20.8807 15.2614 19.0711 17.0711L17.6569 15.6569C19.1046 14.2092 20 12.2092 20 10C20 7.79088 19.1046 5.79088 17.6569 4.34317L19.0711 2.92896ZM7.75736 5.75738L9.17157 7.1716C8.44771 7.89545 8 8.89545 8 10C8 11.1046 8.44771 12.1046 9.17157 12.8285L7.75736 14.2427C6.67157 13.1569 6 11.6569 6 10C6 8.34317 6.67157 6.84317 7.75736 5.75738ZM16.2426 5.75738C17.3284 6.84317 18 8.34317 18 10C18 11.6569 17.3284 13.1569 16.2426 14.2427L14.8284 12.8285C15.5523 12.1046 16 11.1046 16 10C16 8.89545 15.5523 7.89545 14.8284 7.1716L16.2426 5.75738ZM12 12C10.8954 12 10 11.1046 10 10C10 8.89545 10.8954 8.00002 12 8.00002C13.1046 8.00002 14 8.89545 14 10C14 11.1046 13.1046 12 12 12ZM12 14C12.5798 14 13.0774 14.413 13.1843 14.9829L14.5 22H9.5L10.8157 14.9829C10.9226 14.413 11.4202 14 12 14Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+2
View File
@@ -95,8 +95,10 @@ func newAlbumPage(
a.tracklist.SetSorting(sort) a.tracklist.SetSorting(sort)
_, canRate := a.mp.(mediaprovider.SupportsRating) _, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing) _, canShare := a.mp.(mediaprovider.SupportsSharing)
_, canSongRadio := a.mp.(mediaprovider.SupportsSongRadio)
a.tracklist.Options.DisableRating = !canRate a.tracklist.Options.DisableRating = !canRate
a.tracklist.Options.DisableSharing = !canShare a.tracklist.Options.DisableSharing = !canShare
a.tracklist.Options.DisableSongRadio = !canSongRadio
a.tracklist.OnVisibleColumnsChanged = func(cols []string) { a.tracklist.OnVisibleColumnsChanged = func(cols []string) {
a.cfg.TracklistColumns = cols a.cfg.TracklistColumns = cols
} }
+2
View File
@@ -238,8 +238,10 @@ func (a *ArtistPage) showTopTracks() {
tl.Options = widgets.TracklistOptions{AutoNumber: true} tl.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating) _, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing) _, canShare := a.mp.(mediaprovider.SupportsSharing)
_, canSongRadio := a.mp.(mediaprovider.SupportsSongRadio)
tl.Options.DisableRating = !canRate tl.Options.DisableRating = !canRate
tl.Options.DisableSharing = !canShare tl.Options.DisableSharing = !canShare
tl.Options.DisableSongRadio = !canSongRadio
tl.SetVisibleColumns(a.cfg.TracklistColumns) tl.SetVisibleColumns(a.cfg.TracklistColumns)
tl.SetSorting(a.trackSort) tl.SetSorting(a.trackSort)
tl.OnVisibleColumnsChanged = func(cols []string) { tl.OnVisibleColumnsChanged = func(cols []string) {
+2
View File
@@ -403,8 +403,10 @@ func (a *FavoritesPage) onShowFavoriteSongs() {
tracklist.Options = widgets.TracklistOptions{AutoNumber: true} tracklist.Options = widgets.TracklistOptions{AutoNumber: true}
_, canRate := a.mp.(mediaprovider.SupportsRating) _, canRate := a.mp.(mediaprovider.SupportsRating)
_, canShare := a.mp.(mediaprovider.SupportsSharing) _, canShare := a.mp.(mediaprovider.SupportsSharing)
_, canSongRadio := a.mp.(mediaprovider.SupportsSongRadio)
tracklist.Options.DisableRating = !canRate tracklist.Options.DisableRating = !canRate
tracklist.Options.DisableSharing = !canShare tracklist.Options.DisableSharing = !canShare
tracklist.Options.DisableSongRadio = !canSongRadio
tracklist.SetVisibleColumns(a.cfg.TracklistColumns) tracklist.SetVisibleColumns(a.cfg.TracklistColumns)
tracklist.SetSorting(a.trackSort) tracklist.SetSorting(a.trackSort)
tracklist.OnVisibleColumnsChanged = func(cols []string) { tracklist.OnVisibleColumnsChanged = func(cols []string) {
+4 -2
View File
@@ -88,11 +88,13 @@ func newPlaylistPage(
} }
_, canRate := a.sm.Server.(mediaprovider.SupportsRating) _, canRate := a.sm.Server.(mediaprovider.SupportsRating)
_, canShare := a.sm.Server.(mediaprovider.SupportsSharing) _, canShare := a.sm.Server.(mediaprovider.SupportsSharing)
_, canSongRadio := a.sm.Server.(mediaprovider.SupportsSongRadio)
remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist) remove := fyne.NewMenuItem("Remove from playlist", a.onRemoveSelectedFromPlaylist)
remove.Icon = theme.ContentClearIcon() remove.Icon = theme.ContentClearIcon()
a.tracklist.Options = widgets.TracklistOptions{ a.tracklist.Options = widgets.TracklistOptions{
DisableRating: !canRate, DisableRating: !canRate,
DisableSharing: !canShare, DisableSharing: !canShare,
DisableSongRadio: !canSongRadio,
AuxiliaryMenuItems: []*fyne.MenuItem{ AuxiliaryMenuItems: []*fyne.MenuItem{
util.NewReorderTracksSubmenu(a.doSetNewTrackOrder), util.NewReorderTracksSubmenu(a.doSetNewTrackOrder),
remove, remove,
+19 -15
View File
@@ -34,13 +34,14 @@ type TracksPage struct {
} }
type tracksPageState struct { type tracksPageState struct {
searchText string searchText string
widgetPool *util.WidgetPool widgetPool *util.WidgetPool
contr *controller.Controller contr *controller.Controller
conf *backend.TracksPageConfig conf *backend.TracksPageConfig
mp mediaprovider.MediaProvider mp mediaprovider.MediaProvider
canRate bool canRate bool
canShare bool canShare bool
canSongRadio bool
} }
func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage { func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig, pool *util.WidgetPool, mp mediaprovider.MediaProvider) *TracksPage {
@@ -50,11 +51,13 @@ func NewTracksPage(contr *controller.Controller, conf *backend.TracksPageConfig,
t.tracklist = t.obtainTracklist() t.tracklist = t.obtainTracklist()
_, t.canRate = mp.(mediaprovider.SupportsRating) _, t.canRate = mp.(mediaprovider.SupportsRating)
_, t.canShare = mp.(mediaprovider.SupportsSharing) _, t.canShare = mp.(mediaprovider.SupportsSharing)
_, t.canSongRadio = mp.(mediaprovider.SupportsSongRadio)
t.tracklist.Options = widgets.TracklistOptions{ t.tracklist.Options = widgets.TracklistOptions{
DisableSorting: true, DisableSorting: true,
DisableRating: !t.canRate, DisableRating: !t.canRate,
DisableSharing: !t.canShare, DisableSharing: !t.canShare,
AutoNumber: true, DisableSongRadio: !t.canSongRadio,
AutoNumber: true,
} }
t.tracklist.SetVisibleColumns(conf.TracklistColumns) t.tracklist.SetVisibleColumns(conf.TracklistColumns)
t.tracklist.OnVisibleColumnsChanged = func(cols []string) { t.tracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -137,10 +140,11 @@ func (t *TracksPage) doSearch(query string) {
if t.searchTracklist == nil { if t.searchTracklist == nil {
t.searchTracklist = t.obtainTracklist() t.searchTracklist = t.obtainTracklist()
t.searchTracklist.Options = widgets.TracklistOptions{ t.searchTracklist.Options = widgets.TracklistOptions{
AutoNumber: true, AutoNumber: true,
DisableSorting: true, DisableSorting: true,
DisableRating: !t.canRate, DisableRating: !t.canRate,
DisableSharing: !t.canShare, DisableSharing: !t.canShare,
DisableSongRadio: !t.canSongRadio,
} }
t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns) t.searchTracklist.SetVisibleColumns(t.conf.TracklistColumns)
t.searchTracklist.SetNowPlaying(t.nowPlayingID) t.searchTracklist.SetNowPlaying(t.nowPlayingID)
+34
View File
@@ -145,6 +145,20 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget
tracklist.OnShare = func(trackID string) { tracklist.OnShare = func(trackID string) {
go m.ShowShareDialog(trackID) go m.ShowShareDialog(trackID)
} }
tracklist.OnPlaySongRadio = func(track *mediaprovider.Track) {
go func() {
tracks, err := m.GetSongRadioTracks(track)
if err != nil {
log.Println("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) { func (m *Controller) ConnectAlbumGridActions(grid *widgets.GridView) {
@@ -868,3 +882,23 @@ func (c *Controller) ShowAlbumInfoDialog(albumID, albumName string, albumCover i
pop.Show() 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: %s", err.Error())
}
// 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
}
+1
View File
@@ -27,6 +27,7 @@ const (
var ( var (
AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg) AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg)
ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg) ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg)
BroadcastIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg)
FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg) FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg)
NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg) NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg)
NowPlayingIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg) NowPlayingIcon fyne.Resource = theme.NewThemedResource(res.ResHeadphonesSvg)
+27 -9
View File
@@ -76,6 +76,9 @@ type TracklistOptions struct {
// Disables the sharing option. // Disables the sharing option.
DisableSharing bool DisableSharing bool
// Disables the song radio option.
DisableSongRadio bool
} }
type Tracklist struct { type Tracklist struct {
@@ -92,6 +95,7 @@ type Tracklist struct {
OnSetRating func(trackIDs []string, rating int) OnSetRating func(trackIDs []string, rating int)
OnDownload func(tracks []*mediaprovider.Track, downloadName string) OnDownload func(tracks []*mediaprovider.Track, downloadName string)
OnShare func(trackID string) OnShare func(trackID string)
OnPlaySongRadio func(track *mediaprovider.Track)
OnShowArtistPage func(artistID string) OnShowArtistPage func(artistID string)
OnShowAlbumPage func(albumID string) OnShowAlbumPage func(albumID string)
@@ -107,14 +111,15 @@ type Tracklist struct {
tracks []*util.TrackListModel tracks []*util.TrackListModel
tracksOrigOrder []*util.TrackListModel tracksOrigOrder []*util.TrackListModel
nowPlayingID string nowPlayingID string
colLayout *layouts.ColumnsLayout colLayout *layouts.ColumnsLayout
hdr *ListHeader hdr *ListHeader
list *FocusList list *FocusList
ctxMenu *fyne.Menu ctxMenu *fyne.Menu
ratingSubmenu *fyne.MenuItem ratingSubmenu *fyne.MenuItem
shareMenuItem *fyne.MenuItem shareMenuItem *fyne.MenuItem
container *fyne.Container songRadioMenuItem *fyne.MenuItem
container *fyne.Container
} }
func NewTracklist(tracks []*mediaprovider.Track) *Tracklist { func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
@@ -539,8 +544,12 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
} }
}) })
add.Icon = theme.ContentAddIcon() 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, t.ctxMenu.Items = append(t.ctxMenu.Items,
play, shuffle, add) play, shuffle, add, t.songRadioMenuItem)
} }
playlist := fyne.NewMenuItem("Add to playlist...", func() { playlist := fyne.NewMenuItem("Add to playlist...", func() {
if t.OnAddToPlaylist != nil { if t.OnAddToPlaylist != nil {
@@ -579,6 +588,7 @@ func (t *Tracklist) onShowContextMenu(e *fyne.PointEvent, trackIdx int) {
} }
t.ratingSubmenu.Disabled = t.Options.DisableRating t.ratingSubmenu.Disabled = t.Options.DisableRating
t.shareMenuItem.Disabled = t.Options.DisableSharing || len(t.selectedTracks()) != 1 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) 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 { func (t *Tracklist) selectedTracks() []*mediaprovider.Track {
t.tracksMutex.RLock() t.tracksMutex.RLock()
defer t.tracksMutex.RUnlock() defer t.tracksMutex.RUnlock()