Skip to main content

selene_daemon/
ipc.rs

1use std::{
2    fmt::Display,
3    io,
4    sync::mpsc::{Sender, channel},
5};
6
7use blake3::Hash;
8use lunar_lib::config::ConfigError;
9use selene_core::library::{collection::Collectable, track::TrackId};
10use serde::{Deserialize, Serialize, de::DeserializeOwned};
11use thiserror::Error;
12
13use crate::{
14    daemon::unix_socket_handle::CallbackFn,
15    player::PlayerQueryFlags,
16    playlist::{LoopMode, ResolvedTrack, ShuffleMode},
17};
18
19#[derive(Debug, Serialize, Deserialize, Clone)]
20pub enum IpcCommand {
21    // Generic
22    Flush,
23    Disconnect,
24    ReloadConfig,
25
26    // Playback
27    Play {
28        collectable: Collectable,
29    },
30    Stop,
31    SetIsPlaying {
32        is_playing: bool,
33    },
34    TogglePlaying,
35    Seek {
36        seconds: f64,
37        increment: bool,
38    },
39
40    SetVolume {
41        volume: f32,
42        increment: bool,
43    },
44
45    // The override queue
46    QueueSet {
47        tracks: Vec<Collectable>,
48        expected_state: Hash,
49    },
50    QueueExtend(Vec<Collectable>),
51    QueueShuffle,
52    QueueClear,
53
54    // Playlist
55    PlaylistSet {
56        collectables: Vec<Collectable>,
57        expected_state: Hash,
58    },
59    PlaylistExtend(Vec<Collectable>),
60    PlaylistClear,
61
62    // Playlist // Shuffle mode
63    PlaylistSetShuffleMode {
64        shuffle_mode: ShuffleMode,
65    },
66    TracklistRebuild,
67
68    // Playlist // Loop mode
69    PlaylistSetLoopMode {
70        loop_mode: LoopMode,
71    },
72
73    // Tracklist
74    TracklistSeek {
75        index: isize,
76        increment: bool,
77    },
78    Next,
79    Previous,
80
81    // Queries
82    GetState {
83        flags: PlayerQueryFlags,
84    },
85}
86
87impl IpcCommand {
88    #[must_use]
89    pub fn responds(&self) -> bool {
90        matches!(
91            self,
92            IpcCommand::Flush
93                | IpcCommand::TogglePlaying
94                | IpcCommand::Seek { .. }
95                | IpcCommand::SetVolume { .. }
96                | IpcCommand::QueueSet { .. }
97                | IpcCommand::PlaylistSet { .. }
98                | IpcCommand::TracklistSeek { .. }
99                | IpcCommand::GetState { .. }
100        )
101    }
102}
103
104pub struct IpcRequest {
105    pub command: IpcCommand,
106    pub callback: Option<CallbackFn>,
107}
108
109pub trait IpcTx {
110    fn no_response(&self, command: IpcCommand) -> Result<(), PacketError>;
111
112    fn request<T: DeserializeOwned + Send + 'static>(
113        &self,
114        command: IpcCommand,
115    ) -> Result<T, PacketError>;
116
117    fn action(&self, command: IpcCommand);
118
119    fn disconnect(&self);
120}
121
122impl IpcTx for Sender<IpcRequest> {
123    fn no_response(&self, command: IpcCommand) -> Result<(), PacketError> {
124        self.send(IpcRequest {
125            command,
126            callback: None,
127        })
128        .unwrap();
129
130        Ok(())
131    }
132
133    fn request<T: DeserializeOwned + Send + 'static>(
134        &self,
135        command: IpcCommand,
136    ) -> Result<T, PacketError> {
137        assert!(
138            command.responds(),
139            "Commands must always respond with request()"
140        );
141
142        let (tx, rx) = channel();
143        let callback: CallbackFn = Box::new(move |result| {
144            let _ = tx.send(result.map(|bytes| {
145                ciborium::from_reader::<T, &[u8]>(bytes).expect("Daemon sent invalid bytes")
146            }));
147        });
148
149        self.send(IpcRequest {
150            command,
151            callback: Some(Box::new(callback)),
152        })
153        .unwrap();
154
155        rx.recv().unwrap()
156    }
157
158    fn action(&self, command: IpcCommand) {
159        assert!(
160            !command.responds(),
161            "Commands must never respond with action()"
162        );
163
164        self.send(IpcRequest {
165            command,
166            callback: None,
167        })
168        .unwrap();
169    }
170
171    fn disconnect(&self) {
172        let _ = self.send(IpcRequest {
173            command: IpcCommand::Disconnect,
174            callback: None,
175        });
176    }
177}
178
179#[repr(u8)]
180pub enum PacketType {
181    /// Unknown packet. Either the client is out of date, or the daemon has a logic bug
182    Unknown,
183
184    /// An event, can be sent without input
185    Event,
186
187    /// A response to a command
188    Response,
189
190    /// A [`PacketError`]
191    Error,
192
193    /// Client has been disconnected from the daemon. This can happen from a manual disconnect, or from the daemon shutting down
194    Disconnect,
195}
196
197#[derive(Debug, Error, Serialize, Deserialize, Clone, Copy)]
198pub enum PacketError {
199    #[error("Packet size '{size}' too large: Max size is {max_size}")]
200    PacketTooLarge { size: usize, max_size: usize },
201
202    #[error("Client was disconnected while waiting for a packet")]
203    Disconnect,
204}
205
206impl From<u8> for PacketType {
207    fn from(value: u8) -> Self {
208        match value {
209            1 => Self::Event,
210            2 => Self::Response,
211            3 => Self::Error,
212            4 => Self::Disconnect,
213            _ => Self::Unknown,
214        }
215    }
216}
217
218#[derive(Debug, Serialize, Deserialize, Clone)]
219pub enum PlayerEvent {
220    CurrentlyPlayingChanged {
221        currently_playing: Box<ResolvedTrack>,
222    },
223    PlaybackIsPlayingChanged {
224        is_playing: bool,
225        changed_at: f64,
226    },
227    PlaybackStopped,
228
229    ShuffleModeChanged {
230        shuffle_mode: ShuffleMode,
231    },
232    LoopModeChanged {
233        loop_mode: LoopMode,
234    },
235
236    VolumeChanged {
237        volume: f32,
238    },
239    SeekOccured {
240        time: f64,
241    },
242
243    QueueChanged {
244        queue: Vec<TrackId>,
245    },
246    PlaylistChanged {
247        playlist: Vec<Collectable>,
248    },
249    TracklistChanged {
250        tracklist: Vec<TrackId>,
251    },
252
253    Shutdown,
254}
255
256impl Display for PlayerEvent {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        match self {
259            PlayerEvent::CurrentlyPlayingChanged { .. } => f.write_str("CurrentlyPlayingChanged"),
260            PlayerEvent::PlaybackIsPlayingChanged { .. } => f.write_str("PlaybackIsPlayingChanged"),
261            PlayerEvent::PlaybackStopped => f.write_str("PlaybackStopped"),
262            PlayerEvent::ShuffleModeChanged { .. } => f.write_str("ShuffleModeChanged"),
263            PlayerEvent::LoopModeChanged { .. } => f.write_str("LoopModeChanged"),
264            PlayerEvent::VolumeChanged { .. } => f.write_str("VolumeChanged"),
265            PlayerEvent::SeekOccured { .. } => f.write_str("SeekOccured"),
266            PlayerEvent::QueueChanged { .. } => f.write_str("QueueChanged"),
267            PlayerEvent::PlaylistChanged { .. } => f.write_str("PlaylistChanged"),
268            PlayerEvent::TracklistChanged { .. } => f.write_str("TracklistChanged"),
269            PlayerEvent::Shutdown => f.write_str("Shutdown"),
270        }
271    }
272}
273
274#[derive(Debug)]
275pub enum ConnectErrorKind {
276    DaemonNotRunning,
277    ConnectionRefused,
278    FailedToLoadConfig(String),
279    Other(io::Error),
280}
281
282impl Display for ConnectErrorKind {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        match self {
285            ConnectErrorKind::DaemonNotRunning => f.write_str("The daemon is not running"),
286            ConnectErrorKind::ConnectionRefused => {
287                f.write_str("The daemon listener thread halted and must be restarted")
288            }
289            ConnectErrorKind::FailedToLoadConfig(string) => string.fmt(f),
290            ConnectErrorKind::Other(error) => error.fmt(f),
291        }
292    }
293}
294
295#[derive(Debug, Error)]
296pub enum IpcHandleError {
297    #[error("Failed to connect: {0}")]
298    FailedToConnect(ConnectErrorKind),
299
300    #[error("The handling thread cannot be communicated with")]
301    HandleDied,
302
303    #[error("The current platform is not supported")]
304    UnsupportedPlatform,
305}
306
307impl From<ConfigError> for IpcHandleError {
308    fn from(value: ConfigError) -> Self {
309        Self::FailedToConnect(ConnectErrorKind::FailedToLoadConfig(value.to_string()))
310    }
311}