129 lines
4.4 KiB
Rust
129 lines
4.4 KiB
Rust
use std::io::{BufRead, BufReader, Write};
|
|
use std::net::TcpStream;
|
|
|
|
pub struct MpdClient {
|
|
reader: BufReader<TcpStream>,
|
|
stream: TcpStream,
|
|
}
|
|
|
|
impl MpdClient {
|
|
pub fn connect() -> Result<Self, String> {
|
|
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<Vec<String>, 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<String> = 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}")
|
|
}
|
|
}
|