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 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| musicdb-lib = { path = "../musicdb-lib" } | musicdb-lib = { path = "../musicdb-lib", default-features = false } | ||||||
| clap = { version = "4.4.6", features = ["derive"] } | clap = { version = "4.4.6", features = ["derive"] } | ||||||
| directories = "5.0.1" | directories = "5.0.1" | ||||||
| regex = "1.9.3" | regex = "1.9.3" | ||||||
| @ -16,7 +16,7 @@ musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true } | |||||||
| uianimator = "0.1.1" | uianimator = "0.1.1" | ||||||
| 
 | 
 | ||||||
| [features] | [features] | ||||||
| default = ["gui", "playback", "merscfg"] | default = ["gui", "default-playback"] | ||||||
| # gui: | # gui: | ||||||
| #   enables the gui modes | #   enables the gui modes | ||||||
| # merscfg: | # merscfg: | ||||||
| @ -26,6 +26,9 @@ default = ["gui", "playback", "merscfg"] | |||||||
| # playback: | # playback: | ||||||
| #   enables syncplayer modes, where the client mirrors the server's playback | #   enables syncplayer modes, where the client mirrors the server's playback | ||||||
| gui = ["speedy2d"] | gui = ["speedy2d"] | ||||||
| merscfg = ["mers", "speedy2d"] | merscfg = ["mers", "gui"] | ||||||
| mers = ["musicdb-mers"] | 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, |         AlbumId, ArtistId, CoverId, SongId, | ||||||
|     }, |     }, | ||||||
|     load::ToFromBytes, |     load::ToFromBytes, | ||||||
|     server::{get, Command}, |     server::{get, Action}, | ||||||
| }; | }; | ||||||
| use speedy2d::{ | use speedy2d::{ | ||||||
|     color::Color, |     color::Color, | ||||||
| @ -363,56 +363,57 @@ impl Gui { | |||||||
|             Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"), |             Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"), | ||||||
|         } |         } | ||||||
|         database.lock().unwrap().update_endpoints.push( |         database.lock().unwrap().update_endpoints.push( | ||||||
|             musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { |             musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| { | ||||||
|                 Command::Resume |                 match &cmd.action { | ||||||
|                 | Command::Pause |                     Action::Resume | ||||||
|                 | Command::Stop |                     | Action::Pause | ||||||
|                 | Command::Save |                     | Action::Stop | ||||||
|                 | Command::InitComplete => {} |                     | Action::Save | ||||||
|                 Command::NextSong |                     | Action::InitComplete => {} | ||||||
|                 | Command::QueueUpdate(..) |                     Action::NextSong | ||||||
|                 | Command::QueueAdd(..) |                     | Action::QueueUpdate(..) | ||||||
|                 | Command::QueueInsert(..) |                     | Action::QueueAdd(..) | ||||||
|                 | Command::QueueRemove(..) |                     | Action::QueueInsert(..) | ||||||
|                 | Command::QueueMove(..) |                     | Action::QueueRemove(..) | ||||||
|                 | Command::QueueMoveInto(..) |                     | Action::QueueMove(..) | ||||||
|                 | Command::QueueGoto(..) |                     | Action::QueueMoveInto(..) | ||||||
|                 | Command::QueueShuffle(..) |                     | Action::QueueGoto(..) | ||||||
|                 | Command::QueueSetShuffle(..) |                     | Action::QueueShuffle(..) | ||||||
|                 | Command::QueueUnshuffle(..) => { |                     | Action::QueueSetShuffle(..) | ||||||
|  |                     | Action::QueueUnshuffle(..) => { | ||||||
|                         if let Some(s) = &*event_sender_arc.lock().unwrap() { |                         if let Some(s) = &*event_sender_arc.lock().unwrap() { | ||||||
|                             _ = s.send_event(GuiEvent::UpdatedQueue); |                             _ = s.send_event(GuiEvent::UpdatedQueue); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 Command::SyncDatabase(..) |                     Action::SyncDatabase(..) | ||||||
|                 | Command::AddSong(_) |                     | Action::AddSong(_) | ||||||
|                 | Command::AddAlbum(_) |                     | Action::AddAlbum(_) | ||||||
|                 | Command::AddArtist(_) |                     | Action::AddArtist(_) | ||||||
|                 | Command::AddCover(_) |                     | Action::AddCover(_) | ||||||
|                 | Command::ModifySong(_) |                     | Action::ModifySong(_) | ||||||
|                 | Command::ModifyAlbum(_) |                     | Action::ModifyAlbum(_) | ||||||
|                 | Command::ModifyArtist(_) |                     | Action::ModifyArtist(_) | ||||||
|                 | Command::RemoveSong(_) |                     | Action::RemoveSong(_) | ||||||
|                 | Command::RemoveAlbum(_) |                     | Action::RemoveAlbum(_) | ||||||
|                 | Command::RemoveArtist(_) |                     | Action::RemoveArtist(_) | ||||||
|                 | Command::TagSongFlagSet(..) |                     | Action::TagSongFlagSet(..) | ||||||
|                 | Command::TagSongFlagUnset(..) |                     | Action::TagSongFlagUnset(..) | ||||||
|                 | Command::TagAlbumFlagSet(..) |                     | Action::TagAlbumFlagSet(..) | ||||||
|                 | Command::TagAlbumFlagUnset(..) |                     | Action::TagAlbumFlagUnset(..) | ||||||
|                 | Command::TagArtistFlagSet(..) |                     | Action::TagArtistFlagSet(..) | ||||||
|                 | Command::TagArtistFlagUnset(..) |                     | Action::TagArtistFlagUnset(..) | ||||||
|                 | Command::TagSongPropertySet(..) |                     | Action::TagSongPropertySet(..) | ||||||
|                 | Command::TagSongPropertyUnset(..) |                     | Action::TagSongPropertyUnset(..) | ||||||
|                 | Command::TagAlbumPropertySet(..) |                     | Action::TagAlbumPropertySet(..) | ||||||
|                 | Command::TagAlbumPropertyUnset(..) |                     | Action::TagAlbumPropertyUnset(..) | ||||||
|                 | Command::TagArtistPropertySet(..) |                     | Action::TagArtistPropertySet(..) | ||||||
|                 | Command::TagArtistPropertyUnset(..) |                     | Action::TagArtistPropertyUnset(..) | ||||||
|                 | Command::SetSongDuration(..) => { |                     | Action::SetSongDuration(..) => { | ||||||
|                         if let Some(s) = &*event_sender_arc.lock().unwrap() { |                         if let Some(s) = &*event_sender_arc.lock().unwrap() { | ||||||
|                             _ = s.send_event(GuiEvent::UpdatedLibrary); |                             _ = s.send_event(GuiEvent::UpdatedLibrary); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 Command::ErrorInfo(t, d) => { |                     Action::ErrorInfo(t, d) => { | ||||||
|                         let (t, d) = (t.clone(), d.clone()); |                         let (t, d) = (t.clone(), d.clone()); | ||||||
|                         notif_sender_two |                         notif_sender_two | ||||||
|                             .send(Box::new(move |_| { |                             .send(Box::new(move |_| { | ||||||
| @ -442,6 +443,7 @@ impl Gui { | |||||||
|                             })) |                             })) | ||||||
|                             .unwrap(); |                             .unwrap(); | ||||||
|                     } |                     } | ||||||
|  |                 } | ||||||
|             })), |             })), | ||||||
|         ); |         ); | ||||||
|         let no_animations = false; |         let no_animations = false; | ||||||
| @ -1191,7 +1193,7 @@ pub enum GuiAction { | |||||||
|     ShowNotification(Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>), |     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 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>>), |     Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>), | ||||||
|     SendToServer(Command), |     SendToServer(Action), | ||||||
|     ContextMenu(Option<(Vec<Box<dyn GuiElem>>)>), |     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.
 |     /// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one.
 | ||||||
|     ResetKeyboardFocus, |     ResetKeyboardFocus, | ||||||
| @ -1304,10 +1306,17 @@ impl Gui { | |||||||
|                     self.keybinds.insert(bind, action.with_priority(priority)); |                     self.keybinds.insert(bind, action.with_priority(priority)); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             GuiAction::SendToServer(cmd) => { |             GuiAction::SendToServer(action) => { | ||||||
|                 #[cfg(debug_assertions)] |                 #[cfg(debug_assertions)] | ||||||
|                 eprintln!("[DEBUG] Sending command to server: {cmd:?}"); |                 eprintln!("[DEBUG] Sending command to server: {action:?}"); | ||||||
|                 if let Err(e) = cmd.to_bytes(&mut self.connection) { |                 if let Err(e) = self | ||||||
|  |                     .database | ||||||
|  |                     .lock() | ||||||
|  |                     .unwrap() | ||||||
|  |                     .seq | ||||||
|  |                     .pack(action) | ||||||
|  |                     .to_bytes(&mut self.connection) | ||||||
|  |                 { | ||||||
|                     eprintln!("Error sending command to server: {e}"); |                     eprintln!("Error sending command to server: {e}"); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -1551,7 +1560,7 @@ impl WindowHandler<GuiEvent> for Gui { | |||||||
|                     | Dragging::Queue(Ok(_)) |                     | Dragging::Queue(Ok(_)) | ||||||
|                     | Dragging::Queues(_) => (), |                     | Dragging::Queues(_) => (), | ||||||
|                     Dragging::Queue(Err(path)) => { |                     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::{ | use musicdb_lib::{ | ||||||
|     data::{song::Song, ArtistId}, |     data::{song::Song, ArtistId}, | ||||||
|     server::Command, |     server::Action, | ||||||
| }; | }; | ||||||
| use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; | use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; | ||||||
| 
 | 
 | ||||||
| @ -189,7 +189,7 @@ impl GuiElem for EditorForSongs { | |||||||
|                                 song.album = None; |                                 song.album = None; | ||||||
|                             } |                             } | ||||||
|                             info.actions |                             info.actions | ||||||
|                                 .push(GuiAction::SendToServer(Command::ModifySong(song))); |                                 .push(GuiAction::SendToServer(Action::ModifySong(song))); | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                     Event::SetArtist(name, id) => { |                     Event::SetArtist(name, id) => { | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| use std::sync::{atomic::AtomicBool, Arc}; | 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 speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
| @ -28,9 +28,9 @@ impl PlayPause { | |||||||
|                             if let Some(song) = db.get_song(song_id) { |                             if let Some(song) = db.get_song(song_id) { | ||||||
|                                 vec![GuiAction::SendToServer( |                                 vec![GuiAction::SendToServer( | ||||||
|                                     if song.general.tags.iter().any(|v| v == "Fav") { |                                     if song.general.tags.iter().any(|v| v == "Fav") { | ||||||
|                                         Command::TagSongFlagUnset(*song_id, "Fav".to_owned()) |                                         Action::TagSongFlagUnset(*song_id, "Fav".to_owned()) | ||||||
|                                     } else { |                                     } else { | ||||||
|                                         Command::TagSongFlagSet(*song_id, "Fav".to_owned()) |                                         Action::TagSongFlagSet(*song_id, "Fav".to_owned()) | ||||||
|                                     }, |                                     }, | ||||||
|                                 )] |                                 )] | ||||||
|                             } else { |                             } else { | ||||||
| @ -48,7 +48,7 @@ impl PlayPause { | |||||||
|             ), |             ), | ||||||
|             to_zero: Button::new( |             to_zero: Button::new( | ||||||
|                 GuiElemCfg::at(Rectangle::from_tuples((0.26, 0.01), (0.49, 0.99))), |                 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( |                 [Panel::with_background( | ||||||
|                     GuiElemCfg::at(Rectangle::from_tuples((0.2, 0.2), (0.8, 0.8))), |                     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))), |                 GuiElemCfg::at(Rectangle::from_tuples((0.51, 0.01), (0.74, 0.99))), | ||||||
|                 |btn| { |                 |btn| { | ||||||
|                     vec![GuiAction::SendToServer(if btn.children[0].is_playing { |                     vec![GuiAction::SendToServer(if btn.children[0].is_playing { | ||||||
|                         Command::Pause |                         Action::Pause | ||||||
|                     } else { |                     } else { | ||||||
|                         Command::Resume |                         Action::Resume | ||||||
|                     })] |                     })] | ||||||
|                 }, |                 }, | ||||||
|                 [PlayPauseDisplay::new(GuiElemCfg::at( |                 [PlayPauseDisplay::new(GuiElemCfg::at( | ||||||
| @ -70,7 +70,7 @@ impl PlayPause { | |||||||
|             ), |             ), | ||||||
|             to_end: Button::new( |             to_end: Button::new( | ||||||
|                 GuiElemCfg::at(Rectangle::from_tuples((0.76, 0.01), (0.99, 0.99))), |                 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( |                 [NextSongShape::new(GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|                     (0.2, 0.2), |                     (0.2, 0.2), | ||||||
|                     (0.8, 0.8), |                     (0.8, 0.8), | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ use musicdb_lib::{ | |||||||
|         song::Song, |         song::Song, | ||||||
|         AlbumId, ArtistId, |         AlbumId, ArtistId, | ||||||
|     }, |     }, | ||||||
|     server::Command, |     server::Action, | ||||||
| }; | }; | ||||||
| use speedy2d::{ | use speedy2d::{ | ||||||
|     color::Color, |     color::Color, | ||||||
| @ -404,8 +404,8 @@ impl GuiElem for QueueEmptySpaceDragHandler { | |||||||
|         dragged_add_to_queue( |         dragged_add_to_queue( | ||||||
|             dragged, |             dragged, | ||||||
|             (), |             (), | ||||||
|             |_, q| Command::QueueAdd(vec![], q), |             |_, q| Action::QueueAdd(vec![], q), | ||||||
|             |_, q| Command::QueueMoveInto(q, vec![]), |             |_, q| Action::QueueMoveInto(q, vec![]), | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -563,7 +563,7 @@ impl GuiElem for QueueSong { | |||||||
|         if self.mouse && button == MouseButton::Left { |         if self.mouse && button == MouseButton::Left { | ||||||
|             self.mouse = false; |             self.mouse = false; | ||||||
|             if e.take() && !self.always_copy { |             if e.take() && !self.always_copy { | ||||||
|                 vec![GuiAction::SendToServer(Command::QueueGoto( |                 vec![GuiAction::SendToServer(Action::QueueGoto( | ||||||
|                     self.path.clone(), |                     self.path.clone(), | ||||||
|                 ))] |                 ))] | ||||||
|             } else { |             } else { | ||||||
| @ -631,9 +631,9 @@ impl GuiElem for QueueSong { | |||||||
|                 self.path.clone(), |                 self.path.clone(), | ||||||
|                 move |mut p: Vec<usize>, q| { |                 move |mut p: Vec<usize>, q| { | ||||||
|                     if let Some(j) = p.pop() { |                     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 { |                     } else { | ||||||
|                         Command::QueueAdd(p, q) |                         Action::QueueAdd(p, q) | ||||||
|                     } |                     } | ||||||
|                 }, |                 }, | ||||||
|                 move |mut p, q| { |                 move |mut p, q| { | ||||||
| @ -642,7 +642,7 @@ impl GuiElem for QueueSong { | |||||||
|                             *l += 1; |                             *l += 1; | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                     Command::QueueMove(q, p) |                     Action::QueueMove(q, p) | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|         } else { |         } else { | ||||||
| @ -787,9 +787,9 @@ impl GuiElem for QueueFolder { | |||||||
|             //     Panel::with_background(GuiElemCfg::default(), (), Color::DARK_GRAY),
 |             //     Panel::with_background(GuiElemCfg::default(), (), Color::DARK_GRAY),
 | ||||||
|             // )]))];
 |             // )]))];
 | ||||||
|             return vec![GuiAction::SendToServer(if self.queue.order.is_some() { |             return vec![GuiAction::SendToServer(if self.queue.order.is_some() { | ||||||
|                 Command::QueueUnshuffle(self.path.clone()) |                 Action::QueueUnshuffle(self.path.clone()) | ||||||
|             } else { |             } else { | ||||||
|                 Command::QueueShuffle(self.path.clone()) |                 Action::QueueShuffle(self.path.clone()) | ||||||
|             })]; |             })]; | ||||||
|         } |         } | ||||||
|         vec![] |         vec![] | ||||||
| @ -798,7 +798,7 @@ impl GuiElem for QueueFolder { | |||||||
|         if self.mouse && button == MouseButton::Left { |         if self.mouse && button == MouseButton::Left { | ||||||
|             self.mouse = false; |             self.mouse = false; | ||||||
|             if e.take() && !self.always_copy { |             if e.take() && !self.always_copy { | ||||||
|                 vec![GuiAction::SendToServer(Command::QueueGoto( |                 vec![GuiAction::SendToServer(Action::QueueGoto( | ||||||
|                     self.path.clone(), |                     self.path.clone(), | ||||||
|                 ))] |                 ))] | ||||||
|             } else { |             } else { | ||||||
| @ -826,8 +826,8 @@ impl GuiElem for QueueFolder { | |||||||
|                 dragged_add_to_queue( |                 dragged_add_to_queue( | ||||||
|                     dragged, |                     dragged, | ||||||
|                     self.path.clone(), |                     self.path.clone(), | ||||||
|                     |p, q| Command::QueueAdd(p, q), |                     |p, q| Action::QueueAdd(p, q), | ||||||
|                     |p, q| Command::QueueMoveInto(q, p), |                     |p, q| Action::QueueMoveInto(q, p), | ||||||
|                 ) |                 ) | ||||||
|             } else { |             } else { | ||||||
|                 dragged_add_to_queue( |                 dragged_add_to_queue( | ||||||
| @ -835,9 +835,9 @@ impl GuiElem for QueueFolder { | |||||||
|                     self.path.clone(), |                     self.path.clone(), | ||||||
|                     |mut p, q| { |                     |mut p, q| { | ||||||
|                         let j = p.pop().unwrap_or(0); |                         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 { |         } else { | ||||||
| @ -903,10 +903,10 @@ impl GuiElem for QueueIndentEnd { | |||||||
|         dragged_add_to_queue( |         dragged_add_to_queue( | ||||||
|             dragged, |             dragged, | ||||||
|             self.path_insert.clone(), |             self.path_insert.clone(), | ||||||
|             |(p, j), q| Command::QueueInsert(p, j, q), |             |(p, j), q| Action::QueueInsert(p, j, q), | ||||||
|             |(mut p, j), q| { |             |(mut p, j), q| { | ||||||
|                 p.push(j); |                 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 { |         if self.mouse && button == MouseButton::Left { | ||||||
|             self.mouse = false; |             self.mouse = false; | ||||||
|             if e.take() && !self.always_copy { |             if e.take() && !self.always_copy { | ||||||
|                 vec![GuiAction::SendToServer(Command::QueueGoto( |                 vec![GuiAction::SendToServer(Action::QueueGoto( | ||||||
|                     self.path.clone(), |                     self.path.clone(), | ||||||
|                 ))] |                 ))] | ||||||
|             } else { |             } else { | ||||||
| @ -1066,8 +1066,8 @@ impl GuiElem for QueueLoop { | |||||||
|             dragged_add_to_queue( |             dragged_add_to_queue( | ||||||
|                 dragged, |                 dragged, | ||||||
|                 p, |                 p, | ||||||
|                 |p, q| Command::QueueAdd(p, q), |                 |p, q| Action::QueueAdd(p, q), | ||||||
|                 |p, q| Command::QueueMoveInto(q, p), |                 |p, q| Action::QueueMoveInto(q, p), | ||||||
|             ) |             ) | ||||||
|         } else { |         } else { | ||||||
|             vec![] |             vec![] | ||||||
| @ -1078,8 +1078,8 @@ impl GuiElem for QueueLoop { | |||||||
| fn dragged_add_to_queue<T: 'static>( | fn dragged_add_to_queue<T: 'static>( | ||||||
|     dragged: Dragging, |     dragged: Dragging, | ||||||
|     data: T, |     data: T, | ||||||
|     f_queues: impl FnOnce(T, Vec<Queue>) -> Command + 'static, |     f_queues: impl FnOnce(T, Vec<Queue>) -> Action + 'static, | ||||||
|     f_queue_by_path: impl FnOnce(T, Vec<usize>) -> Command + 'static, |     f_queue_by_path: impl FnOnce(T, Vec<usize>) -> Action + 'static, | ||||||
| ) -> Vec<GuiAction> { | ) -> Vec<GuiAction> { | ||||||
|     match dragged { |     match dragged { | ||||||
|         Dragging::Artist(id) => { |         Dragging::Artist(id) => { | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ use std::time::Instant; | |||||||
| 
 | 
 | ||||||
| use musicdb_lib::{ | use musicdb_lib::{ | ||||||
|     data::queue::{QueueContent, QueueFolder}, |     data::queue::{QueueContent, QueueFolder}, | ||||||
|     server::Command, |     server::Action, | ||||||
| }; | }; | ||||||
| use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D}; | use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D}; | ||||||
| use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator}; | use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator}; | ||||||
| @ -120,15 +120,13 @@ impl GuiScreen { | |||||||
|                     button_clear_queue: Button::new( |                     button_clear_queue: Button::new( | ||||||
|                         GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))), |                         GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))), | ||||||
|                         |_| { |                         |_| { | ||||||
|                             vec![GuiAction::SendToServer( |                             vec![GuiAction::SendToServer(Action::QueueUpdate( | ||||||
|                                 musicdb_lib::server::Command::QueueUpdate( |  | ||||||
|                                 vec![], |                                 vec![], | ||||||
|                                 musicdb_lib::data::queue::QueueContent::Folder( |                                 musicdb_lib::data::queue::QueueContent::Folder( | ||||||
|                                     musicdb_lib::data::queue::QueueFolder::default(), |                                     musicdb_lib::data::queue::QueueFolder::default(), | ||||||
|                                 ) |                                 ) | ||||||
|                                 .into(), |                                 .into(), | ||||||
|                                 ), |                             ))] | ||||||
|                             )] |  | ||||||
|                         }, |                         }, | ||||||
|                         [Label::new( |                         [Label::new( | ||||||
|                             GuiElemCfg::default(), |                             GuiElemCfg::default(), | ||||||
| @ -305,9 +303,9 @@ impl GuiElem for GuiScreen { | |||||||
|         if key == ' ' && !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && e.take() { |         if key == ' ' && !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && e.take() { | ||||||
|             vec![GuiAction::Build(Box::new(|db| { |             vec![GuiAction::Build(Box::new(|db| { | ||||||
|                 vec![GuiAction::SendToServer(if db.playing { |                 vec![GuiAction::SendToServer(if db.playing { | ||||||
|                     Command::Pause |                     Action::Pause | ||||||
|                 } else { |                 } else { | ||||||
|                     Command::Resume |                     Action::Resume | ||||||
|                 })] |                 })] | ||||||
|             }))] |             }))] | ||||||
|         } else { |         } else { | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| use std::sync::{atomic::AtomicBool, Arc, Mutex}; | use std::sync::{atomic::AtomicBool, Arc, Mutex}; | ||||||
| 
 | 
 | ||||||
| use musicdb_lib::server::Command; | use musicdb_lib::server::Action; | ||||||
| use speedy2d::{ | use speedy2d::{ | ||||||
|     color::Color, |     color::Color, | ||||||
|     dimen::Vec2, |     dimen::Vec2, | ||||||
| @ -450,7 +450,7 @@ impl SettingsContent { | |||||||
|             ), |             ), | ||||||
|             save_button: Button::new( |             save_button: Button::new( | ||||||
|                 GuiElemCfg::default(), |                 GuiElemCfg::default(), | ||||||
|                 |_| vec![GuiAction::SendToServer(Command::Save)], |                 |_| vec![GuiAction::SendToServer(Action::Save)], | ||||||
|                 [Label::new( |                 [Label::new( | ||||||
|                     GuiElemCfg::default(), |                     GuiElemCfg::default(), | ||||||
|                     "Server: Save Changes".to_string(), |                     "Server: Save Changes".to_string(), | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | // #![allow(unused)]
 | ||||||
|  | 
 | ||||||
| use std::{ | use std::{ | ||||||
|     io::{BufReader, Write}, |     io::{BufReader, Write}, | ||||||
|     net::{SocketAddr, TcpStream}, |     net::{SocketAddr, TcpStream}, | ||||||
| @ -12,7 +14,7 @@ use gui::GuiEvent; | |||||||
| #[cfg(feature = "playback")] | #[cfg(feature = "playback")] | ||||||
| use musicdb_lib::data::cache_manager::CacheManager; | use musicdb_lib::data::cache_manager::CacheManager; | ||||||
| #[cfg(feature = "playback")] | #[cfg(feature = "playback")] | ||||||
| use musicdb_lib::player::{playback_rs::PlayerBackendPlaybackRs, Player}; | use musicdb_lib::player::{Player, PlayerBackendFeat}; | ||||||
| use musicdb_lib::{ | use musicdb_lib::{ | ||||||
|     data::{ |     data::{ | ||||||
|         database::{ClientIo, Database}, |         database::{ClientIo, Database}, | ||||||
| @ -152,7 +154,7 @@ fn main() { | |||||||
|                 cm.set_cache_songs_count(20); |                 cm.set_cache_songs_count(20); | ||||||
|                 cache_manager = Some(cm); |                 cache_manager = Some(cm); | ||||||
|                 Some(Player::new_client( |                 Some(Player::new_client( | ||||||
|                     PlayerBackendPlaybackRs::new_without_command_sending().unwrap(), |                     PlayerBackendFeat::new_without_command_sending().unwrap(), | ||||||
|                 )) |                 )) | ||||||
|             } else { |             } else { | ||||||
|                 None |                 None | ||||||
| @ -186,21 +188,22 @@ fn main() { | |||||||
|                 ))); |                 ))); | ||||||
|             } |             } | ||||||
|             loop { |             loop { | ||||||
|                 let update = Command::from_bytes(&mut con).unwrap(); |                 let command = Command::from_bytes(&mut con).unwrap(); | ||||||
|                 let mut db = database.lock().unwrap(); |                 let mut db = database.lock().unwrap(); | ||||||
|  |                 let action = db.seq.recv(command); | ||||||
|                 #[cfg(feature = "playback")] |                 #[cfg(feature = "playback")] | ||||||
|                 if let Some(player) = &mut player { |                 if let Some(player) = &mut player { | ||||||
|                     player.handle_command(&update); |                     player.handle_action(&action); | ||||||
|                 } |                 } | ||||||
|                 #[allow(unused_labels)] |                 #[allow(unused_labels)] | ||||||
|                 'feature_if: { |                 'feature_if: { | ||||||
|                     #[cfg(any(feature = "mers", feature = "merscfg"))] |                     #[cfg(any(feature = "mers", feature = "merscfg"))] | ||||||
|                     if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() { |                     if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() { | ||||||
|                         db.apply_command(update.clone()); |                         db.apply_command(action.clone()); | ||||||
|                         action(update); |                         action(action); | ||||||
|                         break 'feature_if; |                         break 'feature_if; | ||||||
|                     } |                     } | ||||||
|                     db.apply_command(update); |                     db.apply_action_unchecked_seq(action); | ||||||
|                 } |                 } | ||||||
|                 #[cfg(feature = "playback")] |                 #[cfg(feature = "playback")] | ||||||
|                 if let Some(player) = &mut player { |                 if let Some(player) = &mut player { | ||||||
|  | |||||||
| @ -13,8 +13,9 @@ rodio = { version = "0.20.1", optional = true } | |||||||
| sysinfo = "0.30.12" | sysinfo = "0.30.12" | ||||||
| 
 | 
 | ||||||
| [features] | [features] | ||||||
| # default = ["playback"] | default = [] | ||||||
| playback = ["playback-via-playback-rs"] | playback = [] | ||||||
| # playback = ["playback-via-rodio"] | default-playback = ["playback-via-playback-rs"] | ||||||
| playback-via-playback-rs = ["dep:playback-rs"] | # default-playback = ["playback-via-rodio"] | ||||||
| playback-via-rodio = ["dep: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 colorize::AnsiColor; | ||||||
| use rand::thread_rng; | use rand::thread_rng; | ||||||
| 
 | 
 | ||||||
| use crate::{load::ToFromBytes, server::Command}; | use crate::{ | ||||||
|  |     load::ToFromBytes, | ||||||
|  |     server::{Action, Command, Commander}, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| use super::{ | use super::{ | ||||||
|     album::Album, |     album::Album, | ||||||
| @ -22,6 +25,7 @@ use super::{ | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| pub struct Database { | pub struct Database { | ||||||
|  |     pub seq: Commander, | ||||||
|     /// the directory that contains the dbfile, backups, statistics, ...
 |     /// the directory that contains the dbfile, backups, statistics, ...
 | ||||||
|     pub db_dir: PathBuf, |     pub db_dir: PathBuf, | ||||||
|     /// the path to the file used to save/load the data. empty if database is in client mode.
 |     /// 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> { |     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...
 |         // 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.artists().iter().map(|v| v.1.clone()).collect(), | ||||||
|                 self.albums().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(), |                 self.songs().iter().map(|v| v.1.clone()).collect(), | ||||||
|         ) |             )) | ||||||
|  |             .to_bytes(con)?; | ||||||
|  |         self.seq | ||||||
|  |             .pack(Action::QueueUpdate(vec![], self.queue.clone())) | ||||||
|             .to_bytes(con)?; |             .to_bytes(con)?; | ||||||
|         Command::QueueUpdate(vec![], self.queue.clone()).to_bytes(con)?; |  | ||||||
|         if self.playing { |         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.
 |         // 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.
 |         // 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.
 |         // 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).
 |         // we just need to handle commands (receive from the connection).
 | ||||||
|         Ok(()) |         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 !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,
 |                 // 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.
 |                 // BUT only the server can set the Title of the ErrorInfo.
 | ||||||
|                 t.clear(); |                 t.clear(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         // some commands shouldn't be broadcast. these will broadcast a different command in their specific implementation.
 |         // some commands shouldn't be broadcast. these will broadcast a different command in their specific implementation.
 | ||||||
|         match &command { |         match &action { | ||||||
|             // Will broadcast `QueueSetShuffle`
 |             // 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
 |             // 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 { |         match action { | ||||||
|             Command::Resume => self.playing = true, |             Action::Resume => self.playing = true, | ||||||
|             Command::Pause => self.playing = false, |             Action::Pause => self.playing = false, | ||||||
|             Command::Stop => self.playing = false, |             Action::Stop => self.playing = false, | ||||||
|             Command::NextSong => { |             Action::NextSong => { | ||||||
|                 if !Queue::advance_index_db(self) { |                 if !Queue::advance_index_db(self) { | ||||||
|                     // end of queue
 |                     // end of queue
 | ||||||
|                     self.apply_command(Command::Pause); |                     self.apply_action_unchecked_seq(Action::Pause); | ||||||
|                     self.queue.init(); |                     self.queue.init(); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::Save => { |             Action::Save => { | ||||||
|                 if let Err(e) = self.save_database(None) { |                 if let Err(e) = self.save_database(None) { | ||||||
|                     eprintln!("[{}] Couldn't save: {e}", "ERR!".red()); |                     eprintln!("[{}] Couldn't save: {e}", "ERR!".red()); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::SyncDatabase(a, b, c) => self.sync(a, b, c), |             Action::SyncDatabase(a, b, c) => self.sync(a, b, c), | ||||||
|             Command::QueueUpdate(index, new_data) => { |             Action::QueueUpdate(index, new_data) => { | ||||||
|                 if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { |                 if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { | ||||||
|                     *v = new_data; |                     *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) { |                 if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { | ||||||
|                     v.add_to_end(new_data, false); |                     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) { |                 if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { | ||||||
|                     v.insert(new_data, pos, false); |                     v.insert(new_data, pos, false); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::QueueRemove(index) => { |             Action::QueueRemove(index) => { | ||||||
|                 self.queue.remove_by_index(&index, 0); |                 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) { |                 if index_to.len() == 0 || index_to.starts_with(&index_from) { | ||||||
|                     break 'queue_move; |                     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) { |                 if parent_to.starts_with(&index_from) { | ||||||
|                     break 'queue_move_into; |                     break 'queue_move_into; | ||||||
|                 } |                 } | ||||||
| @ -628,8 +650,8 @@ impl Database { | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::QueueGoto(index) => Queue::set_index_db(self, &index), |             Action::QueueGoto(index) => Queue::set_index_db(self, &index), | ||||||
|             Command::QueueShuffle(path) => { |             Action::QueueShuffle(path) => { | ||||||
|                 if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) { |                 if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) { | ||||||
|                     if let QueueContent::Folder(QueueFolder { |                     if let QueueContent::Folder(QueueFolder { | ||||||
|                         index: _, |                         index: _, | ||||||
| @ -640,7 +662,7 @@ impl Database { | |||||||
|                     { |                     { | ||||||
|                         let mut ord: Vec<usize> = (0..content.len()).collect(); |                         let mut ord: Vec<usize> = (0..content.len()).collect(); | ||||||
|                         ord.shuffle(&mut thread_rng()); |                         ord.shuffle(&mut thread_rng()); | ||||||
|                         self.apply_command(Command::QueueSetShuffle(path, ord)); |                         self.apply_action_unchecked_seq(Action::QueueSetShuffle(path, ord)); | ||||||
|                     } else { |                     } else { | ||||||
|                         eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!"); |                         eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!"); | ||||||
|                     } |                     } | ||||||
| @ -648,7 +670,7 @@ impl Database { | |||||||
|                     eprintln!("(QueueShuffle) No QueueElement at {path:?}"); |                     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 Some(elem) = self.queue.get_item_at_index_mut(&path, 0) { | ||||||
|                     if let QueueContent::Folder(QueueFolder { |                     if let QueueContent::Folder(QueueFolder { | ||||||
|                         index, |                         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 Some(elem) = self.queue.get_item_at_index_mut(&path, 0) { | ||||||
|                     if let QueueContent::Folder(QueueFolder { |                     if let QueueContent::Folder(QueueFolder { | ||||||
|                         index, |                         index, | ||||||
| @ -697,77 +719,77 @@ impl Database { | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::AddSong(song) => { |             Action::AddSong(song) => { | ||||||
|                 self.add_song_new(song); |                 self.add_song_new(song); | ||||||
|             } |             } | ||||||
|             Command::AddAlbum(album) => { |             Action::AddAlbum(album) => { | ||||||
|                 self.add_album_new(album); |                 self.add_album_new(album); | ||||||
|             } |             } | ||||||
|             Command::AddArtist(artist) => { |             Action::AddArtist(artist) => { | ||||||
|                 self.add_artist_new(artist); |                 self.add_artist_new(artist); | ||||||
|             } |             } | ||||||
|             Command::AddCover(cover) => _ = self.add_cover_new(cover), |             Action::AddCover(cover) => _ = self.add_cover_new(cover), | ||||||
|             Command::ModifySong(song) => { |             Action::ModifySong(song) => { | ||||||
|                 _ = self.update_song(song); |                 _ = self.update_song(song); | ||||||
|             } |             } | ||||||
|             Command::ModifyAlbum(album) => { |             Action::ModifyAlbum(album) => { | ||||||
|                 _ = self.update_album(album); |                 _ = self.update_album(album); | ||||||
|             } |             } | ||||||
|             Command::ModifyArtist(artist) => { |             Action::ModifyArtist(artist) => { | ||||||
|                 _ = self.update_artist(artist); |                 _ = self.update_artist(artist); | ||||||
|             } |             } | ||||||
|             Command::RemoveSong(song) => { |             Action::RemoveSong(song) => { | ||||||
|                 _ = self.remove_song(song); |                 _ = self.remove_song(song); | ||||||
|             } |             } | ||||||
|             Command::RemoveAlbum(album) => { |             Action::RemoveAlbum(album) => { | ||||||
|                 _ = self.remove_album(album); |                 _ = self.remove_album(album); | ||||||
|             } |             } | ||||||
|             Command::RemoveArtist(artist) => { |             Action::RemoveArtist(artist) => { | ||||||
|                 _ = self.remove_artist(artist); |                 _ = self.remove_artist(artist); | ||||||
|             } |             } | ||||||
|             Command::TagSongFlagSet(id, tag) => { |             Action::TagSongFlagSet(id, tag) => { | ||||||
|                 if let Some(v) = self.get_song_mut(&id) { |                 if let Some(v) = self.get_song_mut(&id) { | ||||||
|                     if !v.general.tags.contains(&tag) { |                     if !v.general.tags.contains(&tag) { | ||||||
|                         v.general.tags.push(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(v) = self.get_song_mut(&id) { | ||||||
|                     if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { |                     if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { | ||||||
|                         v.general.tags.remove(i); |                         v.general.tags.remove(i); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::TagAlbumFlagSet(id, tag) => { |             Action::TagAlbumFlagSet(id, tag) => { | ||||||
|                 if let Some(v) = self.albums.get_mut(&id) { |                 if let Some(v) = self.albums.get_mut(&id) { | ||||||
|                     if !v.general.tags.contains(&tag) { |                     if !v.general.tags.contains(&tag) { | ||||||
|                         v.general.tags.push(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(v) = self.albums.get_mut(&id) { | ||||||
|                     if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { |                     if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { | ||||||
|                         v.general.tags.remove(i); |                         v.general.tags.remove(i); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::TagArtistFlagSet(id, tag) => { |             Action::TagArtistFlagSet(id, tag) => { | ||||||
|                 if let Some(v) = self.artists.get_mut(&id) { |                 if let Some(v) = self.artists.get_mut(&id) { | ||||||
|                     if !v.general.tags.contains(&tag) { |                     if !v.general.tags.contains(&tag) { | ||||||
|                         v.general.tags.push(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(v) = self.artists.get_mut(&id) { | ||||||
|                     if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { |                     if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { | ||||||
|                         v.general.tags.remove(i); |                         v.general.tags.remove(i); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::TagSongPropertySet(id, key, val) => { |             Action::TagSongPropertySet(id, key, val) => { | ||||||
|                 if let Some(v) = self.get_song_mut(&id) { |                 if let Some(v) = self.get_song_mut(&id) { | ||||||
|                     let new = format!("{key}{val}"); |                     let new = format!("{key}{val}"); | ||||||
|                     if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { |                     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) { |                 if let Some(v) = self.get_song_mut(&id) { | ||||||
|                     let tags = std::mem::replace(&mut v.general.tags, vec![]); |                     let tags = std::mem::replace(&mut v.general.tags, vec![]); | ||||||
|                     v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); |                     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) { |                 if let Some(v) = self.albums.get_mut(&id) { | ||||||
|                     let new = format!("{key}{val}"); |                     let new = format!("{key}{val}"); | ||||||
|                     if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { |                     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) { |                 if let Some(v) = self.albums.get_mut(&id) { | ||||||
|                     let tags = std::mem::replace(&mut v.general.tags, vec![]); |                     let tags = std::mem::replace(&mut v.general.tags, vec![]); | ||||||
|                     v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); |                     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) { |                 if let Some(v) = self.artists.get_mut(&id) { | ||||||
|                     let new = format!("{key}{val}"); |                     let new = format!("{key}{val}"); | ||||||
|                     if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { |                     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) { |                 if let Some(v) = self.artists.get_mut(&id) { | ||||||
|                     let tags = std::mem::replace(&mut v.general.tags, vec![]); |                     let tags = std::mem::replace(&mut v.general.tags, vec![]); | ||||||
|                     v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); |                     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) { |                 if let Some(song) = self.get_song_mut(&id) { | ||||||
|                     song.duration_millis = duration; |                     song.duration_millis = duration; | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Command::InitComplete => { |             Action::InitComplete => { | ||||||
|                 self.client_is_init = true; |                 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.
 |     /// A client database doesn't need any storage paths and won't perform autosaves.
 | ||||||
|     pub fn new_clientside() -> Self { |     pub fn new_clientside() -> Self { | ||||||
|         Self { |         Self { | ||||||
|  |             seq: Commander::new(true), | ||||||
|             db_dir: PathBuf::new(), |             db_dir: PathBuf::new(), | ||||||
|             db_file: PathBuf::new(), |             db_file: PathBuf::new(), | ||||||
|             lib_directory: PathBuf::new(), |             lib_directory: PathBuf::new(), | ||||||
| @ -862,6 +885,7 @@ impl Database { | |||||||
|     pub fn new_empty_in_dir(dir: PathBuf, lib_dir: PathBuf) -> Self { |     pub fn new_empty_in_dir(dir: PathBuf, lib_dir: PathBuf) -> Self { | ||||||
|         let path = dir.join("dbfile"); |         let path = dir.join("dbfile"); | ||||||
|         Self { |         Self { | ||||||
|  |             seq: Commander::new(false), | ||||||
|             db_dir: dir, |             db_dir: dir, | ||||||
|             db_file: path, |             db_file: path, | ||||||
|             lib_directory: lib_dir, |             lib_directory: lib_dir, | ||||||
| @ -887,6 +911,7 @@ impl Database { | |||||||
|         let mut file = BufReader::new(File::open(&path)?); |         let mut file = BufReader::new(File::open(&path)?); | ||||||
|         eprintln!("[{}] loading library from {file:?}", "INFO".cyan()); |         eprintln!("[{}] loading library from {file:?}", "INFO".cyan()); | ||||||
|         let s = Self { |         let s = Self { | ||||||
|  |             seq: Commander::new(false), | ||||||
|             db_dir: dir, |             db_dir: dir, | ||||||
|             db_file: path, |             db_file: path, | ||||||
|             lib_directory, |             lib_directory, | ||||||
| @ -948,11 +973,15 @@ impl Database { | |||||||
|         self.times_data_modified = None; |         self.times_data_modified = None; | ||||||
|         Ok(path) |         Ok(path) | ||||||
|     } |     } | ||||||
|     pub fn broadcast_update(&mut self, update: &Command) { |     pub fn broadcast_update(&mut self, update: Action) -> Action { | ||||||
|         match update { |         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 remove = vec![]; | ||||||
|         let mut bytes = None; |         let mut bytes = None; | ||||||
|         let mut arc = None; |         let mut arc = None; | ||||||
| @ -974,7 +1003,7 @@ impl Database { | |||||||
|                         remove.push(i); |                         remove.push(i); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 UpdateEndpoint::Custom(func) => func(update), |                 UpdateEndpoint::Custom(func) => func(&update), | ||||||
|                 UpdateEndpoint::CustomArc(func) => { |                 UpdateEndpoint::CustomArc(func) => { | ||||||
|                     if arc.is_none() { |                     if arc.is_none() { | ||||||
|                         arc = Some(Arc::new(update.clone())); |                         arc = Some(Arc::new(update.clone())); | ||||||
| @ -999,6 +1028,7 @@ impl Database { | |||||||
|                 self.update_endpoints.remove(i); |                 self.update_endpoints.remove(i); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         update.action | ||||||
|     } |     } | ||||||
|     pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) { |     pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) { | ||||||
|         self.modified_data(); |         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 { |     pub fn len(&self) -> usize { | ||||||
|         if !self.enabled { |         if !self.enabled { | ||||||
|             return 0; |             return 0; | ||||||
|  | |||||||
| @ -2,12 +2,16 @@ | |||||||
| pub mod playback_rs; | pub mod playback_rs; | ||||||
| #[cfg(feature = "playback-via-rodio")] | #[cfg(feature = "playback-via-rodio")] | ||||||
| pub mod 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 std::{collections::HashMap, ffi::OsStr, sync::Arc}; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     data::{database::Database, song::CachedData, SongId}, |     data::{database::Database, song::CachedData, SongId}, | ||||||
|     server::Command, |     server::Action, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| pub struct Player<T: PlayerBackend<SongCustomData>> { | pub struct Player<T: PlayerBackend<SongCustomData>> { | ||||||
| @ -94,11 +98,11 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> { | |||||||
|             allow_sending_commands: false, |             allow_sending_commands: false, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     pub fn handle_command(&mut self, command: &Command) { |     pub fn handle_action(&mut self, action: &Action) { | ||||||
|         match command { |         match action { | ||||||
|             Command::Resume => self.resume(), |             Action::Resume => self.resume(), | ||||||
|             Command::Pause => self.pause(), |             Action::Pause => self.pause(), | ||||||
|             Command::Stop => self.stop(), |             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) { |     pub fn update_uncache_opt(&mut self, db: &mut Database, allow_uncaching: bool) { | ||||||
|         if self.allow_sending_commands { |         if self.allow_sending_commands { | ||||||
|             if self.allow_sending_commands && self.backend.song_finished() { |             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); |                     self.backend.next(db.playing, load_duration); | ||||||
|                     if self.allow_sending_commands && load_duration { |                     if self.allow_sending_commands && load_duration { | ||||||
|                         if let Some(dur) = self.backend.current_song_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) { |                 } 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); |                         self.backend.next(db.playing, load_duration); | ||||||
|                         if self.allow_sending_commands && load_duration { |                         if self.allow_sending_commands && load_duration { | ||||||
|                             if let Some(dur) = self.backend.current_song_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 { |                     } else { | ||||||
|                         // only show an error if the user tries to play the song.
 |                         // only show an error if the user tries to play the song.
 | ||||||
|                         // otherwise, the error might be spammed.
 |                         // otherwise, the error might be spammed.
 | ||||||
|                         if self.allow_sending_commands && db.playing { |                         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!("Couldn't load bytes for song {id}"), | ||||||
|                                 format!( |                                 format!( | ||||||
|                                     "Song: {}\nby {:?} on {:?}", |                                     "Song: {}\nby {:?} on {:?}", | ||||||
|                                     song.title, song.artist, song.album |                                     song.title, song.artist, song.album | ||||||
|                                 ), |                                 ), | ||||||
|                             )); |                             )); | ||||||
|                             db.apply_command(Command::NextSong); |                             db.apply_action_unchecked_seq(Action::NextSong); | ||||||
|                         } |                         } | ||||||
|                         self.backend.clear(); |                         self.backend.clear(); | ||||||
|                     } |                     } | ||||||
|  | |||||||
| @ -2,7 +2,10 @@ use std::{ffi::OsStr, io::Cursor, path::Path, sync::Arc, time::Duration}; | |||||||
| 
 | 
 | ||||||
| use playback_rs::Hint; | use playback_rs::Hint; | ||||||
| 
 | 
 | ||||||
| use crate::{data::SongId, server::Command}; | use crate::{ | ||||||
|  |     data::SongId, | ||||||
|  |     server::{Action, Command}, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| use super::PlayerBackend; | use super::PlayerBackend; | ||||||
| 
 | 
 | ||||||
| @ -52,10 +55,13 @@ impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> { | |||||||
|             Ok(v) => Some(v), |             Ok(v) => Some(v), | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 if let Some(s) = &self.command_sender { |                 if let Some(s) = &self.command_sender { | ||||||
|                     s.send(Command::ErrorInfo( |                     s.send( | ||||||
|  |                         Action::ErrorInfo( | ||||||
|                             format!("Couldn't decode song #{id}!"), |                             format!("Couldn't decode song #{id}!"), | ||||||
|                             format!("Error: {e}"), |                             format!("Error: {e}"), | ||||||
|                     )) |                         ) | ||||||
|  |                         .cmd(0xFFu8), | ||||||
|  |                     ) | ||||||
|                     .unwrap(); |                     .unwrap(); | ||||||
|                 } |                 } | ||||||
|                 None |                 None | ||||||
| @ -95,18 +101,21 @@ impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> { | |||||||
|             if let Some(song) = song { |             if let Some(song) = song { | ||||||
|                 if let Err(e) = self.player.play_song_now(song, None) { |                 if let Err(e) = self.player.play_song_now(song, None) { | ||||||
|                     if let Some(s) = &self.command_sender { |                     if let Some(s) = &self.command_sender { | ||||||
|                         s.send(Command::ErrorInfo( |                         s.send( | ||||||
|  |                             Action::ErrorInfo( | ||||||
|                                 format!("Couldn't play song #{id}!"), |                                 format!("Couldn't play song #{id}!"), | ||||||
|                                 format!("Error: {e}"), |                                 format!("Error: {e}"), | ||||||
|                         )) |                             ) | ||||||
|  |                             .cmd(0xFFu8), | ||||||
|  |                         ) | ||||||
|                         .unwrap(); |                         .unwrap(); | ||||||
|                         s.send(Command::NextSong).unwrap(); |                         s.send(Action::NextSong.cmd(0xFFu8)).unwrap(); | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     self.player.set_playing(play); |                     self.player.set_playing(play); | ||||||
|                 } |                 } | ||||||
|             } else if let Some(s) = &self.command_sender { |             } 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 rc_u8_reader::ArcU8Reader; | ||||||
| use rodio::{decoder::DecoderError, Decoder, OutputStream, OutputStreamHandle, Sink, Source}; | 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; | use super::PlayerBackend; | ||||||
| 
 | 
 | ||||||
| @ -57,10 +60,13 @@ impl<T> PlayerBackend<T> for PlayerBackendRodio<T> { | |||||||
|         let decoder = decoder_from_bytes(Arc::clone(&bytes)); |         let decoder = decoder_from_bytes(Arc::clone(&bytes)); | ||||||
|         if let Err(e) = &decoder { |         if let Err(e) = &decoder { | ||||||
|             if let Some(s) = &self.command_sender { |             if let Some(s) = &self.command_sender { | ||||||
|                 s.send(Command::ErrorInfo( |                 s.send( | ||||||
|  |                     Action::ErrorInfo( | ||||||
|                         format!("Couldn't decode song #{id}!"), |                         format!("Couldn't decode song #{id}!"), | ||||||
|                         format!("Error: '{e}'"), |                         format!("Error: '{e}'"), | ||||||
|                 )) |                     ) | ||||||
|  |                     .cmd(0xFFu8), | ||||||
|  |                 ) | ||||||
|                 .unwrap(); |                 .unwrap(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -26,7 +26,54 @@ use crate::{ | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone, Debug)] | #[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, |     Resume, | ||||||
|     Pause, |     Pause, | ||||||
|     Stop, |     Stop, | ||||||
| @ -269,7 +316,7 @@ pub fn run_server_caching_thread_opt( | |||||||
|             checkf = true; |             checkf = true; | ||||||
|             #[cfg(feature = "playback")] |             #[cfg(feature = "playback")] | ||||||
|             if let Some(player) = &mut player { |             if let Some(player) = &mut player { | ||||||
|                 player.handle_command(&command); |                 player.handle_action(&command.action); | ||||||
|             } |             } | ||||||
|             database.lock().unwrap().apply_command(command); |             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; | const SUBBYTE_TAG_ARTIST_PROPERTY_UNSET: u8 = 0b10_100_100; | ||||||
| 
 | 
 | ||||||
| impl ToFromBytes for Command { | 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> |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|     where |     where | ||||||
|         T: Write, |         T: Write, | ||||||
|  | |||||||
| @ -16,6 +16,9 @@ rocket = { version = "0.5.0", optional = true } | |||||||
| html-escape = { version = "0.2.13", optional = true } | html-escape = { version = "0.2.13", optional = true } | ||||||
| 
 | 
 | ||||||
| [features] | [features] | ||||||
| default = ["website", "playback"] | default = ["website", "default-playback"] | ||||||
| website = ["dep:tokio", "dep:rocket", "dep:html-escape"] | 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(); |             writeln!(con, "main").unwrap(); | ||||||
|             loop { |             loop { | ||||||
|                 let cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap(); |                 let cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap(); | ||||||
|                 use musicdb_lib::server::Command::*; |                 use musicdb_lib::server::Action::*; | ||||||
|                 match &cmd { |                 match &cmd.action { | ||||||
|                     // ignore playback and queue commands
 |                     // ignore playback and queue commands
 | ||||||
|                     Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..) |                     Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..) | ||||||
|                     | QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..) |                     | 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::queue::{Queue, QueueContent, QueueFolder}; | ||||||
| use musicdb_lib::data::song::Song; | use musicdb_lib::data::song::Song; | ||||||
| use musicdb_lib::data::SongId; | use musicdb_lib::data::SongId; | ||||||
| use musicdb_lib::server::Command; | use musicdb_lib::server::{Action, Command}; | ||||||
| use rocket::response::content::RawHtml; | use rocket::response::content::RawHtml; | ||||||
| use rocket::{get, routes, Config, State}; | use rocket::{get, routes, Config, State}; | ||||||
| 
 | 
 | ||||||
| @ -258,37 +258,44 @@ fn gen_queue_html_impl( | |||||||
| fn queue_remove(data: &State<Data>, path: &str) { | fn queue_remove(data: &State<Data>, path: &str) { | ||||||
|     if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() { |     if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() { | ||||||
|         data.command_sender |         data.command_sender | ||||||
|             .send(Command::QueueRemove(path)) |             .send(Action::QueueRemove(path).cmd(0xFFu8)) | ||||||
|             .unwrap(); |             .unwrap(); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| #[get("/queue-goto/<path>")] | #[get("/queue-goto/<path>")] | ||||||
| fn queue_goto(data: &State<Data>, path: &str) { | fn queue_goto(data: &State<Data>, path: &str) { | ||||||
|     if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() { |     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")] | #[get("/play")] | ||||||
| fn play(data: &State<Data>) { | fn play(data: &State<Data>) { | ||||||
|     data.command_sender.send(Command::Resume).unwrap(); |     data.command_sender | ||||||
|  |         .send(Action::Resume.cmd(0xFFu8)) | ||||||
|  |         .unwrap(); | ||||||
| } | } | ||||||
| #[get("/pause")] | #[get("/pause")] | ||||||
| fn pause(data: &State<Data>) { | fn pause(data: &State<Data>) { | ||||||
|     data.command_sender.send(Command::Pause).unwrap(); |     data.command_sender.send(Action::Pause.cmd(0xFFu8)).unwrap(); | ||||||
| } | } | ||||||
| #[get("/stop")] | #[get("/stop")] | ||||||
| fn stop(data: &State<Data>) { | fn stop(data: &State<Data>) { | ||||||
|     data.command_sender.send(Command::Stop).unwrap(); |     data.command_sender.send(Action::Stop.cmd(0xFFu8)).unwrap(); | ||||||
| } | } | ||||||
| #[get("/skip")] | #[get("/skip")] | ||||||
| fn skip(data: &State<Data>) { | fn skip(data: &State<Data>) { | ||||||
|     data.command_sender.send(Command::NextSong).unwrap(); |     data.command_sender | ||||||
|  |         .send(Action::NextSong.cmd(0xFFu8)) | ||||||
|  |         .unwrap(); | ||||||
| } | } | ||||||
| #[get("/clear-queue")] | #[get("/clear-queue")] | ||||||
| fn clear_queue(data: &State<Data>) { | fn clear_queue(data: &State<Data>) { | ||||||
|     data.command_sender |     data.command_sender | ||||||
|         .send(Command::QueueUpdate( |         .send( | ||||||
|  |             Action::QueueUpdate( | ||||||
|                 vec![], |                 vec![], | ||||||
|                 QueueContent::Folder(QueueFolder { |                 QueueContent::Folder(QueueFolder { | ||||||
|                     index: 0, |                     index: 0, | ||||||
| @ -297,17 +304,16 @@ fn clear_queue(data: &State<Data>) { | |||||||
|                     order: None, |                     order: None, | ||||||
|                 }) |                 }) | ||||||
|                 .into(), |                 .into(), | ||||||
|         )) |             ) | ||||||
|  |             .cmd(0xFFu8), | ||||||
|  |         ) | ||||||
|         .unwrap(); |         .unwrap(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[get("/add-song/<id>")] | #[get("/add-song/<id>")] | ||||||
| fn add_song(data: &State<Data>, id: SongId) { | fn add_song(data: &State<Data>, id: SongId) { | ||||||
|     data.command_sender |     data.command_sender | ||||||
|         .send(Command::QueueAdd( |         .send(Action::QueueAdd(vec![], vec![QueueContent::Song(id).into()]).cmd(0xFFu8)) | ||||||
|             vec![], |  | ||||||
|             vec![QueueContent::Song(id).into()], |  | ||||||
|         )) |  | ||||||
|         .unwrap(); |         .unwrap(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Mark
						Mark