mirror of
				https://github.com/Dummi26/musicdb.git
				synced 2025-10-31 20:16:14 +01:00 
			
		
		
		
	sequence numbers
This commit is contained in:
		
							parent
							
								
									d02646406d
								
							
						
					
					
						commit
						c3622aca30
					
				| @ -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"] | ||||
|  | ||||
| @ -16,7 +16,7 @@ use musicdb_lib::{ | ||||
|         AlbumId, ArtistId, CoverId, SongId, | ||||
|     }, | ||||
|     load::ToFromBytes, | ||||
|     server::{get, Command}, | ||||
|     server::{get, Action}, | ||||
| }; | ||||
| use speedy2d::{ | ||||
|     color::Color, | ||||
| @ -363,56 +363,57 @@ 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(..) => { | ||||
|             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(..) => { | ||||
|                     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) => { | ||||
|                     Action::ErrorInfo(t, d) => { | ||||
|                         let (t, d) = (t.clone(), d.clone()); | ||||
|                         notif_sender_two | ||||
|                             .send(Box::new(move |_| { | ||||
| @ -442,6 +443,7 @@ impl Gui { | ||||
|                             })) | ||||
|                             .unwrap(); | ||||
|                     } | ||||
|                 } | ||||
|             })), | ||||
|         ); | ||||
|         let no_animations = false; | ||||
| @ -1191,7 +1193,7 @@ pub enum GuiAction { | ||||
|     ShowNotification(Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, 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<dyn FnOnce(&mut Database) -> Vec<Self>>), | ||||
|     SendToServer(Command), | ||||
|     SendToServer(Action), | ||||
|     ContextMenu(Option<(Vec<Box<dyn GuiElem>>)>), | ||||
|     /// 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<GuiEvent> 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))) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @ -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) => { | ||||
|  | ||||
| @ -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), | ||||
|  | ||||
| @ -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<usize>, 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<T: 'static>( | ||||
|     dragged: Dragging, | ||||
|     data: T, | ||||
|     f_queues: impl FnOnce(T, Vec<Queue>) -> Command + 'static, | ||||
|     f_queue_by_path: impl FnOnce(T, Vec<usize>) -> Command + 'static, | ||||
|     f_queues: impl FnOnce(T, Vec<Queue>) -> Action + 'static, | ||||
|     f_queue_by_path: impl FnOnce(T, Vec<usize>) -> Action + 'static, | ||||
| ) -> Vec<GuiAction> { | ||||
|     match dragged { | ||||
|         Dragging::Artist(id) => { | ||||
|  | ||||
| @ -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![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 { | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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"] | ||||
|  | ||||
| @ -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<T: Write>(&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.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)?; | ||||
|         Command::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<usize> = (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<Artist>, albums: Vec<Album>, songs: Vec<Song>) { | ||||
|         self.modified_data(); | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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<T> = playback_rs::PlayerBackendPlaybackRs<T>; | ||||
| #[cfg(feature = "playback-via-rodio")] | ||||
| pub type PlayerBackendFeat<T> = rodio::PlayerBackendRodio<T>; | ||||
| 
 | ||||
| use std::{collections::HashMap, ffi::OsStr, sync::Arc}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     data::{database::Database, song::CachedData, SongId}, | ||||
|     server::Command, | ||||
|     server::Action, | ||||
| }; | ||||
| 
 | ||||
| pub struct Player<T: PlayerBackend<SongCustomData>> { | ||||
| @ -94,11 +98,11 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> { | ||||
|             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<T: PlayerBackend<SongCustomData>> Player<T> { | ||||
|     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<T: PlayerBackend<SongCustomData>> Player<T> { | ||||
|                     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<T: PlayerBackend<SongCustomData>> Player<T> { | ||||
|                         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(); | ||||
|                     } | ||||
|  | ||||
| @ -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<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> { | ||||
|             Ok(v) => Some(v), | ||||
|             Err(e) => { | ||||
|                 if let Some(s) = &self.command_sender { | ||||
|                     s.send(Command::ErrorInfo( | ||||
|                     s.send( | ||||
|                         Action::ErrorInfo( | ||||
|                             format!("Couldn't decode song #{id}!"), | ||||
|                             format!("Error: {e}"), | ||||
|                     )) | ||||
|                         ) | ||||
|                         .cmd(0xFFu8), | ||||
|                     ) | ||||
|                     .unwrap(); | ||||
|                 } | ||||
|                 None | ||||
| @ -95,18 +101,21 @@ impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> { | ||||
|             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( | ||||
|                         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(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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<T> PlayerBackend<T> for PlayerBackendRodio<T> { | ||||
|         let decoder = decoder_from_bytes(Arc::clone(&bytes)); | ||||
|         if let Err(e) = &decoder { | ||||
|             if let Some(s) = &self.command_sender { | ||||
|                 s.send(Command::ErrorInfo( | ||||
|                 s.send( | ||||
|                     Action::ErrorInfo( | ||||
|                         format!("Couldn't decode song #{id}!"), | ||||
|                         format!("Error: '{e}'"), | ||||
|                 )) | ||||
|                     ) | ||||
|                     .cmd(0xFFu8), | ||||
|                 ) | ||||
|                 .unwrap(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -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<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||
|     where | ||||
|         T: Write, | ||||
|     { | ||||
|         s.write_all(&[self.seq])?; | ||||
|         self.action.to_bytes(s) | ||||
|     } | ||||
|     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||
|     where | ||||
|         T: Read, | ||||
|     { | ||||
|         Ok(Self { | ||||
|             seq: ToFromBytes::from_bytes(s)?, | ||||
|             action: Action::from_bytes(s)?, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // impl ToFromBytes for Action {
 | ||||
| impl Action { | ||||
|     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||
|     where | ||||
|         T: Write, | ||||
|  | ||||
| @ -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"] | ||||
|  | ||||
| @ -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(..) | ||||
|  | ||||
| @ -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,37 +258,44 @@ fn gen_queue_html_impl( | ||||
| fn queue_remove(data: &State<Data>, 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/<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(Command::QueueGoto(path)).unwrap(); | ||||
|         data.command_sender | ||||
|             .send(Action::QueueGoto(path).cmd(0xFFu8)) | ||||
|             .unwrap(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[get("/play")] | ||||
| fn play(data: &State<Data>) { | ||||
|     data.command_sender.send(Command::Resume).unwrap(); | ||||
|     data.command_sender | ||||
|         .send(Action::Resume.cmd(0xFFu8)) | ||||
|         .unwrap(); | ||||
| } | ||||
| #[get("/pause")] | ||||
| fn pause(data: &State<Data>) { | ||||
|     data.command_sender.send(Command::Pause).unwrap(); | ||||
|     data.command_sender.send(Action::Pause.cmd(0xFFu8)).unwrap(); | ||||
| } | ||||
| #[get("/stop")] | ||||
| fn stop(data: &State<Data>) { | ||||
|     data.command_sender.send(Command::Stop).unwrap(); | ||||
|     data.command_sender.send(Action::Stop.cmd(0xFFu8)).unwrap(); | ||||
| } | ||||
| #[get("/skip")] | ||||
| fn skip(data: &State<Data>) { | ||||
|     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>) { | ||||
|     data.command_sender | ||||
|         .send(Command::QueueUpdate( | ||||
|         .send( | ||||
|             Action::QueueUpdate( | ||||
|                 vec![], | ||||
|                 QueueContent::Folder(QueueFolder { | ||||
|                     index: 0, | ||||
| @ -297,17 +304,16 @@ fn clear_queue(data: &State<Data>) { | ||||
|                     order: None, | ||||
|                 }) | ||||
|                 .into(), | ||||
|         )) | ||||
|             ) | ||||
|             .cmd(0xFFu8), | ||||
|         ) | ||||
|         .unwrap(); | ||||
| } | ||||
| 
 | ||||
| #[get("/add-song/<id>")] | ||||
| fn add_song(data: &State<Data>, 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(); | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Mark
						Mark