sequence numbers

This commit is contained in:
Mark 2024-12-08 12:03:24 +01:00
parent d02646406d
commit c3622aca30
18 changed files with 429 additions and 257 deletions

View File

@ -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"]

View File

@ -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,84 +363,86 @@ 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(..)
if let Some(s) = &*event_sender_arc.lock().unwrap() { | Action::QueueUnshuffle(..) => {
_ = s.send_event(GuiEvent::UpdatedQueue); if let Some(s) = &*event_sender_arc.lock().unwrap() {
_ = s.send_event(GuiEvent::UpdatedQueue);
}
} }
} Action::SyncDatabase(..)
Command::SyncDatabase(..) | Action::AddSong(_)
| Command::AddSong(_) | Action::AddAlbum(_)
| Command::AddAlbum(_) | Action::AddArtist(_)
| Command::AddArtist(_) | Action::AddCover(_)
| Command::AddCover(_) | Action::ModifySong(_)
| Command::ModifySong(_) | Action::ModifyAlbum(_)
| Command::ModifyAlbum(_) | Action::ModifyArtist(_)
| Command::ModifyArtist(_) | Action::RemoveSong(_)
| Command::RemoveSong(_) | Action::RemoveAlbum(_)
| Command::RemoveAlbum(_) | Action::RemoveArtist(_)
| Command::RemoveArtist(_) | Action::TagSongFlagSet(..)
| Command::TagSongFlagSet(..) | Action::TagSongFlagUnset(..)
| Command::TagSongFlagUnset(..) | Action::TagAlbumFlagSet(..)
| Command::TagAlbumFlagSet(..) | Action::TagAlbumFlagUnset(..)
| Command::TagAlbumFlagUnset(..) | Action::TagArtistFlagSet(..)
| Command::TagArtistFlagSet(..) | Action::TagArtistFlagUnset(..)
| Command::TagArtistFlagUnset(..) | Action::TagSongPropertySet(..)
| Command::TagSongPropertySet(..) | Action::TagSongPropertyUnset(..)
| Command::TagSongPropertyUnset(..) | Action::TagAlbumPropertySet(..)
| Command::TagAlbumPropertySet(..) | Action::TagAlbumPropertyUnset(..)
| Command::TagAlbumPropertyUnset(..) | Action::TagArtistPropertySet(..)
| Command::TagArtistPropertySet(..) | Action::TagArtistPropertyUnset(..)
| Command::TagArtistPropertyUnset(..) | Action::SetSongDuration(..) => {
| Command::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); }
} }
} Action::ErrorInfo(t, d) => {
Command::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 |_| { (
( Box::new(Panel::with_background(
Box::new(Panel::with_background(
GuiElemCfg::default(),
[Label::new(
GuiElemCfg::default(), GuiElemCfg::default(),
if t.is_empty() { [Label::new(
format!("Server message\n{d}") GuiElemCfg::default(),
} else { if t.is_empty() {
format!("Server error ({t})\n{d}") format!("Server message\n{d}")
}, } else {
Color::WHITE, format!("Server error ({t})\n{d}")
None, },
Vec2::new(0.5, 0.5), Color::WHITE,
)], None,
Color::from_rgba(0.0, 0.0, 0.0, 0.8), Vec2::new(0.5, 0.5),
)), )],
if t.is_empty() { Color::from_rgba(0.0, 0.0, 0.0, 0.8),
NotifInfo::new(Duration::from_secs(2)) )),
} else { if t.is_empty() {
NotifInfo::new(Duration::from_secs(5)) NotifInfo::new(Duration::from_secs(2))
.with_highlight(Color::RED) } else {
}, NotifInfo::new(Duration::from_secs(5))
) .with_highlight(Color::RED)
})) },
.unwrap(); )
}))
.unwrap();
}
} }
})), })),
); );
@ -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)))
} }
} }
} }

View File

@ -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) => {

View File

@ -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),

View File

@ -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) => {

View File

@ -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 {

View File

@ -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(),

View File

@ -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 {

View File

@ -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"]

View File

@ -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
self.artists().iter().map(|v| v.1.clone()).collect(), .pack(Action::SyncDatabase(
self.albums().iter().map(|v| v.1.clone()).collect(), self.artists().iter().map(|v| v.1.clone()).collect(),
self.songs().iter().map(|v| v.1.clone()).collect(), self.albums().iter().map(|v| v.1.clone()).collect(),
) self.songs().iter().map(|v| v.1.clone()).collect(),
.to_bytes(con)?; ))
Command::QueueUpdate(vec![], self.queue.clone()).to_bytes(con)?; .to_bytes(con)?;
self.seq
.pack(Action::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();

View File

@ -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;

View File

@ -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();
} }

View File

@ -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(
format!("Couldn't decode song #{id}!"), Action::ErrorInfo(
format!("Error: {e}"), format!("Couldn't decode song #{id}!"),
)) 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(
format!("Couldn't play song #{id}!"), Action::ErrorInfo(
format!("Error: {e}"), format!("Couldn't play song #{id}!"),
)) 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();
} }
} }
} }

View File

@ -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(
format!("Couldn't decode song #{id}!"), Action::ErrorInfo(
format!("Error: '{e}'"), format!("Couldn't decode song #{id}!"),
)) format!("Error: '{e}'"),
)
.cmd(0xFFu8),
)
.unwrap(); .unwrap();
} }
} }

View File

@ -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,

View File

@ -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"]

View File

@ -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(..)

View File

@ -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,56 +258,62 @@ 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(
vec![], Action::QueueUpdate(
QueueContent::Folder(QueueFolder { vec![],
index: 0, QueueContent::Folder(QueueFolder {
content: vec![], index: 0,
name: String::new(), content: vec![],
order: None, name: String::new(),
}) 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();
} }