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}