mrc/
lib.rs

1//! MRC
2//! A library for interacting with the MPV media player using its JSON IPC (Inter-Process Communication) protocol.
3//!
4//! This crate provides a set of utilities to communicate with MPV's IPC socket, enabling you to send commands
5//! and retrieve responses in a structured format.
6//!
7//! ## Features
8//!
9//! - Send commands to MPV's IPC socket
10//! - Retrieve responses in JSON format
11//! - Supports common MPV commands like `set_property`, `seek`, and `playlist-next`
12//! - Flexible socket path configuration
13//!
14//! ## Example Usage
15//! ```rust
16//! use serde_json::json;
17//! use tokio;
18//! use mrc::{send_ipc_command, playlist_next, set_property};
19//!
20//! #[tokio::main]
21//! async fn main() {
22//!     let result = playlist_next(None).await;
23//!     match result {
24//!         Ok(response) => println!("Playlist moved to next: {:?}", response),
25//!         Err(err) => eprintln!("Error: {:?}", err),
26//!     }
27//!
28//!     let property_result = set_property("volume", &json!(50), None).await;
29//!     match property_result {
30//!         Ok(response) => println!("Volume set: {:?}", response),
31//!         Err(err) => eprintln!("Error: {:?}", err),
32//!     }
33//! }
34//! ```
35//!
36//! ## Constants
37//!
38//! ### `SOCKET_PATH`
39//! Default path for the MPV IPC socket: `/tmp/mpvsocket`
40//!
41//! ## Functions
42
43use serde_json::{json, Value};
44use std::io::{self};
45use tokio::io::{AsyncReadExt, AsyncWriteExt};
46use tokio::net::UnixStream;
47use tracing::{debug, error};
48
49pub const SOCKET_PATH: &str = "/tmp/mpvsocket";
50
51/// Sends a generic IPC command to the specified socket and returns the parsed response data.
52///
53/// # Arguments
54/// - `command`: The name of the command to send to MPV.
55/// - `args`: A slice of `Value` arguments to include in the command.
56/// - `socket_path`: An optional custom path to the MPV IPC socket. If `None`, the default path is used.
57///
58/// # Returns
59/// A `Result` containing an `Option<Value>` with the parsed response data if successful.
60///
61/// # Errors
62/// Returns an error if the connection to the socket fails or if the response cannot be parsed.
63pub async fn send_ipc_command(
64    command: &str,
65    args: &[Value],
66    socket_path: Option<&str>,
67) -> io::Result<Option<Value>> {
68    let socket_path = socket_path.unwrap_or(SOCKET_PATH);
69    debug!(
70        "Sending IPC command: {} with arguments: {:?}",
71        command, args
72    );
73
74    match UnixStream::connect(socket_path).await {
75        Ok(mut socket) => {
76            debug!("Connected to socket at {}", socket_path);
77
78            let mut command_array = vec![json!(command)];
79            command_array.extend_from_slice(args);
80            let message = json!({ "command": command_array });
81            let message_str = format!("{}\n", serde_json::to_string(&message)?);
82            debug!("Serialized message to send with newline: {}", message_str);
83
84            socket.write_all(message_str.as_bytes()).await?;
85            socket.flush().await?;
86            debug!("Message sent and flushed");
87
88            let mut response = vec![0; 1024];
89            let n = socket.read(&mut response).await?;
90            let response_str = String::from_utf8_lossy(&response[..n]);
91            debug!("Raw response: {}", response_str);
92
93            match serde_json::from_str::<Value>(&response_str) {
94                Ok(json_response) => {
95                    debug!("Parsed IPC response: {:?}", json_response);
96                    Ok(json_response.get("data").cloned())
97                }
98
99                Err(e) => {
100                    error!("Failed to parse response: {}", e);
101                    Ok(None)
102                }
103            }
104        }
105
106        Err(e) => {
107            error!("Failed to connect to MPV socket: {}", e);
108            Err(e)
109        }
110    }
111}
112
113/// Represents common MPV commands.
114///
115/// This enum provides variants for frequently used MPV commands, which can be converted to their
116/// string equivalents using the `as_str` method.
117///
118/// # Errors
119/// Returns an error if the connection to the socket fails or the command execution encounters issues.
120#[derive(Debug)]
121pub enum MpvCommand {
122    /// Sets a property to a specified value in MPV.
123    SetProperty,
124    /// Moves to the next item in the playlist.
125    PlaylistNext,
126    /// Moves to the previous item in the playlist.
127    PlaylistPrev,
128    /// Seeks to a specific time in the current media.
129    Seek,
130    /// Quits the MPV application.
131    Quit,
132    /// Moves an item in the playlist from one index to another.
133    PlaylistMove,
134    /// Removes an item from the playlist.
135    PlaylistRemove,
136    /// Clears all items from the playlist.
137    PlaylistClear,
138    /// Retrieves the value of a property in MPV.
139    GetProperty,
140    /// Loads a file into MPV.
141    LoadFile,
142}
143
144impl MpvCommand {
145    /// Converts MPV commands to their string equivalents.
146    ///
147    /// # Returns
148    /// A string slice representing the command.
149    #[must_use]
150    pub const fn as_str(&self) -> &str {
151        match self {
152            Self::SetProperty => "set_property",
153            Self::PlaylistNext => "playlist-next",
154            Self::PlaylistPrev => "playlist-prev",
155            Self::Seek => "seek",
156            Self::Quit => "quit",
157            Self::PlaylistMove => "playlist-move",
158            Self::PlaylistRemove => "playlist-remove",
159            Self::PlaylistClear => "playlist-clear",
160            Self::GetProperty => "get_property",
161            Self::LoadFile => "loadfile",
162        }
163    }
164}
165
166/// Sends the `set_property` command to MPV to change a property value.
167///
168/// # Arguments
169/// - `property`: The name of the property to set.
170/// - `value`: The new value to assign to the property.
171/// - `socket_path`: An optional custom socket path.
172///
173/// # Returns
174/// A `Result` containing the response data.
175///
176/// # Errors
177/// Returns an error if the connection to the socket fails or the command execution encounters issues.
178pub async fn set_property(
179    property: &str,
180    value: &Value,
181    socket_path: Option<&str>,
182) -> io::Result<Option<Value>> {
183    send_ipc_command(
184        MpvCommand::SetProperty.as_str(),
185        &[json!(property), value.clone()],
186        socket_path,
187    )
188    .await
189}
190
191/// Sends the `playlist-next` command to move to the next playlist item.
192///
193/// # Arguments
194/// - `socket_path`: An optional custom socket path.
195///
196/// # Returns
197/// A `Result` containing the response data.
198///
199/// # Errors
200/// Returns an error if the connection to the socket fails or the command execution encounters issues.
201pub async fn playlist_next(socket_path: Option<&str>) -> io::Result<Option<Value>> {
202    send_ipc_command(MpvCommand::PlaylistNext.as_str(), &[], socket_path).await
203}
204
205/// Sends the `playlist-prev` command to move to the previous playlist item.
206///
207/// # Arguments
208/// - `socket_path`: An optional custom socket path.
209///
210/// # Returns
211/// A `Result` containing the response data.
212///
213/// # Errors
214/// Returns an error if the connection to the socket fails or the command execution encounters issues.
215pub async fn playlist_prev(socket_path: Option<&str>) -> io::Result<Option<Value>> {
216    send_ipc_command(MpvCommand::PlaylistPrev.as_str(), &[], socket_path).await
217}
218
219/// Sends the `seek` command to seek the media playback by a given number of seconds.
220///
221/// # Arguments
222/// - `seconds`: The number of seconds to seek.
223/// - `socket_path`: An optional custom socket path.
224///
225/// # Returns
226/// A `Result` containing the response data.
227///
228/// # Errors
229/// Returns an error if the connection to the socket fails or the command execution encounters issues.
230pub async fn seek(seconds: f64, socket_path: Option<&str>) -> io::Result<Option<Value>> {
231    send_ipc_command(MpvCommand::Seek.as_str(), &[json!(seconds)], socket_path).await
232}
233
234/// Sends the `quit` command to terminate MPV.
235///
236/// # Arguments
237/// - `socket_path`: An optional custom socket path.
238///
239/// # Returns
240/// A `Result` containing the response data.
241///
242/// # Errors
243/// Returns an error if the connection to the socket fails or the command execution encounters issues.
244pub async fn quit(socket_path: Option<&str>) -> io::Result<Option<Value>> {
245    send_ipc_command(MpvCommand::Quit.as_str(), &[], socket_path).await
246}
247
248/// Sends the `playlist-move` command to move a playlist item from one index to another.
249///
250/// # Arguments
251/// - `from_index`: The index of the item to move.
252/// - `to_index`: The index to move the item to.
253/// - `socket_path`: An optional custom socket path.
254///
255/// # Returns
256/// A `Result` containing the response data.
257///
258/// # Errors
259/// Returns an error if the connection to the socket fails or the command execution encounters issues.
260pub async fn playlist_move(
261    from_index: usize,
262    to_index: usize,
263    socket_path: Option<&str>,
264) -> io::Result<Option<Value>> {
265    send_ipc_command(
266        MpvCommand::PlaylistMove.as_str(),
267        &[json!(from_index), json!(to_index)],
268        socket_path,
269    )
270    .await
271}
272
273/// Sends the `playlist-remove` command to remove an item from the playlist.
274///
275/// # Arguments
276/// - `index`: The index of the item to remove, or `None` to remove the current item.
277/// - `socket_path`: An optional custom socket path.
278///
279/// # Returns
280/// A `Result` containing the response data.
281///
282/// # Errors
283/// Returns an error if the connection to the socket fails or the command execution encounters issues.
284pub async fn playlist_remove(
285    index: Option<usize>,
286    socket_path: Option<&str>,
287) -> io::Result<Option<Value>> {
288    let args = match index {
289        Some(idx) => vec![json!(idx)],
290        None => vec![json!("current")],
291    };
292    send_ipc_command(MpvCommand::PlaylistRemove.as_str(), &args, socket_path).await
293}
294
295/// Sends the `playlist-clear` command to clear the playlist.
296///
297/// # Arguments
298/// - `socket_path`: An optional custom socket path.
299///
300/// # Returns
301/// A `Result` containing the response data.
302///
303/// # Errors
304/// Returns an error if the connection to the socket fails or the command execution encounters issues.
305pub async fn playlist_clear(socket_path: Option<&str>) -> io::Result<Option<Value>> {
306    send_ipc_command(MpvCommand::PlaylistClear.as_str(), &[], socket_path).await
307}
308
309/// Sends the `get_property` command to retrieve a property value from MPV.
310///
311/// # Arguments
312/// - `property`: The name of the property to retrieve.
313/// - `socket_path`: An optional custom socket path.
314///
315/// # Returns
316/// A `Result` containing the response data.
317///
318/// # Errors
319/// Returns an error if the connection to the socket fails or the command execution encounters issues.
320pub async fn get_property(property: &str, socket_path: Option<&str>) -> io::Result<Option<Value>> {
321    send_ipc_command(
322        MpvCommand::GetProperty.as_str(),
323        &[json!(property)],
324        socket_path,
325    )
326    .await
327}
328
329/// Sends the `loadfile` command to load a file into MPV.
330///
331/// # Arguments
332/// - `filename`: The name of the file to load.
333/// - `append`: Whether to append the file to the playlist (`true`) or replace the current file (`false`).
334/// - `socket_path`: An optional custom socket path.
335///
336/// # Returns
337/// A `Result` containing the response data.
338///
339/// # Errors
340/// Returns an error if the connection to the socket fails or the command execution encounters issues.
341pub async fn loadfile(
342    filename: &str,
343    append: bool,
344    socket_path: Option<&str>,
345) -> io::Result<Option<Value>> {
346    let append_flag = if append {
347        json!("append-play")
348    } else {
349        json!("replace")
350    };
351    send_ipc_command(
352        MpvCommand::LoadFile.as_str(),
353        &[json!(filename), append_flag],
354        socket_path,
355    )
356    .await
357}