initial autoplay implementation - TODO: settings + UI control
This commit is contained in:
@@ -5,11 +5,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
"github.com/dweymouth/supersonic/backend/player"
|
"github.com/dweymouth/supersonic/backend/player"
|
||||||
"github.com/dweymouth/supersonic/backend/player/mpv"
|
"github.com/dweymouth/supersonic/backend/player/mpv"
|
||||||
|
"github.com/dweymouth/supersonic/sharedutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A high-level MediaProvider-aware playback engine, serves as an
|
// A high-level MediaProvider-aware playback engine, serves as an
|
||||||
@@ -19,6 +22,8 @@ type PlaybackManager struct {
|
|||||||
cmdQueue *playbackCommandQueue
|
cmdQueue *playbackCommandQueue
|
||||||
cfg *AppConfig
|
cfg *AppConfig
|
||||||
|
|
||||||
|
autoplay bool
|
||||||
|
|
||||||
lastPlayTime float64
|
lastPlayTime float64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,13 +42,14 @@ func NewPlaybackManager(
|
|||||||
engine: e,
|
engine: e,
|
||||||
cmdQueue: q,
|
cmdQueue: q,
|
||||||
cfg: appCfg,
|
cfg: appCfg,
|
||||||
|
autoplay: true, // TODO
|
||||||
}
|
}
|
||||||
pm.workaroundWindowsPlaybackIssue()
|
pm.addOnTrackChangeHook()
|
||||||
go pm.runCmdQueue(ctx)
|
go pm.runCmdQueue(ctx)
|
||||||
return pm
|
return pm
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) workaroundWindowsPlaybackIssue() {
|
func (p *PlaybackManager) addOnTrackChangeHook() {
|
||||||
// See https://github.com/dweymouth/supersonic/issues/483
|
// See https://github.com/dweymouth/supersonic/issues/483
|
||||||
// On Windows, MPV sometimes fails to start playback when switching to a track
|
// 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,
|
// 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.OnPlayTimeUpdate(func(curTime, _ float64, _ bool) {
|
||||||
p.lastPlayTime = curTime
|
p.lastPlayTime = curTime
|
||||||
})
|
})
|
||||||
|
|
||||||
p.OnSongChange(func(mediaprovider.MediaItem, *mediaprovider.Track) {
|
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 {
|
if p.NowPlayingIndex() != len(p.engine.playQueue) && p.PlayerStatus().State == player.Playing {
|
||||||
p.lastPlayTime = 0
|
p.lastPlayTime = 0
|
||||||
go func() {
|
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) {
|
func (p *PlaybackManager) runCmdQueue(ctx context.Context) {
|
||||||
logIfErr := func(action string, err error) {
|
logIfErr := func(action string, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user