mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 05:43: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.
|
||||
|
||||
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::RemoveSong(_)
|
||||
| Command::RemoveAlbum(_)
|
||||
| Command::RemoveArtist(_) => {
|
||||
| Command::RemoveArtist(_)
|
||||
| Command::SetSongDuration(..) => {
|
||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||
_ = s.send_event(GuiEvent::UpdatedLibrary);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ use speedy2d::{
|
||||
use crate::{
|
||||
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
gui_base::{Button, Panel, ScrollBox},
|
||||
gui_text::{Label, TextField},
|
||||
gui_text::{self, AdvancedLabel, Label, TextField},
|
||||
gui_wrappers::WithFocusHotkey,
|
||||
};
|
||||
|
||||
@ -71,148 +71,13 @@ pub struct LibraryBrowser {
|
||||
filter_albums: Arc<Mutex<Filter>>,
|
||||
filter_artists: Arc<Mutex<Filter>>,
|
||||
do_something_receiver: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
|
||||
selected_popup_state: (f32, usize, usize, usize),
|
||||
}
|
||||
impl Clone for LibraryBrowser {
|
||||
fn clone(&self) -> Self {
|
||||
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> {
|
||||
if pat.is_empty() {
|
||||
Ok(None)
|
||||
@ -312,6 +177,17 @@ impl LibraryBrowser {
|
||||
selected.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![],
|
||||
@ -336,6 +212,7 @@ impl LibraryBrowser {
|
||||
filter_albums,
|
||||
filter_artists,
|
||||
do_something_receiver,
|
||||
selected_popup_state: (0.0, 0, 0, 0),
|
||||
}
|
||||
}
|
||||
pub fn selected_add_all(&self) {
|
||||
@ -400,6 +277,9 @@ impl GuiElemTrait for LibraryBrowser {
|
||||
fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
fn draw_rev(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
|
||||
loop {
|
||||
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;
|
||||
}
|
||||
// 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() {
|
||||
self.config.redraw = false;
|
||||
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) {
|
||||
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(
|
||||
GuiElemCfg::default(),
|
||||
id,
|
||||
if let Some(v) = db.albums().get(&id) {
|
||||
v.name.to_owned()
|
||||
} else {
|
||||
format!("[ Album #{id} ]")
|
||||
},
|
||||
name,
|
||||
duration,
|
||||
self.selected.clone(),
|
||||
)),
|
||||
h * 1.5,
|
||||
)
|
||||
}
|
||||
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(
|
||||
GuiElemCfg::default(),
|
||||
id,
|
||||
if let Some(v) = db.songs().get(&id) {
|
||||
v.title.to_owned()
|
||||
} else {
|
||||
format!("[ Song #{id} ]")
|
||||
},
|
||||
name,
|
||||
duration,
|
||||
self.selected.clone(),
|
||||
)),
|
||||
h,
|
||||
@ -989,13 +1009,28 @@ struct ListAlbum {
|
||||
sel: bool,
|
||||
}
|
||||
impl ListAlbum {
|
||||
pub fn new(mut config: GuiElemCfg, id: AlbumId, name: String, selected: Selected) -> Self {
|
||||
let label = Label::new(
|
||||
pub fn new(
|
||||
mut config: GuiElemCfg,
|
||||
id: AlbumId,
|
||||
name: String,
|
||||
half_sized_info: String,
|
||||
selected: Selected,
|
||||
) -> Self {
|
||||
let label = AdvancedLabel::new(
|
||||
GuiElemCfg::default(),
|
||||
name,
|
||||
Color::from_int_rgb(8, 61, 47),
|
||||
None,
|
||||
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;
|
||||
Self {
|
||||
@ -1132,13 +1167,24 @@ struct ListSong {
|
||||
sel: bool,
|
||||
}
|
||||
impl ListSong {
|
||||
pub fn new(mut config: GuiElemCfg, id: SongId, name: String, selected: Selected) -> Self {
|
||||
let label = Label::new(
|
||||
pub fn new(
|
||||
mut config: GuiElemCfg,
|
||||
id: SongId,
|
||||
name: String,
|
||||
duration: String,
|
||||
selected: Selected,
|
||||
) -> Self {
|
||||
let label = AdvancedLabel::new(
|
||||
GuiElemCfg::default(),
|
||||
name,
|
||||
Color::from_int_rgb(175, 175, 175),
|
||||
None,
|
||||
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;
|
||||
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::{
|
||||
data::{
|
||||
database::Database,
|
||||
queue::{Queue, QueueContent, ShuffleState},
|
||||
queue::{Queue, QueueContent, QueueDuration, ShuffleState},
|
||||
song::Song,
|
||||
AlbumId, ArtistId,
|
||||
},
|
||||
@ -19,7 +19,7 @@ use speedy2d::{
|
||||
use crate::{
|
||||
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
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 {
|
||||
config: GuiElemCfg,
|
||||
children: Vec<GuiElem>,
|
||||
queue_updated: bool,
|
||||
}
|
||||
const QP_QUEUE1: f32 = 0.0;
|
||||
const QP_QUEUE2: f32 = 0.95;
|
||||
@ -63,7 +64,7 @@ impl QueueViewer {
|
||||
Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)),
|
||||
))),
|
||||
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![
|
||||
GuiElem::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())
|
||||
}
|
||||
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() {
|
||||
self.config.redraw = false;
|
||||
let mut c = vec![];
|
||||
@ -173,6 +232,7 @@ impl GuiElemTrait for QueueViewer {
|
||||
}
|
||||
}
|
||||
fn updated_queue(&mut self) {
|
||||
self.queue_updated = true;
|
||||
self.config.redraw = true;
|
||||
}
|
||||
}
|
||||
@ -412,16 +472,38 @@ impl QueueSong {
|
||||
Self {
|
||||
config: config.w_mouse().w_keyboard_watch().w_drag_target(),
|
||||
children: vec![
|
||||
GuiElem::new(Label::new(
|
||||
GuiElem::new(AdvancedLabel::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.57))),
|
||||
song.title.clone(),
|
||||
if current {
|
||||
Color::from_int_rgb(194, 76, 178)
|
||||
} else {
|
||||
Color::from_int_rgb(120, 76, 194)
|
||||
},
|
||||
None,
|
||||
Vec2::new(0.0, 0.5),
|
||||
vec![vec![
|
||||
(
|
||||
gui_text::Content::new(
|
||||
song.title.clone(),
|
||||
if current {
|
||||
Color::from_int_rgb(194, 76, 178)
|
||||
} else {
|
||||
Color::from_int_rgb(120, 76, 194)
|
||||
},
|
||||
),
|
||||
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(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((sub_offset, 0.57), (1.0, 1.0))),
|
||||
@ -443,9 +525,9 @@ impl QueueSong {
|
||||
}
|
||||
},
|
||||
if current {
|
||||
Color::from_int_rgb(146, 57, 133)
|
||||
Color::from_int_rgb(97, 38, 89)
|
||||
} else {
|
||||
Color::from_int_rgb(95, 57, 146)
|
||||
Color::from_int_rgb(60, 38, 97)
|
||||
},
|
||||
None,
|
||||
Vec2::new(0.0, 0.5),
|
||||
@ -498,9 +580,13 @@ impl GuiElemTrait for QueueSong {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@ -717,9 +803,13 @@ impl GuiElemTrait for QueueFolder {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@ -944,9 +1034,13 @@ impl GuiElemTrait for QueueLoop {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@ -1077,9 +1171,13 @@ impl GuiElemTrait for QueueRandom {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@ -1209,9 +1307,13 @@ impl GuiElemTrait for QueueShuffle {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
|
@ -34,7 +34,11 @@ pub struct Content {
|
||||
impl Content {
|
||||
pub fn new(text: String, color: Color) -> Self {
|
||||
Self {
|
||||
text,
|
||||
text: if text.starts_with(' ') {
|
||||
text.replacen(' ', "\u{00A0}", 1)
|
||||
} else {
|
||||
text
|
||||
},
|
||||
color,
|
||||
background: 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::{
|
||||
fmt::Display,
|
||||
iter::Peekable,
|
||||
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 crate::gui_text::Content;
|
||||
@ -22,6 +21,7 @@ pub enum TextPart {
|
||||
SongTitle,
|
||||
AlbumName,
|
||||
ArtistName,
|
||||
SongDuration(bool),
|
||||
/// Searches for a tag with exactly the provided value.
|
||||
/// Returns nothing or one of the following characters:
|
||||
/// `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) => {
|
||||
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)) {
|
||||
@ -205,6 +218,14 @@ impl TextBuilder {
|
||||
done!();
|
||||
vec.push(TextPart::ArtistName);
|
||||
}
|
||||
Some('d') => {
|
||||
done!();
|
||||
vec.push(TextPart::SongDuration(false));
|
||||
}
|
||||
Some('D') => {
|
||||
done!();
|
||||
vec.push(TextPart::SongDuration(true));
|
||||
}
|
||||
Some('s') => {
|
||||
done!();
|
||||
vec.push(TextPart::SetScale({
|
||||
|
@ -7,4 +7,5 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
id3 = "1.7.0"
|
||||
mp3-duration = "0.1.10"
|
||||
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
||||
|
@ -17,12 +17,27 @@ use musicdb_lib::data::{
|
||||
|
||||
fn main() {
|
||||
// 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
|
||||
} else {
|
||||
eprintln!("usage: musicdb-filldb <library root>");
|
||||
eprintln!("usage: musicdb-filldb <library root> [--skip-duration]");
|
||||
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'.");
|
||||
std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||
// start
|
||||
@ -34,15 +49,20 @@ fn main() {
|
||||
for (i, file) in files.into_iter().enumerate() {
|
||||
let mut newline = OnceNewline::new();
|
||||
eprint!("\r{}/{}", i + 1, files_count);
|
||||
_ = std::io::stderr().flush();
|
||||
if let Some("mp3") = file.extension().and_then(|ext_os| ext_os.to_str()) {
|
||||
match id3::Tag::read_from_path(&file) {
|
||||
Err(e) => {
|
||||
newline.now();
|
||||
eprintln!("[{file:?}] error reading id3 tag: {e}");
|
||||
if let Ok(metadata) = file.metadata() {
|
||||
_ = std::io::stderr().flush();
|
||||
if let Some("mp3") = file.extension().and_then(|ext_os| ext_os.to_str()) {
|
||||
match id3::Tag::read_from_path(&file) {
|
||||
Err(e) => {
|
||||
newline.now();
|
||||
eprintln!("[{file:?}] error reading id3 tag: {e}");
|
||||
}
|
||||
Ok(tag) => songs.push((file, metadata, tag)),
|
||||
}
|
||||
Ok(tag) => songs.push((file, tag)),
|
||||
}
|
||||
} else {
|
||||
newline.now();
|
||||
eprintln!("[err] couldn't get metadata of file {:?}, skipping", file);
|
||||
}
|
||||
}
|
||||
eprintln!("\nloaded metadata of {} files.", songs.len());
|
||||
@ -55,21 +75,30 @@ fn main() {
|
||||
singles: vec![],
|
||||
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();
|
||||
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();
|
||||
if let Some(year) = song.1.year() {
|
||||
if let Some(year) = song_tags.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}"));
|
||||
}
|
||||
let (artist_id, album_id) = if let Some(artist) = song
|
||||
.1
|
||||
let (artist_id, album_id) = if let Some(artist) = song_tags
|
||||
.album_artist()
|
||||
.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 = database.add_artist_new(Artist {
|
||||
@ -85,7 +114,7 @@ fn main() {
|
||||
} else {
|
||||
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 album_id = if !albums.contains_key(album) {
|
||||
let album_id = database.add_album_new(Album {
|
||||
@ -98,7 +127,7 @@ fn main() {
|
||||
});
|
||||
albums.insert(
|
||||
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
|
||||
} else {
|
||||
@ -106,7 +135,7 @@ fn main() {
|
||||
if album
|
||||
.1
|
||||
.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.1 = None;
|
||||
@ -120,9 +149,8 @@ fn main() {
|
||||
} else {
|
||||
(unknown_artist, None)
|
||||
};
|
||||
let path = song.0.strip_prefix(&lib_dir).unwrap();
|
||||
let title = song
|
||||
.1
|
||||
let path = song_path.strip_prefix(&lib_dir).unwrap();
|
||||
let title = song_tags
|
||||
.title()
|
||||
.map_or(None, |title| {
|
||||
if title.trim().is_empty() {
|
||||
@ -131,7 +159,13 @@ fn main() {
|
||||
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 {
|
||||
id: 0,
|
||||
title: title.clone(),
|
||||
@ -142,6 +176,26 @@ fn main() {
|
||||
artist: artist_id,
|
||||
more_artists: vec![],
|
||||
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,
|
||||
cached_data: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
|
@ -359,6 +359,11 @@ impl Database {
|
||||
Command::RemoveArtist(artist) => {
|
||||
_ = self.remove_artist(artist);
|
||||
}
|
||||
Command::SetSongDuration(id, duration) => {
|
||||
if let Some(song) = self.get_song_mut(&id) {
|
||||
song.duration_millis = duration;
|
||||
}
|
||||
}
|
||||
Command::InitComplete => {
|
||||
self.client_is_init = true;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::{collections::VecDeque, ops::AddAssign};
|
||||
|
||||
use rand::seq::{IteratorRandom, SliceRandom};
|
||||
|
||||
@ -98,6 +98,60 @@ impl Queue {
|
||||
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.
|
||||
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 more_artists: Vec<ArtistId>,
|
||||
pub cover: Option<CoverId>,
|
||||
pub file_size: u64,
|
||||
/// song duration in milliseconds
|
||||
pub duration_millis: u64,
|
||||
pub general: GeneralData,
|
||||
/// None => No cached data
|
||||
/// Some(Err) => No cached data yet, but a thread is working on loading it.
|
||||
@ -36,6 +39,8 @@ impl Song {
|
||||
artist: ArtistId,
|
||||
more_artists: Vec<ArtistId>,
|
||||
cover: Option<CoverId>,
|
||||
file_size: u64,
|
||||
duration_millis: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
@ -45,6 +50,8 @@ impl Song {
|
||||
artist,
|
||||
more_artists,
|
||||
cover,
|
||||
file_size,
|
||||
duration_millis,
|
||||
general: GeneralData::default(),
|
||||
cached_data: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
@ -185,6 +192,8 @@ impl ToFromBytes for Song {
|
||||
self.artist.to_bytes(s)?;
|
||||
self.more_artists.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)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -200,6 +209,8 @@ impl ToFromBytes for Song {
|
||||
artist: ToFromBytes::from_bytes(s)?,
|
||||
more_artists: 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)?,
|
||||
cached_data: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
|
@ -49,6 +49,7 @@ pub enum Command {
|
||||
RemoveAlbum(AlbumId),
|
||||
RemoveArtist(ArtistId),
|
||||
ModifyArtist(Artist),
|
||||
SetSongDuration(SongId, u64),
|
||||
InitComplete,
|
||||
ErrorInfo(String, String),
|
||||
}
|
||||
@ -102,7 +103,7 @@ pub fn run_server(
|
||||
let command_sender = command_sender.clone();
|
||||
let db = Arc::clone(&database);
|
||||
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 db = Arc::clone(&db);
|
||||
thread::spawn(move || {
|
||||
@ -275,6 +276,11 @@ impl ToFromBytes for Command {
|
||||
s.write_all(&[0b11011100])?;
|
||||
artist.to_bytes(s)?;
|
||||
}
|
||||
Self::SetSongDuration(i, d) => {
|
||||
s.write_all(&[0b11100000])?;
|
||||
i.to_bytes(s)?;
|
||||
d.to_bytes(s)?;
|
||||
}
|
||||
Self::InitComplete => {
|
||||
s.write_all(&[0b00110001])?;
|
||||
}
|
||||
@ -326,6 +332,9 @@ impl ToFromBytes for Command {
|
||||
0b11010000 => Self::RemoveSong(ToFromBytes::from_bytes(s)?),
|
||||
0b11010011 => Self::RemoveAlbum(ToFromBytes::from_bytes(s)?),
|
||||
0b11011100 => Self::RemoveArtist(ToFromBytes::from_bytes(s)?),
|
||||
0b11100000 => {
|
||||
Self::SetSongDuration(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?)
|
||||
}
|
||||
0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?),
|
||||
0b00110001 => Self::InitComplete,
|
||||
0b11011011 => Self::ErrorInfo(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?),
|
||||
|
@ -353,7 +353,8 @@ async fn sse_handler(
|
||||
| Command::AddCover(..)
|
||||
| Command::RemoveSong(_)
|
||||
| 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 mut a = db.artists().iter().collect::<Vec<_>>();
|
||||
a.sort_unstable_by_key(|(_id, artist)| &artist.name);
|
||||
|
Loading…
Reference in New Issue
Block a user