diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 15d537b..a175064 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 {