From dfa493501d2994b6f58dc67a9ebb3f71194ee4c9 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sat, 19 Jul 2025 09:33:15 -0700 Subject: [PATCH] wait to set next track until near end of current. (still needs more testing/review) --- backend/playbackengine.go | 107 ++++++++++++++---------------- backend/player/dlna/dlnaplayer.go | 8 ++- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index cad0e6d..c2cc0ea 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -77,6 +77,11 @@ type playbackEngine struct { pendingPlayerChange bool pendingPlayerChangeStatus player.Status + // Whether we need to set the next track on the Player + // before the current track completes (normally when 10 seconds remain + // in the time pos polling function) + needToSetNextTrack bool + // to pass to onSongChange listeners; clear once listeners have been called lastScrobbled *mediaprovider.Track scrobbleCfg *ScrobbleConfig @@ -135,7 +140,7 @@ func NewPlaybackEngine( func (p *playbackEngine) registerPlayerCallbacks(pl player.BasePlayer) { pl.OnTrackChange(p.handleOnTrackChange) pl.OnSeek(func() { - p.doUpdateTimePos(true) + p.handleTimePosUpdate(true) p.invokeNoArgCallbacks(p.onSeek) }) pl.OnStopped(p.handleOnStopped) @@ -239,7 +244,10 @@ func (p *playbackEngine) NowPlayingIndex() int { func (p *playbackEngine) SetLoopMode(loopMode LoopMode) { p.loopMode = loopMode if p.nowPlayingIdx >= 0 { - p.setNextTrackBasedOnLoopMode(true) + // TODO - don't need when going from LoopNone to LoopAll + // if not on last track + p.needToSetNextTrack = true + //p.setNextTrackBasedOnLoopMode(true) } for _, cb := range p.onLoopModeChange { @@ -304,7 +312,7 @@ func (p *playbackEngine) SeekFwdBackN(n int) error { if idx == lastIdx && n > 0 { return nil // already on last track, nothing to seek next to } - newIdx := minInt(lastIdx, maxInt(0, idx+n)) + newIdx := min(lastIdx, max(0, idx+n)) return p.PlayTrackAt(newIdx) } @@ -360,7 +368,7 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu p.nowPlayingIdx = -1 p.playQueue = nil } - needToSetNext := len(items) > 0 && (insertQueueMode == InsertNext || (insertQueueMode == Append && p.nowPlayingIdx == len(p.playQueue)-1)) + p.needToSetNextTrack = len(items) > 0 && (insertQueueMode == InsertNext || (insertQueueMode == Append && p.nowPlayingIdx == len(p.playQueue)-1)) if shuffle { rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) @@ -372,9 +380,9 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu } p.playQueue = append(p.playQueue[:insertIdx], append(items, p.playQueue[insertIdx:]...)...) - if needToSetNext { - p.setNextTrack(p.nowPlayingIdx + 1) - } + //if needToSetNext { + // p.setNextTrack(p.nowPlayingIdx + 1) + //} p.invokeNoArgCallbacks(p.onQueueChange) return nil @@ -386,7 +394,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins p.nowPlayingIdx = -1 p.playQueue = nil } - needToSetNext := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == len(p.playQueue)-1) + p.needToSetNextTrack = insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == len(p.playQueue)-1) insertIdx := len(p.playQueue) if insertMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 @@ -398,9 +406,9 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:]) p.playQueue = new - if needToSetNext { - p.setNextTrack(p.nowPlayingIdx + 1) - } + //if needToSetNext { + // p.setNextTrack(p.nowPlayingIdx + 1) + //} p.invokeNoArgCallbacks(p.onQueueChange) } @@ -460,11 +468,11 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return p.Stop() } - needToUpdateNext := p.nowPlayingIdx >= 0 + p.needToSetNextTrack = p.nowPlayingIdx >= 0 p.nowPlayingIdx = newNowPlayingIdx - if needToUpdateNext { - p.setNextTrackAfterQueueUpdate() - } + //if needToUpdateNext { + // p.setNextTrackBasedOnLoopMode(false) + //} p.invokeNoArgCallbacks(p.onQueueChange) return nil @@ -510,7 +518,8 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { // when we receive new track event from player } else if isNextPlayingTrackremoved { if newNowPlaying < len(newQueue)-1 { - p.setNextTrack(p.nowPlayingIdx + 1) + p.needToSetNextTrack = true + //p.setNextTrack(p.nowPlayingIdx + 1) } else { // no next track to play p.setNextTrack(-1) @@ -606,7 +615,7 @@ func (p *playbackEngine) handleOnTrackChange() { p.curTrackDuration = float64(nowPlaying.Metadata().Duration) p.sendNowPlayingScrobble() // Must come before invokeOnChangeCallbacks b/c track may immediately be scrobbled p.invokeOnSongChangeCallbacks() - p.doUpdateTimePos(false) + p.handleTimePosUpdate(false) p.cacheNextTracks() p.setNextTrackBasedOnLoopMode(false) } @@ -617,7 +626,7 @@ func (p *playbackEngine) handleOnStopped() { p.checkScrobble() } p.stopPollTimePos() - p.doUpdateTimePos(false) + p.handleTimePosUpdate(false) p.invokeOnSongChangeCallbacks() p.invokeNoArgCallbacks(p.onStopped) p.alreadyScrobbled = false @@ -646,32 +655,23 @@ func (p *playbackEngine) setNextTrackBasedOnLoopMode(onLoopModeChange bool) { } } -func (p *playbackEngine) setNextTrackAfterQueueUpdate() { - switch p.loopMode { - case LoopNone: - if p.nowPlayingIdx < len(p.playQueue)-1 { - p.setNextTrack(p.nowPlayingIdx + 1) - } else { - // need to erase next track - p.setNextTrack(-1) - } - case LoopOne: - p.setNextTrack(p.nowPlayingIdx) - case LoopAll: - if p.nowPlayingIdx >= len(p.playQueue)-1 { - p.setNextTrack(0) - } else { - p.setNextTrack(p.nowPlayingIdx + 1) - } - } -} - func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { if urlP, ok := p.player.(player.URLPlayer); ok { url := "" var meta mediaprovider.MediaItemMetadata if idx >= 0 { - url = p.getMediaURLForIdx(idx) + item := p.playQueue[idx] + track, isTrack := item.(*mediaprovider.Track) + meta = item.Metadata() + if isTrack && p.audiocache != nil { + if filepath := p.audiocache.PathForCachedFile(track.ID); filepath != "" { + url = filepath + log.Println("playing file from cache") + } + } + if url == "" { + url = p.getMediaURLForIdx(idx) + } if url == "" { return errors.New("no stream URL") } @@ -815,7 +815,7 @@ func (p *playbackEngine) startPollTimePos() { pollingTick.Stop() return case <-pollingTick.C: - p.doUpdateTimePos(false) + p.handleTimePosUpdate(false) } } }() @@ -828,11 +828,20 @@ func (p *playbackEngine) stopPollTimePos() { } } -func (p *playbackEngine) doUpdateTimePos(seeked bool) { +func (p *playbackEngine) handleTimePosUpdate(seeked bool) { + s := p.PlaybackStatus() + var meta mediaprovider.MediaItemMetadata + if np := p.NowPlaying(); np != nil { + meta = np.Metadata() + } + isNearEnd := meta.Type != mediaprovider.MediaItemTypeRadioStation && s.TimePos > float64(meta.Duration)-10 + if p.needToSetNextTrack && isNearEnd { + p.needToSetNextTrack = false + p.setNextTrackBasedOnLoopMode(false) + } if p.callbacksDisabled { return } - s := p.PlaybackStatus() if s.TimePos > p.latestTrackPosition { p.latestTrackPosition = s.TimePos } @@ -845,17 +854,3 @@ func (p *playbackEngine) doUpdateTimePos(seeked bool) { cb(s.TimePos, duration, seeked) } } - -func minInt(a, b int) int { - if a < b { - return a - } - return b -} - -func maxInt(a, b int) int { - if a > b { - return a - } - return b -} diff --git a/backend/player/dlna/dlnaplayer.go b/backend/player/dlna/dlnaplayer.go index bbd9e2b..60aaf23 100644 --- a/backend/player/dlna/dlnaplayer.go +++ b/backend/player/dlna/dlnaplayer.go @@ -559,7 +559,13 @@ func (d *DLNAPlayer) handleRequest(w http.ResponseWriter, r *http.Request) { return } - // Create a new request to the target server + // if the url is a filepath for a local cached file, serve it + if info, err := os.Stat(url); err == nil && info.Size() > 0 { + http.ServeFile(w, r, url) + return + } + + // Otherwise, proxy request to the music server proxyReq, err := http.NewRequest(r.Method, url, r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)