From 1f1a94f7472c3fc2c3e007a086c8cc17802965c4 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 29 Mar 2026 21:11:07 +0300 Subject: [PATCH] meta: rename to 'mpvrc' mrc is taken :( Signed-off-by: NotAShelf Change-Id: I2cf0cab8dce04cd04981177d43b9b9b76a6a6964 --- README.md | 410 +++++++++++++++++++++++++-------------------- flake.nix | 4 +- nix/package.nix | 9 +- nix/shell.nix | 4 +- src/cli.rs | 51 ++++-- src/commands.rs | 72 +++++++- src/interactive.rs | 40 +++-- src/lib.rs | 28 ++-- src/server.rs | 38 ++--- 9 files changed, 388 insertions(+), 268 deletions(-) diff --git a/README.md b/README.md index 2482870b..6bf70ba2 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,235 @@ -# MRC - MPV Remote Control +# mpvrc - MPV Remote Control [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) +[![crates.io](https://img.shields.io/crates/v/mpvrc.svg)](https://crates.io/crates/mpvrc) [mpv video player]: https://mpv.io/ -**MRC** is a fast, robust JSON IPC wrapper for the [mpv video player]. It -provides: - -- Type-safe MPV IPC communication -- Command-line interface with interactive mode -- Secure remote control over HTTPS -- Async Support with Tokio for high performance +mpvrc is an unofficial command-line tool and library for controlling the +[mpv video player] via its JSON IPC socket interface. Generally designed as a +scriptable playback controller and a real-time interactive REPL in addition to a +TLS-encrypted HTTP API for _remote_ control. mpvrc has you covered for anything +that is even vaguely related to controlling a MPV instance. ## Features -- **Complete MPV Control**: Play, pause, seek, playlist management, and property - access -- **Interactive CLI**: Real-time command interface with help system -- **Secure Remote Access**: TLS-encrypted server with token authentication -- **Flexible Configuration**: Custom socket paths and environment-based setup +The project is equipped with various neat features such as, but not limited to: + +- **Full playback control**: play, pause, stop, seek, skip, and playlist + management +- **Interactive REPL**: vi-style editing, command history, and keyboard + shortcuts +- **HTTP server**: TLS-encrypted remote control with token authentication +- **Library API**: use MRC as a Rust crate in your own projects + +It's also async-first, for high-performance concurrency. + +## Prerequisites + +1. **MPV** with IPC support (any recent version) +2. **Rust** 1.92.0+ (or Nix for shell-based development) + +## Installation + +### From source + +```bash +# Clone and build using a recent cargo version +$ git clone https://github.com/notashelf/mpvrc.git +$ cd mpvrc + +# Build in release mode +$ cargo build --release +``` + +### With Nix + +The recommended way of building mpvrc is using Nix to acquire a developer shell. + +```bash +# Clone and build using cargo from the provided shell +$ git clone https://github.com/notashelf/mpvrc.git +$ cd mpvrc +$ nix develop +$ cargo build --release +``` + +### From crates.io + +You can also get mpvrc from . + +```bash +# Install to ~/.cargo/bin +$ cargo install mpvrc --locked +``` ## Quick Start -### Prerequisites +### 1. Start MPV with IPC -1. **Install MPV** with IPC support enabled -2. **Install Rust** (edition 2024+) - -### Installation +Using mpvrc is quite simple. Start `mpv` with an IPC socket first: ```bash -# Clone the repository -git clone https://github.com/notashelf/mrc.git -cd mrc - -# Build the project -cargo build --release - -# Or install directly -cargo install --path . +# `/tmp/mpvsocket` is the default socket path for mpvrc +$ mpv --idle --input-ipc-server=/tmp/mpvsocket ``` -### Basic Usage +### 2. Control playback -1. **Start MPV with IPC socket**: +Then, once the socket is up, you may use `mpvrc` to control MPV. - ```bash - mpv --idle --input-ipc-server=/tmp/mpvsocket - ``` +```bash +# Play/pause +$ mpvrc play +$ mpvrc pause -2. **Use the CLI**: +# Navigation +$ mpvrc next +$ mpvrc prev - ```bash - # Basic playback control - cargo run --bin cli play - cargo run --bin cli pause - cargo run --bin cli next +# Seek 2 minutes forward +$ mpvrc seek 120 - # Load files - cargo run --bin cli add ~/Music/song.mp3 ~/Videos/movie.mkv +# Add files to playlist +$ mpvrc add ~/Videos/movie.mkv ~/Videos/episode1.mkv - # Interactive mode - cargo run --bin cli interactive - ``` +# View playlist +$ mpvrc list -## Usage Guide +# Interactive mode +$ mpvrc interactive +``` -### CLI Commands +## CLI Options -| Command | Description | Example | -| :--------------------- | -------------------------- | :---------------------------: | -| `play [index]` | Start/resume playback | `mrc play` or `mrc play 2` | -| `pause` | Pause playback | `mrc pause` | -| `stop` | Stop and quit MPV | `mrc stop` | -| `next` | Skip to next playlist item | `mrc next` | -| `prev` | Skip to previous item | `mrc prev` | -| `seek ` | Seek to position | `mrc seek 120` | -| `add ` | Add files to playlist | `mrc add song1.mp3 song2.mp3` | -| `remove [index]` | Remove playlist item | `mrc remove 0` | -| `move ` | Move playlist item | `mrc move 0 3` | -| `clear` | Clear playlist | `mrc clear` | -| `list` | Show playlist | `mrc list` | -| `prop ` | Get properties | `mrc prop volume duration` | -| `interactive` | Enter interactive mode | `mrc interactive` | +```plaintext +Usage: mpvrc [OPTIONS] + +Options: + -s, --socket Path to MPV IPC socket [default: /tmp/mpvsocket] + -y, --yes Skip confirmation prompts for destructive commands + -d, --debug Enable debug logging + -h, --help Print help + -V, --version Print version +``` + +## Commands + + + +| Command | Description | Example | +| :------------------- | ------------------------------ | :----------------------------- | +| `play [index]` | Start/resume playback | `mpvrc play` or `mpvrc play 2` | +| `pause` | Pause playback | `mpvrc pause` | +| `stop` | Stop and quit MPV | `mpvrc stop` | +| `next` | Skip to next playlist item | `mpvrc next` | +| `prev` | Skip to previous item | `mpvrc prev` | +| `seek ` | Seek to position in seconds | `mpvrc seek 120` | +| `add ` | Add files to playlist | `mpvrc add a.mp3 b.mp3` | +| `remove [index]` | Remove item (default: current) | `mpvrc remove 0` | +| `move ` | Move playlist item | `mpvrc move 0 3` | +| `clear` | Clear playlist | `mpvrc clear` | +| `list` | Show playlist | `mpvrc list` | +| `prop ` | Get property values | `mpvrc prop volume` | +| `interactive` | Enter interactive REPL | `mpvrc interactive` | +| `completion ` | Generate shell completions | `mpvrc completion zsh` | + + + +### Shell Completions + +Generate completions for your shell: + +```bash +# Zsh +$ mpvrc completion zsh > ~/.zsh/completions/_mpvrc + +# Bash +$ mpvrc completion bash > /etc/bash_completion.d/mpvrc + +# Fish +$ mpvrc completion fish > ~/.config/fish/completions/mpvrc.fish +``` ### Interactive Mode -Enter interactive mode for real-time control: - ```bash -$ cargo run --bin cli interactive -Entering interactive mode. Type 'exit' to quit. +$ mpvrc interactive +Entering interactive mode. Type 'help' for commands or 'exit' to quit. +Socket: /tmp/mpvsocket + mpv> play mpv> seek 60 -mpv> set volume 80 -mpv> get filename -mpv> help +mpv> list + 0 | Episode 1 | /path/to/episode1.mkv +> 1 | Episode 2 | /path/to/episode2.mkv + 2 | Episode 3 | /path/to/episode3.mkv mpv> exit ``` -### Library Usage +Keyboard shortcuts in interactive mode: -Basic example: +- `Ctrl+C` - Interrupt current command +- `Ctrl+L` - Clear screen +- `Ctrl+D` - Exit (EOF) + +## Server Mode + +Run MRC as a TLS-encrypted HTTP server for remote control: + +### 1. Generate TLS certificates + +```bash +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes +openssl pkcs12 -export -out identity.pfx -inkey key.pem -in cert.pem +``` + +### 2. Configure environment + +```bash +export TLS_PFX_PATH="./identity.pfx" +export TLS_PASSWORD="your_password" +export AUTH_TOKEN="your_secret_token" +``` + +### 3. Start server + +```bash +mpvrc server --bind 127.0.0.1:8080 --socket /tmp/mpvsocket +``` + +### 4. Send commands + +```bash +curl -k -H "Authorization: Bearer your_secret_token" \ + -d "play" https://127.0.0.1:8080/ +``` + +## Library Usage + +Add MRC to your `Cargo.toml`: + +```toml +[dependencies] +mpvrc = "0.2" +tokio = { version = "1", features = ["full"] } +serde_json = "1" +``` + +Control MPV from Rust: ```rust -use mrc::{playlist_next, set_property, get_property}; +use mpvrc::{get_property, playlist_next, set_property}; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { - // Set volume to 50% - set_property("volume", &json!(50), None).await?; + // Set volume + set_property("volume", &json!(75), None).await?; // Get current filename if let Some(filename) = get_property("filename", None).await? { - println!("Playing: {}", filename); + println!("Now playing: {}", filename); } // Skip to next track @@ -124,134 +239,67 @@ async fn main() -> Result<(), Box> { } ``` -### Server Mode - -For remote control over HTTPS: - -1. **Generate TLS certificates**: - - ```bash - # Create self-signed certificate - openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out certificate.pem -days 365 -nodes - - # Convert to PKCS#12 format - openssl pkcs12 -export -out identity.pfx -inkey private_key.pem -in certificate.pem - ``` - -2. **Set environment variables**: - - ```bash - export TLS_PFX_PATH="./identity.pfx" - export TLS_PASSWORD="your_pfx_password" - export AUTH_TOKEN="your_secure_token" - ``` - -3. **Start the server**: - - ```bash - cargo run --bin server - # Server starts on https://127.0.0.1:8080 - ``` - -4. **Make requests**: - - ```bash - # Using curl - curl -k -H "Authorization: Bearer your_secure_token" \ - -d "pause" https://127.0.0.1:8080/ - - # Using any HTTP client - POST https://127.0.0.1:8080/ - Authorization: Bearer your_secure_token - Content-Type: text/plain - - play - ``` - ## Configuration ### Environment Variables -| Variable | Description | Default | Required | -| -------------- | -------------------------------- | ------- | ----------- | -| `TLS_PFX_PATH` | Path to PKCS#12 certificate file | - | Server only | -| `TLS_PASSWORD` | Password for PKCS#12 file | - | Server only | -| `AUTH_TOKEN` | Authentication token for server | - | Server only | +| Variable | Description | Required | +| -------------- | ------------------------------- | -------- | +| `TLS_PFX_PATH` | Path to PKCS#12 certificate | Server | +| `TLS_PASSWORD` | Password for PKCS#12 file | Server | +| `AUTH_TOKEN` | Bearer token for authentication | Server | -### Socket Path +### Custom Socket Path -By default, MRC uses `/tmp/mpvsocket`. You can customize this: - -```rust -// In your code -use mrc::set_property; - -set_property("volume", &json!(50), Some("/your/socket/path")).await?; -``` - -## Development - -### Building +Use `-s` or `--socket` to specify a different MPV socket: ```bash -# Development build -cargo build +# As of 0.3.0, mvrc supports custom socket path +$ mpvrc -s /var/run/mpv/socket play +``` + +## Building & Development + +The recommended developer setup for working with mpvrc is using Nix. Use +`nix develop` or `direnv allow` to enter a reproducible devshell with the +expected tooling, then work with Rust sources as usual: + +```bash +# Build +$ cargo build # Release build -cargo build --release +$ cargo build --release # Run tests -cargo test +$ cargo test -# Check linting -cargo clippy -``` - -### Testing MPV IPC - -Test IPC communication manually: - -```bash -# Send raw command to MPV socket -echo '{ "command": ["get_property", "volume"] }' | socat - /tmp/mpvsocket - -# Expected response -{"data":50,"error":"success","request_id":0} +# Lint +$ cargo clippy --all-targets ``` ## Troubleshooting -### Common Issues - -**Socket not found error**: - -- Ensure MPV is running: `mpv --idle --input-ipc-server=/tmp/mpvsocket` -- Check socket exists: `ls -la /tmp/mpvsocket` - -**Server certificate errors**: - -- Verify certificate files exist and are readable -- Check environment variables are set correctly -- Test certificate: `openssl pkcs12 -info -in identity.pfx` - -**Permission denied**: - -- Check socket permissions: `ls -la /tmp/mpvsocket` -- Run MPV with appropriate user permissions - -### Debug Mode - -Enable debug logging: +### Socket not found ```bash -RUST_LOG=debug cargo run --bin cli interactive +# Verify MPV is running with IPC +ls -la /tmp/mpvsocket + +# Restart MPV with correct socket path +mpv --idle --input-ipc-server=/tmp/mpvsocket +``` + +### Debug output + +```bash +mpvrc -d interactive +# or +RUST_LOG=debug mpvrc interactive ``` ## License -This project is licensed under the Mozilla Public License 2.0 - see the -[LICENSE](LICENSE) file for details. - -## Acknowledgments - -- [MPV Media Player](https://mpv.io/) for the excellent IPC interface +This project is made available under Mozilla Public License (MPL) version 2.0. +See [LICENSE](LICENSE) for more details on the exact conditions. An online copy +is provided [here](https://www.mozilla.org/en-US/MPL/2.0/). diff --git a/flake.nix b/flake.nix index 18f2608f..67b64d53 100644 --- a/flake.nix +++ b/flake.nix @@ -12,8 +12,8 @@ pkgsForEach = nixpkgs.legacyPackages; in rec { packages = forEachSystem (system: { - mrc = pkgsForEach.${system}.callPackage ./nix/package.nix {}; - default = self.packages.${system}.mrc; + mpvrc = pkgsForEach.${system}.callPackage ./nix/package.nix {}; + default = self.packages.${system}.mpvrc; }); devShells = forEachSystem (system: { diff --git a/nix/package.nix b/nix/package.nix index 8431f6e4..07150283 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -8,8 +8,8 @@ s = ../.; in rustPlatform.buildRustPackage (finalAttrs: { - pname = "mrc"; - version = (builtins.fromTOML (builtins.readFile (s + /Cargo.toml))).package.version; + pname = "mpvrc"; + version = (lib.importTOML (s + /Cargo.toml)).package.version; src = fs.toSource { root = s; @@ -32,12 +32,11 @@ in ]; cargoLock.lockFile = "${finalAttrs.src}/Cargo.lock"; - useFetchCargoVendor = true; meta = { description = "IPC wrapper & command-line controller for MPV, the video player"; - homePage = "https://github.com/notashelf/mrc"; - mainProgram = "mrc"; + homePage = "https://github.com/notashelf/mpvrc"; + mainProgram = "mpvrc"; license = lib.licenses.mpl20; maintainers = [lib.maintainers.NotAShelf]; }; diff --git a/nix/shell.nix b/nix/shell.nix index c3c816b9..bc24eff0 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -4,12 +4,13 @@ rustfmt, clippy, cargo, + taplo, openssl, pkg-config, rustc, }: mkShell { - name = "mrc"; + name = "mpvrc"; packages = [ cargo rustc @@ -17,6 +18,7 @@ mkShell { rust-analyzer clippy (rustfmt.override {asNightly = true;}) + taplo # For TLS and friends openssl diff --git a/src/cli.rs b/src/cli.rs index a6574b79..f6eb35e5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,10 @@ +use std::{ + io::{self, Write}, + path::PathBuf, +}; + use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; -use mrc::commands::Commands; -use mrc::interactive::InteractiveMode; -use mrc::{MrcError, Result, SOCKET_PATH}; -use std::io::{self, Write}; -use std::path::PathBuf; +use mpvrc::{MrcError, Result, SOCKET_PATH, commands::Commands, interactive::InteractiveMode}; use tracing::{debug, error}; #[derive(Parser)] @@ -115,27 +116,42 @@ pub enum Shell { impl Shell { pub fn generate(&self, app: &mut clap::Command) { match self { - Shell::Bash => { - clap_complete::generate(clap_complete::Shell::Bash, app, "mrc", &mut io::stdout()) + Self::Bash => { + clap_complete::generate( + clap_complete::Shell::Bash, + app, + "mpvrc", + &mut io::stdout(), + ); } - Shell::Elvish => { - clap_complete::generate(clap_complete::Shell::Elvish, app, "mrc", &mut io::stdout()) + Self::Elvish => { + clap_complete::generate( + clap_complete::Shell::Elvish, + app, + "mpvrc", + &mut io::stdout(), + ); } - Shell::Fish => { - clap_complete::generate(clap_complete::Shell::Fish, app, "mrc", &mut io::stdout()) + Self::Fish => { + clap_complete::generate( + clap_complete::Shell::Fish, + app, + "mpvrc", + &mut io::stdout(), + ); } - Shell::PowerShell => clap_complete::generate( + Self::PowerShell => clap_complete::generate( clap_complete::Shell::PowerShell, app, - "mrc", + "mpvrc", &mut io::stdout(), ), - Shell::Zsh => { - clap_complete::generate(clap_complete::Shell::Zsh, app, "mrc", &mut io::stdout()) + Self::Zsh => { + clap_complete::generate(clap_complete::Shell::Zsh, app, "mpvrc", &mut io::stdout()); } } } @@ -151,7 +167,7 @@ fn confirm(prompt: &str, yes: bool) -> bool { if yes { return true; } - print!("{} [y/N] ", prompt); + print!("{prompt} [y/N] "); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); @@ -220,7 +236,7 @@ async fn main() -> Result<()> { CommandOptions::Remove { index } => { if confirm( - &format!("This will remove item at index {:?}. Continue?", index), + &format!("This will remove item at index {index:?}. Continue?"), cli.yes, ) { Commands::remove_item(index, Some(&socket_path)).await?; @@ -262,4 +278,3 @@ async fn main() -> Result<()> { Ok(()) } - diff --git a/src/commands.rs b/src/commands.rs index 0bc99c45..ccd23e7c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -3,9 +3,9 @@ //! # Examples //! //! ```rust -//! use mrc::commands::Commands; +//! use mpvrc::commands::Commands; //! -//! # async fn example() -> mrc::Result<()> { +//! # async fn example() -> mpvrc::Result<()> { //! // Play media at a specific playlist index //! Commands::play(Some(0), None).await; //! @@ -39,6 +39,10 @@ impl Commands { /// # Arguments /// /// * `index` - Optional playlist index to play from + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn play(index: Option, socket_path: Option<&str>) -> Result<()> { if let Some(idx) = index { info!("Playing media at index: {}", idx); @@ -50,6 +54,10 @@ impl Commands { } /// Pauses the currently playing media. + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn pause(socket_path: Option<&str>) -> Result<()> { info!("Pausing playback"); set_property("pause", &json!(true), socket_path).await?; @@ -59,6 +67,10 @@ impl Commands { /// Stops playback and quits MPV. /// /// This is a destructive operation that will terminate the MPV process. + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn stop(socket_path: Option<&str>) -> Result<()> { info!("Stopping playback and quitting MPV"); quit(socket_path).await?; @@ -66,6 +78,10 @@ impl Commands { } /// Advances to the next item in the playlist. + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn next(socket_path: Option<&str>) -> Result<()> { info!("Skipping to next item in the playlist"); playlist_next(socket_path).await?; @@ -73,6 +89,10 @@ impl Commands { } /// Goes back to the previous item in the playlist. + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn prev(socket_path: Option<&str>) -> Result<()> { info!("Skipping to previous item in the playlist"); playlist_prev(socket_path).await?; @@ -84,6 +104,10 @@ impl Commands { /// # Arguments /// /// * `seconds` - The position in seconds to seek to + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn seek_to(seconds: f64, socket_path: Option<&str>) -> Result<()> { info!("Seeking to {} seconds", seconds); seek(seconds, socket_path).await?; @@ -96,6 +120,10 @@ impl Commands { /// /// * `from_index` - The current index of the item /// * `to_index` - The desired new index for the item + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn move_item( from_index: usize, to_index: usize, @@ -113,6 +141,10 @@ impl Commands { /// # Arguments /// /// * `index` - Optional specific index to remove + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn remove_item(index: Option, socket_path: Option<&str>) -> Result<()> { if let Some(idx) = index { info!("Removing item at index {}", idx); @@ -125,6 +157,10 @@ impl Commands { } /// Clears the entire playlist. + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn clear_playlist(socket_path: Option<&str>) -> Result<()> { info!("Clearing the playlist"); playlist_clear(socket_path).await?; @@ -132,6 +168,10 @@ impl Commands { } /// Lists all items in the playlist. + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn list_playlist(socket_path: Option<&str>) -> Result<()> { info!("Listing playlist items"); if let Some(data) = get_property("playlist", socket_path).await? { @@ -145,6 +185,10 @@ impl Commands { /// # Arguments /// /// * `filenames` - List of file paths to add + /// + /// # Errors + /// + /// Returns an error if no files are provided or socket communication fails. pub async fn add_files(filenames: &[String], socket_path: Option<&str>) -> Result<()> { if filenames.is_empty() { let e = "No files provided to add to the playlist"; @@ -163,6 +207,10 @@ impl Commands { /// # Arguments /// /// * `filenames` - List of file paths to replace the playlist with + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn replace_playlist(filenames: &[String], socket_path: Option<&str>) -> Result<()> { info!("Replacing current playlist with {} files", filenames.len()); if let Some(first_file) = filenames.first() { @@ -179,6 +227,10 @@ impl Commands { /// # Arguments /// /// * `properties` - List of property names to fetch + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn get_properties(properties: &[String], socket_path: Option<&str>) -> Result<()> { info!("Fetching properties: {:?}", properties); for property in properties { @@ -194,6 +246,10 @@ impl Commands { /// # Arguments /// /// * `property` - The property name to fetch + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn get_single_property(property: &str, socket_path: Option<&str>) -> Result<()> { if let Some(data) = get_property(property, socket_path).await? { println!("{property}: {data}"); @@ -207,6 +263,10 @@ impl Commands { /// /// * `property` - The property name to set /// * `value` - The JSON value to set + /// + /// # Errors + /// + /// Returns an error if the socket communication fails. pub async fn set_single_property( property: &str, value: &serde_json::Value, @@ -236,15 +296,15 @@ fn print_playlist(data: &serde_json::Value) { .unwrap_or("unknown"); let current = item .get("current") - .and_then(|v| v.as_bool()) + .and_then(serde_json::Value::as_bool) .unwrap_or(false); let marker = if current { ">" } else { " " }; - println!("{} {:3} | {} | {}", marker, i, title, filename); + println!("{marker} {i:3} | {title} | {filename}"); } } else { let pretty_json = serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string()); - println!("{}", pretty_json); + println!("{pretty_json}"); } } @@ -274,4 +334,4 @@ mod tests { let result = Commands::get_properties(&[], None).await; assert!(result.is_ok()); } -} \ No newline at end of file +} diff --git a/src/interactive.rs b/src/interactive.rs index 7459f3d1..4341e4ed 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -1,18 +1,23 @@ -use crate::commands::Commands; -use crate::{MrcError, Result}; -use rustyline::config::EditMode; -use rustyline::{Cmd, Config, Editor, KeyEvent}; +use std::{io::Error, path::PathBuf}; + +use rustyline::{Cmd, Config, Editor, KeyEvent, config::EditMode}; use serde_json::json; -use std::io::Error; -use std::path::PathBuf; + +use crate::{MrcError, Result, commands::Commands}; pub struct InteractiveMode; impl InteractiveMode { + /// Runs the interactive mode REPL. + /// + /// # Errors + /// + /// Returns an error if the socket communication fails or the REPL encounters an error. pub async fn run(socket_path: Option<&str>) -> Result<()> { - let hist_path = dirs::data_local_dir() - .map(|p| p.join("mrc").join("history.txt")) - .unwrap_or_else(|| PathBuf::from("history.txt")); + let hist_path = dirs::data_local_dir().map_or_else( + || PathBuf::from("history.txt"), + |p| p.join("mpvrc").join("history.txt"), + ); let config = Config::builder().edit_mode(EditMode::Vi).build(); @@ -48,19 +53,18 @@ impl InteractiveMode { } if let Err(e) = Self::process_command(trimmed, socket_path).await { - eprintln!("Error: {}", e); + eprintln!("Error: {e}"); } } Err(rustyline::error::ReadlineError::Interrupted) => { println!("(interrupted)"); - continue; } Err(rustyline::error::ReadlineError::Eof) => { println!("Exiting interactive mode."); break; } Err(err) => { - eprintln!("Error: {:?}", err); + eprintln!("Error: {err:?}"); break; } } @@ -95,7 +99,7 @@ impl InteractiveMode { ]; for (command, description) in commands { - println!(" {:<22} - {}", command, description); + println!(" {command:<22} - {description}"); } println!("\nKeyboard shortcuts:"); @@ -116,7 +120,7 @@ impl InteractiveMode { if let Ok(idx) = index.parse::() { Commands::play(Some(idx), socket_path).await?; } else { - println!("Invalid index: {}", index); + println!("Invalid index: {index}"); } } @@ -140,7 +144,7 @@ impl InteractiveMode { if let Ok(sec) = seconds.parse::() { Commands::seek_to(sec.into(), socket_path).await?; } else { - println!("Invalid seconds: {}", seconds); + println!("Invalid seconds: {seconds}"); } } @@ -153,7 +157,8 @@ impl InteractiveMode { } ["add", files @ ..] => { - let file_strings: Vec = files.iter().map(|s| s.to_string()).collect(); + let file_strings: Vec = + files.iter().map(std::string::ToString::to_string).collect(); if file_strings.is_empty() { println!("No files provided to add to the playlist"); } else { @@ -172,7 +177,7 @@ impl InteractiveMode { } _ => { - println!("Unknown command: {}", input); + println!("Unknown command: {input}"); println!("Type 'help' for a list of available commands."); } } @@ -180,4 +185,3 @@ impl InteractiveMode { Ok(()) } } - diff --git a/src/lib.rs b/src/lib.rs index 4cf1558b..f22b4bed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ //! ```rust //! use serde_json::json; //! use tokio; -//! use mrc::{send_ipc_command, playlist_next, set_property}; +//! use mpvrc::{send_ipc_command, playlist_next, set_property}; //! //! #[tokio::main] //! async fn main() { @@ -175,10 +175,10 @@ async fn read_response(socket: &mut UnixStream) -> Result { 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())); - } + if let Some(error) = json_response.get("error").and_then(|e| e.as_str()) + && !error.is_empty() + { + return Err(MrcError::MpvError(error.to_string())); } Ok(json_response) @@ -389,10 +389,7 @@ pub async fn playlist_remove( index: Option, socket_path: Option<&str>, ) -> Result> { - let args = match index { - Some(idx) => vec![json!(idx)], - None => vec![json!("current")], - }; + let args = index.map_or_else(|| vec![json!("current")], |idx| vec![json!(idx)]); send_ipc_command(MpvCommand::PlaylistRemove.as_str(), &args, socket_path).await } @@ -469,20 +466,20 @@ mod tests { use super::*; #[test] - fn test_mrc_error_display() { + fn test_mpvrc_error_display() { let error = MrcError::InvalidInput("test message".to_string()); assert_eq!(error.to_string(), "invalid input: test message"); } #[test] - fn test_mrc_error_from_io_error() { + fn test_mpvrc_error_from_io_error() { let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); let mrc_error = MrcError::from(io_error); assert!(matches!(mrc_error, MrcError::ConnectionError(_))); } #[test] - fn test_mrc_error_from_json_error() { + fn test_mpvrc_error_from_json_error() { let json_error = serde_json::from_str::("invalid json").unwrap_err(); let mrc_error = MrcError::from(json_error); assert!(matches!(mrc_error, MrcError::ParseError(_))); @@ -548,12 +545,13 @@ mod tests { #[test] fn test_mpv_command_debug() { let cmd = MpvCommand::SetProperty; - let debug_str = format!("{:?}", cmd); + let debug_str = format!("{cmd:?}"); assert_eq!(debug_str, "SetProperty"); } #[test] fn test_result_type_alias() { + #[allow(clippy::unnecessary_wraps)] fn test_function() -> Result { Ok("test".to_string()) } @@ -567,7 +565,7 @@ mod tests { fn test_error_variants_exhaustive() { // Test that all error variants are properly handled let errors = vec![ - MrcError::ConnectionError(std::io::Error::new(std::io::ErrorKind::Other, "test")), + MrcError::ConnectionError(std::io::Error::other("test")), MrcError::ParseError(serde_json::from_str::("").unwrap_err()), MrcError::SocketTimeout(10), MrcError::MpvError("test".to_string()), @@ -584,7 +582,7 @@ mod tests { // Ensure all errors implement Display let _ = error.to_string(); // Ensure all errors implement Debug - let _ = format!("{:?}", error); + let _ = format!("{error:?}"); } } diff --git a/src/server.rs b/src/server.rs index 95a6e32a..5f0125e5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,7 @@ use std::{env, io::Read, sync::Arc}; use clap::Parser; -use mrc::{MrcError, Result as MrcResult, SOCKET_PATH, commands::Commands}; +use mpvrc::{MrcError, Result as MrcResult, SOCKET_PATH, commands::Commands}; use native_tls::{Identity, TlsAcceptor as NativeTlsAcceptor}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio_native_tls::TlsAcceptor; @@ -50,19 +50,14 @@ async fn handle_connection( let token_line = headers .iter() .find(|&&line| line.starts_with("Authorization:")); - let token = match token_line { - Some(line) => line.split(" ").nth(1).unwrap_or_default(), - None => "", - }; + let token = token_line.map_or("", |line| line.split(' ').nth(1).unwrap_or_default()); - let auth_token = match env::var("AUTH_TOKEN") { - Ok(token) => token, - Err(_) => { - warn!("AUTH_TOKEN environment variable not set. Authentication disabled."); - let response = "HTTP/1.1 401 Unauthorized\r\nContent-Length: 29\r\n\r\nAuthentication token not set\n"; - stream.write_all(response.as_bytes()).await?; - return Ok(()); - } + let Ok(auth_token) = env::var("AUTH_TOKEN") else { + warn!("AUTH_TOKEN environment variable not set. Authentication disabled."); + let response = + "HTTP/1.1 401 Unauthorized\r\nContent-Length: 29\r\n\r\nAuthentication token not set\n"; + stream.write_all(response.as_bytes()).await?; + return Ok(()); }; if token.is_empty() || token != auth_token { @@ -98,7 +93,7 @@ async fn handle_connection( Ok(response) => ("200 OK", response), Err(e) => { error!("Error processing command '{}': {}", command, e); - ("400 Bad Request", format!("Error: {}\n", e)) + ("400 Bad Request", format!("Error: {e}\n")) } }; @@ -131,9 +126,9 @@ async fn process_command(command: &str, socket_path: &str) -> MrcResult ["play", index] => { if let Ok(idx) = index.parse::() { Commands::play(Some(idx), Some(socket_path)).await?; - Ok(format!("Playing from index {}\n", idx)) + Ok(format!("Playing from index {idx}\n")) } else { - Err(MrcError::InvalidInput(format!("Invalid index: {}", index))) + Err(MrcError::InvalidInput(format!("Invalid index: {index}"))) } } @@ -155,11 +150,10 @@ async fn process_command(command: &str, socket_path: &str) -> MrcResult ["seek", seconds] => { if let Ok(sec) = seconds.parse::() { Commands::seek_to(sec, Some(socket_path)).await?; - Ok(format!("Seeking to {} seconds\n", sec)) + Ok(format!("Seeking to {sec} seconds\n")) } else { Err(MrcError::InvalidInput(format!( - "Invalid seconds: {}", - seconds + "Invalid seconds: {seconds}" ))) } } @@ -169,11 +163,11 @@ async fn process_command(command: &str, socket_path: &str) -> MrcResult Ok("Cleared playlist\n".to_string()) } - ["list"] => match mrc::get_property("playlist", Some(socket_path)).await? { + ["list"] => match mpvrc::get_property("playlist", Some(socket_path)).await? { Some(data) => { let pretty_json = serde_json::to_string_pretty(&data).map_err(MrcError::ParseError)?; - Ok(format!("Playlist: {}\n", pretty_json)) + Ok(format!("Playlist: {pretty_json}\n")) } None => Ok("Playlist is empty\n".to_string()), }, @@ -216,7 +210,7 @@ async fn main() -> MrcResult<()> { ); return Err(MrcError::ConnectionError(std::io::Error::new( std::io::ErrorKind::NotFound, - format!("MPV socket not found at '{}'", socket_path), + format!("MPV socket not found at '{socket_path}'"), ))); }