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" }
|
||||
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…
Reference in New Issue
Block a user