diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 5cd1acf..e568544 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -64,25 +64,26 @@ func NewPlaybackManager( // clamp to 99% to avoid any possible rounding issues scrobbleCfg.ThresholdPercent = clamp(scrobbleCfg.ThresholdPercent, 0, 99) pm := &PlaybackManager{ - ctx: ctx, - sm: s, - player: p, - scrobbleCfg: scrobbleCfg, - transcodeCfg: transcodeCfg, + ctx: ctx, + sm: s, + player: p, + scrobbleCfg: scrobbleCfg, + transcodeCfg: transcodeCfg, + nowPlayingIdx: -1, } - p.OnTrackChange(func(tracknum int) { - if tracknum >= len(pm.playQueue) { - return - } + p.OnTrackChange(func() { pm.checkScrobble() // scrobble the previous song if needed if pm.player.GetStatus().State == player.Playing { pm.playTimeStopwatch.Start() } - pm.nowPlayingIdx = tracknum + pm.nowPlayingIdx++ pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration) pm.sendNowPlayingScrobble() // Must come before invokeOnChangeCallbacks b/c track may immediately be scrobbled pm.invokeOnSongChangeCallbacks() pm.doUpdateTimePos() + if pm.nowPlayingIdx < len(pm.playQueue)-1 { + pm.setTrack(pm.nowPlayingIdx+1, true) + } }) p.OnSeek(func() { pm.doUpdateTimePos() @@ -95,6 +96,7 @@ func NewPlaybackManager( pm.doUpdateTimePos() pm.invokeOnSongChangeCallbacks() pm.invokeNoArgCallbacks(pm.onStopped) + pm.nowPlayingIdx = -1 }) p.OnPaused(func() { pm.playTimeStopwatch.Stop() @@ -205,32 +207,24 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool, sh func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, appendToQueue, shuffle bool) error { if !appendToQueue { p.player.Stop() - p.nowPlayingIdx = 0 + p.nowPlayingIdx = -1 p.playQueue = nil } nums := util.Range(len(tracks)) if shuffle { util.ShuffleSlice(nums) } + needToSetNext := appendToQueue && len(tracks) > 0 && p.nowPlayingIdx == len(p.playQueue)-1 for _, i := range nums { - - if urlP, ok := p.player.(player.URLPlayer); ok { - url, err := p.sm.Server.GetStreamURL(tracks[i].ID, p.transcodeCfg.ForceRawFile) - if err != nil { - return err - } - urlP.AppendFile(url) - } else if trP, ok := p.player.(player.TrackPlayer); ok { - trP.AppendTrack(tracks[i]) - } else { - panic("unsupported player type") - } // ensure a deep copy of the track info so that we can maintain our own state // (tracking play count increases, favorite, and rating) without messing up // other views' track models tr := *tracks[i] p.playQueue = append(p.playQueue, &tr) } + if needToSetNext { + p.setTrack(p.nowPlayingIdx+1, true) + } return nil } @@ -241,7 +235,7 @@ func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int, shuffle bool if p.replayGainCfg.Mode == ReplayGainAuto { p.SetReplayGainMode(player.ReplayGainAlbum) } - return p.player.PlayTrackAt(firstTrack) + return p.PlayTrackAt(firstTrack) } func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffle bool) error { @@ -251,7 +245,7 @@ func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffl if p.replayGainCfg.Mode == ReplayGainAuto { p.SetReplayGainMode(player.ReplayGainTrack) } - return p.player.PlayTrackAt(firstTrack) + return p.PlayTrackAt(firstTrack) } func (p *PlaybackManager) PlayTrack(trackID string) error { @@ -267,11 +261,15 @@ func (p *PlaybackManager) PlayTrack(trackID string) error { } func (p *PlaybackManager) PlayFromBeginning() error { - return p.player.PlayTrackAt(0) + return p.PlayTrackAt(0) } func (p *PlaybackManager) PlayTrackAt(idx int) error { - return p.player.PlayTrackAt(idx) + if idx < 0 || idx >= len(p.playQueue) { + return errors.New("track index out of range") + } + p.nowPlayingIdx = idx - 1 + return p.setTrack(idx, false) } func (p *PlaybackManager) PlayRandomSongs(genreName string) { @@ -325,7 +323,7 @@ func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) { func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) { newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIDs)) - rmCount := 0 + //rmCount := 0 idSet := sharedutil.ToSet(trackIDs) isPlayingTrackRemoved := false for i, tr := range p.playQueue { @@ -336,13 +334,14 @@ func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) { // If we are removing the currently playing track, we need to scrobble it p.checkScrobble() } - if err := p.player.RemoveTrackAt(i - rmCount); err == nil { - rmCount++ - } else { - log.Printf("error removing track: %v", err.Error()) - // did not remove this track - newQueue = append(newQueue, tr) - } + // if err := p.player.RemoveTrackAt(i - rmCount); err == nil { + // rmCount++ + //} else { + var err error + log.Printf("error removing track: %v", err.Error()) + // did not remove this track + newQueue = append(newQueue, tr) + //} } else { // not removing this track newQueue = append(newQueue, tr) @@ -359,9 +358,9 @@ func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) { // Stop playback and clear the play queue. func (p *PlaybackManager) StopAndClearPlayQueue() { p.player.Stop() - p.player.ClearPlayQueue() p.doUpdateTimePos() p.playQueue = nil + p.nowPlayingIdx = -1 } func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) { @@ -423,17 +422,17 @@ func (p *PlaybackManager) SetNextLoopMode() error { if err != nil { return err } - for _, cb := range p.onLoopModeChange { - cb(p.player.GetLoopMode()) - } + //for _, cb := range p.onLoopModeChange { + // cb(p.player.GetLoopMode()) + //} return nil } func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error { - if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil { - return err - } + //if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil { + // return err + //} for _, cb := range p.onLoopModeChange { cb(loopMode) @@ -443,7 +442,8 @@ func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error { } func (p *PlaybackManager) GetLoopMode() player.LoopMode { - return p.player.GetLoopMode() + return 0 + //return p.player.GetLoopMode() } func (p *PlaybackManager) PlayerStatus() player.Status { @@ -466,14 +466,17 @@ func (p *PlaybackManager) Volume() int { } func (p *PlaybackManager) SeekNext() error { - return p.player.SeekNext() + if p.CurrentPlayer().GetStatus().State == player.Stopped { + return nil + } + return p.PlayTrackAt(p.nowPlayingIdx + 1) } func (p *PlaybackManager) SeekBackOrPrevious() error { - if p.player.GetStatus().TimePos > 3 { + if p.nowPlayingIdx == 0 || p.player.GetStatus().TimePos > 3 { return p.player.SeekSeconds(0) } - return p.player.SeekPrevious() + return p.PlayTrackAt(p.nowPlayingIdx - 1) } // Seek to given absolute position in the current track by seconds. @@ -501,6 +504,9 @@ func (p *PlaybackManager) Pause() error { } func (p *PlaybackManager) Continue() error { + if p.player.GetStatus().State == player.Stopped { + return p.PlayFromBeginning() + } return p.player.Continue() } @@ -516,6 +522,25 @@ func (p *PlaybackManager) PlayPause() error { return errors.New("unreached - invalid player state") } +func (p *PlaybackManager) setTrack(idx int, next bool) error { + if urlP, ok := p.player.(player.URLPlayer); ok { + url, err := p.sm.Server.GetStreamURL(p.playQueue[idx].ID, p.transcodeCfg.ForceRawFile) + if err != nil { + return err + } + if next { + return urlP.SetNextFile(url) + } + return urlP.PlayFile(url) + } else if trP, ok := p.player.(player.TrackPlayer); ok { + if next { + return trP.SetNextTrack(p.playQueue[idx]) + } + return trP.PlayTrack(p.playQueue[idx]) + } + panic("Unsupported player type") +} + // call BEFORE updating p.nowPlayingIdx func (p *PlaybackManager) checkScrobble() { if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { diff --git a/player/mpv/player.go b/player/mpv/player.go index 9d99885..6d27318 100644 --- a/player/mpv/player.go +++ b/player/mpv/player.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "math" "strconv" @@ -58,9 +57,9 @@ type Player struct { haveRGainOpts bool audioExclusive bool status player.Status - loopMode player.LoopMode seeking bool curPlaylistPos int64 + lenPlaylist int64 prePausedState player.State clientName string equalizer Equalizer @@ -72,7 +71,7 @@ type Player struct { onStopped []func() onPlaying []func() onSeek []func() - onTrackChange []func(int) + onTrackChange []func() } // Returns a new player. @@ -133,38 +132,19 @@ func (p *Player) Init(maxCacheMB int) error { return nil } -// Appends the given file to the play queue. -// Note that the Player API does not provide methods to read -// the play queue. Clients are expected to maintain their own play queue model. -func (p *Player) AppendFile(url string) error { - log.Printf("Adding playback URL: %s", url) - if !p.initialized { - return ErrUnitialized - } - return p.mpv.Command([]string{"loadfile", url, "append"}) -} - // Plays the specified file, clearing the previous play queue, if any. func (p *Player) PlayFile(url string) error { - log.Printf("Adding playback URL: %s", url) if !p.initialized { return ErrUnitialized } err := p.mpv.Command([]string{"loadfile", url, "replace"}) if err == nil { p.setState(player.Playing) + p.lenPlaylist = 1 } return err } -// Removes the item at the given index from the internal playqueue. -func (p *Player) RemoveTrackAt(idx int) error { - if !p.initialized { - return ErrUnitialized - } - return p.mpv.Command([]string{"playlist-remove", strconv.Itoa(idx)}) -} - // Stops playback and clears the play queue. func (p *Player) Stop() error { if !p.initialized { @@ -180,30 +160,24 @@ func (p *Player) Stop() error { } } if err == nil { + p.lenPlaylist = 0 p.setState(player.Stopped) } return err } -// Clears the play queue, except for the currently playing file. -func (p *Player) ClearPlayQueue() error { - if !p.initialized { - return ErrUnitialized +func (p *Player) SetNextFile(url string) error { + if p.lenPlaylist > p.curPlaylistPos+1 { + if err := p.mpv.Command([]string{"playlist-remove", strconv.Itoa(int(p.curPlaylistPos) + 1)}); err != nil { + return err + } + p.lenPlaylist-- } - return p.mpv.Command([]string{"playlist-clear"}) -} - -func (p *Player) SetFile(url string) error { - if !p.initialized { - return ErrUnitialized + err := p.mpv.Command([]string{"loadfile", url, "append"}) + if err == nil { + p.lenPlaylist++ } - if err := p.mpv.Command([]string{"stop"}); err != nil { - return err - } - if err := p.mpv.Command([]string{"playlist-clear"}); err != nil { - return err - } - return p.mpv.Command([]string{"loadfile", url, "append"}) + return err } // Seeks within the currently playing track. @@ -218,27 +192,6 @@ func (p *Player) SeekSeconds(secs float64) error { return err } -// Seeks to the beginning of the previous track, -// or if no previous track, seeks to the beginning of the current track. -func (p *Player) SeekPrevious() error { - if !p.initialized { - return ErrUnitialized - } - - if pos, err := p.getInt64Property("playlist-pos"); err == nil && pos == 0 { - return p.SeekSeconds(0) - } - return p.mpv.Command([]string{"playlist-prev"}) -} - -// Seeks to the next track in the play queue, if any. -func (p *Player) SeekNext() error { - if !p.initialized { - return ErrUnitialized - } - return p.mpv.Command([]string{"playlist-next"}) -} - // Sets the volume of the player (0-100). // Unlike most Player functions, SetVolume can be called before Init, // to set the initial volume of the player on startup. @@ -325,22 +278,6 @@ func (p *Player) setPaused(paused bool) error { return err } -// Start playback from the specified track index in the play queue. -func (p *Player) PlayTrackAt(idx int) error { - // check if we have anything to play - if c, err := p.getInt64Property("playlist-count"); err == nil && c <= int64(idx) { - return nil - } - err := p.mpv.Command([]string{"playlist-play-index", strconv.Itoa(idx)}) - if p.GetStatus().State == player.Paused { - err = p.setPaused(false) - } - if err == nil { - p.setState(player.Playing) - } - return err -} - // Pause playback and update the player state func (p *Player) Pause() error { if p.status.State != player.Playing { @@ -362,57 +299,11 @@ func (p *Player) Continue() error { p.setState(p.prePausedState) } return err - } else if p.status.State == player.Stopped { - return p.PlayTrackAt(0) } return nil } -// Get the loop mode of the player. -func (p *Player) GetLoopMode() player.LoopMode { - return p.loopMode -} - -// Set the loop mode of the player. -func (p *Player) SetLoopMode(mode player.LoopMode) error { - if !p.initialized { - return ErrUnitialized - } - - // Return early if player is already in specified mode - if mode == p.loopMode { - return nil - } - - switch mode { - case player.LoopNone: - if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil { - return err - } - if err := p.mpv.SetOptionString("loop-file", "no"); err != nil { - return err - } - case player.LoopAll: - if err := p.mpv.SetOptionString("loop-playlist", "inf"); err != nil { - return err - } - if err := p.mpv.SetOptionString("loop-file", "no"); err != nil { - return err - } - case player.LoopOne: - if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil { - return err - } - if err := p.mpv.SetOptionString("loop-file", "inf"); err != nil { - return err - } - } - p.loopMode = mode - - return nil -} - // Get the current status of the player. func (p *Player) GetStatus() player.Status { if !p.initialized { @@ -539,7 +430,7 @@ func (p *Player) OnSeek(cb func()) { // Registers a callback which is invoked when the currently playing track changes, // or when playback begins at any time from the Stopped state. // Callback is invoked with the index of the currently playing track (zero-based). -func (p *Player) OnTrackChange(cb func(int)) { +func (p *Player) OnTrackChange(cb func()) { p.onTrackChange = append(p.onTrackChange, cb) } @@ -600,6 +491,7 @@ func (p *Player) eventHandler(ctx context.Context) { cb() } case mpv.EVENT_FILE_LOADED: + p.curPlaylistPos, _ = p.getInt64Property("playlist-pos") if p.status.State == player.Paused { // seek while paused switches to a new file // mpv does not fire seek event in this case @@ -607,11 +499,8 @@ func (p *Player) eventHandler(ctx context.Context) { cb() } } - if pos, err := p.getInt64Property("playlist-pos"); err == nil { - p.curPlaylistPos = pos - for _, cb := range p.onTrackChange { - cb(int(pos)) - } + for _, cb := range p.onTrackChange { + cb() } case mpv.EVENT_IDLE: p.status.Duration = 0 diff --git a/player/player.go b/player/player.go index a862715..b944904 100644 --- a/player/player.go +++ b/player/player.go @@ -4,57 +4,17 @@ import "github.com/dweymouth/supersonic/backend/mediaprovider" type URLPlayer interface { BasePlayer - AppendFile(url string) error -} - -type URLPlayerNew interface { - BasePlayer - SetFile(url string) - SetNextFile(url string) + PlayFile(url string) error + SetNextFile(url string) error } type TrackPlayer interface { BasePlayer - AppendTrack(track *mediaprovider.Track) error -} - -type TrackPlayerNew interface { - BasePlayer - SetTrack(track *mediaprovider.Track) error + PlayTrack(track *mediaprovider.Track) error SetNextTrack(track *mediaprovider.Track) error } type BasePlayer interface { - // Transport - PlayTrackAt(idx int) error - Continue() error - Pause() error - Stop() error - SeekPrevious() error - SeekNext() error - SeekSeconds(secs float64) error - IsSeeking() bool - - SetVolume(int) error - GetVolume() int - - GetStatus() Status - - ClearPlayQueue() error - RemoveTrackAt(idx int) error - - SetLoopMode(LoopMode) error - GetLoopMode() LoopMode - - // Event API - OnPaused(func()) - OnStopped(func()) - OnPlaying(func()) - OnSeek(func()) - OnTrackChange(func(int)) -} - -type BasePlayerNew interface { Continue() error Pause() error Stop() error @@ -72,7 +32,7 @@ type BasePlayerNew interface { OnStopped(func()) OnPlaying(func()) OnSeek(func()) - OnTrackChange(func(int)) + OnTrackChange(func()) } type ReplayGainPlayer interface {