From f1dd980b5296b5131ee747d3745471a34bc32730 Mon Sep 17 00:00:00 2001 From: Mark <> Date: Fri, 28 Feb 2025 20:38:30 +0100 Subject: [PATCH] improve website, add "sleep" playback backend --- musicdb-lib/Cargo.toml | 1 + musicdb-lib/src/data/song.rs | 2 +- musicdb-lib/src/player/mod.rs | 11 +- musicdb-lib/src/player/playback_rs.rs | 3 +- musicdb-lib/src/player/rodio.rs | 3 +- musicdb-lib/src/player/sleep.rs | 146 ++++++++++ musicdb-lib/src/server/mod.rs | 4 + musicdb-server/Cargo.toml | 3 + musicdb-server/src/main.rs | 6 +- musicdb-server/src/web.rs | 373 +++++++++++++++++++++++++- 10 files changed, 534 insertions(+), 18 deletions(-) create mode 100644 musicdb-lib/src/player/sleep.rs diff --git a/musicdb-lib/Cargo.toml b/musicdb-lib/Cargo.toml index 4b4e251..9684195 100644 --- a/musicdb-lib/Cargo.toml +++ b/musicdb-lib/Cargo.toml @@ -17,5 +17,6 @@ default = [] playback = [] default-playback = ["playback-via-playback-rs"] # default-playback = ["playback-via-rodio"] +playback-via-sleep = ["playback"] playback-via-playback-rs = ["playback", "dep:playback-rs"] playback-via-rodio = ["playback", "dep:rodio"] diff --git a/musicdb-lib/src/data/song.rs b/musicdb-lib/src/data/song.rs index 6198ad3..d04561e 100755 --- a/musicdb-lib/src/data/song.rs +++ b/musicdb-lib/src/data/song.rs @@ -186,7 +186,7 @@ impl CachedData { } /// Gets the cached data, if available. /// If a thread is running to load the data, it is awaited. - /// This function doesn't block. + /// This function blocks! pub fn cached_data_await(&self) -> Option>> { let mut cd = self.0.lock().unwrap(); let (out, next) = match replace(&mut cd.0, Err(None)) { diff --git a/musicdb-lib/src/player/mod.rs b/musicdb-lib/src/player/mod.rs index 502ad53..1051669 100755 --- a/musicdb-lib/src/player/mod.rs +++ b/musicdb-lib/src/player/mod.rs @@ -2,6 +2,8 @@ pub mod playback_rs; #[cfg(feature = "playback-via-rodio")] pub mod rodio; +#[cfg(feature = "playback-via-sleep")] +pub mod sleep; #[cfg(feature = "playback-via-playback-rs")] pub type PlayerBackendFeat = playback_rs::PlayerBackendPlaybackRs; #[cfg(feature = "playback-via-rodio")] @@ -10,7 +12,11 @@ pub type PlayerBackendFeat = rodio::PlayerBackendRodio; use std::{collections::HashMap, ffi::OsStr, sync::Arc}; use crate::{ - data::{database::Database, song::CachedData, SongId}, + data::{ + database::Database, + song::{CachedData, Song}, + SongId, + }, server::Action, }; @@ -28,6 +34,7 @@ pub trait PlayerBackend { fn load_next_song( &mut self, id: SongId, + song: &Song, filename: &OsStr, bytes: Arc>, load_duration: bool, @@ -158,6 +165,7 @@ impl> Player { let load_duration = song.duration_millis == 0; self.backend.load_next_song( id, + song, song.location .rel_path .file_name() @@ -211,6 +219,7 @@ impl> Player { let load_duration = song.duration_millis == 0; self.backend.load_next_song( id, + song, song.location .rel_path .file_name() diff --git a/musicdb-lib/src/player/playback_rs.rs b/musicdb-lib/src/player/playback_rs.rs index 9f38a89..b22da4c 100644 --- a/musicdb-lib/src/player/playback_rs.rs +++ b/musicdb-lib/src/player/playback_rs.rs @@ -3,7 +3,7 @@ use std::{ffi::OsStr, io::Cursor, path::Path, sync::Arc, time::Duration}; use playback_rs::Hint; use crate::{ - data::SongId, + data::{song::Song, SongId}, server::{Action, Command}, }; @@ -41,6 +41,7 @@ impl PlayerBackend for PlayerBackendPlaybackRs { fn load_next_song( &mut self, id: SongId, + _song: &Song, filename: &OsStr, bytes: Arc>, _load_duration: bool, diff --git a/musicdb-lib/src/player/rodio.rs b/musicdb-lib/src/player/rodio.rs index a5b7fe6..b06bce2 100644 --- a/musicdb-lib/src/player/rodio.rs +++ b/musicdb-lib/src/player/rodio.rs @@ -4,7 +4,7 @@ use rc_u8_reader::ArcU8Reader; use rodio::{decoder::DecoderError, Decoder, OutputStream, OutputStreamHandle, Sink, Source}; use crate::{ - data::SongId, + data::{song::Song, SongId}, server::{Action, Command}, }; @@ -52,6 +52,7 @@ impl PlayerBackend for PlayerBackendRodio { fn load_next_song( &mut self, id: SongId, + _song: &Song, _filename: &OsStr, bytes: Arc>, _load_duration: bool, diff --git a/musicdb-lib/src/player/sleep.rs b/musicdb-lib/src/player/sleep.rs new file mode 100644 index 0000000..6e2ddcd --- /dev/null +++ b/musicdb-lib/src/player/sleep.rs @@ -0,0 +1,146 @@ +use std::{ + ffi::OsStr, + sync::Arc, + time::{Duration, Instant}, +}; + +use crate::{ + data::{song::Song, SongId}, + server::Command, +}; + +use super::PlayerBackend; + +pub struct PlayerBackendSleep { + current: Option<(SongId, u64, T)>, + next: Option<(SongId, u64, T)>, + /// unused, but could be used to do something smarter than polling at some point + #[allow(unused)] + command_sender: Option)>>, + finished: SongFinished, +} +#[derive(Debug)] +enum SongFinished { + Never, + In(Duration), + At(Instant), +} + +impl PlayerBackendSleep { + pub fn new( + command_sender: Option)>>, + ) -> Result> { + Ok(Self { + current: None, + next: None, + command_sender, + finished: SongFinished::Never, + }) + } + fn set_finished(&mut self, play: bool) { + self.finished = if let Some((_, duration, _)) = &self.current { + let duration = Duration::from_millis(*duration); + if play { + SongFinished::At(Instant::now() + duration) + } else { + SongFinished::In(duration) + } + } else { + SongFinished::Never + }; + } +} + +impl PlayerBackend for PlayerBackendSleep { + fn load_next_song( + &mut self, + id: SongId, + song: &Song, + _filename: &OsStr, + _bytes: Arc>, + _load_duration: bool, + custom_data: T, + ) { + self.next = Some((id, song.duration_millis, custom_data)); + } + fn pause(&mut self) { + match self.finished { + SongFinished::Never | SongFinished::In(_) => {} + SongFinished::At(time) => { + self.finished = SongFinished::In(time.saturating_duration_since(Instant::now())); + } + } + } + fn stop(&mut self) { + self.set_finished(false); + } + fn resume(&mut self) { + match self.finished { + SongFinished::Never | SongFinished::At(_) => {} + SongFinished::In(dur) => { + self.finished = SongFinished::At(Instant::now() + dur); + } + } + } + fn next(&mut self, play: bool, _load_duration: bool) { + self.current = self.next.take(); + self.set_finished(play); + } + fn clear(&mut self) { + self.current = None; + self.next = None; + self.finished = SongFinished::Never; + } + fn playing(&self) -> bool { + match self.finished { + SongFinished::Never => false, + SongFinished::In(_) => false, + SongFinished::At(_) => true, + } + } + fn current_song(&self) -> Option<(SongId, bool, &T)> { + self.current + .as_ref() + .map(|(id, _, custom)| (*id, true, custom)) + } + fn next_song(&self) -> Option<(SongId, bool, &T)> { + self.next + .as_ref() + .map(|(id, _, custom)| (*id, true, custom)) + } + fn gen_data_mut(&mut self) -> (Option<&mut T>, Option<&mut T>) { + ( + self.current.as_mut().map(|(_, _, t)| t), + self.next.as_mut().map(|(_, _, t)| t), + ) + } + fn song_finished_polling(&self) -> bool { + true + } + fn song_finished(&self) -> bool { + match self.finished { + SongFinished::Never => true, + SongFinished::In(dur) => dur <= Duration::ZERO, + SongFinished::At(time) => time <= Instant::now(), + } + } + fn current_song_duration(&self) -> Option { + self.current + .as_ref() + .map(|(_, dur, _)| *dur) + .filter(|dur| *dur > 0) + } + fn current_song_playback_position(&self) -> Option { + if let Some(duration) = self.current_song_duration() { + match self.finished { + SongFinished::Never => None, + SongFinished::In(dur) => Some(duration.saturating_sub(dur.as_millis() as u64)), + SongFinished::At(time) => Some(duration.saturating_sub( + time.saturating_duration_since(Instant::now()).as_millis() as u64, + )), + } + } else { + None + } + } +} diff --git a/musicdb-lib/src/server/mod.rs b/musicdb-lib/src/server/mod.rs index f186719..5921374 100755 --- a/musicdb-lib/src/server/mod.rs +++ b/musicdb-lib/src/server/mod.rs @@ -292,6 +292,8 @@ pub fn run_server_caching_thread_opt( use crate::player::playback_rs::PlayerBackendPlaybackRs; #[cfg(feature = "playback-via-rodio")] use crate::player::rodio::PlayerBackendRodio; + #[cfg(feature = "playback-via-sleep")] + use crate::player::sleep::PlayerBackendSleep; #[cfg(any( feature = "playback", feature = "playback-via-playback-rs", @@ -305,6 +307,8 @@ pub fn run_server_caching_thread_opt( #[cfg(feature = "playback")] let mut player = if play_audio { + #[cfg(feature = "playback-via-sleep")] + let backend = PlayerBackendSleep::new(Some(command_sender.clone())).unwrap(); #[cfg(feature = "playback-via-playback-rs")] let backend = PlayerBackendPlaybackRs::new(command_sender.clone()).unwrap(); #[cfg(feature = "playback-via-rodio")] diff --git a/musicdb-server/Cargo.toml b/musicdb-server/Cargo.toml index cc62921..3f197dc 100644 --- a/musicdb-server/Cargo.toml +++ b/musicdb-server/Cargo.toml @@ -14,6 +14,8 @@ serde_json = "1.0" tokio = { version = "1.37.0", optional = true, features = ["rt"] } rocket = { version = "0.5.0", optional = true } html-escape = { version = "0.2.13", optional = true } +rocket_ws = "0.1.1" +rocket_seek_stream = "0.2.6" [target.aarch64-linux-android.dependencies] # required for cross-compilation to android to work: link to shared c++ stdlib instead of c++_static @@ -24,5 +26,6 @@ default = ["website", "default-playback"] website = ["dep:tokio", "dep:rocket", "dep:html-escape"] playback = [] default-playback = ["playback", "musicdb-lib/default-playback"] +playback-via-sleep = ["playback", "musicdb-lib/playback-via-sleep"] playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"] playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"] diff --git a/musicdb-server/src/main.rs b/musicdb-server/src/main.rs index 3f94d1e..5565999 100755 --- a/musicdb-server/src/main.rs +++ b/musicdb-server/src/main.rs @@ -241,11 +241,7 @@ fn main() { run_server(database, Some(Box::new(move |c| s.send(c).unwrap()))) }); let sender = r.recv().unwrap(); - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(web::main(db, sender, *addr)); + web::main(db, sender, *addr); } } else { run_server(database, None); diff --git a/musicdb-server/src/web.rs b/musicdb-server/src/web.rs index a2b2bcd..0d0778a 100755 --- a/musicdb-server/src/web.rs +++ b/musicdb-server/src/web.rs @@ -3,13 +3,20 @@ use std::sync::{mpsc, Arc, Mutex}; use musicdb_lib::data::album::Album; use musicdb_lib::data::artist::Artist; -use musicdb_lib::data::database::Database; +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::http::ContentType; use rocket::response::content::RawHtml; -use rocket::{get, routes, Config, State}; +use rocket::response::Responder; +use rocket::{get, routes, Config, Response, State}; +use rocket_seek_stream::SeekStream; +use rocket_ws::{Message, WebSocket}; +use tokio::select; +use tokio::sync::mpsc::Sender; /* @@ -41,6 +48,7 @@ const HTML_END: &'static str = ""; struct Data { db: Arc>, command_sender: mpsc::Sender<(Command, Option)>, + websocket_connections: Arc>>>, } #[get("/")] @@ -91,7 +99,151 @@ 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; @@ -103,8 +255,8 @@ async function runLoop() { await sleep(100); } }); - nowPlayingDiv.innerHTML = await (await fetch("/now-playing-html")).text(); - queueDiv.innerHTML = await (await fetch("/queue-html")).text(); + await updateNowPlaying(); + await updateQueue(); var elapsedTime = new Date() - startTime; didFinish = true; averageLoopTimeMs = ((averageLoopTimeMs * 4) + elapsedTime) / 5; @@ -115,6 +267,7 @@ runLoop();"#; let buttons = ""; let search = "
"; + let playback_live = r#"
"#; let db = data.db.lock().unwrap(); let now_playing = gen_now_playing(&db); let mut queue = String::new(); @@ -122,13 +275,54 @@ runLoop();"#; dbg!(&queue); drop(db); RawHtml(format!( - "{HTML_START}MusicDb{script}{HTML_SEP}
no javascript? reload to see updated information.
{now_playing}
{buttons}
{search}
{queue}
{script2}{HTML_END}", + "{HTML_START}MusicDb{script}{HTML_SEP}
no javascript? reload to see updated information.
{now_playing}
{buttons}
{playback_live}
{search}
{queue}
{script2}{HTML_END}", )) } #[get("/now-playing-html")] fn now_playing_html(data: &State) -> RawHtml { RawHtml(gen_now_playing(&*data.db.lock().unwrap())) } +#[get("/now-playing-ids")] +fn now_playing_ids(data: &State) -> 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//")] +fn song(data: &State, id: SongId, name: String) -> Option { + 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>); +impl AsRef<[u8]> for ArcBytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + #[get("/queue-html")] fn queue_html(data: &State) -> RawHtml { let mut str = String::new(); @@ -139,7 +333,7 @@ fn queue_html(data: &State) -> RawHtml { 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!( - "

Now Playing

{}

", + "

Now Playing

{}

", html_escape::encode_safe(¤t_song.title), ) } else { @@ -545,22 +739,181 @@ fn search( RawHtml(out) } -pub async fn main( +#[get("/ws")] +async fn websocket(websocket: WebSocket, state: &State) -> 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>, command_sender: mpsc::Sender<(Command, Option)>, 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) { + 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)); +} +pub async fn async_main(data: Data, addr: SocketAddr) { rocket::build() .configure(Config { address: addr.ip(), port: addr.port(), ..Default::default() }) - .manage(Data { db, command_sender }) + .manage(data) .mount( "/", routes![ index, + websocket, play, pause, stop, @@ -571,7 +924,9 @@ pub async fn main( add_song, search, now_playing_html, - queue_html + now_playing_ids, + song, + queue_html, ], ) .launch()