Add Unix socket API for OS-wide keyboard shortcuts
All checks were successful
Build and Release / build-and-release (push) Successful in 55s
All checks were successful
Build and Release / build-and-release (push) Successful in 55s
Implement single binary pattern where cm-player acts as both server (TUI mode) and client (command sender) based on CLI arguments. - Add Unix socket API server at $XDG_RUNTIME_DIR/cm-player.sock - Add client mode for sending commands: cm-player play-pause, next, etc. - Support all playback commands: play-pause, stop, next, prev - Support volume commands: volume-up, volume-down, volume <0-100> - Support seek commands: seek-forward, seek-backward - Support status query and quit commands - Add short command aliases (pp, n, p, vu, vd, sf, sb, s, q) - Commands run in parallel thread, non-blocking main loop - Enable OS-wide keyboard shortcut integration (XF86Audio* keys)
This commit is contained in:
parent
7b4c664011
commit
93741320ac
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-player"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
136
src/api/mod.rs
Normal file
136
src/api/mod.rs
Normal file
@ -0,0 +1,136 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::thread;
|
||||
|
||||
/// Commands that can be sent to cm-player via the API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "command", rename_all = "kebab-case")]
|
||||
pub enum ApiCommand {
|
||||
/// Toggle play/pause (play if stopped, pause if playing)
|
||||
PlayPause,
|
||||
/// Stop playback
|
||||
Stop,
|
||||
/// Next track
|
||||
Next,
|
||||
/// Previous track
|
||||
Prev,
|
||||
/// Volume up by 5%
|
||||
VolumeUp,
|
||||
/// Volume down by 5%
|
||||
VolumeDown,
|
||||
/// Set volume to specific value (0-100)
|
||||
VolumeSet { volume: i64 },
|
||||
/// Seek forward by seconds
|
||||
SeekForward { seconds: f64 },
|
||||
/// Seek backward by seconds
|
||||
SeekBackward { seconds: f64 },
|
||||
/// Get current player status
|
||||
GetStatus,
|
||||
/// Quit the application
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// Response from the API
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApiResponse {
|
||||
pub success: bool,
|
||||
pub message: Option<String>,
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ApiResponse {
|
||||
pub fn success() -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
message: None,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success_with_data(data: serde_json::Value) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
message: None,
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(message: String) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
message: Some(message),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the API server on a Unix socket
|
||||
pub fn start_api_server(socket_path: PathBuf) -> Result<Receiver<ApiCommand>> {
|
||||
// Remove old socket if it exists
|
||||
if socket_path.exists() {
|
||||
std::fs::remove_file(&socket_path)?;
|
||||
}
|
||||
|
||||
let listener = UnixListener::bind(&socket_path)
|
||||
.context("Failed to bind Unix socket for API")?;
|
||||
|
||||
tracing::info!("API server listening on {:?}", socket_path);
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
// Spawn thread to handle incoming connections
|
||||
thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => {
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = handle_client(stream, tx) {
|
||||
tracing::warn!("API client error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("API connection error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
fn handle_client(mut stream: UnixStream, tx: Sender<ApiCommand>) -> Result<()> {
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
let mut line = String::new();
|
||||
|
||||
reader.read_line(&mut line)?;
|
||||
|
||||
// Parse command
|
||||
let command: ApiCommand = serde_json::from_str(&line)
|
||||
.context("Failed to parse API command")?;
|
||||
|
||||
tracing::debug!("Received API command: {:?}", command);
|
||||
|
||||
// Send command to main thread
|
||||
tx.send(command.clone())
|
||||
.context("Failed to send command to main thread")?;
|
||||
|
||||
// Send response
|
||||
let response = ApiResponse::success();
|
||||
let response_json = serde_json::to_string(&response)?;
|
||||
writeln!(stream, "{}", response_json)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to get default socket path
|
||||
pub fn get_default_socket_path() -> Result<PathBuf> {
|
||||
let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
|
||||
.unwrap_or_else(|_| "/tmp".to_string());
|
||||
Ok(PathBuf::from(runtime_dir).join("cm-player.sock"))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user