Several changes:
- Add MPD_HOST and MPD_PORT environment variables - Improve styling - Move browser to separate template - Reload directory on database update
This commit is contained in:
parent
2f0c35d6fe
commit
b2ff3b60c8
6 changed files with 143 additions and 73 deletions
31
src/main.rs
31
src/main.rs
|
@ -29,7 +29,7 @@ struct IndexQuery {
|
||||||
path: String,
|
path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn index(req: tide::Request<()>) -> tide::Result {
|
async fn get_index(req: tide::Request<()>) -> tide::Result {
|
||||||
let query: IndexQuery = req.query()?;
|
let query: IndexQuery = req.query()?;
|
||||||
let entries = mpd::ls(&query.path)?;
|
let entries = mpd::ls(&query.path)?;
|
||||||
let queue = mpd::playlist()?;
|
let queue = mpd::playlist()?;
|
||||||
|
@ -94,6 +94,28 @@ async fn get_player(_req: tide::Request<()>) -> tide::Result {
|
||||||
Ok(template.into())
|
Ok(template.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "browser.html")]
|
||||||
|
struct BrowserTemplate {
|
||||||
|
path: Vec<String>,
|
||||||
|
entries: Vec<mpd::Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_browser(req: tide::Request<()>) -> tide::Result {
|
||||||
|
let query: IndexQuery = req.query()?;
|
||||||
|
let entries = mpd::ls(&query.path)?;
|
||||||
|
|
||||||
|
let template = BrowserTemplate {
|
||||||
|
path: Path::new(&query.path)
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.collect(),
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(template.into())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PostQueueQuery {
|
struct PostQueueQuery {
|
||||||
path: String,
|
path: String,
|
||||||
|
@ -139,7 +161,7 @@ async fn get_art(req: tide::Request<()>) -> tide::Result {
|
||||||
|
|
||||||
async fn sse(_req: tide::Request<()>, sender: tide::sse::Sender) -> tide::Result<()> {
|
async fn sse(_req: tide::Request<()>, sender: tide::sse::Sender) -> tide::Result<()> {
|
||||||
// Needs to be async and all async mpd libraries suck
|
// Needs to be async and all async mpd libraries suck
|
||||||
let mut stream = TcpStream::connect(mpd::HOST).await?;
|
let mut stream = TcpStream::connect(mpd::host()).await?;
|
||||||
let mut reader = BufReader::new(stream.clone());
|
let mut reader = BufReader::new(stream.clone());
|
||||||
|
|
||||||
// skip OK MPD line
|
// skip OK MPD line
|
||||||
|
@ -152,7 +174,7 @@ async fn sse(_req: tide::Request<()>, sender: tide::sse::Sender) -> tide::Result
|
||||||
sender.send("player", "", None).await?;
|
sender.send("player", "", None).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
stream.write_all(b"idle playlist player\n").await?;
|
stream.write_all(b"idle playlist player database\n").await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
|
@ -179,10 +201,11 @@ async fn main() -> tide::Result<()> {
|
||||||
let mut app = tide::new();
|
let mut app = tide::new();
|
||||||
app.with(tide_tracing::TraceMiddleware::new());
|
app.with(tide_tracing::TraceMiddleware::new());
|
||||||
|
|
||||||
app.at("/").get(index);
|
app.at("/").get(get_index);
|
||||||
app.at("/queue").get(get_queue);
|
app.at("/queue").get(get_queue);
|
||||||
app.at("/player").get(get_player);
|
app.at("/player").get(get_player);
|
||||||
app.at("/art").get(get_art);
|
app.at("/art").get(get_art);
|
||||||
|
app.at("/browser").get(get_browser);
|
||||||
|
|
||||||
app.at("/sse").get(tide::sse::endpoint(sse));
|
app.at("/sse").get(tide::sse::endpoint(sse));
|
||||||
|
|
||||||
|
|
12
src/mpd.rs
12
src/mpd.rs
|
@ -2,10 +2,14 @@ use std::borrow::Cow;
|
||||||
|
|
||||||
use mpdrs::lsinfo::LsInfoResponse;
|
use mpdrs::lsinfo::LsInfoResponse;
|
||||||
|
|
||||||
pub(crate) const HOST: &str = "192.168.1.203:6600";
|
pub(crate) fn host() -> String {
|
||||||
|
let host = std::env::var("MPD_HOST").unwrap_or("localhost".to_string());
|
||||||
|
let port = std::env::var("MPD_PORT").unwrap_or("6600".to_string());
|
||||||
|
format!("{host}:{port}")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn connect() -> Result<mpdrs::Client, mpdrs::error::Error> {
|
pub(crate) fn connect() -> Result<mpdrs::Client, mpdrs::error::Error> {
|
||||||
mpdrs::Client::connect(HOST)
|
mpdrs::Client::connect(host())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn ls(path: &str) -> anyhow::Result<Vec<Entry>> {
|
pub(crate) fn ls(path: &str) -> anyhow::Result<Vec<Entry>> {
|
||||||
|
@ -41,7 +45,9 @@ pub(crate) fn ls(path: &str) -> anyhow::Result<Vec<Entry>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct QueueItem {
|
pub(crate) struct QueueItem {
|
||||||
|
pub(crate) file: String,
|
||||||
pub(crate) title: String,
|
pub(crate) title: String,
|
||||||
|
pub(crate) artist: Option<String>,
|
||||||
pub(crate) playing: bool,
|
pub(crate) playing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +60,9 @@ pub(crate) fn playlist() -> anyhow::Result<Vec<QueueItem>> {
|
||||||
.queue()?
|
.queue()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|song| QueueItem {
|
.map(|song| QueueItem {
|
||||||
|
file: song.file.clone(),
|
||||||
title: song.title.as_ref().unwrap_or(&song.file).clone(),
|
title: song.title.as_ref().unwrap_or(&song.file).clone(),
|
||||||
|
artist: song.artist.clone(),
|
||||||
playing: current == song.place,
|
playing: current == song.place,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
43
templates/browser.html
Normal file
43
templates/browser.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{# #}
|
||||||
|
<ul class="breadcrumb">
|
||||||
|
<li><a href="?path=">Root</a></li>
|
||||||
|
{% for (i, component) in path.iter().enumerate() %}
|
||||||
|
<li>
|
||||||
|
{% if i == path.len() - 1 %}
|
||||||
|
{{ component }}
|
||||||
|
{% else %}
|
||||||
|
<a href="?path={{ path[..i + 1].join("/") }}">
|
||||||
|
{{ component }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="dir" hx-boost="true">
|
||||||
|
{% for entry in entries %}
|
||||||
|
{% match entry %}
|
||||||
|
{% when mpd::Entry::Song with { name, path, artist } %}
|
||||||
|
<li hx-post="/queue?path={{path}}" hx-swap="none" role="button">
|
||||||
|
<span class="material-symbols-outlined">music_note</span>
|
||||||
|
<!-- img src="/art?path={{path}}" -->
|
||||||
|
<div class="song">
|
||||||
|
<div class="song__name">{{ name }}</div>
|
||||||
|
<div class="song__artist">{{ artist }}</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% when mpd::Entry::Directory with { name, path }%}
|
||||||
|
<li onclick="window.location = '?path={{path}}'">
|
||||||
|
<span class="material-symbols-outlined">folder</span>
|
||||||
|
<a href="?path={{path}}">{{ name }}</a>
|
||||||
|
</li>
|
||||||
|
{% when mpd::Entry::Playlist with { name, path } %}
|
||||||
|
<li hx-post="/queue?path={{path}}" hx-swap="none" role="button" >
|
||||||
|
<span class="material-symbols-outlined">playlist_play</span>
|
||||||
|
<div class="song">
|
||||||
|
<div class="song__name">{{ name }}</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endmatch %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
|
@ -10,12 +10,17 @@
|
||||||
<link href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABqklEQVRoQ+2XOy8FURSF720oJRK3U3lfohYahULjF6CU3Erjf0hoJH6BUqeWeIRGId5RoVMpaH1bLpkIkz17ztw5wz7JSiaZvfesddY+j6nXKj7qFedfcwFlO+gO/AcHbhE5pBR6R9ywMvYjrBMt5ALSHHEHFP3qLVR2C90kdpZTnt9SCD3wblnh6ldIp9fACF+Wlgo2XIBiKpOL2B34PmHeQt5Cihko+xzwRewO5GxTbyFvob/cQnKSL4J50ATjoAucgE2wA64S12n5YZcf92Ajz1WiBxa7YDaFzTXvGqC3HRONgG4Iyc/JZMapjEbANsRXMpKX8Ciu0xMQOTeQl5QoHNiAyKpRQBQOHEN+qsoCLiAvW6ZlDJJ0b0n8LceyjR5QbMZIQrbeF2Puj2kWAVtUahlIPJLTb8hLTbEIkFN3z0BknZw1Q15wAVLwEExnICNtMwCeM+SoQi0OSOE+sA/GFF95JWYOyO4VfFgFCBHNXeiJuAVwFpx5u2AeAZ+clniQdSE30VEgpC/BEZAbqThQ2AghoDBymsIuQDNLRca4A0XOrqZ25R14B64XVDFuhNlbAAAAAElFTkSuQmCC" rel="icon" type="image/png">
|
<link href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABqklEQVRoQ+2XOy8FURSF720oJRK3U3lfohYahULjF6CU3Erjf0hoJH6BUqeWeIRGId5RoVMpaH1bLpkIkz17ztw5wz7JSiaZvfesddY+j6nXKj7qFedfcwFlO+gO/AcHbhE5pBR6R9ywMvYjrBMt5ALSHHEHFP3qLVR2C90kdpZTnt9SCD3wblnh6ldIp9fACF+Wlgo2XIBiKpOL2B34PmHeQt5Cihko+xzwRewO5GxTbyFvob/cQnKSL4J50ATjoAucgE2wA64S12n5YZcf92Ajz1WiBxa7YDaFzTXvGqC3HRONgG4Iyc/JZMapjEbANsRXMpKX8Ciu0xMQOTeQl5QoHNiAyKpRQBQOHEN+qsoCLiAvW6ZlDJJ0b0n8LceyjR5QbMZIQrbeF2Puj2kWAVtUahlIPJLTb8hLTbEIkFN3z0BknZw1Q15wAVLwEExnICNtMwCeM+SoQi0OSOE+sA/GFF95JWYOyO4VfFgFCBHNXeiJuAVwFpx5u2AeAZ+clniQdSE30VEgpC/BEZAbqThQ2AghoDBymsIuQDNLRca4A0XOrqZ25R14B64XVDFuhNlbAAAAAElFTkSuQmCC" rel="icon" type="image/png">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans;
|
font-family: sans;
|
||||||
background-color: #112;
|
background-color: #112;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div {
|
body > div {
|
||||||
|
@ -24,6 +29,9 @@
|
||||||
|
|
||||||
.browser {
|
.browser {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -43,13 +51,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.queue li {
|
ul.queue li {
|
||||||
padding: 1.0rem 0.75rem;
|
padding: 0 0.5rem;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.queue li.playing {
|
ul.queue li.playing {
|
||||||
background-color: #334;
|
background-color: #334;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.breadcrumb {
|
ul.breadcrumb {
|
||||||
|
@ -70,6 +79,11 @@
|
||||||
content: "/";
|
content: "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.dir {
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
ul.dir li {
|
ul.dir li {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: .75rem .75rem;
|
padding: .75rem .75rem;
|
||||||
|
@ -82,7 +96,7 @@
|
||||||
ul.dir li img {
|
ul.dir li img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.dir li:hover {
|
ul.dir li:hover {
|
||||||
|
@ -98,6 +112,24 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.albumart {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue .albumart {
|
||||||
|
margin: 0.75rem;
|
||||||
|
margin-left: 0;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.albumart img {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.player {
|
.player {
|
||||||
width: 25rem;
|
width: 25rem;
|
||||||
}
|
}
|
||||||
|
@ -129,78 +161,36 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .albumart {
|
.nowplaying .albumart {
|
||||||
|
margin-right: 1.0rem;
|
||||||
|
background-color: #445;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
object-fit: contain;
|
|
||||||
display: block;
|
|
||||||
margin-right: 1.0rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .metadata {
|
.nowplaying .metadata {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .idle {
|
.nowplaying .idle {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body hx-ext="sse" sse-connect="/sse">
|
||||||
<div class="browser">
|
<div class="browser" hx-trigger="sse:database" hx-get="/browser">
|
||||||
<ul class="breadcrumb">
|
{% include "browser.html" %}
|
||||||
<li><a href="?path=">Root</a></li>
|
|
||||||
{% for (i, component) in path.iter().enumerate() %}
|
|
||||||
<li>
|
|
||||||
{% if i == path.len() - 1 %}
|
|
||||||
{{ component }}
|
|
||||||
{% else %}
|
|
||||||
<a href="?path={{ path[..i + 1].join("/") }}">
|
|
||||||
{{ component }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul class="dir">
|
|
||||||
{% for entry in entries %}
|
|
||||||
{% match entry %}
|
|
||||||
{% when mpd::Entry::Song with { name, path, artist } %}
|
|
||||||
<li
|
|
||||||
onclick="(new Image()).src = '/art?path={{path}}'"
|
|
||||||
hx-post="/queue?path={{path}}" hx-swap="none" role="button"
|
|
||||||
>
|
|
||||||
<span class="material-symbols-outlined">music_note</span>
|
|
||||||
<!-- img src="/art?path={{path}}" -->
|
|
||||||
<div class="song">
|
|
||||||
<div class="song__name">{{ name }}</div>
|
|
||||||
<div class="song__artist">{{ artist }}</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% when mpd::Entry::Directory with { name, path }%}
|
|
||||||
<li onclick="window.location = '?path={{path}}'">
|
|
||||||
<span class="material-symbols-outlined">folder</span>
|
|
||||||
<a href="?path={{path}}">{{ name }}</a>
|
|
||||||
</li>
|
|
||||||
{% when mpd::Entry::Playlist with { name, path } %}
|
|
||||||
<li hx-post="/queue?path={{path}}" hx-swap="none" role="button" >
|
|
||||||
<span class="material-symbols-outlined">playlist_play</span>
|
|
||||||
<div class="song">
|
|
||||||
<div class="song__name">{{ name }}</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endmatch %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="player" hx-ext="sse" sse-connect="/sse">
|
<div class="player">
|
||||||
<div hx-trigger="sse:player" hx-get="/player"></div>
|
<div hx-trigger="sse:player" hx-get="/player">
|
||||||
<div hx-trigger="sse:playlist,sse:player" hx-get="/queue"></div>
|
{% include "player.html" %}
|
||||||
|
</div>
|
||||||
|
<div hx-trigger="sse:playlist,sse:player" hx-get="/queue">
|
||||||
|
{% include "queue.html" %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,12 +3,10 @@
|
||||||
<div class="nowplaying">
|
<div class="nowplaying">
|
||||||
<div class="current">
|
<div class="current">
|
||||||
{% if let Some(song) = song %}
|
{% if let Some(song) = song %}
|
||||||
<img
|
<div class="albumart">
|
||||||
class="albumart"
|
<img src="/art?path={{ song.file }}" onerror="this.style.opacity = 0">
|
||||||
src="/art?path={{ song.file }}"
|
</div>
|
||||||
onerror="this.style.opacity = 0"
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
{% if let Some(name) = name %}
|
{% if let Some(name) = name %}
|
||||||
<div class="song__name">{{ name }}</div>
|
<div class="song__name">{{ name }}</div>
|
||||||
|
|
|
@ -3,7 +3,15 @@
|
||||||
<ul class="queue">
|
<ul class="queue">
|
||||||
{% for item in queue %}
|
{% for item in queue %}
|
||||||
<li {% if item.playing %}class="playing"{% endif %}>
|
<li {% if item.playing %}class="playing"{% endif %}>
|
||||||
<input type="checkbox" /> {{ item.title }}
|
<div class="albumart">
|
||||||
|
<img src="/art?path={{ item.file }}">
|
||||||
|
</div>
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="song__name">{{ item.title }}</div>
|
||||||
|
{% if let Some(artist) = item.artist %}
|
||||||
|
<div class="song__artist">{{ artist }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
Loading…
Reference in a new issue