From c3622aca30ed627ddd89ec145fcb86cb9c317d99 Mon Sep 17 00:00:00 2001 From: Mark <> Date: Sun, 8 Dec 2024 12:03:24 +0100 Subject: [PATCH] sequence numbers --- musicdb-client/Cargo.toml | 11 +- musicdb-client/src/gui.rs | 171 ++++++++++++++------------ musicdb-client/src/gui_edit_song.rs | 4 +- musicdb-client/src/gui_playpause.rs | 14 +-- musicdb-client/src/gui_queue.rs | 42 +++---- musicdb-client/src/gui_screen.rs | 22 ++-- musicdb-client/src/gui_settings.rs | 4 +- musicdb-client/src/main.rs | 17 +-- musicdb-lib/Cargo.toml | 11 +- musicdb-lib/src/data/database.rs | 154 +++++++++++++---------- musicdb-lib/src/data/queue.rs | 33 +++++ musicdb-lib/src/player/mod.rs | 26 ++-- musicdb-lib/src/player/playback_rs.rs | 31 +++-- musicdb-lib/src/player/rodio.rs | 16 ++- musicdb-lib/src/server/mod.rs | 71 ++++++++++- musicdb-server/Cargo.toml | 7 +- musicdb-server/src/main.rs | 4 +- musicdb-server/src/web.rs | 48 ++++---- 18 files changed, 429 insertions(+), 257 deletions(-) diff --git a/musicdb-client/Cargo.toml b/musicdb-client/Cargo.toml index 9965443..e884b76 100644 --- a/musicdb-client/Cargo.toml +++ b/musicdb-client/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -musicdb-lib = { path = "../musicdb-lib" } +musicdb-lib = { path = "../musicdb-lib", default-features = false } clap = { version = "4.4.6", features = ["derive"] } directories = "5.0.1" regex = "1.9.3" @@ -16,7 +16,7 @@ musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true } uianimator = "0.1.1" [features] -default = ["gui", "playback", "merscfg"] +default = ["gui", "default-playback"] # gui: # enables the gui modes # merscfg: @@ -26,6 +26,9 @@ default = ["gui", "playback", "merscfg"] # playback: # enables syncplayer modes, where the client mirrors the server's playback gui = ["speedy2d"] -merscfg = ["mers", "speedy2d"] +merscfg = ["mers", "gui"] mers = ["musicdb-mers"] -playback = ["musicdb-lib/playback"] +playback = [] +default-playback = ["playback", "musicdb-lib/default-playback"] +playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"] +playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"] diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index 6b1bff7..34828e2 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -16,7 +16,7 @@ use musicdb_lib::{ AlbumId, ArtistId, CoverId, SongId, }, load::ToFromBytes, - server::{get, Command}, + server::{get, Action}, }; use speedy2d::{ color::Color, @@ -363,84 +363,86 @@ impl Gui { Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"), } database.lock().unwrap().update_endpoints.push( - musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { - Command::Resume - | Command::Pause - | Command::Stop - | Command::Save - | Command::InitComplete => {} - Command::NextSong - | Command::QueueUpdate(..) - | Command::QueueAdd(..) - | Command::QueueInsert(..) - | Command::QueueRemove(..) - | Command::QueueMove(..) - | Command::QueueMoveInto(..) - | Command::QueueGoto(..) - | Command::QueueShuffle(..) - | Command::QueueSetShuffle(..) - | Command::QueueUnshuffle(..) => { - if let Some(s) = &*event_sender_arc.lock().unwrap() { - _ = s.send_event(GuiEvent::UpdatedQueue); + musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| { + match &cmd.action { + Action::Resume + | Action::Pause + | Action::Stop + | Action::Save + | Action::InitComplete => {} + Action::NextSong + | Action::QueueUpdate(..) + | Action::QueueAdd(..) + | Action::QueueInsert(..) + | Action::QueueRemove(..) + | Action::QueueMove(..) + | Action::QueueMoveInto(..) + | Action::QueueGoto(..) + | Action::QueueShuffle(..) + | Action::QueueSetShuffle(..) + | Action::QueueUnshuffle(..) => { + if let Some(s) = &*event_sender_arc.lock().unwrap() { + _ = s.send_event(GuiEvent::UpdatedQueue); + } } - } - Command::SyncDatabase(..) - | Command::AddSong(_) - | Command::AddAlbum(_) - | Command::AddArtist(_) - | Command::AddCover(_) - | Command::ModifySong(_) - | Command::ModifyAlbum(_) - | Command::ModifyArtist(_) - | Command::RemoveSong(_) - | Command::RemoveAlbum(_) - | Command::RemoveArtist(_) - | Command::TagSongFlagSet(..) - | Command::TagSongFlagUnset(..) - | Command::TagAlbumFlagSet(..) - | Command::TagAlbumFlagUnset(..) - | Command::TagArtistFlagSet(..) - | Command::TagArtistFlagUnset(..) - | Command::TagSongPropertySet(..) - | Command::TagSongPropertyUnset(..) - | Command::TagAlbumPropertySet(..) - | Command::TagAlbumPropertyUnset(..) - | Command::TagArtistPropertySet(..) - | Command::TagArtistPropertyUnset(..) - | Command::SetSongDuration(..) => { - if let Some(s) = &*event_sender_arc.lock().unwrap() { - _ = s.send_event(GuiEvent::UpdatedLibrary); + Action::SyncDatabase(..) + | Action::AddSong(_) + | Action::AddAlbum(_) + | Action::AddArtist(_) + | Action::AddCover(_) + | Action::ModifySong(_) + | Action::ModifyAlbum(_) + | Action::ModifyArtist(_) + | Action::RemoveSong(_) + | Action::RemoveAlbum(_) + | Action::RemoveArtist(_) + | Action::TagSongFlagSet(..) + | Action::TagSongFlagUnset(..) + | Action::TagAlbumFlagSet(..) + | Action::TagAlbumFlagUnset(..) + | Action::TagArtistFlagSet(..) + | Action::TagArtistFlagUnset(..) + | Action::TagSongPropertySet(..) + | Action::TagSongPropertyUnset(..) + | Action::TagAlbumPropertySet(..) + | Action::TagAlbumPropertyUnset(..) + | Action::TagArtistPropertySet(..) + | Action::TagArtistPropertyUnset(..) + | Action::SetSongDuration(..) => { + if let Some(s) = &*event_sender_arc.lock().unwrap() { + _ = s.send_event(GuiEvent::UpdatedLibrary); + } } - } - Command::ErrorInfo(t, d) => { - let (t, d) = (t.clone(), d.clone()); - notif_sender_two - .send(Box::new(move |_| { - ( - Box::new(Panel::with_background( - GuiElemCfg::default(), - [Label::new( + Action::ErrorInfo(t, d) => { + let (t, d) = (t.clone(), d.clone()); + notif_sender_two + .send(Box::new(move |_| { + ( + Box::new(Panel::with_background( GuiElemCfg::default(), - if t.is_empty() { - format!("Server message\n{d}") - } else { - format!("Server error ({t})\n{d}") - }, - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - )], - Color::from_rgba(0.0, 0.0, 0.0, 0.8), - )), - if t.is_empty() { - NotifInfo::new(Duration::from_secs(2)) - } else { - NotifInfo::new(Duration::from_secs(5)) - .with_highlight(Color::RED) - }, - ) - })) - .unwrap(); + [Label::new( + GuiElemCfg::default(), + if t.is_empty() { + format!("Server message\n{d}") + } else { + format!("Server error ({t})\n{d}") + }, + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + Color::from_rgba(0.0, 0.0, 0.0, 0.8), + )), + if t.is_empty() { + NotifInfo::new(Duration::from_secs(2)) + } else { + NotifInfo::new(Duration::from_secs(5)) + .with_highlight(Color::RED) + }, + ) + })) + .unwrap(); + } } })), ); @@ -1191,7 +1193,7 @@ pub enum GuiAction { ShowNotification(Box (Box, NotifInfo) + Send>), /// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc) Build(Box Vec>), - SendToServer(Command), + SendToServer(Action), ContextMenu(Option<(Vec>)>), /// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one. ResetKeyboardFocus, @@ -1304,10 +1306,17 @@ impl Gui { self.keybinds.insert(bind, action.with_priority(priority)); } } - GuiAction::SendToServer(cmd) => { + GuiAction::SendToServer(action) => { #[cfg(debug_assertions)] - eprintln!("[DEBUG] Sending command to server: {cmd:?}"); - if let Err(e) = cmd.to_bytes(&mut self.connection) { + eprintln!("[DEBUG] Sending command to server: {action:?}"); + if let Err(e) = self + .database + .lock() + .unwrap() + .seq + .pack(action) + .to_bytes(&mut self.connection) + { eprintln!("Error sending command to server: {e}"); } } @@ -1551,7 +1560,7 @@ impl WindowHandler for Gui { | Dragging::Queue(Ok(_)) | Dragging::Queues(_) => (), Dragging::Queue(Err(path)) => { - self.exec_gui_action(GuiAction::SendToServer(Command::QueueRemove(path))) + self.exec_gui_action(GuiAction::SendToServer(Action::QueueRemove(path))) } } } diff --git a/musicdb-client/src/gui_edit_song.rs b/musicdb-client/src/gui_edit_song.rs index 3fa3aca..52436a3 100644 --- a/musicdb-client/src/gui_edit_song.rs +++ b/musicdb-client/src/gui_edit_song.rs @@ -2,7 +2,7 @@ use std::time::Instant; use musicdb_lib::{ data::{song::Song, ArtistId}, - server::Command, + server::Action, }; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; @@ -189,7 +189,7 @@ impl GuiElem for EditorForSongs { song.album = None; } info.actions - .push(GuiAction::SendToServer(Command::ModifySong(song))); + .push(GuiAction::SendToServer(Action::ModifySong(song))); } } Event::SetArtist(name, id) => { diff --git a/musicdb-client/src/gui_playpause.rs b/musicdb-client/src/gui_playpause.rs index 70e49a2..34e7cbb 100644 --- a/musicdb-client/src/gui_playpause.rs +++ b/musicdb-client/src/gui_playpause.rs @@ -1,6 +1,6 @@ use std::sync::{atomic::AtomicBool, Arc}; -use musicdb_lib::server::Command; +use musicdb_lib::server::Action; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; use crate::{ @@ -28,9 +28,9 @@ impl PlayPause { if let Some(song) = db.get_song(song_id) { vec![GuiAction::SendToServer( if song.general.tags.iter().any(|v| v == "Fav") { - Command::TagSongFlagUnset(*song_id, "Fav".to_owned()) + Action::TagSongFlagUnset(*song_id, "Fav".to_owned()) } else { - Command::TagSongFlagSet(*song_id, "Fav".to_owned()) + Action::TagSongFlagSet(*song_id, "Fav".to_owned()) }, )] } else { @@ -48,7 +48,7 @@ impl PlayPause { ), to_zero: Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.26, 0.01), (0.49, 0.99))), - |_| vec![GuiAction::SendToServer(Command::Stop)], + |_| vec![GuiAction::SendToServer(Action::Stop)], [Panel::with_background( GuiElemCfg::at(Rectangle::from_tuples((0.2, 0.2), (0.8, 0.8))), (), @@ -59,9 +59,9 @@ impl PlayPause { GuiElemCfg::at(Rectangle::from_tuples((0.51, 0.01), (0.74, 0.99))), |btn| { vec![GuiAction::SendToServer(if btn.children[0].is_playing { - Command::Pause + Action::Pause } else { - Command::Resume + Action::Resume })] }, [PlayPauseDisplay::new(GuiElemCfg::at( @@ -70,7 +70,7 @@ impl PlayPause { ), to_end: Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.76, 0.01), (0.99, 0.99))), - |_| vec![GuiAction::SendToServer(Command::NextSong)], + |_| vec![GuiAction::SendToServer(Action::NextSong)], [NextSongShape::new(GuiElemCfg::at(Rectangle::from_tuples( (0.2, 0.2), (0.8, 0.8), diff --git a/musicdb-client/src/gui_queue.rs b/musicdb-client/src/gui_queue.rs index d89e8f4..3a08f0d 100755 --- a/musicdb-client/src/gui_queue.rs +++ b/musicdb-client/src/gui_queue.rs @@ -5,7 +5,7 @@ use musicdb_lib::{ song::Song, AlbumId, ArtistId, }, - server::Command, + server::Action, }; use speedy2d::{ color::Color, @@ -404,8 +404,8 @@ impl GuiElem for QueueEmptySpaceDragHandler { dragged_add_to_queue( dragged, (), - |_, q| Command::QueueAdd(vec![], q), - |_, q| Command::QueueMoveInto(q, vec![]), + |_, q| Action::QueueAdd(vec![], q), + |_, q| Action::QueueMoveInto(q, vec![]), ) } } @@ -563,7 +563,7 @@ impl GuiElem for QueueSong { if self.mouse && button == MouseButton::Left { self.mouse = false; if e.take() && !self.always_copy { - vec![GuiAction::SendToServer(Command::QueueGoto( + vec![GuiAction::SendToServer(Action::QueueGoto( self.path.clone(), ))] } else { @@ -631,9 +631,9 @@ impl GuiElem for QueueSong { self.path.clone(), move |mut p: Vec, q| { if let Some(j) = p.pop() { - Command::QueueInsert(p, if insert_below { j + 1 } else { j }, q) + Action::QueueInsert(p, if insert_below { j + 1 } else { j }, q) } else { - Command::QueueAdd(p, q) + Action::QueueAdd(p, q) } }, move |mut p, q| { @@ -642,7 +642,7 @@ impl GuiElem for QueueSong { *l += 1; } } - Command::QueueMove(q, p) + Action::QueueMove(q, p) }, ) } else { @@ -787,9 +787,9 @@ impl GuiElem for QueueFolder { // Panel::with_background(GuiElemCfg::default(), (), Color::DARK_GRAY), // )]))]; return vec![GuiAction::SendToServer(if self.queue.order.is_some() { - Command::QueueUnshuffle(self.path.clone()) + Action::QueueUnshuffle(self.path.clone()) } else { - Command::QueueShuffle(self.path.clone()) + Action::QueueShuffle(self.path.clone()) })]; } vec![] @@ -798,7 +798,7 @@ impl GuiElem for QueueFolder { if self.mouse && button == MouseButton::Left { self.mouse = false; if e.take() && !self.always_copy { - vec![GuiAction::SendToServer(Command::QueueGoto( + vec![GuiAction::SendToServer(Action::QueueGoto( self.path.clone(), ))] } else { @@ -826,8 +826,8 @@ impl GuiElem for QueueFolder { dragged_add_to_queue( dragged, self.path.clone(), - |p, q| Command::QueueAdd(p, q), - |p, q| Command::QueueMoveInto(q, p), + |p, q| Action::QueueAdd(p, q), + |p, q| Action::QueueMoveInto(q, p), ) } else { dragged_add_to_queue( @@ -835,9 +835,9 @@ impl GuiElem for QueueFolder { self.path.clone(), |mut p, q| { let j = p.pop().unwrap_or(0); - Command::QueueInsert(p, j, q) + Action::QueueInsert(p, j, q) }, - |p, q| Command::QueueMove(q, p), + |p, q| Action::QueueMove(q, p), ) } } else { @@ -903,10 +903,10 @@ impl GuiElem for QueueIndentEnd { dragged_add_to_queue( dragged, self.path_insert.clone(), - |(p, j), q| Command::QueueInsert(p, j, q), + |(p, j), q| Action::QueueInsert(p, j, q), |(mut p, j), q| { p.push(j); - Command::QueueMove(q, p) + Action::QueueMove(q, p) }, ) } @@ -1037,7 +1037,7 @@ impl GuiElem for QueueLoop { if self.mouse && button == MouseButton::Left { self.mouse = false; if e.take() && !self.always_copy { - vec![GuiAction::SendToServer(Command::QueueGoto( + vec![GuiAction::SendToServer(Action::QueueGoto( self.path.clone(), ))] } else { @@ -1066,8 +1066,8 @@ impl GuiElem for QueueLoop { dragged_add_to_queue( dragged, p, - |p, q| Command::QueueAdd(p, q), - |p, q| Command::QueueMoveInto(q, p), + |p, q| Action::QueueAdd(p, q), + |p, q| Action::QueueMoveInto(q, p), ) } else { vec![] @@ -1078,8 +1078,8 @@ impl GuiElem for QueueLoop { fn dragged_add_to_queue( dragged: Dragging, data: T, - f_queues: impl FnOnce(T, Vec) -> Command + 'static, - f_queue_by_path: impl FnOnce(T, Vec) -> Command + 'static, + f_queues: impl FnOnce(T, Vec) -> Action + 'static, + f_queue_by_path: impl FnOnce(T, Vec) -> Action + 'static, ) -> Vec { match dragged { Dragging::Artist(id) => { diff --git a/musicdb-client/src/gui_screen.rs b/musicdb-client/src/gui_screen.rs index 2c13c20..f3f3599 100755 --- a/musicdb-client/src/gui_screen.rs +++ b/musicdb-client/src/gui_screen.rs @@ -2,7 +2,7 @@ use std::time::Instant; use musicdb_lib::{ data::queue::{QueueContent, QueueFolder}, - server::Command, + server::Action, }; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D}; use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator}; @@ -120,15 +120,13 @@ impl GuiScreen { button_clear_queue: Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))), |_| { - vec![GuiAction::SendToServer( - musicdb_lib::server::Command::QueueUpdate( - vec![], - musicdb_lib::data::queue::QueueContent::Folder( - musicdb_lib::data::queue::QueueFolder::default(), - ) - .into(), - ), - )] + vec![GuiAction::SendToServer(Action::QueueUpdate( + vec![], + musicdb_lib::data::queue::QueueContent::Folder( + musicdb_lib::data::queue::QueueFolder::default(), + ) + .into(), + ))] }, [Label::new( GuiElemCfg::default(), @@ -305,9 +303,9 @@ impl GuiElem for GuiScreen { if key == ' ' && !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && e.take() { vec![GuiAction::Build(Box::new(|db| { vec![GuiAction::SendToServer(if db.playing { - Command::Pause + Action::Pause } else { - Command::Resume + Action::Resume })] }))] } else { diff --git a/musicdb-client/src/gui_settings.rs b/musicdb-client/src/gui_settings.rs index 1998b40..1bf05af 100755 --- a/musicdb-client/src/gui_settings.rs +++ b/musicdb-client/src/gui_settings.rs @@ -1,6 +1,6 @@ use std::sync::{atomic::AtomicBool, Arc, Mutex}; -use musicdb_lib::server::Command; +use musicdb_lib::server::Action; use speedy2d::{ color::Color, dimen::Vec2, @@ -450,7 +450,7 @@ impl SettingsContent { ), save_button: Button::new( GuiElemCfg::default(), - |_| vec![GuiAction::SendToServer(Command::Save)], + |_| vec![GuiAction::SendToServer(Action::Save)], [Label::new( GuiElemCfg::default(), "Server: Save Changes".to_string(), diff --git a/musicdb-client/src/main.rs b/musicdb-client/src/main.rs index 0b70cd1..aea10f5 100755 --- a/musicdb-client/src/main.rs +++ b/musicdb-client/src/main.rs @@ -1,3 +1,5 @@ +// #![allow(unused)] + use std::{ io::{BufReader, Write}, net::{SocketAddr, TcpStream}, @@ -12,7 +14,7 @@ use gui::GuiEvent; #[cfg(feature = "playback")] use musicdb_lib::data::cache_manager::CacheManager; #[cfg(feature = "playback")] -use musicdb_lib::player::{playback_rs::PlayerBackendPlaybackRs, Player}; +use musicdb_lib::player::{Player, PlayerBackendFeat}; use musicdb_lib::{ data::{ database::{ClientIo, Database}, @@ -152,7 +154,7 @@ fn main() { cm.set_cache_songs_count(20); cache_manager = Some(cm); Some(Player::new_client( - PlayerBackendPlaybackRs::new_without_command_sending().unwrap(), + PlayerBackendFeat::new_without_command_sending().unwrap(), )) } else { None @@ -186,21 +188,22 @@ fn main() { ))); } loop { - let update = Command::from_bytes(&mut con).unwrap(); + let command = Command::from_bytes(&mut con).unwrap(); let mut db = database.lock().unwrap(); + let action = db.seq.recv(command); #[cfg(feature = "playback")] if let Some(player) = &mut player { - player.handle_command(&update); + player.handle_action(&action); } #[allow(unused_labels)] 'feature_if: { #[cfg(any(feature = "mers", feature = "merscfg"))] if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() { - db.apply_command(update.clone()); - action(update); + db.apply_command(action.clone()); + action(action); break 'feature_if; } - db.apply_command(update); + db.apply_action_unchecked_seq(action); } #[cfg(feature = "playback")] if let Some(player) = &mut player { diff --git a/musicdb-lib/Cargo.toml b/musicdb-lib/Cargo.toml index f8a4948..4b4e251 100644 --- a/musicdb-lib/Cargo.toml +++ b/musicdb-lib/Cargo.toml @@ -13,8 +13,9 @@ rodio = { version = "0.20.1", optional = true } sysinfo = "0.30.12" [features] -# default = ["playback"] -playback = ["playback-via-playback-rs"] -# playback = ["playback-via-rodio"] -playback-via-playback-rs = ["dep:playback-rs"] -playback-via-rodio = ["dep:rodio"] +default = [] +playback = [] +default-playback = ["playback-via-playback-rs"] +# default-playback = ["playback-via-rodio"] +playback-via-playback-rs = ["playback", "dep:playback-rs"] +playback-via-rodio = ["playback", "dep:rodio"] diff --git a/musicdb-lib/src/data/database.rs b/musicdb-lib/src/data/database.rs index 299d499..d9518de 100755 --- a/musicdb-lib/src/data/database.rs +++ b/musicdb-lib/src/data/database.rs @@ -11,7 +11,10 @@ use std::{ use colorize::AnsiColor; use rand::thread_rng; -use crate::{load::ToFromBytes, server::Command}; +use crate::{ + load::ToFromBytes, + server::{Action, Command, Commander}, +}; use super::{ album::Album, @@ -22,6 +25,7 @@ use super::{ }; pub struct Database { + pub seq: Commander, /// the directory that contains the dbfile, backups, statistics, ... pub db_dir: PathBuf, /// the path to the file used to save/load the data. empty if database is in client mode. @@ -495,75 +499,93 @@ impl Database { pub fn init_connection(&self, con: &mut T) -> Result<(), std::io::Error> { // TODO! this is slow because it clones everything - there has to be a better way... - Command::SyncDatabase( - self.artists().iter().map(|v| v.1.clone()).collect(), - self.albums().iter().map(|v| v.1.clone()).collect(), - self.songs().iter().map(|v| v.1.clone()).collect(), - ) - .to_bytes(con)?; - Command::QueueUpdate(vec![], self.queue.clone()).to_bytes(con)?; + self.seq + .pack(Action::SyncDatabase( + self.artists().iter().map(|v| v.1.clone()).collect(), + self.albums().iter().map(|v| v.1.clone()).collect(), + self.songs().iter().map(|v| v.1.clone()).collect(), + )) + .to_bytes(con)?; + self.seq + .pack(Action::QueueUpdate(vec![], self.queue.clone())) + .to_bytes(con)?; if self.playing { - Command::Resume.to_bytes(con)?; + self.seq.pack(Action::Resume).to_bytes(con)?; } // this allows clients to find out when init_connection is done. - Command::InitComplete.to_bytes(con)?; + self.seq.pack(Action::InitComplete).to_bytes(con)?; // is initialized now - client can receive updates after this point. // NOTE: Don't write to connection anymore - the db will dispatch updates on its own. // we just need to handle commands (receive from the connection). Ok(()) } - pub fn apply_command(&mut self, mut command: Command) { + /// `apply_action_unchecked_seq(command.action)` if `command.seq` is correct or `0xFF` + pub fn apply_command(&mut self, command: Command) { + if command.seq != self.seq.seq() && command.seq != 0xFF { + eprintln!( + "Invalid sequence number: got {} but expected {}.", + command.seq, + self.seq.seq() + ); + return; + } + self.apply_action_unchecked_seq(command.action) + } + pub fn apply_action_unchecked_seq(&mut self, mut action: Action) { if !self.is_client() { - if let Command::ErrorInfo(t, _) = &mut command { + if let Action::ErrorInfo(t, _) = &mut action { // clients can send ErrorInfo to the server and it will show up on other clients, // BUT only the server can set the Title of the ErrorInfo. t.clear(); } } // some commands shouldn't be broadcast. these will broadcast a different command in their specific implementation. - match &command { + match &action { // Will broadcast `QueueSetShuffle` - Command::QueueShuffle(_) => (), + Action::QueueShuffle(_) => (), + Action::NextSong if self.queue.is_almost_empty() => (), + Action::Pause if !self.playing => (), + Action::Resume if self.playing => (), // since db.update_endpoints is empty for clients, this won't cause unwanted back and forth - _ => self.broadcast_update(&command), + _ => action = self.broadcast_update(action), } - match command { - Command::Resume => self.playing = true, - Command::Pause => self.playing = false, - Command::Stop => self.playing = false, - Command::NextSong => { + match action { + Action::Resume => self.playing = true, + Action::Pause => self.playing = false, + Action::Stop => self.playing = false, + Action::NextSong => { if !Queue::advance_index_db(self) { // end of queue - self.apply_command(Command::Pause); + self.apply_action_unchecked_seq(Action::Pause); self.queue.init(); } } - Command::Save => { + Action::Save => { if let Err(e) = self.save_database(None) { eprintln!("[{}] Couldn't save: {e}", "ERR!".red()); } } - Command::SyncDatabase(a, b, c) => self.sync(a, b, c), - Command::QueueUpdate(index, new_data) => { + Action::SyncDatabase(a, b, c) => self.sync(a, b, c), + Action::QueueUpdate(index, new_data) => { if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { *v = new_data; } } - Command::QueueAdd(index, new_data) => { + Action::QueueAdd(index, new_data) => { if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { v.add_to_end(new_data, false); } } - Command::QueueInsert(index, pos, new_data) => { + Action::QueueInsert(index, pos, new_data) => { if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { v.insert(new_data, pos, false); } } - Command::QueueRemove(index) => { + Action::QueueRemove(index) => { self.queue.remove_by_index(&index, 0); } - Command::QueueMove(index_from, mut index_to) => 'queue_move: { + Action::QueueMove(index_from, mut index_to) => 'queue_move: { if index_to.len() == 0 || index_to.starts_with(&index_from) { break 'queue_move; } @@ -605,7 +627,7 @@ impl Database { } } } - Command::QueueMoveInto(index_from, mut parent_to) => 'queue_move_into: { + Action::QueueMoveInto(index_from, mut parent_to) => 'queue_move_into: { if parent_to.starts_with(&index_from) { break 'queue_move_into; } @@ -628,8 +650,8 @@ impl Database { } } } - Command::QueueGoto(index) => Queue::set_index_db(self, &index), - Command::QueueShuffle(path) => { + Action::QueueGoto(index) => Queue::set_index_db(self, &index), + Action::QueueShuffle(path) => { if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) { if let QueueContent::Folder(QueueFolder { index: _, @@ -640,7 +662,7 @@ impl Database { { let mut ord: Vec = (0..content.len()).collect(); ord.shuffle(&mut thread_rng()); - self.apply_command(Command::QueueSetShuffle(path, ord)); + self.apply_action_unchecked_seq(Action::QueueSetShuffle(path, ord)); } else { eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!"); } @@ -648,7 +670,7 @@ impl Database { eprintln!("(QueueShuffle) No QueueElement at {path:?}"); } } - Command::QueueSetShuffle(path, ord) => { + Action::QueueSetShuffle(path, ord) => { if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) { if let QueueContent::Folder(QueueFolder { index, @@ -681,7 +703,7 @@ impl Database { ); } } - Command::QueueUnshuffle(path) => { + Action::QueueUnshuffle(path) => { if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) { if let QueueContent::Folder(QueueFolder { index, @@ -697,77 +719,77 @@ impl Database { } } } - Command::AddSong(song) => { + Action::AddSong(song) => { self.add_song_new(song); } - Command::AddAlbum(album) => { + Action::AddAlbum(album) => { self.add_album_new(album); } - Command::AddArtist(artist) => { + Action::AddArtist(artist) => { self.add_artist_new(artist); } - Command::AddCover(cover) => _ = self.add_cover_new(cover), - Command::ModifySong(song) => { + Action::AddCover(cover) => _ = self.add_cover_new(cover), + Action::ModifySong(song) => { _ = self.update_song(song); } - Command::ModifyAlbum(album) => { + Action::ModifyAlbum(album) => { _ = self.update_album(album); } - Command::ModifyArtist(artist) => { + Action::ModifyArtist(artist) => { _ = self.update_artist(artist); } - Command::RemoveSong(song) => { + Action::RemoveSong(song) => { _ = self.remove_song(song); } - Command::RemoveAlbum(album) => { + Action::RemoveAlbum(album) => { _ = self.remove_album(album); } - Command::RemoveArtist(artist) => { + Action::RemoveArtist(artist) => { _ = self.remove_artist(artist); } - Command::TagSongFlagSet(id, tag) => { + Action::TagSongFlagSet(id, tag) => { if let Some(v) = self.get_song_mut(&id) { if !v.general.tags.contains(&tag) { v.general.tags.push(tag); } } } - Command::TagSongFlagUnset(id, tag) => { + Action::TagSongFlagUnset(id, tag) => { if let Some(v) = self.get_song_mut(&id) { if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { v.general.tags.remove(i); } } } - Command::TagAlbumFlagSet(id, tag) => { + Action::TagAlbumFlagSet(id, tag) => { if let Some(v) = self.albums.get_mut(&id) { if !v.general.tags.contains(&tag) { v.general.tags.push(tag); } } } - Command::TagAlbumFlagUnset(id, tag) => { + Action::TagAlbumFlagUnset(id, tag) => { if let Some(v) = self.albums.get_mut(&id) { if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { v.general.tags.remove(i); } } } - Command::TagArtistFlagSet(id, tag) => { + Action::TagArtistFlagSet(id, tag) => { if let Some(v) = self.artists.get_mut(&id) { if !v.general.tags.contains(&tag) { v.general.tags.push(tag); } } } - Command::TagArtistFlagUnset(id, tag) => { + Action::TagArtistFlagUnset(id, tag) => { if let Some(v) = self.artists.get_mut(&id) { if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { v.general.tags.remove(i); } } } - Command::TagSongPropertySet(id, key, val) => { + Action::TagSongPropertySet(id, key, val) => { if let Some(v) = self.get_song_mut(&id) { let new = format!("{key}{val}"); if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { @@ -777,13 +799,13 @@ impl Database { } } } - Command::TagSongPropertyUnset(id, key) => { + Action::TagSongPropertyUnset(id, key) => { if let Some(v) = self.get_song_mut(&id) { let tags = std::mem::replace(&mut v.general.tags, vec![]); v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); } } - Command::TagAlbumPropertySet(id, key, val) => { + Action::TagAlbumPropertySet(id, key, val) => { if let Some(v) = self.albums.get_mut(&id) { let new = format!("{key}{val}"); if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { @@ -793,13 +815,13 @@ impl Database { } } } - Command::TagAlbumPropertyUnset(id, key) => { + Action::TagAlbumPropertyUnset(id, key) => { if let Some(v) = self.albums.get_mut(&id) { let tags = std::mem::replace(&mut v.general.tags, vec![]); v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); } } - Command::TagArtistPropertySet(id, key, val) => { + Action::TagArtistPropertySet(id, key, val) => { if let Some(v) = self.artists.get_mut(&id) { let new = format!("{key}{val}"); if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { @@ -809,21 +831,21 @@ impl Database { } } } - Command::TagArtistPropertyUnset(id, key) => { + Action::TagArtistPropertyUnset(id, key) => { if let Some(v) = self.artists.get_mut(&id) { let tags = std::mem::replace(&mut v.general.tags, vec![]); v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); } } - Command::SetSongDuration(id, duration) => { + Action::SetSongDuration(id, duration) => { if let Some(song) = self.get_song_mut(&id) { song.duration_millis = duration; } } - Command::InitComplete => { + Action::InitComplete => { self.client_is_init = true; } - Command::ErrorInfo(..) => {} + Action::ErrorInfo(..) => {} } } } @@ -842,6 +864,7 @@ impl Database { /// A client database doesn't need any storage paths and won't perform autosaves. pub fn new_clientside() -> Self { Self { + seq: Commander::new(true), db_dir: PathBuf::new(), db_file: PathBuf::new(), lib_directory: PathBuf::new(), @@ -862,6 +885,7 @@ impl Database { pub fn new_empty_in_dir(dir: PathBuf, lib_dir: PathBuf) -> Self { let path = dir.join("dbfile"); Self { + seq: Commander::new(false), db_dir: dir, db_file: path, lib_directory: lib_dir, @@ -887,6 +911,7 @@ impl Database { let mut file = BufReader::new(File::open(&path)?); eprintln!("[{}] loading library from {file:?}", "INFO".cyan()); let s = Self { + seq: Commander::new(false), db_dir: dir, db_file: path, lib_directory, @@ -948,11 +973,15 @@ impl Database { self.times_data_modified = None; Ok(path) } - pub fn broadcast_update(&mut self, update: &Command) { + pub fn broadcast_update(&mut self, update: Action) -> Action { match update { - Command::InitComplete => return, + Action::InitComplete => return update, _ => {} } + if !self.is_client() { + self.seq.inc(); + } + let update = self.seq.pack(update); let mut remove = vec![]; let mut bytes = None; let mut arc = None; @@ -974,7 +1003,7 @@ impl Database { remove.push(i); } } - UpdateEndpoint::Custom(func) => func(update), + UpdateEndpoint::Custom(func) => func(&update), UpdateEndpoint::CustomArc(func) => { if arc.is_none() { arc = Some(Arc::new(update.clone())); @@ -999,6 +1028,7 @@ impl Database { self.update_endpoints.remove(i); } } + update.action } pub fn sync(&mut self, artists: Vec, albums: Vec, songs: Vec) { self.modified_data(); diff --git a/musicdb-lib/src/data/queue.rs b/musicdb-lib/src/data/queue.rs index 2ac0624..cce4742 100755 --- a/musicdb-lib/src/data/queue.rs +++ b/musicdb-lib/src/data/queue.rs @@ -64,6 +64,39 @@ impl Queue { } } + pub fn is_empty(&self) -> bool { + if !self.enabled { + return true; + } + match &self.content { + QueueContent::Song(_) => false, + QueueContent::Folder(folder) => folder.content.iter().all(|v| v.is_empty()), + QueueContent::Loop(_total, _done, inner) => inner.is_empty(), + } + } + /// returns true if there is at most one song in the queue + pub fn is_almost_empty(&self) -> bool { + self.is_almost_empty_int() < 2 + } + fn is_almost_empty_int(&self) -> u8 { + if !self.enabled { + return 0; + } + match &self.content { + QueueContent::Song(_) => 1, + QueueContent::Folder(folder) => { + let mut o = 0; + for v in folder.content.iter() { + o += v.is_almost_empty_int(); + if o >= 2 { + return 2; + } + } + o + } + QueueContent::Loop(_total, _done, inner) => inner.is_almost_empty_int(), + } + } pub fn len(&self) -> usize { if !self.enabled { return 0; diff --git a/musicdb-lib/src/player/mod.rs b/musicdb-lib/src/player/mod.rs index 7a01c2b..0151f5e 100755 --- a/musicdb-lib/src/player/mod.rs +++ b/musicdb-lib/src/player/mod.rs @@ -2,12 +2,16 @@ pub mod playback_rs; #[cfg(feature = "playback-via-rodio")] pub mod rodio; +#[cfg(feature = "playback-via-playback-rs")] +pub type PlayerBackendFeat = playback_rs::PlayerBackendPlaybackRs; +#[cfg(feature = "playback-via-rodio")] +pub type PlayerBackendFeat = rodio::PlayerBackendRodio; use std::{collections::HashMap, ffi::OsStr, sync::Arc}; use crate::{ data::{database::Database, song::CachedData, SongId}, - server::Command, + server::Action, }; pub struct Player> { @@ -94,11 +98,11 @@ impl> Player { allow_sending_commands: false, } } - pub fn handle_command(&mut self, command: &Command) { - match command { - Command::Resume => self.resume(), - Command::Pause => self.pause(), - Command::Stop => self.stop(), + pub fn handle_action(&mut self, action: &Action) { + match action { + Action::Resume => self.resume(), + Action::Pause => self.pause(), + Action::Stop => self.stop(), _ => {} } } @@ -122,7 +126,7 @@ impl> Player { pub fn update_uncache_opt(&mut self, db: &mut Database, allow_uncaching: bool) { if self.allow_sending_commands { if self.allow_sending_commands && self.backend.song_finished() { - db.apply_command(Command::NextSong); + db.apply_action_unchecked_seq(Action::NextSong); } } @@ -141,7 +145,7 @@ impl> Player { self.backend.next(db.playing, load_duration); if self.allow_sending_commands && load_duration { if let Some(dur) = self.backend.current_song_duration() { - db.apply_command(Command::SetSongDuration(id, dur)) + db.apply_action_unchecked_seq(Action::SetSongDuration(id, dur)) } } } else if let Some(song) = db.get_song(&id) { @@ -165,21 +169,21 @@ impl> Player { self.backend.next(db.playing, load_duration); if self.allow_sending_commands && load_duration { if let Some(dur) = self.backend.current_song_duration() { - db.apply_command(Command::SetSongDuration(id, dur)) + db.apply_action_unchecked_seq(Action::SetSongDuration(id, dur)) } } } else { // only show an error if the user tries to play the song. // otherwise, the error might be spammed. if self.allow_sending_commands && db.playing { - db.apply_command(Command::ErrorInfo( + db.apply_action_unchecked_seq(Action::ErrorInfo( format!("Couldn't load bytes for song {id}"), format!( "Song: {}\nby {:?} on {:?}", song.title, song.artist, song.album ), )); - db.apply_command(Command::NextSong); + db.apply_action_unchecked_seq(Action::NextSong); } self.backend.clear(); } diff --git a/musicdb-lib/src/player/playback_rs.rs b/musicdb-lib/src/player/playback_rs.rs index 4008f21..0b31263 100644 --- a/musicdb-lib/src/player/playback_rs.rs +++ b/musicdb-lib/src/player/playback_rs.rs @@ -2,7 +2,10 @@ use std::{ffi::OsStr, io::Cursor, path::Path, sync::Arc, time::Duration}; use playback_rs::Hint; -use crate::{data::SongId, server::Command}; +use crate::{ + data::SongId, + server::{Action, Command}, +}; use super::PlayerBackend; @@ -52,10 +55,13 @@ impl PlayerBackend for PlayerBackendPlaybackRs { Ok(v) => Some(v), Err(e) => { if let Some(s) = &self.command_sender { - s.send(Command::ErrorInfo( - format!("Couldn't decode song #{id}!"), - format!("Error: {e}"), - )) + s.send( + Action::ErrorInfo( + format!("Couldn't decode song #{id}!"), + format!("Error: {e}"), + ) + .cmd(0xFFu8), + ) .unwrap(); } None @@ -95,18 +101,21 @@ impl PlayerBackend for PlayerBackendPlaybackRs { if let Some(song) = song { if let Err(e) = self.player.play_song_now(song, None) { if let Some(s) = &self.command_sender { - s.send(Command::ErrorInfo( - format!("Couldn't play song #{id}!"), - format!("Error: {e}"), - )) + s.send( + Action::ErrorInfo( + format!("Couldn't play song #{id}!"), + format!("Error: {e}"), + ) + .cmd(0xFFu8), + ) .unwrap(); - s.send(Command::NextSong).unwrap(); + s.send(Action::NextSong.cmd(0xFFu8)).unwrap(); } } else { self.player.set_playing(play); } } else if let Some(s) = &self.command_sender { - s.send(Command::NextSong).unwrap(); + s.send(Action::NextSong.cmd(0xFFu8)).unwrap(); } } } diff --git a/musicdb-lib/src/player/rodio.rs b/musicdb-lib/src/player/rodio.rs index ed7a920..b3f20d8 100644 --- a/musicdb-lib/src/player/rodio.rs +++ b/musicdb-lib/src/player/rodio.rs @@ -3,7 +3,10 @@ use std::{ffi::OsStr, sync::Arc}; use rc_u8_reader::ArcU8Reader; use rodio::{decoder::DecoderError, Decoder, OutputStream, OutputStreamHandle, Sink, Source}; -use crate::{data::SongId, server::Command}; +use crate::{ + data::SongId, + server::{Action, Command}, +}; use super::PlayerBackend; @@ -57,10 +60,13 @@ impl PlayerBackend for PlayerBackendRodio { let decoder = decoder_from_bytes(Arc::clone(&bytes)); if let Err(e) = &decoder { if let Some(s) = &self.command_sender { - s.send(Command::ErrorInfo( - format!("Couldn't decode song #{id}!"), - format!("Error: '{e}'"), - )) + s.send( + Action::ErrorInfo( + format!("Couldn't decode song #{id}!"), + format!("Error: '{e}'"), + ) + .cmd(0xFFu8), + ) .unwrap(); } } diff --git a/musicdb-lib/src/server/mod.rs b/musicdb-lib/src/server/mod.rs index 5f3f652..859eae3 100755 --- a/musicdb-lib/src/server/mod.rs +++ b/musicdb-lib/src/server/mod.rs @@ -26,7 +26,54 @@ use crate::{ }; #[derive(Clone, Debug)] -pub enum Command { +pub struct Command { + /// when sending to the server, this should be the most recent sequence number, + /// or `0xFF` to indicate that the action should be performed regardless of if the sequence number would be up to date or not. + /// when receiving from the server, this contains the most recent sequence number. It is never `0xFF`. + /// used to avoid issues due to desynchronization + pub seq: u8, + pub action: Action, +} +impl Command { + pub fn new(seq: u8, action: Action) -> Self { + Self { seq, action } + } +} +impl Action { + pub fn cmd(self, seq: u8) -> Command { + Command::new(seq, self) + } +} +/// Should be stored in the same lock as the database +pub struct Commander { + seq: u8, +} +impl Commander { + pub fn new(ff: bool) -> Self { + Self { + seq: if ff { 0xFFu8 } else { 0u8 }, + } + } + pub fn inc(&mut self) { + if self.seq < 0xFEu8 { + self.seq += 1; + } else { + self.seq = 0; + } + } + pub fn pack(&self, action: Action) -> Command { + Command::new(self.seq, action) + } + pub fn recv(&mut self, command: Command) -> Action { + self.seq = command.seq; + command.action + } + pub fn seq(&self) -> u8 { + self.seq + } +} +#[derive(Clone, Debug)] +pub enum Action { Resume, Pause, Stop, @@ -269,7 +316,7 @@ pub fn run_server_caching_thread_opt( checkf = true; #[cfg(feature = "playback")] if let Some(player) = &mut player { - player.handle_command(&command); + player.handle_action(&command.action); } database.lock().unwrap().apply_command(command); } @@ -365,6 +412,26 @@ const SUBBYTE_TAG_ARTIST_PROPERTY_SET: u8 = 0b10_100_010; const SUBBYTE_TAG_ARTIST_PROPERTY_UNSET: u8 = 0b10_100_100; impl ToFromBytes for Command { + fn to_bytes(&self, s: &mut T) -> Result<(), std::io::Error> + where + T: Write, + { + s.write_all(&[self.seq])?; + self.action.to_bytes(s) + } + fn from_bytes(s: &mut T) -> Result + where + T: Read, + { + Ok(Self { + seq: ToFromBytes::from_bytes(s)?, + action: Action::from_bytes(s)?, + }) + } +} + +// impl ToFromBytes for Action { +impl Action { fn to_bytes(&self, s: &mut T) -> Result<(), std::io::Error> where T: Write, diff --git a/musicdb-server/Cargo.toml b/musicdb-server/Cargo.toml index 675125e..e5b2ffa 100644 --- a/musicdb-server/Cargo.toml +++ b/musicdb-server/Cargo.toml @@ -16,6 +16,9 @@ rocket = { version = "0.5.0", optional = true } html-escape = { version = "0.2.13", optional = true } [features] -default = ["website", "playback"] +default = ["website", "default-playback"] website = ["dep:tokio", "dep:rocket", "dep:html-escape"] -playback = ["musicdb-lib/playback"] +playback = [] +default-playback = ["playback", "musicdb-lib/default-playback"] +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 690f79a..1e4489d 100755 --- a/musicdb-server/src/main.rs +++ b/musicdb-server/src/main.rs @@ -149,8 +149,8 @@ fn main() { writeln!(con, "main").unwrap(); loop { let cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap(); - use musicdb_lib::server::Command::*; - match &cmd { + use musicdb_lib::server::Action::*; + match &cmd.action { // ignore playback and queue commands Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..) | QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..) diff --git a/musicdb-server/src/web.rs b/musicdb-server/src/web.rs index 406fe4c..9ddeac9 100755 --- a/musicdb-server/src/web.rs +++ b/musicdb-server/src/web.rs @@ -7,7 +7,7 @@ use musicdb_lib::data::database::Database; use musicdb_lib::data::queue::{Queue, QueueContent, QueueFolder}; use musicdb_lib::data::song::Song; use musicdb_lib::data::SongId; -use musicdb_lib::server::Command; +use musicdb_lib::server::{Action, Command}; use rocket::response::content::RawHtml; use rocket::{get, routes, Config, State}; @@ -258,56 +258,62 @@ fn gen_queue_html_impl( fn queue_remove(data: &State, path: &str) { if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() { data.command_sender - .send(Command::QueueRemove(path)) + .send(Action::QueueRemove(path).cmd(0xFFu8)) .unwrap(); } } #[get("/queue-goto/")] fn queue_goto(data: &State, path: &str) { if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() { - data.command_sender.send(Command::QueueGoto(path)).unwrap(); + data.command_sender + .send(Action::QueueGoto(path).cmd(0xFFu8)) + .unwrap(); } } #[get("/play")] fn play(data: &State) { - data.command_sender.send(Command::Resume).unwrap(); + data.command_sender + .send(Action::Resume.cmd(0xFFu8)) + .unwrap(); } #[get("/pause")] fn pause(data: &State) { - data.command_sender.send(Command::Pause).unwrap(); + data.command_sender.send(Action::Pause.cmd(0xFFu8)).unwrap(); } #[get("/stop")] fn stop(data: &State) { - data.command_sender.send(Command::Stop).unwrap(); + data.command_sender.send(Action::Stop.cmd(0xFFu8)).unwrap(); } #[get("/skip")] fn skip(data: &State) { - data.command_sender.send(Command::NextSong).unwrap(); + data.command_sender + .send(Action::NextSong.cmd(0xFFu8)) + .unwrap(); } #[get("/clear-queue")] fn clear_queue(data: &State) { data.command_sender - .send(Command::QueueUpdate( - vec![], - QueueContent::Folder(QueueFolder { - index: 0, - content: vec![], - name: String::new(), - order: None, - }) - .into(), - )) + .send( + Action::QueueUpdate( + vec![], + QueueContent::Folder(QueueFolder { + index: 0, + content: vec![], + name: String::new(), + order: None, + }) + .into(), + ) + .cmd(0xFFu8), + ) .unwrap(); } #[get("/add-song/")] fn add_song(data: &State, id: SongId) { data.command_sender - .send(Command::QueueAdd( - vec![], - vec![QueueContent::Song(id).into()], - )) + .send(Action::QueueAdd(vec![], vec![QueueContent::Song(id).into()]).cmd(0xFFu8)) .unwrap(); }