cli: remove redundant import

This commit is contained in:
raf 2025-06-12 17:58:11 +03:00
commit f21b0941a1
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 107 additions and 78 deletions

2
.envrc
View file

@ -1 +1 @@
use flake use flake . --substituters "https://cache.nixos.org"

View file

@ -1,12 +1,14 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use mrc::set_property;
use mrc::SOCKET_PATH; use mrc::SOCKET_PATH;
use mrc::{ use mrc::{
get_property, loadfile, playlist_clear, playlist_move, playlist_next, playlist_prev, MrcError, Result, get_property, loadfile, playlist_clear, playlist_move, playlist_next,
playlist_remove, quit, seek, MrcError, Result, playlist_prev, playlist_remove, quit, seek, set_property,
}; };
use serde_json::json; use serde_json::json;
use std::{io::{self, Write}, path::PathBuf}; use std::{
io::{self, Write},
path::PathBuf,
};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
#[derive(Parser)] #[derive(Parser)]
@ -165,8 +167,8 @@ async fn main() -> Result<()> {
CommandOptions::List => { CommandOptions::List => {
info!("Listing playlist items"); info!("Listing playlist items");
if let Some(data) = get_property("playlist", None).await? { if let Some(data) = get_property("playlist", None).await? {
let pretty_json = serde_json::to_string_pretty(&data) let pretty_json =
.map_err(MrcError::ParseError)?; serde_json::to_string_pretty(&data).map_err(MrcError::ParseError)?;
println!("{}", pretty_json); println!("{}", pretty_json);
} }
} }
@ -212,7 +214,9 @@ async fn main() -> Result<()> {
print!("mpv> "); print!("mpv> ");
stdout.flush().map_err(MrcError::ConnectionError)?; stdout.flush().map_err(MrcError::ConnectionError)?;
let mut input = String::new(); let mut input = String::new();
stdin.read_line(&mut input).map_err(MrcError::ConnectionError)?; stdin
.read_line(&mut input)
.map_err(MrcError::ConnectionError)?;
let trimmed = input.trim(); let trimmed = input.trim();
if trimmed.eq_ignore_ascii_case("exit") { if trimmed.eq_ignore_ascii_case("exit") {
@ -338,7 +342,9 @@ async fn main() -> Result<()> {
_ => { _ => {
println!("Unknown command: {}", trimmed); println!("Unknown command: {}", trimmed);
println!("Valid commands: play <index>, pause, stop, next, prev, seek <seconds>, clear, list, add <files>, get <property>, set <property> <value>, help, exit"); println!(
"Valid commands: play <index>, pause, stop, next, prev, seek <seconds>, clear, list, add <files>, get <property>, set <property> <value>, help, exit"
);
} }
} }
} }

View file

@ -40,7 +40,7 @@
//! //!
//! ## Functions //! ## Functions
use serde_json::{json, Value}; use serde_json::{Value, json};
use std::io; use std::io;
use thiserror::Error; use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
@ -55,43 +55,43 @@ pub enum MrcError {
/// Connection to the MPV socket could not be established. /// Connection to the MPV socket could not be established.
#[error("failed to connect to MPV socket: {0}")] #[error("failed to connect to MPV socket: {0}")]
ConnectionError(#[from] io::Error), ConnectionError(#[from] io::Error),
/// Error when parsing a JSON response from MPV. /// Error when parsing a JSON response from MPV.
#[error("failed to parse JSON response: {0}")] #[error("failed to parse JSON response: {0}")]
ParseError(#[from] serde_json::Error), ParseError(#[from] serde_json::Error),
/// Error when a socket operation times out. /// Error when a socket operation times out.
#[error("socket operation timed out after {0} seconds")] #[error("socket operation timed out after {0} seconds")]
SocketTimeout(u64), SocketTimeout(u64),
/// Error when MPV returns an error response. /// Error when MPV returns an error response.
#[error("MPV error: {0}")] #[error("MPV error: {0}")]
MpvError(String), MpvError(String),
/// Error when trying to use a property that doesn't exist. /// Error when trying to use a property that doesn't exist.
#[error("property '{0}' not found")] #[error("property '{0}' not found")]
PropertyNotFound(String), PropertyNotFound(String),
/// Error when the socket response is not valid UTF-8. /// Error when the socket response is not valid UTF-8.
#[error("invalid UTF-8 in socket response: {0}")] #[error("invalid UTF-8 in socket response: {0}")]
InvalidUtf8(#[from] std::string::FromUtf8Error), InvalidUtf8(#[from] std::string::FromUtf8Error),
/// Error when a network operation fails. /// Error when a network operation fails.
#[error("network error: {0}")] #[error("network error: {0}")]
NetworkError(String), NetworkError(String),
/// Error when the server connection is lost or broken. /// Error when the server connection is lost or broken.
#[error("server connection lost: {0}")] #[error("server connection lost: {0}")]
ConnectionLost(String), ConnectionLost(String),
/// Error when a communication protocol is violated. /// Error when a communication protocol is violated.
#[error("protocol error: {0}")] #[error("protocol error: {0}")]
ProtocolError(String), ProtocolError(String),
/// Error when invalid input is provided. /// Error when invalid input is provided.
#[error("invalid input: {0}")] #[error("invalid input: {0}")]
InvalidInput(String), InvalidInput(String),
/// Error related to TLS operations. /// Error related to TLS operations.
#[error("TLS error: {0}")] #[error("TLS error: {0}")]
TlsError(String), TlsError(String),
@ -100,6 +100,81 @@ pub enum MrcError {
/// A specialized Result type for MRC operations. /// A specialized Result type for MRC operations.
pub type Result<T> = std::result::Result<T, MrcError>; pub type Result<T> = std::result::Result<T, MrcError>;
/// Connects to the MPV IPC socket with timeout.
async fn connect_to_socket(socket_path: &str) -> Result<UnixStream> {
debug!("Connecting to socket at {}", socket_path);
tokio::time::timeout(
std::time::Duration::from_secs(5),
UnixStream::connect(socket_path),
)
.await
.map_err(|_| MrcError::SocketTimeout(5))?
.map_err(MrcError::ConnectionError)
}
/// Sends a command message to the socket with timeout.
async fn send_message(socket: &mut UnixStream, command: &str, args: &[Value]) -> Result<()> {
let mut command_array = vec![json!(command)];
command_array.extend_from_slice(args);
let message = json!({ "command": command_array });
let message_str = format!("{}\n", serde_json::to_string(&message)?);
debug!("Serialized message to send with newline: {}", message_str);
// Write with timeout
tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.write_all(message_str.as_bytes()),
)
.await
.map_err(|_| MrcError::SocketTimeout(5))??;
// Flush with timeout
tokio::time::timeout(std::time::Duration::from_secs(5), socket.flush())
.await
.map_err(|_| MrcError::SocketTimeout(5))??;
debug!("Message sent and flushed");
Ok(())
}
/// Reads and parses the response from the socket.
async fn read_response(socket: &mut UnixStream) -> Result<Value> {
let mut response = vec![0; 1024];
// Read with timeout
let n = tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.read(&mut response),
)
.await
.map_err(|_| MrcError::SocketTimeout(5))??;
if n == 0 {
return Err(MrcError::ConnectionLost(
"Socket closed unexpectedly".into(),
));
}
let response_str = String::from_utf8(response[..n].to_vec())?;
debug!("Raw response: {}", response_str);
let json_response =
serde_json::from_str::<Value>(&response_str).map_err(MrcError::ParseError)?;
debug!("Parsed IPC response: {:?}", json_response);
// Check if MPV returned an error
if let Some(error) = json_response.get("error").and_then(|e| e.as_str()) {
if !error.is_empty() {
return Err(MrcError::MpvError(error.to_string()));
}
}
Ok(json_response)
}
/// Sends a generic IPC command to the specified socket and returns the parsed response data. /// Sends a generic IPC command to the specified socket and returns the parsed response data.
/// ///
/// # Arguments /// # Arguments
@ -123,71 +198,10 @@ pub async fn send_ipc_command(
command, args command, args
); );
// Add timeout for connection let mut socket = connect_to_socket(socket_path).await?;
let stream = tokio::time::timeout( send_message(&mut socket, command, args).await?;
std::time::Duration::from_secs(5), let json_response = read_response(&mut socket).await?;
UnixStream::connect(socket_path),
)
.await
.map_err(|_| MrcError::SocketTimeout(5))?
.map_err(MrcError::ConnectionError)?;
let mut socket = stream;
debug!("Connected to socket at {}", socket_path);
let mut command_array = vec![json!(command)];
command_array.extend_from_slice(args);
let message = json!({ "command": command_array });
let message_str = format!("{}\n", serde_json::to_string(&message)?);
debug!("Serialized message to send with newline: {}", message_str);
// Write with timeout
tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.write_all(message_str.as_bytes()),
)
.await
.map_err(|_| MrcError::SocketTimeout(5))??;
// Flush with timeout
tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.flush(),
)
.await
.map_err(|_| MrcError::SocketTimeout(5))??;
debug!("Message sent and flushed");
let mut response = vec![0; 1024];
// Read with timeout
let n = tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.read(&mut response),
)
.await
.map_err(|_| MrcError::SocketTimeout(5))??;
if n == 0 {
return Err(MrcError::ConnectionLost("Socket closed unexpectedly".into()));
}
let response_str = String::from_utf8(response[..n].to_vec())?;
debug!("Raw response: {}", response_str);
let json_response = serde_json::from_str::<Value>(&response_str)
.map_err(MrcError::ParseError)?;
debug!("Parsed IPC response: {:?}", json_response);
// Check if MPV returned an error
if let Some(error) = json_response.get("error").and_then(|e| e.as_str()) {
if !error.is_empty() {
return Err(MrcError::MpvError(error.to_string()));
}
}
Ok(json_response.get("data").cloned()) Ok(json_response.get("data").cloned())
} }

