@@ -5,11 +5,14 @@ import (
55 "fmt"
66 "log"
77 "math/rand"
8+ "runtime"
9+ "slices"
810 "time"
911
1012 "github.com/dweymouth/supersonic/backend/mediaprovider"
1113 "github.com/dweymouth/supersonic/backend/player"
1214 "github.com/dweymouth/supersonic/backend/player/mpv"
15+ "github.com/dweymouth/supersonic/sharedutil"
1316)
1417
1518// A high-level MediaProvider-aware playback engine, serves as an
@@ -19,6 +22,8 @@ type PlaybackManager struct {
1922 cmdQueue * playbackCommandQueue
2023 cfg * AppConfig
2124
25+ autoplay bool
26+
2227 lastPlayTime float64
2328}
2429
@@ -37,21 +42,32 @@ func NewPlaybackManager(
3742 engine : e ,
3843 cmdQueue : q ,
3944 cfg : appCfg ,
45+ autoplay : true , // TODO
4046 }
41- pm .workaroundWindowsPlaybackIssue ()
47+ pm .addOnTrackChangeHook ()
4248 go pm .runCmdQueue (ctx )
4349 return pm
4450}
4551
46- func (p * PlaybackManager ) workaroundWindowsPlaybackIssue () {
52+ func (p * PlaybackManager ) addOnTrackChangeHook () {
4753 // See https://github.com/dweymouth/supersonic/issues/483
4854 // On Windows, MPV sometimes fails to start playback when switching to a track
4955 // with a different sample rate than the previous. If this is detected,
5056 // send a command to the MPV player to force restart playback.
5157 p .OnPlayTimeUpdate (func (curTime , _ float64 , _ bool ) {
5258 p .lastPlayTime = curTime
5359 })
60+
5461 p .OnSongChange (func (mediaprovider.MediaItem , * mediaprovider.Track ) {
62+ // Autoplay if enabled and we are on the last track
63+ if p .autoplay && p .NowPlayingIndex () == len (p .engine .playQueue )- 1 {
64+ p .enqueueAutoplayTracks ()
65+ }
66+
67+ if runtime .GOOS != "windows" {
68+ return
69+ }
70+ // workaround for https://github.com/dweymouth/supersonic/issues/483 (see above comment)
5571 if p .NowPlayingIndex () != len (p .engine .playQueue ) && p .PlayerStatus ().State == player .Playing {
5672 p .lastPlayTime = 0
5773 go func () {
@@ -419,6 +435,84 @@ func (p *PlaybackManager) PlayPause() {
419435 }
420436}
421437
438+ func (p * PlaybackManager ) enqueueAutoplayTracks () {
439+ nowPlaying := p .NowPlaying ()
440+ if nowPlaying == nil {
441+ return
442+ }
443+
444+ s := p .engine .sm .Server
445+ if s == nil {
446+ return
447+ }
448+
449+ // last 500 played items
450+ queue := p .GetPlayQueue ()
451+ if l := len (queue ); l > 500 {
452+ queue = queue [l - 500 :]
453+ }
454+
455+ // tracks we will enqueue
456+ var tracks []* mediaprovider.Track
457+
458+ filterRecentlyPlayed := func (tracks []* mediaprovider.Track ) []* mediaprovider.Track {
459+ return sharedutil .FilterSlice (tracks , func (t * mediaprovider.Track ) bool {
460+ return ! slices .ContainsFunc (queue , func (i mediaprovider.MediaItem ) bool {
461+ return i .Metadata ().Type == mediaprovider .MediaItemTypeTrack && i .Metadata ().ID == t .ID
462+ })
463+ })
464+ }
465+
466+ // since this func is invoked in a callback from the playback engine,
467+ // need to do the rest async as it may take time and block other callbacks
468+ go func () {
469+ // first 2 strategies - similar by artist, and similar by genres - only work for tracks
470+ if nowPlaying .Metadata ().Type == mediaprovider .MediaItemTypeTrack {
471+ tr := nowPlaying .(* mediaprovider.Track )
472+
473+ // similar tracks by artist
474+ if len (tr .ArtistIDs ) > 0 {
475+ similar , err := s .GetSimilarTracks (tr .ArtistIDs [0 ], p .cfg .EnqueueBatchSize )
476+ if err != nil {
477+ log .Println ("autoplay error: failed to get similar tracks: %v" , err )
478+ }
479+ tracks = filterRecentlyPlayed (similar )
480+ }
481+
482+ // fallback to random tracks from genre
483+ if len (tracks ) == 0 {
484+ for _ , g := range tr .Genres {
485+ if g == "" {
486+ continue
487+ }
488+ byGenre , err := s .GetRandomTracks (g , p .cfg .EnqueueBatchSize )
489+ if err != nil {
490+ log .Println ("autoplay error: failed to get tracks by genre: %v" , err )
491+ }
492+ tracks = filterRecentlyPlayed (byGenre )
493+ if len (tracks ) > 0 {
494+ break
495+ }
496+ }
497+ }
498+ }
499+
500+ // random tracks works regardless of the type of the last playing media
501+ if len (tracks ) == 0 {
502+ // fallback to random tracks
503+ random , err := s .GetRandomTracks ("" , p .cfg .EnqueueBatchSize )
504+ if err != nil {
505+ log .Println ("autoplay error: failed to get random tracks: %v" , err )
506+ }
507+ tracks = filterRecentlyPlayed (random )
508+ }
509+
510+ if len (tracks ) > 0 {
511+ p .LoadTracks (tracks , Append , false /*no need to shuffle, already random*/ )
512+ }
513+ }()
514+ }
515+
422516func (p * PlaybackManager ) runCmdQueue (ctx context.Context ) {
423517 logIfErr := func (action string , err error ) {
424518 if err != nil {
0 commit comments