add ways to modify tags, and add a Fav button to client

This commit is contained in:
Mark 2023-12-30 18:12:02 +01:00
parent daad5c6aae
commit b848a0d511
8 changed files with 501 additions and 77 deletions

View File

@ -234,7 +234,7 @@ pub fn main(
), ),
( (
"Year".to_owned(), "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![ filter_presets_album: vec![
@ -244,7 +244,7 @@ pub fn main(
), ),
( (
"Year".to_owned(), "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![ filter_presets_artist: vec![
@ -254,7 +254,7 @@ pub fn main(
), ),
( (
"Year".to_owned(), "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")] #[cfg(feature = "merscfg")]
@ -371,6 +371,18 @@ impl Gui {
| Command::RemoveSong(_) | Command::RemoveSong(_)
| Command::RemoveAlbum(_) | Command::RemoveAlbum(_)
| Command::RemoveArtist(_) | 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(..) => { | 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);

View File

@ -1,4 +1,7 @@
use std::{sync::Arc, time::Instant}; use std::{
sync::{atomic::AtomicBool, Arc},
time::Instant,
};
use musicdb_lib::data::ArtistId; use musicdb_lib::data::ArtistId;
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; 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 artist_image_to_cover_margin: f32,
pub force_reset_texts: bool, pub force_reset_texts: bool,
is_fav: (bool, Arc<AtomicBool>),
} }
impl IdleDisplay { impl IdleDisplay {
pub fn new(config: GuiElemCfg) -> Self { pub fn new(config: GuiElemCfg) -> Self {
let cover_bottom = 0.79; let cover_bottom = 0.79;
let is_fav = Arc::new(AtomicBool::new(false));
Self { Self {
config, config,
idle_mode: 0.0, 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_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_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, c_buttons_custom_pos: false,
cover_aspect_ratio: AnimationController::new( cover_aspect_ratio: AnimationController::new(
1.0, 1.0,
@ -122,6 +129,19 @@ impl GuiElem for IdleDisplay {
self.current_info.update(info, g); self.current_info.update(info, g);
if self.current_info.new_song || self.force_reset_texts { if self.current_info.new_song || self.force_reset_texts {
self.current_info.new_song = false; 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 { self.c_top_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
.idle_top_text .idle_top_text
@ -278,9 +298,10 @@ impl GuiElem for IdleDisplay {
self.c_side2_label.config_mut().pos = self.c_side2_label.config_mut().pos =
Rectangle::from_tuples((left, ai_top), (max_right, bottom)); Rectangle::from_tuples((left, ai_top), (max_right, bottom));
// limit width of c_buttons // limit width of c_buttons
let buttons_right_pos = 0.99; let buttons_right_pos = 1.0;
let buttons_width_max = info.pos.height() * 0.08 / 0.3 / info.pos.width(); let buttons_width_max = info.pos.height() * 0.08 * 4.0 / info.pos.width();
let buttons_width = buttons_width_max.min(0.2); // 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 { if !self.c_buttons_custom_pos {
self.c_buttons.config_mut().pos = Rectangle::from_tuples( self.c_buttons.config_mut().pos = Rectangle::from_tuples(
(buttons_right_pos - buttons_width, 0.86), (buttons_right_pos - buttons_width, 0.86),
@ -309,6 +330,7 @@ impl GuiElem for IdleDisplay {
} }
fn updated_library(&mut self) { fn updated_library(&mut self) {
self.current_info.update = true; self.current_info.update = true;
self.force_reset_texts = true;
} }
fn updated_queue(&mut self) { fn updated_queue(&mut self) {
self.current_info.update = true; self.current_info.update = true;

View File

@ -1,3 +1,5 @@
use std::sync::{atomic::AtomicBool, Arc};
use musicdb_lib::server::Command; use musicdb_lib::server::Command;
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
@ -8,17 +10,44 @@ use crate::{
pub struct PlayPause { pub struct PlayPause {
config: GuiElemCfg, config: GuiElemCfg,
set_fav: Button<[FavIcon; 1]>,
to_zero: Button<[Panel<()>; 1]>, to_zero: Button<[Panel<()>; 1]>,
play_pause: Button<[PlayPauseDisplay; 1]>, play_pause: Button<[PlayPauseDisplay; 1]>,
to_end: Button<[NextSongShape; 1]>, to_end: Button<[NextSongShape; 1]>,
} }
impl PlayPause { impl PlayPause {
pub fn new(config: GuiElemCfg) -> Self { pub fn new(config: GuiElemCfg, is_fav: Arc<AtomicBool>) -> Self {
Self { Self {
config, 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( 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)], |_| vec![GuiAction::SendToServer(Command::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))),
@ -27,7 +56,7 @@ impl PlayPause {
)], )],
), ),
play_pause: Button::new( 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| { |btn| {
vec![GuiAction::SendToServer(if btn.children[0].is_playing { vec![GuiAction::SendToServer(if btn.children[0].is_playing {
Command::Pause Command::Pause
@ -40,7 +69,7 @@ impl PlayPause {
))], ))],
), ),
to_end: Button::new( 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)], |_| vec![GuiAction::SendToServer(Command::NextSong)],
[NextSongShape::new(GuiElemCfg::at(Rectangle::from_tuples( [NextSongShape::new(GuiElemCfg::at(Rectangle::from_tuples(
(0.2, 0.2), (0.2, 0.2),
@ -175,6 +204,90 @@ impl GuiElem for NextSongShape {
} }
} }
struct FavIcon {
config: GuiElemCfg,
is_fav: Arc<AtomicBool>,
}
impl FavIcon {
pub fn new(config: GuiElemCfg, is_fav: Arc<AtomicBool>) -> 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<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
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 { impl GuiElem for PlayPause {
fn config(&self) -> &GuiElemCfg { fn config(&self) -> &GuiElemCfg {
&self.config &self.config
@ -185,6 +298,7 @@ impl GuiElem for PlayPause {
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> { fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new( Box::new(
[ [
self.set_fav.elem_mut(),
self.to_zero.elem_mut(), self.to_zero.elem_mut(),
self.play_pause.elem_mut(), self.play_pause.elem_mut(),
self.to_end.elem_mut(), self.to_end.elem_mut(),

View File

@ -1,3 +1,4 @@
use musicdb_lib::server::Command;
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
use crate::{ use crate::{
@ -55,6 +56,7 @@ pub struct SettingsContent {
pub line_height: Panel<(Label, Slider)>, pub line_height: Panel<(Label, Slider)>,
pub scroll_sensitivity: Panel<(Label, Slider)>, pub scroll_sensitivity: Panel<(Label, Slider)>,
pub idle_time: Panel<(Label, Slider)>, pub idle_time: Panel<(Label, Slider)>,
pub save_button: Button<[Label; 1]>,
} }
impl GuiElemChildren for SettingsContent { impl GuiElemChildren for SettingsContent {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> { fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
@ -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),
)],
),
} }
} }
} }

View File

@ -1,4 +1,7 @@
use std::time::Instant; use std::{
sync::{atomic::AtomicBool, Arc},
time::Instant,
};
use speedy2d::{dimen::Vec2, shape::Rectangle}; use speedy2d::{dimen::Vec2, shape::Rectangle};
@ -18,10 +21,12 @@ pub struct StatusBar {
c_song_label: AdvancedLabel, c_song_label: AdvancedLabel,
pub force_reset_texts: bool, pub force_reset_texts: bool,
c_buttons: PlayPause, c_buttons: PlayPause,
is_fav: (bool, Arc<AtomicBool>),
} }
impl StatusBar { impl StatusBar {
pub fn new(config: GuiElemCfg) -> Self { pub fn new(config: GuiElemCfg) -> Self {
let is_fav = Arc::new(AtomicBool::new(false));
Self { Self {
config, config,
idle_mode: 0.0, idle_mode: 0.0,
@ -37,7 +42,8 @@ impl StatusBar {
), ),
c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]),
force_reset_texts: false, 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); self.current_info.update(info, g);
if self.current_info.new_song || self.force_reset_texts { if self.current_info.new_song || self.force_reset_texts {
self.current_info.new_song = false; 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 { self.c_song_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
.status_bar_text .status_bar_text
@ -78,7 +98,7 @@ impl GuiElem for StatusBar {
} }
// limit width of c_buttons // limit width of c_buttons
let buttons_right_pos = 0.99; 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); let buttons_width = buttons_width_max.min(0.2);
self.c_buttons.config_mut().pos = Rectangle::from_tuples( self.c_buttons.config_mut().pos = Rectangle::from_tuples(
(buttons_right_pos - buttons_width, 0.15), (buttons_right_pos - buttons_width, 0.15),
@ -130,6 +150,7 @@ impl GuiElem for StatusBar {
} }
fn updated_library(&mut self) { fn updated_library(&mut self) {
self.current_info.update = true; self.current_info.update = true;
self.force_reset_texts = true;
} }
fn updated_queue(&mut self) { fn updated_queue(&mut self) {
self.current_info.update = true; self.current_info.update = true;

View File

@ -359,6 +359,96 @@ impl Database {
Command::RemoveArtist(artist) => { Command::RemoveArtist(artist) => {
_ = self.remove_artist(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) => { Command::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;

View File

@ -31,7 +31,6 @@ pub enum Command {
Resume, Resume,
Pause, Pause,
Stop, Stop,
Save,
NextSong, NextSong,
SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>), SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>),
QueueUpdate(Vec<usize>, Queue), QueueUpdate(Vec<usize>, Queue),
@ -40,6 +39,7 @@ pub enum Command {
QueueRemove(Vec<usize>), QueueRemove(Vec<usize>),
QueueGoto(Vec<usize>), QueueGoto(Vec<usize>),
QueueSetShuffle(Vec<usize>, Vec<usize>), QueueSetShuffle(Vec<usize>, Vec<usize>),
/// .id field is ignored! /// .id field is ignored!
AddSong(Song), AddSong(Song),
/// .id field is ignored! /// .id field is ignored!
@ -54,7 +54,26 @@ pub enum Command {
RemoveArtist(ArtistId), RemoveArtist(ArtistId),
ModifyArtist(Artist), ModifyArtist(Artist),
SetSongDuration(SongId, u64), 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<anything>`, 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<anything>`, it will be removed.
TagSongPropertyUnset(SongId, String),
TagAlbumPropertySet(AlbumId, String, String),
TagAlbumPropertyUnset(AlbumId, String),
TagArtistPropertySet(ArtistId, String, String),
TagArtistPropertyUnset(ArtistId, String),
InitComplete, InitComplete,
Save,
ErrorInfo(String, String), ErrorInfo(String, String),
} }
impl Command { 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 { impl ToFromBytes for Command {
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,
{ {
match self { match self {
Self::Resume => s.write_all(&[0b11000000])?, Self::Resume => s.write_all(&[BYTE_RESUME])?,
Self::Pause => s.write_all(&[0b00110000])?, Self::Pause => s.write_all(&[BYTE_PAUSE])?,
Self::Stop => s.write_all(&[0b11110000])?, Self::Stop => s.write_all(&[BYTE_STOP])?,
Self::Save => s.write_all(&[0b11110011])?, Self::NextSong => s.write_all(&[BYTE_NEXT_SONG])?,
Self::NextSong => s.write_all(&[0b11110010])?,
Self::SyncDatabase(a, b, c) => { Self::SyncDatabase(a, b, c) => {
s.write_all(&[0b01011000])?; s.write_all(&[BYTE_SYNC_DATABASE])?;
a.to_bytes(s)?; a.to_bytes(s)?;
b.to_bytes(s)?; b.to_bytes(s)?;
c.to_bytes(s)?; c.to_bytes(s)?;
} }
Self::QueueUpdate(index, new_data) => { Self::QueueUpdate(index, new_data) => {
s.write_all(&[0b00011100])?; s.write_all(&[BYTE_QUEUE_UPDATE])?;
index.to_bytes(s)?; index.to_bytes(s)?;
new_data.to_bytes(s)?; new_data.to_bytes(s)?;
} }
Self::QueueAdd(index, new_data) => { Self::QueueAdd(index, new_data) => {
s.write_all(&[0b00011010])?; s.write_all(&[BYTE_QUEUE_ADD])?;
index.to_bytes(s)?; index.to_bytes(s)?;
new_data.to_bytes(s)?; new_data.to_bytes(s)?;
} }
Self::QueueInsert(index, pos, new_data) => { Self::QueueInsert(index, pos, new_data) => {
s.write_all(&[0b00011110])?; s.write_all(&[BYTE_QUEUE_INSERT])?;
index.to_bytes(s)?; index.to_bytes(s)?;
pos.to_bytes(s)?; pos.to_bytes(s)?;
new_data.to_bytes(s)?; new_data.to_bytes(s)?;
} }
Self::QueueRemove(index) => { Self::QueueRemove(index) => {
s.write_all(&[0b00011001])?; s.write_all(&[BYTE_QUEUE_REMOVE])?;
index.to_bytes(s)?; index.to_bytes(s)?;
} }
Self::QueueGoto(index) => { Self::QueueGoto(index) => {
s.write_all(&[0b00011011])?; s.write_all(&[BYTE_QUEUE_GOTO])?;
index.to_bytes(s)?; index.to_bytes(s)?;
} }
Self::QueueSetShuffle(path, map) => { Self::QueueSetShuffle(path, map) => {
s.write_all(&[0b10011011])?; s.write_all(&[BYTE_QUEUE_SET_SHUFFLE])?;
path.to_bytes(s)?; path.to_bytes(s)?;
map.to_bytes(s)?; map.to_bytes(s)?;
} }
Self::AddSong(song) => { Self::AddSong(song) => {
s.write_all(&[0b01010000])?; s.write_all(&[BYTE_ADD_SONG])?;
song.to_bytes(s)?; song.to_bytes(s)?;
} }
Self::AddAlbum(album) => { Self::AddAlbum(album) => {
s.write_all(&[0b01010011])?; s.write_all(&[BYTE_ADD_ALBUM])?;
album.to_bytes(s)?; album.to_bytes(s)?;
} }
Self::AddArtist(artist) => { Self::AddArtist(artist) => {
s.write_all(&[0b01011100])?; s.write_all(&[BYTE_ADD_ARTIST])?;
artist.to_bytes(s)?; artist.to_bytes(s)?;
} }
Self::AddCover(cover) => { Self::AddCover(cover) => {
s.write_all(&[0b01011101])?; s.write_all(&[BYTE_ADD_COVER])?;
cover.to_bytes(s)?; cover.to_bytes(s)?;
} }
Self::ModifySong(song) => { Self::ModifySong(song) => {
s.write_all(&[0b10010000])?; s.write_all(&[BYTE_MODIFY_SONG])?;
song.to_bytes(s)?; song.to_bytes(s)?;
} }
Self::ModifyAlbum(album) => { Self::ModifyAlbum(album) => {
s.write_all(&[0b10010011])?; s.write_all(&[BYTE_MODIFY_ALBUM])?;
album.to_bytes(s)?; album.to_bytes(s)?;
} }
Self::ModifyArtist(artist) => { Self::ModifyArtist(artist) => {
s.write_all(&[0b10011100])?; s.write_all(&[BYTE_MODIFY_ARTIST])?;
artist.to_bytes(s)?; artist.to_bytes(s)?;
} }
Self::RemoveSong(song) => { Self::RemoveSong(song) => {
s.write_all(&[0b11010000])?; s.write_all(&[BYTE_REMOVE_SONG])?;
song.to_bytes(s)?; song.to_bytes(s)?;
} }
Self::RemoveAlbum(album) => { Self::RemoveAlbum(album) => {
s.write_all(&[0b11010011])?; s.write_all(&[BYTE_REMOVE_ALBUM])?;
album.to_bytes(s)?; album.to_bytes(s)?;
} }
Self::RemoveArtist(artist) => { Self::RemoveArtist(artist) => {
s.write_all(&[0b11011100])?; s.write_all(&[BYTE_REMOVE_ARTIST])?;
artist.to_bytes(s)?; 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) => { Self::SetSongDuration(i, d) => {
s.write_all(&[0b11100000])?; s.write_all(&[BYTE_SET_SONG_DURATION])?;
i.to_bytes(s)?; i.to_bytes(s)?;
d.to_bytes(s)?; d.to_bytes(s)?;
} }
Self::InitComplete => { Self::InitComplete => {
s.write_all(&[0b00110001])?; s.write_all(&[BYTE_INIT_COMPLETE])?;
} }
Self::Save => s.write_all(&[BYTE_SAVE])?,
Self::ErrorInfo(t, d) => { Self::ErrorInfo(t, d) => {
s.write_all(&[0b11011011])?; s.write_all(&[BYTE_ERRORINFO])?;
t.to_bytes(s)?; t.to_bytes(s)?;
d.to_bytes(s)?; d.to_bytes(s)?;
} }
@ -303,46 +428,61 @@ impl ToFromBytes for Command {
{ {
let mut kind = [0]; let mut kind = [0];
s.read_exact(&mut kind)?; s.read_exact(&mut kind)?;
macro_rules! from_bytes {
() => {
ToFromBytes::from_bytes(s)?
};
}
Ok(match kind[0] { Ok(match kind[0] {
0b11000000 => Self::Resume, BYTE_RESUME => Self::Resume,
0b00110000 => Self::Pause, BYTE_PAUSE => Self::Pause,
0b11110000 => Self::Stop, BYTE_STOP => Self::Stop,
0b11110011 => Self::Save, BYTE_NEXT_SONG => Self::NextSong,
0b11110010 => Self::NextSong, BYTE_SYNC_DATABASE => Self::SyncDatabase(from_bytes!(), from_bytes!(), from_bytes!()),
0b01011000 => Self::SyncDatabase( BYTE_QUEUE_UPDATE => Self::QueueUpdate(from_bytes!(), from_bytes!()),
ToFromBytes::from_bytes(s)?, BYTE_QUEUE_ADD => Self::QueueAdd(from_bytes!(), from_bytes!()),
ToFromBytes::from_bytes(s)?, BYTE_QUEUE_INSERT => Self::QueueInsert(from_bytes!(), from_bytes!(), from_bytes!()),
ToFromBytes::from_bytes(s)?, BYTE_QUEUE_REMOVE => Self::QueueRemove(from_bytes!()),
), BYTE_QUEUE_GOTO => Self::QueueGoto(from_bytes!()),
0b00011100 => { BYTE_QUEUE_SET_SHUFFLE => Self::QueueSetShuffle(from_bytes!(), from_bytes!()),
Self::QueueUpdate(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?) 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)?), BYTE_TAG_SONG_PROPERTY_UNSET => {
0b00011110 => Self::QueueInsert( Self::TagSongPropertyUnset(from_bytes!(), from_bytes!())
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)?)
} }
0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?), BYTE_TAG_ALBUM_PROPERTY_SET => {
0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?), Self::TagAlbumPropertySet(from_bytes!(), from_bytes!(), from_bytes!())
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)?)
} }
0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?), BYTE_TAG_ALBUM_PROPERTY_UNSET => {
0b00110001 => Self::InitComplete, Self::TagAlbumPropertyUnset(from_bytes!(), from_bytes!())
0b11011011 => Self::ErrorInfo(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?), }
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."); eprintln!("unexpected byte when reading command; stopping playback.");
Self::Stop Self::Stop

View File

@ -354,6 +354,18 @@ async fn sse_handler(
| Command::RemoveSong(_) | Command::RemoveSong(_)
| Command::RemoveAlbum(_) | Command::RemoveAlbum(_)
| Command::RemoveArtist(_) | 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({ | Command::SetSongDuration(..) => Event::default().event("artists").data({
let db = state.db.lock().unwrap(); let db = state.db.lock().unwrap();
let mut a = db.artists().iter().collect::<Vec<_>>(); let mut a = db.artists().iter().collect::<Vec<_>>();