mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 14:13:53 +01:00
Added song duration support
This commit is contained in:
parent
273eac4b43
commit
7cae108ffa
2
musicdb-client/src/config_gui.toml
Normal file → Executable file
2
musicdb-client/src/config_gui.toml
Normal file → Executable 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'''
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
0
musicdb-client/src/gui_notif.rs
Normal file → Executable 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,
|
||||||
|
@ -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
25
musicdb-client/src/textcfg.rs
Normal file → Executable 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, ¤t_song).into_iter().enumerate() {
|
for (i, gen) in all_general(db, ¤t_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({
|
||||||
|
@ -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" }
|
||||||
|
@ -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)),
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)),
|
||||||
})
|
})
|
||||||
|
@ -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)?),
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user