Replace mpdrs with own implementation
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
751d19f4f3
commit
039509a08d
5 changed files with 130 additions and 114 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
51
src/main.rs
51
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<mpdrs::Song>,
|
||||
struct PlayerTemplate<'a> {
|
||||
song: Option<&'a HashMap<String, String>>,
|
||||
name: Option<String>,
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
164
src/mpd.rs
164
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<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 id: u32,
|
||||
pub file: String,
|
||||
|
@ -62,26 +20,7 @@ pub struct QueueItem {
|
|||
pub playing: bool,
|
||||
}
|
||||
|
||||
pub fn playlist() -> anyhow::Result<Vec<QueueItem>> {
|
||||
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<TcpStream>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandResult {
|
||||
properties: Vec<(String, String)>,
|
||||
binary: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl CommandResult {
|
||||
pub fn as_hashmap<'a>(&'a self) -> HashMap<String, String> {
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
<div class="current">
|
||||
{% if let Some(song) = song %}
|
||||
<div class="albumart">
|
||||
<a href="/art?path={{ song.file|urlencode }}" target="_blank">
|
||||
<a href="/art?path={{ song["file"]|urlencode }}" target="_blank">
|
||||
<img
|
||||
src="/art?path={{ song.file|urlencode }}"
|
||||
src="/art?path={{ song["file"]|urlencode }}"
|
||||
onload="this.style.visibility = 'visible'"
|
||||
alt="Album art"
|
||||
>
|
||||
|
@ -17,7 +17,7 @@
|
|||
{% if let Some(name) = name %}
|
||||
<div class="song__name" title="Song name">{{ name }}</div>
|
||||
{% endif %}
|
||||
{% if let Some(artist) = song.artist %}
|
||||
{% if let Some(artist) = song.get("Artist") %}
|
||||
<div class="song__artist" title="Artist">{{ artist }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -34,7 +34,7 @@
|
|||
class="control material-symbols-outlined" role="button" title="Previous track"
|
||||
>skip_previous</button>
|
||||
|
||||
{% if state == mpdrs::State::Play %}
|
||||
{% if state == "play" %}
|
||||
<button
|
||||
hx-post="/pause"
|
||||
class="control material-symbols-outlined" role="button" title="Pause"
|
||||
|
@ -56,7 +56,7 @@
|
|||
|
||||
<script>
|
||||
{% if let Some(name) = name %}
|
||||
{% if state == mpdrs::State::Play %}
|
||||
{% if state == "play" %}
|
||||
document.title = "▶ " + {{ name|json|safe }} + " - Empede";
|
||||
{% else %}
|
||||
document.title = "⏸ " + {{ name|json|safe }} + " - Empede";
|
||||
|
@ -65,7 +65,7 @@
|
|||
document.title = "Empede";
|
||||
{% endif %}
|
||||
|
||||
{% if state == mpdrs::State::Play %}
|
||||
{% if state == "play" %}
|
||||
progressBar = document.querySelector(".nowplaying .progress");
|
||||
elapsed = {{ elapsed }};
|
||||
duration = {{ duration }};
|
||||
|
|
Loading…
Reference in a new issue