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.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
}
type SupportsSongRadio interface {
GetSongRadio(trackID string, count int) ([]*Track, error)
}
type LyricsProvider interface {
GetLyrics(track *Track) (*Lyrics, error)
}
@@ -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
}
+5
View File
@@ -80,6 +80,11 @@ var ResFilterSvg = &fyne.StaticResource{
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"),
}
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{
StaticName: "repeat.svg",
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/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
+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)
_, 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
}
+2
View File
@@ -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) {
+2
View File
@@ -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) {
+4 -2
View File
@@ -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,
DisableRating: !canRate,
DisableSharing: !canShare,
DisableSongRadio: !canSongRadio,
AuxiliaryMenuItems: []*fyne.MenuItem{
util.NewReorderTracksSubmenu(a.doSetNewTrackOrder),
remove,
+19 -15
View File
@@ -34,13 +34,14 @@ 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
}
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.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,
AutoNumber: true,
DisableSorting: true,
DisableRating: !t.canRate,
DisableSharing: !t.canShare,
DisableSongRadio: !t.canSongRadio,
AutoNumber: true,
}
t.tracklist.SetVisibleColumns(conf.TracklistColumns)
t.tracklist.OnVisibleColumnsChanged = func(cols []string) {
@@ -137,10 +140,11 @@ 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)
t.searchTracklist.SetNowPlaying(t.nowPlayingID)
+34
View File
@@ -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.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) {
@@ -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: %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 (
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)
+27 -9
View File
@@ -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)
@@ -107,14 +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
container *fyne.Container
nowPlayingID string
colLayout *layouts.ColumnsLayout
hdr *ListHeader
list *FocusList
ctxMenu *fyne.Menu
ratingSubmenu *fyne.MenuItem
shareMenuItem *fyne.MenuItem
songRadioMenuItem *fyne.MenuItem
container *fyne.Container
}
func NewTracklist(tracks []*mediaprovider.Track) *Tracklist {
@@ -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()