mirror of
https://github.com/NotAShelf/mpvrc.git
synced 2026-04-16 16:03:48 +00:00
mrc: refactor interactive mode into its own module
This commit is contained in:
parent
f21b0941a1
commit
74f2927b86
3 changed files with 186 additions and 156 deletions
149
src/cli.rs
149
src/cli.rs
|
|
@ -1,14 +1,12 @@
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use mrc::SOCKET_PATH;
|
use mrc::SOCKET_PATH;
|
||||||
|
use mrc::interactive::InteractiveMode;
|
||||||
use mrc::{
|
use mrc::{
|
||||||
MrcError, Result, get_property, loadfile, playlist_clear, playlist_move, playlist_next,
|
MrcError, Result, get_property, loadfile, playlist_clear, playlist_move, playlist_next,
|
||||||
playlist_prev, playlist_remove, quit, seek, set_property,
|
playlist_prev, playlist_remove, quit, seek, set_property,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
io::{self, Write},
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -206,148 +204,7 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandOptions::Interactive => {
|
CommandOptions::Interactive => {
|
||||||
println!("Entering interactive mode. Type 'exit' to quit.");
|
InteractiveMode::run().await?;
|
||||||
let stdin = io::stdin();
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
print!("mpv> ");
|
|
||||||
stdout.flush().map_err(MrcError::ConnectionError)?;
|
|
||||||
let mut input = String::new();
|
|
||||||
stdin
|
|
||||||
.read_line(&mut input)
|
|
||||||
.map_err(MrcError::ConnectionError)?;
|
|
||||||
let trimmed = input.trim();
|
|
||||||
|
|
||||||
if trimmed.eq_ignore_ascii_case("exit") {
|
|
||||||
println!("Exiting interactive mode.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// I don't like this either, but it looks cleaner than a multi-line
|
|
||||||
// print macro just cramped in here.
|
|
||||||
let commands = vec![
|
|
||||||
(
|
|
||||||
"play [index]",
|
|
||||||
"Play or unpause playback, optionally at the specified index",
|
|
||||||
),
|
|
||||||
("pause", "Pause playback"),
|
|
||||||
("stop", "Stop playback and quit MPV"),
|
|
||||||
("next", "Skip to the next item in the playlist"),
|
|
||||||
("prev", "Skip to the previous item in the playlist"),
|
|
||||||
("seek <seconds>", "Seek to the specified position"),
|
|
||||||
("clear", "Clear the playlist"),
|
|
||||||
("list", "List all items in the playlist"),
|
|
||||||
("add <files>", "Add files to the playlist"),
|
|
||||||
("get <property>", "Get the specified property"),
|
|
||||||
(
|
|
||||||
"set <property> <value>",
|
|
||||||
"Set the specified property to a value",
|
|
||||||
),
|
|
||||||
("help", "Show this help message"),
|
|
||||||
("exit", "Quit interactive mode"),
|
|
||||||
];
|
|
||||||
|
|
||||||
if trimmed.eq_ignore_ascii_case("help") {
|
|
||||||
println!("Valid commands:");
|
|
||||||
for (command, description) in commands {
|
|
||||||
println!(" {} - {}", command, description);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parts: Vec<&str> = trimmed.split_whitespace().collect();
|
|
||||||
match parts.as_slice() {
|
|
||||||
["play"] => {
|
|
||||||
info!("Unpausing playback");
|
|
||||||
set_property("pause", &json!(false), None).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
["play", index] => {
|
|
||||||
if let Ok(idx) = index.parse::<usize>() {
|
|
||||||
info!("Playing media at index: {}", idx);
|
|
||||||
set_property("playlist-pos", &json!(idx), None).await?;
|
|
||||||
set_property("pause", &json!(false), None).await?;
|
|
||||||
} else {
|
|
||||||
println!("Invalid index: {}", index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
["pause"] => {
|
|
||||||
info!("Pausing playback");
|
|
||||||
set_property("pause", &json!(true), None).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
["stop"] => {
|
|
||||||
info!("Pausing playback");
|
|
||||||
quit(None).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
["next"] => {
|
|
||||||
info!("Skipping to next item in the playlist");
|
|
||||||
playlist_next(None).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
["prev"] => {
|
|
||||||
info!("Skipping to previous item in the playlist");
|
|
||||||
playlist_prev(None).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
["seek", seconds] => {
|
|
||||||
if let Ok(sec) = seconds.parse::<i32>() {
|
|
||||||
info!("Seeking to {} seconds", sec);
|
|
||||||
seek(sec.into(), None).await?;
|
|
||||||
} else {
|
|
||||||
println!("Invalid seconds: {}", seconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
["clear"] => {
|
|
||||||
info!("Clearing the playlist");
|
|
||||||
playlist_clear(None).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
["list"] => {
|
|
||||||
info!("Listing playlist items");
|
|
||||||
if let Some(data) = get_property("playlist", None).await? {
|
|
||||||
let pretty_json = serde_json::to_string_pretty(&data)
|
|
||||||
.map_err(MrcError::ParseError)?;
|
|
||||||
println!("{}", pretty_json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
["add", files @ ..] => {
|
|
||||||
if files.is_empty() {
|
|
||||||
println!("No files provided to add to the playlist");
|
|
||||||
} else {
|
|
||||||
info!("Adding {} files to the playlist", files.len());
|
|
||||||
for file in files {
|
|
||||||
loadfile(file, true, None).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
["get", property] => {
|
|
||||||
if let Some(data) = get_property(property, None).await? {
|
|
||||||
println!("{property}: {data}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
["set", property, value] => {
|
|
||||||
let json_value = serde_json::from_str::<serde_json::Value>(value)
|
|
||||||
.unwrap_or_else(|_| json!(value));
|
|
||||||
set_property(property, &json_value, None).await?;
|
|
||||||
println!("Set {property} to {value}");
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
168
src/interactive.rs
Normal file
168
src/interactive.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
use crate::{
|
||||||
|
MrcError, Result, get_property, loadfile, playlist_clear, playlist_next, playlist_prev, quit,
|
||||||
|
seek, set_property,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub struct InteractiveMode;
|
||||||
|
|
||||||
|
impl InteractiveMode {
|
||||||
|
pub async fn run() -> Result<()> {
|
||||||
|
println!("Entering interactive mode. Type 'help' for commands or 'exit' to quit.");
|
||||||
|
let stdin = io::stdin();
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
print!("mpv> ");
|
||||||
|
stdout.flush().map_err(MrcError::ConnectionError)?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
stdin
|
||||||
|
.read_line(&mut input)
|
||||||
|
.map_err(MrcError::ConnectionError)?;
|
||||||
|
let trimmed = input.trim();
|
||||||
|
|
||||||
|
if trimmed.eq_ignore_ascii_case("exit") {
|
||||||
|
println!("Exiting interactive mode.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if trimmed.eq_ignore_ascii_case("help") {
|
||||||
|
Self::show_help();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = Self::process_command(trimmed).await {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_help() {
|
||||||
|
println!("Available commands:");
|
||||||
|
let commands = [
|
||||||
|
(
|
||||||
|
"play [index]",
|
||||||
|
"Play or unpause playback, optionally at the specified index",
|
||||||
|
),
|
||||||
|
("pause", "Pause playback"),
|
||||||
|
("stop", "Stop playback and quit MPV"),
|
||||||
|
("next", "Skip to the next item in the playlist"),
|
||||||
|
("prev", "Skip to the previous item in the playlist"),
|
||||||
|
("seek <seconds>", "Seek to the specified position"),
|
||||||
|
("clear", "Clear the playlist"),
|
||||||
|
("list", "List all items in the playlist"),
|
||||||
|
("add <files>", "Add files to the playlist"),
|
||||||
|
("get <property>", "Get the specified property"),
|
||||||
|
(
|
||||||
|
"set <property> <value>",
|
||||||
|
"Set the specified property to a value",
|
||||||
|
),
|
||||||
|
("help", "Show this help message"),
|
||||||
|
("exit", "Quit interactive mode"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (command, description) in commands {
|
||||||
|
println!(" {} - {}", command, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_command(input: &str) -> Result<()> {
|
||||||
|
let parts: Vec<&str> = input.split_whitespace().collect();
|
||||||
|
|
||||||
|
match parts.as_slice() {
|
||||||
|
["play"] => {
|
||||||
|
info!("Unpausing playback");
|
||||||
|
set_property("pause", &json!(false), None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
["play", index] => {
|
||||||
|
if let Ok(idx) = index.parse::<usize>() {
|
||||||
|
info!("Playing media at index: {}", idx);
|
||||||
|
set_property("playlist-pos", &json!(idx), None).await?;
|
||||||
|
set_property("pause", &json!(false), None).await?;
|
||||||
|
} else {
|
||||||
|
println!("Invalid index: {}", index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
["pause"] => {
|
||||||
|
info!("Pausing playback");
|
||||||
|
set_property("pause", &json!(true), None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
["stop"] => {
|
||||||
|
info!("Stopping playback and quitting MPV");
|
||||||
|
quit(None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
["next"] => {
|
||||||
|
info!("Skipping to next item in the playlist");
|
||||||
|
playlist_next(None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
["prev"] => {
|
||||||
|
info!("Skipping to previous item in the playlist");
|
||||||
|
playlist_prev(None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
["seek", seconds] => {
|
||||||
|
if let Ok(sec) = seconds.parse::<i32>() {
|
||||||
|
info!("Seeking to {} seconds", sec);
|
||||||
|
seek(sec.into(), None).await?;
|
||||||
|
} else {
|
||||||
|
println!("Invalid seconds: {}", seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
["clear"] => {
|
||||||
|
info!("Clearing the playlist");
|
||||||
|
playlist_clear(None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
["list"] => {
|
||||||
|
info!("Listing playlist items");
|
||||||
|
if let Some(data) = get_property("playlist", None).await? {
|
||||||
|
let pretty_json =
|
||||||
|
serde_json::to_string_pretty(&data).map_err(MrcError::ParseError)?;
|
||||||
|
println!("{}", pretty_json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
["add", files @ ..] => {
|
||||||
|
if files.is_empty() {
|
||||||
|
println!("No files provided to add to the playlist");
|
||||||
|
} else {
|
||||||
|
info!("Adding {} files to the playlist", files.len());
|
||||||
|
for file in files {
|
||||||
|
loadfile(file, true, None).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
["get", property] => {
|
||||||
|
if let Some(data) = get_property(property, None).await? {
|
||||||
|
println!("{}: {}", property, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
["set", property, value] => {
|
||||||
|
let json_value = serde_json::from_str::<serde_json::Value>(value)
|
||||||
|
.unwrap_or_else(|_| json!(value));
|
||||||
|
set_property(property, &json_value, None).await?;
|
||||||
|
println!("Set {} to {}", property, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
println!("Unknown command: {}", input);
|
||||||
|
println!("Type 'help' for a list of available commands.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/lib.rs
25
src/lib.rs
|
|
@ -38,7 +38,8 @@
|
||||||
//! ### `SOCKET_PATH`
|
//! ### `SOCKET_PATH`
|
||||||
//! Default path for the MPV IPC socket: `/tmp/mpvsocket`
|
//! Default path for the MPV IPC socket: `/tmp/mpvsocket`
|
||||||
//!
|
//!
|
||||||
//! ## Functions
|
|
||||||
|
pub mod interactive;
|
||||||
|
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
@ -48,6 +49,7 @@ use tokio::net::UnixStream;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
pub const SOCKET_PATH: &str = "/tmp/mpvsocket";
|
pub const SOCKET_PATH: &str = "/tmp/mpvsocket";
|
||||||
|
const SOCKET_TIMEOUT_SECS: u64 = 5;
|
||||||
|
|
||||||
/// Errors that can occur when interacting with the MPV IPC interface.
|
/// Errors that can occur when interacting with the MPV IPC interface.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
@ -105,11 +107,11 @@ async fn connect_to_socket(socket_path: &str) -> Result<UnixStream> {
|
||||||
debug!("Connecting to socket at {}", socket_path);
|
debug!("Connecting to socket at {}", socket_path);
|
||||||
|
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(5),
|
std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS),
|
||||||
UnixStream::connect(socket_path),
|
UnixStream::connect(socket_path),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| MrcError::SocketTimeout(5))?
|
.map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))?
|
||||||
.map_err(MrcError::ConnectionError)
|
.map_err(MrcError::ConnectionError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,16 +126,19 @@ async fn send_message(socket: &mut UnixStream, command: &str, args: &[Value]) ->
|
||||||
|
|
||||||
// Write with timeout
|
// Write with timeout
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(5),
|
std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS),
|
||||||
socket.write_all(message_str.as_bytes()),
|
socket.write_all(message_str.as_bytes()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| MrcError::SocketTimeout(5))??;
|
.map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))??;
|
||||||
|
|
||||||
// Flush with timeout
|
// Flush with timeout
|
||||||
tokio::time::timeout(std::time::Duration::from_secs(5), socket.flush())
|
tokio::time::timeout(
|
||||||
.await
|
std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS),
|
||||||
.map_err(|_| MrcError::SocketTimeout(5))??;
|
socket.flush(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))??;
|
||||||
|
|
||||||
debug!("Message sent and flushed");
|
debug!("Message sent and flushed");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -145,11 +150,11 @@ async fn read_response(socket: &mut UnixStream) -> Result<Value> {
|
||||||
|
|
||||||
// Read with timeout
|
// Read with timeout
|
||||||
let n = tokio::time::timeout(
|
let n = tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(5),
|
std::time::Duration::from_secs(SOCKET_TIMEOUT_SECS),
|
||||||
socket.read(&mut response),
|
socket.read(&mut response),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| MrcError::SocketTimeout(5))??;
|
.map_err(|_| MrcError::SocketTimeout(SOCKET_TIMEOUT_SECS))??;
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return Err(MrcError::ConnectionLost(
|
return Err(MrcError::ConnectionLost(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue