small improvements idk i forgot i had a git repo for this project

This commit is contained in:
Mark 2023-08-24 16:15:01 +02:00
parent 0ae0126f04
commit 9fbe67012e
17 changed files with 1894 additions and 455 deletions

View File

@ -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"]

View File

@ -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()) {

View File

@ -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,

View File

@ -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()

View File

@ -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()
@ -143,6 +247,7 @@ impl GuiElemTrait for CurrentSong {
}
}
}
}
#[derive(Clone)]
pub struct PlayPauseToggle {
@ -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

View File

@ -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,

View File

@ -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),

View File

@ -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(),
})

View File

@ -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"

View File

@ -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))),
})
}
}

View File

@ -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()),
})
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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.");

View File

@ -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>

View File

@ -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,
)
}
}
@ -570,4 +692,3 @@ fn build_queue_content_build(
}
}
}
}