Replace mpdrs with own implementation
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Sijmen 2023-05-14 15:31:16 +02:00
parent 751d19f4f3
commit 039509a08d
Signed by: vijfhoek
GPG Key ID: DAF7821E067D9C48
5 changed files with 130 additions and 114 deletions

16
Cargo.lock generated
View File

@ -426,12 +426,6 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "bufstream"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.12.1" version = "3.12.1"
@ -666,7 +660,6 @@ dependencies = [
"askama_tide", "askama_tide",
"async-std", "async-std",
"infer 0.13.0", "infer 0.13.0",
"mpdrs",
"percent-encoding", "percent-encoding",
"serde", "serde",
"serde_qs 0.12.0", "serde_qs 0.12.0",
@ -1114,15 +1107,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mpdrs"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec81b7ee8968b02af77c52cbcee75a33e83924a4322090f3d83075bd2032686"
dependencies = [
"bufstream",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"

View File

@ -12,7 +12,6 @@ askama = { version = "0.12.0", default-features = false, features = ["serde-json
askama_tide = "0.15.0" askama_tide = "0.15.0"
async-std = { version = "1.12.0", features = ["attributes"] } async-std = { version = "1.12.0", features = ["attributes"] }
infer = { version = "0.13.0", default-features = false } infer = { version = "0.13.0", default-features = false }
mpdrs = "0.1.0"
percent-encoding = "2.2.0" percent-encoding = "2.2.0"
serde = { version = "1.0.160", features = ["derive"] } serde = { version = "1.0.160", features = ["derive"] }
serde_qs = "0.12.0" serde_qs = "0.12.0"

View File

@ -1,4 +1,4 @@
use std::path::Path; use std::{path::Path, collections::HashMap};
use askama::Template; use askama::Template;
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
@ -33,39 +33,39 @@ struct QueueTemplate {
} }
async fn get_queue(_req: tide::Request<()>) -> tide::Result { 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 }; let template = QueueTemplate { queue };
Ok(template.into()) Ok(template.into())
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "player.html")] #[template(path = "player.html")]
struct PlayerTemplate { struct PlayerTemplate<'a> {
song: Option<mpdrs::Song>, song: Option<&'a HashMap<String, String>>,
name: Option<String>, name: Option<String>,
state: mpdrs::State, state: &'a str,
elapsed: f32, elapsed: f32,
duration: f32, duration: f32,
} }
async fn get_player(_req: tide::Request<()>) -> tide::Result { async fn get_player(_req: tide::Request<()>) -> tide::Result {
let mut mpd = mpd::connect()?; let mut mpd = mpd::Mpd::connect().await?;
let song = mpd.currentsong()?; let song = mpd.command("currentsong").await?.as_hashmap();
let status = mpd.status()?; let status = mpd.command("status").await?.as_hashmap();
let elapsed = status.elapsed.map(|d| d.as_secs_f32()).unwrap_or(0.0); let elapsed = status["elapsed"].parse().unwrap_or(0.0);
let duration = status.duration.map(|d| d.as_secs_f32()).unwrap_or(0.0); let duration = status["duration"].parse().unwrap_or(1.0);
let mut template = PlayerTemplate { let mut template = PlayerTemplate {
song: song.clone(), song: if song.is_empty() { None } else { Some(&song) },
name: None, name: None,
state: status.state, state: &status["state"],
elapsed, elapsed,
duration, duration,
}; };
if let Some(song) = song { if !song.is_empty() {
let name = song.title.unwrap_or(song.file); let name = song.get("Title").unwrap_or(&song["file"]).to_string();
template.name = Some(name); template.name = Some(name);
} }
@ -82,7 +82,7 @@ struct BrowserTemplate {
async fn get_browser(req: tide::Request<()>) -> tide::Result { async fn get_browser(req: tide::Request<()>) -> tide::Result {
let query: IndexQuery = req.query()?; let query: IndexQuery = req.query()?;
let path = percent_decode_str(&query.path).decode_utf8_lossy(); 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 { let template = BrowserTemplate {
path: Path::new(&*path) path: Path::new(&*path)
@ -137,33 +137,33 @@ struct DeleteQueueQuery {
async fn delete_queue(req: tide::Request<()>) -> tide::Result { async fn delete_queue(req: tide::Request<()>) -> tide::Result {
let query: DeleteQueueQuery = req.query()?; let query: DeleteQueueQuery = req.query()?;
let mut mpd = mpd::connect()?; let mut mpd = mpd::Mpd::connect().await?;
if let Some(id) = query.id { if let Some(id) = query.id {
mpd.deleteid(id)?; mpd.command(&format!("deleteid {id}")).await?;
} else { } else {
mpd.clear()?; mpd.command("clear").await?;
} }
Ok("".into()) Ok("".into())
} }
async fn post_play(_req: tide::Request<()>) -> tide::Result { async fn post_play(_req: tide::Request<()>) -> tide::Result {
mpd::connect()?.play()?; mpd::Mpd::connect().await?.command("play").await?;
Ok("".into()) Ok("".into())
} }
async fn post_pause(_req: tide::Request<()>) -> tide::Result { async fn post_pause(_req: tide::Request<()>) -> tide::Result {
mpd::connect()?.pause(true)?; mpd::Mpd::connect().await?.command("pause 1").await?;
Ok("".into()) Ok("".into())
} }
async fn post_previous(_req: tide::Request<()>) -> tide::Result { async fn post_previous(_req: tide::Request<()>) -> tide::Result {
mpd::connect()?.prev()?; mpd::Mpd::connect().await?.command("previous").await?;
Ok("".into()) Ok("".into())
} }
async fn post_next(_req: tide::Request<()>) -> tide::Result { async fn post_next(_req: tide::Request<()>) -> tide::Result {
mpd::connect()?.next()?; mpd::Mpd::connect().await?.command("next").await?;
Ok("".into()) Ok("".into())
} }
@ -175,11 +175,8 @@ struct UpdateQueueBody {
async fn post_queue_move(mut req: tide::Request<()>) -> tide::Result { async fn post_queue_move(mut req: tide::Request<()>) -> tide::Result {
let body: UpdateQueueBody = req.body_json().await?; let body: UpdateQueueBody = req.body_json().await?;
let mut mpd = mpd::connect()?; let mut mpd = mpd::Mpd::connect().await?;
mpd.move_range( mpd.command(&format!("move {} {}", body.from, body.to)).await?;
mpdrs::song::Range(Some(body.from), Some(body.from + 1)),
body.to as usize,
)?;
Ok("".into()) Ok("".into())
} }

View File

@ -1,9 +1,10 @@
use std::collections::HashMap;
use anyhow::anyhow; use anyhow::anyhow;
use async_std::{ use async_std::{
io::{prelude::BufReadExt, BufReader, ReadExt, WriteExt}, io::{prelude::BufReadExt, BufReader, ReadExt, WriteExt},
net::TcpStream, net::TcpStream,
}; };
use mpdrs::lsinfo::LsInfoResponse;
pub fn host() -> String { pub fn host() -> String {
let host = std::env::var("MPD_HOST").unwrap_or("localhost".to_string()); let host = std::env::var("MPD_HOST").unwrap_or("localhost".to_string());
@ -11,49 +12,6 @@ pub fn host() -> String {
format!("{host}:{port}") format!("{host}:{port}")
} }
pub fn connect() -> Result<mpdrs::Client, mpdrs::error::Error> {
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<Vec<Entry>> {
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 struct QueueItem {
pub id: u32, pub id: u32,
pub file: String, pub file: String,
@ -62,26 +20,7 @@ pub struct QueueItem {
pub playing: bool, pub playing: bool,
} }
pub fn playlist() -> anyhow::Result<Vec<QueueItem>> { #[derive(Debug)]
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)
}
pub enum Entry { pub enum Entry {
Song { Song {
name: String, name: String,
@ -103,11 +42,18 @@ pub struct Mpd {
reader: BufReader<TcpStream>, reader: BufReader<TcpStream>,
} }
#[derive(Debug)]
pub struct CommandResult { pub struct CommandResult {
properties: Vec<(String, String)>, properties: Vec<(String, String)>,
binary: Option<Vec<u8>>, binary: Option<Vec<u8>>,
} }
impl CommandResult {
pub fn as_hashmap<'a>(&'a self) -> HashMap<String, String> {
self.properties.iter().cloned().collect()
}
}
impl Mpd { impl Mpd {
pub fn escape_str(s: &str) -> String { pub fn escape_str(s: &str) -> String {
s.replace('\"', "\\\"").replace('\'', "\\'") s.replace('\"', "\\\"").replace('\'', "\\'")
@ -275,4 +221,94 @@ impl Mpd {
None => Err(anyhow!("no album art")), None => Err(anyhow!("no album art")),
} }
} }
pub fn split_properties(
properties: Vec<(String, String)>,
at: &[&str],
) -> Vec<HashMap<String, String>> {
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<Vec<Entry>> {
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<Vec<QueueItem>> {
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)
}
} }

View File

@ -4,9 +4,9 @@
<div class="current"> <div class="current">
{% if let Some(song) = song %} {% if let Some(song) = song %}
<div class="albumart"> <div class="albumart">
<a href="/art?path={{ song.file|urlencode }}" target="_blank"> <a href="/art?path={{ song["file"]|urlencode }}" target="_blank">
<img <img
src="/art?path={{ song.file|urlencode }}" src="/art?path={{ song["file"]|urlencode }}"
onload="this.style.visibility = 'visible'" onload="this.style.visibility = 'visible'"
alt="Album art" alt="Album art"
> >
@ -17,7 +17,7 @@
{% if let Some(name) = name %} {% if let Some(name) = name %}
<div class="song__name" title="Song name">{{ name }}</div> <div class="song__name" title="Song name">{{ name }}</div>
{% endif %} {% endif %}
{% if let Some(artist) = song.artist %} {% if let Some(artist) = song.get("Artist") %}
<div class="song__artist" title="Artist">{{ artist }}</div> <div class="song__artist" title="Artist">{{ artist }}</div>
{% endif %} {% endif %}
</div> </div>
@ -34,7 +34,7 @@
class="control material-symbols-outlined" role="button" title="Previous track" class="control material-symbols-outlined" role="button" title="Previous track"
>skip_previous</button> >skip_previous</button>
{% if state == mpdrs::State::Play %} {% if state == "play" %}
<button <button
hx-post="/pause" hx-post="/pause"
class="control material-symbols-outlined" role="button" title="Pause" class="control material-symbols-outlined" role="button" title="Pause"
@ -56,7 +56,7 @@
<script> <script>
{% if let Some(name) = name %} {% if let Some(name) = name %}
{% if state == mpdrs::State::Play %} {% if state == "play" %}
document.title = "▶ " + {{ name|json|safe }} + " - Empede"; document.title = "▶ " + {{ name|json|safe }} + " - Empede";
{% else %} {% else %}
document.title = "⏸ " + {{ name|json|safe }} + " - Empede"; document.title = "⏸ " + {{ name|json|safe }} + " - Empede";
@ -65,7 +65,7 @@
document.title = "Empede"; document.title = "Empede";
{% endif %} {% endif %}
{% if state == mpdrs::State::Play %} {% if state == "play" %}
progressBar = document.querySelector(".nowplaying .progress"); progressBar = document.querySelector(".nowplaying .progress");
elapsed = {{ elapsed }}; elapsed = {{ elapsed }};
duration = {{ duration }}; duration = {{ duration }};