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