The seek bar was a setInterval counter reconciled only on track change: measured 3 s behind during steady playback and 30 s behind after four keyboard seeks, because the seek shortcut never told it. And `loadCurrentTrack`/`playCurrentTrack` logged, returned false and emitted nothing, so double-clicking a moved file did nothing, twice, forever — while auto-advance onto a bad file stopped playback dead. - A 1 Hz position ticker while playing, plus an immediate report on load, play, pause, seek and natural finish. The payload carries a `trackChangeId` (the store is a singleton, so a bar mounting later must not adopt a report about the previous track) and a `seq` (the same second reported twice still has to reset interpolation). - `PlaybackFailed` from both failure paths, and `playCurrentOrSkip` steps over tracks that will not load — bounded by the queue length, so a disconnected drive stops after one pass instead of spinning through a RepeatAll wrap. `PlayIndex` still reverts: the user picked that track. - `SeekFailed` is emitted when the seek itself fails, not only when nothing is loaded, and is followed by a position report so the optimistic move is taken back by the mechanism that fixed the drift. - A queue that simply ran out no longer unloads the player, so the finished track stays on the bar at 0:00.
56 lines
1.2 KiB
Go
56 lines
1.2 KiB
Go
package queue
|
|
|
|
// OnPlaybackFinished is called when a track finishes playing naturally.
|
|
// This drives the auto-advance behavior and records the play.
|
|
func (q *Queue) OnPlaybackFinished() {
|
|
q.mu.Lock()
|
|
|
|
if len(q.tracks) == 0 {
|
|
q.mu.Unlock()
|
|
|
|
return
|
|
}
|
|
|
|
// Capture the track that just finished before advancing.
|
|
finishedID := q.tracks[q.currentIndex].AudioFileID
|
|
|
|
// Repeat One: replay the current track.
|
|
if q.repeatMode == RepeatOne {
|
|
if q.playCurrentTrack() {
|
|
q.emitIndexChanged()
|
|
}
|
|
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
|
|
return
|
|
}
|
|
|
|
nextIdx := q.nextIndex()
|
|
if nextIdx == -1 {
|
|
// Queue exhausted — this is the extension point for a future fallback playlist.
|
|
q.onQueueExhausted(false)
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
|
|
return
|
|
}
|
|
|
|
q.currentIndex = nextIdx
|
|
|
|
// Skip over tracks that cannot be played instead of reverting.
|
|
// Reverting stopped playback dead on the first moved file and left
|
|
// Next unable to get past it, since Next hit the same track.
|
|
if !q.playCurrentOrSkip(true, q.nextIndex) {
|
|
q.onQueueExhausted(false)
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
|
|
return
|
|
}
|
|
|
|
q.emitIndexChanged()
|
|
q.mu.Unlock()
|
|
q.recordPlay(finishedID)
|
|
}
|