View file

@ -9,7 +9,10 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_native_tls::TlsAcceptor; use tokio_native_tls::TlsAcceptor;
use tracing::{debug, error, info}; use tracing::{debug, error, info};
use mrc::{get_property, playlist_clear, playlist_next, playlist_prev, quit, seek, set_property, MrcError, Result as MrcResult}; use mrc::{
MrcError, Result as MrcResult, get_property, playlist_clear, playlist_next, playlist_prev,
quit, seek, set_property,
};
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about)] #[command(author, version, about)]
@ -27,11 +30,15 @@ async fn handle_connection(
stream: tokio::net::TcpStream, stream: tokio::net::TcpStream,
acceptor: Arc<TlsAcceptor>, acceptor: Arc<TlsAcceptor>,
) -> MrcResult<()> { ) -> MrcResult<()> {
let mut stream = acceptor.accept(stream).await let mut stream = acceptor
.accept(stream)
.await
.map_err(|e| MrcError::TlsError(e.to_string()))?; .map_err(|e| MrcError::TlsError(e.to_string()))?;
let mut buffer = vec![0; 2048]; let mut buffer = vec![0; 2048];
let n = stream.read(&mut buffer).await let n = stream
.read(&mut buffer)
.await
.map_err(MrcError::ConnectionError)?; .map_err(MrcError::ConnectionError)?;
let request = String::from_utf8_lossy(&buffer[..n]); let request = String::from_utf8_lossy(&buffer[..n]);
@ -143,15 +150,18 @@ async fn process_command(command: &str) -> MrcResult<String> {
info!("Listing playlist items"); info!("Listing playlist items");
match get_property("playlist", None).await { match get_property("playlist", None).await {
Ok(Some(data)) => { Ok(Some(data)) => {
let pretty_json = serde_json::to_string_pretty(&data) let pretty_json =
.map_err(MrcError::ParseError)?; serde_json::to_string_pretty(&data).map_err(MrcError::ParseError)?;
Ok(format!("Playlist: {}", pretty_json)) Ok(format!("Playlist: {}", pretty_json))
}, }
Ok(None) => Err(MrcError::PropertyNotFound("playlist".to_string())), Ok(None) => Err(MrcError::PropertyNotFound("playlist".to_string())),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
_ => Err(MrcError::InvalidInput(format!("Unknown command: {}", command))), _ => Err(MrcError::InvalidInput(format!(
"Unknown command: {}",
command
))),
} }
} }
@ -161,16 +171,15 @@ fn create_tls_acceptor() -> MrcResult<TlsAcceptor> {
let password = env::var("TLS_PASSWORD") let password = env::var("TLS_PASSWORD")
.map_err(|_| MrcError::InvalidInput("TLS_PASSWORD not set".to_string()))?; .map_err(|_| MrcError::InvalidInput("TLS_PASSWORD not set".to_string()))?;
let mut file = std::fs::File::open(&pfx_path) let mut file = std::fs::File::open(&pfx_path).map_err(MrcError::ConnectionError)?;
.map_err(MrcError::ConnectionError)?;
let mut identity = vec![]; let mut identity = vec![];
file.read_to_end(&mut identity) file.read_to_end(&mut identity)
.map_err(MrcError::ConnectionError)?; .map_err(MrcError::ConnectionError)?;
let identity = Identity::from_pkcs12(&identity, &password) let identity = Identity::from_pkcs12(&identity, &password)
.map_err(|e| MrcError::TlsError(e.to_string()))?; .map_err(|e| MrcError::TlsError(e.to_string()))?;
let native_acceptor = NativeTlsAcceptor::new(identity) let native_acceptor =
.map_err(|e| MrcError::TlsError(e.to_string()))?; NativeTlsAcceptor::new(identity).map_err(|e| MrcError::TlsError(e.to_string()))?;
Ok(TlsAcceptor::from(native_acceptor)) Ok(TlsAcceptor::from(native_acceptor))
} }
@ -194,13 +203,13 @@ async fn main() -> MrcResult<()> {
match create_tls_acceptor() { match create_tls_acceptor() {
Ok(acceptor) => { Ok(acceptor) => {
let acceptor = Arc::new(acceptor); let acceptor = Arc::new(acceptor);
let listener = tokio::net::TcpListener::bind(&config.bind).await let listener = tokio::net::TcpListener::bind(&config.bind)
.await
.map_err(MrcError::ConnectionError)?; .map_err(MrcError::ConnectionError)?;
info!("Server is listening on {}", config.bind); info!("Server is listening on {}", config.bind);
loop { loop {
let (stream, _) = listener.accept().await let (stream, _) = listener.accept().await.map_err(MrcError::ConnectionError)?;
.map_err(MrcError::ConnectionError)?;
info!("New connection accepted."); info!("New connection accepted.");
let acceptor = Arc::clone(&acceptor); let acceptor = Arc::clone(&acceptor);