mirror of
				https://github.com/Dummi26/musicdb.git
				synced 2025-11-04 13:21:26 +01:00 
			
		
		
		
	small improvements idk i forgot i had a git repo for this project
This commit is contained in:
		
							parent
							
								
									0ae0126f04
								
							
						
					
					
						commit
						9fbe67012e
					
				@ -9,6 +9,7 @@ edition = "2021"
 | 
			
		||||
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
 | 
			
		||||
regex = "1.9.3"
 | 
			
		||||
speedy2d = { version = "1.12.0", optional = true }
 | 
			
		||||
toml = "0.7.6"
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
default = ["speedy2d"]
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
use std::{
 | 
			
		||||
    any::Any,
 | 
			
		||||
    eprintln,
 | 
			
		||||
    io::{Read, Write},
 | 
			
		||||
    net::TcpStream,
 | 
			
		||||
    sync::{Arc, Mutex},
 | 
			
		||||
    time::Instant,
 | 
			
		||||
@ -10,7 +11,7 @@ use std::{
 | 
			
		||||
use musicdb_lib::{
 | 
			
		||||
    data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId},
 | 
			
		||||
    load::ToFromBytes,
 | 
			
		||||
    server::Command,
 | 
			
		||||
    server::{get, Command},
 | 
			
		||||
};
 | 
			
		||||
use speedy2d::{
 | 
			
		||||
    color::Color,
 | 
			
		||||
@ -33,11 +34,74 @@ pub enum GuiEvent {
 | 
			
		||||
    Exit,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn main(
 | 
			
		||||
pub fn main<T: Write + Read + 'static + Sync + Send>(
 | 
			
		||||
    database: Arc<Mutex<Database>>,
 | 
			
		||||
    connection: TcpStream,
 | 
			
		||||
    get_con: get::Client<T>,
 | 
			
		||||
    event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
 | 
			
		||||
) {
 | 
			
		||||
    let mut config_file = super::get_config_file_path();
 | 
			
		||||
    config_file.push("config_gui.toml");
 | 
			
		||||
    let mut font = None;
 | 
			
		||||
    let mut line_height = 32.0;
 | 
			
		||||
    let mut scroll_pixels_multiplier = 1.0;
 | 
			
		||||
    let mut scroll_lines_multiplier = 3.0;
 | 
			
		||||
    let mut scroll_pages_multiplier = 0.75;
 | 
			
		||||
    match std::fs::read_to_string(&config_file) {
 | 
			
		||||
        Ok(cfg) => {
 | 
			
		||||
            if let Ok(table) = cfg.parse::<toml::Table>() {
 | 
			
		||||
                if let Some(path) = table["font"].as_str() {
 | 
			
		||||
                    if let Ok(bytes) = std::fs::read(path) {
 | 
			
		||||
                        if let Ok(f) = Font::new(&bytes) {
 | 
			
		||||
                            font = Some(f);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            eprintln!("[toml] couldn't load font")
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        eprintln!("[toml] couldn't read font file")
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(v) = table.get("line_height").and_then(|v| v.as_float()) {
 | 
			
		||||
                    line_height = v as _;
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(v) = table
 | 
			
		||||
                    .get("scroll_pixels_multiplier")
 | 
			
		||||
                    .and_then(|v| v.as_float())
 | 
			
		||||
                {
 | 
			
		||||
                    scroll_pixels_multiplier = v;
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(v) = table
 | 
			
		||||
                    .get("scroll_lines_multiplier")
 | 
			
		||||
                    .and_then(|v| v.as_float())
 | 
			
		||||
                {
 | 
			
		||||
                    scroll_lines_multiplier = v;
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(v) = table
 | 
			
		||||
                    .get("scroll_pages_multiplier")
 | 
			
		||||
                    .and_then(|v| v.as_float())
 | 
			
		||||
                {
 | 
			
		||||
                    scroll_pages_multiplier = v;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                eprintln!("Couldn't parse config file {config_file:?} as toml!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            eprintln!("[exit] no config file found at {config_file:?}: {e}");
 | 
			
		||||
            if let Some(p) = config_file.parent() {
 | 
			
		||||
                _ = std::fs::create_dir_all(p);
 | 
			
		||||
            }
 | 
			
		||||
            _ = std::fs::write(&config_file, "font = \"\"");
 | 
			
		||||
            std::process::exit(25);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    let font = if let Some(v) = font {
 | 
			
		||||
        v
 | 
			
		||||
    } else {
 | 
			
		||||
        eprintln!("[toml] required: font = <string>");
 | 
			
		||||
        std::process::exit(30);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let window = speedy2d::Window::<GuiEvent>::new_with_user_events(
 | 
			
		||||
        "MusicDB Client",
 | 
			
		||||
        WindowCreationOptions::new_fullscreen_borderless(),
 | 
			
		||||
@ -45,7 +109,18 @@ pub fn main(
 | 
			
		||||
    .expect("couldn't open window");
 | 
			
		||||
    *event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender());
 | 
			
		||||
    let sender = window.create_user_event_sender();
 | 
			
		||||
    window.run_loop(Gui::new(database, connection, event_sender_arc, sender));
 | 
			
		||||
    window.run_loop(Gui::new(
 | 
			
		||||
        font,
 | 
			
		||||
        database,
 | 
			
		||||
        connection,
 | 
			
		||||
        get_con,
 | 
			
		||||
        event_sender_arc,
 | 
			
		||||
        sender,
 | 
			
		||||
        line_height,
 | 
			
		||||
        scroll_pixels_multiplier,
 | 
			
		||||
        scroll_lines_multiplier,
 | 
			
		||||
        scroll_pages_multiplier,
 | 
			
		||||
    ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Gui {
 | 
			
		||||
@ -69,11 +144,17 @@ pub struct Gui {
 | 
			
		||||
    pub scroll_pages_multiplier: f64,
 | 
			
		||||
}
 | 
			
		||||
impl Gui {
 | 
			
		||||
    fn new(
 | 
			
		||||
    fn new<T: Read + Write + 'static + Sync + Send>(
 | 
			
		||||
        font: Font,
 | 
			
		||||
        database: Arc<Mutex<Database>>,
 | 
			
		||||
        connection: TcpStream,
 | 
			
		||||
        get_con: get::Client<T>,
 | 
			
		||||
        event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
 | 
			
		||||
        event_sender: UserEventSender<GuiEvent>,
 | 
			
		||||
        line_height: f32,
 | 
			
		||||
        scroll_pixels_multiplier: f64,
 | 
			
		||||
        scroll_lines_multiplier: f64,
 | 
			
		||||
        scroll_pages_multiplier: f64,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        database.lock().unwrap().update_endpoints.push(
 | 
			
		||||
            musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
 | 
			
		||||
@ -87,7 +168,8 @@ impl Gui {
 | 
			
		||||
                | Command::QueueAdd(..)
 | 
			
		||||
                | Command::QueueInsert(..)
 | 
			
		||||
                | Command::QueueRemove(..)
 | 
			
		||||
                | Command::QueueGoto(..) => {
 | 
			
		||||
                | Command::QueueGoto(..)
 | 
			
		||||
                | Command::QueueSetShuffle(..) => {
 | 
			
		||||
                    if let Some(s) = &*event_sender_arc.lock().unwrap() {
 | 
			
		||||
                        _ = s.send_event(GuiEvent::UpdatedQueue);
 | 
			
		||||
                    }
 | 
			
		||||
@ -96,6 +178,7 @@ impl Gui {
 | 
			
		||||
                | Command::AddSong(_)
 | 
			
		||||
                | Command::AddAlbum(_)
 | 
			
		||||
                | Command::AddArtist(_)
 | 
			
		||||
                | Command::AddCover(_)
 | 
			
		||||
                | Command::ModifySong(_)
 | 
			
		||||
                | Command::ModifyAlbum(_)
 | 
			
		||||
                | Command::ModifyArtist(_) => {
 | 
			
		||||
@ -105,10 +188,6 @@ impl Gui {
 | 
			
		||||
                }
 | 
			
		||||
            })),
 | 
			
		||||
        );
 | 
			
		||||
        let line_height = 32.0;
 | 
			
		||||
        let scroll_pixels_multiplier = 1.0;
 | 
			
		||||
        let scroll_lines_multiplier = 3.0;
 | 
			
		||||
        let scroll_pages_multiplier = 0.75;
 | 
			
		||||
        Gui {
 | 
			
		||||
            event_sender,
 | 
			
		||||
            database,
 | 
			
		||||
@ -117,6 +196,7 @@ impl Gui {
 | 
			
		||||
                VirtualKeyCode::Escape,
 | 
			
		||||
                GuiScreen::new(
 | 
			
		||||
                    GuiElemCfg::default(),
 | 
			
		||||
                    get_con,
 | 
			
		||||
                    line_height,
 | 
			
		||||
                    scroll_pixels_multiplier,
 | 
			
		||||
                    scroll_lines_multiplier,
 | 
			
		||||
@ -125,10 +205,7 @@ impl Gui {
 | 
			
		||||
            )),
 | 
			
		||||
            size: UVec2::ZERO,
 | 
			
		||||
            mouse_pos: Vec2::ZERO,
 | 
			
		||||
            font: Font::new(include_bytes!(
 | 
			
		||||
                "/usr/share/fonts/mozilla-fira/FiraSans-Regular.otf"
 | 
			
		||||
            ))
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
            font,
 | 
			
		||||
            // font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
 | 
			
		||||
            last_draw: Instant::now(),
 | 
			
		||||
            modifiers: ModifiersState::default(),
 | 
			
		||||
@ -328,6 +405,10 @@ pub struct DrawInfo<'a> {
 | 
			
		||||
    pub child_has_keyboard_focus: bool,
 | 
			
		||||
    /// the height of one line of text (in pixels)
 | 
			
		||||
    pub line_height: f32,
 | 
			
		||||
    pub dragging: Option<(
 | 
			
		||||
        Dragging,
 | 
			
		||||
        Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
 | 
			
		||||
    )>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Generic wrapper over anything that implements GuiElemTrait
 | 
			
		||||
@ -679,9 +760,11 @@ impl WindowHandler<GuiEvent> for Gui {
 | 
			
		||||
            has_keyboard_focus: false,
 | 
			
		||||
            child_has_keyboard_focus: true,
 | 
			
		||||
            line_height: self.line_height,
 | 
			
		||||
            dragging: self.dragging.take(),
 | 
			
		||||
        };
 | 
			
		||||
        self.gui.draw(&mut info, graphics);
 | 
			
		||||
        let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
 | 
			
		||||
        self.dragging = info.dragging.take();
 | 
			
		||||
        if let Some((d, f)) = &mut self.dragging {
 | 
			
		||||
            if let Some(f) = f {
 | 
			
		||||
                f(&mut info, graphics);
 | 
			
		||||
@ -753,12 +836,15 @@ impl WindowHandler<GuiEvent> for Gui {
 | 
			
		||||
        distance: speedy2d::window::MouseScrollDistance,
 | 
			
		||||
    ) {
 | 
			
		||||
        let dist = match distance {
 | 
			
		||||
            MouseScrollDistance::Pixels { y, .. } => (self.scroll_pixels_multiplier * y) as f32,
 | 
			
		||||
            MouseScrollDistance::Pixels { y, .. } => {
 | 
			
		||||
                (self.scroll_pixels_multiplier * y * self.scroll_lines_multiplier) as f32
 | 
			
		||||
            }
 | 
			
		||||
            MouseScrollDistance::Lines { y, .. } => {
 | 
			
		||||
                (self.scroll_lines_multiplier * y) as f32 * self.line_height
 | 
			
		||||
            }
 | 
			
		||||
            MouseScrollDistance::Pages { y, .. } => {
 | 
			
		||||
                (self.scroll_pages_multiplier * y) as f32 * self.last_height
 | 
			
		||||
                (self.scroll_pages_multiplier * y * self.scroll_lines_multiplier) as f32
 | 
			
		||||
                    * self.last_height
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(a) = self.gui.mouse_wheel(dist, self.mouse_pos.clone()) {
 | 
			
		||||
 | 
			
		||||
@ -63,6 +63,55 @@ impl GuiElemTrait for Panel {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct Square {
 | 
			
		||||
    config: GuiElemCfg,
 | 
			
		||||
    pub inner: GuiElem,
 | 
			
		||||
}
 | 
			
		||||
impl Square {
 | 
			
		||||
    pub fn new(mut config: GuiElemCfg, inner: GuiElem) -> Self {
 | 
			
		||||
        config.redraw = true;
 | 
			
		||||
        Self { config, inner }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl GuiElemTrait for Square {
 | 
			
		||||
    fn config(&self) -> &GuiElemCfg {
 | 
			
		||||
        &self.config
 | 
			
		||||
    }
 | 
			
		||||
    fn config_mut(&mut self) -> &mut GuiElemCfg {
 | 
			
		||||
        &mut self.config
 | 
			
		||||
    }
 | 
			
		||||
    fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
 | 
			
		||||
        Box::new([&mut self.inner].into_iter())
 | 
			
		||||
    }
 | 
			
		||||
    fn any(&self) -> &dyn std::any::Any {
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    fn any_mut(&mut self) -> &mut dyn std::any::Any {
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
 | 
			
		||||
        Box::new(self.clone())
 | 
			
		||||
    }
 | 
			
		||||
    fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
 | 
			
		||||
        if info.pos.size() != self.config.pixel_pos.size() {
 | 
			
		||||
            self.config.redraw = true;
 | 
			
		||||
        }
 | 
			
		||||
        if self.config.redraw {
 | 
			
		||||
            self.config.redraw = false;
 | 
			
		||||
            if info.pos.width() > info.pos.height() {
 | 
			
		||||
                let w = 0.5 * info.pos.height() / info.pos.width();
 | 
			
		||||
                self.inner.inner.config_mut().pos =
 | 
			
		||||
                    Rectangle::from_tuples((0.5 - w, 0.0), (0.5 + w, 1.0));
 | 
			
		||||
            } else {
 | 
			
		||||
                let h = 0.5 * info.pos.width() / info.pos.height();
 | 
			
		||||
                self.inner.inner.config_mut().pos =
 | 
			
		||||
                    Rectangle::from_tuples((0.0, 0.5 - h), (1.0, 0.5 + h));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct ScrollBox {
 | 
			
		||||
    config: GuiElemCfg,
 | 
			
		||||
 | 
			
		||||
@ -189,6 +189,28 @@ impl LibraryBrowser {
 | 
			
		||||
                    )),
 | 
			
		||||
                    artist_height,
 | 
			
		||||
                ));
 | 
			
		||||
                for song_id in &artist.singles {
 | 
			
		||||
                    if let Some(song) = db.songs().get(song_id) {
 | 
			
		||||
                        if self.search_song.is_empty()
 | 
			
		||||
                            || self
 | 
			
		||||
                                .search_song_regex
 | 
			
		||||
                                .as_ref()
 | 
			
		||||
                                .is_some_and(|regex| regex.is_match(&song.title))
 | 
			
		||||
                        {
 | 
			
		||||
                            if let Some(g) = artist_gui.take() {
 | 
			
		||||
                                gui_elements.push(g);
 | 
			
		||||
                            }
 | 
			
		||||
                            gui_elements.push((
 | 
			
		||||
                                GuiElem::new(ListSong::new(
 | 
			
		||||
                                    GuiElemCfg::default(),
 | 
			
		||||
                                    *song_id,
 | 
			
		||||
                                    song.title.clone(),
 | 
			
		||||
                                )),
 | 
			
		||||
                                song_height,
 | 
			
		||||
                            ));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                for album_id in &artist.albums {
 | 
			
		||||
                    if let Some(album) = db.albums().get(album_id) {
 | 
			
		||||
                        if self.search_album.is_empty()
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,15 @@
 | 
			
		||||
use musicdb_lib::{
 | 
			
		||||
    data::{queue::QueueContent, SongId},
 | 
			
		||||
    server::Command,
 | 
			
		||||
use std::{
 | 
			
		||||
    io::{Cursor, Read, Write},
 | 
			
		||||
    thread::{self, JoinHandle},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use musicdb_lib::{
 | 
			
		||||
    data::{queue::QueueContent, CoverId, SongId},
 | 
			
		||||
    server::{get, Command},
 | 
			
		||||
};
 | 
			
		||||
use speedy2d::{
 | 
			
		||||
    color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle, window::MouseButton,
 | 
			
		||||
};
 | 
			
		||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    gui::{adjust_area, adjust_pos, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
 | 
			
		||||
@ -16,38 +23,60 @@ This file could probably have a better name.
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct CurrentSong {
 | 
			
		||||
pub struct CurrentSong<T: Read + Write> {
 | 
			
		||||
    config: GuiElemCfg,
 | 
			
		||||
    children: Vec<GuiElem>,
 | 
			
		||||
    get_con: Option<get::Client<T>>,
 | 
			
		||||
    prev_song: Option<SongId>,
 | 
			
		||||
    cover_pos: Rectangle,
 | 
			
		||||
    cover_id: Option<CoverId>,
 | 
			
		||||
    cover: Option<ImageHandle>,
 | 
			
		||||
    new_cover: Option<JoinHandle<(get::Client<T>, Option<Vec<u8>>)>>,
 | 
			
		||||
}
 | 
			
		||||
impl CurrentSong {
 | 
			
		||||
    pub fn new(config: GuiElemCfg) -> Self {
 | 
			
		||||
impl<T: Read + Write> Clone for CurrentSong<T> {
 | 
			
		||||
    fn clone(&self) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            config: self.config.clone(),
 | 
			
		||||
            children: self.children.clone(),
 | 
			
		||||
            get_con: None,
 | 
			
		||||
            prev_song: None,
 | 
			
		||||
            cover_pos: self.cover_pos.clone(),
 | 
			
		||||
            cover_id: None,
 | 
			
		||||
            cover: None,
 | 
			
		||||
            new_cover: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl<T: Read + Write + 'static + Sync + Send> CurrentSong<T> {
 | 
			
		||||
    pub fn new(config: GuiElemCfg, get_con: get::Client<T>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            config,
 | 
			
		||||
            children: vec![
 | 
			
		||||
                GuiElem::new(Label::new(
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.5))),
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.0), (1.0, 0.5))),
 | 
			
		||||
                    "".to_owned(),
 | 
			
		||||
                    Color::from_int_rgb(180, 180, 210),
 | 
			
		||||
                    None,
 | 
			
		||||
                    Vec2::new(0.1, 1.0),
 | 
			
		||||
                    Vec2::new(0.0, 1.0),
 | 
			
		||||
                )),
 | 
			
		||||
                GuiElem::new(Label::new(
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.5), (0.5, 1.0))),
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.5), (1.0, 1.0))),
 | 
			
		||||
                    "".to_owned(),
 | 
			
		||||
                    Color::from_int_rgb(120, 120, 120),
 | 
			
		||||
                    None,
 | 
			
		||||
                    Vec2::new(0.3, 0.0),
 | 
			
		||||
                    Vec2::new(0.0, 0.0),
 | 
			
		||||
                )),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            get_con: Some(get_con),
 | 
			
		||||
            cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
 | 
			
		||||
            cover_id: None,
 | 
			
		||||
            prev_song: None,
 | 
			
		||||
            cover: None,
 | 
			
		||||
            new_cover: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl GuiElemTrait for CurrentSong {
 | 
			
		||||
impl<T: Read + Write + 'static + Sync + Send> GuiElemTrait for CurrentSong<T> {
 | 
			
		||||
    fn config(&self) -> &GuiElemCfg {
 | 
			
		||||
        &self.config
 | 
			
		||||
    }
 | 
			
		||||
@ -67,34 +96,109 @@ impl GuiElemTrait for CurrentSong {
 | 
			
		||||
        Box::new(self.clone())
 | 
			
		||||
    }
 | 
			
		||||
    fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
 | 
			
		||||
        let song = if let Some(v) = info.database.queue.get_current() {
 | 
			
		||||
            if let QueueContent::Song(song) = v.content() {
 | 
			
		||||
        // check if there is a new song
 | 
			
		||||
        let new_song = if let Some(song) = info.database.queue.get_current_song() {
 | 
			
		||||
            if Some(*song) == self.prev_song {
 | 
			
		||||
                // same song as before
 | 
			
		||||
                    return;
 | 
			
		||||
                None
 | 
			
		||||
            } else {
 | 
			
		||||
                    Some(*song)
 | 
			
		||||
                Some(Some(*song))
 | 
			
		||||
            }
 | 
			
		||||
        } else if self.prev_song.is_none() {
 | 
			
		||||
            // no song, nothing in queue
 | 
			
		||||
                return;
 | 
			
		||||
            } else {
 | 
			
		||||
            None
 | 
			
		||||
            }
 | 
			
		||||
        } else if self.prev_song.is_none() {
 | 
			
		||||
            // no song, nothing in queue
 | 
			
		||||
            return;
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
            self.cover = None;
 | 
			
		||||
            Some(None)
 | 
			
		||||
        };
 | 
			
		||||
        if self.prev_song != song {
 | 
			
		||||
        // drawing stuff
 | 
			
		||||
        if self.config.pixel_pos.size() != info.pos.size() {
 | 
			
		||||
            let leftright = 0.05;
 | 
			
		||||
            let topbottom = 0.05;
 | 
			
		||||
            let mut width = 0.3;
 | 
			
		||||
            let mut height = 1.0 - topbottom * 2.0;
 | 
			
		||||
            if width * info.pos.width() < height * info.pos.height() {
 | 
			
		||||
                height = width * info.pos.width() / info.pos.height();
 | 
			
		||||
            } else {
 | 
			
		||||
                width = height * info.pos.height() / info.pos.width();
 | 
			
		||||
            }
 | 
			
		||||
            let right = leftright + width + leftright;
 | 
			
		||||
            self.cover_pos = Rectangle::from_tuples(
 | 
			
		||||
                (leftright, 0.5 - 0.5 * height),
 | 
			
		||||
                (leftright + width, 0.5 + 0.5 * height),
 | 
			
		||||
            );
 | 
			
		||||
            for el in self.children.iter_mut().take(2) {
 | 
			
		||||
                let pos = &mut el.inner.config_mut().pos;
 | 
			
		||||
                *pos = Rectangle::new(Vec2::new(right, pos.top_left().y), *pos.bottom_right());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if self.new_cover.as_ref().is_some_and(|v| v.is_finished()) {
 | 
			
		||||
            let (get_con, cover) = self.new_cover.take().unwrap().join().unwrap();
 | 
			
		||||
            self.get_con = Some(get_con);
 | 
			
		||||
            if let Some(cover) = cover {
 | 
			
		||||
                self.cover = g
 | 
			
		||||
                    .create_image_from_file_bytes(
 | 
			
		||||
                        None,
 | 
			
		||||
                        speedy2d::image::ImageSmoothingMode::Linear,
 | 
			
		||||
                        Cursor::new(cover),
 | 
			
		||||
                    )
 | 
			
		||||
                    .ok();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(cover) = &self.cover {
 | 
			
		||||
            g.draw_rectangle_image(
 | 
			
		||||
                Rectangle::new(
 | 
			
		||||
                    Vec2::new(
 | 
			
		||||
                        info.pos.top_left().x + info.pos.width() * self.cover_pos.top_left().x,
 | 
			
		||||
                        info.pos.top_left().y + info.pos.height() * self.cover_pos.top_left().y,
 | 
			
		||||
                    ),
 | 
			
		||||
                    Vec2::new(
 | 
			
		||||
                        info.pos.top_left().x + info.pos.width() * self.cover_pos.bottom_right().x,
 | 
			
		||||
                        info.pos.top_left().y + info.pos.height() * self.cover_pos.bottom_right().y,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                cover,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if let Some(new_song) = new_song {
 | 
			
		||||
            // if there is a new song:
 | 
			
		||||
            if self.prev_song != new_song {
 | 
			
		||||
                self.config.redraw = true;
 | 
			
		||||
            self.prev_song = song;
 | 
			
		||||
                self.prev_song = new_song;
 | 
			
		||||
            }
 | 
			
		||||
            if self.config.redraw {
 | 
			
		||||
                self.config.redraw = false;
 | 
			
		||||
            let (name, subtext) = if let Some(song) = song {
 | 
			
		||||
                let (name, subtext) = if let Some(song) = new_song {
 | 
			
		||||
                    if let Some(song) = info.database.get_song(&song) {
 | 
			
		||||
                        let cover = if let Some(v) = song.cover {
 | 
			
		||||
                            Some(v)
 | 
			
		||||
                        } else if let Some(v) = song
 | 
			
		||||
                            .album
 | 
			
		||||
                            .as_ref()
 | 
			
		||||
                            .and_then(|id| info.database.albums().get(id))
 | 
			
		||||
                            .and_then(|album| album.cover)
 | 
			
		||||
                        {
 | 
			
		||||
                            Some(v)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            None
 | 
			
		||||
                        };
 | 
			
		||||
                        if cover != self.cover_id {
 | 
			
		||||
                            self.cover = None;
 | 
			
		||||
                            if let Some(cover) = cover {
 | 
			
		||||
                                if let Some(mut get_con) = self.get_con.take() {
 | 
			
		||||
                                    self.new_cover = Some(thread::spawn(move || {
 | 
			
		||||
                                        match get_con.cover_bytes(cover).unwrap() {
 | 
			
		||||
                                            Ok(v) => (get_con, Some(v)),
 | 
			
		||||
                                            Err(e) => {
 | 
			
		||||
                                                eprintln!("couldn't get cover (response: {e})");
 | 
			
		||||
                                                (get_con, None)
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }));
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            self.cover_id = cover;
 | 
			
		||||
                        }
 | 
			
		||||
                        let sub = match (
 | 
			
		||||
                            song.artist
 | 
			
		||||
                                .as_ref()
 | 
			
		||||
@ -142,6 +246,7 @@ impl GuiElemTrait for CurrentSong {
 | 
			
		||||
                    .text() = subtext;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
@ -230,6 +335,8 @@ impl GuiElemTrait for PlayPauseToggle {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
 | 
			
		||||
        match button {
 | 
			
		||||
            MouseButton::Left => {
 | 
			
		||||
                if !self.playing_waiting_for_change {
 | 
			
		||||
                    self.playing_target = !self.playing_target;
 | 
			
		||||
                    self.playing_waiting_for_change = true;
 | 
			
		||||
@ -242,4 +349,8 @@ impl GuiElemTrait for PlayPauseToggle {
 | 
			
		||||
                    vec![]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            MouseButton::Right => vec![GuiAction::SendToServer(Command::NextSong)],
 | 
			
		||||
            _ => vec![],
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,5 +1,9 @@
 | 
			
		||||
use std::time::{Duration, Instant};
 | 
			
		||||
use std::{
 | 
			
		||||
    io::{Read, Write},
 | 
			
		||||
    time::{Duration, Instant},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use musicdb_lib::server::get;
 | 
			
		||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
@ -44,8 +48,9 @@ pub struct GuiScreen {
 | 
			
		||||
    pub prev_mouse_pos: Vec2,
 | 
			
		||||
}
 | 
			
		||||
impl GuiScreen {
 | 
			
		||||
    pub fn new(
 | 
			
		||||
    pub fn new<T: Read + Write + 'static + Sync + Send>(
 | 
			
		||||
        config: GuiElemCfg,
 | 
			
		||||
        get_con: get::Client<T>,
 | 
			
		||||
        line_height: f32,
 | 
			
		||||
        scroll_sensitivity_pixels: f64,
 | 
			
		||||
        scroll_sensitivity_lines: f64,
 | 
			
		||||
@ -57,6 +62,7 @@ impl GuiScreen {
 | 
			
		||||
                GuiElem::new(StatusBar::new(
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
 | 
			
		||||
                    true,
 | 
			
		||||
                    get_con,
 | 
			
		||||
                )),
 | 
			
		||||
                GuiElem::new(Settings::new(
 | 
			
		||||
                    GuiElemCfg::default().disabled(),
 | 
			
		||||
@ -69,7 +75,7 @@ impl GuiScreen {
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
 | 
			
		||||
                    vec![
 | 
			
		||||
                        GuiElem::new(Button::new(
 | 
			
		||||
                            GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.1))),
 | 
			
		||||
                            GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))),
 | 
			
		||||
                            |_| vec![GuiAction::OpenSettings(true)],
 | 
			
		||||
                            vec![GuiElem::new(Label::new(
 | 
			
		||||
                                GuiElemCfg::default(),
 | 
			
		||||
@ -80,7 +86,7 @@ impl GuiScreen {
 | 
			
		||||
                            ))],
 | 
			
		||||
                        )),
 | 
			
		||||
                        GuiElem::new(Button::new(
 | 
			
		||||
                            GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 0.1))),
 | 
			
		||||
                            GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 0.03))),
 | 
			
		||||
                            |_| vec![GuiAction::Exit],
 | 
			
		||||
                            vec![GuiElem::new(Label::new(
 | 
			
		||||
                                GuiElemCfg::default(),
 | 
			
		||||
@ -95,7 +101,7 @@ impl GuiScreen {
 | 
			
		||||
                            (0.5, 1.0),
 | 
			
		||||
                        )))),
 | 
			
		||||
                        GuiElem::new(QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples(
 | 
			
		||||
                            (0.5, 0.1),
 | 
			
		||||
                            (0.5, 0.03),
 | 
			
		||||
                            (1.0, 1.0),
 | 
			
		||||
                        )))),
 | 
			
		||||
                    ],
 | 
			
		||||
@ -199,9 +205,10 @@ impl GuiElemTrait for GuiScreen {
 | 
			
		||||
            if !self.idle.0 || self.idle.1.is_none() {
 | 
			
		||||
                if let Some(h) = &info.helper {
 | 
			
		||||
                    h.set_cursor_visible(!self.idle.0);
 | 
			
		||||
                    for el in self.children.iter_mut().skip(1) {
 | 
			
		||||
                        el.inner.config_mut().enabled = !self.idle.0;
 | 
			
		||||
                    if self.settings.0 {
 | 
			
		||||
                        self.children[1].inner.config_mut().enabled = !self.idle.0;
 | 
			
		||||
                    }
 | 
			
		||||
                    self.children[2].inner.config_mut().enabled = !self.idle.0;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            let p = transition(p1);
 | 
			
		||||
@ -241,14 +248,18 @@ pub struct StatusBar {
 | 
			
		||||
    idle_mode: f32,
 | 
			
		||||
}
 | 
			
		||||
impl StatusBar {
 | 
			
		||||
    pub fn new(config: GuiElemCfg, playing: bool) -> Self {
 | 
			
		||||
    pub fn new<T: Read + Write + 'static + Sync + Send>(
 | 
			
		||||
        config: GuiElemCfg,
 | 
			
		||||
        playing: bool,
 | 
			
		||||
        get_con: get::Client<T>,
 | 
			
		||||
    ) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            config,
 | 
			
		||||
            children: vec![
 | 
			
		||||
                GuiElem::new(CurrentSong::new(GuiElemCfg::at(Rectangle::new(
 | 
			
		||||
                    Vec2::ZERO,
 | 
			
		||||
                    Vec2::new(0.8, 1.0),
 | 
			
		||||
                )))),
 | 
			
		||||
                GuiElem::new(CurrentSong::new(
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::new(Vec2::ZERO, Vec2::new(0.8, 1.0))),
 | 
			
		||||
                    get_con,
 | 
			
		||||
                )),
 | 
			
		||||
                GuiElem::new(PlayPauseToggle::new(
 | 
			
		||||
                    GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
 | 
			
		||||
                    false,
 | 
			
		||||
 | 
			
		||||
@ -124,7 +124,7 @@ impl Settings {
 | 
			
		||||
                                            (0.0, 0.0),
 | 
			
		||||
                                            (0.33, 1.0),
 | 
			
		||||
                                        )),
 | 
			
		||||
                                        "Scroll Sensitivity (lines)".to_string(),
 | 
			
		||||
                                        "Scroll Sensitivity".to_string(),
 | 
			
		||||
                                        Color::WHITE,
 | 
			
		||||
                                        None,
 | 
			
		||||
                                        Vec2::new(0.9, 0.5),
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
use std::{
 | 
			
		||||
    eprintln, fs,
 | 
			
		||||
    io::{BufReader, Write},
 | 
			
		||||
    net::{SocketAddr, TcpStream},
 | 
			
		||||
    path::PathBuf,
 | 
			
		||||
    sync::{Arc, Mutex},
 | 
			
		||||
@ -10,12 +11,16 @@ use std::{
 | 
			
		||||
use gui::GuiEvent;
 | 
			
		||||
use musicdb_lib::{
 | 
			
		||||
    data::{
 | 
			
		||||
        album::Album, artist::Artist, database::Database, queue::QueueContent, song::Song,
 | 
			
		||||
        album::Album,
 | 
			
		||||
        artist::Artist,
 | 
			
		||||
        database::{Cover, Database},
 | 
			
		||||
        queue::QueueContent,
 | 
			
		||||
        song::Song,
 | 
			
		||||
        DatabaseLocation, GeneralData,
 | 
			
		||||
    },
 | 
			
		||||
    load::ToFromBytes,
 | 
			
		||||
    player::Player,
 | 
			
		||||
    server::Command,
 | 
			
		||||
    server::{get, Command},
 | 
			
		||||
};
 | 
			
		||||
#[cfg(feature = "speedy2d")]
 | 
			
		||||
mod gui;
 | 
			
		||||
@ -43,6 +48,22 @@ enum Mode {
 | 
			
		||||
    FillDb,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_config_file_path() -> PathBuf {
 | 
			
		||||
    if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
 | 
			
		||||
        let mut config_home: PathBuf = config_home.into();
 | 
			
		||||
        config_home.push("musicdb-client");
 | 
			
		||||
        config_home
 | 
			
		||||
    } else if let Ok(home) = std::env::var("HOME") {
 | 
			
		||||
        let mut config_home: PathBuf = home.into();
 | 
			
		||||
        config_home.push(".config");
 | 
			
		||||
        config_home.push("musicdb-client");
 | 
			
		||||
        config_home
 | 
			
		||||
    } else {
 | 
			
		||||
        eprintln!("No config directory!");
 | 
			
		||||
        std::process::exit(24);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    let mut args = std::env::args().skip(1);
 | 
			
		||||
    let mode = match args.next().as_ref().map(|v| v.trim()) {
 | 
			
		||||
@ -56,7 +77,9 @@ fn main() {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    let addr = args.next().unwrap_or("127.0.0.1:26314".to_string());
 | 
			
		||||
    let mut con = TcpStream::connect(addr.parse::<SocketAddr>().unwrap()).unwrap();
 | 
			
		||||
    let addr = addr.parse::<SocketAddr>().unwrap();
 | 
			
		||||
    let mut con = TcpStream::connect(addr).unwrap();
 | 
			
		||||
    writeln!(con, "main").unwrap();
 | 
			
		||||
    let database = Arc::new(Mutex::new(Database::new_clientside()));
 | 
			
		||||
    #[cfg(feature = "speedy2d")]
 | 
			
		||||
    let update_gui_sender: Arc<Mutex<Option<speedy2d::window::UserEventSender<GuiEvent>>>> =
 | 
			
		||||
@ -111,7 +134,15 @@ fn main() {
 | 
			
		||||
                        v.send_event(GuiEvent::Refresh).unwrap();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                gui::main(database, con, sender)
 | 
			
		||||
                gui::main(
 | 
			
		||||
                    database,
 | 
			
		||||
                    con,
 | 
			
		||||
                    get::Client::new(BufReader::new(
 | 
			
		||||
                        TcpStream::connect(addr).expect("opening get client connection"),
 | 
			
		||||
                    ))
 | 
			
		||||
                    .expect("initializing get client connection"),
 | 
			
		||||
                    sender,
 | 
			
		||||
                )
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        Mode::SyncPlayer => {
 | 
			
		||||
@ -134,6 +165,7 @@ fn main() {
 | 
			
		||||
            let mut line = String::new();
 | 
			
		||||
            std::io::stdin().read_line(&mut line).unwrap();
 | 
			
		||||
            if line.trim().to_lowercase() == "yes" {
 | 
			
		||||
                let mut covers = 0;
 | 
			
		||||
                for artist in fs::read_dir(&dir)
 | 
			
		||||
                    .expect("reading lib-dir")
 | 
			
		||||
                    .filter_map(|v| v.ok())
 | 
			
		||||
@ -147,6 +179,16 @@ fn main() {
 | 
			
		||||
                                let mut album_id = None;
 | 
			
		||||
                                let mut songs: Vec<_> = songs.filter_map(|v| v.ok()).collect();
 | 
			
		||||
                                songs.sort_unstable_by_key(|v| v.file_name());
 | 
			
		||||
                                let cover = songs.iter().map(|entry| entry.path()).find(|path| {
 | 
			
		||||
                                    path.extension().is_some_and(|ext| {
 | 
			
		||||
                                        ext.to_str().is_some_and(|ext| {
 | 
			
		||||
                                            matches!(
 | 
			
		||||
                                                ext.to_lowercase().trim(),
 | 
			
		||||
                                                "png" | "jpg" | "jpeg"
 | 
			
		||||
                                            )
 | 
			
		||||
                                        })
 | 
			
		||||
                                    })
 | 
			
		||||
                                });
 | 
			
		||||
                                for song in songs {
 | 
			
		||||
                                    match song.path().extension().map(|v| v.to_str()) {
 | 
			
		||||
                                        Some(Some(
 | 
			
		||||
@ -229,11 +271,39 @@ fn main() {
 | 
			
		||||
                                                        drop(db);
 | 
			
		||||
                                                        if !adding_album {
 | 
			
		||||
                                                            adding_album = true;
 | 
			
		||||
                                                            let cover = if let Some(cover) = &cover
 | 
			
		||||
                                                            {
 | 
			
		||||
                                                                eprintln!("Adding cover {cover:?}");
 | 
			
		||||
                                                                Command::AddCover(Cover {
 | 
			
		||||
                                                                    location: DatabaseLocation {
 | 
			
		||||
                                                                        rel_path: PathBuf::from(
 | 
			
		||||
                                                                            artist.file_name(),
 | 
			
		||||
                                                                        )
 | 
			
		||||
                                                                        .join(album.file_name())
 | 
			
		||||
                                                                        .join(
 | 
			
		||||
                                                                            cover
 | 
			
		||||
                                                                                .file_name()
 | 
			
		||||
                                                                                .unwrap(),
 | 
			
		||||
                                                                        ),
 | 
			
		||||
                                                                    },
 | 
			
		||||
                                                                    data: Arc::new(Mutex::new((
 | 
			
		||||
                                                                        false, None,
 | 
			
		||||
                                                                    ))),
 | 
			
		||||
                                                                })
 | 
			
		||||
                                                                .to_bytes(&mut con)
 | 
			
		||||
                                                                .expect(
 | 
			
		||||
                                                                    "sending AddCover to db failed",
 | 
			
		||||
                                                                );
 | 
			
		||||
                                                                covers += 1;
 | 
			
		||||
                                                                Some(covers - 1)
 | 
			
		||||
                                                            } else {
 | 
			
		||||
                                                                None
 | 
			
		||||
                                                            };
 | 
			
		||||
                                                            Command::AddAlbum(Album {
 | 
			
		||||
                                                                id: 0,
 | 
			
		||||
                                                                name: album_name.clone(),
 | 
			
		||||
                                                                artist: Some(artist_id),
 | 
			
		||||
                                                                cover: None,
 | 
			
		||||
                                                                cover,
 | 
			
		||||
                                                                songs: vec![],
 | 
			
		||||
                                                                general: GeneralData::default(),
 | 
			
		||||
                                                            })
 | 
			
		||||
 | 
			
		||||
@ -8,5 +8,6 @@ edition = "2021"
 | 
			
		||||
[dependencies]
 | 
			
		||||
awedio = "0.2.0"
 | 
			
		||||
base64 = "0.21.2"
 | 
			
		||||
rand = "0.8.5"
 | 
			
		||||
rc-u8-reader = "2.0.16"
 | 
			
		||||
tokio = "1.29.1"
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,8 @@ use std::{
 | 
			
		||||
    fs::{self, File},
 | 
			
		||||
    io::{BufReader, Write},
 | 
			
		||||
    path::PathBuf,
 | 
			
		||||
    sync::{mpsc, Arc},
 | 
			
		||||
    time::Instant,
 | 
			
		||||
    sync::{mpsc, Arc, Mutex},
 | 
			
		||||
    time::{Duration, Instant},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{load::ToFromBytes, server::Command};
 | 
			
		||||
@ -18,16 +18,14 @@ use super::{
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub struct Database {
 | 
			
		||||
    /// the path to the file used to save/load the data
 | 
			
		||||
    db_file: PathBuf,
 | 
			
		||||
    /// the path to the file used to save/load the data. empty if database is in client mode.
 | 
			
		||||
    pub db_file: PathBuf,
 | 
			
		||||
    /// the path to the directory containing the actual music and cover image files
 | 
			
		||||
    pub lib_directory: PathBuf,
 | 
			
		||||
    artists: HashMap<ArtistId, Artist>,
 | 
			
		||||
    albums: HashMap<AlbumId, Album>,
 | 
			
		||||
    songs: HashMap<SongId, Song>,
 | 
			
		||||
    covers: HashMap<CoverId, DatabaseLocation>,
 | 
			
		||||
    // TODO! make sure this works out for the server AND clients
 | 
			
		||||
    // cover_cache: HashMap<CoverId, Vec<u8>>,
 | 
			
		||||
    covers: HashMap<CoverId, Cover>,
 | 
			
		||||
    // These will be used for autosave once that gets implemented
 | 
			
		||||
    db_data_file_change_first: Option<Instant>,
 | 
			
		||||
    db_data_file_change_last: Option<Instant>,
 | 
			
		||||
@ -132,6 +130,21 @@ impl Database {
 | 
			
		||||
        }
 | 
			
		||||
        self.panic("database.artists all keys used - no more capacity for new artists!");
 | 
			
		||||
    }
 | 
			
		||||
    /// adds a cover to the database.
 | 
			
		||||
    /// assigns a new id, which it then returns.
 | 
			
		||||
    pub fn add_cover_new(&mut self, cover: Cover) -> AlbumId {
 | 
			
		||||
        self.add_cover_new_nomagic(cover)
 | 
			
		||||
    }
 | 
			
		||||
    /// used internally
 | 
			
		||||
    fn add_cover_new_nomagic(&mut self, cover: Cover) -> AlbumId {
 | 
			
		||||
        for key in 0.. {
 | 
			
		||||
            if !self.covers.contains_key(&key) {
 | 
			
		||||
                self.covers.insert(key, cover);
 | 
			
		||||
                return key;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        self.panic("database.artists all keys used - no more capacity for new artists!");
 | 
			
		||||
    }
 | 
			
		||||
    /// updates an existing song in the database with the new value.
 | 
			
		||||
    /// uses song.id to find the correct song.
 | 
			
		||||
    /// if the id doesn't exist in the db, Err(()) is returned.
 | 
			
		||||
@ -193,7 +206,13 @@ impl Database {
 | 
			
		||||
            Command::Pause => self.playing = false,
 | 
			
		||||
            Command::Stop => self.playing = false,
 | 
			
		||||
            Command::NextSong => {
 | 
			
		||||
                self.queue.advance_index();
 | 
			
		||||
                if !Queue::advance_index_db(self) {
 | 
			
		||||
                    // end of queue
 | 
			
		||||
                    self.apply_command(Command::Pause);
 | 
			
		||||
                    let mut actions = Vec::new();
 | 
			
		||||
                    self.queue.init(vec![], &mut actions);
 | 
			
		||||
                    Queue::handle_actions(self, actions);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Command::Save => {
 | 
			
		||||
                if let Err(e) = self.save_database(None) {
 | 
			
		||||
@ -208,18 +227,37 @@ impl Database {
 | 
			
		||||
            }
 | 
			
		||||
            Command::QueueAdd(mut index, new_data) => {
 | 
			
		||||
                if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
 | 
			
		||||
                    v.add_to_end(new_data);
 | 
			
		||||
                    if let Some(i) = v.add_to_end(new_data) {
 | 
			
		||||
                        index.push(i);
 | 
			
		||||
                        if let Some(q) = self.queue.get_item_at_index_mut(&index, 0) {
 | 
			
		||||
                            let mut actions = Vec::new();
 | 
			
		||||
                            q.init(index, &mut actions);
 | 
			
		||||
                            Queue::handle_actions(self, actions);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            Command::QueueInsert(mut index, pos, new_data) => {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Command::QueueInsert(mut index, pos, mut new_data) => {
 | 
			
		||||
                if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
 | 
			
		||||
                    index.push(pos);
 | 
			
		||||
                    let mut actions = Vec::new();
 | 
			
		||||
                    new_data.init(index, &mut actions);
 | 
			
		||||
                    v.insert(new_data, pos);
 | 
			
		||||
                    Queue::handle_actions(self, actions);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Command::QueueRemove(index) => {
 | 
			
		||||
                self.queue.remove_by_index(&index, 0);
 | 
			
		||||
            }
 | 
			
		||||
            Command::QueueGoto(index) => self.queue.set_index(&index, 0),
 | 
			
		||||
            Command::QueueGoto(index) => Queue::set_index_db(self, &index),
 | 
			
		||||
            Command::QueueSetShuffle(path, map, next) => {
 | 
			
		||||
                if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
 | 
			
		||||
                    if let QueueContent::Shuffle(_, m, _, n) = elem.content_mut() {
 | 
			
		||||
                        *m = map;
 | 
			
		||||
                        *n = next;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Command::AddSong(song) => {
 | 
			
		||||
                self.add_song_new(song);
 | 
			
		||||
            }
 | 
			
		||||
@ -229,6 +267,7 @@ impl Database {
 | 
			
		||||
            Command::AddArtist(artist) => {
 | 
			
		||||
                self.add_artist_new(artist);
 | 
			
		||||
            }
 | 
			
		||||
            Command::AddCover(cover) => _ = self.add_cover_new(cover),
 | 
			
		||||
            Command::ModifySong(song) => {
 | 
			
		||||
                _ = self.update_song(song);
 | 
			
		||||
            }
 | 
			
		||||
@ -302,6 +341,7 @@ impl Database {
 | 
			
		||||
            command_sender: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    /// saves the database's contents. save path can be overridden
 | 
			
		||||
    pub fn save_database(&self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> {
 | 
			
		||||
        let path = if let Some(p) = path {
 | 
			
		||||
            p
 | 
			
		||||
@ -386,4 +426,75 @@ impl Database {
 | 
			
		||||
    pub fn artists(&self) -> &HashMap<ArtistId, Artist> {
 | 
			
		||||
        &self.artists
 | 
			
		||||
    }
 | 
			
		||||
    pub fn covers(&self) -> &HashMap<CoverId, Cover> {
 | 
			
		||||
        &self.covers
 | 
			
		||||
    }
 | 
			
		||||
    /// you should probably use a Command to do this...
 | 
			
		||||
    pub fn songs_mut(&mut self) -> &mut HashMap<SongId, Song> {
 | 
			
		||||
        &mut self.songs
 | 
			
		||||
    }
 | 
			
		||||
    /// you should probably use a Command to do this...
 | 
			
		||||
    pub fn albums_mut(&mut self) -> &mut HashMap<AlbumId, Album> {
 | 
			
		||||
        &mut self.albums
 | 
			
		||||
    }
 | 
			
		||||
    /// you should probably use a Command to do this...
 | 
			
		||||
    pub fn artists_mut(&mut self) -> &mut HashMap<ArtistId, Artist> {
 | 
			
		||||
        &mut self.artists
 | 
			
		||||
    }
 | 
			
		||||
    /// you should probably use a Command to do this...
 | 
			
		||||
    pub fn covers_mut(&mut self) -> &mut HashMap<CoverId, Cover> {
 | 
			
		||||
        &mut self.covers
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct Cover {
 | 
			
		||||
    pub location: DatabaseLocation,
 | 
			
		||||
    pub data: Arc<Mutex<(bool, Option<(Instant, Vec<u8>)>)>>,
 | 
			
		||||
}
 | 
			
		||||
impl Cover {
 | 
			
		||||
    pub fn get_bytes<O>(
 | 
			
		||||
        &self,
 | 
			
		||||
        path: impl FnOnce(&DatabaseLocation) -> PathBuf,
 | 
			
		||||
        conv: impl FnOnce(&Vec<u8>) -> O,
 | 
			
		||||
    ) -> Option<O> {
 | 
			
		||||
        let mut data = loop {
 | 
			
		||||
            let data = self.data.lock().unwrap();
 | 
			
		||||
            if data.0 {
 | 
			
		||||
                drop(data);
 | 
			
		||||
                std::thread::sleep(Duration::from_secs(1));
 | 
			
		||||
            } else {
 | 
			
		||||
                break data;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        if let Some((accessed, data)) = &mut data.1 {
 | 
			
		||||
            *accessed = Instant::now();
 | 
			
		||||
            Some(conv(&data))
 | 
			
		||||
        } else {
 | 
			
		||||
            match std::fs::read(path(&self.location)) {
 | 
			
		||||
                Ok(bytes) => {
 | 
			
		||||
                    data.1 = Some((Instant::now(), bytes));
 | 
			
		||||
                    Some(conv(&data.1.as_ref().unwrap().1))
 | 
			
		||||
                }
 | 
			
		||||
                Err(_) => None,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl ToFromBytes for Cover {
 | 
			
		||||
    fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
 | 
			
		||||
    where
 | 
			
		||||
        T: Write,
 | 
			
		||||
    {
 | 
			
		||||
        self.location.to_bytes(s)
 | 
			
		||||
    }
 | 
			
		||||
    fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
 | 
			
		||||
    where
 | 
			
		||||
        T: std::io::Read,
 | 
			
		||||
    {
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            location: ToFromBytes::from_bytes(s)?,
 | 
			
		||||
            data: Arc::new(Mutex::new((false, None))),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,13 @@
 | 
			
		||||
use crate::load::ToFromBytes;
 | 
			
		||||
use std::collections::VecDeque;
 | 
			
		||||
 | 
			
		||||
use super::SongId;
 | 
			
		||||
use rand::{
 | 
			
		||||
    seq::{IteratorRandom, SliceRandom},
 | 
			
		||||
    Rng,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{load::ToFromBytes, server::Command};
 | 
			
		||||
 | 
			
		||||
use super::{database::Database, SongId};
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
pub struct Queue {
 | 
			
		||||
@ -11,6 +18,14 @@ pub struct Queue {
 | 
			
		||||
pub enum QueueContent {
 | 
			
		||||
    Song(SongId),
 | 
			
		||||
    Folder(usize, Vec<Queue>, String),
 | 
			
		||||
    Loop(usize, usize, Box<Queue>),
 | 
			
		||||
    Random(VecDeque<Queue>),
 | 
			
		||||
    Shuffle(usize, Vec<usize>, Vec<Queue>, usize),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum QueueAction {
 | 
			
		||||
    AddRandomSong(Vec<usize>),
 | 
			
		||||
    SetShuffle(Vec<usize>, Vec<usize>, usize),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Queue {
 | 
			
		||||
@ -20,27 +35,53 @@ impl Queue {
 | 
			
		||||
    pub fn content(&self) -> &QueueContent {
 | 
			
		||||
        &self.content
 | 
			
		||||
    }
 | 
			
		||||
    pub fn content_mut(&mut self) -> &mut QueueContent {
 | 
			
		||||
        &mut self.content
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_to_end(&mut self, v: Self) -> bool {
 | 
			
		||||
    pub fn add_to_end(&mut self, v: Self) -> Option<usize> {
 | 
			
		||||
        match &mut self.content {
 | 
			
		||||
            QueueContent::Song(_) => false,
 | 
			
		||||
            QueueContent::Song(_) => None,
 | 
			
		||||
            QueueContent::Folder(_, vec, _) => {
 | 
			
		||||
                vec.push(v);
 | 
			
		||||
                true
 | 
			
		||||
                Some(vec.len() - 1)
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Loop(..) => None,
 | 
			
		||||
            QueueContent::Random(q) => {
 | 
			
		||||
                q.push_back(v);
 | 
			
		||||
                Some(q.len() - 1)
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Shuffle(_, map, elems, _) => {
 | 
			
		||||
                map.push(elems.len());
 | 
			
		||||
                elems.push(v);
 | 
			
		||||
                Some(map.len() - 1)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    pub fn insert(&mut self, v: Self, index: usize) -> bool {
 | 
			
		||||
        match &mut self.content {
 | 
			
		||||
            QueueContent::Song(_) => false,
 | 
			
		||||
            QueueContent::Folder(_, vec, _) => {
 | 
			
		||||
            QueueContent::Folder(current, vec, _) => {
 | 
			
		||||
                if index <= vec.len() {
 | 
			
		||||
                    if *current >= index {
 | 
			
		||||
                        *current += 1;
 | 
			
		||||
                    }
 | 
			
		||||
                    vec.insert(index, v);
 | 
			
		||||
                    true
 | 
			
		||||
                } else {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Shuffle(_, map, elems, _) => {
 | 
			
		||||
                if index <= map.len() {
 | 
			
		||||
                    map.insert(index, elems.len());
 | 
			
		||||
                    elems.push(v);
 | 
			
		||||
                    true
 | 
			
		||||
                } else {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Loop(..) | QueueContent::Random(..) => false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -51,12 +92,22 @@ impl Queue {
 | 
			
		||||
        match &self.content {
 | 
			
		||||
            QueueContent::Song(_) => 1,
 | 
			
		||||
            QueueContent::Folder(_, v, _) => v.iter().map(|v| v.len()).sum(),
 | 
			
		||||
            QueueContent::Random(v) => v.iter().map(|v| v.len()).sum(),
 | 
			
		||||
            QueueContent::Loop(total, _done, inner) => {
 | 
			
		||||
                if *total == 0 {
 | 
			
		||||
                    inner.len()
 | 
			
		||||
                } else {
 | 
			
		||||
                    *total * inner.len()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Shuffle(_, _, v, _) => v.iter().map(|v| v.len()).sum(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// recursively descends the queue until the current active element is found, then returns it.
 | 
			
		||||
    pub fn get_current(&self) -> Option<&Self> {
 | 
			
		||||
        match &self.content {
 | 
			
		||||
            QueueContent::Song(_) => Some(self),
 | 
			
		||||
            QueueContent::Folder(i, v, _) => {
 | 
			
		||||
                let i = *i;
 | 
			
		||||
                if let Some(v) = v.get(i) {
 | 
			
		||||
@ -65,7 +116,9 @@ impl Queue {
 | 
			
		||||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Song(_) => Some(self),
 | 
			
		||||
            QueueContent::Loop(_, _, inner) => inner.get_current(),
 | 
			
		||||
            QueueContent::Random(v) => v.get(v.len().saturating_sub(2))?.get_current(),
 | 
			
		||||
            QueueContent::Shuffle(i, map, elems, _) => elems.get(*map.get(*i)?),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    pub fn get_current_song(&self) -> Option<&SongId> {
 | 
			
		||||
@ -84,6 +137,7 @@ impl Queue {
 | 
			
		||||
    }
 | 
			
		||||
    pub fn get_next(&self) -> Option<&Self> {
 | 
			
		||||
        match &self.content {
 | 
			
		||||
            QueueContent::Song(_) => None,
 | 
			
		||||
            QueueContent::Folder(i, vec, _) => {
 | 
			
		||||
                let i = *i;
 | 
			
		||||
                if let Some(v) = vec.get(i) {
 | 
			
		||||
@ -100,17 +154,107 @@ impl Queue {
 | 
			
		||||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Song(_) => None,
 | 
			
		||||
            QueueContent::Loop(total, current, inner) => {
 | 
			
		||||
                if let Some(v) = inner.get_next() {
 | 
			
		||||
                    Some(v)
 | 
			
		||||
                } else if *total == 0 || current < total {
 | 
			
		||||
                    inner.get_first()
 | 
			
		||||
                } else {
 | 
			
		||||
                    None
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Random(v) => v.get(v.len().saturating_sub(1))?.get_current(),
 | 
			
		||||
            QueueContent::Shuffle(i, map, elems, _) => elems.get(*map.get(*i + 1)?),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    pub fn get_first(&self) -> Option<&Self> {
 | 
			
		||||
        match &self.content {
 | 
			
		||||
            QueueContent::Song(..) => Some(self),
 | 
			
		||||
            QueueContent::Folder(_, v, _) => v.first(),
 | 
			
		||||
            QueueContent::Loop(_, _, q) => q.get_first(),
 | 
			
		||||
            QueueContent::Random(q) => q.front(),
 | 
			
		||||
            QueueContent::Shuffle(i, _, v, next) => {
 | 
			
		||||
                if *i == 0 {
 | 
			
		||||
                    v.get(*i)
 | 
			
		||||
                } else {
 | 
			
		||||
                    v.get(*next)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn advance_index(&mut self) -> bool {
 | 
			
		||||
    pub fn advance_index_db(db: &mut Database) -> bool {
 | 
			
		||||
        let mut actions = vec![];
 | 
			
		||||
        let o = db.queue.advance_index_inner(vec![], &mut actions);
 | 
			
		||||
        Self::handle_actions(db, actions);
 | 
			
		||||
        o
 | 
			
		||||
    }
 | 
			
		||||
    pub fn init(&mut self, path: Vec<usize>, actions: &mut Vec<QueueAction>) {
 | 
			
		||||
        match &mut self.content {
 | 
			
		||||
            QueueContent::Song(..) => {}
 | 
			
		||||
            QueueContent::Folder(_, v, _) => {
 | 
			
		||||
                if let Some(v) = v.first_mut() {
 | 
			
		||||
                    v.init(path, actions);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Loop(_, _, inner) => inner.init(path, actions),
 | 
			
		||||
            QueueContent::Random(q) => {
 | 
			
		||||
                if q.len() == 0 {
 | 
			
		||||
                    actions.push(QueueAction::AddRandomSong(path.clone()));
 | 
			
		||||
                    actions.push(QueueAction::AddRandomSong(path.clone()));
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(q) = q.get_mut(q.len().saturating_sub(2)) {
 | 
			
		||||
                    q.init(path, actions)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Shuffle(current, map, elems, next) => {
 | 
			
		||||
                let mut new_map = (0..elems.len()).filter(|v| *v != *next).collect::<Vec<_>>();
 | 
			
		||||
                new_map.shuffle(&mut rand::thread_rng());
 | 
			
		||||
                if let Some(first) = new_map.first_mut() {
 | 
			
		||||
                    let was_first = std::mem::replace(first, *next);
 | 
			
		||||
                    new_map.push(was_first);
 | 
			
		||||
                } else if *next < elems.len() {
 | 
			
		||||
                    new_map.push(*next);
 | 
			
		||||
                }
 | 
			
		||||
                let new_next = if elems.is_empty() {
 | 
			
		||||
                    0
 | 
			
		||||
                } else {
 | 
			
		||||
                    rand::thread_rng().gen_range(0..elems.len())
 | 
			
		||||
                };
 | 
			
		||||
                actions.push(QueueAction::SetShuffle(path, new_map, new_next));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    pub fn handle_actions(db: &mut Database, actions: Vec<QueueAction>) {
 | 
			
		||||
        for action in actions {
 | 
			
		||||
            match action {
 | 
			
		||||
                QueueAction::AddRandomSong(path) => {
 | 
			
		||||
                    if !db.db_file.as_os_str().is_empty() {
 | 
			
		||||
                        if let Some(song) = db.songs().keys().choose(&mut rand::thread_rng()) {
 | 
			
		||||
                            db.apply_command(Command::QueueAdd(
 | 
			
		||||
                                path,
 | 
			
		||||
                                QueueContent::Song(*song).into(),
 | 
			
		||||
                            ));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                QueueAction::SetShuffle(path, shuf, next) => {
 | 
			
		||||
                    if !db.db_file.as_os_str().is_empty() {
 | 
			
		||||
                        db.apply_command(Command::QueueSetShuffle(path, shuf, next));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    fn advance_index_inner(&mut self, path: Vec<usize>, actions: &mut Vec<QueueAction>) -> bool {
 | 
			
		||||
        match &mut self.content {
 | 
			
		||||
            QueueContent::Song(_) => false,
 | 
			
		||||
            QueueContent::Folder(index, contents, _) => {
 | 
			
		||||
                if let Some(c) = contents.get_mut(*index) {
 | 
			
		||||
                    let mut p = path.clone();
 | 
			
		||||
                    p.push(*index);
 | 
			
		||||
                    if c.advance_index_inner(p, actions) {
 | 
			
		||||
                        // inner value could advance index, do nothing.
 | 
			
		||||
                    if c.advance_index() {
 | 
			
		||||
                        true
 | 
			
		||||
                    } else {
 | 
			
		||||
                        loop {
 | 
			
		||||
@ -118,6 +262,7 @@ impl Queue {
 | 
			
		||||
                                // can advance
 | 
			
		||||
                                *index += 1;
 | 
			
		||||
                                if contents[*index].enabled {
 | 
			
		||||
                                    contents[*index].init(path, actions);
 | 
			
		||||
                                    break true;
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
@ -132,22 +277,113 @@ impl Queue {
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Loop(total, current, inner) => {
 | 
			
		||||
                let mut p = path.clone();
 | 
			
		||||
                p.push(0);
 | 
			
		||||
                if inner.advance_index_inner(p, actions) {
 | 
			
		||||
                    true
 | 
			
		||||
                } else {
 | 
			
		||||
                    *current += 1;
 | 
			
		||||
                    if *total == 0 || *current < *total {
 | 
			
		||||
                        inner.init(path, actions);
 | 
			
		||||
                        true
 | 
			
		||||
                    } else {
 | 
			
		||||
                        *current = 0;
 | 
			
		||||
                        false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Random(q) => {
 | 
			
		||||
                let i = q.len().saturating_sub(2);
 | 
			
		||||
                let mut p = path.clone();
 | 
			
		||||
                p.push(i);
 | 
			
		||||
                if q.get_mut(i)
 | 
			
		||||
                    .is_some_and(|inner| inner.advance_index_inner(p, actions))
 | 
			
		||||
                {
 | 
			
		||||
                    true
 | 
			
		||||
                } else {
 | 
			
		||||
                    if q.len() >= 2 {
 | 
			
		||||
                        q.pop_front();
 | 
			
		||||
                    }
 | 
			
		||||
                    // only sub 1 here because this is before the next random song is added
 | 
			
		||||
                    let i2 = q.len().saturating_sub(1);
 | 
			
		||||
                    if let Some(q) = q.get_mut(i2) {
 | 
			
		||||
                        let mut p = path.clone();
 | 
			
		||||
                        p.push(i2);
 | 
			
		||||
                        q.init(p, actions);
 | 
			
		||||
                    }
 | 
			
		||||
                    actions.push(QueueAction::AddRandomSong(path));
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Shuffle(current, map, elems, _) => {
 | 
			
		||||
                if map
 | 
			
		||||
                    .get(*current)
 | 
			
		||||
                    .and_then(|i| elems.get_mut(*i))
 | 
			
		||||
                    .is_some_and(|q| {
 | 
			
		||||
                        let mut p = path.clone();
 | 
			
		||||
                        p.push(*current);
 | 
			
		||||
                        q.advance_index_inner(p, actions)
 | 
			
		||||
                    })
 | 
			
		||||
                {
 | 
			
		||||
                    true
 | 
			
		||||
                } else {
 | 
			
		||||
                    *current += 1;
 | 
			
		||||
                    if *current < map.len() {
 | 
			
		||||
                        if let Some(elem) = map.get(*current).and_then(|i| elems.get_mut(*i)) {
 | 
			
		||||
                            elem.init(path, actions);
 | 
			
		||||
                        }
 | 
			
		||||
                        true
 | 
			
		||||
                    } else {
 | 
			
		||||
                        *current = 0;
 | 
			
		||||
                        false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn set_index(&mut self, index: &Vec<usize>, depth: usize) {
 | 
			
		||||
        let i = index.get(depth).map(|v| *v).unwrap_or(0);
 | 
			
		||||
    pub fn set_index_db(db: &mut Database, index: &Vec<usize>) {
 | 
			
		||||
        let mut actions = vec![];
 | 
			
		||||
        db.queue.set_index_inner(index, 0, vec![], &mut actions);
 | 
			
		||||
        Self::handle_actions(db, actions);
 | 
			
		||||
    }
 | 
			
		||||
    pub fn set_index_inner(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        index: &Vec<usize>,
 | 
			
		||||
        depth: usize,
 | 
			
		||||
        mut build_index: Vec<usize>,
 | 
			
		||||
        actions: &mut Vec<QueueAction>,
 | 
			
		||||
    ) {
 | 
			
		||||
        let i = if let Some(i) = index.get(depth) {
 | 
			
		||||
            *i
 | 
			
		||||
        } else {
 | 
			
		||||
            return;
 | 
			
		||||
        };
 | 
			
		||||
        build_index.push(i);
 | 
			
		||||
        match &mut self.content {
 | 
			
		||||
            QueueContent::Song(_) => {}
 | 
			
		||||
            QueueContent::Folder(idx, contents, _) => {
 | 
			
		||||
                if i != *idx {
 | 
			
		||||
                    *idx = i;
 | 
			
		||||
                for (i2, c) in contents.iter_mut().enumerate() {
 | 
			
		||||
                    if i2 != i {
 | 
			
		||||
                        c.set_index(&vec![], 0)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(c) = contents.get_mut(i) {
 | 
			
		||||
                    c.set_index(index, depth + 1);
 | 
			
		||||
                    c.init(build_index.clone(), actions);
 | 
			
		||||
                    c.set_index_inner(index, depth + 1, build_index, actions);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Loop(_, _, inner) => {
 | 
			
		||||
                inner.init(build_index.clone(), actions);
 | 
			
		||||
                inner.set_index_inner(index, depth + 1, build_index, actions)
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Random(_) => {}
 | 
			
		||||
            QueueContent::Shuffle(current, map, elems, next) => {
 | 
			
		||||
                if i != *current {
 | 
			
		||||
                    *current = i;
 | 
			
		||||
                }
 | 
			
		||||
                if let Some(c) = map.get(i).and_then(|i| elems.get_mut(*i)) {
 | 
			
		||||
                    c.init(build_index.clone(), actions);
 | 
			
		||||
                    c.set_index_inner(index, depth + 1, build_index, actions);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -164,6 +400,12 @@ impl Queue {
 | 
			
		||||
                        None
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                QueueContent::Loop(_, _, inner) => inner.get_item_at_index(index, depth + 1),
 | 
			
		||||
                QueueContent::Random(vec) => vec.get(*i)?.get_item_at_index(index, depth + 1),
 | 
			
		||||
                QueueContent::Shuffle(_, map, elems, _) => map
 | 
			
		||||
                    .get(*i)
 | 
			
		||||
                    .and_then(|i| elems.get(*i))
 | 
			
		||||
                    .and_then(|elem| elem.get_item_at_index(index, depth + 1)),
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Some(self)
 | 
			
		||||
@ -180,6 +422,14 @@ impl Queue {
 | 
			
		||||
                        None
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                QueueContent::Loop(_, _, inner) => inner.get_item_at_index_mut(index, depth + 1),
 | 
			
		||||
                QueueContent::Random(vec) => {
 | 
			
		||||
                    vec.get_mut(*i)?.get_item_at_index_mut(index, depth + 1)
 | 
			
		||||
                }
 | 
			
		||||
                QueueContent::Shuffle(_, map, elems, _) => map
 | 
			
		||||
                    .get(*i)
 | 
			
		||||
                    .and_then(|i| elems.get_mut(*i))
 | 
			
		||||
                    .and_then(|elem| elem.get_item_at_index_mut(index, depth + 1)),
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Some(self)
 | 
			
		||||
@ -210,6 +460,32 @@ impl Queue {
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                QueueContent::Loop(_, _, inner) => {
 | 
			
		||||
                    if depth + 1 < index.len() {
 | 
			
		||||
                        inner.remove_by_index(index, depth + 1)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        None
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                QueueContent::Random(v) => v.remove(*i),
 | 
			
		||||
                QueueContent::Shuffle(current, map, elems, next) => {
 | 
			
		||||
                    if *i < *current {
 | 
			
		||||
                        *current -= 1;
 | 
			
		||||
                    }
 | 
			
		||||
                    if *i < *next {
 | 
			
		||||
                        *next -= 1;
 | 
			
		||||
                    }
 | 
			
		||||
                    if *i < map.len() {
 | 
			
		||||
                        let elem = map.remove(*i);
 | 
			
		||||
                        if elem < elems.len() {
 | 
			
		||||
                            Some(elems.remove(elem))
 | 
			
		||||
                        } else {
 | 
			
		||||
                            None
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        None
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            None
 | 
			
		||||
@ -264,6 +540,23 @@ impl ToFromBytes for QueueContent {
 | 
			
		||||
                contents.to_bytes(s)?;
 | 
			
		||||
                name.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::Loop(total, current, inner) => {
 | 
			
		||||
                s.write_all(&[0b11000000])?;
 | 
			
		||||
                total.to_bytes(s)?;
 | 
			
		||||
                current.to_bytes(s)?;
 | 
			
		||||
                inner.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::Random(q) => {
 | 
			
		||||
                s.write_all(&[0b00110000])?;
 | 
			
		||||
                q.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::Shuffle(current, map, elems, next) => {
 | 
			
		||||
                s.write_all(&[0b00001100])?;
 | 
			
		||||
                current.to_bytes(s)?;
 | 
			
		||||
                map.to_bytes(s)?;
 | 
			
		||||
                elems.to_bytes(s)?;
 | 
			
		||||
                next.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
@ -273,14 +566,26 @@ impl ToFromBytes for QueueContent {
 | 
			
		||||
    {
 | 
			
		||||
        let mut switch_on = [0];
 | 
			
		||||
        s.read_exact(&mut switch_on)?;
 | 
			
		||||
        Ok(if switch_on[0].count_ones() > 4 {
 | 
			
		||||
            Self::Song(ToFromBytes::from_bytes(s)?)
 | 
			
		||||
        } else {
 | 
			
		||||
            Self::Folder(
 | 
			
		||||
        Ok(match switch_on[0] {
 | 
			
		||||
            0b11111111 => Self::Song(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b00000000 => Self::Folder(
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
            )
 | 
			
		||||
            ),
 | 
			
		||||
            0b11000000 => Self::Loop(
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                Box::new(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            ),
 | 
			
		||||
            0b00110000 => Self::Random(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b00001100 => Self::Shuffle(
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
            ),
 | 
			
		||||
            _ => Self::Folder(0, vec![], "<invalid byte received>".to_string()),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
use std::{
 | 
			
		||||
    collections::HashMap,
 | 
			
		||||
    collections::{HashMap, VecDeque},
 | 
			
		||||
    io::{Read, Write},
 | 
			
		||||
    path::PathBuf,
 | 
			
		||||
};
 | 
			
		||||
@ -81,6 +81,32 @@ where
 | 
			
		||||
        Ok(buf)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl<C> ToFromBytes for VecDeque<C>
 | 
			
		||||
where
 | 
			
		||||
    C: ToFromBytes,
 | 
			
		||||
{
 | 
			
		||||
    fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
 | 
			
		||||
    where
 | 
			
		||||
        T: Write,
 | 
			
		||||
    {
 | 
			
		||||
        self.len().to_bytes(s)?;
 | 
			
		||||
        for elem in self {
 | 
			
		||||
            elem.to_bytes(s)?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
 | 
			
		||||
    where
 | 
			
		||||
        T: Read,
 | 
			
		||||
    {
 | 
			
		||||
        let len = ToFromBytes::from_bytes(s)?;
 | 
			
		||||
        let mut buf = VecDeque::with_capacity(len);
 | 
			
		||||
        for _ in 0..len {
 | 
			
		||||
            buf.push_back(ToFromBytes::from_bytes(s)?);
 | 
			
		||||
        }
 | 
			
		||||
        Ok(buf)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
impl<A> ToFromBytes for Option<A>
 | 
			
		||||
where
 | 
			
		||||
    A: ToFromBytes,
 | 
			
		||||
 | 
			
		||||
@ -67,8 +67,12 @@ impl Player {
 | 
			
		||||
        if let Some((source, _notif)) = &mut self.source {
 | 
			
		||||
            source.set_paused(true);
 | 
			
		||||
        }
 | 
			
		||||
        if let SongOpt::Some(id) | SongOpt::New(Some(id)) = self.current_song_id {
 | 
			
		||||
            self.current_song_id = SongOpt::New(Some(id));
 | 
			
		||||
        } else {
 | 
			
		||||
            self.current_song_id = SongOpt::New(None);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    pub fn update(&mut self, db: &mut Database) {
 | 
			
		||||
        if db.playing && self.source.is_none() {
 | 
			
		||||
            if let Some(song) = db.queue.get_current_song() {
 | 
			
		||||
@ -76,7 +80,10 @@ impl Player {
 | 
			
		||||
                self.current_song_id = SongOpt::New(Some(*song));
 | 
			
		||||
            } else {
 | 
			
		||||
                // db.playing, but no song in queue...
 | 
			
		||||
                db.apply_command(Command::Stop);
 | 
			
		||||
            }
 | 
			
		||||
        } else if !db.playing && self.source.is_some() {
 | 
			
		||||
            self.current_song_id = SongOpt::New(None);
 | 
			
		||||
        } else if let Some((_source, notif)) = &mut self.source {
 | 
			
		||||
            if let Ok(()) = notif.try_recv() {
 | 
			
		||||
                // song has finished playing
 | 
			
		||||
@ -103,35 +110,36 @@ impl Player {
 | 
			
		||||
        // new current song
 | 
			
		||||
        if let SongOpt::New(song_opt) = &self.current_song_id {
 | 
			
		||||
            // stop playback
 | 
			
		||||
            eprintln!("[play] stopping playback");
 | 
			
		||||
            // eprintln!("[play] stopping playback");
 | 
			
		||||
            self.manager.clear();
 | 
			
		||||
            if let Some(song_id) = song_opt {
 | 
			
		||||
                if db.playing {
 | 
			
		||||
                // start playback again
 | 
			
		||||
                if let Some(song) = db.get_song(song_id) {
 | 
			
		||||
                        eprintln!("[play] starting playback...");
 | 
			
		||||
                    // eprintln!("[play] starting playback...");
 | 
			
		||||
                    // add our song
 | 
			
		||||
                    let ext = match &song.location.rel_path.extension() {
 | 
			
		||||
                        Some(s) => s.to_str().unwrap_or(""),
 | 
			
		||||
                        None => "",
 | 
			
		||||
                    };
 | 
			
		||||
                        let (sound, notif) = Self::sound_from_bytes(
 | 
			
		||||
                            ext,
 | 
			
		||||
                            song.cached_data_now(db).expect("no cached data"),
 | 
			
		||||
                        )
 | 
			
		||||
                        .unwrap()
 | 
			
		||||
                        .pausable()
 | 
			
		||||
                        .with_async_completion_notifier();
 | 
			
		||||
                    if let Some(bytes) = song.cached_data_now(db) {
 | 
			
		||||
                        match Self::sound_from_bytes(ext, bytes) {
 | 
			
		||||
                            Ok(v) => {
 | 
			
		||||
                                let (sound, notif) = v.pausable().with_async_completion_notifier();
 | 
			
		||||
                                // add it
 | 
			
		||||
                                let (sound, controller) = sound.controllable();
 | 
			
		||||
                                self.source = Some((controller, notif));
 | 
			
		||||
                                // and play it
 | 
			
		||||
                                self.manager.play(Box::new(sound));
 | 
			
		||||
                        eprintln!("[play] started playback");
 | 
			
		||||
                            }
 | 
			
		||||
                            Err(e) => {
 | 
			
		||||
                                eprintln!("[player] Can't play, skipping! {e}");
 | 
			
		||||
                                db.apply_command(Command::NextSong);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    panic!("invalid song ID: current_song_id not found in DB!");
 | 
			
		||||
                }
 | 
			
		||||
                }
 | 
			
		||||
                self.current_song_id = SongOpt::Some(*song_id);
 | 
			
		||||
            } else {
 | 
			
		||||
                self.current_song_id = SongOpt::None;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
pub mod get;
 | 
			
		||||
 | 
			
		||||
use std::{
 | 
			
		||||
    eprintln,
 | 
			
		||||
    io::Write,
 | 
			
		||||
    io::{BufRead, BufReader, Read, Write},
 | 
			
		||||
    net::{SocketAddr, TcpListener},
 | 
			
		||||
    path::PathBuf,
 | 
			
		||||
    sync::{mpsc, Arc, Mutex},
 | 
			
		||||
@ -12,12 +14,13 @@ use crate::{
 | 
			
		||||
    data::{
 | 
			
		||||
        album::Album,
 | 
			
		||||
        artist::Artist,
 | 
			
		||||
        database::{Database, UpdateEndpoint},
 | 
			
		||||
        database::{Cover, Database, UpdateEndpoint},
 | 
			
		||||
        queue::Queue,
 | 
			
		||||
        song::Song,
 | 
			
		||||
    },
 | 
			
		||||
    load::ToFromBytes,
 | 
			
		||||
    player::Player,
 | 
			
		||||
    server::get::handle_one_connection_as_get,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, Debug)]
 | 
			
		||||
@ -33,12 +36,14 @@ pub enum Command {
 | 
			
		||||
    QueueInsert(Vec<usize>, usize, Queue),
 | 
			
		||||
    QueueRemove(Vec<usize>),
 | 
			
		||||
    QueueGoto(Vec<usize>),
 | 
			
		||||
    QueueSetShuffle(Vec<usize>, Vec<usize>, usize),
 | 
			
		||||
    /// .id field is ignored!
 | 
			
		||||
    AddSong(Song),
 | 
			
		||||
    /// .id field is ignored!
 | 
			
		||||
    AddAlbum(Album),
 | 
			
		||||
    /// .id field is ignored!
 | 
			
		||||
    AddArtist(Artist),
 | 
			
		||||
    AddCover(Cover),
 | 
			
		||||
    ModifySong(Song),
 | 
			
		||||
    ModifyAlbum(Album),
 | 
			
		||||
    ModifyArtist(Artist),
 | 
			
		||||
@ -93,33 +98,41 @@ pub fn run_server(
 | 
			
		||||
            Ok(v) => {
 | 
			
		||||
                let command_sender = command_sender.clone();
 | 
			
		||||
                let db = Arc::clone(&database);
 | 
			
		||||
                // each connection gets its own thread, but they will be idle most of the time (waiting for data on the tcp stream)
 | 
			
		||||
                thread::spawn(move || loop {
 | 
			
		||||
                    if let Ok((mut connection, con_addr)) = v.accept() {
 | 
			
		||||
                        eprintln!("[info] TCP connection accepted from {con_addr}.");
 | 
			
		||||
                    if let Ok((connection, con_addr)) = v.accept() {
 | 
			
		||||
                        let command_sender = command_sender.clone();
 | 
			
		||||
                        let db = Arc::clone(&db);
 | 
			
		||||
                        thread::spawn(move || {
 | 
			
		||||
                            // sync database
 | 
			
		||||
                            let mut db = db.lock().unwrap();
 | 
			
		||||
                            db.init_connection(&mut connection)?;
 | 
			
		||||
                            // keep the client in sync:
 | 
			
		||||
                            // the db will send all updates to the client once it is added to update_endpoints
 | 
			
		||||
                            db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new(
 | 
			
		||||
                                // try_clone is used here to split a TcpStream into Writer and Reader
 | 
			
		||||
                                connection.try_clone().unwrap(),
 | 
			
		||||
                            )));
 | 
			
		||||
                            // drop the mutex lock
 | 
			
		||||
                            drop(db);
 | 
			
		||||
                            // read updates from the tcp stream and send them to the database, exit on EOF or Err
 | 
			
		||||
                            loop {
 | 
			
		||||
                                if let Ok(command) = Command::from_bytes(&mut connection) {
 | 
			
		||||
                                    command_sender.send(command).unwrap();
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    break;
 | 
			
		||||
                            eprintln!("[info] TCP connection accepted from {con_addr}.");
 | 
			
		||||
                            // each connection first has to send one line to tell us what it wants
 | 
			
		||||
                            let mut connection = BufReader::new(connection);
 | 
			
		||||
                            let mut line = String::new();
 | 
			
		||||
                            if connection.read_line(&mut line).is_ok() {
 | 
			
		||||
                                // based on that line, we adjust behavior
 | 
			
		||||
                                match line.as_str().trim() {
 | 
			
		||||
                                    // sends all updates to this connection and reads commands from it
 | 
			
		||||
                                    "main" => {
 | 
			
		||||
                                        let connection = connection.into_inner();
 | 
			
		||||
                                        _ = handle_one_connection_as_main(
 | 
			
		||||
                                            db,
 | 
			
		||||
                                            &mut connection.try_clone().unwrap(),
 | 
			
		||||
                                            connection,
 | 
			
		||||
                                            &command_sender,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }
 | 
			
		||||
                                    // reads commands from the connection, but (unlike main) doesn't send any updates
 | 
			
		||||
                                    "control" => handle_one_connection_as_control(
 | 
			
		||||
                                        &mut connection,
 | 
			
		||||
                                        &command_sender,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    "get" => _ = handle_one_connection_as_get(db, &mut connection),
 | 
			
		||||
                                    _ => {
 | 
			
		||||
                                        _ = connection
 | 
			
		||||
                                            .into_inner()
 | 
			
		||||
                                            .shutdown(std::net::Shutdown::Both)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            Ok::<(), std::io::Error>(())
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
@ -141,24 +154,39 @@ pub fn run_server(
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait Connection: Sized + Send + 'static {
 | 
			
		||||
    type SendError: Send;
 | 
			
		||||
    fn send_command(&mut self, command: Command) -> Result<(), Self::SendError>;
 | 
			
		||||
    fn receive_updates(&mut self) -> Result<Vec<Command>, Self::SendError>;
 | 
			
		||||
    fn receive_update_blocking(&mut self) -> Result<Command, Self::SendError>;
 | 
			
		||||
    fn move_to_thread<F: FnMut(&mut Self, Command) -> bool + Send + 'static>(
 | 
			
		||||
        mut self,
 | 
			
		||||
        mut handler: F,
 | 
			
		||||
    ) -> JoinHandle<Result<Self, Self::SendError>> {
 | 
			
		||||
        std::thread::spawn(move || loop {
 | 
			
		||||
            let update = self.receive_update_blocking()?;
 | 
			
		||||
            if handler(&mut self, update) {
 | 
			
		||||
                return Ok(self);
 | 
			
		||||
pub fn handle_one_connection_as_main(
 | 
			
		||||
    db: Arc<Mutex<Database>>,
 | 
			
		||||
    connection: &mut impl Read,
 | 
			
		||||
    mut send_to: (impl Write + Sync + Send + 'static),
 | 
			
		||||
    command_sender: &mpsc::Sender<Command>,
 | 
			
		||||
) -> Result<(), std::io::Error> {
 | 
			
		||||
    // sync database
 | 
			
		||||
    let mut db = db.lock().unwrap();
 | 
			
		||||
    db.init_connection(&mut send_to)?;
 | 
			
		||||
    // keep the client in sync:
 | 
			
		||||
    // the db will send all updates to the client once it is added to update_endpoints
 | 
			
		||||
    db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new(
 | 
			
		||||
        // try_clone is used here to split a TcpStream into Writer and Reader
 | 
			
		||||
        send_to,
 | 
			
		||||
    )));
 | 
			
		||||
    // drop the mutex lock
 | 
			
		||||
    drop(db);
 | 
			
		||||
    handle_one_connection_as_control(connection, command_sender);
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
pub fn handle_one_connection_as_control(
 | 
			
		||||
    connection: &mut impl Read,
 | 
			
		||||
    command_sender: &mpsc::Sender<Command>,
 | 
			
		||||
) {
 | 
			
		||||
    // read updates from the tcp stream and send them to the database, exit on EOF or Err
 | 
			
		||||
    loop {
 | 
			
		||||
        if let Ok(command) = Command::from_bytes(connection) {
 | 
			
		||||
            command_sender.send(command).unwrap();
 | 
			
		||||
        } else {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ToFromBytes for Command {
 | 
			
		||||
    fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
 | 
			
		||||
    where
 | 
			
		||||
@ -200,6 +228,12 @@ impl ToFromBytes for Command {
 | 
			
		||||
                s.write_all(&[0b00011011])?;
 | 
			
		||||
                index.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::QueueSetShuffle(path, map, next) => {
 | 
			
		||||
                s.write_all(&[0b10011011])?;
 | 
			
		||||
                path.to_bytes(s)?;
 | 
			
		||||
                map.to_bytes(s)?;
 | 
			
		||||
                next.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::AddSong(song) => {
 | 
			
		||||
                s.write_all(&[0b01010000])?;
 | 
			
		||||
                song.to_bytes(s)?;
 | 
			
		||||
@ -212,6 +246,10 @@ impl ToFromBytes for Command {
 | 
			
		||||
                s.write_all(&[0b01011100])?;
 | 
			
		||||
                artist.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::AddCover(cover) => {
 | 
			
		||||
                s.write_all(&[0b01011101])?;
 | 
			
		||||
                cover.to_bytes(s)?;
 | 
			
		||||
            }
 | 
			
		||||
            Self::ModifySong(song) => {
 | 
			
		||||
                s.write_all(&[0b10010000])?;
 | 
			
		||||
                song.to_bytes(s)?;
 | 
			
		||||
@ -259,12 +297,18 @@ impl ToFromBytes for Command {
 | 
			
		||||
            ),
 | 
			
		||||
            0b00011001 => Self::QueueRemove(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b00011011 => Self::QueueGoto(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b10011011 => Self::QueueSetShuffle(
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
                ToFromBytes::from_bytes(s)?,
 | 
			
		||||
            ),
 | 
			
		||||
            0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b10010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b10010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b10011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
 | 
			
		||||
            _ => {
 | 
			
		||||
                eprintln!("unexpected byte when reading command; stopping playback.");
 | 
			
		||||
 | 
			
		||||
@ -139,8 +139,8 @@ Error getting information about the provided path '{path_s}': {e}"
 | 
			
		||||
    } else {
 | 
			
		||||
        eprintln!(
 | 
			
		||||
            "[EXIT]
 | 
			
		||||
musicdb - help
 | 
			
		||||
musicdb <path to database file> <options> <options> <...>
 | 
			
		||||
musicdb-server - help
 | 
			
		||||
musicdb-server <path to database file> <options> <options> <...>
 | 
			
		||||
options:
 | 
			
		||||
  --init <lib directory>
 | 
			
		||||
  --tcp <addr:port>
 | 
			
		||||
 | 
			
		||||
@ -78,6 +78,22 @@ pub struct AppHtml {
 | 
			
		||||
    queue_folder: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, content, name
 | 
			
		||||
    queue_folder_current: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, total, current, inner
 | 
			
		||||
    queue_loop: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, total, current, inner
 | 
			
		||||
    queue_loop_current: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, current, inner
 | 
			
		||||
    queue_loopinf: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, current, inner
 | 
			
		||||
    queue_loopinf_current: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, content
 | 
			
		||||
    queue_random: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, content
 | 
			
		||||
    queue_random_current: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, content
 | 
			
		||||
    queue_shuffle: Vec<HtmlPart>,
 | 
			
		||||
    /// can use: path, content
 | 
			
		||||
    queue_shuffle_current: Vec<HtmlPart>,
 | 
			
		||||
}
 | 
			
		||||
impl AppHtml {
 | 
			
		||||
    pub fn from_dir<P: AsRef<std::path::Path>>(dir: P) -> std::io::Result<Self> {
 | 
			
		||||
@ -99,6 +115,22 @@ impl AppHtml {
 | 
			
		||||
            queue_folder_current: Self::parse(&std::fs::read_to_string(
 | 
			
		||||
                dir.join("queue_folder_current.html"),
 | 
			
		||||
            )?),
 | 
			
		||||
            queue_loop: Self::parse(&std::fs::read_to_string(dir.join("queue_loop.html"))?),
 | 
			
		||||
            queue_loop_current: Self::parse(&std::fs::read_to_string(
 | 
			
		||||
                dir.join("queue_loop_current.html"),
 | 
			
		||||
            )?),
 | 
			
		||||
            queue_loopinf: Self::parse(&std::fs::read_to_string(dir.join("queue_loopinf.html"))?),
 | 
			
		||||
            queue_loopinf_current: Self::parse(&std::fs::read_to_string(
 | 
			
		||||
                dir.join("queue_loopinf_current.html"),
 | 
			
		||||
            )?),
 | 
			
		||||
            queue_random: Self::parse(&std::fs::read_to_string(dir.join("queue_random.html"))?),
 | 
			
		||||
            queue_random_current: Self::parse(&std::fs::read_to_string(
 | 
			
		||||
                dir.join("queue_random_current.html"),
 | 
			
		||||
            )?),
 | 
			
		||||
            queue_shuffle: Self::parse(&std::fs::read_to_string(dir.join("queue_shuffle.html"))?),
 | 
			
		||||
            queue_shuffle_current: Self::parse(&std::fs::read_to_string(
 | 
			
		||||
                dir.join("queue_shuffle_current.html"),
 | 
			
		||||
            )?),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    pub fn parse(s: &str) -> Vec<HtmlPart> {
 | 
			
		||||
@ -317,7 +349,8 @@ async fn sse_handler(
 | 
			
		||||
                | Command::ModifyArtist(..)
 | 
			
		||||
                | Command::AddSong(..)
 | 
			
		||||
                | Command::AddAlbum(..)
 | 
			
		||||
                | Command::AddArtist(..) => Event::default().event("artists").data({
 | 
			
		||||
                | Command::AddArtist(..)
 | 
			
		||||
                | Command::AddCover(..) => 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);
 | 
			
		||||
@ -352,7 +385,8 @@ async fn sse_handler(
 | 
			
		||||
                | Command::QueueAdd(..)
 | 
			
		||||
                | Command::QueueInsert(..)
 | 
			
		||||
                | Command::QueueRemove(..)
 | 
			
		||||
                | Command::QueueGoto(..) => {
 | 
			
		||||
                | Command::QueueGoto(..)
 | 
			
		||||
                | Command::QueueSetShuffle(..) => {
 | 
			
		||||
                    let db = state.db.lock().unwrap();
 | 
			
		||||
                    let current = db
 | 
			
		||||
                        .queue
 | 
			
		||||
@ -370,6 +404,7 @@ async fn sse_handler(
 | 
			
		||||
                        &db.queue,
 | 
			
		||||
                        String::new(),
 | 
			
		||||
                        true,
 | 
			
		||||
                        false,
 | 
			
		||||
                    );
 | 
			
		||||
                    Event::default().event("queue").data(
 | 
			
		||||
                        state
 | 
			
		||||
@ -510,6 +545,7 @@ fn build_queue_content_build(
 | 
			
		||||
    queue: &Queue,
 | 
			
		||||
    path: String,
 | 
			
		||||
    current: bool,
 | 
			
		||||
    skip_folder: bool,
 | 
			
		||||
) {
 | 
			
		||||
    // TODO: Do something for disabled ones too (they shouldn't just be hidden)
 | 
			
		||||
    if queue.enabled() {
 | 
			
		||||
@ -533,10 +569,10 @@ fn build_queue_content_build(
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Folder(ci, c, name) => {
 | 
			
		||||
                if path.is_empty() {
 | 
			
		||||
                if skip_folder || path.is_empty() {
 | 
			
		||||
                    for (i, c) in c.iter().enumerate() {
 | 
			
		||||
                        let current = current && *ci == i;
 | 
			
		||||
                        build_queue_content_build(db, state, html, c, i.to_string(), current)
 | 
			
		||||
                        build_queue_content_build(db, state, html, c, i.to_string(), current, false)
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    for v in if current {
 | 
			
		||||
@ -559,6 +595,92 @@ fn build_queue_content_build(
 | 
			
		||||
                                            c,
 | 
			
		||||
                                            format!("{path}-{i}"),
 | 
			
		||||
                                            current,
 | 
			
		||||
                                            false,
 | 
			
		||||
                                        )
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                                _ => {}
 | 
			
		||||
                            },
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Loop(total, cur, inner) => {
 | 
			
		||||
                for v in match (*total, current) {
 | 
			
		||||
                    (0, false) => &state.html.queue_loopinf,
 | 
			
		||||
                    (0, true) => &state.html.queue_loopinf_current,
 | 
			
		||||
                    (_, false) => &state.html.queue_loop,
 | 
			
		||||
                    (_, true) => &state.html.queue_loop_current,
 | 
			
		||||
                } {
 | 
			
		||||
                    match v {
 | 
			
		||||
                        HtmlPart::Plain(v) => html.push_str(v),
 | 
			
		||||
                        HtmlPart::Insert(key) => match key.as_str() {
 | 
			
		||||
                            "path" => html.push_str(&path),
 | 
			
		||||
                            "total" => html.push_str(&format!("{total}")),
 | 
			
		||||
                            "current" => html.push_str(&format!("{cur}")),
 | 
			
		||||
                            "inner" => build_queue_content_build(
 | 
			
		||||
                                db,
 | 
			
		||||
                                state,
 | 
			
		||||
                                html,
 | 
			
		||||
                                &inner,
 | 
			
		||||
                                format!("{path}-0"),
 | 
			
		||||
                                current,
 | 
			
		||||
                                true,
 | 
			
		||||
                            ),
 | 
			
		||||
                            _ => {}
 | 
			
		||||
                        },
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Random(q) => {
 | 
			
		||||
                for v in if current {
 | 
			
		||||
                    &state.html.queue_random_current
 | 
			
		||||
                } else {
 | 
			
		||||
                    &state.html.queue_random
 | 
			
		||||
                } {
 | 
			
		||||
                    match v {
 | 
			
		||||
                        HtmlPart::Plain(v) => html.push_str(v),
 | 
			
		||||
                        HtmlPart::Insert(key) => match key.as_str() {
 | 
			
		||||
                            "path" => html.push_str(&path),
 | 
			
		||||
                            "content" => {
 | 
			
		||||
                                for (i, v) in q.iter().enumerate() {
 | 
			
		||||
                                    build_queue_content_build(
 | 
			
		||||
                                        db,
 | 
			
		||||
                                        state,
 | 
			
		||||
                                        html,
 | 
			
		||||
                                        &v,
 | 
			
		||||
                                        format!("{path}-0"),
 | 
			
		||||
                                        current && i == q.len().saturating_sub(2),
 | 
			
		||||
                                        true,
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            _ => {}
 | 
			
		||||
                        },
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            QueueContent::Shuffle(cur, map, content, _) => {
 | 
			
		||||
                for v in if current {
 | 
			
		||||
                    &state.html.queue_shuffle_current
 | 
			
		||||
                } else {
 | 
			
		||||
                    &state.html.queue_shuffle
 | 
			
		||||
                } {
 | 
			
		||||
                    match v {
 | 
			
		||||
                        HtmlPart::Plain(v) => html.push_str(v),
 | 
			
		||||
                        HtmlPart::Insert(key) => match key.as_str() {
 | 
			
		||||
                            "path" => html.push_str(&path),
 | 
			
		||||
                            "content" => {
 | 
			
		||||
                                for (i, v) in map.iter().filter_map(|i| content.get(*i)).enumerate()
 | 
			
		||||
                                {
 | 
			
		||||
                                    build_queue_content_build(
 | 
			
		||||
                                        db,
 | 
			
		||||
                                        state,
 | 
			
		||||
                                        html,
 | 
			
		||||
                                        &v,
 | 
			
		||||
                                        format!("{path}-0"),
 | 
			
		||||
                                        current && i == *cur,
 | 
			
		||||
                                        true,
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
@ -569,5 +691,4 @@ fn build_queue_content_build(
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user