use std::io::{BufRead, BufReader, Write}; use std::net::TcpStream; pub struct MpdClient { reader: BufReader, stream: TcpStream, } impl MpdClient { pub fn connect() -> Result { let host = std::env::var("MPD_HOST").unwrap_or_else(|_| "127.0.0.1".into()); let port = std::env::var("MPD_PORT").unwrap_or_else(|_| "6600".into()); let addr = format!("{host}:{port}"); let stream = TcpStream::connect(&addr).map_err(|e| format!("connect to {addr}: {e}"))?; let reader = BufReader::new(stream.try_clone().map_err(|e| e.to_string())?); let mut client = MpdClient { reader, stream }; // Consume greeting line ("OK MPD ...") let mut greeting = String::new(); client .reader .read_line(&mut greeting) .map_err(|e| format!("read greeting: {e}"))?; if !greeting.starts_with("OK MPD") { return Err(format!("unexpected greeting: {greeting}")); } Ok(client) } fn send_command(&mut self, cmd: &str) -> Result<(), String> { self.stream .write_all(format!("{cmd}\n").as_bytes()) .map_err(|e| format!("write '{cmd}': {e}"))?; loop { let mut line = String::new(); self.reader .read_line(&mut line) .map_err(|e| format!("read response to '{cmd}': {e}"))?; if line == "OK\n" { return Ok(()); } if line.starts_with("ACK") { return Err(line.trim().to_string()); } } } /// Send `update` and wait for the updating_db job to finish. fn update_and_wait(&mut self) -> Result<(), String> { self.send_command("update")?; // Poll `status` until `updating_db` key disappears. loop { let status = self.send_command_read("status")?; if !status.iter().any(|l| l.starts_with("updating_db:")) { return Ok(()); } std::thread::sleep(std::time::Duration::from_millis(200)); } } /// Send a command and return all response lines (before OK/ACK). fn send_command_read(&mut self, cmd: &str) -> Result, String> { self.stream .write_all(format!("{cmd}\n").as_bytes()) .map_err(|e| format!("write '{cmd}': {e}"))?; let mut lines = Vec::new(); loop { let mut line = String::new(); self.reader .read_line(&mut line) .map_err(|e| format!("read response to '{cmd}': {e}"))?; if line == "OK\n" { return Ok(lines); } if line.starts_with("ACK") { return Err(line.trim().to_string()); } lines.push(line.trim().to_string()); } } pub fn queue_playlist(&mut self, tracks: &[String], local_dir: &str, mpd_dir: &str) { if let Err(e) = self.send_command("clear") { eprintln!("MPD clear: {e}"); return; } let mut failed: Vec = Vec::new(); for track in tracks { let uri = Self::track_to_uri(track, local_dir, mpd_dir); let escaped = uri.replace('\\', "\\\\").replace('"', "\\\""); if self.send_command(&format!("add \"{escaped}\"")).is_err() { failed.push(uri.to_string()); } } // If some tracks failed, update MPD's DB and retry them if !failed.is_empty() { eprintln!("MPD: {} tracks not found, updating database...", failed.len()); if self.update_and_wait().is_ok() { for uri in &failed { let escaped = uri.replace('\\', "\\\\").replace('"', "\\\""); if let Err(e) = self.send_command(&format!("add \"{escaped}\"")) { eprintln!("MPD add {uri}: {e}"); } } } } if let Err(e) = self.send_command("play") { eprintln!("MPD play: {e}"); } } fn track_to_uri(track: &str, local_dir: &str, mpd_dir: &str) -> String { let relative = track .strip_prefix(local_dir) .map(|p| p.trim_start_matches('/')) .unwrap_or(track); let mpd_base = mpd_dir.trim_end_matches('/'); format!("{mpd_base}/{relative}") } }