chilen_ipc/lib.rs
1#![doc = include_str!("../README.md")]
2
3pub mod library;
4pub mod playback;
5
6use std::{
7 env::temp_dir,
8 io::{BufReader, Write},
9 path::PathBuf,
10};
11
12pub use interprocess::local_socket::Stream;
13use interprocess::local_socket::{GenericFilePath, GenericNamespaced, Name, ToNsName, prelude::*};
14use log::{error, info, trace};
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 library::{LibraryCommand, MusicLibrary},
19 playback::{PlaybackCommand, PlaybackEvent, PlaybackResponse},
20};
21
22/// The default name of the socket the daemon listens on.
23///
24/// This can be used for testing, but please do not use this socket name in a finished project.
25pub const DEFAULT_SOCKET_NAME: &str = "DEFAULT_MUSIC_PLAYER.socket";
26
27/// Error related to the daemon.
28///
29/// Can either originate from a [`Response`] or from a function in this crate.
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub enum Error {
32 /// The provided command could not be encoded.
33 EncodingError,
34 /// The daemon response could not be decoded.
35 DecodingError,
36 /// Could not connect to the daemon.
37 ConnectionError,
38 /// Could not send the command to the daemon.
39 SendingError,
40 /// The response received from the daemon was unexpected or invalid.
41 InvalidResponse,
42 /// Could not obtain a socket.
43 SocketError(String),
44 /// Raise requests from external clients are not allowed.
45 RaiseDisabled,
46 /// Toggling fullscreen mode by external clients is not allowed.
47 SetFullscreenDisabled,
48 /// Quit requests from external clients are not allowed.
49 QuitDisabled,
50 /// The audio player is not connected.
51 ///
52 /// This may happen if the device doesn't have an audio device or none of the audio devices are
53 /// marked as default.
54 PlayerNotConnected,
55 /// The playback state is not initialized.
56 ///
57 /// This error may occur when a [`PlaybackCommand`] is sent to the daemon too early, before the
58 /// state is restored from cache.
59 StateNotInitialized,
60 /// The queue is empty.
61 QueueEmpty,
62 /// The audio file could not be opened, has an unsupported format or is corrupt.
63 SourceError,
64 /// The player is already playing.
65 PlayerPlaying,
66 /// The player is already paused.
67 PlayerPaused,
68 /// Thrown when a client attempts to stop the player when it was already stopped or when a
69 /// client attempts to seek while the player is stopped.
70 PlayerStopped,
71 /// Seek is not supported for the current audio source.
72 SeekNotSupported,
73 /// Cannot go to the previous track.
74 ///
75 /// This means that the current track is first in the queue and the
76 /// [loop state](playback::LoopState) is set to [`LoopState::Off`](playback::LoopState::Off).
77 CannotGoPrevious,
78 /// Cannot go to the next track.
79 ///
80 /// This means that the current track is last in the queue and the
81 /// [loop state](playback::LoopState) is set to [`LoopState::Off`](playback::LoopState).
82 CannotGoNext,
83 /// The daemon was not built with shuffle support.
84 ShuffleNotSupported,
85 /// No track at this index.
86 NoTrackAtIndex(usize),
87 /// The specified rate value was out of the allowed range.
88 RateOutOfRange,
89 /// The modification of the playback rate is not allowed.
90 FixedRate,
91 /// The player position could not be set because the duration provided was invalid.
92 ///
93 /// The player will additionally refuse to seek by 0s to prevent audio popping.
94 InvalidDuration,
95 /// Overflow detected while performing a seek operation.
96 DurationOverflow,
97 /// Could not complete the operation because a [playlist](library::Playlist) with the provided
98 /// name already exists.
99 PlaylistExists,
100 /// Could not perform the operation because the [music library](MusicLibrary) is not
101 /// initialized.
102 ///
103 /// This can happen if a command is sent to early and the music library is not yet initialized.
104 LibraryNotInitialized,
105 /// There is no [playlist](library::Playlist) in the [music library](MusicLibrary) with the
106 /// provided name.
107 UnknownPlaylist,
108 /// The provided item index was out of bounds.
109 IndexOutOfBounds,
110 /// The provided list contained duplicate values.
111 DuplicateItems,
112 /// The provided track is not registered in the library.
113 UnknownTrack,
114 /// Could not read the contents of the library state file.
115 StateNotReadable,
116 /// Could not write the library state to a file.
117 StateWriteFailed,
118 /// The library state path is not a file.
119 StateNotAFile,
120 /// The provided path does not exist or access to it was denied.
121 PathDoesNotExist,
122 /// Could not check if the provided file exists.
123 PathExistenceUnknown,
124 /// Could not find any audio files in the provided directory path.
125 DirectoryWithNoTracks,
126 /// Could not parse the M3U8 playlist.
127 ///
128 /// Please make sure that the playlist has the correct format and is not corrupted.
129 PlaylistParsingError,
130 /// Could not export the playlist to M3U8.
131 ///
132 /// This likely either means that the specified file path doesn't exists or is not writable.
133 PlaylistExportFailed,
134}
135
136impl std::fmt::Display for Error {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 match self {
139 Self::EncodingError => write!(f, "Could not encode the daemon command"),
140 Self::DecodingError => write!(f, "Could not decode the response from the daemon"),
141 Self::ConnectionError => write!(f, "Could not connect to the daemon"),
142 Self::SendingError => write!(f, "Could not send the command to the daemon"),
143 Self::InvalidResponse => {
144 write!(f, "The response from the daemon was invalid or malformed")
145 }
146 Self::SocketError(e) => write!(f, "Could not obtain a socket: {e}"),
147 Self::RaiseDisabled => {
148 write!(f, "Raise requests from external clients are not allowed")
149 }
150 Self::SetFullscreenDisabled => write!(
151 f,
152 "Toggling fullscreen mode by external clients is not allowed"
153 ),
154 Self::QuitDisabled => write!(f, "Quit requests from external clients are not allowed"),
155 Self::PlayerNotConnected => write!(f, "The audio player is not connected"),
156 Self::StateNotInitialized => write!(f, "The playback state is not initialized"),
157 Self::QueueEmpty => write!(f, "The queue is empty"),
158 Self::SourceError => write!(
159 f,
160 "The audio file could not be opened, has an unsupported format or is corrupt"
161 ),
162 Self::PlayerPlaying => write!(f, "The player is already playing"),
163 Self::PlayerPaused => write!(f, "The player is already paused"),
164 Self::PlayerStopped => write!(f, "The player is stopped"),
165 Self::SeekNotSupported => write!(f, "Seek is not supported"),
166 Self::CannotGoPrevious => write!(f, "Cannot go to the previous track"),
167 Self::CannotGoNext => write!(f, "Cannot go to the next track"),
168 Self::ShuffleNotSupported => write!(f, "The daemon was not built with shuffle support"),
169 Self::NoTrackAtIndex(index) => write!(f, "No track was found at index {index}"),
170 Self::RateOutOfRange => {
171 write!(f, "The specified rate value was out of the allowed range")
172 }
173 Self::FixedRate => write!(f, "The modification of the playback rate is disallowed"),
174 Self::InvalidDuration => write!(
175 f,
176 "The player position could not be set because the duration provided was invalid"
177 ),
178 Self::DurationOverflow => {
179 write!(f, "Overflow detected while performing a seek operation")
180 }
181 Self::UnknownTrack => {
182 write!(f, "The provided track was not found in the music library")
183 }
184 Self::PlaylistExists => write!(f, "Playlist with this name already exists"),
185 Self::LibraryNotInitialized => write!(f, "The music library is not initialized"),
186 Self::UnknownPlaylist => write!(f, "There is no playlist with this name"),
187 Self::IndexOutOfBounds => write!(f, "The provided item index was out of bounds"),
188 Self::DuplicateItems => write!(f, "The provided vector contained duplicate values"),
189 Self::StateNotReadable => {
190 write!(f, "Could not read the contents of the library state file")
191 }
192 Self::StateWriteFailed => write!(f, "Could not write the library state to a file"),
193 Self::StateNotAFile => write!(f, "The library state path is not a file"),
194 Self::PathDoesNotExist => write!(
195 f,
196 "The provided path does not exist or access to it was denied"
197 ),
198 Self::PathExistenceUnknown => write!(f, "Could not check if the provided file exists"),
199 Self::DirectoryWithNoTracks => write!(
200 f,
201 "Could not find any audio files in the provided directory path"
202 ),
203 Self::PlaylistParsingError => write!(f, "Could not parse the M3U8 playlist"),
204 Self::PlaylistExportFailed => write!(f, "Could not export the playlist to M3U8"),
205 }
206 }
207}
208
209/// Event from the daemon received in [`Response::Event`].
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub enum Event {
212 /// Sent before the daemon quits.
213 Quit,
214 /// Sent after the contents of the music library have changed.
215 LibraryChanged(MusicLibrary),
216 /// Event originating from the playback module of the daemon.
217 PlaybackEvent(PlaybackEvent),
218 /// Sent when the `can_raise` property of the daemon config changes.
219 CanRaiseChanged(bool),
220 /// Sent when the `can_go_fullscreen` property of the daemon config changes.
221 CanGoFullscreenChanged(bool),
222 /// Sent when the `can_quit` property of the daemon config changes.
223 CanQuitChanged(bool),
224}
225
226/// Response sent to a client from the daemon.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub enum Response {
229 /// The client command was executed successfully.
230 Ok,
231 /// Response to [`Command::Ping`].
232 Pong,
233 /// The contents of the music library.
234 Library(MusicLibrary),
235 /// An event from the daemon.
236 Event(Event),
237 /// Response from the playback module.
238 Playback(PlaybackResponse),
239 /// The client command has failed.
240 Error(Error),
241 /// Response to [`Command::CanRaise`].
242 CanRaise(bool),
243 /// Response to [`Command::CanSetFullscreen`].
244 CanSetFullscreen(bool),
245 /// Response to [`Command::CanQuit`].
246 CanQuit(bool),
247}
248
249/// Command that can be executed by a daemon instance.
250///
251/// The expected response may be different depending on the command sent. If it isn't specified in
252/// the variant documentation, assume [`Response::Ok`] is the expected response.
253#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
254pub enum Command {
255 /// Stop the daemon instance.
256 ///
257 /// After sending this command, the daemon will close almost immediately, so all connections
258 /// to it should be considered closed.
259 Quit,
260 /// Subcommand for managing the music library.
261 Library(LibraryCommand),
262 /// Stream [events](Event) from the daemon.
263 ///
264 /// The daemon will stop accepting requests from the connection this command was executed on.
265 ///
266 /// This command may fail if initial events cannot be obtained (eg. the music library is not
267 /// initialized or the queue state is not ready). The daemon will never return an incomplete
268 /// set of initial events if some cannot be obtained.
269 EventStream,
270 /// Close the connection to the daemon.
271 Disconnect,
272 /// Command to the playback module.
273 Playback(PlaybackCommand),
274 /// Ping the daemon.
275 ///
276 /// The daemon will respond to this with [`Response::Pong`] if successful.
277 Ping,
278 /// Check if the daemon can raise.
279 CanRaise,
280 /// Request the daemon to raise.
281 Raise,
282 /// Check if clients can toggle fullscreen mode on daemon's user interface.
283 CanSetFullscreen,
284 /// Toggle fullscreen mode for the daemon's user interface.
285 SetFullscreen(bool),
286 /// Check if the daemon accepts quit requests from clients.
287 CanQuit,
288}
289
290/// Defines the socket type to use when attempting to connect to a daemon.
291#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
292pub enum SocketType {
293 /// Only use a namespaced socket with no fallback.
294 ///
295 /// The connection will fail if a namespaced socket with the specified name doesn't exist.
296 NamespacedOnly,
297 /// Use a namespaced socket when possible, but allow a fallback to a filesystem socket.
298 ///
299 /// The connection will fail if there are no namespaced or filesystem sockets with the
300 /// specified name.
301 #[default]
302 NamespacedOrFilesystem,
303 /// Only use a filesystem socket with no fallback.
304 ///
305 /// The connection will fail if a filesystem socket with the specified name doesn't exist.
306 FilesystemOnly,
307}
308
309/// Returns a filesystem path for the given socket name.
310fn get_fs_socket_path(socket_name: &str) -> PathBuf {
311 let mut temp_dir = temp_dir();
312 temp_dir.push(socket_name);
313 temp_dir
314}
315
316/// Attempts to get a socket address for daemon IPC.
317///
318/// # Examples
319/// ```
320/// match chilen_ipc::get_socket(
321/// chilen_ipc::DEFAULT_SOCKET_NAME,
322/// &chilen_ipc::SocketType::NamespacedOrFilesystem
323/// ) {
324/// Ok(socket) => eprintln!("Got a socket: {socket:?}"),
325/// Err(e) => panic!("Could not obtain a socket: {e}"),
326/// }
327/// ```
328pub fn get_socket<'a>(socket_name: &'a str, mode: &SocketType) -> Result<Name<'a>, std::io::Error> {
329 match mode {
330 SocketType::NamespacedOnly => socket_name.to_ns_name::<GenericNamespaced>(),
331 SocketType::FilesystemOnly => {
332 get_fs_socket_path(socket_name).to_fs_name::<GenericFilePath>()
333 }
334 SocketType::NamespacedOrFilesystem => match socket_name.to_ns_name::<GenericNamespaced>() {
335 Ok(socket) => Ok(socket),
336 Err(e) => {
337 info!("Could not obtain a namespaced socket: {e}");
338 info!("Trying a filesystem socket instead");
339 get_fs_socket_path(socket_name).to_fs_name::<GenericFilePath>()
340 }
341 },
342 }
343}
344
345/// Serialize a client command to a format that can be sent to the daemon.
346///
347/// This uses the [`rmp_serde`] crate under the hood.
348///
349/// # Examples
350///
351/// Connect to the daemon and immediately disconnect.
352/// ```no_run
353/// # use std::io::{BufReader, Write};
354/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, serialize_command, Command, Error};
355/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
356/// let mut conn = BufReader::new(conn);
357/// let cmd = serialize_command(&Command::Disconnect).unwrap();
358/// conn.get_mut().write_all(&cmd).unwrap();
359/// ```
360pub fn serialize_command(cmd: &Command) -> Result<Vec<u8>, Error> {
361 let mut data = Vec::new();
362 if let Err(e) = cmd.serialize(&mut rmp_serde::Serializer::new(&mut data)) {
363 error!("Could not encode the client command: {e}");
364 return Err(Error::EncodingError);
365 }
366 Ok(data)
367}
368
369/// Receive a daemon response from a buffered stream connection.
370///
371/// This function will block until a response is received or the connection is dropped.
372///
373/// # Examples
374/// ```no_run
375/// # use std::io::{BufReader, Write};
376/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, serialize_command, Command, receive_response};
377/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
378/// let mut conn = BufReader::new(conn);
379/// let cmd = serialize_command(&Command::EventStream).unwrap();
380/// conn.get_mut().write_all(&cmd).unwrap();
381/// loop {
382/// let response = receive_response(&mut conn).unwrap();
383/// println!("Got a response from the daemon: {response:?}");
384/// }
385/// ```
386pub fn receive_response(conn: &mut BufReader<Stream>) -> Result<Response, Error> {
387 match rmp_serde::from_read(conn) {
388 Ok(response) => Ok(response),
389 Err(e) => {
390 error!("Failed decoding a daemon response: {e}");
391 Err(Error::DecodingError)
392 }
393 }
394}
395
396/// Disconnects from the daemon by sending the [`Command::Disconnect`] command.
397///
398/// This is a convenience function, its effect could be achieved using utilities already provided
399/// in this crate.
400///
401/// # Examples
402/// ```no_run
403/// # use std::io::BufReader;
404/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, disconnect};
405/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
406/// let mut conn = BufReader::new(conn);
407///
408/// // Do some stuff with the connection here...
409///
410/// match disconnect(&mut conn) {
411/// Ok(_) => eprintln!("Disconnected from the daemon!"),
412/// Err(e) => panic!("Could not close the daemon connection: {e}"),
413/// }
414/// ```
415pub fn disconnect(conn: &mut BufReader<Stream>) -> Result<(), Error> {
416 trace!("Closing connection with the daemon");
417
418 let data = serialize_command(&Command::Disconnect)?;
419
420 match conn.get_mut().write_all(&data) {
421 Ok(_) => {}
422 Err(e) => {
423 error!("Failed sending the command to the daemon: {e}");
424 return Err(Error::SendingError);
425 }
426 }
427
428 let response = receive_response(conn)?;
429
430 match response {
431 Response::Error(e) => {
432 error!("Could not close the connection to the daemon: {e}");
433 return Err(e);
434 }
435 Response::Ok => {
436 trace!("Connection with the daemon closed");
437 }
438 _ => {
439 error!("Got an unexpected response while closing the daemon connection: {response:?}");
440 return Err(Error::InvalidResponse);
441 }
442 }
443
444 Ok(())
445}
446
447/// Connects to the daemon via a local socket and returns the connection [`Stream`].
448///
449/// # Examples
450/// ```no_run
451/// # use std::io::BufReader;
452/// # use chilen_ipc::{connect, DEFAULT_SOCKET_NAME, SocketType, disconnect};
453/// let conn = connect(DEFAULT_SOCKET_NAME, &SocketType::default()).unwrap();
454/// let mut conn = BufReader::new(conn);
455/// eprintln!("Connected to the daemon: {conn:?}");
456/// // Run all your commands here!
457/// disconnect(&mut conn).unwrap();
458/// ```
459pub fn connect(socket_name: &str, socket_type: &SocketType) -> Result<Stream, Error> {
460 trace!("Connecting to daemon on socket \"{socket_name}\"");
461
462 let socket = match get_socket(socket_name, socket_type) {
463 Ok(sock) => sock,
464 Err(e) => {
465 error!("Could not obtain a socket: {e}");
466 return Err(Error::SocketError(e.to_string()));
467 }
468 };
469
470 let conn = match Stream::connect(socket) {
471 Ok(conn) => conn,
472 Err(e) => {
473 error!("Could not initialize a connection to the daemon: {e}");
474 return Err(Error::ConnectionError);
475 }
476 };
477
478 Ok(conn)
479}
480
481/// Executes a single daemon command on a new connection and closes it.
482///
483/// **Warning:** if this function returns the [`Ok`] variant, this only means that the command was
484/// successfully delivered to the daemon. It doesn't necessarily mean it was executed
485/// successfully on the daemon side.
486///
487/// # Examples
488/// ```no_run
489/// # use chilen_ipc::{send_command, Command, DEFAULT_SOCKET_NAME, SocketType, Response};
490/// match send_command(Command::Ping, DEFAULT_SOCKET_NAME, &SocketType::default()) {
491/// // The `Ok` variant only means the command was delivered
492/// Ok(response) => {
493/// match response {
494/// Response::Ok => eprintln!("Got an `Ok` response, all good!"),
495/// // Depending on the type of command sent, the response from the daemon may be different.
496/// _ => panic!("Got an unexpected response: {response:?}"),
497/// }
498/// },
499/// Err(error) => panic!("Could not send a command to the daemon: {error}"),
500/// }
501/// ```
502pub fn send_command(
503 cmd: Command,
504 socket_name: &str,
505 socket_type: &SocketType,
506) -> Result<Response, Error> {
507 trace!("Executing daemon command: {cmd:?}");
508
509 let mut conn = match connect(socket_name, socket_type) {
510 Ok(conn) => BufReader::new(conn),
511 Err(e) => {
512 error!("{e}");
513 return Err(e);
514 }
515 };
516
517 let data = serialize_command(&cmd)?;
518
519 if let Err(e) = conn.get_mut().write_all(&data) {
520 error!("Failed sending the daemon command: {e}");
521 return Err(Error::SendingError);
522 }
523
524 let response = receive_response(&mut conn)?;
525
526 if cmd == Command::Quit {
527 trace!("Not trying to close the connection to the daemon");
528 } else if let Err(e) = disconnect(&mut conn) {
529 error!("Could not close the connection to the daemon: {e}");
530 }
531
532 Ok(response)
533}