Skip to content

Commit

Permalink
initial autoplay implementation - TODO: settings + UI control
Browse files Browse the repository at this point in the history
  • Loading branch information
dweymouth committed Jan 7, 2025
1 parent f1621fe commit 834d024
Showing 1 changed file with 96 additions and 2 deletions.
98 changes: 96 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: 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,
// 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 @@ -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 {
Expand Down

0 comments on commit 834d024

Please sign in to comment.