Replace mpdrs with own implementation
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
751d19f4f3
commit
039509a08d
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
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 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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
164
src/mpd.rs
164
src/mpd.rs
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }};
|
||||||
|
|
Loading…
Reference in New Issue