diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index 5753457..b8dec35 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -234,7 +234,7 @@ pub fn main( ), ( "Year".to_owned(), - crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), + crate::gui_library::FilterType::TagWithValueInt("Year=".to_owned(), 1990, 2000), ), ], filter_presets_album: vec![ @@ -244,7 +244,7 @@ pub fn main( ), ( "Year".to_owned(), - crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), + crate::gui_library::FilterType::TagWithValueInt("Year=".to_owned(), 1990, 2000), ), ], filter_presets_artist: vec![ @@ -254,7 +254,7 @@ pub fn main( ), ( "Year".to_owned(), - crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), + crate::gui_library::FilterType::TagWithValueInt("Year=".to_owned(), 1990, 2000), ), ], #[cfg(feature = "merscfg")] @@ -371,6 +371,18 @@ impl Gui { | Command::RemoveSong(_) | Command::RemoveAlbum(_) | Command::RemoveArtist(_) + | Command::TagSongFlagSet(..) + | Command::TagSongFlagUnset(..) + | Command::TagAlbumFlagSet(..) + | Command::TagAlbumFlagUnset(..) + | Command::TagArtistFlagSet(..) + | Command::TagArtistFlagUnset(..) + | Command::TagSongPropertySet(..) + | Command::TagSongPropertyUnset(..) + | Command::TagAlbumPropertySet(..) + | Command::TagAlbumPropertyUnset(..) + | Command::TagArtistPropertySet(..) + | Command::TagArtistPropertyUnset(..) | Command::SetSongDuration(..) => { if let Some(s) = &*event_sender_arc.lock().unwrap() { _ = s.send_event(GuiEvent::UpdatedLibrary); diff --git a/musicdb-client/src/gui_idle_display.rs b/musicdb-client/src/gui_idle_display.rs index 2947ebc..4fbe7f1 100644 --- a/musicdb-client/src/gui_idle_display.rs +++ b/musicdb-client/src/gui_idle_display.rs @@ -1,4 +1,7 @@ -use std::{sync::Arc, time::Instant}; +use std::{ + sync::{atomic::AtomicBool, Arc}, + time::Instant, +}; use musicdb_lib::data::ArtistId; use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; @@ -39,11 +42,14 @@ pub struct IdleDisplay { pub artist_image_to_cover_margin: f32, pub force_reset_texts: bool, + + is_fav: (bool, Arc), } impl IdleDisplay { pub fn new(config: GuiElemCfg) -> Self { let cover_bottom = 0.79; + let is_fav = Arc::new(AtomicBool::new(false)); Self { config, idle_mode: 0.0, @@ -67,7 +73,8 @@ impl IdleDisplay { ), c_side1_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), c_side2_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), - c_buttons: PlayPause::new(GuiElemCfg::default()), + is_fav: (false, Arc::clone(&is_fav)), + c_buttons: PlayPause::new(GuiElemCfg::default(), is_fav), c_buttons_custom_pos: false, cover_aspect_ratio: AnimationController::new( 1.0, @@ -122,6 +129,19 @@ impl GuiElem for IdleDisplay { self.current_info.update(info, g); if self.current_info.new_song || self.force_reset_texts { self.current_info.new_song = false; + self.force_reset_texts = false; + let is_fav = self + .current_info + .current_song + .and_then(|id| info.database.get_song(&id)) + .map(|song| song.general.tags.iter().any(|v| v == "Fav")) + .unwrap_or(false); + if self.is_fav.0 != is_fav { + self.is_fav.0 = is_fav; + self.is_fav + .1 + .store(is_fav, std::sync::atomic::Ordering::Relaxed); + } self.c_top_label.content = if let Some(song) = self.current_info.current_song { info.gui_config .idle_top_text @@ -278,9 +298,10 @@ impl GuiElem for IdleDisplay { self.c_side2_label.config_mut().pos = Rectangle::from_tuples((left, ai_top), (max_right, bottom)); // limit width of c_buttons - let buttons_right_pos = 0.99; - let buttons_width_max = info.pos.height() * 0.08 / 0.3 / info.pos.width(); - let buttons_width = buttons_width_max.min(0.2); + let buttons_right_pos = 1.0; + let buttons_width_max = info.pos.height() * 0.08 * 4.0 / info.pos.width(); + // buttons use at most half the width (set to 0.2 later, when screen space is used for other things) + let buttons_width = buttons_width_max.min(0.5); if !self.c_buttons_custom_pos { self.c_buttons.config_mut().pos = Rectangle::from_tuples( (buttons_right_pos - buttons_width, 0.86), @@ -309,6 +330,7 @@ impl GuiElem for IdleDisplay { } fn updated_library(&mut self) { self.current_info.update = true; + self.force_reset_texts = true; } fn updated_queue(&mut self) { self.current_info.update = true; diff --git a/musicdb-client/src/gui_playpause.rs b/musicdb-client/src/gui_playpause.rs index 8a6e53a..70e49a2 100644 --- a/musicdb-client/src/gui_playpause.rs +++ b/musicdb-client/src/gui_playpause.rs @@ -1,3 +1,5 @@ +use std::sync::{atomic::AtomicBool, Arc}; + use musicdb_lib::server::Command; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; @@ -8,17 +10,44 @@ use crate::{ pub struct PlayPause { config: GuiElemCfg, + set_fav: Button<[FavIcon; 1]>, to_zero: Button<[Panel<()>; 1]>, play_pause: Button<[PlayPauseDisplay; 1]>, to_end: Button<[NextSongShape; 1]>, } impl PlayPause { - pub fn new(config: GuiElemCfg) -> Self { + pub fn new(config: GuiElemCfg, is_fav: Arc) -> Self { Self { config, + set_fav: Button::new( + GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.01), (0.24, 0.99))), + |_| { + vec![GuiAction::Build(Box::new(|db| { + if let Some(song_id) = db.queue.get_current_song() { + if let Some(song) = db.get_song(song_id) { + vec![GuiAction::SendToServer( + if song.general.tags.iter().any(|v| v == "Fav") { + Command::TagSongFlagUnset(*song_id, "Fav".to_owned()) + } else { + Command::TagSongFlagSet(*song_id, "Fav".to_owned()) + }, + )] + } else { + vec![] + } + } else { + vec![] + } + }))] + }, + [FavIcon::new( + GuiElemCfg::at(Rectangle::from_tuples((0.2, 0.2), (0.8, 0.8))), + is_fav, + )], + ), to_zero: Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.3, 1.0))), + GuiElemCfg::at(Rectangle::from_tuples((0.26, 0.01), (0.49, 0.99))), |_| vec![GuiAction::SendToServer(Command::Stop)], [Panel::with_background( GuiElemCfg::at(Rectangle::from_tuples((0.2, 0.2), (0.8, 0.8))), @@ -27,7 +56,7 @@ impl PlayPause { )], ), play_pause: Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.35, 0.0), (0.65, 1.0))), + GuiElemCfg::at(Rectangle::from_tuples((0.51, 0.01), (0.74, 0.99))), |btn| { vec![GuiAction::SendToServer(if btn.children[0].is_playing { Command::Pause @@ -40,7 +69,7 @@ impl PlayPause { ))], ), to_end: Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.7, 0.0), (1.0, 1.0))), + GuiElemCfg::at(Rectangle::from_tuples((0.76, 0.01), (0.99, 0.99))), |_| vec![GuiAction::SendToServer(Command::NextSong)], [NextSongShape::new(GuiElemCfg::at(Rectangle::from_tuples( (0.2, 0.2), @@ -175,6 +204,90 @@ impl GuiElem for NextSongShape { } } +struct FavIcon { + config: GuiElemCfg, + is_fav: Arc, +} +impl FavIcon { + pub fn new(config: GuiElemCfg, is_fav: Arc) -> Self { + Self { config, is_fav } + } +} +impl GuiElem for FavIcon { + fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { + let clr = if self.is_fav.load(std::sync::atomic::Ordering::Relaxed) { + Color::from_rgb(0.7, 0.1, 0.1) + } else { + Color::from_rgb(0.3, 0.2, 0.2) + }; + let pos = if info.pos.width() > info.pos.height() { + let c = info.pos.top_left().x + info.pos.width() * 0.5; + let d = info.pos.height() * 0.5; + Rectangle::from_tuples( + (c - d, info.pos.top_left().y), + (c + d, info.pos.bottom_right().y), + ) + } else if info.pos.height() > info.pos.width() { + let c = info.pos.top_left().y + info.pos.height() * 0.5; + let d = info.pos.width() * 0.5; + Rectangle::from_tuples( + (info.pos.top_left().x, c - d), + (info.pos.bottom_right().x, c + d), + ) + } else { + info.pos.clone() + }; + let circle_radius = 0.25; + let out_dist = pos.height() * circle_radius * std::f32::consts::SQRT_2 * 0.5; + let x_cntr = pos.top_left().x + pos.width() * 0.5; + let left_circle_cntr = Vec2::new( + pos.top_left().x + pos.width() * circle_radius, + pos.top_left().y + pos.height() * circle_radius, + ); + let right_circle_cntr = Vec2::new( + pos.bottom_right().x - pos.width() * circle_radius, + pos.top_left().y + pos.height() * circle_radius, + ); + let circle_radius = circle_radius * pos.height(); + let x1 = x_cntr - circle_radius - out_dist; + let x2 = x_cntr + circle_radius + out_dist; + let h1 = pos.top_left().y + circle_radius; + let h2 = pos.top_left().y + circle_radius + out_dist; + g.draw_circle(left_circle_cntr, circle_radius, clr); + g.draw_circle(right_circle_cntr, circle_radius, clr); + g.draw_rectangle(Rectangle::from_tuples((x1, h1), (x2, h2)), clr); + g.draw_triangle( + [ + Vec2::new(x1, h2), + Vec2::new(x2, h2), + Vec2::new(x_cntr, pos.bottom_right().y), + ], + clr, + ) + } + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + fn children(&mut self) -> Box + '_> { + Box::new([].into_iter()) + } + fn any(&self) -> &dyn std::any::Any { + self + } + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn elem(&self) -> &dyn GuiElem { + self + } + fn elem_mut(&mut self) -> &mut dyn GuiElem { + self + } +} + impl GuiElem for PlayPause { fn config(&self) -> &GuiElemCfg { &self.config @@ -185,6 +298,7 @@ impl GuiElem for PlayPause { fn children(&mut self) -> Box + '_> { Box::new( [ + self.set_fav.elem_mut(), self.to_zero.elem_mut(), self.play_pause.elem_mut(), self.to_end.elem_mut(), diff --git a/musicdb-client/src/gui_settings.rs b/musicdb-client/src/gui_settings.rs index 788164a..c629d8a 100755 --- a/musicdb-client/src/gui_settings.rs +++ b/musicdb-client/src/gui_settings.rs @@ -1,3 +1,4 @@ +use musicdb_lib::server::Command; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; use crate::{ @@ -55,6 +56,7 @@ pub struct SettingsContent { pub line_height: Panel<(Label, Slider)>, pub scroll_sensitivity: Panel<(Label, Slider)>, pub idle_time: Panel<(Label, Slider)>, + pub save_button: Button<[Label; 1]>, } impl GuiElemChildren for SettingsContent { fn iter(&mut self) -> Box + '_> { @@ -259,6 +261,17 @@ impl SettingsContent { ), ), ), + save_button: Button::new( + GuiElemCfg::default(), + |_| vec![GuiAction::SendToServer(Command::Save)], + [Label::new( + GuiElemCfg::default(), + "Server: Save Changes".to_string(), + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + ), } } } diff --git a/musicdb-client/src/gui_statusbar.rs b/musicdb-client/src/gui_statusbar.rs index b4a5f1a..7226bab 100644 --- a/musicdb-client/src/gui_statusbar.rs +++ b/musicdb-client/src/gui_statusbar.rs @@ -1,4 +1,7 @@ -use std::time::Instant; +use std::{ + sync::{atomic::AtomicBool, Arc}, + time::Instant, +}; use speedy2d::{dimen::Vec2, shape::Rectangle}; @@ -18,10 +21,12 @@ pub struct StatusBar { c_song_label: AdvancedLabel, pub force_reset_texts: bool, c_buttons: PlayPause, + is_fav: (bool, Arc), } impl StatusBar { pub fn new(config: GuiElemCfg) -> Self { + let is_fav = Arc::new(AtomicBool::new(false)); Self { config, idle_mode: 0.0, @@ -37,7 +42,8 @@ impl StatusBar { ), c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), force_reset_texts: false, - c_buttons: PlayPause::new(GuiElemCfg::default()), + is_fav: (false, Arc::clone(&is_fav)), + c_buttons: PlayPause::new(GuiElemCfg::default(), is_fav), } } } @@ -50,6 +56,20 @@ impl GuiElem for StatusBar { self.current_info.update(info, g); if self.current_info.new_song || self.force_reset_texts { self.current_info.new_song = false; + self.force_reset_texts = false; + let is_fav = self + .current_info + .current_song + .and_then(|id| info.database.get_song(&id)) + .map(|song| song.general.tags.iter().any(|v| v == "Fav")) + .unwrap_or(false); + eprintln!("is_fav: {is_fav}"); + if self.is_fav.0 != is_fav { + self.is_fav.0 = is_fav; + self.is_fav + .1 + .store(is_fav, std::sync::atomic::Ordering::Relaxed); + } self.c_song_label.content = if let Some(song) = self.current_info.current_song { info.gui_config .status_bar_text @@ -78,7 +98,7 @@ impl GuiElem for StatusBar { } // limit width of c_buttons let buttons_right_pos = 0.99; - let buttons_width_max = info.pos.height() * 0.7 / 0.3 / info.pos.width(); + let buttons_width_max = info.pos.height() * 0.7 * 4.0 / info.pos.width(); let buttons_width = buttons_width_max.min(0.2); self.c_buttons.config_mut().pos = Rectangle::from_tuples( (buttons_right_pos - buttons_width, 0.15), @@ -130,6 +150,7 @@ impl GuiElem for StatusBar { } fn updated_library(&mut self) { self.current_info.update = true; + self.force_reset_texts = true; } fn updated_queue(&mut self) { self.current_info.update = true; diff --git a/musicdb-lib/src/data/database.rs b/musicdb-lib/src/data/database.rs index 07f0c15..c52d3de 100755 --- a/musicdb-lib/src/data/database.rs +++ b/musicdb-lib/src/data/database.rs @@ -359,6 +359,96 @@ impl Database { Command::RemoveArtist(artist) => { _ = self.remove_artist(artist); } + Command::TagSongFlagSet(id, tag) => { + if let Some(v) = self.get_song_mut(&id) { + if !v.general.tags.contains(&tag) { + v.general.tags.push(tag); + } + } + }, + Command::TagSongFlagUnset(id, tag) => { + if let Some(v) = self.get_song_mut(&id) { + if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { + v.general.tags.remove(i); + } + } + }, + Command::TagAlbumFlagSet(id, tag) => { + if let Some(v) = self.albums.get_mut(&id) { + if !v.general.tags.contains(&tag) { + v.general.tags.push(tag); + } + } + }, + Command::TagAlbumFlagUnset(id, tag) => { + if let Some(v) = self.albums.get_mut(&id) { + if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { + v.general.tags.remove(i); + } + } + }, + Command::TagArtistFlagSet(id, tag) => { + if let Some(v) = self.artists.get_mut(&id) { + if !v.general.tags.contains(&tag) { + v.general.tags.push(tag); + } + } + }, + Command::TagArtistFlagUnset(id, tag) => { + if let Some(v) = self.artists.get_mut(&id) { + if let Some(i) = v.general.tags.iter().position(|v| v == &tag) { + v.general.tags.remove(i); + } + } + }, + Command::TagSongPropertySet(id, key, val) => { + if let Some(v) = self.get_song_mut(&id) { + let new = format!("{key}{val}"); + if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { + *v = new; + } else { + v.general.tags.push(new); + } + } + } + Command::TagSongPropertyUnset(id, key) => { + if let Some(v) = self.get_song_mut(&id) { + let tags = std::mem::replace(&mut v.general.tags, vec![]); + v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); + } + } + Command::TagAlbumPropertySet(id, key, val) => { + if let Some(v) = self.albums.get_mut(&id) { + let new = format!("{key}{val}"); + if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { + *v = new; + } else { + v.general.tags.push(new); + } + } + } + Command::TagAlbumPropertyUnset(id, key) => { + if let Some(v) = self.albums.get_mut(&id) { + let tags = std::mem::replace(&mut v.general.tags, vec![]); + v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); + } + } + Command::TagArtistPropertySet(id, key, val) => { + if let Some(v) = self.artists.get_mut(&id) { + let new = format!("{key}{val}"); + if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) { + *v = new; + } else { + v.general.tags.push(new); + } + } + } + Command::TagArtistPropertyUnset(id, key) => { + if let Some(v) = self.artists.get_mut(&id) { + let tags = std::mem::replace(&mut v.general.tags, vec![]); + v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect(); + } + } Command::SetSongDuration(id, duration) => { if let Some(song) = self.get_song_mut(&id) { song.duration_millis = duration; diff --git a/musicdb-lib/src/server/mod.rs b/musicdb-lib/src/server/mod.rs index d30c0a0..fd63bad 100755 --- a/musicdb-lib/src/server/mod.rs +++ b/musicdb-lib/src/server/mod.rs @@ -31,7 +31,6 @@ pub enum Command { Resume, Pause, Stop, - Save, NextSong, SyncDatabase(Vec, Vec, Vec), QueueUpdate(Vec, Queue), @@ -40,6 +39,7 @@ pub enum Command { QueueRemove(Vec), QueueGoto(Vec), QueueSetShuffle(Vec, Vec), + /// .id field is ignored! AddSong(Song), /// .id field is ignored! @@ -54,7 +54,26 @@ pub enum Command { RemoveArtist(ArtistId), ModifyArtist(Artist), SetSongDuration(SongId, u64), + /// Add the given Tag to the song's tags, if it isn't set already. + TagSongFlagSet(SongId, String), + /// Remove the given Tag fron the song's tags, if it exists. + TagSongFlagUnset(SongId, String), + TagAlbumFlagSet(AlbumId, String), + TagAlbumFlagUnset(AlbumId, String), + TagArtistFlagSet(ArtistId, String), + TagArtistFlagUnset(ArtistId, String), + /// For the arguments `Key`, `Val`: If the song has a Tag `Key`, it will be removed. Then, `KeyVal` will be added. + /// For example, to set "Year=2010", Key would be "Year=", and Val would be "2010". Then, "Year=1990", ..., would be removed and "Year=2010" would be added. + TagSongPropertySet(SongId, String, String), + /// For the arguments `Key`, `Val`: If the song has a Tag `Key`, it will be removed. + TagSongPropertyUnset(SongId, String), + TagAlbumPropertySet(AlbumId, String, String), + TagAlbumPropertyUnset(AlbumId, String), + TagArtistPropertySet(ArtistId, String, String), + TagArtistPropertyUnset(ArtistId, String), + InitComplete, + Save, ErrorInfo(String, String), } impl Command { @@ -195,102 +214,208 @@ pub fn handle_one_connection_as_control( } } } + +const BYTE_RESUME: u8 = 0b11000000; +const BYTE_PAUSE: u8 = 0b00110000; +const BYTE_STOP: u8 = 0b11110000; +const BYTE_NEXT_SONG: u8 = 0b11110010; + +const BYTE_SYNC_DATABASE: u8 = 0b01011000; +const BYTE_QUEUE_UPDATE: u8 = 0b00011100; +const BYTE_QUEUE_ADD: u8 = 0b00011010; +const BYTE_QUEUE_INSERT: u8 = 0b00011110; +const BYTE_QUEUE_REMOVE: u8 = 0b00011001; +const BYTE_QUEUE_GOTO: u8 = 0b00011011; +const BYTE_QUEUE_SET_SHUFFLE: u8 = 0b10011011; + +const BYTE_ADD_SONG: u8 = 0b01010000; +const BYTE_ADD_ALBUM: u8 = 0b01010011; +const BYTE_ADD_ARTIST: u8 = 0b01011100; +const BYTE_ADD_COVER: u8 = 0b01011101; +const BYTE_MODIFY_SONG: u8 = 0b10010000; +const BYTE_MODIFY_ALBUM: u8 = 0b10010011; +const BYTE_MODIFY_ARTIST: u8 = 0b10011100; +const BYTE_REMOVE_SONG: u8 = 0b11010000; +const BYTE_REMOVE_ALBUM: u8 = 0b11010011; +const BYTE_REMOVE_ARTIST: u8 = 0b11011100; +const BYTE_TAG_SONG_FLAG_SET: u8 = 0b11100000; +const BYTE_TAG_SONG_FLAG_UNSET: u8 = 0b11100001; +const BYTE_TAG_ALBUM_FLAG_SET: u8 = 0b11100010; +const BYTE_TAG_ALBUM_FLAG_UNSET: u8 = 0b11100011; +const BYTE_TAG_ARTIST_FLAG_SET: u8 = 0b11100110; +const BYTE_TAG_ARTIST_FLAG_UNSET: u8 = 0b11100111; +const BYTE_TAG_SONG_PROPERTY_SET: u8 = 0b11101001; +const BYTE_TAG_SONG_PROPERTY_UNSET: u8 = 0b11101010; +const BYTE_TAG_ALBUM_PROPERTY_SET: u8 = 0b11101011; +const BYTE_TAG_ALBUM_PROPERTY_UNSET: u8 = 0b11101100; +const BYTE_TAG_ARTIST_PROPERTY_SET: u8 = 0b11101110; +const BYTE_TAG_ARTIST_PROPERTY_UNSET: u8 = 0b11101111; + +const BYTE_SET_SONG_DURATION: u8 = 0b11111000; + +const BYTE_INIT_COMPLETE: u8 = 0b00110001; +const BYTE_SAVE: u8 = 0b11110011; +const BYTE_ERRORINFO: u8 = 0b11011011; + impl ToFromBytes for Command { fn to_bytes(&self, s: &mut T) -> Result<(), std::io::Error> where T: Write, { match self { - Self::Resume => s.write_all(&[0b11000000])?, - Self::Pause => s.write_all(&[0b00110000])?, - Self::Stop => s.write_all(&[0b11110000])?, - Self::Save => s.write_all(&[0b11110011])?, - Self::NextSong => s.write_all(&[0b11110010])?, + Self::Resume => s.write_all(&[BYTE_RESUME])?, + Self::Pause => s.write_all(&[BYTE_PAUSE])?, + Self::Stop => s.write_all(&[BYTE_STOP])?, + Self::NextSong => s.write_all(&[BYTE_NEXT_SONG])?, Self::SyncDatabase(a, b, c) => { - s.write_all(&[0b01011000])?; + s.write_all(&[BYTE_SYNC_DATABASE])?; a.to_bytes(s)?; b.to_bytes(s)?; c.to_bytes(s)?; } Self::QueueUpdate(index, new_data) => { - s.write_all(&[0b00011100])?; + s.write_all(&[BYTE_QUEUE_UPDATE])?; index.to_bytes(s)?; new_data.to_bytes(s)?; } Self::QueueAdd(index, new_data) => { - s.write_all(&[0b00011010])?; + s.write_all(&[BYTE_QUEUE_ADD])?; index.to_bytes(s)?; new_data.to_bytes(s)?; } Self::QueueInsert(index, pos, new_data) => { - s.write_all(&[0b00011110])?; + s.write_all(&[BYTE_QUEUE_INSERT])?; index.to_bytes(s)?; pos.to_bytes(s)?; new_data.to_bytes(s)?; } Self::QueueRemove(index) => { - s.write_all(&[0b00011001])?; + s.write_all(&[BYTE_QUEUE_REMOVE])?; index.to_bytes(s)?; } Self::QueueGoto(index) => { - s.write_all(&[0b00011011])?; + s.write_all(&[BYTE_QUEUE_GOTO])?; index.to_bytes(s)?; } Self::QueueSetShuffle(path, map) => { - s.write_all(&[0b10011011])?; + s.write_all(&[BYTE_QUEUE_SET_SHUFFLE])?; path.to_bytes(s)?; map.to_bytes(s)?; } Self::AddSong(song) => { - s.write_all(&[0b01010000])?; + s.write_all(&[BYTE_ADD_SONG])?; song.to_bytes(s)?; } Self::AddAlbum(album) => { - s.write_all(&[0b01010011])?; + s.write_all(&[BYTE_ADD_ALBUM])?; album.to_bytes(s)?; } Self::AddArtist(artist) => { - s.write_all(&[0b01011100])?; + s.write_all(&[BYTE_ADD_ARTIST])?; artist.to_bytes(s)?; } Self::AddCover(cover) => { - s.write_all(&[0b01011101])?; + s.write_all(&[BYTE_ADD_COVER])?; cover.to_bytes(s)?; } Self::ModifySong(song) => { - s.write_all(&[0b10010000])?; + s.write_all(&[BYTE_MODIFY_SONG])?; song.to_bytes(s)?; } Self::ModifyAlbum(album) => { - s.write_all(&[0b10010011])?; + s.write_all(&[BYTE_MODIFY_ALBUM])?; album.to_bytes(s)?; } Self::ModifyArtist(artist) => { - s.write_all(&[0b10011100])?; + s.write_all(&[BYTE_MODIFY_ARTIST])?; artist.to_bytes(s)?; } Self::RemoveSong(song) => { - s.write_all(&[0b11010000])?; + s.write_all(&[BYTE_REMOVE_SONG])?; song.to_bytes(s)?; } Self::RemoveAlbum(album) => { - s.write_all(&[0b11010011])?; + s.write_all(&[BYTE_REMOVE_ALBUM])?; album.to_bytes(s)?; } Self::RemoveArtist(artist) => { - s.write_all(&[0b11011100])?; + s.write_all(&[BYTE_REMOVE_ARTIST])?; artist.to_bytes(s)?; } + Self::TagSongFlagSet(id, tag) => { + s.write_all(&[BYTE_TAG_SONG_FLAG_SET])?; + id.to_bytes(s)?; + tag.to_bytes(s)?; + } + Self::TagSongFlagUnset(id, tag) => { + s.write_all(&[BYTE_TAG_SONG_FLAG_UNSET])?; + id.to_bytes(s)?; + tag.to_bytes(s)?; + } + Self::TagAlbumFlagSet(id, tag) => { + s.write_all(&[BYTE_TAG_ALBUM_FLAG_SET])?; + id.to_bytes(s)?; + tag.to_bytes(s)?; + } + Self::TagAlbumFlagUnset(id, tag) => { + s.write_all(&[BYTE_TAG_ALBUM_FLAG_UNSET])?; + id.to_bytes(s)?; + tag.to_bytes(s)?; + } + Self::TagArtistFlagSet(id, tag) => { + s.write_all(&[BYTE_TAG_ARTIST_FLAG_SET])?; + id.to_bytes(s)?; + tag.to_bytes(s)?; + } + Self::TagArtistFlagUnset(id, tag) => { + s.write_all(&[BYTE_TAG_ARTIST_FLAG_UNSET])?; + id.to_bytes(s)?; + tag.to_bytes(s)?; + } + Self::TagSongPropertySet(id, key, val) => { + s.write_all(&[BYTE_TAG_SONG_PROPERTY_SET])?; + id.to_bytes(s)?; + key.to_bytes(s)?; + val.to_bytes(s)?; + } + Self::TagSongPropertyUnset(id, key) => { + s.write_all(&[BYTE_TAG_SONG_PROPERTY_UNSET])?; + id.to_bytes(s)?; + key.to_bytes(s)?; + } + Self::TagAlbumPropertySet(id, key, val) => { + s.write_all(&[BYTE_TAG_ALBUM_PROPERTY_SET])?; + id.to_bytes(s)?; + key.to_bytes(s)?; + val.to_bytes(s)?; + } + Self::TagAlbumPropertyUnset(id, key) => { + s.write_all(&[BYTE_TAG_ALBUM_PROPERTY_UNSET])?; + id.to_bytes(s)?; + key.to_bytes(s)?; + } + Self::TagArtistPropertySet(id, key, val) => { + s.write_all(&[BYTE_TAG_ARTIST_PROPERTY_SET])?; + id.to_bytes(s)?; + key.to_bytes(s)?; + val.to_bytes(s)?; + } + Self::TagArtistPropertyUnset(id, key) => { + s.write_all(&[BYTE_TAG_ARTIST_PROPERTY_UNSET])?; + id.to_bytes(s)?; + key.to_bytes(s)?; + } Self::SetSongDuration(i, d) => { - s.write_all(&[0b11100000])?; + s.write_all(&[BYTE_SET_SONG_DURATION])?; i.to_bytes(s)?; d.to_bytes(s)?; } Self::InitComplete => { - s.write_all(&[0b00110001])?; + s.write_all(&[BYTE_INIT_COMPLETE])?; } + Self::Save => s.write_all(&[BYTE_SAVE])?, Self::ErrorInfo(t, d) => { - s.write_all(&[0b11011011])?; + s.write_all(&[BYTE_ERRORINFO])?; t.to_bytes(s)?; d.to_bytes(s)?; } @@ -303,46 +428,61 @@ impl ToFromBytes for Command { { let mut kind = [0]; s.read_exact(&mut kind)?; + macro_rules! from_bytes { + () => { + ToFromBytes::from_bytes(s)? + }; + } Ok(match kind[0] { - 0b11000000 => Self::Resume, - 0b00110000 => Self::Pause, - 0b11110000 => Self::Stop, - 0b11110011 => Self::Save, - 0b11110010 => Self::NextSong, - 0b01011000 => Self::SyncDatabase( - ToFromBytes::from_bytes(s)?, - ToFromBytes::from_bytes(s)?, - ToFromBytes::from_bytes(s)?, - ), - 0b00011100 => { - Self::QueueUpdate(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?) + BYTE_RESUME => Self::Resume, + BYTE_PAUSE => Self::Pause, + BYTE_STOP => Self::Stop, + BYTE_NEXT_SONG => Self::NextSong, + BYTE_SYNC_DATABASE => Self::SyncDatabase(from_bytes!(), from_bytes!(), from_bytes!()), + BYTE_QUEUE_UPDATE => Self::QueueUpdate(from_bytes!(), from_bytes!()), + BYTE_QUEUE_ADD => Self::QueueAdd(from_bytes!(), from_bytes!()), + BYTE_QUEUE_INSERT => Self::QueueInsert(from_bytes!(), from_bytes!(), from_bytes!()), + BYTE_QUEUE_REMOVE => Self::QueueRemove(from_bytes!()), + BYTE_QUEUE_GOTO => Self::QueueGoto(from_bytes!()), + BYTE_QUEUE_SET_SHUFFLE => Self::QueueSetShuffle(from_bytes!(), from_bytes!()), + BYTE_ADD_SONG => Self::AddSong(from_bytes!()), + BYTE_ADD_ALBUM => Self::AddAlbum(from_bytes!()), + BYTE_ADD_ARTIST => Self::AddArtist(from_bytes!()), + BYTE_MODIFY_SONG => Self::ModifySong(from_bytes!()), + BYTE_MODIFY_ALBUM => Self::ModifyAlbum(from_bytes!()), + BYTE_MODIFY_ARTIST => Self::ModifyArtist(from_bytes!()), + BYTE_REMOVE_SONG => Self::RemoveSong(from_bytes!()), + BYTE_REMOVE_ALBUM => Self::RemoveAlbum(from_bytes!()), + BYTE_REMOVE_ARTIST => Self::RemoveArtist(from_bytes!()), + BYTE_TAG_SONG_FLAG_SET => Self::TagSongFlagSet(from_bytes!(), from_bytes!()), + BYTE_TAG_SONG_FLAG_UNSET => Self::TagSongFlagUnset(from_bytes!(), from_bytes!()), + BYTE_TAG_ALBUM_FLAG_SET => Self::TagAlbumFlagSet(from_bytes!(), from_bytes!()), + BYTE_TAG_ALBUM_FLAG_UNSET => Self::TagAlbumFlagUnset(from_bytes!(), from_bytes!()), + BYTE_TAG_ARTIST_FLAG_SET => Self::TagArtistFlagSet(from_bytes!(), from_bytes!()), + BYTE_TAG_ARTIST_FLAG_UNSET => Self::TagArtistFlagUnset(from_bytes!(), from_bytes!()), + BYTE_TAG_SONG_PROPERTY_SET => { + Self::TagSongPropertySet(from_bytes!(), from_bytes!(), from_bytes!()) } - 0b00011010 => Self::QueueAdd(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?), - 0b00011110 => Self::QueueInsert( - ToFromBytes::from_bytes(s)?, - ToFromBytes::from_bytes(s)?, - ToFromBytes::from_bytes(s)?, - ), - 0b00011001 => Self::QueueRemove(ToFromBytes::from_bytes(s)?), - 0b00011011 => Self::QueueGoto(ToFromBytes::from_bytes(s)?), - 0b10011011 => { - Self::QueueSetShuffle(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?) + BYTE_TAG_SONG_PROPERTY_UNSET => { + Self::TagSongPropertyUnset(from_bytes!(), from_bytes!()) } - 0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?), - 0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?), - 0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?), - 0b10010000 => Self::ModifySong(ToFromBytes::from_bytes(s)?), - 0b10010011 => Self::ModifyAlbum(ToFromBytes::from_bytes(s)?), - 0b10011100 => Self::ModifyArtist(ToFromBytes::from_bytes(s)?), - 0b11010000 => Self::RemoveSong(ToFromBytes::from_bytes(s)?), - 0b11010011 => Self::RemoveAlbum(ToFromBytes::from_bytes(s)?), - 0b11011100 => Self::RemoveArtist(ToFromBytes::from_bytes(s)?), - 0b11100000 => { - Self::SetSongDuration(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?) + BYTE_TAG_ALBUM_PROPERTY_SET => { + Self::TagAlbumPropertySet(from_bytes!(), from_bytes!(), from_bytes!()) } - 0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?), - 0b00110001 => Self::InitComplete, - 0b11011011 => Self::ErrorInfo(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?), + BYTE_TAG_ALBUM_PROPERTY_UNSET => { + Self::TagAlbumPropertyUnset(from_bytes!(), from_bytes!()) + } + BYTE_TAG_ARTIST_PROPERTY_SET => { + Self::TagArtistPropertySet(from_bytes!(), from_bytes!(), from_bytes!()) + } + BYTE_TAG_ARTIST_PROPERTY_UNSET => { + Self::TagArtistPropertyUnset(from_bytes!(), from_bytes!()) + } + BYTE_SET_SONG_DURATION => Self::SetSongDuration(from_bytes!(), from_bytes!()), + BYTE_ADD_COVER => Self::AddCover(from_bytes!()), + BYTE_INIT_COMPLETE => Self::InitComplete, + BYTE_SAVE => Self::Save, + BYTE_ERRORINFO => Self::ErrorInfo(from_bytes!(), from_bytes!()), _ => { eprintln!("unexpected byte when reading command; stopping playback."); Self::Stop diff --git a/musicdb-server/src/web.rs b/musicdb-server/src/web.rs index 9931845..ae0961d 100755 --- a/musicdb-server/src/web.rs +++ b/musicdb-server/src/web.rs @@ -354,6 +354,18 @@ async fn sse_handler( | Command::RemoveSong(_) | Command::RemoveAlbum(_) | Command::RemoveArtist(_) + | Command::TagSongFlagSet(..) + | Command::TagSongFlagUnset(..) + | Command::TagAlbumFlagSet(..) + | Command::TagAlbumFlagUnset(..) + | Command::TagArtistFlagSet(..) + | Command::TagArtistFlagUnset(..) + | Command::TagSongPropertySet(..) + | Command::TagSongPropertyUnset(..) + | Command::TagAlbumPropertySet(..) + | Command::TagAlbumPropertyUnset(..) + | Command::TagArtistPropertySet(..) + | Command::TagArtistPropertyUnset(..) | Command::SetSongDuration(..) => Event::default().event("artists").data({ let db = state.db.lock().unwrap(); let mut a = db.artists().iter().collect::>();