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,
|
||||
}
|
||||
|
||||
async fn index(req: tide::Request<()>) -> tide::Result {
|
||||
async fn get_index(req: tide::Request<()>) -> tide::Result {
|
||||
let query: IndexQuery = req.query()?;
|
||||
let entries = mpd::ls(&query.path)?;
|
||||
let queue = mpd::playlist()?;
|
||||
|
@ -94,6 +94,28 @@ async fn get_player(_req: tide::Request<()>) -> tide::Result {
|
|||
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)]
|
||||
struct PostQueueQuery {
|
||||
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<()> {
|
||||
// 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());
|
||||
|
||||
// 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?;
|
||||
|
||||
loop {
|
||||
stream.write_all(b"idle playlist player\n").await?;
|
||||
stream.write_all(b"idle playlist player database\n").await?;
|
||||
|
||||
loop {
|
||||
buffer.clear();
|
||||
|
@ -179,10 +201,11 @@ async fn main() -> tide::Result<()> {
|
|||
let mut app = tide::new();
|
||||
app.with(tide_tracing::TraceMiddleware::new());
|
||||
|
||||
app.at("/").get(index);
|
||||
app.at("/").get(get_index);
|
||||
app.at("/queue").get(get_queue);
|
||||
app.at("/player").get(get_player);
|
||||
app.at("/art").get(get_art);
|
||||
app.at("/browser").get(get_browser);
|
||||
|
||||
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;
|
||||
|
||||
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> {
|
||||
mpdrs::Client::connect(HOST)
|
||||
mpdrs::Client::connect(host())
|
||||
}
|
||||
|
||||
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) file: String,
|
||||
pub(crate) title: String,
|
||||
pub(crate) artist: Option<String>,
|
||||
pub(crate) playing: bool,
|
||||
}
|
||||
|
||||
|
@ -54,7 +60,9 @@ pub(crate) fn playlist() -> anyhow::Result<Vec<QueueItem>> {
|
|||
.queue()?
|
||||
.into_iter()
|
||||
.map(|song| QueueItem {
|
||||
file: song.file.clone(),
|
||||
title: song.title.as_ref().unwrap_or(&song.file).clone(),
|
||||
artist: song.artist.clone(),
|
||||
playing: current == song.place,
|
||||
})
|
||||
.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">
|
||||
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans;
|
||||
background-color: #112;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body > div {
|
||||
|
@ -24,6 +29,9 @@
|
|||
|
||||
.browser {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -43,13 +51,14 @@
|
|||
}
|
||||
|
||||
ul.queue li {
|
||||
padding: 1.0rem 0.75rem;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: .25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul.queue li.playing {
|
||||
background-color: #334;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.breadcrumb {
|
||||
|
@ -70,6 +79,11 @@
|
|||
content: "/";
|
||||
}
|
||||
|
||||
ul.dir {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ul.dir li {
|
||||
cursor: pointer;
|
||||
padding: .75rem .75rem;
|
||||
|
@ -82,7 +96,7 @@
|
|||
ul.dir li img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
ul.dir li:hover {
|
||||
|
@ -98,6 +112,24 @@
|
|||
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 {
|
||||
width: 25rem;
|
||||
}
|
||||
|
@ -129,78 +161,36 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.player .albumart {
|
||||
.nowplaying .albumart {
|
||||
margin-right: 1.0rem;
|
||||
background-color: #445;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
margin-right: 1.0rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.player .metadata {
|
||||
.nowplaying .metadata {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.player .idle {
|
||||
.nowplaying .idle {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="browser">
|
||||
<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">
|
||||
{% 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>
|
||||
<body hx-ext="sse" sse-connect="/sse">
|
||||
<div class="browser" hx-trigger="sse:database" hx-get="/browser">
|
||||
{% include "browser.html" %}
|
||||
</div>
|
||||
|
||||
<div class="player" hx-ext="sse" sse-connect="/sse">
|
||||
<div hx-trigger="sse:player" hx-get="/player"></div>
|
||||
<div hx-trigger="sse:playlist,sse:player" hx-get="/queue"></div>
|
||||
<div class="player">
|
||||
<div hx-trigger="sse:player" hx-get="/player">
|
||||
{% include "player.html" %}
|
||||
</div>
|
||||
<div hx-trigger="sse:playlist,sse:player" hx-get="/queue">
|
||||
{% include "queue.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
<div class="nowplaying">
|
||||
<div class="current">
|
||||
{% if let Some(song) = song %}
|
||||
<img
|
||||
class="albumart"
|
||||
src="/art?path={{ song.file }}"
|
||||
onerror="this.style.opacity = 0"
|
||||
>
|
||||
|
||||
<div class="albumart">
|
||||
<img src="/art?path={{ song.file }}" onerror="this.style.opacity = 0">
|
||||
</div>
|
||||
|
||||
<div class="metadata">
|
||||
{% if let Some(name) = name %}
|
||||
<div class="song__name">{{ name }}</div>
|
||||
|
|
|
@ -3,7 +3,15 @@
|
|||
<ul class="queue">
|
||||
{% for item in queue %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
Loading…
Reference in a new issue