Skip to main content

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