From 834d0247356441f7ff5762b79401563f303c7b51 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 6 Jan 2025 17:23:36 -0800 Subject: [PATCH 1/3] initial autoplay implementation - TODO: settings + UI control --- backend/playbackmanager.go | 98 +++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 15d537b2..a175064d 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -5,11 +5,14 @@ import ( "fmt" "log" "math/rand" + "runtime" + "slices" "time" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player/mpv" + "github.com/dweymouth/supersonic/sharedutil" ) // A high-level MediaProvider-aware playback engine, serves as an @@ -19,6 +22,8 @@ type PlaybackManager struct { cmdQueue *playbackCommandQueue cfg *AppConfig + autoplay bool + lastPlayTime float64 } @@ -37,13 +42,14 @@ func NewPlaybackManager( engine: e, cmdQueue: q, cfg: appCfg, + autoplay: true, // TODO } - pm.workaroundWindowsPlaybackIssue() + pm.addOnTrackChangeHook() go pm.runCmdQueue(ctx) return pm } -func (p *PlaybackManager) workaroundWindowsPlaybackIssue() { +func (p *PlaybackManager) addOnTrackChangeHook() { // See https://github.com/dweymouth/supersonic/issues/483 // On Windows, MPV sometimes fails to start playback when switching to a track // with a different sample rate than the previous. If this is detected, @@ -51,7 +57,17 @@ func (p *PlaybackManager) workaroundWindowsPlaybackIssue() { p.OnPlayTimeUpdate(func(curTime, _ float64, _ bool) { p.lastPlayTime = curTime }) + p.OnSongChange(func(mediaprovider.MediaItem, *mediaprovider.Track) { + // Autoplay if enabled and we are on the last track + if p.autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + p.enqueueAutoplayTracks() + } + + if runtime.GOOS != "windows" { + return + } + // workaround for https://github.com/dweymouth/supersonic/issues/483 (see above comment) if p.NowPlayingIndex() != len(p.engine.playQueue) && p.PlayerStatus().State == player.Playing { p.lastPlayTime = 0 go func() { @@ -419,6 +435,84 @@ func (p *PlaybackManager) PlayPause() { } } +func (p *PlaybackManager) enqueueAutoplayTracks() { + nowPlaying := p.NowPlaying() + if nowPlaying == nil { + return + } + + s := p.engine.sm.Server + if s == nil { + return + } + + // last 500 played items + queue := p.GetPlayQueue() + if l := len(queue); l > 500 { + queue = queue[l-500:] + } + + // tracks we will enqueue + var tracks []*mediaprovider.Track + + filterRecentlyPlayed := func(tracks []*mediaprovider.Track) []*mediaprovider.Track { + return sharedutil.FilterSlice(tracks, func(t *mediaprovider.Track) bool { + return !slices.ContainsFunc(queue, func(i mediaprovider.MediaItem) bool { + return i.Metadata().Type == mediaprovider.MediaItemTypeTrack && i.Metadata().ID == t.ID + }) + }) + } + + // since this func is invoked in a callback from the playback engine, + // need to do the rest async as it may take time and block other callbacks + go func() { + // first 2 strategies - similar by artist, and similar by genres - only work for tracks + if nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeTrack { + tr := nowPlaying.(*mediaprovider.Track) + + // similar tracks by artist + if len(tr.ArtistIDs) > 0 { + similar, err := s.GetSimilarTracks(tr.ArtistIDs[0], p.cfg.EnqueueBatchSize) + if err != nil { + log.Println("autoplay error: failed to get similar tracks: %v", err) + } + tracks = filterRecentlyPlayed(similar) + } + + // fallback to random tracks from genre + if len(tracks) == 0 { + for _, g := range tr.Genres { + if g == "" { + continue + } + byGenre, err := s.GetRandomTracks(g, p.cfg.EnqueueBatchSize) + if err != nil { + log.Println("autoplay error: failed to get tracks by genre: %v", err) + } + tracks = filterRecentlyPlayed(byGenre) + if len(tracks) > 0 { + break + } + } + } + } + + // random tracks works regardless of the type of the last playing media + if len(tracks) == 0 { + // fallback to random tracks + random, err := s.GetRandomTracks("", p.cfg.EnqueueBatchSize) + if err != nil { + log.Println("autoplay error: failed to get random tracks: %v", err) + } + tracks = filterRecentlyPlayed(random) + } + + if len(tracks) > 0 { + p.LoadTracks(tracks, Append, false /*no need to shuffle, already random*/) + } + }() +} + func (p *PlaybackManager) runCmdQueue(ctx context.Context) { logIfErr := func(action string, err error) { if err != nil { From 966e792f6f8197bb3411766a8a3e8d0835b21e0c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 6 Jan 2025 18:34:35 -0800 Subject: [PATCH 2/3] add autoplay UI toggle --- backend/app.go | 1 + backend/config.go | 2 ++ backend/playbackmanager.go | 13 ++++++++++++- res/bundled.go | 5 +++++ res/bundled_gen.sh | 1 + res/translations/en.json | 1 + ui/bottompanel.go | 5 ++++- ui/mainwindow.go | 1 + ui/theme/theme.go | 1 + ui/widgets/auxcontrols.go | 25 +++++++++++++++++++++++-- 10 files changed, 51 insertions(+), 4 deletions(-) diff --git a/backend/app.go b/backend/app.go index 39461148..06f1fc96 100644 --- a/backend/app.go +++ b/backend/app.go @@ -361,6 +361,7 @@ func (a *App) Shutdown() { repeatMode = "All" } a.Config.Playback.RepeatMode = repeatMode + a.Config.Playback.Autoplay = a.PlaybackManager.IsAutoplay() a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume() a.SavePlayQueueIfEnabled() a.SaveConfigFile() diff --git a/backend/config.go b/backend/config.go index 41626fcb..6b88f42b 100644 --- a/backend/config.go +++ b/backend/config.go @@ -99,6 +99,7 @@ type NowPlayingPageConfig struct { } type PlaybackConfig struct { + Autoplay bool RepeatMode string } @@ -215,6 +216,7 @@ func DefaultConfig(appVersionTag string) *Config { TracklistColumns: []string{"Album", "Time", "Plays"}, }, Playback: PlaybackConfig{ + Autoplay: false, RepeatMode: "None", }, LocalPlayback: LocalPlaybackConfig{ diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index a175064d..6d985044 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -42,7 +42,7 @@ func NewPlaybackManager( engine: e, cmdQueue: q, cfg: appCfg, - autoplay: true, // TODO + autoplay: playbackCfg.Autoplay, } pm.addOnTrackChangeHook() go pm.runCmdQueue(ctx) @@ -364,6 +364,10 @@ func (p *PlaybackManager) GetLoopMode() LoopMode { return p.engine.loopMode } +func (p *PlaybackManager) IsAutoplay() bool { + return p.autoplay +} + func (p *PlaybackManager) PlayerStatus() player.Status { return p.engine.PlayerStatus() } @@ -372,6 +376,13 @@ func (p *PlaybackManager) SetVolume(vol int) { p.cmdQueue.SetVolume(vol) } +func (p *PlaybackManager) SetAutoplay(autoplay bool) { + p.autoplay = autoplay + if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + p.enqueueAutoplayTracks() + } +} + func (p *PlaybackManager) Volume() int { return p.engine.CurrentPlayer().GetVolume() } diff --git a/res/bundled.go b/res/bundled.go index b0806e45..dfb3f955 100644 --- a/res/bundled.go +++ b/res/bundled.go @@ -40,6 +40,11 @@ var ResHeartOutlineSvg = &fyne.StaticResource{ StaticContent: []byte( ""), } +var ResInfinitySvg = &fyne.StaticResource{ + StaticName: "infinity.svg", + StaticContent: []byte( + "\n\n \n"), +} var ResMusicnotesSvg = &fyne.StaticResource{ StaticName: "musicnotes.svg", StaticContent: []byte( diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh index aa21d2ea..a963c502 100755 --- a/res/bundled_gen.sh +++ b/res/bundled_gen.sh @@ -7,6 +7,7 @@ fyne bundle -append -prefix Res icons/publicdomain/disc.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/headphones.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/heart-filled.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/heart-outline.svg >> bundled.go +fyne bundle -append -prefix Res icons/publicdomain/infinity.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/musicnotes.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/people.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/playlist.svg >> bundled.go diff --git a/res/translations/en.json b/res/translations/en.json index bf7b4380..f5f8a853 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -28,6 +28,7 @@ "Audiobook": "Audiobook", "Authentication failed": "Authentication failed", "Auto": "Auto", + "Autoplay": "Autoplay", "Autoselect device": "Autoselect device", "Back": "Back", "Bit rate": "Bit rate", diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 5851ffc9..09255710 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -102,7 +102,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr pm.SeekFraction(f) }) - bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode()) + bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay()) pm.OnLoopModeChange(bp.AuxControls.SetLoopMode) pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume) bp.AuxControls.VolumeControl.OnSetVolume = func(v int) { @@ -111,6 +111,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr bp.AuxControls.OnChangeLoopMode(func() { pm.SetNextLoopMode() }) + bp.AuxControls.OnChangeAutoplay = func(autoplay bool) { + pm.SetAutoplay(autoplay) + } bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue) bp.imageLoader = util.NewThumbnailLoader(im, bp.NowPlaying.SetImage) diff --git a/ui/mainwindow.go b/ui/mainwindow.go index c8f1f89a..f6fbaa98 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -160,6 +160,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, repeatMode = "All" } app.Config.Playback.RepeatMode = repeatMode + app.Config.Playback.Autoplay = app.PlaybackManager.IsAutoplay() app.SavePlayQueueIfEnabled() app.SaveConfigFile() diff --git a/ui/theme/theme.go b/ui/theme/theme.go index 5f92cbce..a9a1bcc0 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -38,6 +38,7 @@ var ( AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg) ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg) + AutoplayIcon fyne.Resource = theme.NewThemedResource(res.ResInfinitySvg) RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg) FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg) NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg) diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index 582657d2..b5f7a1d7 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -18,21 +18,34 @@ import ( type AuxControls struct { widget.BaseWidget + OnChangeAutoplay func(autoplay bool) + VolumeControl *VolumeControl + autoplay *IconButton loop *IconButton showQueue *IconButton container *fyne.Container } -func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode) *AuxControls { +func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool) *AuxControls { a := &AuxControls{ VolumeControl: NewVolumeControl(initialVolume), + autoplay: NewIconButton(myTheme.AutoplayIcon, nil), loop: NewIconButton(myTheme.RepeatIcon, nil), showQueue: NewIconButton(myTheme.PlayQueueIcon, nil), } a.loop.IconSize = IconButtonSizeSmaller a.loop.SetToolTip(lang.L("Repeat")) + a.loop.Highlighted = initialAutoplay + //a.autoplay.IconSize = IconButtonSizeSmaller + a.autoplay.SetToolTip(lang.L("Autoplay")) + a.autoplay.OnTapped = func() { + a.SetAutoplay(!a.autoplay.Highlighted) + if a.OnChangeAutoplay != nil { + a.OnChangeAutoplay(a.autoplay.Highlighted) + } + } a.SetLoopMode(initialLoopMode) a.showQueue.IconSize = IconButtonSizeSmaller a.showQueue.SetToolTip(lang.L("Show play queue")) @@ -43,7 +56,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode) *AuxCon a.VolumeControl, container.New( layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5), - layout.NewSpacer(), a.loop, a.showQueue, util.NewHSpace(5)), + layout.NewSpacer(), a.autoplay, a.loop, a.showQueue, util.NewHSpace(5)), layout.NewSpacer(), ), ) @@ -73,6 +86,14 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) { } } +func (a *AuxControls) SetAutoplay(autoplay bool) { + if autoplay == a.autoplay.Highlighted { + return + } + a.autoplay.Highlighted = autoplay + a.autoplay.Refresh() +} + func (a *AuxControls) OnShowPlayQueue(f func()) { a.showQueue.OnTapped = f } From a6a82571b25839840ca419210c21d6f6c0a062f5 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 6 Jan 2025 19:19:59 -0800 Subject: [PATCH 3/3] bug fix and add infinity svg --- res/icons/publicdomain/infinity.svg | 4 ++++ ui/widgets/auxcontrols.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 res/icons/publicdomain/infinity.svg diff --git a/res/icons/publicdomain/infinity.svg b/res/icons/publicdomain/infinity.svg new file mode 100644 index 00000000..cb714116 --- /dev/null +++ b/res/icons/publicdomain/infinity.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index b5f7a1d7..1074a195 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -37,7 +37,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial } a.loop.IconSize = IconButtonSizeSmaller a.loop.SetToolTip(lang.L("Repeat")) - a.loop.Highlighted = initialAutoplay + a.autoplay.Highlighted = initialAutoplay //a.autoplay.IconSize = IconButtonSizeSmaller a.autoplay.SetToolTip(lang.L("Autoplay")) a.autoplay.OnTapped = func() {