Skip to main content

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}