mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 14:13:53 +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" }
|
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
||||||
regex = "1.9.3"
|
regex = "1.9.3"
|
||||||
speedy2d = { version = "1.12.0", optional = true }
|
speedy2d = { version = "1.12.0", optional = true }
|
||||||
|
toml = "0.7.6"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["speedy2d"]
|
default = ["speedy2d"]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
eprintln,
|
eprintln,
|
||||||
|
io::{Read, Write},
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
@ -10,7 +11,7 @@ use std::{
|
|||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId},
|
data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId},
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
server::Command,
|
server::{get, Command},
|
||||||
};
|
};
|
||||||
use speedy2d::{
|
use speedy2d::{
|
||||||
color::Color,
|
color::Color,
|
||||||
@ -33,11 +34,74 @@ pub enum GuiEvent {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main(
|
pub fn main<T: Write + Read + 'static + Sync + Send>(
|
||||||
database: Arc<Mutex<Database>>,
|
database: Arc<Mutex<Database>>,
|
||||||
connection: TcpStream,
|
connection: TcpStream,
|
||||||
|
get_con: get::Client<T>,
|
||||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
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(
|
let window = speedy2d::Window::<GuiEvent>::new_with_user_events(
|
||||||
"MusicDB Client",
|
"MusicDB Client",
|
||||||
WindowCreationOptions::new_fullscreen_borderless(),
|
WindowCreationOptions::new_fullscreen_borderless(),
|
||||||
@ -45,7 +109,18 @@ pub fn main(
|
|||||||
.expect("couldn't open window");
|
.expect("couldn't open window");
|
||||||
*event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender());
|
*event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender());
|
||||||
let sender = 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 {
|
pub struct Gui {
|
||||||
@ -69,11 +144,17 @@ pub struct Gui {
|
|||||||
pub scroll_pages_multiplier: f64,
|
pub scroll_pages_multiplier: f64,
|
||||||
}
|
}
|
||||||
impl Gui {
|
impl Gui {
|
||||||
fn new(
|
fn new<T: Read + Write + 'static + Sync + Send>(
|
||||||
|
font: Font,
|
||||||
database: Arc<Mutex<Database>>,
|
database: Arc<Mutex<Database>>,
|
||||||
connection: TcpStream,
|
connection: TcpStream,
|
||||||
|
get_con: get::Client<T>,
|
||||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
||||||
event_sender: UserEventSender<GuiEvent>,
|
event_sender: UserEventSender<GuiEvent>,
|
||||||
|
line_height: f32,
|
||||||
|
scroll_pixels_multiplier: f64,
|
||||||
|
scroll_lines_multiplier: f64,
|
||||||
|
scroll_pages_multiplier: f64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
database.lock().unwrap().update_endpoints.push(
|
database.lock().unwrap().update_endpoints.push(
|
||||||
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
|
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
|
||||||
@ -87,7 +168,8 @@ impl Gui {
|
|||||||
| Command::QueueAdd(..)
|
| Command::QueueAdd(..)
|
||||||
| Command::QueueInsert(..)
|
| Command::QueueInsert(..)
|
||||||
| Command::QueueRemove(..)
|
| Command::QueueRemove(..)
|
||||||
| Command::QueueGoto(..) => {
|
| Command::QueueGoto(..)
|
||||||
|
| Command::QueueSetShuffle(..) => {
|
||||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||||
_ = s.send_event(GuiEvent::UpdatedQueue);
|
_ = s.send_event(GuiEvent::UpdatedQueue);
|
||||||
}
|
}
|
||||||
@ -96,6 +178,7 @@ impl Gui {
|
|||||||
| Command::AddSong(_)
|
| Command::AddSong(_)
|
||||||
| Command::AddAlbum(_)
|
| Command::AddAlbum(_)
|
||||||
| Command::AddArtist(_)
|
| Command::AddArtist(_)
|
||||||
|
| Command::AddCover(_)
|
||||||
| Command::ModifySong(_)
|
| Command::ModifySong(_)
|
||||||
| Command::ModifyAlbum(_)
|
| Command::ModifyAlbum(_)
|
||||||
| Command::ModifyArtist(_) => {
|
| 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 {
|
Gui {
|
||||||
event_sender,
|
event_sender,
|
||||||
database,
|
database,
|
||||||
@ -117,6 +196,7 @@ impl Gui {
|
|||||||
VirtualKeyCode::Escape,
|
VirtualKeyCode::Escape,
|
||||||
GuiScreen::new(
|
GuiScreen::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
|
get_con,
|
||||||
line_height,
|
line_height,
|
||||||
scroll_pixels_multiplier,
|
scroll_pixels_multiplier,
|
||||||
scroll_lines_multiplier,
|
scroll_lines_multiplier,
|
||||||
@ -125,10 +205,7 @@ impl Gui {
|
|||||||
)),
|
)),
|
||||||
size: UVec2::ZERO,
|
size: UVec2::ZERO,
|
||||||
mouse_pos: Vec2::ZERO,
|
mouse_pos: Vec2::ZERO,
|
||||||
font: Font::new(include_bytes!(
|
font,
|
||||||
"/usr/share/fonts/mozilla-fira/FiraSans-Regular.otf"
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
|
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
|
||||||
last_draw: Instant::now(),
|
last_draw: Instant::now(),
|
||||||
modifiers: ModifiersState::default(),
|
modifiers: ModifiersState::default(),
|
||||||
@ -328,6 +405,10 @@ pub struct DrawInfo<'a> {
|
|||||||
pub child_has_keyboard_focus: bool,
|
pub child_has_keyboard_focus: bool,
|
||||||
/// the height of one line of text (in pixels)
|
/// the height of one line of text (in pixels)
|
||||||
pub line_height: f32,
|
pub line_height: f32,
|
||||||
|
pub dragging: Option<(
|
||||||
|
Dragging,
|
||||||
|
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
|
||||||
|
)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic wrapper over anything that implements GuiElemTrait
|
/// Generic wrapper over anything that implements GuiElemTrait
|
||||||
@ -679,9 +760,11 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
has_keyboard_focus: false,
|
has_keyboard_focus: false,
|
||||||
child_has_keyboard_focus: true,
|
child_has_keyboard_focus: true,
|
||||||
line_height: self.line_height,
|
line_height: self.line_height,
|
||||||
|
dragging: self.dragging.take(),
|
||||||
};
|
};
|
||||||
self.gui.draw(&mut info, graphics);
|
self.gui.draw(&mut info, graphics);
|
||||||
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
|
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((d, f)) = &mut self.dragging {
|
||||||
if let Some(f) = f {
|
if let Some(f) = f {
|
||||||
f(&mut info, graphics);
|
f(&mut info, graphics);
|
||||||
@ -753,12 +836,15 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
distance: speedy2d::window::MouseScrollDistance,
|
distance: speedy2d::window::MouseScrollDistance,
|
||||||
) {
|
) {
|
||||||
let dist = match distance {
|
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, .. } => {
|
MouseScrollDistance::Lines { y, .. } => {
|
||||||
(self.scroll_lines_multiplier * y) as f32 * self.line_height
|
(self.scroll_lines_multiplier * y) as f32 * self.line_height
|
||||||
}
|
}
|
||||||
MouseScrollDistance::Pages { y, .. } => {
|
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()) {
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct ScrollBox {
|
pub struct ScrollBox {
|
||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
|
@ -189,6 +189,28 @@ impl LibraryBrowser {
|
|||||||
)),
|
)),
|
||||||
artist_height,
|
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 {
|
for album_id in &artist.albums {
|
||||||
if let Some(album) = db.albums().get(album_id) {
|
if let Some(album) = db.albums().get(album_id) {
|
||||||
if self.search_album.is_empty()
|
if self.search_album.is_empty()
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
use musicdb_lib::{
|
use std::{
|
||||||
data::{queue::QueueContent, SongId},
|
io::{Cursor, Read, Write},
|
||||||
server::Command,
|
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::{
|
use crate::{
|
||||||
gui::{adjust_area, adjust_pos, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
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<T: Read + Write> {
|
||||||
pub struct CurrentSong {
|
|
||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
children: Vec<GuiElem>,
|
children: Vec<GuiElem>,
|
||||||
|
get_con: Option<get::Client<T>>,
|
||||||
prev_song: Option<SongId>,
|
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 {
|
impl<T: Read + Write> Clone for CurrentSong<T> {
|
||||||
pub fn new(config: GuiElemCfg) -> Self {
|
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 {
|
Self {
|
||||||
config,
|
config,
|
||||||
children: vec![
|
children: vec![
|
||||||
GuiElem::new(Label::new(
|
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(),
|
"".to_owned(),
|
||||||
Color::from_int_rgb(180, 180, 210),
|
Color::from_int_rgb(180, 180, 210),
|
||||||
None,
|
None,
|
||||||
Vec2::new(0.1, 1.0),
|
Vec2::new(0.0, 1.0),
|
||||||
)),
|
)),
|
||||||
GuiElem::new(Label::new(
|
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(),
|
"".to_owned(),
|
||||||
Color::from_int_rgb(120, 120, 120),
|
Color::from_int_rgb(120, 120, 120),
|
||||||
None,
|
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,
|
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 {
|
fn config(&self) -> &GuiElemCfg {
|
||||||
&self.config
|
&self.config
|
||||||
}
|
}
|
||||||
@ -67,79 +96,155 @@ impl GuiElemTrait for CurrentSong {
|
|||||||
Box::new(self.clone())
|
Box::new(self.clone())
|
||||||
}
|
}
|
||||||
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
|
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
|
||||||
let song = if let Some(v) = info.database.queue.get_current() {
|
// check if there is a new song
|
||||||
if let QueueContent::Song(song) = v.content() {
|
let new_song = if let Some(song) = info.database.queue.get_current_song() {
|
||||||
if Some(*song) == self.prev_song {
|
if Some(*song) == self.prev_song {
|
||||||
// same song as before
|
// same song as before
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
Some(*song)
|
|
||||||
}
|
|
||||||
} else if self.prev_song.is_none() {
|
|
||||||
// no song, nothing in queue
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
|
} else {
|
||||||
|
Some(Some(*song))
|
||||||
}
|
}
|
||||||
} else if self.prev_song.is_none() {
|
} else if self.prev_song.is_none() {
|
||||||
// no song, nothing in queue
|
// no song, nothing in queue
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
None
|
None
|
||||||
|
} else {
|
||||||
|
self.cover = None;
|
||||||
|
Some(None)
|
||||||
};
|
};
|
||||||
if self.prev_song != song {
|
// drawing stuff
|
||||||
self.config.redraw = true;
|
if self.config.pixel_pos.size() != info.pos.size() {
|
||||||
self.prev_song = song;
|
let leftright = 0.05;
|
||||||
}
|
let topbottom = 0.05;
|
||||||
if self.config.redraw {
|
let mut width = 0.3;
|
||||||
self.config.redraw = false;
|
let mut height = 1.0 - topbottom * 2.0;
|
||||||
let (name, subtext) = if let Some(song) = song {
|
if width * info.pos.width() < height * info.pos.height() {
|
||||||
if let Some(song) = info.database.get_song(&song) {
|
height = width * info.pos.width() / info.pos.height();
|
||||||
let sub = match (
|
|
||||||
song.artist
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|id| info.database.artists().get(id)),
|
|
||||||
song.album
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|id| info.database.albums().get(id)),
|
|
||||||
) {
|
|
||||||
(None, None) => String::new(),
|
|
||||||
(Some(artist), None) => format!("by {}", artist.name),
|
|
||||||
(None, Some(album)) => {
|
|
||||||
if let Some(artist) = album
|
|
||||||
.artist
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|id| info.database.artists().get(id))
|
|
||||||
{
|
|
||||||
format!("on {} by {}", album.name, artist.name)
|
|
||||||
} else {
|
|
||||||
format!("on {}", album.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(artist), Some(album)) => {
|
|
||||||
format!("by {} on {}", artist.name, album.name)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(song.title.clone(), sub)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
"< song not in db >".to_owned(),
|
|
||||||
"maybe restart the client to resync the database?".to_owned(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
(String::new(), String::new())
|
width = height * info.pos.height() / info.pos.width();
|
||||||
};
|
}
|
||||||
*self.children[0]
|
let right = leftright + width + leftright;
|
||||||
.try_as_mut::<Label>()
|
self.cover_pos = Rectangle::from_tuples(
|
||||||
.unwrap()
|
(leftright, 0.5 - 0.5 * height),
|
||||||
.content
|
(leftright + width, 0.5 + 0.5 * height),
|
||||||
.text() = name;
|
);
|
||||||
*self.children[1]
|
for el in self.children.iter_mut().take(2) {
|
||||||
.try_as_mut::<Label>()
|
let pos = &mut el.inner.config_mut().pos;
|
||||||
.unwrap()
|
*pos = Rectangle::new(Vec2::new(right, pos.top_left().y), *pos.bottom_right());
|
||||||
.content
|
}
|
||||||
.text() = subtext;
|
}
|
||||||
|
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 = new_song;
|
||||||
|
}
|
||||||
|
if self.config.redraw {
|
||||||
|
self.config.redraw = false;
|
||||||
|
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()
|
||||||
|
.and_then(|id| info.database.artists().get(id)),
|
||||||
|
song.album
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| info.database.albums().get(id)),
|
||||||
|
) {
|
||||||
|
(None, None) => String::new(),
|
||||||
|
(Some(artist), None) => format!("by {}", artist.name),
|
||||||
|
(None, Some(album)) => {
|
||||||
|
if let Some(artist) = album
|
||||||
|
.artist
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| info.database.artists().get(id))
|
||||||
|
{
|
||||||
|
format!("on {} by {}", album.name, artist.name)
|
||||||
|
} else {
|
||||||
|
format!("on {}", album.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(artist), Some(album)) => {
|
||||||
|
format!("by {} on {}", artist.name, album.name)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(song.title.clone(), sub)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"< song not in db >".to_owned(),
|
||||||
|
"maybe restart the client to resync the database?".to_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(String::new(), String::new())
|
||||||
|
};
|
||||||
|
*self.children[0]
|
||||||
|
.try_as_mut::<Label>()
|
||||||
|
.unwrap()
|
||||||
|
.content
|
||||||
|
.text() = name;
|
||||||
|
*self.children[1]
|
||||||
|
.try_as_mut::<Label>()
|
||||||
|
.unwrap()
|
||||||
|
.content
|
||||||
|
.text() = subtext;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,16 +335,22 @@ impl GuiElemTrait for PlayPauseToggle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||||
if !self.playing_waiting_for_change {
|
match button {
|
||||||
self.playing_target = !self.playing_target;
|
MouseButton::Left => {
|
||||||
self.playing_waiting_for_change = true;
|
if !self.playing_waiting_for_change {
|
||||||
vec![GuiAction::SendToServer(if self.playing_target {
|
self.playing_target = !self.playing_target;
|
||||||
Command::Resume
|
self.playing_waiting_for_change = true;
|
||||||
} else {
|
vec![GuiAction::SendToServer(if self.playing_target {
|
||||||
Command::Pause
|
Command::Resume
|
||||||
})]
|
} else {
|
||||||
} else {
|
Command::Pause
|
||||||
vec![]
|
})]
|
||||||
|
} else {
|
||||||
|
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 speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -44,8 +48,9 @@ pub struct GuiScreen {
|
|||||||
pub prev_mouse_pos: Vec2,
|
pub prev_mouse_pos: Vec2,
|
||||||
}
|
}
|
||||||
impl GuiScreen {
|
impl GuiScreen {
|
||||||
pub fn new(
|
pub fn new<T: Read + Write + 'static + Sync + Send>(
|
||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
|
get_con: get::Client<T>,
|
||||||
line_height: f32,
|
line_height: f32,
|
||||||
scroll_sensitivity_pixels: f64,
|
scroll_sensitivity_pixels: f64,
|
||||||
scroll_sensitivity_lines: f64,
|
scroll_sensitivity_lines: f64,
|
||||||
@ -57,6 +62,7 @@ impl GuiScreen {
|
|||||||
GuiElem::new(StatusBar::new(
|
GuiElem::new(StatusBar::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
|
||||||
true,
|
true,
|
||||||
|
get_con,
|
||||||
)),
|
)),
|
||||||
GuiElem::new(Settings::new(
|
GuiElem::new(Settings::new(
|
||||||
GuiElemCfg::default().disabled(),
|
GuiElemCfg::default().disabled(),
|
||||||
@ -69,7 +75,7 @@ impl GuiScreen {
|
|||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
|
||||||
vec![
|
vec![
|
||||||
GuiElem::new(Button::new(
|
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![GuiAction::OpenSettings(true)],
|
||||||
vec![GuiElem::new(Label::new(
|
vec![GuiElem::new(Label::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
@ -80,7 +86,7 @@ impl GuiScreen {
|
|||||||
))],
|
))],
|
||||||
)),
|
)),
|
||||||
GuiElem::new(Button::new(
|
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![GuiAction::Exit],
|
||||||
vec![GuiElem::new(Label::new(
|
vec![GuiElem::new(Label::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
@ -95,7 +101,7 @@ impl GuiScreen {
|
|||||||
(0.5, 1.0),
|
(0.5, 1.0),
|
||||||
)))),
|
)))),
|
||||||
GuiElem::new(QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples(
|
GuiElem::new(QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples(
|
||||||
(0.5, 0.1),
|
(0.5, 0.03),
|
||||||
(1.0, 1.0),
|
(1.0, 1.0),
|
||||||
)))),
|
)))),
|
||||||
],
|
],
|
||||||
@ -199,9 +205,10 @@ impl GuiElemTrait for GuiScreen {
|
|||||||
if !self.idle.0 || self.idle.1.is_none() {
|
if !self.idle.0 || self.idle.1.is_none() {
|
||||||
if let Some(h) = &info.helper {
|
if let Some(h) = &info.helper {
|
||||||
h.set_cursor_visible(!self.idle.0);
|
h.set_cursor_visible(!self.idle.0);
|
||||||
for el in self.children.iter_mut().skip(1) {
|
if self.settings.0 {
|
||||||
el.inner.config_mut().enabled = !self.idle.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);
|
let p = transition(p1);
|
||||||
@ -241,14 +248,18 @@ pub struct StatusBar {
|
|||||||
idle_mode: f32,
|
idle_mode: f32,
|
||||||
}
|
}
|
||||||
impl StatusBar {
|
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 {
|
Self {
|
||||||
config,
|
config,
|
||||||
children: vec![
|
children: vec![
|
||||||
GuiElem::new(CurrentSong::new(GuiElemCfg::at(Rectangle::new(
|
GuiElem::new(CurrentSong::new(
|
||||||
Vec2::ZERO,
|
GuiElemCfg::at(Rectangle::new(Vec2::ZERO, Vec2::new(0.8, 1.0))),
|
||||||
Vec2::new(0.8, 1.0),
|
get_con,
|
||||||
)))),
|
)),
|
||||||
GuiElem::new(PlayPauseToggle::new(
|
GuiElem::new(PlayPauseToggle::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
|
||||||
false,
|
false,
|
||||||
|
@ -124,7 +124,7 @@ impl Settings {
|
|||||||
(0.0, 0.0),
|
(0.0, 0.0),
|
||||||
(0.33, 1.0),
|
(0.33, 1.0),
|
||||||
)),
|
)),
|
||||||
"Scroll Sensitivity (lines)".to_string(),
|
"Scroll Sensitivity".to_string(),
|
||||||
Color::WHITE,
|
Color::WHITE,
|
||||||
None,
|
None,
|
||||||
Vec2::new(0.9, 0.5),
|
Vec2::new(0.9, 0.5),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
eprintln, fs,
|
eprintln, fs,
|
||||||
|
io::{BufReader, Write},
|
||||||
net::{SocketAddr, TcpStream},
|
net::{SocketAddr, TcpStream},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
@ -10,12 +11,16 @@ use std::{
|
|||||||
use gui::GuiEvent;
|
use gui::GuiEvent;
|
||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::{
|
data::{
|
||||||
album::Album, artist::Artist, database::Database, queue::QueueContent, song::Song,
|
album::Album,
|
||||||
|
artist::Artist,
|
||||||
|
database::{Cover, Database},
|
||||||
|
queue::QueueContent,
|
||||||
|
song::Song,
|
||||||
DatabaseLocation, GeneralData,
|
DatabaseLocation, GeneralData,
|
||||||
},
|
},
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
player::Player,
|
player::Player,
|
||||||
server::Command,
|
server::{get, Command},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
mod gui;
|
mod gui;
|
||||||
@ -43,6 +48,22 @@ enum Mode {
|
|||||||
FillDb,
|
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() {
|
fn main() {
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
let mode = match args.next().as_ref().map(|v| v.trim()) {
|
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 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()));
|
let database = Arc::new(Mutex::new(Database::new_clientside()));
|
||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
let update_gui_sender: Arc<Mutex<Option<speedy2d::window::UserEventSender<GuiEvent>>>> =
|
let update_gui_sender: Arc<Mutex<Option<speedy2d::window::UserEventSender<GuiEvent>>>> =
|
||||||
@ -111,7 +134,15 @@ fn main() {
|
|||||||
v.send_event(GuiEvent::Refresh).unwrap();
|
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 => {
|
Mode::SyncPlayer => {
|
||||||
@ -134,6 +165,7 @@ fn main() {
|
|||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
std::io::stdin().read_line(&mut line).unwrap();
|
std::io::stdin().read_line(&mut line).unwrap();
|
||||||
if line.trim().to_lowercase() == "yes" {
|
if line.trim().to_lowercase() == "yes" {
|
||||||
|
let mut covers = 0;
|
||||||
for artist in fs::read_dir(&dir)
|
for artist in fs::read_dir(&dir)
|
||||||
.expect("reading lib-dir")
|
.expect("reading lib-dir")
|
||||||
.filter_map(|v| v.ok())
|
.filter_map(|v| v.ok())
|
||||||
@ -147,6 +179,16 @@ fn main() {
|
|||||||
let mut album_id = None;
|
let mut album_id = None;
|
||||||
let mut songs: Vec<_> = songs.filter_map(|v| v.ok()).collect();
|
let mut songs: Vec<_> = songs.filter_map(|v| v.ok()).collect();
|
||||||
songs.sort_unstable_by_key(|v| v.file_name());
|
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 {
|
for song in songs {
|
||||||
match song.path().extension().map(|v| v.to_str()) {
|
match song.path().extension().map(|v| v.to_str()) {
|
||||||
Some(Some(
|
Some(Some(
|
||||||
@ -229,11 +271,39 @@ fn main() {
|
|||||||
drop(db);
|
drop(db);
|
||||||
if !adding_album {
|
if !adding_album {
|
||||||
adding_album = true;
|
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 {
|
Command::AddAlbum(Album {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: album_name.clone(),
|
name: album_name.clone(),
|
||||||
artist: Some(artist_id),
|
artist: Some(artist_id),
|
||||||
cover: None,
|
cover,
|
||||||
songs: vec![],
|
songs: vec![],
|
||||||
general: GeneralData::default(),
|
general: GeneralData::default(),
|
||||||
})
|
})
|
||||||
|
@ -8,5 +8,6 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
awedio = "0.2.0"
|
awedio = "0.2.0"
|
||||||
base64 = "0.21.2"
|
base64 = "0.21.2"
|
||||||
|
rand = "0.8.5"
|
||||||
rc-u8-reader = "2.0.16"
|
rc-u8-reader = "2.0.16"
|
||||||
tokio = "1.29.1"
|
tokio = "1.29.1"
|
||||||
|
@ -3,8 +3,8 @@ use std::{
|
|||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{BufReader, Write},
|
io::{BufReader, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{mpsc, Arc},
|
sync::{mpsc, Arc, Mutex},
|
||||||
time::Instant,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{load::ToFromBytes, server::Command};
|
use crate::{load::ToFromBytes, server::Command};
|
||||||
@ -18,16 +18,14 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
/// the path to the file used to save/load the data
|
/// the path to the file used to save/load the data. empty if database is in client mode.
|
||||||
db_file: PathBuf,
|
pub db_file: PathBuf,
|
||||||
/// the path to the directory containing the actual music and cover image files
|
/// the path to the directory containing the actual music and cover image files
|
||||||
pub lib_directory: PathBuf,
|
pub lib_directory: PathBuf,
|
||||||
artists: HashMap<ArtistId, Artist>,
|
artists: HashMap<ArtistId, Artist>,
|
||||||
albums: HashMap<AlbumId, Album>,
|
albums: HashMap<AlbumId, Album>,
|
||||||
songs: HashMap<SongId, Song>,
|
songs: HashMap<SongId, Song>,
|
||||||
covers: HashMap<CoverId, DatabaseLocation>,
|
covers: HashMap<CoverId, Cover>,
|
||||||
// TODO! make sure this works out for the server AND clients
|
|
||||||
// cover_cache: HashMap<CoverId, Vec<u8>>,
|
|
||||||
// These will be used for autosave once that gets implemented
|
// These will be used for autosave once that gets implemented
|
||||||
db_data_file_change_first: Option<Instant>,
|
db_data_file_change_first: Option<Instant>,
|
||||||
db_data_file_change_last: 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!");
|
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.
|
/// updates an existing song in the database with the new value.
|
||||||
/// uses song.id to find the correct song.
|
/// uses song.id to find the correct song.
|
||||||
/// if the id doesn't exist in the db, Err(()) is returned.
|
/// if the id doesn't exist in the db, Err(()) is returned.
|
||||||
@ -193,7 +206,13 @@ impl Database {
|
|||||||
Command::Pause => self.playing = false,
|
Command::Pause => self.playing = false,
|
||||||
Command::Stop => self.playing = false,
|
Command::Stop => self.playing = false,
|
||||||
Command::NextSong => {
|
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 => {
|
Command::Save => {
|
||||||
if let Err(e) = self.save_database(None) {
|
if let Err(e) = self.save_database(None) {
|
||||||
@ -208,18 +227,37 @@ impl Database {
|
|||||||
}
|
}
|
||||||
Command::QueueAdd(mut index, new_data) => {
|
Command::QueueAdd(mut index, new_data) => {
|
||||||
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
|
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) {
|
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);
|
v.insert(new_data, pos);
|
||||||
|
Queue::handle_actions(self, actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::QueueRemove(index) => {
|
Command::QueueRemove(index) => {
|
||||||
self.queue.remove_by_index(&index, 0);
|
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) => {
|
Command::AddSong(song) => {
|
||||||
self.add_song_new(song);
|
self.add_song_new(song);
|
||||||
}
|
}
|
||||||
@ -229,6 +267,7 @@ impl Database {
|
|||||||
Command::AddArtist(artist) => {
|
Command::AddArtist(artist) => {
|
||||||
self.add_artist_new(artist);
|
self.add_artist_new(artist);
|
||||||
}
|
}
|
||||||
|
Command::AddCover(cover) => _ = self.add_cover_new(cover),
|
||||||
Command::ModifySong(song) => {
|
Command::ModifySong(song) => {
|
||||||
_ = self.update_song(song);
|
_ = self.update_song(song);
|
||||||
}
|
}
|
||||||
@ -302,6 +341,7 @@ impl Database {
|
|||||||
command_sender: None,
|
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> {
|
pub fn save_database(&self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> {
|
||||||
let path = if let Some(p) = path {
|
let path = if let Some(p) = path {
|
||||||
p
|
p
|
||||||
@ -386,4 +426,75 @@ impl Database {
|
|||||||
pub fn artists(&self) -> &HashMap<ArtistId, Artist> {
|
pub fn artists(&self) -> &HashMap<ArtistId, Artist> {
|
||||||
&self.artists
|
&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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Queue {
|
pub struct Queue {
|
||||||
@ -11,6 +18,14 @@ pub struct Queue {
|
|||||||
pub enum QueueContent {
|
pub enum QueueContent {
|
||||||
Song(SongId),
|
Song(SongId),
|
||||||
Folder(usize, Vec<Queue>, String),
|
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 {
|
impl Queue {
|
||||||
@ -20,27 +35,53 @@ impl Queue {
|
|||||||
pub fn content(&self) -> &QueueContent {
|
pub fn content(&self) -> &QueueContent {
|
||||||
&self.content
|
&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 {
|
match &mut self.content {
|
||||||
QueueContent::Song(_) => false,
|
QueueContent::Song(_) => None,
|
||||||
QueueContent::Folder(_, vec, _) => {
|
QueueContent::Folder(_, vec, _) => {
|
||||||
vec.push(v);
|
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 {
|
pub fn insert(&mut self, v: Self, index: usize) -> bool {
|
||||||
match &mut self.content {
|
match &mut self.content {
|
||||||
QueueContent::Song(_) => false,
|
QueueContent::Song(_) => false,
|
||||||
QueueContent::Folder(_, vec, _) => {
|
QueueContent::Folder(current, vec, _) => {
|
||||||
if index <= vec.len() {
|
if index <= vec.len() {
|
||||||
|
if *current >= index {
|
||||||
|
*current += 1;
|
||||||
|
}
|
||||||
vec.insert(index, v);
|
vec.insert(index, v);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
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 {
|
match &self.content {
|
||||||
QueueContent::Song(_) => 1,
|
QueueContent::Song(_) => 1,
|
||||||
QueueContent::Folder(_, v, _) => v.iter().map(|v| v.len()).sum(),
|
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.
|
/// recursively descends the queue until the current active element is found, then returns it.
|
||||||
pub fn get_current(&self) -> Option<&Self> {
|
pub fn get_current(&self) -> Option<&Self> {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
|
QueueContent::Song(_) => Some(self),
|
||||||
QueueContent::Folder(i, v, _) => {
|
QueueContent::Folder(i, v, _) => {
|
||||||
let i = *i;
|
let i = *i;
|
||||||
if let Some(v) = v.get(i) {
|
if let Some(v) = v.get(i) {
|
||||||
@ -65,7 +116,9 @@ impl Queue {
|
|||||||
None
|
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> {
|
pub fn get_current_song(&self) -> Option<&SongId> {
|
||||||
@ -84,6 +137,7 @@ impl Queue {
|
|||||||
}
|
}
|
||||||
pub fn get_next(&self) -> Option<&Self> {
|
pub fn get_next(&self) -> Option<&Self> {
|
||||||
match &self.content {
|
match &self.content {
|
||||||
|
QueueContent::Song(_) => None,
|
||||||
QueueContent::Folder(i, vec, _) => {
|
QueueContent::Folder(i, vec, _) => {
|
||||||
let i = *i;
|
let i = *i;
|
||||||
if let Some(v) = vec.get(i) {
|
if let Some(v) = vec.get(i) {
|
||||||
@ -100,17 +154,107 @@ impl Queue {
|
|||||||
None
|
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 {
|
match &mut self.content {
|
||||||
QueueContent::Song(_) => false,
|
QueueContent::Song(_) => false,
|
||||||
QueueContent::Folder(index, contents, _) => {
|
QueueContent::Folder(index, contents, _) => {
|
||||||
if let Some(c) = contents.get_mut(*index) {
|
if let Some(c) = contents.get_mut(*index) {
|
||||||
// inner value could advance index, do nothing.
|
let mut p = path.clone();
|
||||||
if c.advance_index() {
|
p.push(*index);
|
||||||
|
if c.advance_index_inner(p, actions) {
|
||||||
|
// inner value could advance index, do nothing.
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
loop {
|
loop {
|
||||||
@ -118,6 +262,7 @@ impl Queue {
|
|||||||
// can advance
|
// can advance
|
||||||
*index += 1;
|
*index += 1;
|
||||||
if contents[*index].enabled {
|
if contents[*index].enabled {
|
||||||
|
contents[*index].init(path, actions);
|
||||||
break true;
|
break true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -132,22 +277,113 @@ impl Queue {
|
|||||||
false
|
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) {
|
pub fn set_index_db(db: &mut Database, index: &Vec<usize>) {
|
||||||
let i = index.get(depth).map(|v| *v).unwrap_or(0);
|
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 {
|
match &mut self.content {
|
||||||
QueueContent::Song(_) => {}
|
QueueContent::Song(_) => {}
|
||||||
QueueContent::Folder(idx, contents, _) => {
|
QueueContent::Folder(idx, contents, _) => {
|
||||||
*idx = i;
|
if i != *idx {
|
||||||
for (i2, c) in contents.iter_mut().enumerate() {
|
*idx = i;
|
||||||
if i2 != i {
|
|
||||||
c.set_index(&vec![], 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Some(c) = contents.get_mut(i) {
|
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
|
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 {
|
} else {
|
||||||
Some(self)
|
Some(self)
|
||||||
@ -180,6 +422,14 @@ impl Queue {
|
|||||||
None
|
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 {
|
} else {
|
||||||
Some(self)
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
@ -264,6 +540,23 @@ impl ToFromBytes for QueueContent {
|
|||||||
contents.to_bytes(s)?;
|
contents.to_bytes(s)?;
|
||||||
name.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -273,14 +566,26 @@ impl ToFromBytes for QueueContent {
|
|||||||
{
|
{
|
||||||
let mut switch_on = [0];
|
let mut switch_on = [0];
|
||||||
s.read_exact(&mut switch_on)?;
|
s.read_exact(&mut switch_on)?;
|
||||||
Ok(if switch_on[0].count_ones() > 4 {
|
Ok(match switch_on[0] {
|
||||||
Self::Song(ToFromBytes::from_bytes(s)?)
|
0b11111111 => Self::Song(ToFromBytes::from_bytes(s)?),
|
||||||
} else {
|
0b00000000 => Self::Folder(
|
||||||
Self::Folder(
|
|
||||||
ToFromBytes::from_bytes(s)?,
|
ToFromBytes::from_bytes(s)?,
|
||||||
ToFromBytes::from_bytes(s)?,
|
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::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, VecDeque},
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
@ -81,6 +81,32 @@ where
|
|||||||
Ok(buf)
|
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>
|
impl<A> ToFromBytes for Option<A>
|
||||||
where
|
where
|
||||||
A: ToFromBytes,
|
A: ToFromBytes,
|
||||||
|
@ -67,7 +67,11 @@ impl Player {
|
|||||||
if let Some((source, _notif)) = &mut self.source {
|
if let Some((source, _notif)) = &mut self.source {
|
||||||
source.set_paused(true);
|
source.set_paused(true);
|
||||||
}
|
}
|
||||||
self.current_song_id = SongOpt::New(None);
|
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) {
|
pub fn update(&mut self, db: &mut Database) {
|
||||||
if db.playing && self.source.is_none() {
|
if db.playing && self.source.is_none() {
|
||||||
@ -76,7 +80,10 @@ impl Player {
|
|||||||
self.current_song_id = SongOpt::New(Some(*song));
|
self.current_song_id = SongOpt::New(Some(*song));
|
||||||
} else {
|
} else {
|
||||||
// db.playing, but no song in queue...
|
// 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 {
|
} else if let Some((_source, notif)) = &mut self.source {
|
||||||
if let Ok(()) = notif.try_recv() {
|
if let Ok(()) = notif.try_recv() {
|
||||||
// song has finished playing
|
// song has finished playing
|
||||||
@ -103,34 +110,35 @@ impl Player {
|
|||||||
// new current song
|
// new current song
|
||||||
if let SongOpt::New(song_opt) = &self.current_song_id {
|
if let SongOpt::New(song_opt) = &self.current_song_id {
|
||||||
// stop playback
|
// stop playback
|
||||||
eprintln!("[play] stopping playback");
|
// eprintln!("[play] stopping playback");
|
||||||
self.manager.clear();
|
self.manager.clear();
|
||||||
if let Some(song_id) = song_opt {
|
if let Some(song_id) = song_opt {
|
||||||
if db.playing {
|
// start playback again
|
||||||
// start playback again
|
if let Some(song) = db.get_song(song_id) {
|
||||||
if let Some(song) = db.get_song(song_id) {
|
// eprintln!("[play] starting playback...");
|
||||||
eprintln!("[play] starting playback...");
|
// add our song
|
||||||
// add our song
|
let ext = match &song.location.rel_path.extension() {
|
||||||
let ext = match &song.location.rel_path.extension() {
|
Some(s) => s.to_str().unwrap_or(""),
|
||||||
Some(s) => s.to_str().unwrap_or(""),
|
None => "",
|
||||||
None => "",
|
};
|
||||||
};
|
if let Some(bytes) = song.cached_data_now(db) {
|
||||||
let (sound, notif) = Self::sound_from_bytes(
|
match Self::sound_from_bytes(ext, bytes) {
|
||||||
ext,
|
Ok(v) => {
|
||||||
song.cached_data_now(db).expect("no cached data"),
|
let (sound, notif) = v.pausable().with_async_completion_notifier();
|
||||||
)
|
// add it
|
||||||
.unwrap()
|
let (sound, controller) = sound.controllable();
|
||||||
.pausable()
|
self.source = Some((controller, notif));
|
||||||
.with_async_completion_notifier();
|
// and play it
|
||||||
// add it
|
self.manager.play(Box::new(sound));
|
||||||
let (sound, controller) = sound.controllable();
|
}
|
||||||
self.source = Some((controller, notif));
|
Err(e) => {
|
||||||
// and play it
|
eprintln!("[player] Can't play, skipping! {e}");
|
||||||
self.manager.play(Box::new(sound));
|
db.apply_command(Command::NextSong);
|
||||||
eprintln!("[play] started playback");
|
}
|
||||||
} else {
|
}
|
||||||
panic!("invalid song ID: current_song_id not found in DB!");
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
panic!("invalid song ID: current_song_id not found in DB!");
|
||||||
}
|
}
|
||||||
self.current_song_id = SongOpt::Some(*song_id);
|
self.current_song_id = SongOpt::Some(*song_id);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
pub mod get;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
eprintln,
|
eprintln,
|
||||||
io::Write,
|
io::{BufRead, BufReader, Read, Write},
|
||||||
net::{SocketAddr, TcpListener},
|
net::{SocketAddr, TcpListener},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{mpsc, Arc, Mutex},
|
sync::{mpsc, Arc, Mutex},
|
||||||
@ -12,12 +14,13 @@ use crate::{
|
|||||||
data::{
|
data::{
|
||||||
album::Album,
|
album::Album,
|
||||||
artist::Artist,
|
artist::Artist,
|
||||||
database::{Database, UpdateEndpoint},
|
database::{Cover, Database, UpdateEndpoint},
|
||||||
queue::Queue,
|
queue::Queue,
|
||||||
song::Song,
|
song::Song,
|
||||||
},
|
},
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
player::Player,
|
player::Player,
|
||||||
|
server::get::handle_one_connection_as_get,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -33,12 +36,14 @@ pub enum Command {
|
|||||||
QueueInsert(Vec<usize>, usize, Queue),
|
QueueInsert(Vec<usize>, usize, Queue),
|
||||||
QueueRemove(Vec<usize>),
|
QueueRemove(Vec<usize>),
|
||||||
QueueGoto(Vec<usize>),
|
QueueGoto(Vec<usize>),
|
||||||
|
QueueSetShuffle(Vec<usize>, Vec<usize>, usize),
|
||||||
/// .id field is ignored!
|
/// .id field is ignored!
|
||||||
AddSong(Song),
|
AddSong(Song),
|
||||||
/// .id field is ignored!
|
/// .id field is ignored!
|
||||||
AddAlbum(Album),
|
AddAlbum(Album),
|
||||||
/// .id field is ignored!
|
/// .id field is ignored!
|
||||||
AddArtist(Artist),
|
AddArtist(Artist),
|
||||||
|
AddCover(Cover),
|
||||||
ModifySong(Song),
|
ModifySong(Song),
|
||||||
ModifyAlbum(Album),
|
ModifyAlbum(Album),
|
||||||
ModifyArtist(Artist),
|
ModifyArtist(Artist),
|
||||||
@ -93,33 +98,41 @@ pub fn run_server(
|
|||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
let command_sender = command_sender.clone();
|
let command_sender = command_sender.clone();
|
||||||
let db = Arc::clone(&database);
|
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 {
|
thread::spawn(move || loop {
|
||||||
if let Ok((mut connection, con_addr)) = v.accept() {
|
if let Ok((connection, con_addr)) = v.accept() {
|
||||||
eprintln!("[info] TCP connection accepted from {con_addr}.");
|
|
||||||
let command_sender = command_sender.clone();
|
let command_sender = command_sender.clone();
|
||||||
let db = Arc::clone(&db);
|
let db = Arc::clone(&db);
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
// sync database
|
eprintln!("[info] TCP connection accepted from {con_addr}.");
|
||||||
let mut db = db.lock().unwrap();
|
// each connection first has to send one line to tell us what it wants
|
||||||
db.init_connection(&mut connection)?;
|
let mut connection = BufReader::new(connection);
|
||||||
// keep the client in sync:
|
let mut line = String::new();
|
||||||
// the db will send all updates to the client once it is added to update_endpoints
|
if connection.read_line(&mut line).is_ok() {
|
||||||
db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new(
|
// based on that line, we adjust behavior
|
||||||
// try_clone is used here to split a TcpStream into Writer and Reader
|
match line.as_str().trim() {
|
||||||
connection.try_clone().unwrap(),
|
// sends all updates to this connection and reads commands from it
|
||||||
)));
|
"main" => {
|
||||||
// drop the mutex lock
|
let connection = connection.into_inner();
|
||||||
drop(db);
|
_ = handle_one_connection_as_main(
|
||||||
// read updates from the tcp stream and send them to the database, exit on EOF or Err
|
db,
|
||||||
loop {
|
&mut connection.try_clone().unwrap(),
|
||||||
if let Ok(command) = Command::from_bytes(&mut connection) {
|
connection,
|
||||||
command_sender.send(command).unwrap();
|
&command_sender,
|
||||||
} else {
|
)
|
||||||
break;
|
}
|
||||||
|
// 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 {
|
pub fn handle_one_connection_as_main(
|
||||||
type SendError: Send;
|
db: Arc<Mutex<Database>>,
|
||||||
fn send_command(&mut self, command: Command) -> Result<(), Self::SendError>;
|
connection: &mut impl Read,
|
||||||
fn receive_updates(&mut self) -> Result<Vec<Command>, Self::SendError>;
|
mut send_to: (impl Write + Sync + Send + 'static),
|
||||||
fn receive_update_blocking(&mut self) -> Result<Command, Self::SendError>;
|
command_sender: &mpsc::Sender<Command>,
|
||||||
fn move_to_thread<F: FnMut(&mut Self, Command) -> bool + Send + 'static>(
|
) -> Result<(), std::io::Error> {
|
||||||
mut self,
|
// sync database
|
||||||
mut handler: F,
|
let mut db = db.lock().unwrap();
|
||||||
) -> JoinHandle<Result<Self, Self::SendError>> {
|
db.init_connection(&mut send_to)?;
|
||||||
std::thread::spawn(move || loop {
|
// keep the client in sync:
|
||||||
let update = self.receive_update_blocking()?;
|
// the db will send all updates to the client once it is added to update_endpoints
|
||||||
if handler(&mut self, update) {
|
db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new(
|
||||||
return Ok(self);
|
// 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 {
|
impl ToFromBytes for Command {
|
||||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||||
where
|
where
|
||||||
@ -200,6 +228,12 @@ impl ToFromBytes for Command {
|
|||||||
s.write_all(&[0b00011011])?;
|
s.write_all(&[0b00011011])?;
|
||||||
index.to_bytes(s)?;
|
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) => {
|
Self::AddSong(song) => {
|
||||||
s.write_all(&[0b01010000])?;
|
s.write_all(&[0b01010000])?;
|
||||||
song.to_bytes(s)?;
|
song.to_bytes(s)?;
|
||||||
@ -212,6 +246,10 @@ impl ToFromBytes for Command {
|
|||||||
s.write_all(&[0b01011100])?;
|
s.write_all(&[0b01011100])?;
|
||||||
artist.to_bytes(s)?;
|
artist.to_bytes(s)?;
|
||||||
}
|
}
|
||||||
|
Self::AddCover(cover) => {
|
||||||
|
s.write_all(&[0b01011101])?;
|
||||||
|
cover.to_bytes(s)?;
|
||||||
|
}
|
||||||
Self::ModifySong(song) => {
|
Self::ModifySong(song) => {
|
||||||
s.write_all(&[0b10010000])?;
|
s.write_all(&[0b10010000])?;
|
||||||
song.to_bytes(s)?;
|
song.to_bytes(s)?;
|
||||||
@ -259,12 +297,18 @@ impl ToFromBytes for Command {
|
|||||||
),
|
),
|
||||||
0b00011001 => Self::QueueRemove(ToFromBytes::from_bytes(s)?),
|
0b00011001 => Self::QueueRemove(ToFromBytes::from_bytes(s)?),
|
||||||
0b00011011 => Self::QueueGoto(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)?),
|
0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
|
||||||
0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
|
0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
|
||||||
0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
|
0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
|
||||||
0b10010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
|
0b10010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
|
||||||
0b10010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
|
0b10010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
|
||||||
0b10011100 => Self::AddArtist(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)?),
|
0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("unexpected byte when reading command; stopping playback.");
|
eprintln!("unexpected byte when reading command; stopping playback.");
|
||||||
|
@ -139,8 +139,8 @@ Error getting information about the provided path '{path_s}': {e}"
|
|||||||
} else {
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[EXIT]
|
"[EXIT]
|
||||||
musicdb - help
|
musicdb-server - help
|
||||||
musicdb <path to database file> <options> <options> <...>
|
musicdb-server <path to database file> <options> <options> <...>
|
||||||
options:
|
options:
|
||||||
--init <lib directory>
|
--init <lib directory>
|
||||||
--tcp <addr:port>
|
--tcp <addr:port>
|
||||||
|
@ -78,6 +78,22 @@ pub struct AppHtml {
|
|||||||
queue_folder: Vec<HtmlPart>,
|
queue_folder: Vec<HtmlPart>,
|
||||||
/// can use: path, content, name
|
/// can use: path, content, name
|
||||||
queue_folder_current: Vec<HtmlPart>,
|
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 {
|
impl AppHtml {
|
||||||
pub fn from_dir<P: AsRef<std::path::Path>>(dir: P) -> std::io::Result<Self> {
|
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(
|
queue_folder_current: Self::parse(&std::fs::read_to_string(
|
||||||
dir.join("queue_folder_current.html"),
|
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> {
|
pub fn parse(s: &str) -> Vec<HtmlPart> {
|
||||||
@ -317,7 +349,8 @@ async fn sse_handler(
|
|||||||
| Command::ModifyArtist(..)
|
| Command::ModifyArtist(..)
|
||||||
| Command::AddSong(..)
|
| Command::AddSong(..)
|
||||||
| Command::AddAlbum(..)
|
| 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 db = state.db.lock().unwrap();
|
||||||
let mut a = db.artists().iter().collect::<Vec<_>>();
|
let mut a = db.artists().iter().collect::<Vec<_>>();
|
||||||
a.sort_unstable_by_key(|(_id, artist)| &artist.name);
|
a.sort_unstable_by_key(|(_id, artist)| &artist.name);
|
||||||
@ -352,7 +385,8 @@ async fn sse_handler(
|
|||||||
| Command::QueueAdd(..)
|
| Command::QueueAdd(..)
|
||||||
| Command::QueueInsert(..)
|
| Command::QueueInsert(..)
|
||||||
| Command::QueueRemove(..)
|
| Command::QueueRemove(..)
|
||||||
| Command::QueueGoto(..) => {
|
| Command::QueueGoto(..)
|
||||||
|
| Command::QueueSetShuffle(..) => {
|
||||||
let db = state.db.lock().unwrap();
|
let db = state.db.lock().unwrap();
|
||||||
let current = db
|
let current = db
|
||||||
.queue
|
.queue
|
||||||
@ -370,6 +404,7 @@ async fn sse_handler(
|
|||||||
&db.queue,
|
&db.queue,
|
||||||
String::new(),
|
String::new(),
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
Event::default().event("queue").data(
|
Event::default().event("queue").data(
|
||||||
state
|
state
|
||||||
@ -510,6 +545,7 @@ fn build_queue_content_build(
|
|||||||
queue: &Queue,
|
queue: &Queue,
|
||||||
path: String,
|
path: String,
|
||||||
current: bool,
|
current: bool,
|
||||||
|
skip_folder: bool,
|
||||||
) {
|
) {
|
||||||
// TODO: Do something for disabled ones too (they shouldn't just be hidden)
|
// TODO: Do something for disabled ones too (they shouldn't just be hidden)
|
||||||
if queue.enabled() {
|
if queue.enabled() {
|
||||||
@ -533,10 +569,10 @@ fn build_queue_content_build(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
QueueContent::Folder(ci, c, name) => {
|
QueueContent::Folder(ci, c, name) => {
|
||||||
if path.is_empty() {
|
if skip_folder || path.is_empty() {
|
||||||
for (i, c) in c.iter().enumerate() {
|
for (i, c) in c.iter().enumerate() {
|
||||||
let current = current && *ci == i;
|
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 {
|
} else {
|
||||||
for v in if current {
|
for v in if current {
|
||||||
@ -559,6 +595,7 @@ fn build_queue_content_build(
|
|||||||
c,
|
c,
|
||||||
format!("{path}-{i}"),
|
format!("{path}-{i}"),
|
||||||
current,
|
current,
|
||||||
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -568,6 +605,90 @@ fn build_queue_content_build(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user