Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add autoplay mode to queue similar tracks when nearing end of play queue #526

Merged
merged 3 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions backend/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type NowPlayingPageConfig struct {
}

type PlaybackConfig struct {
Autoplay bool
RepeatMode string
}

Expand Down Expand Up @@ -215,6 +216,7 @@ func DefaultConfig(appVersionTag string) *Config {
TracklistColumns: []string{"Album", "Time", "Plays"},
},
Playback: PlaybackConfig{
Autoplay: false,
RepeatMode: "None",
},
LocalPlayback: LocalPlaybackConfig{
Expand Down
109 changes: 107 additions & 2 deletions backend/playbackmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,6 +22,8 @@ type PlaybackManager struct {
cmdQueue *playbackCommandQueue
cfg *AppConfig

autoplay bool

lastPlayTime float64
}

Expand All @@ -37,21 +42,32 @@ func NewPlaybackManager(
engine: e,
cmdQueue: q,
cfg: appCfg,
autoplay: playbackCfg.Autoplay,
}
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,
// send a command to the MPV player to force restart playback.
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() {
Expand Down Expand Up @@ -348,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()
}
Expand All @@ -356,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()
}
Expand Down Expand Up @@ -419,6 +446,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 {
Expand Down
5 changes: 5 additions & 0 deletions res/bundled.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions res/bundled_gen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions res/icons/publicdomain/infinity.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions res/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"Audiobook": "Audiobook",
"Authentication failed": "Authentication failed",
"Auto": "Auto",
"Autoplay": "Autoplay",
"Autoselect device": "Autoselect device",
"Back": "Back",
"Bit rate": "Bit rate",
Expand Down
5 changes: 4 additions & 1 deletion ui/bottompanel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions ui/mainwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions ui/theme/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 23 additions & 2 deletions ui/widgets/auxcontrols.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.autoplay.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"))
Expand All @@ -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(),
),
)
Expand Down Expand Up @@ -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
}
Expand Down
Loading