8.4 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-14 | 14 | execute | 1 |
|
true |
|
Purpose: Ensure the queue index always reflects the track the player actually has loaded. If a track load fails, roll back the index to its previous value and do not emit QueueIndexChanged.
Output: Patched queue.go and handlers.go with roll-back-on-failure semantics.
<execution_context> @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md </execution_context>
@.planning/STATE.md @backend/queue/queue.go @backend/queue/handlers.go @backend/queue/navigation.goFrom backend/queue/queue.go (lines 1115-1182):
// Currently returns nothing — needs to return bool
func (q *Queue) playOrLoadCurrentTrack(autoPlay bool) {
if autoPlay {
q.playCurrentTrack()
} else {
q.loadCurrentTrack()
}
}
// Already returns bool
func (q *Queue) loadCurrentTrack() bool { ... }
// Currently returns nothing — needs to return bool
func (q *Queue) playCurrentTrack() { ... }
From backend/queue/queue.go (lines 871-945):
// Next() — unconditionally emits after advancing index
func (q *Queue) Next() {
q.currentIndex = nextIdx
q.playOrLoadCurrentTrack(wasPlaying) // return value discarded
q.emitIndexChanged() // always fires
}
// Previous() — same pattern, multiple paths
func (q *Queue) Previous() {
// ... restart paths also call playOrLoadCurrentTrack without checking
q.currentIndex = prevIdx
q.playOrLoadCurrentTrack(wasPlaying)
q.emitIndexChanged()
}
From backend/queue/handlers.go (lines 1-33):
func (q *Queue) OnPlaybackFinished() {
q.currentIndex = nextIdx
q.playCurrentTrack() // return value ignored (void)
q.emitIndexChanged() // always fires
}
func (q *Queue) playOrLoadCurrentTrack(autoPlay bool) bool {
if autoPlay {
return q.playCurrentTrack()
}
return q.loadCurrentTrack()
}
Change playCurrentTrack to return bool:
func (q *Queue) playCurrentTrack() bool {
if !q.loadCurrentTrack() {
return false
}
err := q.player.Play()
if err != nil {
track := q.tracks[q.currentIndex]
q.logger.Error(
"Failed to play file from queue",
"filePath", track.FilePath, "err", err,
)
return false
}
return true
}
Update the doc comment on playOrLoadCurrentTrack to document the bool return value (true = success, false = load failed).
Update the doc comment on playCurrentTrack to document the bool return value.
Note: loadCurrentTrack already returns bool — no change needed there.
go build ./backend/...
Both functions return bool; the codebase compiles.
In Next() (queue.go ~line 871):
The main advance path (after the RepeatOne early return):
prevIndex := q.currentIndex
q.currentIndex = nextIdx
if !q.playOrLoadCurrentTrack(wasPlaying) {
q.currentIndex = prevIndex
return
}
q.emitIndexChanged()
For the RepeatOne path (replay current track), the index doesn't change so there's nothing to roll back, but we should still guard the emit:
if q.repeatMode == RepeatOne {
if q.playOrLoadCurrentTrack(wasPlaying) {
q.emitIndexChanged()
}
return
}
In Previous() (queue.go ~line 904):
Same pattern for every branch:
- RepeatOne path (~line 914-919): Guard the emit with the return value:
if q.repeatMode == RepeatOne {
if q.playOrLoadCurrentTrack(wasPlaying) {
q.emitIndexChanged()
}
return
}
- Restart-current-track path (>3 seconds, ~line 922-930): The index doesn't change here either, just guard the emit:
if q.player != nil {
posSecs, err := q.player.CurrentPositionSeconds()
if err == nil && posSecs > PreviousRestartThreshold {
if q.playOrLoadCurrentTrack(wasPlaying) {
q.emitIndexChanged()
}
return
}
}
- Navigate-to-previous path (~line 933-onwards): Apply full roll-back:
prevIdx := q.previousIndex()
if prevIdx == -1 {
// At the beginning — just restart the current track.
if q.playOrLoadCurrentTrack(wasPlaying) {
q.emitIndexChanged()
}
return
}
prevCurrentIndex := q.currentIndex
q.currentIndex = prevIdx
if !q.playOrLoadCurrentTrack(wasPlaying) {
q.currentIndex = prevCurrentIndex
return
}
q.emitIndexChanged()
In OnPlaybackFinished() (handlers.go):
Apply roll-back to the main advance path:
// RepeatOne path — index doesn't change, guard emit:
if q.repeatMode == RepeatOne {
if q.playCurrentTrack() {
q.emitIndexChanged()
}
return
}
nextIdx := q.nextIndex()
if nextIdx == -1 {
q.onQueueExhausted()
return
}
prevIndex := q.currentIndex
q.currentIndex = nextIdx
if !q.playCurrentTrack() {
q.currentIndex = prevIndex
return
}
q.emitIndexChanged()
In handleCurrentTrackRemoved() (queue.go ~line 1184): Check what this does and apply same pattern if it calls loadCurrentTrack.
IMPORTANT: Do NOT change loadCurrentTrack() or loadFileLocked() themselves — they already work correctly. Only change the call sites that consume their return values.
IMPORTANT: Preserve the mutex-protected setter pattern (lock → write → release → callbacks). The emitIndexChanged() calls already happen inside the lock, which is correct. Just make them conditional.
go build ./backend/... && go test ./backend/queue/... -v -count=1
All Next/Previous/OnPlaybackFinished paths check the bool return from playOrLoadCurrentTrack/playCurrentTrack. On failure, currentIndex is rolled back (when it was changed) and QueueIndexChanged is NOT emitted. Tests pass.
<success_criteria>
playOrLoadCurrentTrackreturnsboolpropagated fromloadCurrentTrack/playCurrentTrackplayCurrentTrackreturnsbool(true if load + play succeeded)Next()rolls backcurrentIndexand skipsemitIndexChangedon failurePrevious()rolls backcurrentIndexand skipsemitIndexChangedon failure (all branches)OnPlaybackFinished()rolls backcurrentIndexand skipsemitIndexChangedon failure- All existing tests pass
- Code compiles with no vet warnings </success_criteria>