Files
yellowjacket/frontend/bindings/yellowjacket/backend/queue/queue.ts
T
yonlu 2b84bc53e9 fix(player): stop reporting one track's state against another
Five faults found while auditing the play/pause and position path for
a desktop report of the pause icon showing over a seek bar that was
not moving. They are one commit because they are one file's worth of
tangled state, and two of them do not compile apart.

The finished callback did not know which chain it came from. It is
dispatched as a goroutine from the beep callback and then queues for
p.mu, so a user pressing Next in the last second of a track had it
wake up holding the lock for a player that had loaded something else
-- and rewind it, stop it, and hand a stale finish to the queue's
auto-advance. updateStreamers now stamps a chainID and the callback
carries the one it was registered with. (#123)

It also emitted PlaybackFinished and PlaybackStateChanged(stopped)
*after* releasing p.mu, alone in this file, so a Play() taking the
lock in that gap emitted `playing` first and the stale `stopped`
landed last -- the button showing play over a track that was audibly
running. Both emits are back under the lock. (#123)

A source that failed mid-track was reported to the queue as a natural
end, so a broken file auto-advanced in silence and was counted as
played. The handler takes the reason now: the player cannot name the
track, because the metadata is the queue's, so the queue emits
PlaybackFailed and skips recording the play. (#123)

p.format was assigned once, in the constructor, to the *speaker's*
rate, and never again -- so it claimed 44.1 kHz for every file. The
replay-after-finish path resamples from it, meaning a finished track
played a second time was resampled from a rate the decoder never
produced: audibly wrong speed and pitch, and the length and position
fallbacks wrong with it. The fixtures are 22050 Hz, which is what lets
a test see this at all. (#124)

p.trackLengthMs was written only when the database had a row and
cleared only by UnloadTrack, so a file with no row inherited the
previous track's duration -- and every position report is scaled by
it, so the bar reported one track's progress on another's scale.
(#125)

Queue.OnPlaybackFinished indexed q.tracks[currentIndex] having checked
only that the queue was non-empty. currentIndex is -1 whenever the
queue has been exhausted, and onQueueExhausted deliberately leaves the
finished track loaded -- so playing it from there and letting it end
panicked, on a goroutine with no caller to recover it. (#126)

The position readers guarded the decoder with the speaker lock, which
the read-ahead goroutine has no reason to hold and never takes -- so
Position() raced readAhead's Stream() on every position emit, once a
second for the whole of playback. srcMu is the lock that excludes that
goroutine, and taking it naively deadlocks, because seekLocked already
holds it and then emits the landing position from inside that region.
seekSourceLocked is that region extracted, so the lock is released
before anything is emitted. Found by the race detector, via the test
added here for the chain guard: the existing suite never loads a file
outside the integration guard, so make test was green over it. (#127)

OnPlaybackFinished picks up //wails:ignore along with its error
parameter: v3's generator segfaults on a bound method taking an error,
and this was never IPC. That removes a binding the frontend could have
called to force an auto-advance.

Closes #123
Closes #124
Closes #125
Closes #126
Closes #127
2026-08-19 09:06:43 -04:00

196 lines
6.6 KiB
TypeScript

// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
/**
* Queue manages an ordered list of tracks for playback.
* @module
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Call as $Call, CancellablePromise as $CancellablePromise } from "@wailsio/runtime";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as $models from "./models.js";
/**
* AddTrack appends a track to the end of the queue.
* If the queue was empty, it loads the added track in a paused state.
*/
export function AddTrack(filePath: string): $CancellablePromise<void> {
return $Call.ByID(3566864127, filePath);
}
/**
* AddTracks appends multiple tracks to the end of the queue.
* If the queue was empty, it loads the first added track in a paused state.
*/
export function AddTracks(filePaths: string[] | null): $CancellablePromise<void> {
return $Call.ByID(980962916, filePaths);
}
/**
* Clear removes all tracks from the queue, stops playback, and
* resets the queue state. It persists the cleared state and
* notifies the frontend.
*/
export function Clear(): $CancellablePromise<void> {
return $Call.ByID(4166243766);
}
/**
* CompactAfterLibraryRemoval reloads queue state from the database
* after a library removal has cascade-deleted queue_tracks rows.
* It resets currentIndex to 0 (or -1 if empty), clears shuffleOrder,
* unloads the current track if it was removed, and emits QueueChanged.
*/
export function CompactAfterLibraryRemoval(): $CancellablePromise<void> {
return $Call.ByID(3620379645);
}
/**
* CycleRepeat cycles through repeat modes: off -> all -> one -> off.
*/
export function CycleRepeat(): $CancellablePromise<void> {
return $Call.ByID(3510519482);
}
/**
* EmitCurrentState emits the current queue state to the frontend.
* This is called after the frontend DOM is ready.
*/
export function EmitCurrentState(): $CancellablePromise<void> {
return $Call.ByID(1245022280);
}
/**
* GetState returns the current queue state for the frontend.
*/
export function GetState(): $CancellablePromise<$models.State> {
return $Call.ByID(3472178852);
}
/**
* InsertNext inserts a track right after the currently playing track.
* If the queue was empty, it loads the inserted track in a paused state.
*/
export function InsertNext(filePath: string): $CancellablePromise<void> {
return $Call.ByID(1498463573, filePath);
}
/**
* InsertNextTracks inserts multiple tracks as a contiguous block after the current track.
* If the queue was empty, it loads the first inserted track in a paused state.
*/
export function InsertNextTracks(filePaths: string[] | null): $CancellablePromise<void> {
return $Call.ByID(2824705517, filePaths);
}
/**
* InsertTracksAt inserts multiple tracks at the given index.
* If the queue was empty, it loads the first inserted track in a paused state.
*/
export function InsertTracksAt(filePaths: string[] | null, index: number): $CancellablePromise<void> {
return $Call.ByID(2550945669, filePaths, index);
}
/**
* MoveQueueTracks moves tracks at the given indices to a new position
* as a contiguous block. The toIndex is the target position in the
* original (pre-move) array.
*/
export function MoveQueueTracks(fromIndices: number[] | null, toIndex: number): $CancellablePromise<void> {
return $Call.ByID(2149753655, fromIndices, toIndex);
}
/**
* Next advances to the next track. If the player was paused, the next
* track is loaded but not played. In RepeatOne mode, the current track
* is replayed instead of advancing.
*/
export function Next(): $CancellablePromise<void> {
return $Call.ByID(1968784044);
}
/**
* Play handles a play request by either resuming the current track or
* starting playback from the beginning of the queue. When a track is
* already active (currentIndex != -1) the player is told to resume;
* otherwise playback starts from the first track (or a random one when
* shuffle is enabled).
*/
export function Play(): $CancellablePromise<void> {
return $Call.ByID(3197379127);
}
/**
* PlayIndex jumps to and plays the track at the given index.
*/
export function PlayIndex(index: number): $CancellablePromise<void> {
return $Call.ByID(200440797, index);
}
/**
* Previous goes to the previous track (or restarts current if >3s in).
* If the player was paused, the track is loaded but not played.
* In RepeatOne mode, the current track is replayed instead of navigating.
*/
export function Previous(): $CancellablePromise<void> {
return $Call.ByID(708701476);
}
/**
* RemoveTrack removes a track at the given position from the queue.
*/
export function RemoveTrack(position: number): $CancellablePromise<void> {
return $Call.ByID(6811080, position);
}
/**
* RemoveTracks removes multiple tracks at the given positions from the queue.
* Positions are deduplicated, validated, and removed in descending order so
* that indices remain stable during removal.
*/
export function RemoveTracks(positions: number[] | null): $CancellablePromise<void> {
return $Call.ByID(1587232097, positions);
}
/**
* RestoreState loads the queue state from the database.
*/
export function RestoreState(): $CancellablePromise<void> {
return $Call.ByID(1652105446);
}
/**
* SaveState persists the queue state to the database. Unlike every
* other write here it waits for the writer: its callers are shutdown
* and the tests, both of which need the row to exist on return.
*/
export function SaveState(): $CancellablePromise<void> {
return $Call.ByID(2913531465);
}
/**
* SetQueue replaces the entire queue with new tracks and starts playing.
* When shuffleStart is true and shuffle mode is active, a random first
* track is chosen instead of the one at startIndex. This is intended for
* "Play All" type actions where no specific track was selected.
* It uses a two-phase approach: the first batch of tracks (up to
* initialBatchSize) is resolved immediately so playback begins and the
* queue panel is populated without delay. The remaining tracks are then
* resolved in the background. A generation counter ensures stale
* background work is discarded if SetQueue is called again.
*/
export function SetQueue(filePaths: string[] | null, startIndex: number, shuffleStart: boolean, source: $models.Source): $CancellablePromise<void> {
return $Call.ByID(1990450094, filePaths, startIndex, shuffleStart, source);
}
/**
* ToggleShuffle toggles shuffle mode on/off.
*/
export function ToggleShuffle(): $CancellablePromise<void> {
return $Call.ByID(612647562);
}