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 Flush,
23 Disconnect,
24 ReloadConfig,
25
26 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 QueueSet {
47 tracks: Vec<Collectable>,
48 expected_state: Hash,
49 },
50 QueueExtend(Vec<Collectable>),
51 QueueShuffle,
52 QueueClear,
53
54 PlaylistSet {
56 collectables: Vec<Collectable>,
57 expected_state: Hash,
58 },
59 PlaylistExtend(Vec<Collectable>),
60 PlaylistClear,
61
62 PlaylistSetShuffleMode {
64 shuffle_mode: ShuffleMode,
65 },
66 TracklistRebuild,
67
68 PlaylistSetLoopMode {
70 loop_mode: LoopMode,
71 },
72
73 TracklistSeek {
75 index: isize,
76 increment: bool,
77 },
78 Next,
79 Previous,
80
81 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,
183
184 Event,
186
187 Response,
189
190 Error,
192
193 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}