add Multiple action and tool to remove queue-duplicates

This commit is contained in:
Mark 2024-12-31 10:39:12 +01:00
parent a8e18abfd2
commit b1c0925647
7 changed files with 296 additions and 171 deletions

View File

@ -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, &notif_sender_two);
fn the_impl(
action: &Action,
event_sender_arc: &Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
notif_sender_two: &Sender<
Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, 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;

View File

@ -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<Artist>, albums: Vec<Album>, songs: Vec<Song>) {

View File

@ -43,20 +43,28 @@ impl Action {
pub fn cmd(self, seq: u8) -> Command {
Command::new(seq, self)
}
pub fn take_req(&mut self) -> Option<Req> {
pub fn take_req_all(&mut self) -> Vec<Req> {
self.req_mut()
.into_iter()
.map(|r| std::mem::replace(r, Req::none()))
.collect()
}
pub fn get_req_all(&mut self) -> Vec<Req> {
self.req_mut().into_iter().map(|r| *r).collect()
}
pub fn get_req_if_some(&mut self) -> Vec<Req> {
self.req_mut()
.into_iter()
.map(|r| *r)
.filter(|r| r.is_some())
.collect()
}
pub fn get_req(&mut self) -> Option<Req> {
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<Req>) {
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<Self>),
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<T>(&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!()),

View File

@ -0,0 +1 @@
/target

View File

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

View File

@ -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<Action>,
queue: &Queue,
path: &mut Vec<usize>,
seen: &mut HashSet<SongId>,
) {
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();
}
}
}

View File

@ -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<Action> {
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::<Vec<_>>();
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);
}
});
}