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.
87 lines
1.8 KiB
Go
87 lines
1.8 KiB
Go
package queue
|
|
|
|
import (
|
|
"yellowjacket/backend/events"
|
|
)
|
|
|
|
// emitQueueChanged emits the full queue state to the frontend.
|
|
func (q *Queue) emitQueueChanged() {
|
|
state := State{
|
|
Tracks: q.tracks,
|
|
CurrentIndex: q.currentIndex,
|
|
ShuffleMode: q.shuffleMode,
|
|
RepeatMode: q.repeatMode,
|
|
SourcePlaylistID: q.sourcePlaylistID,
|
|
}
|
|
|
|
// Ensure tracks is never nil in JSON.
|
|
if state.Tracks == nil {
|
|
state.Tracks = []Track{}
|
|
}
|
|
|
|
events.Emit(q.ctx, events.QueueChanged, state)
|
|
}
|
|
|
|
// emitIndexChanged emits only the current index to the frontend.
|
|
func (q *Queue) emitIndexChanged() {
|
|
events.Emit(
|
|
q.ctx,
|
|
events.QueueIndexChanged,
|
|
IndexChanged{CurrentIndex: q.currentIndex},
|
|
)
|
|
}
|
|
|
|
// emitModeChanged emits only the shuffle/repeat mode to the frontend.
|
|
func (q *Queue) emitModeChanged() {
|
|
events.Emit(
|
|
q.ctx,
|
|
events.QueueModeChanged,
|
|
ModeChanged{
|
|
ShuffleMode: q.shuffleMode,
|
|
RepeatMode: q.repeatMode,
|
|
},
|
|
)
|
|
}
|
|
|
|
// emitPlaybackFailed tells the frontend that a track could not be
|
|
// played. Before this existed the failure was logged, the index was
|
|
// reverted and nothing reached the user: a moved file was a button
|
|
// that did nothing, twice, forever (errors.C1).
|
|
func (q *Queue) emitPlaybackFailed(track Track, reason error) {
|
|
msg := ""
|
|
if reason != nil {
|
|
msg = reason.Error()
|
|
}
|
|
|
|
events.Emit(
|
|
q.ctx,
|
|
events.PlaybackFailed,
|
|
PlaybackFailure{
|
|
FilePath: track.FilePath,
|
|
Title: track.Title,
|
|
Artist: track.Artist,
|
|
Reason: msg,
|
|
},
|
|
)
|
|
}
|
|
|
|
// emitTracksModified emits a delta update for track list changes.
|
|
func (q *Queue) emitTracksModified(
|
|
action string,
|
|
tracks []Track,
|
|
index int,
|
|
positions []int,
|
|
) {
|
|
events.Emit(
|
|
q.ctx,
|
|
events.QueueTracksModified,
|
|
TracksModified{
|
|
Action: action,
|
|
Tracks: tracks,
|
|
Index: index,
|
|
Positions: positions,
|
|
CurrentIndex: q.currentIndex,
|
|
},
|
|
)
|
|
}
|