Added song duration support

This commit is contained in:
Mark 2023-11-04 20:06:54 +01:00
parent 273eac4b43
commit 7cae108ffa
14 changed files with 701 additions and 216 deletions

2
musicdb-client/src/config_gui.toml Normal file → Executable file
View File

@ -29,4 +29,4 @@ font = ''
# If we know the title, write it. If not, write "(no title found)" instead. # If we know the title, write it. If not, write "(no title found)" instead.
status_bar = '''\t status_bar = '''\t
\s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##?%>Year=%#\c808080 (%>Year=%)##''' \s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)## | \d'''

View File

@ -257,7 +257,8 @@ impl Gui {
| Command::ModifyArtist(_) | Command::ModifyArtist(_)
| Command::RemoveSong(_) | Command::RemoveSong(_)
| Command::RemoveAlbum(_) | Command::RemoveAlbum(_)
| Command::RemoveArtist(_) => { | Command::RemoveArtist(_)
| 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

@ -27,7 +27,7 @@ use speedy2d::{
use crate::{ use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Button, Panel, ScrollBox}, gui_base::{Button, Panel, ScrollBox},
gui_text::{Label, TextField}, gui_text::{self, AdvancedLabel, Label, TextField},
gui_wrappers::WithFocusHotkey, gui_wrappers::WithFocusHotkey,
}; };
@ -71,148 +71,13 @@ pub struct LibraryBrowser {
filter_albums: Arc<Mutex<Filter>>, filter_albums: Arc<Mutex<Filter>>,
filter_artists: Arc<Mutex<Filter>>, filter_artists: Arc<Mutex<Filter>>,
do_something_receiver: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>, do_something_receiver: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
selected_popup_state: (f32, usize, usize, usize),
} }
impl Clone for LibraryBrowser { impl Clone for LibraryBrowser {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self::new(self.config.clone()) Self::new(self.config.clone())
} }
} }
mod selected {
use super::*;
#[derive(Clone)]
pub struct Selected(
// artist, album, songs
Arc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>,
Arc<AtomicBool>,
);
impl Selected {
pub fn new(update: Arc<AtomicBool>) -> Self {
Self(Default::default(), update)
}
pub fn clear(&self) {
self.set_to(HashSet::new(), HashSet::new(), HashSet::new())
}
pub fn set_to(&self, artists: HashSet<u64>, albums: HashSet<u64>, songs: HashSet<u64>) {
let mut s = self.0.lock().unwrap();
s.0 = artists;
s.1 = albums;
s.2 = songs;
self.changed();
}
pub fn contains_artist(&self, id: &ArtistId) -> bool {
self.0.lock().unwrap().0.contains(id)
}
pub fn contains_album(&self, id: &AlbumId) -> bool {
self.0.lock().unwrap().1.contains(id)
}
pub fn contains_song(&self, id: &SongId) -> bool {
self.0.lock().unwrap().2.contains(id)
}
pub fn insert_artist(&self, id: ArtistId) -> bool {
self.changed();
self.0.lock().unwrap().0.insert(id)
}
pub fn insert_album(&self, id: AlbumId) -> bool {
self.changed();
self.0.lock().unwrap().1.insert(id)
}
pub fn insert_song(&self, id: SongId) -> bool {
self.changed();
self.0.lock().unwrap().2.insert(id)
}
pub fn remove_artist(&self, id: &ArtistId) -> bool {
self.changed();
self.0.lock().unwrap().0.remove(id)
}
pub fn remove_album(&self, id: &AlbumId) -> bool {
self.changed();
self.0.lock().unwrap().1.remove(id)
}
pub fn remove_song(&self, id: &SongId) -> bool {
self.changed();
self.0.lock().unwrap().2.remove(id)
}
pub fn view<T>(
&self,
f: impl FnOnce(&(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
) -> T {
f(&self.0.lock().unwrap())
}
pub fn view_mut<T>(
&self,
f: impl FnOnce(&mut (HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
) -> T {
let v = f(&mut self.0.lock().unwrap());
self.changed();
v
}
fn changed(&self) {
self.1.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec<Queue> {
let lock = self.0.lock().unwrap();
let (sel_artists, sel_albums, sel_songs) = &*lock;
let mut out = vec![];
for (artist, singles, albums, _) in &lb.library_filtered {
let artist_selected = sel_artists.contains(artist);
let mut local_artist_owned = vec![];
let mut local_artist = if artist_selected {
&mut local_artist_owned
} else {
&mut out
};
for (song, _) in singles {
let song_selected = sel_songs.contains(song);
if song_selected {
local_artist.push(QueueContent::Song(*song).into());
}
}
for (album, songs, _) in albums {
let album_selected = sel_albums.contains(album);
let mut local_album_owned = vec![];
let local_album = if album_selected {
&mut local_album_owned
} else {
&mut local_artist
};
for (song, _) in songs {
let song_selected = sel_songs.contains(song);
if song_selected {
local_album.push(QueueContent::Song(*song).into());
}
}
if album_selected {
local_artist.push(
QueueContent::Folder(
0,
local_album_owned,
match db.albums().get(album) {
Some(v) => v.name.clone(),
None => "< unknown album >".to_owned(),
},
)
.into(),
);
}
}
if artist_selected {
out.push(
QueueContent::Folder(
0,
local_artist_owned,
match db.artists().get(artist) {
Some(v) => v.name.to_owned(),
None => "< unknown artist >".to_owned(),
},
)
.into(),
);
}
}
out
}
}
}
fn search_regex_new(pat: &str, case_insensitive: bool) -> Result<Option<Regex>, regex::Error> { fn search_regex_new(pat: &str, case_insensitive: bool) -> Result<Option<Regex>, regex::Error> {
if pat.is_empty() { if pat.is_empty() {
Ok(None) Ok(None)
@ -312,6 +177,17 @@ impl LibraryBrowser {
selected.clone(), selected.clone(),
do_something_sender.clone(), do_something_sender.clone(),
)), )),
GuiElem::new(Panel::with_background(
GuiElemCfg::default().disabled(),
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
String::new(),
Color::LIGHT_GRAY,
None,
Vec2::new(0.5, 0.5),
))],
Color::from_rgba(0.0, 0.0, 0.0, 0.8),
)),
], ],
// - - - // - - -
library_sorted: vec![], library_sorted: vec![],
@ -336,6 +212,7 @@ impl LibraryBrowser {
filter_albums, filter_albums,
filter_artists, filter_artists,
do_something_receiver, do_something_receiver,
selected_popup_state: (0.0, 0, 0, 0),
} }
} }
pub fn selected_add_all(&self) { pub fn selected_add_all(&self) {
@ -400,6 +277,9 @@ impl GuiElemTrait for LibraryBrowser {
fn clone_gui(&self) -> Box<dyn GuiElemTrait> { fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw_rev(&self) -> bool {
false
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
loop { loop {
if let Ok(action) = self.do_something_receiver.try_recv() { if let Ok(action) = self.do_something_receiver.try_recv() {
@ -621,8 +501,121 @@ impl GuiElemTrait for LibraryBrowser {
} }
}, },
); );
// selected
{
let (artists, albums, songs) = self
.selected
.view(|sel| (sel.0.len(), sel.1.len(), sel.2.len()));
if self.selected_popup_state.1 != artists
|| self.selected_popup_state.2 != albums
|| self.selected_popup_state.3 != songs
{
self.selected_popup_state.1 = artists;
self.selected_popup_state.2 = albums;
self.selected_popup_state.3 = songs;
if artists > 0 || albums > 0 || songs > 0 {
if let Some(text) = match (artists, albums, songs) {
(0, 0, 0) => None,
(0, 0, 1) => Some(format!("1 song selected")),
(0, 0, s) => Some(format!("{s} songs selected")),
(0, 1, 0) => Some(format!("1 album selected")),
(0, al, 0) => Some(format!("{al} albums selected")),
(1, 0, 0) => Some(format!("1 artist selected")),
(ar, 0, 0) => Some(format!("{ar} artists selected")),
(0, 1, 1) => Some(format!("1 song and 1 album selected")),
(0, 1, s) => Some(format!("{s} songs and 1 album selected")),
(0, al, 1) => Some(format!("1 song and {al} albums selected")),
(0, al, s) => Some(format!("{s} songs and {al} albums selected")),
(1, 0, 1) => Some(format!("1 song and 1 artist selected")),
(1, 0, s) => Some(format!("{s} songs and 1 artist selected")),
(ar, 0, 1) => Some(format!("1 song and {ar} artists selected")),
(ar, 0, s) => Some(format!("{s} songs and {ar} artists selected")),
(1, 1, 0) => Some(format!("1 album and 1 artist selected")),
(1, al, 0) => Some(format!("{al} albums and 1 artist selected")),
(ar, 1, 0) => Some(format!("1 album and {ar} artists selected")),
(ar, al, 0) => Some(format!("{al} albums and {ar} artists selected")),
(1, 1, 1) => Some(format!("1 song, 1 album and 1 artist selected")),
(1, 1, s) => Some(format!("{s} songs, 1 album and 1 artist selected")),
(1, al, 1) => {
Some(format!("1 song, {al} albums and 1 artist selected"))
}
(ar, 1, 1) => {
Some(format!("1 song, 1 album and {ar} artists selected"))
}
(1, al, s) => {
Some(format!("{s} songs, {al} albums and 1 artist selected"))
}
(ar, 1, s) => {
Some(format!("{s} songs, 1 album and {ar} artists selected"))
}
(ar, al, 1) => {
Some(format!("1 song, {al} albums and {ar} artist selected"))
}
(ar, al, s) => {
Some(format!("{s} songs, {al} albums and {ar} artists selected"))
}
} {
*self.children[6]
.inner
.any_mut()
.downcast_mut::<Panel>()
.unwrap()
.children[0]
.inner
.any_mut()
.downcast_mut::<Label>()
.unwrap()
.content
.text() = text;
}
} else {
}
}
}
self.config.redraw = true; self.config.redraw = true;
} }
// selected popup
{
let mut redraw = false;
if self.selected_popup_state.1 > 0
|| self.selected_popup_state.2 > 0
|| self.selected_popup_state.3 > 0
{
if self.selected_popup_state.0 != 1.0 {
redraw = true;
self.children[6].inner.config_mut().enabled = true;
self.selected_popup_state.0 = 0.3 + 0.7 * self.selected_popup_state.0;
if self.selected_popup_state.0 > 0.99 {
self.selected_popup_state.0 = 1.0;
}
}
} else {
if self.selected_popup_state.0 != 0.0 {
redraw = true;
self.selected_popup_state.0 = 0.7 * self.selected_popup_state.0;
if self.selected_popup_state.0 < 0.01 {
self.selected_popup_state.0 = 0.0;
self.children[6].inner.config_mut().enabled = false;
}
}
}
if redraw {
self.children[6].inner.config_mut().pos = Rectangle::from_tuples(
(0.0, 1.0 - 0.05 * self.selected_popup_state.0),
(1.0, 1.0),
);
if let Some(h) = &info.helper {
h.request_redraw();
}
}
}
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() { if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false; self.config.redraw = false;
self.update_ui(&info.database, info.line_height); self.update_ui(&info.database, info.line_height);
@ -804,30 +797,57 @@ impl LibraryBrowser {
) )
} }
fn build_ui_element_album(&self, id: ArtistId, db: &Database, h: f32) -> (GuiElem, f32) { fn build_ui_element_album(&self, id: ArtistId, db: &Database, h: f32) -> (GuiElem, f32) {
let (name, duration) = if let Some(v) = db.albums().get(&id) {
let duration = v
.songs
.iter()
.filter_map(|id| db.get_song(id))
.map(|s| s.duration_millis)
.fold(0, u64::saturating_add)
/ 1000;
(
v.name.to_owned(),
if duration >= 60 * 60 {
format!(
" {}:{}:{:0>2}",
duration / (60 * 60),
(duration / 60) % 60,
duration % 60
)
} else {
format!(" {}:{:0>2}", duration / 60, duration % 60)
},
)
} else {
(format!("[ Album #{id} ]"), String::new())
};
( (
GuiElem::new(ListAlbum::new( GuiElem::new(ListAlbum::new(
GuiElemCfg::default(), GuiElemCfg::default(),
id, id,
if let Some(v) = db.albums().get(&id) { name,
v.name.to_owned() duration,
} else {
format!("[ Album #{id} ]")
},
self.selected.clone(), self.selected.clone(),
)), )),
h * 1.5, h * 1.5,
) )
} }
fn build_ui_element_song(&self, id: ArtistId, db: &Database, h: f32) -> (GuiElem, f32) { fn build_ui_element_song(&self, id: ArtistId, db: &Database, h: f32) -> (GuiElem, f32) {
let (name, duration) = if let Some(v) = db.songs().get(&id) {
let duration = v.duration_millis / 1000;
(
v.title.to_owned(),
format!(" {}:{:0>2}", duration / 60, duration % 60),
)
} else {
(format!("[ Song #{id} ]"), String::new())
};
( (
GuiElem::new(ListSong::new( GuiElem::new(ListSong::new(
GuiElemCfg::default(), GuiElemCfg::default(),
id, id,
if let Some(v) = db.songs().get(&id) { name,
v.title.to_owned() duration,
} else {
format!("[ Song #{id} ]")
},
self.selected.clone(), self.selected.clone(),
)), )),
h, h,
@ -989,13 +1009,28 @@ struct ListAlbum {
sel: bool, sel: bool,
} }
impl ListAlbum { impl ListAlbum {
pub fn new(mut config: GuiElemCfg, id: AlbumId, name: String, selected: Selected) -> Self { pub fn new(
let label = Label::new( mut config: GuiElemCfg,
id: AlbumId,
name: String,
half_sized_info: String,
selected: Selected,
) -> Self {
let label = AdvancedLabel::new(
GuiElemCfg::default(), GuiElemCfg::default(),
name,
Color::from_int_rgb(8, 61, 47),
None,
Vec2::new(0.0, 0.5), Vec2::new(0.0, 0.5),
vec![vec![
(
gui_text::Content::new(name, Color::from_int_rgb(8, 61, 47)),
1.0,
1.0,
),
(
gui_text::Content::new(half_sized_info, Color::GRAY),
0.5,
1.0,
),
]],
); );
config.redraw = true; config.redraw = true;
Self { Self {
@ -1132,13 +1167,24 @@ struct ListSong {
sel: bool, sel: bool,
} }
impl ListSong { impl ListSong {
pub fn new(mut config: GuiElemCfg, id: SongId, name: String, selected: Selected) -> Self { pub fn new(
let label = Label::new( mut config: GuiElemCfg,
id: SongId,
name: String,
duration: String,
selected: Selected,
) -> Self {
let label = AdvancedLabel::new(
GuiElemCfg::default(), GuiElemCfg::default(),
name,
Color::from_int_rgb(175, 175, 175),
None,
Vec2::new(0.0, 0.5), Vec2::new(0.0, 0.5),
vec![vec![
(
gui_text::Content::new(name, Color::from_int_rgb(175, 175, 175)),
1.0,
1.0,
),
(gui_text::Content::new(duration, Color::GRAY), 0.6, 1.0),
]],
); );
config.redraw = true; config.redraw = true;
Self { Self {
@ -2014,3 +2060,140 @@ impl FilterType {
} }
} }
} }
mod selected {
use super::*;
#[derive(Clone)]
pub struct Selected(
// artist, album, songs
Arc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>,
Arc<AtomicBool>,
);
impl Selected {
pub fn new(update: Arc<AtomicBool>) -> Self {
Self(Default::default(), update)
}
pub fn clear(&self) {
self.set_to(HashSet::new(), HashSet::new(), HashSet::new())
}
pub fn set_to(&self, artists: HashSet<u64>, albums: HashSet<u64>, songs: HashSet<u64>) {
let mut s = self.0.lock().unwrap();
s.0 = artists;
s.1 = albums;
s.2 = songs;
self.changed();
}
pub fn contains_artist(&self, id: &ArtistId) -> bool {
self.0.lock().unwrap().0.contains(id)
}
pub fn contains_album(&self, id: &AlbumId) -> bool {
self.0.lock().unwrap().1.contains(id)
}
pub fn contains_song(&self, id: &SongId) -> bool {
self.0.lock().unwrap().2.contains(id)
}
pub fn insert_artist(&self, id: ArtistId) -> bool {
self.changed();
self.0.lock().unwrap().0.insert(id)
}
pub fn insert_album(&self, id: AlbumId) -> bool {
self.changed();
self.0.lock().unwrap().1.insert(id)
}
pub fn insert_song(&self, id: SongId) -> bool {
self.changed();
self.0.lock().unwrap().2.insert(id)
}
pub fn remove_artist(&self, id: &ArtistId) -> bool {
self.changed();
self.0.lock().unwrap().0.remove(id)
}
pub fn remove_album(&self, id: &AlbumId) -> bool {
self.changed();
self.0.lock().unwrap().1.remove(id)
}
pub fn remove_song(&self, id: &SongId) -> bool {
self.changed();
self.0.lock().unwrap().2.remove(id)
}
pub fn view<T>(
&self,
f: impl FnOnce(&(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
) -> T {
f(&self.0.lock().unwrap())
}
pub fn view_mut<T>(
&self,
f: impl FnOnce(&mut (HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
) -> T {
let v = f(&mut self.0.lock().unwrap());
self.changed();
v
}
fn changed(&self) {
self.1.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec<Queue> {
let lock = self.0.lock().unwrap();
let (sel_artists, sel_albums, sel_songs) = &*lock;
let mut out = vec![];
for (artist, singles, albums, _) in &lb.library_filtered {
let artist_selected = sel_artists.contains(artist);
let mut local_artist_owned = vec![];
let mut local_artist = if artist_selected {
&mut local_artist_owned
} else {
&mut out
};
for (song, _) in singles {
let song_selected = sel_songs.contains(song);
if song_selected {
local_artist.push(QueueContent::Song(*song).into());
}
}
for (album, songs, _) in albums {
let album_selected = sel_albums.contains(album);
let mut local_album_owned = vec![];
let local_album = if album_selected {
&mut local_album_owned
} else {
&mut local_artist
};
for (song, _) in songs {
let song_selected = sel_songs.contains(song);
if song_selected {
local_album.push(QueueContent::Song(*song).into());
}
}
if album_selected {
local_artist.push(
QueueContent::Folder(
0,
local_album_owned,
match db.albums().get(album) {
Some(v) => v.name.clone(),
None => "< unknown album >".to_owned(),
},
)
.into(),
);
}
}
if artist_selected {
out.push(
QueueContent::Folder(
0,
local_artist_owned,
match db.artists().get(artist) {
Some(v) => v.name.to_owned(),
None => "< unknown artist >".to_owned(),
},
)
.into(),
);
}
}
out
}
}
}

0
musicdb-client/src/gui_notif.rs Normal file → Executable file
View File

View File

@ -3,7 +3,7 @@ use std::collections::VecDeque;
use musicdb_lib::{ use musicdb_lib::{
data::{ data::{
database::Database, database::Database,
queue::{Queue, QueueContent, ShuffleState}, queue::{Queue, QueueContent, QueueDuration, ShuffleState},
song::Song, song::Song,
AlbumId, ArtistId, AlbumId, ArtistId,
}, },
@ -19,7 +19,7 @@ use speedy2d::{
use crate::{ use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Panel, ScrollBox}, gui_base::{Panel, ScrollBox},
gui_text::Label, gui_text::{self, AdvancedLabel, Label},
}; };
/* /*
@ -35,6 +35,7 @@ because simple clicks have to be GoTo events.
pub struct QueueViewer { pub struct QueueViewer {
config: GuiElemCfg, config: GuiElemCfg,
children: Vec<GuiElem>, children: Vec<GuiElem>,
queue_updated: bool,
} }
const QP_QUEUE1: f32 = 0.0; const QP_QUEUE1: f32 = 0.0;
const QP_QUEUE2: f32 = 0.95; const QP_QUEUE2: f32 = 0.95;
@ -63,7 +64,7 @@ impl QueueViewer {
Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)), Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)),
))), ))),
GuiElem::new(Panel::new( GuiElem::new(Panel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_INV1), (1.0, QP_INV2))), GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_INV1), (0.5, QP_INV2))),
vec![ vec![
GuiElem::new( GuiElem::new(
QueueLoop::new( QueueLoop::new(
@ -129,7 +130,13 @@ impl QueueViewer {
), ),
], ],
)), )),
GuiElem::new(AdvancedLabel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.5, QP_INV1), (1.0, QP_INV2))),
Vec2::new(0.0, 0.5),
vec![],
)),
], ],
queue_updated: false,
} }
} }
} }
@ -153,6 +160,58 @@ impl GuiElemTrait for QueueViewer {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.queue_updated {
self.queue_updated = false;
let label = self.children[3]
.inner
.any_mut()
.downcast_mut::<AdvancedLabel>()
.unwrap();
fn fmt_dur(dur: QueueDuration) -> String {
if dur.infinite {
"".to_owned()
} else {
let seconds = dur.millis / 1000;
let minutes = seconds / 60;
let h = minutes / 60;
let m = minutes % 60;
let s = seconds % 60;
if dur.random_counter == 0 {
if h > 0 {
format!("{h}:{m:0>2}:{s:0>2}")
} else {
format!("{m:0>2}:{s:0>2}")
}
} else {
let r = dur.random_counter;
if dur.millis > 0 {
if h > 0 {
format!("{h}:{m:0>2}:{s:0>2} + {r} random songs")
} else {
format!("{m:0>2}:{s:0>2} + {r} random songs")
}
} else {
format!("{r} random songs")
}
}
}
}
let dt = fmt_dur(info.database.queue.duration_total(&info.database));
let dr = fmt_dur(info.database.queue.duration_remaining(&info.database));
label.content = vec![
vec![(
gui_text::Content::new(format!("Total: {dt}"), Color::GRAY),
1.0,
1.0,
)],
vec![(
gui_text::Content::new(format!("Remaining: {dr}"), Color::GRAY),
1.0,
1.0,
)],
];
label.config_mut().redraw = true;
}
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() { if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false; self.config.redraw = false;
let mut c = vec![]; let mut c = vec![];
@ -173,6 +232,7 @@ impl GuiElemTrait for QueueViewer {
} }
} }
fn updated_queue(&mut self) { fn updated_queue(&mut self) {
self.queue_updated = true;
self.config.redraw = true; self.config.redraw = true;
} }
} }
@ -412,16 +472,38 @@ impl QueueSong {
Self { Self {
config: config.w_mouse().w_keyboard_watch().w_drag_target(), config: config.w_mouse().w_keyboard_watch().w_drag_target(),
children: vec![ children: vec![
GuiElem::new(Label::new( GuiElem::new(AdvancedLabel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.57))), GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.57))),
Vec2::new(0.0, 0.5),
vec![vec![
(
gui_text::Content::new(
song.title.clone(), song.title.clone(),
if current { if current {
Color::from_int_rgb(194, 76, 178) Color::from_int_rgb(194, 76, 178)
} else { } else {
Color::from_int_rgb(120, 76, 194) Color::from_int_rgb(120, 76, 194)
}, },
None, ),
Vec2::new(0.0, 0.5), 1.0,
1.0,
),
(
gui_text::Content::new(
{
let duration = song.duration_millis / 1000;
format!(" {}:{:0>2}", duration / 60, duration % 60)
},
if current {
Color::GRAY
} else {
Color::DARK_GRAY
},
),
0.6,
1.0,
),
]],
)), )),
GuiElem::new(Label::new( GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((sub_offset, 0.57), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((sub_offset, 0.57), (1.0, 1.0))),
@ -443,9 +525,9 @@ impl QueueSong {
} }
}, },
if current { if current {
Color::from_int_rgb(146, 57, 133) Color::from_int_rgb(97, 38, 89)
} else { } else {
Color::from_int_rgb(95, 57, 146) Color::from_int_rgb(60, 38, 97)
}, },
None, None,
Vec2::new(0.0, 0.5), Vec2::new(0.0, 0.5),
@ -498,12 +580,16 @@ impl GuiElemTrait for QueueSong {
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left { if self.mouse && button == MouseButton::Left {
self.mouse = false; self.mouse = false;
if !self.always_copy {
vec![GuiAction::SendToServer(Command::QueueGoto( vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(), self.path.clone(),
))] ))]
} else { } else {
vec![] vec![]
} }
} else {
vec![]
}
} }
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
self.insert_below = info.mouse_pos.y > info.pos.top_left().y + info.pos.height() * 0.5; self.insert_below = info.mouse_pos.y > info.pos.top_left().y + info.pos.height() * 0.5;
@ -717,12 +803,16 @@ impl GuiElemTrait for QueueFolder {
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left { if self.mouse && button == MouseButton::Left {
self.mouse = false; self.mouse = false;
if !self.always_copy {
vec![GuiAction::SendToServer(Command::QueueGoto( vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(), self.path.clone(),
))] ))]
} else { } else {
vec![] vec![]
} }
} else {
vec![]
}
} }
fn key_watch( fn key_watch(
&mut self, &mut self,
@ -944,12 +1034,16 @@ impl GuiElemTrait for QueueLoop {
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left { if self.mouse && button == MouseButton::Left {
self.mouse = false; self.mouse = false;
if !self.always_copy {
vec![GuiAction::SendToServer(Command::QueueGoto( vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(), self.path.clone(),
))] ))]
} else { } else {
vec![] vec![]
} }
} else {
vec![]
}
} }
fn key_watch( fn key_watch(
&mut self, &mut self,
@ -1077,12 +1171,16 @@ impl GuiElemTrait for QueueRandom {
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left { if self.mouse && button == MouseButton::Left {
self.mouse = false; self.mouse = false;
if !self.always_copy {
vec![GuiAction::SendToServer(Command::QueueGoto( vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(), self.path.clone(),
))] ))]
} else { } else {
vec![] vec![]
} }
} else {
vec![]
}
} }
fn key_watch( fn key_watch(
&mut self, &mut self,
@ -1209,12 +1307,16 @@ impl GuiElemTrait for QueueShuffle {
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left { if self.mouse && button == MouseButton::Left {
self.mouse = false; self.mouse = false;
if !self.always_copy {
vec![GuiAction::SendToServer(Command::QueueGoto( vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(), self.path.clone(),
))] ))]
} else { } else {
vec![] vec![]
} }
} else {
vec![]
}
} }
fn key_watch( fn key_watch(
&mut self, &mut self,

View File

@ -34,7 +34,11 @@ pub struct Content {
impl Content { impl Content {
pub fn new(text: String, color: Color) -> Self { pub fn new(text: String, color: Color) -> Self {
Self { Self {
text, text: if text.starts_with(' ') {
text.replacen(' ', "\u{00A0}", 1)
} else {
text
},
color, color,
background: None, background: None,
formatted: None, formatted: None,

25
musicdb-client/src/textcfg.rs Normal file → Executable file
View File

@ -1,10 +1,9 @@
use std::{ use std::{
fmt::Display, fmt::Display,
iter::Peekable,
str::{Chars, FromStr}, str::{Chars, FromStr},
}; };
use musicdb_lib::data::{database::Database, song::Song, GeneralData, SongId}; use musicdb_lib::data::{database::Database, song::Song, GeneralData};
use speedy2d::color::Color; use speedy2d::color::Color;
use crate::gui_text::Content; use crate::gui_text::Content;
@ -22,6 +21,7 @@ pub enum TextPart {
SongTitle, SongTitle,
AlbumName, AlbumName,
ArtistName, ArtistName,
SongDuration(bool),
/// Searches for a tag with exactly the provided value. /// Searches for a tag with exactly the provided value.
/// Returns nothing or one of the following characters: /// Returns nothing or one of the following characters:
/// `s` for Song, `a` for Album, and `A` for Artist. /// `s` for Song, `a` for Album, and `A` for Artist.
@ -114,6 +114,19 @@ impl TextBuilder {
} }
} }
} }
TextPart::SongDuration(show_millis) => {
if let Some(s) = current_song {
let seconds = s.duration_millis / 1000;
let minutes = seconds / 60;
let seconds = seconds % 60;
push!(if *show_millis {
let ms = s.duration_millis % 1000;
format!("{minutes}:{seconds:0>2}.{ms:0>4}")
} else {
format!("{minutes}:{seconds:0>2}")
});
}
}
TextPart::TagEq(p) => { TextPart::TagEq(p) => {
for (i, gen) in all_general(db, &current_song).into_iter().enumerate() { for (i, gen) in all_general(db, &current_song).into_iter().enumerate() {
if let Some(_) = gen.and_then(|gen| gen.tags.iter().find(|t| *t == p)) { if let Some(_) = gen.and_then(|gen| gen.tags.iter().find(|t| *t == p)) {
@ -205,6 +218,14 @@ impl TextBuilder {
done!(); done!();
vec.push(TextPart::ArtistName); vec.push(TextPart::ArtistName);
} }
Some('d') => {
done!();
vec.push(TextPart::SongDuration(false));
}
Some('D') => {
done!();
vec.push(TextPart::SongDuration(true));
}
Some('s') => { Some('s') => {
done!(); done!();
vec.push(TextPart::SetScale({ vec.push(TextPart::SetScale({

View File

@ -7,4 +7,5 @@ edition = "2021"
[dependencies] [dependencies]
id3 = "1.7.0" id3 = "1.7.0"
mp3-duration = "0.1.10"
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }

View File

@ -17,12 +17,27 @@ use musicdb_lib::data::{
fn main() { fn main() {
// arg parsing // arg parsing
let lib_dir = if let Some(arg) = std::env::args().nth(1) { let mut args = std::env::args().skip(1);
let lib_dir = if let Some(arg) = args.next() {
arg arg
} else { } else {
eprintln!("usage: musicdb-filldb <library root>"); eprintln!("usage: musicdb-filldb <library root> [--skip-duration]");
std::process::exit(1); std::process::exit(1);
}; };
let mut unknown_arg = false;
let mut skip_duration = false;
for arg in args {
match arg.as_str() {
"--skip-duration" => skip_duration = true,
_ => {
unknown_arg = true;
eprintln!("Unknown argument: {arg}");
}
}
}
if unknown_arg {
return;
}
eprintln!("Library: {lib_dir}. press enter to start. result will be saved in 'dbfile'."); eprintln!("Library: {lib_dir}. press enter to start. result will be saved in 'dbfile'.");
std::io::stdin().read_line(&mut String::new()).unwrap(); std::io::stdin().read_line(&mut String::new()).unwrap();
// start // start
@ -34,6 +49,7 @@ fn main() {
for (i, file) in files.into_iter().enumerate() { for (i, file) in files.into_iter().enumerate() {
let mut newline = OnceNewline::new(); let mut newline = OnceNewline::new();
eprint!("\r{}/{}", i + 1, files_count); eprint!("\r{}/{}", i + 1, files_count);
if let Ok(metadata) = file.metadata() {
_ = std::io::stderr().flush(); _ = std::io::stderr().flush();
if let Some("mp3") = file.extension().and_then(|ext_os| ext_os.to_str()) { if let Some("mp3") = file.extension().and_then(|ext_os| ext_os.to_str()) {
match id3::Tag::read_from_path(&file) { match id3::Tag::read_from_path(&file) {
@ -41,9 +57,13 @@ fn main() {
newline.now(); newline.now();
eprintln!("[{file:?}] error reading id3 tag: {e}"); eprintln!("[{file:?}] error reading id3 tag: {e}");
} }
Ok(tag) => songs.push((file, tag)), Ok(tag) => songs.push((file, metadata, tag)),
} }
} }
} else {
newline.now();
eprintln!("[err] couldn't get metadata of file {:?}, skipping", file);
}
} }
eprintln!("\nloaded metadata of {} files.", songs.len()); eprintln!("\nloaded metadata of {} files.", songs.len());
let mut database = Database::new_empty(PathBuf::from("dbfile"), PathBuf::from(&lib_dir)); let mut database = Database::new_empty(PathBuf::from("dbfile"), PathBuf::from(&lib_dir));
@ -55,21 +75,30 @@ fn main() {
singles: vec![], singles: vec![],
general: GeneralData::default(), general: GeneralData::default(),
}); });
eprintln!("searching for artists..."); eprintln!(
"searching for artists and adding songs... (this will be much faster with --skip-duration because it avoids loading and decoding all the mp3 files)"
);
let mut artists = HashMap::new(); let mut artists = HashMap::new();
for song in songs { let len = songs.len();
let mut prev_perc = 999;
for (i, (song_path, song_file_metadata, song_tags)) in songs.into_iter().enumerate() {
let perc = i * 100 / len;
if perc != prev_perc {
eprint!("{perc: >2}%\r");
_ = std::io::stderr().lock().flush();
prev_perc = perc;
}
let mut general = GeneralData::default(); let mut general = GeneralData::default();
if let Some(year) = song.1.year() { if let Some(year) = song_tags.year() {
general.tags.push(format!("Year={year}")); general.tags.push(format!("Year={year}"));
} }
if let Some(genre) = song.1.genre_parsed() { if let Some(genre) = song_tags.genre_parsed() {
general.tags.push(format!("Genre={genre}")); general.tags.push(format!("Genre={genre}"));
} }
let (artist_id, album_id) = if let Some(artist) = song let (artist_id, album_id) = if let Some(artist) = song_tags
.1
.album_artist() .album_artist()
.filter(|v| !v.trim().is_empty()) .filter(|v| !v.trim().is_empty())
.or_else(|| song.1.artist().filter(|v| !v.trim().is_empty())) .or_else(|| song_tags.artist().filter(|v| !v.trim().is_empty()))
{ {
let artist_id = if !artists.contains_key(artist) { let artist_id = if !artists.contains_key(artist) {
let artist_id = database.add_artist_new(Artist { let artist_id = database.add_artist_new(Artist {
@ -85,7 +114,7 @@ fn main() {
} else { } else {
artists.get(artist).unwrap().0 artists.get(artist).unwrap().0
}; };
if let Some(album) = song.1.album().filter(|a| !a.trim().is_empty()) { if let Some(album) = song_tags.album().filter(|a| !a.trim().is_empty()) {
let (_, albums) = artists.get_mut(artist).unwrap(); let (_, albums) = artists.get_mut(artist).unwrap();
let album_id = if !albums.contains_key(album) { let album_id = if !albums.contains_key(album) {
let album_id = database.add_album_new(Album { let album_id = database.add_album_new(Album {
@ -98,7 +127,7 @@ fn main() {
}); });
albums.insert( albums.insert(
album.to_string(), album.to_string(),
(album_id, song.0.parent().map(|dir| dir.to_path_buf())), (album_id, song_path.parent().map(|dir| dir.to_path_buf())),
); );
album_id album_id
} else { } else {
@ -106,7 +135,7 @@ fn main() {
if album if album
.1 .1
.as_ref() .as_ref()
.is_some_and(|dir| Some(dir.as_path()) != song.0.parent()) .is_some_and(|dir| Some(dir.as_path()) != song_path.parent())
{ {
// album directory is inconsistent // album directory is inconsistent
album.1 = None; album.1 = None;
@ -120,9 +149,8 @@ fn main() {
} else { } else {
(unknown_artist, None) (unknown_artist, None)
}; };
let path = song.0.strip_prefix(&lib_dir).unwrap(); let path = song_path.strip_prefix(&lib_dir).unwrap();
let title = song let title = song_tags
.1
.title() .title()
.map_or(None, |title| { .map_or(None, |title| {
if title.trim().is_empty() { if title.trim().is_empty() {
@ -131,7 +159,13 @@ fn main() {
Some(title.to_string()) Some(title.to_string())
} }
}) })
.unwrap_or_else(|| song.0.file_stem().unwrap().to_string_lossy().into_owned()); .unwrap_or_else(|| {
song_path
.file_stem()
.unwrap()
.to_string_lossy()
.into_owned()
});
database.add_song_new(Song { database.add_song_new(Song {
id: 0, id: 0,
title: title.clone(), title: title.clone(),
@ -142,6 +176,26 @@ fn main() {
artist: artist_id, artist: artist_id,
more_artists: vec![], more_artists: vec![],
cover: None, cover: None,
file_size: song_file_metadata.len(),
duration_millis: if let Some(dur) = song_tags.duration() {
dur as u64 * 1000
} else {
if skip_duration {
eprintln!(
"Duration of song {:?} not found in tags, using 0 instead!",
song_path
);
0
} else {
match mp3_duration::from_path(&song_path) {
Ok(dur) => dur.as_millis().min(u64::MAX as _) as u64,
Err(e) => {
eprintln!("Duration of song {song_path:?} not found in tags and can't be determined from the file contents either ({e}). Using duration 0 instead.");
0
}
}
}
},
general, general,
cached_data: Arc::new(Mutex::new(None)), cached_data: Arc::new(Mutex::new(None)),
}); });

View File

@ -359,6 +359,11 @@ impl Database {
Command::RemoveArtist(artist) => { Command::RemoveArtist(artist) => {
_ = self.remove_artist(artist); _ = self.remove_artist(artist);
} }
Command::SetSongDuration(id, duration) => {
if let Some(song) = self.get_song_mut(&id) {
song.duration_millis = duration;
}
}
Command::InitComplete => { Command::InitComplete => {
self.client_is_init = true; self.client_is_init = true;
} }

View File

@ -1,4 +1,4 @@
use std::collections::VecDeque; use std::{collections::VecDeque, ops::AddAssign};
use rand::seq::{IteratorRandom, SliceRandom}; use rand::seq::{IteratorRandom, SliceRandom};
@ -98,6 +98,60 @@ impl Queue {
QueueContent::Shuffle { inner, state: _ } => inner.len(), QueueContent::Shuffle { inner, state: _ } => inner.len(),
} }
} }
pub fn duration_total(&self, db: &Database) -> QueueDuration {
let mut dur = QueueDuration::new_total();
self.add_duration(&mut dur, db);
dur
}
// remaining time, including current song
pub fn duration_remaining(&self, db: &Database) -> QueueDuration {
let mut dur = QueueDuration::new_remaining();
self.add_duration(&mut dur, db);
dur
}
pub fn add_duration(&self, dur: &mut QueueDuration, db: &Database) {
if self.enabled {
match &self.content {
QueueContent::Song(v) => {
dur.millis += db.get_song(v).map(|s| s.duration_millis).unwrap_or(0)
}
QueueContent::Folder(c, inner, _) => {
for (i, inner) in inner.iter().enumerate() {
if dur.include_past || i >= *c {
inner.add_duration(dur, db);
}
}
}
QueueContent::Loop(total, done, inner) => {
if *total == 0 {
dur.infinite = true;
} else if dur.include_past {
// <total duration> * <total iterations>
let dt = inner.duration_total(db);
for _ in 0..*total {
*dur += dt;
}
} else {
// <remaining duration> + <total duration> * <remaining iterations>
inner.add_duration(dur, db);
let dt = inner.duration_total(db);
for _ in 0..(total.saturating_sub(*done + 1)) {
*dur += dt;
}
}
}
QueueContent::Random(q) => {
if let Some(el) = q.iter().next() {
dur.random_known_millis += el.duration_total(db).millis;
}
dur.random_counter += 1;
}
QueueContent::Shuffle { inner, state: _ } => {
inner.add_duration(dur, db);
}
}
}
}
/// recursively descends the queue until the current active element is found, then returns it. /// recursively descends the queue until the current active element is found, then returns it.
pub fn get_current(&self) -> Option<&Self> { pub fn get_current(&self) -> Option<&Self> {
@ -641,3 +695,42 @@ impl ToFromBytes for ShuffleState {
}) })
} }
} }
#[derive(Clone, Copy)]
pub struct QueueDuration {
pub include_past: bool,
pub infinite: bool,
/// number of milliseconds (that we know of)
pub millis: u64,
/// number of milliseconds from the <random> element - only accurate the first time it is reached in queue.
pub random_known_millis: u64,
/// number of <random> elements, which could have pretty much any duration.
pub random_counter: u64,
}
impl QueueDuration {
fn new_total() -> Self {
Self::new(true)
}
fn new_remaining() -> Self {
Self::new(false)
}
fn new(include_past: bool) -> Self {
QueueDuration {
include_past,
infinite: false,
millis: 0,
random_known_millis: 0,
random_counter: 0,
}
}
}
impl AddAssign<QueueDuration> for QueueDuration {
fn add_assign(&mut self, rhs: QueueDuration) {
if rhs.infinite {
self.infinite = true;
}
self.millis += rhs.millis;
self.random_known_millis += rhs.random_known_millis;
self.random_counter += rhs.random_counter;
}
}

View File

@ -22,6 +22,9 @@ pub struct Song {
pub artist: ArtistId, pub artist: ArtistId,
pub more_artists: Vec<ArtistId>, pub more_artists: Vec<ArtistId>,
pub cover: Option<CoverId>, pub cover: Option<CoverId>,
pub file_size: u64,
/// song duration in milliseconds
pub duration_millis: u64,
pub general: GeneralData, pub general: GeneralData,
/// None => No cached data /// None => No cached data
/// Some(Err) => No cached data yet, but a thread is working on loading it. /// Some(Err) => No cached data yet, but a thread is working on loading it.
@ -36,6 +39,8 @@ impl Song {
artist: ArtistId, artist: ArtistId,
more_artists: Vec<ArtistId>, more_artists: Vec<ArtistId>,
cover: Option<CoverId>, cover: Option<CoverId>,
file_size: u64,
duration_millis: u64,
) -> Self { ) -> Self {
Self { Self {
id: 0, id: 0,
@ -45,6 +50,8 @@ impl Song {
artist, artist,
more_artists, more_artists,
cover, cover,
file_size,
duration_millis,
general: GeneralData::default(), general: GeneralData::default(),
cached_data: Arc::new(Mutex::new(None)), cached_data: Arc::new(Mutex::new(None)),
} }
@ -185,6 +192,8 @@ impl ToFromBytes for Song {
self.artist.to_bytes(s)?; self.artist.to_bytes(s)?;
self.more_artists.to_bytes(s)?; self.more_artists.to_bytes(s)?;
self.cover.to_bytes(s)?; self.cover.to_bytes(s)?;
self.file_size.to_bytes(s)?;
self.duration_millis.to_bytes(s)?;
self.general.to_bytes(s)?; self.general.to_bytes(s)?;
Ok(()) Ok(())
} }
@ -200,6 +209,8 @@ impl ToFromBytes for Song {
artist: ToFromBytes::from_bytes(s)?, artist: ToFromBytes::from_bytes(s)?,
more_artists: ToFromBytes::from_bytes(s)?, more_artists: ToFromBytes::from_bytes(s)?,
cover: ToFromBytes::from_bytes(s)?, cover: ToFromBytes::from_bytes(s)?,
file_size: ToFromBytes::from_bytes(s)?,
duration_millis: ToFromBytes::from_bytes(s)?,
general: ToFromBytes::from_bytes(s)?, general: ToFromBytes::from_bytes(s)?,
cached_data: Arc::new(Mutex::new(None)), cached_data: Arc::new(Mutex::new(None)),
}) })

View File

@ -49,6 +49,7 @@ pub enum Command {
RemoveAlbum(AlbumId), RemoveAlbum(AlbumId),
RemoveArtist(ArtistId), RemoveArtist(ArtistId),
ModifyArtist(Artist), ModifyArtist(Artist),
SetSongDuration(SongId, u64),
InitComplete, InitComplete,
ErrorInfo(String, String), ErrorInfo(String, String),
} }
@ -102,7 +103,7 @@ pub fn run_server(
let command_sender = command_sender.clone(); let command_sender = command_sender.clone();
let db = Arc::clone(&database); let db = Arc::clone(&database);
thread::spawn(move || loop { thread::spawn(move || loop {
if let Ok((connection, con_addr)) = v.accept() { if let Ok((connection, _con_addr)) = v.accept() {
let command_sender = command_sender.clone(); let command_sender = command_sender.clone();
let db = Arc::clone(&db); let db = Arc::clone(&db);
thread::spawn(move || { thread::spawn(move || {
@ -275,6 +276,11 @@ impl ToFromBytes for Command {
s.write_all(&[0b11011100])?; s.write_all(&[0b11011100])?;
artist.to_bytes(s)?; artist.to_bytes(s)?;
} }
Self::SetSongDuration(i, d) => {
s.write_all(&[0b11100000])?;
i.to_bytes(s)?;
d.to_bytes(s)?;
}
Self::InitComplete => { Self::InitComplete => {
s.write_all(&[0b00110001])?; s.write_all(&[0b00110001])?;
} }
@ -326,6 +332,9 @@ impl ToFromBytes for Command {
0b11010000 => Self::RemoveSong(ToFromBytes::from_bytes(s)?), 0b11010000 => Self::RemoveSong(ToFromBytes::from_bytes(s)?),
0b11010011 => Self::RemoveAlbum(ToFromBytes::from_bytes(s)?), 0b11010011 => Self::RemoveAlbum(ToFromBytes::from_bytes(s)?),
0b11011100 => Self::RemoveArtist(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)?), 0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?),
0b00110001 => Self::InitComplete, 0b00110001 => Self::InitComplete,
0b11011011 => Self::ErrorInfo(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?), 0b11011011 => Self::ErrorInfo(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?),

View File

@ -353,7 +353,8 @@ async fn sse_handler(
| Command::AddCover(..) | Command::AddCover(..)
| Command::RemoveSong(_) | Command::RemoveSong(_)
| Command::RemoveAlbum(_) | Command::RemoveAlbum(_)
| Command::RemoveArtist(_) => Event::default().event("artists").data({ | Command::RemoveArtist(_)
| 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<_>>();
a.sort_unstable_by_key(|(_id, artist)| &artist.name); a.sort_unstable_by_key(|(_id, artist)| &artist.name);