mirror of
https://github.com/NotAShelf/mpvrc.git
synced 2026-04-18 08:39:53 +00:00
meta: rename to 'mpvrc'
mrc is taken :( Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I2cf0cab8dce04cd04981177d43b9b9b76a6a6964
This commit is contained in:
parent
8c48acf693
commit
1f1a94f747
9 changed files with 375 additions and 255 deletions
410
README.md
410
README.md
|
|
@ -1,120 +1,235 @@
|
||||||
# MRC - MPV Remote Control
|
# mpvrc - MPV Remote Control
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MPL-2.0)
|
[](https://opensource.org/licenses/MPL-2.0)
|
||||||
|
[](https://crates.io/crates/mpvrc)
|
||||||
|
|
||||||
[mpv video player]: https://mpv.io/
|
[mpv video player]: https://mpv.io/
|
||||||
|
|
||||||
**MRC** is a fast, robust JSON IPC wrapper for the [mpv video player]. It
|
mpvrc is an unofficial command-line tool and library for controlling the
|
||||||
provides:
|
[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
|
||||||
- Type-safe MPV IPC communication
|
TLS-encrypted HTTP API for _remote_ control. mpvrc has you covered for anything
|
||||||
- Command-line interface with interactive mode
|
that is even vaguely related to controlling a MPV instance.
|
||||||
- Secure remote control over HTTPS
|
|
||||||
- Async Support with Tokio for high performance
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Complete MPV Control**: Play, pause, seek, playlist management, and property
|
The project is equipped with various neat features such as, but not limited to:
|
||||||
access
|
|
||||||
- **Interactive CLI**: Real-time command interface with help system
|
- **Full playback control**: play, pause, stop, seek, skip, and playlist
|
||||||
- **Secure Remote Access**: TLS-encrypted server with token authentication
|
management
|
||||||
- **Flexible Configuration**: Custom socket paths and environment-based setup
|
- **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 <https://crates.io>.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install to ~/.cargo/bin
|
||||||
|
$ cargo install mpvrc --locked
|
||||||
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### 1. Start MPV with IPC
|
||||||
|
|
||||||
1. **Install MPV** with IPC support enabled
|
Using mpvrc is quite simple. Start `mpv` with an IPC socket first:
|
||||||
2. **Install Rust** (edition 2024+)
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# `/tmp/mpvsocket` is the default socket path for mpvrc
|
||||||
git clone https://github.com/notashelf/mrc.git
|
$ mpv --idle --input-ipc-server=/tmp/mpvsocket
|
||||||
cd mrc
|
|
||||||
|
|
||||||
# Build the project
|
|
||||||
cargo build --release
|
|
||||||
|
|
||||||
# Or install directly
|
|
||||||
cargo install --path .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
```bash
|
||||||
mpv --idle --input-ipc-server=/tmp/mpvsocket
|
# Play/pause
|
||||||
```
|
$ mpvrc play
|
||||||
|
$ mpvrc pause
|
||||||
|
|
||||||
2. **Use the CLI**:
|
# Navigation
|
||||||
|
$ mpvrc next
|
||||||
|
$ mpvrc prev
|
||||||
|
|
||||||
```bash
|
# Seek 2 minutes forward
|
||||||
# Basic playback control
|
$ mpvrc seek 120
|
||||||
cargo run --bin cli play
|
|
||||||
cargo run --bin cli pause
|
|
||||||
cargo run --bin cli next
|
|
||||||
|
|
||||||
# Load files
|
# Add files to playlist
|
||||||
cargo run --bin cli add ~/Music/song.mp3 ~/Videos/movie.mkv
|
$ mpvrc add ~/Videos/movie.mkv ~/Videos/episode1.mkv
|
||||||
|
|
||||||
# Interactive mode
|
# View playlist
|
||||||
cargo run --bin cli interactive
|
$ mpvrc list
|
||||||
```
|
|
||||||
|
|
||||||
## Usage Guide
|
# Interactive mode
|
||||||
|
$ mpvrc interactive
|
||||||
|
```
|
||||||
|
|
||||||
### CLI Commands
|
## CLI Options
|
||||||
|
|
||||||
| Command | Description | Example |
|
```plaintext
|
||||||
| :--------------------- | -------------------------- | :---------------------------: |
|
Usage: mpvrc [OPTIONS] <COMMAND>
|
||||||
| `play [index]` | Start/resume playback | `mrc play` or `mrc play 2` |
|
|
||||||
| `pause` | Pause playback | `mrc pause` |
|
Options:
|
||||||
| `stop` | Stop and quit MPV | `mrc stop` |
|
-s, --socket <PATH> Path to MPV IPC socket [default: /tmp/mpvsocket]
|
||||||
| `next` | Skip to next playlist item | `mrc next` |
|
-y, --yes Skip confirmation prompts for destructive commands
|
||||||
| `prev` | Skip to previous item | `mrc prev` |
|
-d, --debug Enable debug logging
|
||||||
| `seek <seconds>` | Seek to position | `mrc seek 120` |
|
-h, --help Print help
|
||||||
| `add <files...>` | Add files to playlist | `mrc add song1.mp3 song2.mp3` |
|
-V, --version Print version
|
||||||
| `remove [index]` | Remove playlist item | `mrc remove 0` |
|
```
|
||||||
| `move <from> <to>` | Move playlist item | `mrc move 0 3` |
|
|
||||||
| `clear` | Clear playlist | `mrc clear` |
|
## Commands
|
||||||
| `list` | Show playlist | `mrc list` |
|
|
||||||
| `prop <properties...>` | Get properties | `mrc prop volume duration` |
|
<!--markdownlint-disable MD013-->
|
||||||
| `interactive` | Enter interactive mode | `mrc interactive` |
|
|
||||||
|
| 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 <seconds>` | Seek to position in seconds | `mpvrc seek 120` |
|
||||||
|
| `add <files...>` | Add files to playlist | `mpvrc add a.mp3 b.mp3` |
|
||||||
|
| `remove [index]` | Remove item (default: current) | `mpvrc remove 0` |
|
||||||
|
| `move <from> <to>` | Move playlist item | `mpvrc move 0 3` |
|
||||||
|
| `clear` | Clear playlist | `mpvrc clear` |
|
||||||
|
| `list` | Show playlist | `mpvrc list` |
|
||||||
|
| `prop <props...>` | Get property values | `mpvrc prop volume` |
|
||||||
|
| `interactive` | Enter interactive REPL | `mpvrc interactive` |
|
||||||
|
| `completion <shell>` | Generate shell completions | `mpvrc completion zsh` |
|
||||||
|
|
||||||
|
<!--markdownlint-enable MD013-->
|
||||||
|
|
||||||
|
### 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
|
### Interactive Mode
|
||||||
|
|
||||||
Enter interactive mode for real-time control:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cargo run --bin cli interactive
|
$ mpvrc interactive
|
||||||
Entering interactive mode. Type 'exit' to quit.
|
Entering interactive mode. Type 'help' for commands or 'exit' to quit.
|
||||||
|
Socket: /tmp/mpvsocket
|
||||||
|
|
||||||
mpv> play
|
mpv> play
|
||||||
mpv> seek 60
|
mpv> seek 60
|
||||||
mpv> set volume 80
|
mpv> list
|
||||||
mpv> get filename
|
0 | Episode 1 | /path/to/episode1.mkv
|
||||||
mpv> help
|
> 1 | Episode 2 | /path/to/episode2.mkv
|
||||||
|
2 | Episode 3 | /path/to/episode3.mkv
|
||||||
mpv> exit
|
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
|
```rust
|
||||||
use mrc::{playlist_next, set_property, get_property};
|
use mpvrc::{get_property, playlist_next, set_property};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Set volume to 50%
|
// Set volume
|
||||||
set_property("volume", &json!(50), None).await?;
|
set_property("volume", &json!(75), None).await?;
|
||||||
|
|
||||||
// Get current filename
|
// Get current filename
|
||||||
if let Some(filename) = get_property("filename", None).await? {
|
if let Some(filename) = get_property("filename", None).await? {
|
||||||
println!("Playing: {}", filename);
|
println!("Now playing: {}", filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip to next track
|
// Skip to next track
|
||||||
|
|
@ -124,134 +239,67 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
## Configuration
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
| Variable | Description | Default | Required |
|
| Variable | Description | Required |
|
||||||
| -------------- | -------------------------------- | ------- | ----------- |
|
| -------------- | ------------------------------- | -------- |
|
||||||
| `TLS_PFX_PATH` | Path to PKCS#12 certificate file | - | Server only |
|
| `TLS_PFX_PATH` | Path to PKCS#12 certificate | Server |
|
||||||
| `TLS_PASSWORD` | Password for PKCS#12 file | - | Server only |
|
| `TLS_PASSWORD` | Password for PKCS#12 file | Server |
|
||||||
| `AUTH_TOKEN` | Authentication token for server | - | Server only |
|
| `AUTH_TOKEN` | Bearer token for authentication | Server |
|
||||||
|
|
||||||
### Socket Path
|
### Custom Socket Path
|
||||||
|
|
||||||
By default, MRC uses `/tmp/mpvsocket`. You can customize this:
|
Use `-s` or `--socket` to specify a different MPV socket:
|
||||||
|
|
||||||
```rust
|
|
||||||
// In your code
|
|
||||||
use mrc::set_property;
|
|
||||||
|
|
||||||
set_property("volume", &json!(50), Some("/your/socket/path")).await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development build
|
# As of 0.3.0, mvrc supports custom socket path
|
||||||
cargo build
|
$ 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
|
# Release build
|
||||||
cargo build --release
|
$ cargo build --release
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
cargo test
|
$ cargo test
|
||||||
|
|
||||||
# Check linting
|
# Lint
|
||||||
cargo clippy
|
$ cargo clippy --all-targets
|
||||||
```
|
|
||||||
|
|
||||||
### 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}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Common Issues
|
### Socket not found
|
||||||
|
|
||||||
**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:
|
|
||||||
|
|
||||||
```bash
|
```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
|
## License
|
||||||
|
|
||||||
This project is licensed under the Mozilla Public License 2.0 - see the
|
This project is made available under Mozilla Public License (MPL) version 2.0.
|
||||||
[LICENSE](LICENSE) file for details.
|
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/).
|
||||||
## Acknowledgments
|
|
||||||
|
|
||||||
- [MPV Media Player](https://mpv.io/) for the excellent IPC interface
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
pkgsForEach = nixpkgs.legacyPackages;
|
pkgsForEach = nixpkgs.legacyPackages;
|
||||||
in rec {
|
in rec {
|
||||||
packages = forEachSystem (system: {
|
packages = forEachSystem (system: {
|
||||||
mrc = pkgsForEach.${system}.callPackage ./nix/package.nix {};
|
mpvrc = pkgsForEach.${system}.callPackage ./nix/package.nix {};
|
||||||
default = self.packages.${system}.mrc;
|
default = self.packages.${system}.mpvrc;
|
||||||
});
|
});
|
||||||
|
|
||||||
devShells = forEachSystem (system: {
|
devShells = forEachSystem (system: {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
s = ../.;
|
s = ../.;
|
||||||
in
|
in
|
||||||
rustPlatform.buildRustPackage (finalAttrs: {
|
rustPlatform.buildRustPackage (finalAttrs: {
|
||||||
pname = "mrc";
|
pname = "mpvrc";
|
||||||
version = (builtins.fromTOML (builtins.readFile (s + /Cargo.toml))).package.version;
|
version = (lib.importTOML (s + /Cargo.toml)).package.version;
|
||||||
|
|
||||||
src = fs.toSource {
|
src = fs.toSource {
|
||||||
root = s;
|
root = s;
|
||||||
|
|
@ -32,12 +32,11 @@ in
|
||||||
];
|
];
|
||||||
|
|
||||||
cargoLock.lockFile = "${finalAttrs.src}/Cargo.lock";
|
cargoLock.lockFile = "${finalAttrs.src}/Cargo.lock";
|
||||||
useFetchCargoVendor = true;
|
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "IPC wrapper & command-line controller for MPV, the video player";
|
description = "IPC wrapper & command-line controller for MPV, the video player";
|
||||||
homePage = "https://github.com/notashelf/mrc";
|
homePage = "https://github.com/notashelf/mpvrc";
|
||||||
mainProgram = "mrc";
|
mainProgram = "mpvrc";
|
||||||
license = lib.licenses.mpl20;
|
license = lib.licenses.mpl20;
|
||||||
maintainers = [lib.maintainers.NotAShelf];
|
maintainers = [lib.maintainers.NotAShelf];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@
|
||||||
rustfmt,
|
rustfmt,
|
||||||
clippy,
|
clippy,
|
||||||
cargo,
|
cargo,
|
||||||
|
taplo,
|
||||||
openssl,
|
openssl,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
rustc,
|
rustc,
|
||||||
}:
|
}:
|
||||||
mkShell {
|
mkShell {
|
||||||
name = "mrc";
|
name = "mpvrc";
|
||||||
packages = [
|
packages = [
|
||||||
cargo
|
cargo
|
||||||
rustc
|
rustc
|
||||||
|
|
@ -17,6 +18,7 @@ mkShell {
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
clippy
|
clippy
|
||||||
(rustfmt.override {asNightly = true;})
|
(rustfmt.override {asNightly = true;})
|
||||||
|
taplo
|
||||||
|
|
||||||
# For TLS and friends
|
# For TLS and friends
|
||||||
openssl
|
openssl
|
||||||
|
|
|
||||||
51
src/cli.rs
51
src/cli.rs
|
|
@ -1,9 +1,10 @@
|
||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
|
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
|
||||||
use mrc::commands::Commands;
|
use mpvrc::{MrcError, Result, SOCKET_PATH, commands::Commands, interactive::InteractiveMode};
|
||||||
use mrc::interactive::InteractiveMode;
|
|
||||||
use mrc::{MrcError, Result, SOCKET_PATH};
|
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -115,27 +116,42 @@ pub enum Shell {
|
||||||
impl Shell {
|
impl Shell {
|
||||||
pub fn generate(&self, app: &mut clap::Command) {
|
pub fn generate(&self, app: &mut clap::Command) {
|
||||||
match self {
|
match self {
|
||||||
Shell::Bash => {
|
Self::Bash => {
|
||||||
clap_complete::generate(clap_complete::Shell::Bash, app, "mrc", &mut io::stdout())
|
clap_complete::generate(
|
||||||
|
clap_complete::Shell::Bash,
|
||||||
|
app,
|
||||||
|
"mpvrc",
|
||||||
|
&mut io::stdout(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Shell::Elvish => {
|
Self::Elvish => {
|
||||||
clap_complete::generate(clap_complete::Shell::Elvish, app, "mrc", &mut io::stdout())
|
clap_complete::generate(
|
||||||
|
clap_complete::Shell::Elvish,
|
||||||
|
app,
|
||||||
|
"mpvrc",
|
||||||
|
&mut io::stdout(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Shell::Fish => {
|
Self::Fish => {
|
||||||
clap_complete::generate(clap_complete::Shell::Fish, app, "mrc", &mut io::stdout())
|
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,
|
clap_complete::Shell::PowerShell,
|
||||||
app,
|
app,
|
||||||
"mrc",
|
"mpvrc",
|
||||||
&mut io::stdout(),
|
&mut io::stdout(),
|
||||||
),
|
),
|
||||||
|
|
||||||
Shell::Zsh => {
|
Self::Zsh => {
|
||||||
clap_complete::generate(clap_complete::Shell::Zsh, app, "mrc", &mut io::stdout())
|
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 {
|
if yes {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
print!("{} [y/N] ", prompt);
|
print!("{prompt} [y/N] ");
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
io::stdin().read_line(&mut input).unwrap();
|
io::stdin().read_line(&mut input).unwrap();
|
||||||
|
|
@ -220,7 +236,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
CommandOptions::Remove { index } => {
|
CommandOptions::Remove { index } => {
|
||||||
if confirm(
|
if confirm(
|
||||||
&format!("This will remove item at index {:?}. Continue?", index),
|
&format!("This will remove item at index {index:?}. Continue?"),
|
||||||
cli.yes,
|
cli.yes,
|
||||||
) {
|
) {
|
||||||
Commands::remove_item(index, Some(&socket_path)).await?;
|
Commands::remove_item(index, Some(&socket_path)).await?;
|
||||||
|
|
@ -262,4 +278,3 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
//! # Examples
|
//! # Examples
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```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
|
//! // Play media at a specific playlist index
|
||||||
//! Commands::play(Some(0), None).await;
|
//! Commands::play(Some(0), None).await;
|
||||||
//!
|
//!
|
||||||
|
|
@ -39,6 +39,10 @@ impl Commands {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `index` - Optional playlist index to play from
|
/// * `index` - Optional playlist index to play from
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the socket communication fails.
|
||||||
pub async fn play(index: Option<usize>, socket_path: Option<&str>) -> Result<()> {
|
pub async fn play(index: Option<usize>, socket_path: Option<&str>) -> Result<()> {
|
||||||
if let Some(idx) = index {
|
if let Some(idx) = index {
|
||||||
info!("Playing media at index: {}", idx);
|
info!("Playing media at index: {}", idx);
|
||||||
|
|
@ -50,6 +54,10 @@ impl Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pauses the currently playing media.
|
/// Pauses the currently playing media.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the socket communication fails.
|
||||||
pub async fn pause(socket_path: Option<&str>) -> Result<()> {
|
pub async fn pause(socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Pausing playback");
|
info!("Pausing playback");
|
||||||
set_property("pause", &json!(true), socket_path).await?;
|
set_property("pause", &json!(true), socket_path).await?;
|
||||||
|
|
@ -59,6 +67,10 @@ impl Commands {
|
||||||
/// Stops playback and quits MPV.
|
/// Stops playback and quits MPV.
|
||||||
///
|
///
|
||||||
/// This is a destructive operation that will terminate the MPV process.
|
/// 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<()> {
|
pub async fn stop(socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Stopping playback and quitting MPV");
|
info!("Stopping playback and quitting MPV");
|
||||||
quit(socket_path).await?;
|
quit(socket_path).await?;
|
||||||
|
|
@ -66,6 +78,10 @@ impl Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advances to the next item in the playlist.
|
/// 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<()> {
|
pub async fn next(socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Skipping to next item in the playlist");
|
info!("Skipping to next item in the playlist");
|
||||||
playlist_next(socket_path).await?;
|
playlist_next(socket_path).await?;
|
||||||
|
|
@ -73,6 +89,10 @@ impl Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Goes back to the previous item in the playlist.
|
/// 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<()> {
|
pub async fn prev(socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Skipping to previous item in the playlist");
|
info!("Skipping to previous item in the playlist");
|
||||||
playlist_prev(socket_path).await?;
|
playlist_prev(socket_path).await?;
|
||||||
|
|
@ -84,6 +104,10 @@ impl Commands {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `seconds` - The position in seconds to seek to
|
/// * `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<()> {
|
pub async fn seek_to(seconds: f64, socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Seeking to {} seconds", seconds);
|
info!("Seeking to {} seconds", seconds);
|
||||||
seek(seconds, socket_path).await?;
|
seek(seconds, socket_path).await?;
|
||||||
|
|
@ -96,6 +120,10 @@ impl Commands {
|
||||||
///
|
///
|
||||||
/// * `from_index` - The current index of the item
|
/// * `from_index` - The current index of the item
|
||||||
/// * `to_index` - The desired new index for 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(
|
pub async fn move_item(
|
||||||
from_index: usize,
|
from_index: usize,
|
||||||
to_index: usize,
|
to_index: usize,
|
||||||
|
|
@ -113,6 +141,10 @@ impl Commands {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `index` - Optional specific index to remove
|
/// * `index` - Optional specific index to remove
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the socket communication fails.
|
||||||
pub async fn remove_item(index: Option<usize>, socket_path: Option<&str>) -> Result<()> {
|
pub async fn remove_item(index: Option<usize>, socket_path: Option<&str>) -> Result<()> {
|
||||||
if let Some(idx) = index {
|
if let Some(idx) = index {
|
||||||
info!("Removing item at index {}", idx);
|
info!("Removing item at index {}", idx);
|
||||||
|
|
@ -125,6 +157,10 @@ impl Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the entire playlist.
|
/// Clears the entire playlist.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the socket communication fails.
|
||||||
pub async fn clear_playlist(socket_path: Option<&str>) -> Result<()> {
|
pub async fn clear_playlist(socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Clearing the playlist");
|
info!("Clearing the playlist");
|
||||||
playlist_clear(socket_path).await?;
|
playlist_clear(socket_path).await?;
|
||||||
|
|
@ -132,6 +168,10 @@ impl Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lists all items in the playlist.
|
/// 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<()> {
|
pub async fn list_playlist(socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Listing playlist items");
|
info!("Listing playlist items");
|
||||||
if let Some(data) = get_property("playlist", socket_path).await? {
|
if let Some(data) = get_property("playlist", socket_path).await? {
|
||||||
|
|
@ -145,6 +185,10 @@ impl Commands {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `filenames` - List of file paths to add
|
/// * `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<()> {
|
pub async fn add_files(filenames: &[String], socket_path: Option<&str>) -> Result<()> {
|
||||||
if filenames.is_empty() {
|
if filenames.is_empty() {
|
||||||
let e = "No files provided to add to the playlist";
|
let e = "No files provided to add to the playlist";
|
||||||
|
|
@ -163,6 +207,10 @@ impl Commands {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `filenames` - List of file paths to replace the playlist with
|
/// * `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<()> {
|
pub async fn replace_playlist(filenames: &[String], socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Replacing current playlist with {} files", filenames.len());
|
info!("Replacing current playlist with {} files", filenames.len());
|
||||||
if let Some(first_file) = filenames.first() {
|
if let Some(first_file) = filenames.first() {
|
||||||
|
|
@ -179,6 +227,10 @@ impl Commands {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `properties` - List of property names to fetch
|
/// * `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<()> {
|
pub async fn get_properties(properties: &[String], socket_path: Option<&str>) -> Result<()> {
|
||||||
info!("Fetching properties: {:?}", properties);
|
info!("Fetching properties: {:?}", properties);
|
||||||
for property in properties {
|
for property in properties {
|
||||||
|
|
@ -194,6 +246,10 @@ impl Commands {
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `property` - The property name to fetch
|
/// * `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<()> {
|
pub async fn get_single_property(property: &str, socket_path: Option<&str>) -> Result<()> {
|
||||||
if let Some(data) = get_property(property, socket_path).await? {
|
if let Some(data) = get_property(property, socket_path).await? {
|
||||||
println!("{property}: {data}");
|
println!("{property}: {data}");
|
||||||
|
|
@ -207,6 +263,10 @@ impl Commands {
|
||||||
///
|
///
|
||||||
/// * `property` - The property name to set
|
/// * `property` - The property name to set
|
||||||
/// * `value` - The JSON value to set
|
/// * `value` - The JSON value to set
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if the socket communication fails.
|
||||||
pub async fn set_single_property(
|
pub async fn set_single_property(
|
||||||
property: &str,
|
property: &str,
|
||||||
value: &serde_json::Value,
|
value: &serde_json::Value,
|
||||||
|
|
@ -236,15 +296,15 @@ fn print_playlist(data: &serde_json::Value) {
|
||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
let current = item
|
let current = item
|
||||||
.get("current")
|
.get("current")
|
||||||
.and_then(|v| v.as_bool())
|
.and_then(serde_json::Value::as_bool)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let marker = if current { ">" } else { " " };
|
let marker = if current { ">" } else { " " };
|
||||||
println!("{} {:3} | {} | {}", marker, i, title, filename);
|
println!("{marker} {i:3} | {title} | {filename}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let pretty_json = serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string());
|
let pretty_json = serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string());
|
||||||
println!("{}", pretty_json);
|
println!("{pretty_json}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
use crate::commands::Commands;
|
use std::{io::Error, path::PathBuf};
|
||||||
use crate::{MrcError, Result};
|
|
||||||
use rustyline::config::EditMode;
|
use rustyline::{Cmd, Config, Editor, KeyEvent, config::EditMode};
|
||||||
use rustyline::{Cmd, Config, Editor, KeyEvent};
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::io::Error;
|
|
||||||
use std::path::PathBuf;
|
use crate::{MrcError, Result, commands::Commands};
|
||||||
|
|
||||||
pub struct InteractiveMode;
|
pub struct InteractiveMode;
|
||||||
|
|
||||||
impl 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<()> {
|
pub async fn run(socket_path: Option<&str>) -> Result<()> {
|
||||||
let hist_path = dirs::data_local_dir()
|
let hist_path = dirs::data_local_dir().map_or_else(
|
||||||
.map(|p| p.join("mrc").join("history.txt"))
|
|| PathBuf::from("history.txt"),
|
||||||
.unwrap_or_else(|| PathBuf::from("history.txt"));
|
|p| p.join("mpvrc").join("history.txt"),
|
||||||
|
);
|
||||||
|
|
||||||
let config = Config::builder().edit_mode(EditMode::Vi).build();
|
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 {
|
if let Err(e) = Self::process_command(trimmed, socket_path).await {
|
||||||
eprintln!("Error: {}", e);
|
eprintln!("Error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(rustyline::error::ReadlineError::Interrupted) => {
|
Err(rustyline::error::ReadlineError::Interrupted) => {
|
||||||
println!("(interrupted)");
|
println!("(interrupted)");
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
Err(rustyline::error::ReadlineError::Eof) => {
|
Err(rustyline::error::ReadlineError::Eof) => {
|
||||||
println!("Exiting interactive mode.");
|
println!("Exiting interactive mode.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {:?}", err);
|
eprintln!("Error: {err:?}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +99,7 @@ impl InteractiveMode {
|
||||||
];
|
];
|
||||||
|
|
||||||
for (command, description) in commands {
|
for (command, description) in commands {
|
||||||
println!(" {:<22} - {}", command, description);
|
println!(" {command:<22} - {description}");
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\nKeyboard shortcuts:");
|
println!("\nKeyboard shortcuts:");
|
||||||
|
|
@ -116,7 +120,7 @@ impl InteractiveMode {
|
||||||
if let Ok(idx) = index.parse::<usize>() {
|
if let Ok(idx) = index.parse::<usize>() {
|
||||||
Commands::play(Some(idx), socket_path).await?;
|
Commands::play(Some(idx), socket_path).await?;
|
||||||
} else {
|
} else {
|
||||||
println!("Invalid index: {}", index);
|
println!("Invalid index: {index}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +144,7 @@ impl InteractiveMode {
|
||||||
if let Ok(sec) = seconds.parse::<i32>() {
|
if let Ok(sec) = seconds.parse::<i32>() {
|
||||||
Commands::seek_to(sec.into(), socket_path).await?;
|
Commands::seek_to(sec.into(), socket_path).await?;
|
||||||
} else {
|
} else {
|
||||||
println!("Invalid seconds: {}", seconds);
|
println!("Invalid seconds: {seconds}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,7 +157,8 @@ impl InteractiveMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
["add", files @ ..] => {
|
["add", files @ ..] => {
|
||||||
let file_strings: Vec<String> = files.iter().map(|s| s.to_string()).collect();
|
let file_strings: Vec<String> =
|
||||||
|
files.iter().map(std::string::ToString::to_string).collect();
|
||||||
if file_strings.is_empty() {
|
if file_strings.is_empty() {
|
||||||
println!("No files provided to add to the playlist");
|
println!("No files provided to add to the playlist");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -172,7 +177,7 @@ impl InteractiveMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
println!("Unknown command: {}", input);
|
println!("Unknown command: {input}");
|
||||||
println!("Type 'help' for a list of available commands.");
|
println!("Type 'help' for a list of available commands.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -180,4 +185,3 @@ impl InteractiveMode {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
28
src/lib.rs
28
src/lib.rs
|
|
@ -15,7 +15,7 @@
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use serde_json::json;
|
//! use serde_json::json;
|
||||||
//! use tokio;
|
//! use tokio;
|
||||||
//! use mrc::{send_ipc_command, playlist_next, set_property};
|
//! use mpvrc::{send_ipc_command, playlist_next, set_property};
|
||||||
//!
|
//!
|
||||||
//! #[tokio::main]
|
//! #[tokio::main]
|
||||||
//! async fn main() {
|
//! async fn main() {
|
||||||
|
|
@ -175,10 +175,10 @@ async fn read_response(socket: &mut UnixStream) -> Result<Value> {
|
||||||
debug!("Parsed IPC response: {:?}", json_response);
|
debug!("Parsed IPC response: {:?}", json_response);
|
||||||
|
|
||||||
// Check if MPV returned an error
|
// Check if MPV returned an error
|
||||||
if let Some(error) = json_response.get("error").and_then(|e| e.as_str()) {
|
if let Some(error) = json_response.get("error").and_then(|e| e.as_str())
|
||||||
if !error.is_empty() {
|
&& !error.is_empty()
|
||||||
return Err(MrcError::MpvError(error.to_string()));
|
{
|
||||||
}
|
return Err(MrcError::MpvError(error.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(json_response)
|
Ok(json_response)
|
||||||
|
|
@ -389,10 +389,7 @@ pub async fn playlist_remove(
|
||||||
index: Option<usize>,
|
index: Option<usize>,
|
||||||
socket_path: Option<&str>,
|
socket_path: Option<&str>,
|
||||||
) -> Result<Option<Value>> {
|
) -> Result<Option<Value>> {
|
||||||
let args = match index {
|
let args = index.map_or_else(|| vec![json!("current")], |idx| vec![json!(idx)]);
|
||||||
Some(idx) => vec![json!(idx)],
|
|
||||||
None => vec![json!("current")],
|
|
||||||
};
|
|
||||||
send_ipc_command(MpvCommand::PlaylistRemove.as_str(), &args, socket_path).await
|
send_ipc_command(MpvCommand::PlaylistRemove.as_str(), &args, socket_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,20 +466,20 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mrc_error_display() {
|
fn test_mpvrc_error_display() {
|
||||||
let error = MrcError::InvalidInput("test message".to_string());
|
let error = MrcError::InvalidInput("test message".to_string());
|
||||||
assert_eq!(error.to_string(), "invalid input: test message");
|
assert_eq!(error.to_string(), "invalid input: test message");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
|
||||||
let mrc_error = MrcError::from(io_error);
|
let mrc_error = MrcError::from(io_error);
|
||||||
assert!(matches!(mrc_error, MrcError::ConnectionError(_)));
|
assert!(matches!(mrc_error, MrcError::ConnectionError(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mrc_error_from_json_error() {
|
fn test_mpvrc_error_from_json_error() {
|
||||||
let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
|
let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
|
||||||
let mrc_error = MrcError::from(json_error);
|
let mrc_error = MrcError::from(json_error);
|
||||||
assert!(matches!(mrc_error, MrcError::ParseError(_)));
|
assert!(matches!(mrc_error, MrcError::ParseError(_)));
|
||||||
|
|
@ -548,12 +545,13 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mpv_command_debug() {
|
fn test_mpv_command_debug() {
|
||||||
let cmd = MpvCommand::SetProperty;
|
let cmd = MpvCommand::SetProperty;
|
||||||
let debug_str = format!("{:?}", cmd);
|
let debug_str = format!("{cmd:?}");
|
||||||
assert_eq!(debug_str, "SetProperty");
|
assert_eq!(debug_str, "SetProperty");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_result_type_alias() {
|
fn test_result_type_alias() {
|
||||||
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
fn test_function() -> Result<String> {
|
fn test_function() -> Result<String> {
|
||||||
Ok("test".to_string())
|
Ok("test".to_string())
|
||||||
}
|
}
|
||||||
|
|
@ -567,7 +565,7 @@ mod tests {
|
||||||
fn test_error_variants_exhaustive() {
|
fn test_error_variants_exhaustive() {
|
||||||
// Test that all error variants are properly handled
|
// Test that all error variants are properly handled
|
||||||
let errors = vec![
|
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::<serde_json::Value>("").unwrap_err()),
|
MrcError::ParseError(serde_json::from_str::<serde_json::Value>("").unwrap_err()),
|
||||||
MrcError::SocketTimeout(10),
|
MrcError::SocketTimeout(10),
|
||||||
MrcError::MpvError("test".to_string()),
|
MrcError::MpvError("test".to_string()),
|
||||||
|
|
@ -584,7 +582,7 @@ mod tests {
|
||||||
// Ensure all errors implement Display
|
// Ensure all errors implement Display
|
||||||
let _ = error.to_string();
|
let _ = error.to_string();
|
||||||
// Ensure all errors implement Debug
|
// Ensure all errors implement Debug
|
||||||
let _ = format!("{:?}", error);
|
let _ = format!("{error:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{env, io::Read, sync::Arc};
|
use std::{env, io::Read, sync::Arc};
|
||||||
|
|
||||||
use clap::Parser;
|
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 native_tls::{Identity, TlsAcceptor as NativeTlsAcceptor};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio_native_tls::TlsAcceptor;
|
use tokio_native_tls::TlsAcceptor;
|
||||||
|
|
@ -50,19 +50,14 @@ async fn handle_connection(
|
||||||
let token_line = headers
|
let token_line = headers
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&&line| line.starts_with("Authorization:"));
|
.find(|&&line| line.starts_with("Authorization:"));
|
||||||
let token = match token_line {
|
let token = token_line.map_or("", |line| line.split(' ').nth(1).unwrap_or_default());
|
||||||
Some(line) => line.split(" ").nth(1).unwrap_or_default(),
|
|
||||||
None => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
let auth_token = match env::var("AUTH_TOKEN") {
|
let Ok(auth_token) = env::var("AUTH_TOKEN") else {
|
||||||
Ok(token) => token,
|
warn!("AUTH_TOKEN environment variable not set. Authentication disabled.");
|
||||||
Err(_) => {
|
let response =
|
||||||
warn!("AUTH_TOKEN environment variable not set. Authentication disabled.");
|
"HTTP/1.1 401 Unauthorized\r\nContent-Length: 29\r\n\r\nAuthentication token not set\n";
|
||||||
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?;
|
||||||
stream.write_all(response.as_bytes()).await?;
|
return Ok(());
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if token.is_empty() || token != auth_token {
|
if token.is_empty() || token != auth_token {
|
||||||
|
|
@ -98,7 +93,7 @@ async fn handle_connection(
|
||||||
Ok(response) => ("200 OK", response),
|
Ok(response) => ("200 OK", response),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error processing command '{}': {}", command, 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<String>
|
||||||
["play", index] => {
|
["play", index] => {
|
||||||
if let Ok(idx) = index.parse::<usize>() {
|
if let Ok(idx) = index.parse::<usize>() {
|
||||||
Commands::play(Some(idx), Some(socket_path)).await?;
|
Commands::play(Some(idx), Some(socket_path)).await?;
|
||||||
Ok(format!("Playing from index {}\n", idx))
|
Ok(format!("Playing from index {idx}\n"))
|
||||||
} else {
|
} 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<String>
|
||||||
["seek", seconds] => {
|
["seek", seconds] => {
|
||||||
if let Ok(sec) = seconds.parse::<f64>() {
|
if let Ok(sec) = seconds.parse::<f64>() {
|
||||||
Commands::seek_to(sec, Some(socket_path)).await?;
|
Commands::seek_to(sec, Some(socket_path)).await?;
|
||||||
Ok(format!("Seeking to {} seconds\n", sec))
|
Ok(format!("Seeking to {sec} seconds\n"))
|
||||||
} else {
|
} else {
|
||||||
Err(MrcError::InvalidInput(format!(
|
Err(MrcError::InvalidInput(format!(
|
||||||
"Invalid seconds: {}",
|
"Invalid seconds: {seconds}"
|
||||||
seconds
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,11 +163,11 @@ async fn process_command(command: &str, socket_path: &str) -> MrcResult<String>
|
||||||
Ok("Cleared playlist\n".to_string())
|
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) => {
|
Some(data) => {
|
||||||
let pretty_json =
|
let pretty_json =
|
||||||
serde_json::to_string_pretty(&data).map_err(MrcError::ParseError)?;
|
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()),
|
None => Ok("Playlist is empty\n".to_string()),
|
||||||
},
|
},
|
||||||
|
|
@ -216,7 +210,7 @@ async fn main() -> MrcResult<()> {
|
||||||
);
|
);
|
||||||
return Err(MrcError::ConnectionError(std::io::Error::new(
|
return Err(MrcError::ConnectionError(std::io::Error::new(
|
||||||
std::io::ErrorKind::NotFound,
|
std::io::ErrorKind::NotFound,
|
||||||
format!("MPV socket not found at '{}'", socket_path),
|
format!("MPV socket not found at '{socket_path}'"),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue