use anyhow::{Context, Result}; use serde_json::{json, Value}; use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::time::Duration; use tempfile::NamedTempFile; pub struct Player { process: Child, socket_path: PathBuf, socket: Option, position: f64, duration: f64, pub media_title: Option, pub artist: Option, pub album: Option, pub audio_codec: Option, pub audio_bitrate: Option, pub sample_rate: Option, pub cache_duration: Option, } impl Player { pub fn new() -> Result { // Create temporary socket file let socket_file = NamedTempFile::new().context("Failed to create temp socket")?; let socket_path = socket_file.path().to_path_buf(); drop(socket_file); // Remove the file so MPV can create the socket // Spawn MPV with IPC server let process = Command::new("mpv") .arg("--idle") .arg("--no-terminal") .arg("--profile=fast") .arg("--audio-display=no") // Don't show cover art for audio files .arg("--audio-buffer=2") // Larger buffer for WSLg audio stability .arg(format!("--input-ipc-server={}", socket_path.display())) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .context("Failed to spawn MPV process")?; tracing::info!("MPV process started with IPC at {:?}", socket_path); Ok(Self { process, socket_path, socket: None, position: 0.0, duration: 0.0, media_title: None, artist: None, album: None, audio_codec: None, audio_bitrate: None, sample_rate: None, cache_duration: None, }) } fn connect(&mut self) -> Result<()> { if self.socket.is_none() { // CRITICAL: Only try to connect if socket file exists // If socket doesn't exist, MPV hasn't created it yet - fail fast if !self.socket_path.exists() { return Err(anyhow::anyhow!("Socket file doesn't exist yet")); } // Try to connect with a timeout using non-blocking mode // IMPORTANT: UnixStream::connect() blocks in the kernel if socket exists // but server isn't listening yet. We check existence first but still // need to handle connect blocking if MPV just created socket but isn't ready. let stream = match UnixStream::connect(&self.socket_path) { Ok(s) => s, Err(e) => { // Connection failed - MPV probably not ready yet return Err(anyhow::anyhow!("Failed to connect: {}", e)); } }; // Set non-blocking and timeout to prevent hangs on reads/writes stream.set_nonblocking(true)?; stream.set_read_timeout(Some(Duration::from_millis(100)))?; stream.set_write_timeout(Some(Duration::from_millis(100)))?; self.socket = Some(stream); tracing::debug!("Connected to MPV socket successfully"); } Ok(()) } fn send_command(&mut self, command: &str, args: &[Value]) -> Result<()> { self.connect()?; let mut command_array = vec![json!(command)]; command_array.extend_from_slice(args); let cmd = json!({ "command": command_array }); if let Some(ref mut socket) = self.socket { let msg = format!("{}\n", cmd); // Ignore broken pipe errors (mpv closed) if let Err(e) = socket.write_all(msg.as_bytes()) { if e.kind() == std::io::ErrorKind::BrokenPipe { self.socket = None; // Clean up dead process self.process.kill().ok(); return Ok(()); } return Err(e).context("Failed to write to socket"); } } Ok(()) } fn get_properties_batch(&mut self, properties: &[&str]) -> std::collections::HashMap { let mut results = std::collections::HashMap::new(); // Try to connect if self.connect().is_err() { return results; } if let Some(ref mut socket) = self.socket { // Send all property requests at once with unique request_ids for (idx, property) in properties.iter().enumerate() { let cmd = json!({ "command": ["get_property", property], "request_id": idx + 1 }); let msg = format!("{}\n", cmd); if socket.write_all(msg.as_bytes()).is_err() { return results; } } // Read all responses // IMPORTANT: Socket is non-blocking, need to set blocking mode for read socket.set_nonblocking(false).ok(); let cloned_socket = match socket.try_clone() { Ok(s) => s, Err(_) => { socket.set_nonblocking(true).ok(); return results; } }; cloned_socket.set_nonblocking(false).ok(); let mut reader = BufReader::new(cloned_socket); // Read responses - stop if we timeout or hit an error for _ in 0..properties.len() { let mut response = String::new(); if reader.read_line(&mut response).is_err() { // Timeout or error - stop reading break; } // Parse response if let Ok(parsed) = serde_json::from_str::(&response) { // Check for success if let Some(error) = parsed.get("error").and_then(|e| e.as_str()) { if error == "success" { // Get request_id to match with property if let Some(req_id) = parsed.get("request_id").and_then(|id| id.as_i64()) { let idx = (req_id - 1) as usize; if idx < properties.len() { if let Some(data) = parsed.get("data") { results.insert(properties[idx].to_string(), data.clone()); } } } } } } } // Restore non-blocking mode socket.set_nonblocking(true).ok(); } results } pub fn play(&mut self, path: &Path) -> Result<()> { let path_str = path.to_string_lossy(); // Reset position/duration before loading new file to avoid showing stale values self.position = 0.0; self.duration = 0.0; self.send_command("loadfile", &[json!(path_str), json!("replace")])?; tracing::info!("Playing: {}", path_str); Ok(()) } pub fn play_paused(&mut self, path: &Path) -> Result<()> { let path_str = path.to_string_lossy(); // Reset position/duration before loading new file to avoid showing stale values self.position = 0.0; self.duration = 0.0; // Load file but start paused - avoids audio blip when jumping tracks while paused self.send_command("loadfile", &[json!(path_str), json!("replace"), json!({"pause": true})])?; tracing::info!("Playing (paused): {}", path_str); Ok(()) } pub fn pause(&mut self) -> Result<()> { self.send_command("set_property", &[json!("pause"), json!(true)])?; Ok(()) } pub fn resume(&mut self) -> Result<()> { self.send_command("set_property", &[json!("pause"), json!(false)])?; Ok(()) } pub fn stop(&mut self) -> Result<()> { self.send_command("stop", &[])?; self.position = 0.0; self.duration = 0.0; Ok(()) } pub fn set_volume(&mut self, volume: i64) -> Result<()> { self.send_command("set_property", &[json!("volume"), json!(volume)])?; Ok(()) } pub fn update_all_properties(&mut self) { // Fetch ALL properties in one batch let results = self.get_properties_batch(&[ "time-pos", "duration", "metadata/by-key/artist", "metadata/by-key/ARTIST", "metadata/by-key/album", "metadata/by-key/ALBUM", "metadata/by-key/title", "metadata/by-key/TITLE", "media-title", "audio-codec-name", "audio-bitrate", "audio-params/samplerate", "demuxer-cache-duration", ]); // Position if let Some(val) = results.get("time-pos") { if let Some(pos) = val.as_f64() { self.position = pos; } } // Duration if let Some(val) = results.get("duration") { if let Some(dur) = val.as_f64() { self.duration = dur; } } // Artist - try lowercase first, then uppercase self.artist = results.get("metadata/by-key/artist") .and_then(|v| v.as_str().map(|s| s.to_string())) .or_else(|| results.get("metadata/by-key/ARTIST") .and_then(|v| v.as_str().map(|s| s.to_string()))); // Album - try lowercase first, then uppercase self.album = results.get("metadata/by-key/album") .and_then(|v| v.as_str().map(|s| s.to_string())) .or_else(|| results.get("metadata/by-key/ALBUM") .and_then(|v| v.as_str().map(|s| s.to_string()))); // Title - try lowercase, then uppercase, then media-title self.media_title = results.get("metadata/by-key/title") .and_then(|v| v.as_str().map(|s| s.to_string())) .or_else(|| results.get("metadata/by-key/TITLE") .and_then(|v| v.as_str().map(|s| s.to_string()))) .or_else(|| results.get("media-title") .and_then(|v| v.as_str().map(|s| s.to_string()))); // Audio codec self.audio_codec = results.get("audio-codec-name") .and_then(|v| v.as_str().map(|s| s.to_string())); // Audio bitrate (convert from bps to kbps) self.audio_bitrate = results.get("audio-bitrate") .and_then(|v| v.as_f64().map(|b| b / 1000.0)); // Sample rate self.sample_rate = results.get("audio-params/samplerate") .and_then(|v| v.as_i64()); // Cache duration self.cache_duration = results.get("demuxer-cache-duration") .and_then(|v| v.as_f64()); } pub fn get_position(&self) -> Option { Some(self.position) } pub fn get_duration(&self) -> Option { Some(self.duration) } pub fn get_player_state(&mut self) -> Option { use crate::state::PlayerState; // Batch fetch both properties at once let results = self.get_properties_batch(&["idle-active", "pause"]); let is_idle = results.get("idle-active").and_then(|v| v.as_bool())?; let is_paused = results.get("pause").and_then(|v| v.as_bool())?; Some(if is_idle { PlayerState::Stopped } else if is_paused { PlayerState::Paused } else { PlayerState::Playing }) } pub fn is_process_alive(&mut self) -> bool { // Check if mpv process is still running match self.process.try_wait() { Ok(Some(_)) => { // Process has exited - clean up socket self.socket = None; false } Ok(None) => true, // Process is still running Err(_) => { // Error checking, assume dead and clean up self.socket = None; false } } } pub fn seek(&mut self, seconds: f64) -> Result<()> { self.send_command("seek", &[json!(seconds), json!("relative")])?; Ok(()) } } impl Drop for Player { fn drop(&mut self) { self.send_command("quit", &[]).ok(); self.process.kill().ok(); std::fs::remove_file(&self.socket_path).ok(); } }