mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-09-13 07:06:14 +02:00
944 lines
33 KiB
Rust
Executable File
944 lines
33 KiB
Rust
Executable File
use std::net::SocketAddr;
|
|
use std::sync::{mpsc, Arc, Mutex};
|
|
|
|
use musicdb_lib::data::album::Album;
|
|
use musicdb_lib::data::artist::Artist;
|
|
use musicdb_lib::data::database::{Database, UpdateEndpoint};
|
|
use musicdb_lib::data::queue::{Queue, QueueContent, QueueFolder};
|
|
use musicdb_lib::data::song::Song;
|
|
use musicdb_lib::data::SongId;
|
|
use musicdb_lib::server::{Action, Command, Req};
|
|
use rocket::futures::{SinkExt, StreamExt};
|
|
use rocket::response::content::RawHtml;
|
|
use rocket::{get, routes, Config, State};
|
|
use rocket_seek_stream::SeekStream;
|
|
use rocket_ws::{Message, WebSocket};
|
|
use tokio::select;
|
|
use tokio::sync::mpsc::Sender;
|
|
|
|
/*
|
|
|
|
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
|
|
|
|
*/
|
|
|
|
const HTML_START: &'static str =
|
|
"<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"color-scheme\" content=\"light dark\">";
|
|
const HTML_SEP: &'static str = "</head><body>";
|
|
const HTML_END: &'static str = "</body></html>";
|
|
|
|
struct Data {
|
|
db: Arc<Mutex<Database>>,
|
|
command_sender: mpsc::Sender<(Command, Option<u64>)>,
|
|
websocket_connections: Arc<tokio::sync::Mutex<Vec<Sender<Message>>>>,
|
|
}
|
|
|
|
#[get("/")]
|
|
fn index(data: &State<Data>) -> RawHtml<String> {
|
|
let script = r#"<script>
|
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
async function performSearch() {
|
|
var searchResultDiv = document.getElementById("searchResultDiv");
|
|
searchResultDiv.innerHTML = "Loading...";
|
|
var sfArtist = document.getElementById("searchFieldArtist").value;
|
|
var sfAlbum = document.getElementById("searchFieldAlbum").value;
|
|
var sfTitle = document.getElementById("searchFieldTitle").value;
|
|
var query = "";
|
|
if (sfArtist) {
|
|
query += "artist=" + encodeURIComponent(sfArtist);
|
|
}
|
|
if (sfAlbum) {
|
|
if (query) {
|
|
query += "&";
|
|
}
|
|
query += "album=" + encodeURIComponent(sfAlbum);
|
|
}
|
|
if (sfTitle) {
|
|
if (query) {
|
|
query += "&";
|
|
}
|
|
query += "title=" + encodeURIComponent(sfTitle);
|
|
}
|
|
if (query || confirm("You didn't search for anything specific. If you continue, the whole library will be loaded, which can take a while, use a lot of bandwidth, and may crash your browser!")) {
|
|
console.log("Performing search with query '" + query + "'.");
|
|
var r1 = await fetch("/search?" + query);
|
|
var r2 = await r1.text();
|
|
searchResultDiv.innerHTML = r2;
|
|
} else {
|
|
searchResultDiv.innerHTML = "";
|
|
}
|
|
}
|
|
async function addSong(id) {
|
|
await fetch("/add-song/" + id);
|
|
}
|
|
</script>
|
|
"#;
|
|
let script2 = r#"<script>
|
|
const searchDiv = document.getElementById("searchDiv");
|
|
searchDiv.style.display = "";
|
|
document.getElementById("warnLag").innerText = "connecting...";
|
|
const nowPlayingDiv = document.getElementById("nowPlayingDiv");
|
|
const queueDiv = document.getElementById("queueDiv");
|
|
var didFinish = false;
|
|
var averageLoopTimeMs = 250;
|
|
var dbPlaying = null;
|
|
function showDbPlaying() {
|
|
if (dbPlaying === true) {
|
|
document.getElementById("playLiveCurrent").play();
|
|
} else if (dbPlaying === false) {
|
|
document.getElementById("playLiveCurrent").pause();
|
|
} else {
|
|
document.getElementById("playLiveCurrent").pause();
|
|
}
|
|
const postheader = document.getElementById("postheader");
|
|
if (postheader) {
|
|
if (dbPlaying === true) {
|
|
postheader.innerText = "";
|
|
} else if (dbPlaying === false) {
|
|
postheader.innerText = "(paused)";
|
|
} else {
|
|
postheader.innerText = "";
|
|
}
|
|
}
|
|
}
|
|
async function updateNowPlaying() {
|
|
nowPlayingDiv.innerHTML = await (await fetch("/now-playing-html")).text();
|
|
showDbPlaying();
|
|
}
|
|
async function updateQueue() {
|
|
queueDiv.innerHTML = await (await fetch("/queue-html")).text();
|
|
}
|
|
var livePlaybackCurrentId = null;
|
|
var livePlaybackNextId = null;
|
|
async function updateLivePlaybackIds() {
|
|
if (document.getElementById("playLiveEnabled").checked) {
|
|
let resp = (await (await fetch("/now-playing-ids")).text()).trim();
|
|
let current = null;
|
|
let next = null;
|
|
if (resp != "") {
|
|
if (resp.includes("/")) {
|
|
let [c, n] = resp.split("/");
|
|
current = c.trim();
|
|
next = n.trim();
|
|
} else {
|
|
current = resp;
|
|
}
|
|
}
|
|
if (current !== livePlaybackCurrentId) {
|
|
livePlaybackCurrentId = current;
|
|
document.getElementById("playLiveCurrentSrc").src = livePlaybackCurrentId == null ? "" : "/song/" + livePlaybackCurrentId + "/current";
|
|
let audioElem = document.getElementById("playLiveCurrent");
|
|
audioElem.pause();
|
|
audioElem.currentTime = 0;
|
|
if (dbPlaying) {
|
|
audioElem.setAttribute("autoplay", "");
|
|
} else {
|
|
audioElem.removeAttribute("autoplay");
|
|
}
|
|
audioElem.load();
|
|
if (dbPlaying) {
|
|
audioElem.play();
|
|
}
|
|
}
|
|
if (next !== livePlaybackNextId) {
|
|
livePlaybackNextId = next;
|
|
document.getElementById("playLiveNextSrc").src = livePlaybackNextId == null ? "" : "/song/" + livePlaybackNextId + "/next";
|
|
document.getElementById("playLiveNext").load();
|
|
}
|
|
} else {
|
|
if (livePlaybackCurrentId !== null) {
|
|
livePlaybackCurrentId = null;
|
|
let audioElem = document.getElementById("playLiveCurrent");
|
|
audioElem.pause();
|
|
audioElem.currentTime = 0;
|
|
document.getElementById("playLiveCurrentSrc").src = "";
|
|
audioElem.load();
|
|
}
|
|
if (livePlaybackNextId !== null) {
|
|
livePlaybackNextId = null;
|
|
document.getElementById("playLiveNextSrc").src = "";
|
|
document.getElementById("playLiveNext").load();
|
|
}
|
|
}
|
|
}
|
|
async function runLoop() {
|
|
let websocketConnection = null;
|
|
try {
|
|
websocketConnection = new WebSocket("/ws");
|
|
var websocketCounter = 0;
|
|
websocketConnection.addEventListener("message", async function(e) {
|
|
++websocketCounter;
|
|
if (websocketCounter > 2) {
|
|
websocketCounter = 0;
|
|
}
|
|
document.getElementById("warnLag").innerText = "using websocket" + (websocketCounter == 0 ? "." : (websocketCounter == 1 ? ".." : "..."));
|
|
switch (e.data.trim()) {
|
|
case "init/playing=true":
|
|
if (dbPlaying === null) {
|
|
dbPlaying = true;
|
|
showDbPlaying();
|
|
}
|
|
break;
|
|
case "init/playing=false":
|
|
if (dbPlaying === null) {
|
|
dbPlaying = false;
|
|
showDbPlaying();
|
|
}
|
|
break;
|
|
case "pause":
|
|
dbPlaying = false;
|
|
showDbPlaying();
|
|
break;
|
|
case "stop":
|
|
dbPlaying = false;
|
|
document.getElementById("playLiveCurrent").pause();
|
|
document.getElementById("playLiveCurrent").currentTime = 0;
|
|
showDbPlaying();
|
|
break;
|
|
case "resume":
|
|
dbPlaying = true;
|
|
showDbPlaying();
|
|
break;
|
|
case "next":
|
|
await updateLivePlaybackIds();
|
|
await updateNowPlaying();
|
|
await updateQueue();
|
|
break;
|
|
case "update/data":
|
|
await updateLivePlaybackIds();
|
|
await updateNowPlaying();
|
|
await updateQueue();
|
|
break;
|
|
case "update/queue":
|
|
await updateLivePlaybackIds();
|
|
await updateNowPlaying();
|
|
await updateQueue();
|
|
break;
|
|
default:
|
|
console.log("Unknown websocket message: ", e.data.trim());
|
|
break;
|
|
}
|
|
});
|
|
return;
|
|
} catch (e) {
|
|
console.log("Error in websocket connection:");
|
|
console.log(e);
|
|
console.log("Falling back to polling.");
|
|
websocketConnection = null;
|
|
}
|
|
while (true) {
|
|
await sleep(1000);
|
|
didFinish = false;
|
|
var startTime = new Date();
|
|
sleep(averageLoopTimeMs*2).then(async function() {
|
|
while (!didFinish) {
|
|
var elapsed = new Date() - startTime;
|
|
document.getElementById("warnLag").innerText = "Warning: slow connection, server may be busy. be patient. (" + Math.round(averageLoopTimeMs) + "ms exceeded by " + Math.round((elapsed-averageLoopTimeMs)/averageLoopTimeMs) + "x)";
|
|
await sleep(100);
|
|
}
|
|
});
|
|
await updateNowPlaying();
|
|
await updateQueue();
|
|
var elapsedTime = new Date() - startTime;
|
|
didFinish = true;
|
|
averageLoopTimeMs = ((averageLoopTimeMs * 4) + elapsedTime) / 5;
|
|
document.getElementById("warnLag").innerText = "Average update time: " + Math.round(averageLoopTimeMs) + "ms";
|
|
}
|
|
}
|
|
updateLivePlaybackIds();
|
|
runLoop();
|
|
</script>
|
|
"#;
|
|
let buttons = "<button onclick=\"fetch('/play')\">play</button><button onclick=\"fetch('/pause')\">pause</button><button onclick=\"fetch('/stop')\">stop</button><button onclick=\"fetch('/skip')\">skip</button><button onclick=\"fetch('/clear-queue')\">clear queue</button>";
|
|
let search = "<input id=\"searchFieldArtist\" placeholder=\"artist\"><input id=\"searchFieldAlbum\" placeholder=\"album\"><input id=\"searchFieldTitle\" placeholder=\"title\">
|
|
<button onclick=\"performSearch()\">search</button><div id=\"searchResultDiv\"></div>";
|
|
let playback_live = r#"<div><input id="playLiveEnabled" onchange="updateLivePlaybackIds();" type="checkbox"><audio controls autoplay id="playLiveCurrent"><source id="playLiveCurrentSrc" src=""></audio><audio style="visibility:hidden;" id="playLiveNext"><source id="playLiveNextSrc" src=""></audio></span></div>"#;
|
|
let db = data.db.lock().unwrap();
|
|
let now_playing = gen_now_playing(&db);
|
|
let mut queue = String::new();
|
|
gen_queue_html(&db.queue, &mut queue, &db);
|
|
drop(db);
|
|
RawHtml(format!(
|
|
"{HTML_START}<title>MusicDb</title>{script}{HTML_SEP}<small><small><div id=\"warnLag\">no javascript? reload to see updated information.</div></small></small><div id=\"nowPlayingDiv\">{now_playing}</div><div>{buttons}</div>{playback_live}<div id=\"searchDiv\" style=\"display:none;\">{search}</div><div id=\"queueDiv\">{queue}</div>{script2}{HTML_END}",
|
|
))
|
|
}
|
|
#[get("/now-playing-html")]
|
|
fn now_playing_html(data: &State<Data>) -> RawHtml<String> {
|
|
RawHtml(gen_now_playing(&*data.db.lock().unwrap()))
|
|
}
|
|
#[get("/now-playing-ids")]
|
|
fn now_playing_ids(data: &State<Data>) -> String {
|
|
let db = data.db.lock().unwrap();
|
|
let (c, n) = (
|
|
db.queue.get_current_song().copied(),
|
|
db.queue.get_next_song().copied(),
|
|
);
|
|
drop(db);
|
|
if let Some(c) = c {
|
|
if let Some(n) = n {
|
|
format!("{c}/{n}")
|
|
} else {
|
|
format!("{c}")
|
|
}
|
|
} else {
|
|
"".to_owned()
|
|
}
|
|
}
|
|
|
|
#[get("/song/<id>")]
|
|
fn song1(data: &State<Data>, id: SongId) -> Option<SeekStream<'_>> {
|
|
song(data, id)
|
|
}
|
|
#[get("/song/<id>/<_>")]
|
|
fn song2(data: &State<Data>, id: SongId) -> Option<SeekStream<'_>> {
|
|
song(data, id)
|
|
}
|
|
fn song(data: &State<Data>, id: SongId) -> Option<SeekStream<'_>> {
|
|
let db = data.db.lock().unwrap();
|
|
if let Some(song) = db.get_song(&id) {
|
|
song.cached_data().cache_data_start_thread(&*db, song);
|
|
if let Some(bytes) = song.cached_data().cached_data_await() {
|
|
drop(db);
|
|
Some(SeekStream::new(std::io::Cursor::new(ArcBytes(bytes))))
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
struct ArcBytes(pub Arc<Vec<u8>>);
|
|
impl AsRef<[u8]> for ArcBytes {
|
|
fn as_ref(&self) -> &[u8] {
|
|
self.0.as_ref()
|
|
}
|
|
}
|
|
|
|
#[get("/queue-html")]
|
|
fn queue_html(data: &State<Data>) -> RawHtml<String> {
|
|
let mut str = String::new();
|
|
let db = data.db.lock().unwrap();
|
|
gen_queue_html(&db.queue, &mut str, &db);
|
|
RawHtml(str)
|
|
}
|
|
fn gen_now_playing(db: &Database) -> String {
|
|
if let Some(current_song) = db.queue.get_current_song().and_then(|id| db.get_song(id)) {
|
|
format!(
|
|
"<h1>Now Playing <small id=\"postheader\"></small></h1><h4>{}</h4>",
|
|
html_escape::encode_safe(¤t_song.title),
|
|
)
|
|
} else {
|
|
format!("<h1>Now Playing</h1><p>nothing</p>",)
|
|
}
|
|
}
|
|
fn gen_queue_html(queue: &Queue, str: &mut String, db: &Database) {
|
|
gen_queue_html_impl(queue, str, db, true, &mut "".to_owned());
|
|
}
|
|
fn gen_queue_html_impl(
|
|
queue: &Queue,
|
|
str: &mut String,
|
|
db: &Database,
|
|
active_highlight: bool,
|
|
path: &mut String,
|
|
) {
|
|
match queue.content() {
|
|
QueueContent::Song(id) => {
|
|
if let Some(song) = db.songs().get(id) {
|
|
str.push_str("<div>");
|
|
str.push_str(&format!("<button onclick=\"fetch('/queue-goto/{path}')\">"));
|
|
if active_highlight {
|
|
str.push_str("<b>");
|
|
}
|
|
str.push_str(&html_escape::encode_text(&song.title));
|
|
if active_highlight {
|
|
str.push_str("</b>");
|
|
}
|
|
str.push_str("</button>");
|
|
str.push_str("<small>");
|
|
if let Some(artist) = db.artists().get(&song.artist) {
|
|
str.push_str(" by ");
|
|
str.push_str(&html_escape::encode_text(&artist.name));
|
|
}
|
|
if let Some(album) = song.album.as_ref().and_then(|id| db.albums().get(id)) {
|
|
str.push_str(" on ");
|
|
str.push_str(&html_escape::encode_text(&album.name));
|
|
}
|
|
str.push_str(&format!(
|
|
"<button onclick=\"fetch('/queue-remove/{path}')\">rm</button>"
|
|
));
|
|
str.push_str("</small></div>");
|
|
} else {
|
|
str.push_str("<div><small>unknown song</small></div>");
|
|
}
|
|
}
|
|
QueueContent::Folder(f) => {
|
|
let html_shuf: &'static str = " <small><small>shuffled</small></small>";
|
|
if f.content.is_empty() {
|
|
str.push_str("[0/0] ");
|
|
if active_highlight {
|
|
str.push_str("<b>");
|
|
}
|
|
str.push_str(&html_escape::encode_text(&f.name));
|
|
if active_highlight {
|
|
str.push_str("</b>");
|
|
}
|
|
if f.order.is_some() {
|
|
str.push_str(html_shuf);
|
|
}
|
|
} else {
|
|
str.push_str(&format!("[{}/{}] ", f.index + 1, f.content.len(),));
|
|
if active_highlight {
|
|
str.push_str("<b>");
|
|
}
|
|
str.push_str(&html_escape::encode_text(&f.name));
|
|
if active_highlight {
|
|
str.push_str("</b>");
|
|
}
|
|
if f.order.is_some() {
|
|
str.push_str(html_shuf);
|
|
}
|
|
str.push_str("<ol>");
|
|
for (i, v) in f.iter().enumerate() {
|
|
str.push_str("<li>");
|
|
if !path.is_empty() {
|
|
path.push('_');
|
|
}
|
|
path.push_str(&format!("{i}"));
|
|
gen_queue_html_impl(v, str, db, active_highlight && i == f.index, path);
|
|
while !(path.is_empty() || path.ends_with('_')) {
|
|
path.pop();
|
|
}
|
|
path.pop();
|
|
str.push_str("</li>");
|
|
}
|
|
str.push_str("</ol>");
|
|
}
|
|
}
|
|
QueueContent::Loop(d, t, i) => {
|
|
if active_highlight {
|
|
str.push_str("<b>");
|
|
}
|
|
if *t == 0 {
|
|
str.push_str(&format!("<small>[{}/∞]</small>", d + 1));
|
|
} else {
|
|
str.push_str(&format!("<small>[{}/{}]</small>", d + 1, t));
|
|
}
|
|
if active_highlight {
|
|
str.push_str("</b>");
|
|
}
|
|
if !path.is_empty() {
|
|
path.push('_');
|
|
}
|
|
path.push('0');
|
|
gen_queue_html_impl(i, str, db, active_highlight, path);
|
|
while !(path.is_empty() || path.ends_with('_')) {
|
|
path.pop();
|
|
}
|
|
path.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[get("/queue-remove/<path>")]
|
|
fn queue_remove(data: &State<Data>, path: &str) {
|
|
if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() {
|
|
data.command_sender
|
|
.send((Action::QueueRemove(path).cmd(0xFFu8), None))
|
|
.unwrap();
|
|
}
|
|
}
|
|
#[get("/queue-goto/<path>")]
|
|
fn queue_goto(data: &State<Data>, path: &str) {
|
|
if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() {
|
|
data.command_sender
|
|
.send((Action::QueueGoto(path).cmd(0xFFu8), None))
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[get("/play")]
|
|
fn play(data: &State<Data>) {
|
|
data.command_sender
|
|
.send((Action::Resume.cmd(0xFFu8), None))
|
|
.unwrap();
|
|
}
|
|
#[get("/pause")]
|
|
fn pause(data: &State<Data>) {
|
|
data.command_sender
|
|
.send((Action::Pause.cmd(0xFFu8), None))
|
|
.unwrap();
|
|
}
|
|
#[get("/stop")]
|
|
fn stop(data: &State<Data>) {
|
|
data.command_sender
|
|
.send((Action::Stop.cmd(0xFFu8), None))
|
|
.unwrap();
|
|
}
|
|
#[get("/skip")]
|
|
fn skip(data: &State<Data>) {
|
|
data.command_sender
|
|
.send((Action::NextSong.cmd(0xFFu8), None))
|
|
.unwrap();
|
|
}
|
|
#[get("/clear-queue")]
|
|
fn clear_queue(data: &State<Data>) {
|
|
data.command_sender
|
|
.send((
|
|
Action::QueueUpdate(
|
|
vec![],
|
|
QueueContent::Folder(QueueFolder {
|
|
index: 0,
|
|
content: vec![],
|
|
name: String::new(),
|
|
order: None,
|
|
})
|
|
.into(),
|
|
Req::none(),
|
|
)
|
|
.cmd(0xFFu8),
|
|
None,
|
|
))
|
|
.unwrap();
|
|
}
|
|
|
|
#[get("/add-song/<id>")]
|
|
fn add_song(data: &State<Data>, id: SongId) {
|
|
data.command_sender
|
|
.send((
|
|
Action::QueueAdd(vec![], vec![QueueContent::Song(id).into()], Req::none()).cmd(0xFFu8),
|
|
None,
|
|
))
|
|
.unwrap();
|
|
}
|
|
|
|
#[get("/search?<artist>&<album>&<title>&<artist_tags>&<album_tags>&<song_tags>")]
|
|
fn search(
|
|
data: &State<Data>,
|
|
artist: Option<&str>,
|
|
album: Option<&str>,
|
|
title: Option<&str>,
|
|
artist_tags: Vec<&str>,
|
|
album_tags: Vec<&str>,
|
|
song_tags: Vec<&str>,
|
|
) -> RawHtml<String> {
|
|
let db = data.db.lock().unwrap();
|
|
let mut out = String::new();
|
|
let artist = artist.map(|v| v.to_lowercase());
|
|
let artist = artist.as_ref().map(|v| v.as_str());
|
|
let album = album.map(|v| v.to_lowercase());
|
|
let album = album.as_ref().map(|v| v.as_str());
|
|
let title = title.map(|v| v.to_lowercase());
|
|
let title = title.as_ref().map(|v| v.as_str());
|
|
find1(
|
|
&*db,
|
|
artist,
|
|
album,
|
|
title,
|
|
&artist_tags,
|
|
&album_tags,
|
|
&song_tags,
|
|
&mut out,
|
|
);
|
|
fn find1(
|
|
db: &Database,
|
|
artist: Option<&str>,
|
|
album: Option<&str>,
|
|
title: Option<&str>,
|
|
artist_tags: &[&str],
|
|
album_tags: &[&str],
|
|
song_tags: &[&str],
|
|
out: &mut String,
|
|
) {
|
|
if let Some(f) = artist {
|
|
find2(
|
|
db,
|
|
db.artists()
|
|
.values()
|
|
.filter(|v| v.name.to_lowercase().contains(f)),
|
|
album,
|
|
title,
|
|
artist_tags,
|
|
album_tags,
|
|
song_tags,
|
|
out,
|
|
)
|
|
} else {
|
|
find2(
|
|
db,
|
|
db.artists().values(),
|
|
album,
|
|
title,
|
|
artist_tags,
|
|
album_tags,
|
|
song_tags,
|
|
out,
|
|
)
|
|
}
|
|
}
|
|
fn find2<'a>(
|
|
db: &'a Database,
|
|
artists: impl IntoIterator<Item = &'a Artist>,
|
|
album: Option<&str>,
|
|
title: Option<&str>,
|
|
artist_tags: &[&str],
|
|
album_tags: &[&str],
|
|
song_tags: &[&str],
|
|
out: &mut String,
|
|
) {
|
|
for artist in artists {
|
|
if artist_tags
|
|
.iter()
|
|
.all(|t| artist.general.tags.iter().any(|v| v == t))
|
|
{
|
|
let mut func_artist = Some(|out: &mut String| {
|
|
out.push_str("<h3>");
|
|
out.push_str(&artist.name);
|
|
out.push_str("</h3>");
|
|
});
|
|
let mut func_album = None;
|
|
if false {
|
|
// so they have the same type
|
|
std::mem::swap(&mut func_artist, &mut func_album);
|
|
}
|
|
if album.is_none() && album_tags.is_empty() {
|
|
find4(
|
|
db,
|
|
artist.singles.iter().filter_map(|v| db.get_song(v)),
|
|
title,
|
|
song_tags,
|
|
out,
|
|
&mut func_artist,
|
|
&mut func_album,
|
|
);
|
|
}
|
|
let iter = artist.albums.iter().filter_map(|v| db.albums().get(v));
|
|
if let Some(f) = album {
|
|
find3(
|
|
db,
|
|
iter.filter(|v| v.name.to_lowercase().contains(f)),
|
|
title,
|
|
album_tags,
|
|
song_tags,
|
|
out,
|
|
&mut func_artist,
|
|
)
|
|
} else {
|
|
find3(
|
|
db,
|
|
iter,
|
|
title,
|
|
album_tags,
|
|
song_tags,
|
|
out,
|
|
&mut func_artist,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn find3<'a>(
|
|
db: &'a Database,
|
|
albums: impl IntoIterator<Item = &'a Album>,
|
|
title: Option<&str>,
|
|
album_tags: &[&str],
|
|
song_tags: &[&str],
|
|
out: &mut String,
|
|
func_artist: &mut Option<impl FnOnce(&'_ mut String)>,
|
|
) {
|
|
for album in albums {
|
|
if album_tags
|
|
.iter()
|
|
.all(|t| album.general.tags.iter().any(|v| v == t))
|
|
{
|
|
let mut func_album = Some(|out: &mut String| {
|
|
out.push_str("<h4>");
|
|
out.push_str(&album.name);
|
|
out.push_str("</h4>");
|
|
});
|
|
find4(
|
|
db,
|
|
album.songs.iter().filter_map(|v| db.get_song(v)),
|
|
title,
|
|
song_tags,
|
|
out,
|
|
func_artist,
|
|
&mut func_album,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
fn find4<'a>(
|
|
db: &'a Database,
|
|
songs: impl IntoIterator<Item = &'a Song>,
|
|
title: Option<&str>,
|
|
song_tags: &[&str],
|
|
out: &mut String,
|
|
func_artist: &mut Option<impl FnOnce(&'_ mut String)>,
|
|
func_album: &mut Option<impl FnOnce(&'_ mut String)>,
|
|
) {
|
|
if let Some(f) = title {
|
|
find5(
|
|
db,
|
|
songs
|
|
.into_iter()
|
|
.filter(|v| v.title.to_lowercase().contains(f)),
|
|
song_tags,
|
|
out,
|
|
func_artist,
|
|
func_album,
|
|
)
|
|
} else {
|
|
find5(db, songs, song_tags, out, func_artist, func_album)
|
|
}
|
|
}
|
|
fn find5<'a>(
|
|
db: &'a Database,
|
|
songs: impl IntoIterator<Item = &'a Song>,
|
|
song_tags: &[&str],
|
|
out: &mut String,
|
|
func_artist: &mut Option<impl FnOnce(&'_ mut String)>,
|
|
func_album: &mut Option<impl FnOnce(&'_ mut String)>,
|
|
) {
|
|
for song in songs {
|
|
if song_tags
|
|
.iter()
|
|
.all(|t| song.general.tags.iter().any(|v| v == t))
|
|
{
|
|
find6(db, song, out, func_artist, func_album)
|
|
}
|
|
}
|
|
}
|
|
fn find6<'a>(
|
|
_db: &Database,
|
|
song: &Song,
|
|
out: &mut String,
|
|
func_artist: &mut Option<impl FnOnce(&'_ mut String)>,
|
|
func_album: &mut Option<impl FnOnce(&'_ mut String)>,
|
|
) {
|
|
if let Some(f) = func_artist.take() {
|
|
f(out)
|
|
}
|
|
if let Some(f) = func_album.take() {
|
|
f(out)
|
|
}
|
|
out.push_str("<button onclick=\"addSong('");
|
|
out.push_str(&format!("{}", song.id));
|
|
out.push_str("')\">");
|
|
out.push_str(&song.title);
|
|
out.push_str("</button><br>");
|
|
}
|
|
RawHtml(out)
|
|
}
|
|
|
|
#[get("/ws")]
|
|
async fn websocket(websocket: WebSocket, state: &State<Data>) -> rocket_ws::Channel<'static> {
|
|
// a channel so other threads/tasks can send messages to this websocket client
|
|
let (sender, mut receiver) = tokio::sync::mpsc::channel(5);
|
|
state.websocket_connections.lock().await.push(sender);
|
|
let (db_playing, ()) = tokio::task::block_in_place(|| {
|
|
let db = state.db.lock().unwrap();
|
|
(db.playing, ())
|
|
});
|
|
|
|
// handle messages from the websocket and from the channel
|
|
websocket.channel(move |mut websocket| {
|
|
Box::pin(async move {
|
|
if db_playing {
|
|
let _ = websocket.send(Message::text("init/playing=true")).await;
|
|
} else {
|
|
let _ = websocket.send(Message::text("init/playing=false")).await;
|
|
}
|
|
loop {
|
|
// async magic:
|
|
// handle a message from the websocket client or from other
|
|
// threads/tasks in the server, whichever happens first
|
|
select! {
|
|
message = websocket.next() => {
|
|
if let Some(message) = message {
|
|
// server received `message` from the websocket client
|
|
match message? {
|
|
Message::Text(text) => {
|
|
// it was a text message, prefix it with "You sent: " and echo
|
|
websocket
|
|
.send(Message::text(format!("You sent: {text}")))
|
|
.await?
|
|
}
|
|
Message::Binary(_bytes) => {
|
|
// it was a binary message, ignore it
|
|
}
|
|
Message::Ping(payload) => {
|
|
websocket.send(Message::Pong(payload)).await?
|
|
}
|
|
Message::Close(close) => {
|
|
websocket.close(close).await?;
|
|
break;
|
|
}
|
|
// these messages get ignored
|
|
Message::Pong(_) | Message::Frame(_) => (),
|
|
}
|
|
} else {
|
|
// websocket connection was closed
|
|
break;
|
|
}
|
|
},
|
|
message_to_be_sent = receiver.recv() => {
|
|
if let Some(message) = message_to_be_sent {
|
|
// server received `message` from another thread/task
|
|
websocket.send(message).await?;
|
|
} else {
|
|
// channel has been closed, close websocket connection too
|
|
websocket.close(None).await?;
|
|
break;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
Ok(())
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn main(
|
|
db: Arc<Mutex<Database>>,
|
|
command_sender: mpsc::Sender<(Command, Option<u64>)>,
|
|
addr: SocketAddr,
|
|
) {
|
|
let websocket_connections = Arc::new(tokio::sync::Mutex::new(vec![]));
|
|
let data = Data {
|
|
db: Arc::clone(&db),
|
|
command_sender,
|
|
websocket_connections: Arc::clone(&websocket_connections),
|
|
};
|
|
let mut db = db.lock().unwrap();
|
|
let udepid = db.update_endpoints_id;
|
|
db.update_endpoints_id += 1;
|
|
db.update_endpoints.push((
|
|
udepid,
|
|
UpdateEndpoint::Custom(Box::new(move |cmd| {
|
|
let mut msgs = vec![];
|
|
fn action(a: &Action, msgs: &mut Vec<Message>) {
|
|
match a {
|
|
Action::Resume => msgs.push(Message::text("resume")),
|
|
Action::Pause => msgs.push(Message::text("pause")),
|
|
Action::Stop => msgs.push(Message::text("stop")),
|
|
Action::NextSong => msgs.push(Message::text("next")),
|
|
Action::SyncDatabase(..)
|
|
| Action::AddSong(..)
|
|
| Action::AddAlbum(..)
|
|
| Action::AddArtist(..)
|
|
| Action::AddCover(..)
|
|
| Action::ModifySong(..)
|
|
| Action::ModifyAlbum(..)
|
|
| Action::ModifyArtist(..)
|
|
| Action::RemoveSong(..)
|
|
| Action::RemoveAlbum(..)
|
|
| Action::RemoveArtist(..)
|
|
| Action::SetSongDuration(..)
|
|
| Action::TagSongFlagSet(..)
|
|
| Action::TagSongFlagUnset(..)
|
|
| Action::TagAlbumFlagSet(..)
|
|
| Action::TagAlbumFlagUnset(..)
|
|
| Action::TagArtistFlagSet(..)
|
|
| Action::TagArtistFlagUnset(..)
|
|
| Action::TagSongPropertySet(..)
|
|
| Action::TagSongPropertyUnset(..)
|
|
| Action::TagAlbumPropertySet(..)
|
|
| Action::TagAlbumPropertyUnset(..)
|
|
| Action::TagArtistPropertySet(..)
|
|
| Action::TagArtistPropertyUnset(..) => msgs.push(Message::text("update/data")),
|
|
Action::QueueUpdate(..)
|
|
| Action::QueueAdd(..)
|
|
| Action::QueueInsert(..)
|
|
| Action::QueueRemove(..)
|
|
| Action::QueueMove(..)
|
|
| Action::QueueMoveInto(..)
|
|
| Action::QueueGoto(..)
|
|
| Action::QueueShuffle(..)
|
|
| Action::QueueSetShuffle(..)
|
|
| Action::QueueUnshuffle(..) => msgs.push(Message::text("update/queue")),
|
|
Action::Multiple(actions) => {
|
|
for inner in actions {
|
|
action(inner, msgs);
|
|
}
|
|
}
|
|
Action::InitComplete
|
|
| Action::Save
|
|
| Action::ErrorInfo(..)
|
|
| Action::Denied(..) => {}
|
|
}
|
|
}
|
|
action(&cmd.action, &mut msgs);
|
|
if !msgs.is_empty() {
|
|
let mut ws_cons = websocket_connections.blocking_lock();
|
|
let mut rm = vec![];
|
|
for msg in msgs {
|
|
rm.clear();
|
|
for (i, con) in ws_cons.iter_mut().enumerate() {
|
|
if con.blocking_send(msg.clone()).is_err() {
|
|
rm.push(i);
|
|
}
|
|
}
|
|
for i in rm.iter().rev() {
|
|
ws_cons.remove(*i);
|
|
}
|
|
}
|
|
}
|
|
})),
|
|
));
|
|
drop(db);
|
|
tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async_main(data, addr));
|
|
}
|
|
async fn async_main(data: Data, addr: SocketAddr) {
|
|
rocket::build()
|
|
.configure(Config {
|
|
address: addr.ip(),
|
|
port: addr.port(),
|
|
..Default::default()
|
|
})
|
|
.manage(data)
|
|
.mount(
|
|
"/",
|
|
routes![
|
|
index,
|
|
websocket,
|
|
play,
|
|
pause,
|
|
stop,
|
|
skip,
|
|
clear_queue,
|
|
queue_goto,
|
|
queue_remove,
|
|
add_song,
|
|
search,
|
|
now_playing_html,
|
|
now_playing_ids,
|
|
song1,
|
|
song2,
|
|
queue_html,
|
|
],
|
|
)
|
|
.launch()
|
|
.await
|
|
.unwrap();
|
|
}
|