mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-12-14 11:56:16 +01:00
init
This commit is contained in:
1
musicdb-server/.gitignore
vendored
Executable file
1
musicdb-server/.gitignore
vendored
Executable file
@@ -0,0 +1 @@
|
||||
/target
|
||||
19
musicdb-server/Cargo.toml
Executable file
19
musicdb-server/Cargo.toml
Executable file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "musicdb-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.19", features = ["headers"] }
|
||||
futures = "0.3.28"
|
||||
headers = "0.3.8"
|
||||
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio-stream = "0.1.14"
|
||||
tower = { version = "0.4", features = ["util"] }
|
||||
tower-http = { version = "0.4.0", features = ["fs", "trace"] }
|
||||
trace = "0.1.7"
|
||||
3
musicdb-server/assets/album-view.html
Normal file
3
musicdb-server/assets/album-view.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h3>\:name</h3>
|
||||
<button hx-post="/queue/add-album/\:id" hx-swap="none">Queue</button>
|
||||
\:songs
|
||||
1
musicdb-server/assets/albums_one.html
Normal file
1
musicdb-server/assets/albums_one.html
Normal file
@@ -0,0 +1 @@
|
||||
<button hx-get="/album-view/\:id" hx-target="#album-view">\:name</button>
|
||||
2
musicdb-server/assets/artist-view.html
Normal file
2
musicdb-server/assets/artist-view.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<h3>\:name</h3>
|
||||
\:albums
|
||||
2
musicdb-server/assets/artists.html
Normal file
2
musicdb-server/assets/artists.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<h3>Artists</h3>
|
||||
\:artists
|
||||
1
musicdb-server/assets/artists_one.html
Normal file
1
musicdb-server/assets/artists_one.html
Normal file
@@ -0,0 +1 @@
|
||||
<button hx-get="/artist-view/\:id" hx-target="#artist-view">\:name</button>
|
||||
3
musicdb-server/assets/queue.html
Normal file
3
musicdb-server/assets/queue.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<h2>Queue</h2>
|
||||
<div>Now Playing: <b>\:currentTitle</b></div>
|
||||
\:content
|
||||
10
musicdb-server/assets/queue_folder.html
Normal file
10
musicdb-server/assets/queue_folder.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<small>>></small>
|
||||
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||
<small>\:name</small>
|
||||
</div>
|
||||
\:content
|
||||
<div>
|
||||
<small><<</small>
|
||||
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||
</div>
|
||||
10
musicdb-server/assets/queue_folder_current.html
Normal file
10
musicdb-server/assets/queue_folder_current.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<small>>></small>
|
||||
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||
<small><b>\:name</b></small>
|
||||
</div>
|
||||
\:content
|
||||
<div>
|
||||
<small><<</small>
|
||||
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||
</div>
|
||||
5
musicdb-server/assets/queue_song.html
Normal file
5
musicdb-server/assets/queue_song.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||
\:title
|
||||
</div>
|
||||
5
musicdb-server/assets/queue_song_current.html
Normal file
5
musicdb-server/assets/queue_song_current.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<div>
|
||||
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||
<b>\:title</b>
|
||||
</div>
|
||||
24
musicdb-server/assets/root.html
Normal file
24
musicdb-server/assets/root.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.3"></script>
|
||||
<title>MusicDb</title>
|
||||
</head>
|
||||
<body>
|
||||
<div hx-sse="connect:/sse">
|
||||
<div hx-sse="swap:playing">(loading)</div>
|
||||
<button hx-post="/resume" hx-swap="none">⏵</button>
|
||||
<button hx-post="/pause" hx-swap="none">⏸</button>
|
||||
<button hx-post="/stop" hx-swap="none">⏹</button>
|
||||
<button hx-post="/next" hx-swap="none">⏭</button>
|
||||
<button hx-post="/queue/clear" hx-swap="none">-</button>
|
||||
<div hx-sse="swap:queue">(loading)</div>
|
||||
<div hx-sse="swap:artists">(loading)</div>
|
||||
<div id="artist-view"></div>
|
||||
<div id="album-view"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1
musicdb-server/assets/songs_one.html
Normal file
1
musicdb-server/assets/songs_one.html
Normal file
@@ -0,0 +1 @@
|
||||
<button hx-post="/queue/add-song/\:id" hx-swap="none">\:title</button>
|
||||
184
musicdb-server/src/main.rs
Executable file
184
musicdb-server/src/main.rs
Executable file
@@ -0,0 +1,184 @@
|
||||
mod web;
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
use musicdb_lib::server::{run_server, Command};
|
||||
|
||||
use musicdb_lib::data::database::Database;
|
||||
|
||||
/*
|
||||
|
||||
# Exit codes
|
||||
|
||||
0 => exited as requested by the user
|
||||
1 => exit after printing help message
|
||||
3 => error parsing cli arguments
|
||||
10 => tried to start with a path that caused some io::Error
|
||||
11 => tried to start with a path that does not exist (--init prevents this)
|
||||
|
||||
*/
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let mut tcp_addr = None;
|
||||
let mut web_addr = None;
|
||||
let mut lib_dir_for_init = None;
|
||||
let database = if let Some(path_s) = args.next() {
|
||||
loop {
|
||||
if let Some(arg) = args.next() {
|
||||
if arg.starts_with("--") {
|
||||
match &arg[2..] {
|
||||
"init" => {
|
||||
if let Some(lib_dir) = args.next() {
|
||||
lib_dir_for_init = Some(lib_dir);
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
missing argument: --init <lib path>"
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
"tcp" => {
|
||||
if let Some(addr) = args.next() {
|
||||
if let Ok(addr) = addr.parse() {
|
||||
tcp_addr = Some(addr)
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
bad argument: --tcp <addr:port>: couldn't parse <addr:port>"
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
missing argument: --tcp <addr:port>"
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
"web" => {
|
||||
if let Some(addr) = args.next() {
|
||||
if let Ok(addr) = addr.parse() {
|
||||
web_addr = Some(addr)
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
bad argument: --web <addr:port>: couldn't parse <addr:port>"
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
missing argument: --web <addr:port>"
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
o => {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
Unknown long argument --{o}"
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
} else if arg.starts_with("-") {
|
||||
match &arg[1..] {
|
||||
o => {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
Unknown short argument -{o}"
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
Argument didn't start with - or -- ({arg})."
|
||||
);
|
||||
exit(3);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let path = PathBuf::from(&path_s);
|
||||
match path.try_exists() {
|
||||
Ok(exists) => {
|
||||
if let Some(lib_directory) = lib_dir_for_init {
|
||||
Database::new_empty(path, lib_directory.into())
|
||||
} else if exists {
|
||||
Database::load_database(path).unwrap()
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
The provided path does not exist."
|
||||
);
|
||||
exit(11);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
Error getting information about the provided path '{path_s}': {e}"
|
||||
);
|
||||
exit(10);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"[EXIT]
|
||||
musicdb - help
|
||||
musicdb <path to database file> <options> <options> <...>
|
||||
options:
|
||||
--init <lib directory>
|
||||
--tcp <addr:port>
|
||||
--web <addr:port>
|
||||
this help was shown because no arguments were provided."
|
||||
);
|
||||
exit(1);
|
||||
};
|
||||
// database.add_song_new(Song::new(
|
||||
// "Amaranthe/Manifest/02 Make It Better.mp3".into(),
|
||||
// "Make It Better".to_owned(),
|
||||
// None,
|
||||
// None,
|
||||
// vec![],
|
||||
// None,
|
||||
// ));
|
||||
// let mut player = Player::new();
|
||||
// eprintln!("[info] database.songs: {:?}", database.songs());
|
||||
// database.save_database(Some("/tmp/dbfile".into())).unwrap();
|
||||
// eprintln!("{}", database.get_song(&0).unwrap());
|
||||
// database.queue.add_to_end(QueueContent::Song(1).into());
|
||||
// player.update_and_restart_playing_song(&database);
|
||||
let database = Arc::new(Mutex::new(database));
|
||||
if tcp_addr.is_some() || web_addr.is_some() {
|
||||
if let Some(addr) = web_addr {
|
||||
let (s, mut r) = tokio::sync::mpsc::channel(2);
|
||||
let db = Arc::clone(&database);
|
||||
thread::spawn(move || run_server(database, tcp_addr, Some(s)));
|
||||
if let Some(sender) = r.recv().await {
|
||||
web::main(db, sender, addr).await;
|
||||
}
|
||||
} else {
|
||||
run_server(database, tcp_addr, None);
|
||||
}
|
||||
} else {
|
||||
eprintln!("nothing to do, not starting the server.");
|
||||
}
|
||||
// std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||
// dbg!(Update::from_bytes(&mut BufReader::new(
|
||||
// TcpStream::connect("127.0.0.1:26314".parse::<SocketAddr>().unwrap()).unwrap()
|
||||
// )));
|
||||
}
|
||||
573
musicdb-server/src/web.rs
Normal file
573
musicdb-server/src/web.rs
Normal file
@@ -0,0 +1,573 @@
|
||||
use std::convert::Infallible;
|
||||
use std::mem;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::task::Poll;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::sse::Event;
|
||||
use axum::response::{Html, Sse};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Router, TypedHeader};
|
||||
use futures::{stream, Stream};
|
||||
use musicdb_lib::data::database::{Database, UpdateEndpoint};
|
||||
use musicdb_lib::data::queue::{Queue, QueueContent};
|
||||
use musicdb_lib::server::Command;
|
||||
use tokio_stream::StreamExt as _;
|
||||
|
||||
/*
|
||||
|
||||
23E9 ⏩︎ fast forward
|
||||
23EA ⏪︎ rewind, fast backwards
|
||||
23EB ⏫︎ fast increase
|
||||
23EC ⏬︎ fast decrease
|
||||
23ED ⏭︎ skip to end, next
|
||||
23EE ⏮︎ skip to start, previous
|
||||
23EF ⏯︎ play/pause toggle
|
||||
23F1 ⏱︎ stopwatch
|
||||
23F2 ⏲︎ timer clock
|
||||
23F3 ⏳︎ hourglass
|
||||
23F4 ⏴︎ reverse, back
|
||||
23F5 ⏵︎ forward, next, play
|
||||
23F6 ⏶︎ increase
|
||||
23F7 ⏷︎ decrease
|
||||
23F8 ⏸︎ pause
|
||||
23F9 ⏹︎ stop
|
||||
23FA ⏺︎ record
|
||||
|
||||
*/
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
db: Arc<Mutex<Database>>,
|
||||
html: Arc<AppHtml>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct AppHtml {
|
||||
/// /
|
||||
/// can use:
|
||||
root: Vec<HtmlPart>,
|
||||
|
||||
/// sse:artists
|
||||
/// can use: artists (0+ repeats of artists_one)
|
||||
artists: Vec<HtmlPart>,
|
||||
/// can use: id, name
|
||||
artists_one: Vec<HtmlPart>,
|
||||
|
||||
/// /artist-view/:artist-id
|
||||
/// can use: albums (0+ repeats of albums_one)
|
||||
artist_view: Vec<HtmlPart>,
|
||||
/// can use: name
|
||||
albums_one: Vec<HtmlPart>,
|
||||
|
||||
/// /album-view/:album-id
|
||||
/// can use: id, name, songs (0+ repeats of songs_one)
|
||||
album_view: Vec<HtmlPart>,
|
||||
/// can use: title
|
||||
songs_one: Vec<HtmlPart>,
|
||||
|
||||
/// /queue
|
||||
/// can use: currentTitle, nextTitle, content
|
||||
queue: Vec<HtmlPart>,
|
||||
/// can use: path, title
|
||||
queue_song: Vec<HtmlPart>,
|
||||
/// can use: path, title
|
||||
queue_song_current: Vec<HtmlPart>,
|
||||
/// can use: path, content, name
|
||||
queue_folder: Vec<HtmlPart>,
|
||||
/// can use: path, content, name
|
||||
queue_folder_current: Vec<HtmlPart>,
|
||||
}
|
||||
impl AppHtml {
|
||||
pub fn from_dir<P: AsRef<std::path::Path>>(dir: P) -> std::io::Result<Self> {
|
||||
let dir = dir.as_ref();
|
||||
Ok(Self {
|
||||
root: Self::parse(&std::fs::read_to_string(dir.join("root.html"))?),
|
||||
artists: Self::parse(&std::fs::read_to_string(dir.join("artists.html"))?),
|
||||
artists_one: Self::parse(&std::fs::read_to_string(dir.join("artists_one.html"))?),
|
||||
artist_view: Self::parse(&std::fs::read_to_string(dir.join("artist-view.html"))?),
|
||||
albums_one: Self::parse(&std::fs::read_to_string(dir.join("albums_one.html"))?),
|
||||
album_view: Self::parse(&std::fs::read_to_string(dir.join("album-view.html"))?),
|
||||
songs_one: Self::parse(&std::fs::read_to_string(dir.join("songs_one.html"))?),
|
||||
queue: Self::parse(&std::fs::read_to_string(dir.join("queue.html"))?),
|
||||
queue_song: Self::parse(&std::fs::read_to_string(dir.join("queue_song.html"))?),
|
||||
queue_song_current: Self::parse(&std::fs::read_to_string(
|
||||
dir.join("queue_song_current.html"),
|
||||
)?),
|
||||
queue_folder: Self::parse(&std::fs::read_to_string(dir.join("queue_folder.html"))?),
|
||||
queue_folder_current: Self::parse(&std::fs::read_to_string(
|
||||
dir.join("queue_folder_current.html"),
|
||||
)?),
|
||||
})
|
||||
}
|
||||
pub fn parse(s: &str) -> Vec<HtmlPart> {
|
||||
let mut o = Vec::new();
|
||||
let mut c = String::new();
|
||||
let mut chars = s.chars().peekable();
|
||||
loop {
|
||||
if let Some(ch) = chars.next() {
|
||||
if ch == '\\' && chars.peek().is_some_and(|ch| *ch == ':') {
|
||||
chars.next();
|
||||
o.push(HtmlPart::Plain(mem::replace(&mut c, String::new())));
|
||||
loop {
|
||||
if let Some(ch) = chars.peek() {
|
||||
if !ch.is_ascii_alphabetic() {
|
||||
o.push(HtmlPart::Insert(mem::replace(&mut c, String::new())));
|
||||
break;
|
||||
} else {
|
||||
c.push(*ch);
|
||||
chars.next();
|
||||
}
|
||||
} else {
|
||||
if c.len() > 0 {
|
||||
o.push(HtmlPart::Insert(c));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.push(ch);
|
||||
}
|
||||
} else {
|
||||
if c.len() > 0 {
|
||||
o.push(HtmlPart::Plain(c));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum HtmlPart {
|
||||
/// text as plain html
|
||||
Plain(String),
|
||||
/// insert some value depending on context and key
|
||||
Insert(String),
|
||||
}
|
||||
|
||||
pub async fn main(db: Arc<Mutex<Database>>, sender: mpsc::Sender<Command>, addr: SocketAddr) {
|
||||
let db1 = Arc::clone(&db);
|
||||
let state = AppState {
|
||||
db,
|
||||
html: Arc::new(AppHtml::from_dir("assets").unwrap()),
|
||||
};
|
||||
let (s1, s2, s3, s4, s5, s6, s7, s8, s9) = (
|
||||
sender.clone(),
|
||||
sender.clone(),
|
||||
sender.clone(),
|
||||
sender.clone(),
|
||||
sender.clone(),
|
||||
sender.clone(),
|
||||
sender.clone(),
|
||||
sender.clone(),
|
||||
sender,
|
||||
);
|
||||
let state1 = state.clone();
|
||||
|
||||
let app = Router::new()
|
||||
// root
|
||||
.nest_service(
|
||||
"/",
|
||||
get(move || async move {
|
||||
Html(
|
||||
state1
|
||||
.html
|
||||
.root
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
HtmlPart::Plain(v) => v,
|
||||
HtmlPart::Insert(_) => "",
|
||||
})
|
||||
.collect::<String>(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
// server-sent events
|
||||
.route("/sse", get(sse_handler))
|
||||
// inner views (embedded in root)
|
||||
.route("/artist-view/:artist-id", get(artist_view_handler))
|
||||
.route("/album-view/:album-id", get(album_view_handler))
|
||||
// handle POST requests via the mpsc::Sender instead of locking the db.
|
||||
.route(
|
||||
"/pause",
|
||||
post(move || async move {
|
||||
_ = s1.send(Command::Pause);
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/resume",
|
||||
post(move || async move {
|
||||
_ = s2.send(Command::Resume);
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/stop",
|
||||
post(move || async move {
|
||||
_ = s3.send(Command::Stop);
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/next",
|
||||
post(move || async move {
|
||||
_ = s4.send(Command::NextSong);
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/queue/clear",
|
||||
post(move || async move {
|
||||
_ = s5.send(Command::QueueUpdate(
|
||||
vec![],
|
||||
QueueContent::Folder(0, vec![], String::new()).into(),
|
||||
));
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/queue/remove/:i",
|
||||
post(move |Path(i): Path<String>| async move {
|
||||
let mut ids = vec![];
|
||||
for id in i.split('-') {
|
||||
if let Ok(n) = id.parse() {
|
||||
ids.push(n);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ = s8.send(Command::QueueRemove(ids));
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/queue/goto/:i",
|
||||
post(move |Path(i): Path<String>| async move {
|
||||
let mut ids = vec![];
|
||||
for id in i.split('-') {
|
||||
if let Ok(n) = id.parse() {
|
||||
ids.push(n);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ = s9.send(Command::QueueGoto(ids));
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/queue/add-song/:song-id",
|
||||
post(move |Path(song_id)| async move {
|
||||
_ = s6.send(Command::QueueAdd(
|
||||
vec![],
|
||||
QueueContent::Song(song_id).into(),
|
||||
));
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/queue/add-album/:album-id",
|
||||
post(move |Path(album_id)| async move {
|
||||
if let Some(album) = db1.lock().unwrap().albums().get(&album_id) {
|
||||
_ = s7.send(Command::QueueAdd(
|
||||
vec![],
|
||||
QueueContent::Folder(
|
||||
0,
|
||||
album
|
||||
.songs
|
||||
.iter()
|
||||
.map(|id| QueueContent::Song(*id).into())
|
||||
.collect(),
|
||||
album.name.clone(),
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}),
|
||||
)
|
||||
.with_state(state);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn sse_handler(
|
||||
TypedHeader(user_agent): TypedHeader<headers::UserAgent>,
|
||||
State(state): State<AppState>,
|
||||
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
||||
println!("`{}` connected", user_agent.as_str());
|
||||
|
||||
let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut db = state.db.lock().unwrap();
|
||||
_ = sender.send(Arc::new(Command::SyncDatabase(vec![], vec![], vec![])));
|
||||
_ = sender.send(Arc::new(Command::NextSong));
|
||||
_ = sender.send(Arc::new(if db.playing {
|
||||
Command::Resume
|
||||
} else {
|
||||
Command::Pause
|
||||
}));
|
||||
db.update_endpoints
|
||||
.push(UpdateEndpoint::CmdChannelTokio(sender));
|
||||
drop(db);
|
||||
|
||||
let stream = stream::poll_fn(move |_ctx| {
|
||||
if let Ok(cmd) = receiver.try_recv() {
|
||||
Poll::Ready(Some(match cmd.as_ref() {
|
||||
Command::Resume => Event::default().event("playing").data("playing"),
|
||||
Command::Pause => Event::default().event("playing").data("paused"),
|
||||
Command::Stop => Event::default().event("playing").data("stopped"),
|
||||
Command::SyncDatabase(..)
|
||||
| Command::ModifySong(..)
|
||||
| Command::ModifyAlbum(..)
|
||||
| Command::ModifyArtist(..)
|
||||
| Command::AddSong(..)
|
||||
| Command::AddAlbum(..)
|
||||
| Command::AddArtist(..) => Event::default().event("artists").data({
|
||||
let db = state.db.lock().unwrap();
|
||||
let mut a = db.artists().iter().collect::<Vec<_>>();
|
||||
a.sort_unstable_by_key(|(_id, artist)| &artist.name);
|
||||
let mut artists = String::new();
|
||||
for (id, artist) in a {
|
||||
for v in &state.html.artists_one {
|
||||
match v {
|
||||
HtmlPart::Plain(v) => artists.push_str(v),
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"id" => artists.push_str(&id.to_string()),
|
||||
"name" => artists.push_str(&artist.name),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
state
|
||||
.html
|
||||
.artists
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
HtmlPart::Plain(v) => v,
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"artists" => &artists,
|
||||
_ => "",
|
||||
},
|
||||
})
|
||||
.collect::<String>()
|
||||
}),
|
||||
Command::NextSong
|
||||
| Command::QueueUpdate(..)
|
||||
| Command::QueueAdd(..)
|
||||
| Command::QueueInsert(..)
|
||||
| Command::QueueRemove(..)
|
||||
| Command::QueueGoto(..) => {
|
||||
let db = state.db.lock().unwrap();
|
||||
let current = db
|
||||
.queue
|
||||
.get_current_song()
|
||||
.map_or(None, |id| db.songs().get(id));
|
||||
let next = db
|
||||
.queue
|
||||
.get_next_song()
|
||||
.map_or(None, |id| db.songs().get(id));
|
||||
let mut content = String::new();
|
||||
build_queue_content_build(
|
||||
&db,
|
||||
&state,
|
||||
&mut content,
|
||||
&db.queue,
|
||||
String::new(),
|
||||
true,
|
||||
);
|
||||
Event::default().event("queue").data(
|
||||
state
|
||||
.html
|
||||
.queue
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
HtmlPart::Plain(v) => v,
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"currentTitle" => {
|
||||
if let Some(s) = current {
|
||||
&s.title
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
"nextTitle" => {
|
||||
if let Some(s) = next {
|
||||
&s.title
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
"content" => &content,
|
||||
_ => "",
|
||||
},
|
||||
})
|
||||
.collect::<String>(),
|
||||
)
|
||||
}
|
||||
Command::Save | Command::SetLibraryDirectory(_) => return Poll::Pending,
|
||||
}))
|
||||
} else {
|
||||
return Poll::Pending;
|
||||
}
|
||||
})
|
||||
.map(Ok);
|
||||
// .throttle(Duration::from_millis(100));
|
||||
|
||||
Sse::new(stream)
|
||||
.keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_millis(250)))
|
||||
}
|
||||
|
||||
async fn artist_view_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(artist_id): Path<u64>,
|
||||
) -> Html<String> {
|
||||
let db = state.db.lock().unwrap();
|
||||
if let Some(artist) = db.artists().get(&artist_id) {
|
||||
let mut albums = String::new();
|
||||
for id in artist.albums.iter() {
|
||||
if let Some(album) = db.albums().get(id) {
|
||||
for v in &state.html.albums_one {
|
||||
match v {
|
||||
HtmlPart::Plain(v) => albums.push_str(v),
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"id" => albums.push_str(&id.to_string()),
|
||||
"name" => albums.push_str(&album.name),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = artist_id.to_string();
|
||||
Html(
|
||||
state
|
||||
.html
|
||||
.artist_view
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
HtmlPart::Plain(v) => v,
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"id" => &id,
|
||||
"name" => &artist.name,
|
||||
"albums" => &albums,
|
||||
_ => "",
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
Html(format!(
|
||||
"<h1>Bad ID</h1><p>There is no artist with the id {artist_id} in the database</p>"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn album_view_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(album_id): Path<u64>,
|
||||
) -> Html<String> {
|
||||
let db = state.db.lock().unwrap();
|
||||
if let Some(album) = db.albums().get(&album_id) {
|
||||
let mut songs = String::new();
|
||||
for id in album.songs.iter() {
|
||||
if let Some(song) = db.songs().get(id) {
|
||||
for v in &state.html.songs_one {
|
||||
match v {
|
||||
HtmlPart::Plain(v) => songs.push_str(v),
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"id" => songs.push_str(&id.to_string()),
|
||||
"title" => songs.push_str(&song.title),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let id = album_id.to_string();
|
||||
Html(
|
||||
state
|
||||
.html
|
||||
.album_view
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
HtmlPart::Plain(v) => v,
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"id" => &id,
|
||||
"name" => &album.name,
|
||||
"songs" => &songs,
|
||||
_ => "",
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
Html(format!(
|
||||
"<h1>Bad ID</h1><p>There is no album with the id {album_id} in the database</p>"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_queue_content_build(
|
||||
db: &Database,
|
||||
state: &AppState,
|
||||
html: &mut String,
|
||||
queue: &Queue,
|
||||
path: String,
|
||||
current: bool,
|
||||
) {
|
||||
// TODO: Do something for disabled ones too (they shouldn't just be hidden)
|
||||
if queue.enabled() {
|
||||
match queue.content() {
|
||||
QueueContent::Song(id) => {
|
||||
if let Some(song) = db.songs().get(id) {
|
||||
for v in if current {
|
||||
&state.html.queue_song_current
|
||||
} else {
|
||||
&state.html.queue_song
|
||||
} {
|
||||
match v {
|
||||
HtmlPart::Plain(v) => html.push_str(v),
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"path" => html.push_str(&path),
|
||||
"title" => html.push_str(&song.title),
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
QueueContent::Folder(ci, c, name) => {
|
||||
if path.is_empty() {
|
||||
for (i, c) in c.iter().enumerate() {
|
||||
let current = current && *ci == i;
|
||||
build_queue_content_build(db, state, html, c, i.to_string(), current)
|
||||
}
|
||||
} else {
|
||||
for v in if current {
|
||||
&state.html.queue_folder_current
|
||||
} else {
|
||||
&state.html.queue_folder
|
||||
} {
|
||||
match v {
|
||||
HtmlPart::Plain(v) => html.push_str(v),
|
||||
HtmlPart::Insert(key) => match key.as_str() {
|
||||
"path" => html.push_str(&path),
|
||||
"name" => html.push_str(name),
|
||||
"content" => {
|
||||
for (i, c) in c.iter().enumerate() {
|
||||
let current = current && *ci == i;
|
||||
build_queue_content_build(
|
||||
db,
|
||||
state,
|
||||
html,
|
||||
c,
|
||||
format!("{path}-{i}"),
|
||||
current,
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user