diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index c6215e2..ba0246c 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -368,116 +368,130 @@ impl Gui { db.update_endpoints_id += 1; db.update_endpoints.push(( udepid, - musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new( - move |cmd| match &cmd.action { - Action::Resume - | Action::Pause - | Action::Stop - | Action::Save - | Action::InitComplete => {} - Action::NextSong - | Action::QueueUpdate(..) - | Action::QueueAdd(..) - | Action::QueueInsert(..) - | Action::QueueRemove(..) - | Action::QueueMove(..) - | Action::QueueMoveInto(..) - | Action::QueueGoto(..) - | Action::QueueShuffle(..) - | Action::QueueSetShuffle(..) - | Action::QueueUnshuffle(..) => { - if let Some(s) = &*event_sender_arc.lock().unwrap() { - _ = s.send_event(GuiEvent::UpdatedQueue); + musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| { + the_impl(&cmd.action, &event_sender_arc, ¬if_sender_two); + fn the_impl( + action: &Action, + event_sender_arc: &Arc>>>, + notif_sender_two: &Sender< + Box (Box, NotifInfo) + Send>, + >, + ) { + match action { + Action::Resume + | Action::Pause + | Action::Stop + | Action::Save + | Action::InitComplete => {} + Action::NextSong + | Action::QueueUpdate(..) + | Action::QueueAdd(..) + | Action::QueueInsert(..) + | Action::QueueRemove(..) + | Action::QueueMove(..) + | Action::QueueMoveInto(..) + | Action::QueueGoto(..) + | Action::QueueShuffle(..) + | Action::QueueSetShuffle(..) + | Action::QueueUnshuffle(..) => { + if let Some(s) = &*event_sender_arc.lock().unwrap() { + _ = s.send_event(GuiEvent::UpdatedQueue); + } } - } - Action::SyncDatabase(..) - | Action::AddSong(_, _) - | Action::AddAlbum(_, _) - | Action::AddArtist(_, _) - | Action::AddCover(_, _) - | Action::ModifySong(_, _) - | Action::ModifyAlbum(_, _) - | Action::ModifyArtist(_, _) - | Action::RemoveSong(_) - | Action::RemoveAlbum(_) - | Action::RemoveArtist(_) - | Action::TagSongFlagSet(..) - | Action::TagSongFlagUnset(..) - | Action::TagAlbumFlagSet(..) - | Action::TagAlbumFlagUnset(..) - | Action::TagArtistFlagSet(..) - | Action::TagArtistFlagUnset(..) - | Action::TagSongPropertySet(..) - | Action::TagSongPropertyUnset(..) - | Action::TagAlbumPropertySet(..) - | Action::TagAlbumPropertyUnset(..) - | Action::TagArtistPropertySet(..) - | Action::TagArtistPropertyUnset(..) - | Action::SetSongDuration(..) => { - if let Some(s) = &*event_sender_arc.lock().unwrap() { - _ = s.send_event(GuiEvent::UpdatedLibrary); + Action::SyncDatabase(..) + | Action::AddSong(_, _) + | Action::AddAlbum(_, _) + | Action::AddArtist(_, _) + | Action::AddCover(_, _) + | Action::ModifySong(_, _) + | Action::ModifyAlbum(_, _) + | Action::ModifyArtist(_, _) + | Action::RemoveSong(_) + | Action::RemoveAlbum(_) + | Action::RemoveArtist(_) + | Action::TagSongFlagSet(..) + | Action::TagSongFlagUnset(..) + | Action::TagAlbumFlagSet(..) + | Action::TagAlbumFlagUnset(..) + | Action::TagArtistFlagSet(..) + | Action::TagArtistFlagUnset(..) + | Action::TagSongPropertySet(..) + | Action::TagSongPropertyUnset(..) + | Action::TagAlbumPropertySet(..) + | Action::TagAlbumPropertyUnset(..) + | Action::TagArtistPropertySet(..) + | Action::TagArtistPropertyUnset(..) + | Action::SetSongDuration(..) => { + if let Some(s) = &*event_sender_arc.lock().unwrap() { + _ = s.send_event(GuiEvent::UpdatedLibrary); + } } - } - Action::ErrorInfo(t, d) => { - let (t, d) = (t.clone(), d.clone()); - notif_sender_two - .send(Box::new(move |_| { - ( - Box::new(Panel::with_background( - GuiElemCfg::default(), - [Label::new( + Action::Multiple(actions) => { + for action in actions { + the_impl(action, event_sender_arc, notif_sender_two); + } + } + Action::ErrorInfo(t, d) => { + let (t, d) = (t.clone(), d.clone()); + notif_sender_two + .send(Box::new(move |_| { + ( + Box::new(Panel::with_background( GuiElemCfg::default(), - if t.is_empty() { - format!("Server message\n{d}") - } else { - format!("Server error ({t})\n{d}") - }, - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - )], - Color::from_rgba(0.0, 0.0, 0.0, 0.8), - )), - if t.is_empty() { - NotifInfo::new(Duration::from_secs(2)) - } else { - NotifInfo::new(Duration::from_secs(5)) - .with_highlight(Color::RED) - }, - ) - })) - .unwrap(); - } - Action::Denied(req) => { - let req = *req; - notif_sender_two - .send(Box::new(move |_| { - ( - Box::new(Panel::with_background( - GuiElemCfg::default(), - [Label::new( - GuiElemCfg::default(), - format!( - "server denied {}", - if req.is_some() { - "request, maybe desynced" + [Label::new( + GuiElemCfg::default(), + if t.is_empty() { + format!("Server message\n{d}") } else { - "action, likely desynced" + format!("Server error ({t})\n{d}") }, - ), - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - )], - Color::from_rgba(0.0, 0.0, 0.0, 0.8), - )), - NotifInfo::new(Duration::from_secs(1)), - ) - })) - .unwrap(); + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + Color::from_rgba(0.0, 0.0, 0.0, 0.8), + )), + if t.is_empty() { + NotifInfo::new(Duration::from_secs(2)) + } else { + NotifInfo::new(Duration::from_secs(5)) + .with_highlight(Color::RED) + }, + ) + })) + .unwrap(); + } + Action::Denied(req) => { + let req = *req; + notif_sender_two + .send(Box::new(move |_| { + ( + Box::new(Panel::with_background( + GuiElemCfg::default(), + [Label::new( + GuiElemCfg::default(), + format!( + "server denied {}", + if req.is_some() { + "request, maybe desynced" + } else { + "action, likely desynced" + }, + ), + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + Color::from_rgba(0.0, 0.0, 0.0, 0.8), + )), + NotifInfo::new(Duration::from_secs(1)), + ) + })) + .unwrap(); + } } - }, - )), + } + })), )); } let no_animations = false; diff --git a/musicdb-lib/src/data/database.rs b/musicdb-lib/src/data/database.rs index 4bb0fd8..67cdde7 100755 --- a/musicdb-lib/src/data/database.rs +++ b/musicdb-lib/src/data/database.rs @@ -527,19 +527,23 @@ impl Database { if let Some(client) = client { for (udepid, udep) in &mut self.update_endpoints { if client == *udepid { - let denied = - Action::Denied(command.action.get_req().unwrap_or_else(Req::none)) - .cmd(0xFFu8); - match udep { - UpdateEndpoint::Bytes(w) => { - let _ = w.write(&denied.to_bytes_vec()); + let mut reqs = command.action.get_req_if_some(); + if reqs.is_empty() { + reqs.push(Req::none()); + } + for req in reqs { + let denied = Action::Denied(req).cmd(0xFFu8); + match udep { + UpdateEndpoint::Bytes(w) => { + let _ = w.write(&denied.to_bytes_vec()); + } + UpdateEndpoint::CmdChannel(w) => { + let _ = w.send(Arc::new(denied)); + } + UpdateEndpoint::Custom(w) => w(&denied), + UpdateEndpoint::CustomArc(w) => w(Arc::new(denied)), + UpdateEndpoint::CustomBytes(w) => w(&denied.to_bytes_vec()), } - UpdateEndpoint::CmdChannel(w) => { - let _ = w.send(Arc::new(denied)); - } - UpdateEndpoint::Custom(w) => w(&denied), - UpdateEndpoint::CustomArc(w) => w(Arc::new(denied)), - UpdateEndpoint::CustomBytes(w) => w(&denied.to_bytes_vec()), } return; } @@ -864,6 +868,11 @@ impl Database { song.duration_millis = duration; } } + Action::Multiple(actions) => { + for action in actions { + self.apply_action_unchecked_seq(action, client); + } + } Action::InitComplete => { self.client_is_init = true; } @@ -1008,13 +1017,13 @@ impl Database { self.seq.inc(); } let mut update = self.seq.pack(update); - let req = update.action.take_req(); + let reqs = update.action.take_req_all(); let mut remove = vec![]; let mut bytes = None; let mut arc = None; for (i, (udepid, udep)) in self.update_endpoints.iter_mut().enumerate() { - if req.is_some_and(|r| r.is_some()) && client.is_some_and(|v| *udepid == v) { - update.action.put_req(req.unwrap()); + if reqs.iter().any(|r| r.is_some()) && client.is_some_and(|v| *udepid == v) { + update.action.put_req_all(reqs.clone()); match udep { UpdateEndpoint::Bytes(writer) => { if writer.write_all(&update.to_bytes_vec()).is_err() { @@ -1035,7 +1044,7 @@ impl Database { func(bytes.as_ref().unwrap()) } } - update.action.take_req(); + update.action.take_req_all(); } match udep { UpdateEndpoint::Bytes(writer) => { @@ -1079,9 +1088,7 @@ impl Database { self.update_endpoints.remove(i); } } - if let Some(req) = req { - update.action.put_req(req); - } + update.action.put_req_all(reqs); update.action } pub fn sync(&mut self, artists: Vec, albums: Vec, songs: Vec) { diff --git a/musicdb-lib/src/server/mod.rs b/musicdb-lib/src/server/mod.rs index 9c533da..f186719 100755 --- a/musicdb-lib/src/server/mod.rs +++ b/musicdb-lib/src/server/mod.rs @@ -43,20 +43,28 @@ impl Action { pub fn cmd(self, seq: u8) -> Command { Command::new(seq, self) } - pub fn take_req(&mut self) -> Option { + pub fn take_req_all(&mut self) -> Vec { self.req_mut() + .into_iter() .map(|r| std::mem::replace(r, Req::none())) + .collect() + } + pub fn get_req_all(&mut self) -> Vec { + self.req_mut().into_iter().map(|r| *r).collect() + } + pub fn get_req_if_some(&mut self) -> Vec { + self.req_mut() + .into_iter() + .map(|r| *r) .filter(|r| r.is_some()) + .collect() } - pub fn get_req(&mut self) -> Option { - self.req_mut().map(|r| *r).filter(|r| r.is_some()) - } - pub fn put_req(&mut self, req: Req) { - if let Some(r) = self.req_mut() { - *r = req; + pub fn put_req_all(&mut self, reqs: Vec) { + for (o, n) in self.req_mut().into_iter().zip(reqs) { + *o = n; } } - fn req_mut(&mut self) -> Option<&mut Req> { + fn req_mut(&mut self) -> Vec<&mut Req> { match self { Self::QueueUpdate(_, _, req) | Self::QueueAdd(_, _, req) @@ -68,7 +76,7 @@ impl Action { | Self::ModifySong(_, req) | Self::ModifyAlbum(_, req) | Self::ModifyArtist(_, req) - | Self::Denied(req) => Some(req), + | Self::Denied(req) => vec![req], Self::Resume | Self::Pause | Self::Stop @@ -99,7 +107,8 @@ impl Action { | Self::TagArtistPropertyUnset(_, _) | Self::InitComplete | Self::Save - | Self::ErrorInfo(_, _) => None, + | Self::ErrorInfo(_, _) => vec![], + Self::Multiple(actions) => actions.iter_mut().flat_map(|v| v.req_mut()).collect(), } } } @@ -215,6 +224,8 @@ pub enum Action { TagArtistPropertySet(ArtistId, String, String), TagArtistPropertyUnset(ArtistId, String), + Multiple(Vec), + InitComplete, Save, ErrorInfo(String, String), @@ -471,6 +482,7 @@ const BYTE_PAUSE: u8 = 0b01_000_001; const BYTE_STOP: u8 = 0b01_000_010; const BYTE_NEXT_SONG: u8 = 0b01_000_100; +const BYTE_MULTIPLE: u8 = 0b01_010_100; const BYTE_INIT_COMPLETE: u8 = 0b01_010_000; const BYTE_SET_SONG_DURATION: u8 = 0b01_010_001; const BYTE_SAVE: u8 = 0b01_010_010; @@ -546,8 +558,7 @@ impl ToFromBytes for Req { Ok(Self(ToFromBytes::from_bytes(s)?)) } } -// impl ToFromBytes for Action { -impl Action { +impl ToFromBytes for Action { fn to_bytes(&self, s: &mut T) -> Result<(), std::io::Error> where T: Write, @@ -753,6 +764,10 @@ impl Action { i.to_bytes(s)?; d.to_bytes(s)?; } + Self::Multiple(actions) => { + s.write_all(&[BYTE_MULTIPLE])?; + actions.to_bytes(s)?; + } Self::InitComplete => { s.write_all(&[BYTE_INIT_COMPLETE])?; } @@ -880,6 +895,7 @@ impl Action { } }, BYTE_SET_SONG_DURATION => Self::SetSongDuration(from_bytes!(), from_bytes!()), + BYTE_MULTIPLE => Self::Multiple(from_bytes!()), BYTE_INIT_COMPLETE => Self::InitComplete, BYTE_SAVE => Self::Save, BYTE_ERRORINFO => Self::ErrorInfo(from_bytes!(), from_bytes!()), diff --git a/musicdb-remove-duplicates-from-queue/.gitignore b/musicdb-remove-duplicates-from-queue/.gitignore new file mode 100755 index 0000000..ea8c4bf --- /dev/null +++ b/musicdb-remove-duplicates-from-queue/.gitignore @@ -0,0 +1 @@ +/target diff --git a/musicdb-remove-duplicates-from-queue/Cargo.toml b/musicdb-remove-duplicates-from-queue/Cargo.toml new file mode 100644 index 0000000..3426962 --- /dev/null +++ b/musicdb-remove-duplicates-from-queue/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "musicdb-test" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +musicdb-lib = { path = "../musicdb-lib" } diff --git a/musicdb-remove-duplicates-from-queue/src/main.rs b/musicdb-remove-duplicates-from-queue/src/main.rs new file mode 100755 index 0000000..faa7708 --- /dev/null +++ b/musicdb-remove-duplicates-from-queue/src/main.rs @@ -0,0 +1,61 @@ +use std::{collections::HashSet, io::Write, net::TcpStream}; + +use musicdb_lib::{ + data::{ + database::Database, + queue::{Queue, QueueContent}, + SongId, + }, + load::ToFromBytes, + server::{Action, Command}, +}; + +fn main() { + let mut con = TcpStream::connect( + std::env::args() + .nth(1) + .expect("required argument: server address and port"), + ) + .unwrap(); + writeln!(con, "main").unwrap(); + let mut db = Database::new_clientside(); + while !db.is_client_init() { + db.apply_action_unchecked_seq(Command::from_bytes(&mut con).unwrap().action, None); + } + let mut actions = vec![]; + rev_actions(&mut actions, &db.queue, &mut vec![], &mut HashSet::new()); + eprintln!("Removing {} queue elements", actions.len()); + db.seq + .pack(Action::Multiple(actions)) + .to_bytes(&mut con) + .unwrap(); +} + +fn rev_actions( + actions: &mut Vec, + queue: &Queue, + path: &mut Vec, + seen: &mut HashSet, +) { + match queue.content() { + QueueContent::Song(id) => { + if seen.contains(id) { + actions.push(Action::QueueRemove(path.clone())); + } else { + seen.insert(*id); + } + } + QueueContent::Folder(folder) => { + for (i, queue) in folder.iter().enumerate() { + path.push(i); + rev_actions(actions, queue, path, seen); + path.pop(); + } + } + QueueContent::Loop(_, _, inner) => { + path.push(0); + rev_actions(actions, &*inner, path, seen); + path.pop(); + } + } +} diff --git a/musicdb-server/src/main.rs b/musicdb-server/src/main.rs index 8ec8fc4..3f94d1e 100755 --- a/musicdb-server/src/main.rs +++ b/musicdb-server/src/main.rs @@ -149,43 +149,60 @@ fn main() { writeln!(con, "main").unwrap(); loop { let mut cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap(); - use musicdb_lib::server::Action::*; - match &cmd.action { - // ignore playback and queue commands, and denials - Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..) - | QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..) - | QueueGoto(..) | QueueShuffle(..) | QueueSetShuffle(..) - | QueueUnshuffle(..) | Denied(..) => continue, - SyncDatabase(..) - | AddSong(..) - | AddAlbum(..) - | AddArtist(..) - | AddCover(..) - | ModifySong(..) - | ModifyAlbum(..) - | RemoveSong(..) - | RemoveAlbum(..) - | RemoveArtist(..) - | ModifyArtist(..) - | SetSongDuration(..) - | TagSongFlagSet(..) - | TagSongFlagUnset(..) - | TagAlbumFlagSet(..) - | TagAlbumFlagUnset(..) - | TagArtistFlagSet(..) - | TagArtistFlagUnset(..) - | TagSongPropertySet(..) - | TagSongPropertyUnset(..) - | TagAlbumPropertySet(..) - | TagAlbumPropertyUnset(..) - | TagArtistPropertySet(..) - | TagArtistPropertyUnset(..) - | InitComplete - | Save - | ErrorInfo(..) => (), + use musicdb_lib::server::Action::{self, *}; + fn sanitize_actions(action: Action) -> Option { + match action { + // ignore playback and queue commands, and denials + Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..) + | QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..) + | QueueGoto(..) | QueueShuffle(..) | QueueSetShuffle(..) + | QueueUnshuffle(..) | Denied(..) => None, + SyncDatabase(..) + | AddSong(..) + | AddAlbum(..) + | AddArtist(..) + | AddCover(..) + | ModifySong(..) + | ModifyAlbum(..) + | RemoveSong(..) + | RemoveAlbum(..) + | RemoveArtist(..) + | ModifyArtist(..) + | SetSongDuration(..) + | TagSongFlagSet(..) + | TagSongFlagUnset(..) + | TagAlbumFlagSet(..) + | TagAlbumFlagUnset(..) + | TagArtistFlagSet(..) + | TagArtistFlagUnset(..) + | TagSongPropertySet(..) + | TagSongPropertyUnset(..) + | TagAlbumPropertySet(..) + | TagAlbumPropertyUnset(..) + | TagArtistPropertySet(..) + | TagArtistPropertyUnset(..) + | InitComplete + | Save + | ErrorInfo(..) => Some(action), + Multiple(actions) => { + let actions = actions + .into_iter() + .flat_map(|action| sanitize_actions(action)) + .collect::>(); + if actions.is_empty() { + None + } else { + Some(Multiple(actions)) + } + } + } + } + if let Some(action) = sanitize_actions(cmd.action) { + database + .lock() + .unwrap() + .apply_action_unchecked_seq(action, None); } - cmd.seq = 0xFF; - database.lock().unwrap().apply_command(cmd, None); } }); }