From 039509a08d82c2230b22186f9f041e18277bb117 Mon Sep 17 00:00:00 2001 From: Sijmen Date: Sun, 14 May 2023 15:31:16 +0200 Subject: [PATCH] Replace mpdrs with own implementation --- Cargo.lock | 16 ----- Cargo.toml | 1 - src/main.rs | 51 +++++++------ src/mpd.rs | 164 +++++++++++++++++++++++++----------------- templates/player.html | 12 ++-- 5 files changed, 130 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7bd747..07eb0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,12 +426,6 @@ dependencies = [ "log", ] -[[package]] -name = "bufstream" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" - [[package]] name = "bumpalo" version = "3.12.1" @@ -666,7 +660,6 @@ dependencies = [ "askama_tide", "async-std", "infer 0.13.0", - "mpdrs", "percent-encoding", "serde", "serde_qs 0.12.0", @@ -1114,15 +1107,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "mpdrs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec81b7ee8968b02af77c52cbcee75a33e83924a4322090f3d83075bd2032686" -dependencies = [ - "bufstream", -] - [[package]] name = "nom" version = "7.1.3" diff --git a/Cargo.toml b/Cargo.toml index 15cf950..2387e49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ askama = { version = "0.12.0", default-features = false, features = ["serde-json askama_tide = "0.15.0" async-std = { version = "1.12.0", features = ["attributes"] } infer = { version = "0.13.0", default-features = false } -mpdrs = "0.1.0" percent-encoding = "2.2.0" serde = { version = "1.0.160", features = ["derive"] } serde_qs = "0.12.0" diff --git a/src/main.rs b/src/main.rs index 43ac3bb..7025200 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{path::Path, collections::HashMap}; use askama::Template; use percent_encoding::percent_decode_str; @@ -33,39 +33,39 @@ struct QueueTemplate { } async fn get_queue(_req: tide::Request<()>) -> tide::Result { - let queue = mpd::playlist()?; + let queue = mpd::Mpd::connect().await?.playlist().await?; let template = QueueTemplate { queue }; Ok(template.into()) } #[derive(Template)] #[template(path = "player.html")] -struct PlayerTemplate { - song: Option, +struct PlayerTemplate<'a> { + song: Option<&'a HashMap>, name: Option, - state: mpdrs::State, + state: &'a str, elapsed: f32, duration: f32, } async fn get_player(_req: tide::Request<()>) -> tide::Result { - let mut mpd = mpd::connect()?; - let song = mpd.currentsong()?; - let status = mpd.status()?; + let mut mpd = mpd::Mpd::connect().await?; + let song = mpd.command("currentsong").await?.as_hashmap(); + let status = mpd.command("status").await?.as_hashmap(); - let elapsed = status.elapsed.map(|d| d.as_secs_f32()).unwrap_or(0.0); - let duration = status.duration.map(|d| d.as_secs_f32()).unwrap_or(0.0); + let elapsed = status["elapsed"].parse().unwrap_or(0.0); + let duration = status["duration"].parse().unwrap_or(1.0); let mut template = PlayerTemplate { - song: song.clone(), + song: if song.is_empty() { None } else { Some(&song) }, name: None, - state: status.state, + state: &status["state"], elapsed, duration, }; - if let Some(song) = song { - let name = song.title.unwrap_or(song.file); + if !song.is_empty() { + let name = song.get("Title").unwrap_or(&song["file"]).to_string(); template.name = Some(name); } @@ -82,7 +82,7 @@ struct BrowserTemplate { async fn get_browser(req: tide::Request<()>) -> tide::Result { let query: IndexQuery = req.query()?; let path = percent_decode_str(&query.path).decode_utf8_lossy(); - let entries = mpd::ls(&path)?; + let entries = mpd::Mpd::connect().await?.ls(&path).await?; let template = BrowserTemplate { path: Path::new(&*path) @@ -137,33 +137,33 @@ struct DeleteQueueQuery { async fn delete_queue(req: tide::Request<()>) -> tide::Result { let query: DeleteQueueQuery = req.query()?; - let mut mpd = mpd::connect()?; + let mut mpd = mpd::Mpd::connect().await?; if let Some(id) = query.id { - mpd.deleteid(id)?; + mpd.command(&format!("deleteid {id}")).await?; } else { - mpd.clear()?; + mpd.command("clear").await?; } Ok("".into()) } async fn post_play(_req: tide::Request<()>) -> tide::Result { - mpd::connect()?.play()?; + mpd::Mpd::connect().await?.command("play").await?; Ok("".into()) } async fn post_pause(_req: tide::Request<()>) -> tide::Result { - mpd::connect()?.pause(true)?; + mpd::Mpd::connect().await?.command("pause 1").await?; Ok("".into()) } async fn post_previous(_req: tide::Request<()>) -> tide::Result { - mpd::connect()?.prev()?; + mpd::Mpd::connect().await?.command("previous").await?; Ok("".into()) } async fn post_next(_req: tide::Request<()>) -> tide::Result { - mpd::connect()?.next()?; + mpd::Mpd::connect().await?.command("next").await?; Ok("".into()) } @@ -175,11 +175,8 @@ struct UpdateQueueBody { async fn post_queue_move(mut req: tide::Request<()>) -> tide::Result { let body: UpdateQueueBody = req.body_json().await?; - let mut mpd = mpd::connect()?; - mpd.move_range( - mpdrs::song::Range(Some(body.from), Some(body.from + 1)), - body.to as usize, - )?; + let mut mpd = mpd::Mpd::connect().await?; + mpd.command(&format!("move {} {}", body.from, body.to)).await?; Ok("".into()) } diff --git a/src/mpd.rs b/src/mpd.rs index 743cc08..ec8e981 100644 --- a/src/mpd.rs +++ b/src/mpd.rs @@ -1,9 +1,10 @@ +use std::collections::HashMap; + use anyhow::anyhow; use async_std::{ io::{prelude::BufReadExt, BufReader, ReadExt, WriteExt}, net::TcpStream, }; -use mpdrs::lsinfo::LsInfoResponse; pub fn host() -> String { let host = std::env::var("MPD_HOST").unwrap_or("localhost".to_string()); @@ -11,49 +12,6 @@ pub fn host() -> String { format!("{host}:{port}") } -pub fn connect() -> Result { - let mut client = mpdrs::Client::connect(host())?; - - let password = std::env::var("MPD_PASSWORD").unwrap_or(String::new()); - if !password.is_empty() { - client.login(&password)?; - } - - Ok(client) -} - -pub fn ls(path: &str) -> anyhow::Result> { - let info = connect()?.lsinfo(path)?; - - fn filename(path: &str) -> String { - std::path::Path::new(path) - .file_name() - .map(|x| x.to_string_lossy().to_string()) - .unwrap_or("n/a".to_string()) - } - - Ok(info - .iter() - .map(|e| match e { - LsInfoResponse::Song(song) => Entry::Song { - name: song.title.as_ref().unwrap_or(&filename(&song.file)).clone(), - artist: song.artist.clone().unwrap_or(String::new()), - path: song.file.clone(), - }, - - LsInfoResponse::Directory { path, .. } => Entry::Directory { - name: filename(path), - path: path.to_string(), - }, - - LsInfoResponse::Playlist { path, .. } => Entry::Playlist { - name: filename(path), - path: path.to_string(), - }, - }) - .collect()) -} - pub struct QueueItem { pub id: u32, pub file: String, @@ -62,26 +20,7 @@ pub struct QueueItem { pub playing: bool, } -pub fn playlist() -> anyhow::Result> { - let mut client = connect()?; - - let current = client.status()?.song; - - let queue = client - .queue()? - .into_iter() - .map(|song| QueueItem { - id: song.place.unwrap().id, - file: song.file.clone(), - title: song.title.as_ref().unwrap_or(&song.file).clone(), - artist: song.artist.clone(), - playing: current == song.place, - }) - .collect(); - - Ok(queue) -} - +#[derive(Debug)] pub enum Entry { Song { name: String, @@ -103,11 +42,18 @@ pub struct Mpd { reader: BufReader, } +#[derive(Debug)] pub struct CommandResult { properties: Vec<(String, String)>, binary: Option>, } +impl CommandResult { + pub fn as_hashmap<'a>(&'a self) -> HashMap { + self.properties.iter().cloned().collect() + } +} + impl Mpd { pub fn escape_str(s: &str) -> String { s.replace('\"', "\\\"").replace('\'', "\\'") @@ -275,4 +221,94 @@ impl Mpd { None => Err(anyhow!("no album art")), } } + + pub fn split_properties( + properties: Vec<(String, String)>, + at: &[&str], + ) -> Vec> { + let mut output = Vec::new(); + let mut current = None; + + for (key, value) in properties { + if at.contains(&key.as_str()) { + if let Some(current) = current { + output.push(current); + } + current = Some(HashMap::new()); + } + + if let Some(current) = current.as_mut() { + current.insert(key, value); + } + } + + if let Some(current) = current { + output.push(current); + } + + output + } + + pub async fn ls(&mut self, path: &str) -> anyhow::Result> { + fn get_filename(path: &str) -> String { + std::path::Path::new(path) + .file_name() + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or("n/a".to_string()) + } + + let result = self + .command(&format!("lsinfo \"{}\"", Self::escape_str(&path))) + .await?; + + let props = Self::split_properties(result.properties, &["file", "directory", "playlist"]); + + let files = props + .iter() + .flat_map(|prop| { + if let Some(file) = prop.get("file") { + Some(Entry::Song { + name: prop.get("Title").unwrap_or(&get_filename(&file)).clone(), + artist: prop.get("Artist").unwrap_or(&String::new()).clone(), + path: file.to_string(), + }) + } else if let Some(file) = prop.get("directory") { + Some(Entry::Directory { + name: get_filename(&file), + path: file.to_string(), + }) + } else if let Some(file) = prop.get("playlist") { + Some(Entry::Playlist { + name: get_filename(&file), + path: file.to_string(), + }) + } else { + None + } + }) + .collect(); + + Ok(files) + } + + pub async fn playlist(&mut self) -> anyhow::Result> { + let status = self.command("status").await?.as_hashmap(); + let current_songid = status.get("songid"); + + let playlistinfo = self.command("playlistinfo").await?; + let queue = Self::split_properties(playlistinfo.properties, &["file"]); + + let queue = queue + .iter() + .map(|song| QueueItem { + id: song["Id"].parse().unwrap(), + file: song["file"].clone(), + title: song.get("Title").unwrap_or(&song["file"]).clone(), + artist: song.get("Artist").cloned(), + playing: current_songid == song.get("Id"), + }) + .collect(); + + Ok(queue) + } } diff --git a/templates/player.html b/templates/player.html index cd3bd1f..1a502b4 100644 --- a/templates/player.html +++ b/templates/player.html @@ -4,9 +4,9 @@