koan_core/player/commands.rs
1use std::path::PathBuf;
2
3use crossbeam_channel::{Receiver, Sender, bounded};
4
5use super::state::{PlaylistItem, QueueItemId};
6
7/// Commands from the UI layer to the audio engine.
8#[derive(Debug)]
9pub enum PlayerCommand {
10 /// Set cursor + start playback. Replaces Play/SkipTo/SkipBack/PlayInterrupt.
11 Play(QueueItemId),
12 Pause,
13 Resume,
14 Stop,
15 Seek(u64), // position in ms
16 NextTrack,
17 PrevTrack,
18 AddToPlaylist(Vec<PlaylistItem>),
19 RemoveFromPlaylist(QueueItemId),
20 /// Batch remove: delete multiple items as a single undoable operation.
21 RemoveFromPlaylistBatch(Vec<QueueItemId>),
22 MoveInPlaylist {
23 id: QueueItemId,
24 target: QueueItemId,
25 after: bool,
26 },
27 /// Batch move: extract `ids` and reinsert them at `target` position.
28 MoveItemsInPlaylist {
29 ids: Vec<QueueItemId>,
30 target: QueueItemId,
31 after: bool,
32 },
33 /// Put the queue in exactly this order, keeping every item.
34 ///
35 /// A queue locked to a playlist follows it, and following a reorder means
36 /// moving the items that are already there — rebuilding them would issue
37 /// new ids and throw away what has played, what is mid-download and where
38 /// the cursor is. Ids not named keep their relative order at the end.
39 ReorderPlaylist(Vec<QueueItemId>),
40 /// Update file paths for playlist items after an organize operation.
41 /// On Unix, rename() doesn't invalidate open FDs so playback continues.
42 UpdatePaths(Vec<(QueueItemId, PathBuf)>),
43 /// Insert items after a specific queue item (for drag/drop at cursor position).
44 InsertInPlaylist {
45 items: Vec<PlaylistItem>,
46 after: QueueItemId,
47 },
48 /// Clear the entire playlist (stop + remove all items).
49 ClearPlaylist,
50 /// Replace the playlist and start playing at `start`, as one operation.
51 ///
52 /// Doing this as ClearPlaylist + AddToPlaylist + Play sends three commands
53 /// down a bounded channel, and the player acts on each as it arrives: the
54 /// first track starts, then the cursor jumps. Clicking track nine of an
55 /// album visibly flashed track one as playing first. It is also three undo
56 /// entries for one user action.
57 ///
58 /// `start` past the end starts at the beginning.
59 ReplacePlaylist {
60 items: Vec<PlaylistItem>,
61 start: usize,
62 },
63 /// Download complete — check if cursor is waiting on this item.
64 TrackReady(QueueItemId),
65 /// Enough data buffered for streaming playback — check if cursor is waiting.
66 TrackStreamReady(QueueItemId),
67 /// A partial file has been probed off-thread and can be started.
68 ///
69 /// Probing reads as much of the container as it takes to describe itself,
70 /// which for Ogg means its last page — the whole remaining download. That
71 /// cannot happen on this loop, so it happens on its own thread and arrives
72 /// here as a command like anything else. Stale by the time it lands is the
73 /// normal case, and simply ignored.
74 StreamProbed {
75 id: QueueItemId,
76 info: Box<crate::audio::buffer::StreamInfo>,
77 /// What the probe had to settle for. Decoding has to be opened the same
78 /// way — given a length, a container that went looking for its tail
79 /// once will do it again, on the decode thread, where the cost is
80 /// silence instead of a busy player.
81 mode: crate::audio::streaming::ProbeMode,
82 },
83 /// Download failed — a cursor parked on this item must stop waiting.
84 ///
85 /// Without it the player sits on a `Pending` item forever: `Ready` is the
86 /// only thing it listens for, and a track that cannot be fetched never
87 /// becomes Ready. That is the offline-library stall.
88 TrackFailed(QueueItemId),
89 /// Decode thread exhausted the playlist — auto-advance or stop.
90 DecodeFinished,
91 /// Undo the last reversible playlist operation.
92 Undo,
93 /// Redo the last undone operation.
94 Redo,
95 /// Begin collecting undo entries into a single batch (e.g. drag operations).
96 BeginUndoBatch,
97 /// End the batch — collapse collected entries into one undo step.
98 EndUndoBatch,
99 /// Switch output audio device by name. Restarts engine on current track.
100 SetOutputDevice(String),
101 /// Clear the configured output device, reverting to system default.
102 ClearOutputDevice,
103 /// Build the output again on the same device and carry on from where the
104 /// current track is, paused if it was. For an output the system stopped
105 /// underneath us: an iOS interruption (a call, Siri) or a reset of its
106 /// media services leaves the old unit unable to start again.
107 RestartOutput,
108}
109
110/// Bounded SPSC command channel.
111///
112/// Small capacity — we don't want commands queuing up. If the engine is busy,
113/// the UI should know about it, not silently buffer 50 seeks.
114pub struct CommandChannel {
115 pub tx: Sender<PlayerCommand>,
116 pub rx: Receiver<PlayerCommand>,
117}
118
119impl Default for CommandChannel {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125impl CommandChannel {
126 pub fn new() -> Self {
127 let (tx, rx) = bounded(16);
128 Self { tx, rx }
129 }
130}