Skip to content

Commit 834d024

Browse files
committed
initial autoplay implementation - TODO: settings + UI control
1 parent f1621fe commit 834d024

File tree

1 file changed

+96
-2
lines changed

1 file changed

+96
-2
lines changed

backend/playbackmanager.go

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
422516
func (p *PlaybackManager) runCmdQueue(ctx context.Context) {
423517
logIfErr := func(action string, err error) {
424518
if err != nil {

0 commit comments

Comments
 (0)