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 Flush,
26 Disconnect,
27 ReloadConfig,
28
29 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 QueueSet {
50 tracks: Vec<Collectable>,
51 expected_state: Hash,
52 },
53 QueueExtend(Vec<Collectable>),
54 QueueShuffle,
55 QueueClear,
56
57 PlaylistSet {
59 collectables: Vec<Collectable>,
60 expected_state: Hash,
61 },
62 PlaylistExtend(Vec<Collectable>),
63 PlaylistClear,
64
65 PlaylistSetShuffleMode {
67 shuffle_mode: ShuffleMode,
68 },
69 TracklistRebuild,
70
71 PlaylistSetLoopMode {
73 loop_mode: LoopMode,
74 },
75
76 TracklistSeek {
78 index: isize,
79 increment: bool,
80 },
81 Next,
82 Previous,
83
84 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,
186
187 Event,
189
190 Response,
192
193 Error,
195
196 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}