This commit is contained in:
Mark 2024-12-19 18:52:56 +01:00
parent 85c79a21e1
commit d3a1facba0
20 changed files with 544 additions and 231 deletions

View File

@ -12,7 +12,7 @@ directories = "5.0.1"
regex = "1.9.3" regex = "1.9.3"
speedy2d = { version = "1.12.0", optional = true } speedy2d = { version = "1.12.0", optional = true }
toml = "0.7.6" toml = "0.7.6"
musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true } # musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true }
uianimator = "0.1.1" uianimator = "0.1.1"
[features] [features]
@ -26,8 +26,8 @@ default = ["gui", "default-playback"]
# playback: # playback:
# enables syncplayer modes, where the client mirrors the server's playback # enables syncplayer modes, where the client mirrors the server's playback
gui = ["speedy2d"] gui = ["speedy2d"]
merscfg = ["mers", "gui"] # merscfg = ["mers", "gui"]
mers = ["musicdb-mers"] # mers = ["musicdb-mers"]
playback = [] playback = []
default-playback = ["playback", "musicdb-lib/default-playback"] default-playback = ["playback", "musicdb-lib/default-playback"]
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"] playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]

View File

@ -1,6 +1,13 @@
# [build]
# pre-build = [
# "dpkg --add-architecture $CROSS_DEB_ARCH",
# "apt-get update && apt-get --assume-yes install libasound2-dev libasound2-dev:$CROSS_DEB_ARCH"
# ]
# default-target = "aarch64-unknown-linux-gnu"
[build] [build]
pre-build = [ pre-build = [
"dpkg --add-architecture $CROSS_DEB_ARCH", "dpkg --add-architecture $CROSS_DEB_ARCH",
"apt-get update && apt-get --assume-yes install libasound2-dev libasound2-dev:$CROSS_DEB_ARCH" "apt-get update && apt-get --assume-yes install libasound2-dev"
] ]
default-target = "aarch64-unknown-linux-gnu" default-target = "x86_64-unknown-linux-musl"

View File

@ -362,9 +362,14 @@ impl Gui {
} }
Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"), Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"),
} }
database.lock().unwrap().update_endpoints.push( {
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| { let mut db = database.lock().unwrap();
match &cmd.action { let udepid = db.update_endpoints_id;
db.update_endpoints_id += 1;
db.update_endpoints.push((
udepid,
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(
move |cmd| match &cmd.action {
Action::Resume Action::Resume
| Action::Pause | Action::Pause
| Action::Stop | Action::Stop
@ -386,13 +391,13 @@ impl Gui {
} }
} }
Action::SyncDatabase(..) Action::SyncDatabase(..)
| Action::AddSong(_) | Action::AddSong(_, _)
| Action::AddAlbum(_) | Action::AddAlbum(_, _)
| Action::AddArtist(_) | Action::AddArtist(_, _)
| Action::AddCover(_) | Action::AddCover(_, _)
| Action::ModifySong(_) | Action::ModifySong(_, _)
| Action::ModifyAlbum(_) | Action::ModifyAlbum(_, _)
| Action::ModifyArtist(_) | Action::ModifyArtist(_, _)
| Action::RemoveSong(_) | Action::RemoveSong(_)
| Action::RemoveAlbum(_) | Action::RemoveAlbum(_)
| Action::RemoveArtist(_) | Action::RemoveArtist(_)
@ -443,9 +448,38 @@ impl Gui {
})) }))
.unwrap(); .unwrap();
} }
Action::Denied(req) => {
let req = *req;
notif_sender_two
.send(Box::new(move |_| {
(
Box::new(Panel::with_background(
GuiElemCfg::default(),
[Label::new(
GuiElemCfg::default(),
format!(
"server denied {}",
if req.is_some() {
"request, maybe desynced"
} else {
"action, likely desynced"
},
),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
)],
Color::from_rgba(0.0, 0.0, 0.0, 0.8),
)),
NotifInfo::new(Duration::from_secs(1)),
)
}))
.unwrap();
}
},
)),
));
} }
})),
);
let no_animations = false; let no_animations = false;
Gui { Gui {
event_sender, event_sender,
@ -1307,16 +1341,10 @@ impl Gui {
} }
} }
GuiAction::SendToServer(action) => { GuiAction::SendToServer(action) => {
let command = self.database.lock().unwrap().seq.pack(action);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
eprintln!("[DEBUG] Sending command to server: {action:?}"); eprintln!("[DEBUG] Sending command to server: {command:?}");
if let Err(e) = self if let Err(e) = command.to_bytes(&mut self.connection) {
.database
.lock()
.unwrap()
.seq
.pack(action)
.to_bytes(&mut self.connection)
{
eprintln!("Error sending command to server: {e}"); eprintln!("Error sending command to server: {e}");
} }
} }

View File

@ -2,7 +2,7 @@ use std::time::Instant;
use musicdb_lib::{ use musicdb_lib::{
data::{song::Song, ArtistId}, data::{song::Song, ArtistId},
server::Action, server::{Action, Req},
}; };
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
@ -189,7 +189,10 @@ impl GuiElem for EditorForSongs {
song.album = None; song.album = None;
} }
info.actions info.actions
.push(GuiAction::SendToServer(Action::ModifySong(song))); .push(GuiAction::SendToServer(Action::ModifySong(
song,
Req::none(),
)));
} }
} }
Event::SetArtist(name, id) => { Event::SetArtist(name, id) => {

View File

@ -5,7 +5,7 @@ use musicdb_lib::{
song::Song, song::Song,
AlbumId, ArtistId, AlbumId, ArtistId,
}, },
server::Action, server::{Action, Req},
}; };
use speedy2d::{ use speedy2d::{
color::Color, color::Color,
@ -404,7 +404,7 @@ impl GuiElem for QueueEmptySpaceDragHandler {
dragged_add_to_queue( dragged_add_to_queue(
dragged, dragged,
(), (),
|_, q| Action::QueueAdd(vec![], q), |_, q| Action::QueueAdd(vec![], q, Req::none()),
|_, q| Action::QueueMoveInto(q, vec![]), |_, q| Action::QueueMoveInto(q, vec![]),
) )
} }
@ -631,9 +631,9 @@ impl GuiElem for QueueSong {
self.path.clone(), self.path.clone(),
move |mut p: Vec<usize>, q| { move |mut p: Vec<usize>, q| {
if let Some(j) = p.pop() { if let Some(j) = p.pop() {
Action::QueueInsert(p, if insert_below { j + 1 } else { j }, q) Action::QueueInsert(p, if insert_below { j + 1 } else { j }, q, Req::none())
} else { } else {
Action::QueueAdd(p, q) Action::QueueAdd(p, q, Req::none())
} }
}, },
move |mut p, q| { move |mut p, q| {
@ -826,7 +826,7 @@ impl GuiElem for QueueFolder {
dragged_add_to_queue( dragged_add_to_queue(
dragged, dragged,
self.path.clone(), self.path.clone(),
|p, q| Action::QueueAdd(p, q), |p, q| Action::QueueAdd(p, q, Req::none()),
|p, q| Action::QueueMoveInto(q, p), |p, q| Action::QueueMoveInto(q, p),
) )
} else { } else {
@ -835,7 +835,7 @@ impl GuiElem for QueueFolder {
self.path.clone(), self.path.clone(),
|mut p, q| { |mut p, q| {
let j = p.pop().unwrap_or(0); let j = p.pop().unwrap_or(0);
Action::QueueInsert(p, j, q) Action::QueueInsert(p, j, q, Req::none())
}, },
|p, q| Action::QueueMove(q, p), |p, q| Action::QueueMove(q, p),
) )
@ -903,7 +903,7 @@ impl GuiElem for QueueIndentEnd {
dragged_add_to_queue( dragged_add_to_queue(
dragged, dragged,
self.path_insert.clone(), self.path_insert.clone(),
|(p, j), q| Action::QueueInsert(p, j, q), |(p, j), q| Action::QueueInsert(p, j, q, Req::none()),
|(mut p, j), q| { |(mut p, j), q| {
p.push(j); p.push(j);
Action::QueueMove(q, p) Action::QueueMove(q, p)
@ -1066,7 +1066,7 @@ impl GuiElem for QueueLoop {
dragged_add_to_queue( dragged_add_to_queue(
dragged, dragged,
p, p,
|p, q| Action::QueueAdd(p, q), |p, q| Action::QueueAdd(p, q, Req::none()),
|p, q| Action::QueueMoveInto(q, p), |p, q| Action::QueueMoveInto(q, p),
) )
} else { } else {

View File

@ -2,7 +2,7 @@ use std::time::Instant;
use musicdb_lib::{ use musicdb_lib::{
data::queue::{QueueContent, QueueFolder}, data::queue::{QueueContent, QueueFolder},
server::Action, server::{Action, Req},
}; };
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D};
use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator}; use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator};
@ -126,6 +126,7 @@ impl GuiScreen {
musicdb_lib::data::queue::QueueFolder::default(), musicdb_lib::data::queue::QueueFolder::default(),
) )
.into(), .into(),
Req::none(),
))] ))]
}, },
[Label::new( [Label::new(

View File

@ -203,7 +203,7 @@ fn main() {
action(action); action(action);
break 'feature_if; break 'feature_if;
} }
db.apply_action_unchecked_seq(action); db.apply_action_unchecked_seq(action, None);
} }
#[cfg(feature = "playback")] #[cfg(feature = "playback")]
if let Some(player) = &mut player { if let Some(player) = &mut player {

View File

@ -4,7 +4,7 @@ use crate::load::ToFromBytes;
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId}; use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub struct Album { pub struct Album {
pub id: AlbumId, pub id: AlbumId,
pub name: String, pub name: String,

View File

@ -4,7 +4,7 @@ use crate::load::ToFromBytes;
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId}; use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub struct Artist { pub struct Artist {
pub id: ArtistId, pub id: ArtistId,
pub name: String, pub name: String,

View File

@ -13,7 +13,7 @@ use rand::thread_rng;
use crate::{ use crate::{
load::ToFromBytes, load::ToFromBytes,
server::{Action, Command, Commander}, server::{Action, Command, Commander, Req},
}; };
use super::{ use super::{
@ -45,10 +45,11 @@ pub struct Database {
pub queue: Queue, pub queue: Queue,
/// if the database receives an update, it will inform all of its clients so they can stay in sync. /// if the database receives an update, it will inform all of its clients so they can stay in sync.
/// this is a list containing all the clients. /// this is a list containing all the clients.
pub update_endpoints: Vec<UpdateEndpoint>, pub update_endpoints: Vec<(u64, UpdateEndpoint)>,
pub update_endpoints_id: u64,
/// true if a song is/should be playing /// true if a song is/should be playing
pub playing: bool, pub playing: bool,
pub command_sender: Option<mpsc::Sender<Command>>, pub command_sender: Option<mpsc::Sender<(Command, Option<u64>)>>,
pub remote_server_as_song_file_source: pub remote_server_as_song_file_source:
Option<Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>>, Option<Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>>,
/// only relevant for clients. true if init is done /// only relevant for clients. true if init is done
@ -65,7 +66,7 @@ pub enum UpdateEndpoint {
Bytes(Box<dyn Write + Sync + Send>), Bytes(Box<dyn Write + Sync + Send>),
CmdChannel(mpsc::Sender<Arc<Command>>), CmdChannel(mpsc::Sender<Arc<Command>>),
Custom(Box<dyn FnMut(&Command) + Send>), Custom(Box<dyn FnMut(&Command) + Send>),
CustomArc(Box<dyn FnMut(&Arc<Command>) + Send>), CustomArc(Box<dyn FnMut(Arc<Command>) + Send>),
CustomBytes(Box<dyn FnMut(&[u8]) + Send>), CustomBytes(Box<dyn FnMut(&[u8]) + Send>),
} }
@ -507,7 +508,7 @@ impl Database {
)) ))
.to_bytes(con)?; .to_bytes(con)?;
self.seq self.seq
.pack(Action::QueueUpdate(vec![], self.queue.clone())) .pack(Action::QueueUpdate(vec![], self.queue.clone(), Req::none()))
.to_bytes(con)?; .to_bytes(con)?;
if self.playing { if self.playing {
self.seq.pack(Action::Resume).to_bytes(con)?; self.seq.pack(Action::Resume).to_bytes(con)?;
@ -521,8 +522,29 @@ impl Database {
} }
/// `apply_action_unchecked_seq(command.action)` if `command.seq` is correct or `0xFF` /// `apply_action_unchecked_seq(command.action)` if `command.seq` is correct or `0xFF`
pub fn apply_command(&mut self, command: Command) { pub fn apply_command(&mut self, mut command: Command, client: Option<u64>) {
if command.seq != self.seq.seq() && command.seq != 0xFF { if command.seq != self.seq.seq() && command.seq != 0xFF {
if let Some(client) = client {
for (udepid, udep) in &mut self.update_endpoints {
if client == *udepid {
let denied =
Action::Denied(command.action.get_req().unwrap_or_else(Req::none))
.cmd(0xFFu8);
match udep {
UpdateEndpoint::Bytes(w) => {
let _ = w.write(&denied.to_bytes_vec());
}
UpdateEndpoint::CmdChannel(w) => {
let _ = w.send(Arc::new(denied));
}
UpdateEndpoint::Custom(w) => w(&denied),
UpdateEndpoint::CustomArc(w) => w(Arc::new(denied)),
UpdateEndpoint::CustomBytes(w) => w(&denied.to_bytes_vec()),
}
return;
}
}
}
eprintln!( eprintln!(
"Invalid sequence number: got {} but expected {}.", "Invalid sequence number: got {} but expected {}.",
command.seq, command.seq,
@ -530,9 +552,9 @@ impl Database {
); );
return; return;
} }
self.apply_action_unchecked_seq(command.action) self.apply_action_unchecked_seq(command.action, client)
} }
pub fn apply_action_unchecked_seq(&mut self, mut action: Action) { pub fn apply_action_unchecked_seq(&mut self, mut action: Action, client: Option<u64>) {
if !self.is_client() { if !self.is_client() {
if let Action::ErrorInfo(t, _) = &mut action { if let Action::ErrorInfo(t, _) = &mut action {
// clients can send ErrorInfo to the server and it will show up on other clients, // clients can send ErrorInfo to the server and it will show up on other clients,
@ -548,7 +570,7 @@ impl Database {
Action::Pause if !self.playing => (), Action::Pause if !self.playing => (),
Action::Resume if self.playing => (), Action::Resume if self.playing => (),
// since db.update_endpoints is empty for clients, this won't cause unwanted back and forth // since db.update_endpoints is empty for clients, this won't cause unwanted back and forth
_ => action = self.broadcast_update(action), _ => action = self.broadcast_update(action, client),
} }
match action { match action {
Action::Resume => self.playing = true, Action::Resume => self.playing = true,
@ -557,7 +579,7 @@ impl Database {
Action::NextSong => { Action::NextSong => {
if !Queue::advance_index_db(self) { if !Queue::advance_index_db(self) {
// end of queue // end of queue
self.apply_action_unchecked_seq(Action::Pause); self.apply_action_unchecked_seq(Action::Pause, client);
self.queue.init(); self.queue.init();
} }
} }
@ -567,17 +589,17 @@ impl Database {
} }
} }
Action::SyncDatabase(a, b, c) => self.sync(a, b, c), Action::SyncDatabase(a, b, c) => self.sync(a, b, c),
Action::QueueUpdate(index, new_data) => { Action::QueueUpdate(index, new_data, _) => {
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
*v = new_data; *v = new_data;
} }
} }
Action::QueueAdd(index, new_data) => { Action::QueueAdd(index, new_data, _) => {
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
v.add_to_end(new_data, false); v.add_to_end(new_data, false);
} }
} }
Action::QueueInsert(index, pos, new_data) => { Action::QueueInsert(index, pos, new_data, _) => {
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
v.insert(new_data, pos, false); v.insert(new_data, pos, false);
} }
@ -662,7 +684,7 @@ impl Database {
{ {
let mut ord: Vec<usize> = (0..content.len()).collect(); let mut ord: Vec<usize> = (0..content.len()).collect();
ord.shuffle(&mut thread_rng()); ord.shuffle(&mut thread_rng());
self.apply_action_unchecked_seq(Action::QueueSetShuffle(path, ord)); self.apply_action_unchecked_seq(Action::QueueSetShuffle(path, ord), client);
} else { } else {
eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!"); eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!");
} }
@ -719,23 +741,23 @@ impl Database {
} }
} }
} }
Action::AddSong(song) => { Action::AddSong(song, _) => {
self.add_song_new(song); self.add_song_new(song);
} }
Action::AddAlbum(album) => { Action::AddAlbum(album, _) => {
self.add_album_new(album); self.add_album_new(album);
} }
Action::AddArtist(artist) => { Action::AddArtist(artist, _) => {
self.add_artist_new(artist); self.add_artist_new(artist);
} }
Action::AddCover(cover) => _ = self.add_cover_new(cover), Action::AddCover(cover, _) => _ = self.add_cover_new(cover),
Action::ModifySong(song) => { Action::ModifySong(song, _) => {
_ = self.update_song(song); _ = self.update_song(song);
} }
Action::ModifyAlbum(album) => { Action::ModifyAlbum(album, _) => {
_ = self.update_album(album); _ = self.update_album(album);
} }
Action::ModifyArtist(artist) => { Action::ModifyArtist(artist, _) => {
_ = self.update_artist(artist); _ = self.update_artist(artist);
} }
Action::RemoveSong(song) => { Action::RemoveSong(song) => {
@ -846,6 +868,7 @@ impl Database {
self.client_is_init = true; self.client_is_init = true;
} }
Action::ErrorInfo(..) => {} Action::ErrorInfo(..) => {}
Action::Denied(..) => {}
} }
} }
} }
@ -875,6 +898,7 @@ impl Database {
custom_files: None, custom_files: None,
queue: QueueContent::Folder(QueueFolder::default()).into(), queue: QueueContent::Folder(QueueFolder::default()).into(),
update_endpoints: vec![], update_endpoints: vec![],
update_endpoints_id: 0,
playing: false, playing: false,
command_sender: None, command_sender: None,
remote_server_as_song_file_source: None, remote_server_as_song_file_source: None,
@ -896,6 +920,7 @@ impl Database {
custom_files: None, custom_files: None,
queue: QueueContent::Folder(QueueFolder::default()).into(), queue: QueueContent::Folder(QueueFolder::default()).into(),
update_endpoints: vec![], update_endpoints: vec![],
update_endpoints_id: 0,
playing: false, playing: false,
command_sender: None, command_sender: None,
remote_server_as_song_file_source: None, remote_server_as_song_file_source: None,
@ -922,6 +947,7 @@ impl Database {
custom_files: None, custom_files: None,
queue: QueueContent::Folder(QueueFolder::default()).into(), queue: QueueContent::Folder(QueueFolder::default()).into(),
update_endpoints: vec![], update_endpoints: vec![],
update_endpoints_id: 0,
playing: false, playing: false,
command_sender: None, command_sender: None,
remote_server_as_song_file_source: None, remote_server_as_song_file_source: None,
@ -973,7 +999,7 @@ impl Database {
self.times_data_modified = None; self.times_data_modified = None;
Ok(path) Ok(path)
} }
pub fn broadcast_update(&mut self, update: Action) -> Action { pub fn broadcast_update(&mut self, update: Action, client: Option<u64>) -> Action {
match update { match update {
Action::InitComplete => return update, Action::InitComplete => return update,
_ => {} _ => {}
@ -981,11 +1007,36 @@ impl Database {
if !self.is_client() { if !self.is_client() {
self.seq.inc(); self.seq.inc();
} }
let update = self.seq.pack(update); let mut update = self.seq.pack(update);
let req = update.action.take_req();
let mut remove = vec![]; let mut remove = vec![];
let mut bytes = None; let mut bytes = None;
let mut arc = None; let mut arc = None;
for (i, udep) in self.update_endpoints.iter_mut().enumerate() { for (i, (udepid, udep)) in self.update_endpoints.iter_mut().enumerate() {
if req.is_some_and(|r| r.is_some()) && client.is_some_and(|v| *udepid == v) {
update.action.put_req(req.unwrap());
match udep {
UpdateEndpoint::Bytes(writer) => {
if writer.write_all(&update.to_bytes_vec()).is_err() {
remove.push(i);
}
}
UpdateEndpoint::CmdChannel(sender) => {
if sender.send(Arc::new(update.clone())).is_err() {
remove.push(i);
}
}
UpdateEndpoint::Custom(func) => func(&update),
UpdateEndpoint::CustomArc(func) => func(Arc::new(update.clone())),
UpdateEndpoint::CustomBytes(func) => {
if bytes.is_none() {
bytes = Some(update.to_bytes_vec());
}
func(bytes.as_ref().unwrap())
}
}
update.action.take_req();
}
match udep { match udep {
UpdateEndpoint::Bytes(writer) => { UpdateEndpoint::Bytes(writer) => {
if bytes.is_none() { if bytes.is_none() {
@ -1008,7 +1059,7 @@ impl Database {
if arc.is_none() { if arc.is_none() {
arc = Some(Arc::new(update.clone())); arc = Some(Arc::new(update.clone()));
} }
func(arc.as_ref().unwrap()) func(Arc::clone(arc.as_ref().unwrap()))
} }
UpdateEndpoint::CustomBytes(func) => { UpdateEndpoint::CustomBytes(func) => {
if bytes.is_none() { if bytes.is_none() {
@ -1028,6 +1079,9 @@ impl Database {
self.update_endpoints.remove(i); self.update_endpoints.remove(i);
} }
} }
if let Some(req) = req {
update.action.put_req(req);
}
update.action update.action
} }
pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) { pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) {
@ -1078,6 +1132,11 @@ pub struct Cover {
pub location: DatabaseLocation, pub location: DatabaseLocation,
pub data: Arc<Mutex<(bool, Option<(Instant, Vec<u8>)>)>>, pub data: Arc<Mutex<(bool, Option<(Instant, Vec<u8>)>)>>,
} }
impl PartialEq for Cover {
fn eq(&self, other: &Self) -> bool {
self.location == other.location
}
}
impl Cover { impl Cover {
pub fn get_bytes_from_file<O>( pub fn get_bytes_from_file<O>(
&self, &self,

View File

@ -17,13 +17,13 @@ pub type AlbumId = u64;
pub type ArtistId = u64; pub type ArtistId = u64;
pub type CoverId = u64; pub type CoverId = u64;
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug, PartialEq)]
/// general data for songs, albums and artists /// general data for songs, albums and artists
pub struct GeneralData { pub struct GeneralData {
pub tags: Vec<String>, pub tags: Vec<String>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
/// the location of a file relative to the lib directory, often Artist/Album/Song.ext or similar /// the location of a file relative to the lib directory, often Artist/Album/Song.ext or similar
pub struct DatabaseLocation { pub struct DatabaseLocation {
pub rel_path: PathBuf, pub rel_path: PathBuf,

View File

@ -4,18 +4,18 @@ use crate::load::ToFromBytes;
use super::{database::Database, SongId}; use super::{database::Database, SongId};
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub struct Queue { pub struct Queue {
enabled: bool, enabled: bool,
content: QueueContent, content: QueueContent,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub enum QueueContent { pub enum QueueContent {
Song(SongId), Song(SongId),
Folder(QueueFolder), Folder(QueueFolder),
Loop(usize, usize, Box<Queue>), Loop(usize, usize, Box<Queue>),
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct QueueFolder { pub struct QueueFolder {
pub index: usize, pub index: usize,
pub content: Vec<Queue>, pub content: Vec<Queue>,

View File

@ -17,7 +17,7 @@ use super::{
AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId, AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub struct Song { pub struct Song {
pub id: SongId, pub id: SongId,
pub location: DatabaseLocation, pub location: DatabaseLocation,
@ -303,6 +303,12 @@ pub struct CachedData(
)>, )>,
>, >,
); );
impl PartialEq for CachedData {
fn eq(&self, _other: &Self) -> bool {
// for testing
true
}
}
impl Clone for CachedData { impl Clone for CachedData {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self(Arc::clone(&self.0)) Self(Arc::clone(&self.0))

View File

@ -126,7 +126,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
pub fn update_uncache_opt(&mut self, db: &mut Database, allow_uncaching: bool) { pub fn update_uncache_opt(&mut self, db: &mut Database, allow_uncaching: bool) {
if self.allow_sending_commands { if self.allow_sending_commands {
if self.allow_sending_commands && self.backend.song_finished() { if self.allow_sending_commands && self.backend.song_finished() {
db.apply_action_unchecked_seq(Action::NextSong); db.apply_action_unchecked_seq(Action::NextSong, None);
} }
} }
@ -145,7 +145,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
self.backend.next(db.playing, load_duration); self.backend.next(db.playing, load_duration);
if self.allow_sending_commands && load_duration { if self.allow_sending_commands && load_duration {
if let Some(dur) = self.backend.current_song_duration() { if let Some(dur) = self.backend.current_song_duration() {
db.apply_action_unchecked_seq(Action::SetSongDuration(id, dur)) db.apply_action_unchecked_seq(Action::SetSongDuration(id, dur), None)
} }
} }
} else if let Some(song) = db.get_song(&id) { } else if let Some(song) = db.get_song(&id) {
@ -169,21 +169,27 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
self.backend.next(db.playing, load_duration); self.backend.next(db.playing, load_duration);
if self.allow_sending_commands && load_duration { if self.allow_sending_commands && load_duration {
if let Some(dur) = self.backend.current_song_duration() { if let Some(dur) = self.backend.current_song_duration() {
db.apply_action_unchecked_seq(Action::SetSongDuration(id, dur)) db.apply_action_unchecked_seq(
Action::SetSongDuration(id, dur),
None,
)
} }
} }
} else { } else {
// only show an error if the user tries to play the song. // only show an error if the user tries to play the song.
// otherwise, the error might be spammed. // otherwise, the error might be spammed.
if self.allow_sending_commands && db.playing { if self.allow_sending_commands && db.playing {
db.apply_action_unchecked_seq(Action::ErrorInfo( db.apply_action_unchecked_seq(
Action::ErrorInfo(
format!("Couldn't load bytes for song {id}"), format!("Couldn't load bytes for song {id}"),
format!( format!(
"Song: {}\nby {:?} on {:?}", "Song: {}\nby {:?} on {:?}",
song.title, song.artist, song.album song.title, song.artist, song.album
), ),
)); ),
db.apply_action_unchecked_seq(Action::NextSong); None,
);
db.apply_action_unchecked_seq(Action::NextSong, None);
} }
self.backend.clear(); self.backend.clear();
} }

View File

@ -13,12 +13,12 @@ pub struct PlayerBackendPlaybackRs<T> {
player: playback_rs::Player, player: playback_rs::Player,
current: Option<(SongId, Option<playback_rs::Song>, T)>, current: Option<(SongId, Option<playback_rs::Song>, T)>,
next: Option<(SongId, Option<playback_rs::Song>, T)>, next: Option<(SongId, Option<playback_rs::Song>, T)>,
command_sender: Option<std::sync::mpsc::Sender<Command>>, command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
} }
impl<T> PlayerBackendPlaybackRs<T> { impl<T> PlayerBackendPlaybackRs<T> {
pub fn new( pub fn new(
command_sender: std::sync::mpsc::Sender<Command>, command_sender: std::sync::mpsc::Sender<(Command, Option<u64>)>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(Some(command_sender)) Self::new_with_optional_command_sending(Some(command_sender))
} }
@ -26,7 +26,7 @@ impl<T> PlayerBackendPlaybackRs<T> {
Self::new_with_optional_command_sending(None) Self::new_with_optional_command_sending(None)
} }
pub fn new_with_optional_command_sending( pub fn new_with_optional_command_sending(
command_sender: Option<std::sync::mpsc::Sender<Command>>, command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self { Ok(Self {
player: playback_rs::Player::new(None)?, player: playback_rs::Player::new(None)?,
@ -55,13 +55,14 @@ impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> {
Ok(v) => Some(v), Ok(v) => Some(v),
Err(e) => { Err(e) => {
if let Some(s) = &self.command_sender { if let Some(s) = &self.command_sender {
s.send( s.send((
Action::ErrorInfo( Action::ErrorInfo(
format!("Couldn't decode song #{id}!"), format!("Couldn't decode song #{id}!"),
format!("Error: {e}"), format!("Error: {e}"),
) )
.cmd(0xFFu8), .cmd(0xFFu8),
) None,
))
.unwrap(); .unwrap();
} }
None None
@ -101,21 +102,22 @@ impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> {
if let Some(song) = song { if let Some(song) = song {
if let Err(e) = self.player.play_song_now(song, None) { if let Err(e) = self.player.play_song_now(song, None) {
if let Some(s) = &self.command_sender { if let Some(s) = &self.command_sender {
s.send( s.send((
Action::ErrorInfo( Action::ErrorInfo(
format!("Couldn't play song #{id}!"), format!("Couldn't play song #{id}!"),
format!("Error: {e}"), format!("Error: {e}"),
) )
.cmd(0xFFu8), .cmd(0xFFu8),
) None,
))
.unwrap(); .unwrap();
s.send(Action::NextSong.cmd(0xFFu8)).unwrap(); s.send((Action::NextSong.cmd(0xFFu8), None)).unwrap();
} }
} else { } else {
self.player.set_playing(play); self.player.set_playing(play);
} }
} else if let Some(s) = &self.command_sender { } else if let Some(s) = &self.command_sender {
s.send(Action::NextSong.cmd(0xFFu8)).unwrap(); s.send((Action::NextSong.cmd(0xFFu8), None)).unwrap();
} }
} }
} }

View File

@ -19,12 +19,12 @@ pub struct PlayerBackendRodio<T> {
stopped: bool, stopped: bool,
current: Option<(SongId, Arc<Vec<u8>>, Option<u128>, T)>, current: Option<(SongId, Arc<Vec<u8>>, Option<u128>, T)>,
next: Option<(SongId, Arc<Vec<u8>>, Option<MyDecoder>, T)>, next: Option<(SongId, Arc<Vec<u8>>, Option<MyDecoder>, T)>,
command_sender: Option<std::sync::mpsc::Sender<Command>>, command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
} }
impl<T> PlayerBackendRodio<T> { impl<T> PlayerBackendRodio<T> {
pub fn new( pub fn new(
command_sender: std::sync::mpsc::Sender<Command>, command_sender: std::sync::mpsc::Sender<(Command, Option<u64>)>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(Some(command_sender)) Self::new_with_optional_command_sending(Some(command_sender))
} }
@ -32,7 +32,7 @@ impl<T> PlayerBackendRodio<T> {
Self::new_with_optional_command_sending(None) Self::new_with_optional_command_sending(None)
} }
pub fn new_with_optional_command_sending( pub fn new_with_optional_command_sending(
command_sender: Option<std::sync::mpsc::Sender<Command>>, command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
) -> Result<Self, Box<dyn std::error::Error>> { ) -> Result<Self, Box<dyn std::error::Error>> {
let (output_stream, output_stream_handle) = rodio::OutputStream::try_default()?; let (output_stream, output_stream_handle) = rodio::OutputStream::try_default()?;
let sink = Sink::try_new(&output_stream_handle)?; let sink = Sink::try_new(&output_stream_handle)?;
@ -60,13 +60,14 @@ impl<T> PlayerBackend<T> for PlayerBackendRodio<T> {
let decoder = decoder_from_bytes(Arc::clone(&bytes)); let decoder = decoder_from_bytes(Arc::clone(&bytes));
if let Err(e) = &decoder { if let Err(e) = &decoder {
if let Some(s) = &self.command_sender { if let Some(s) = &self.command_sender {
s.send( s.send((
Action::ErrorInfo( Action::ErrorInfo(
format!("Couldn't decode song #{id}!"), format!("Couldn't decode song #{id}!"),
format!("Error: '{e}'"), format!("Error: '{e}'"),
) )
.cmd(0xFFu8), .cmd(0xFFu8),
) None,
))
.unwrap(); .unwrap();
} }
} }

View File

@ -43,11 +43,73 @@ impl Action {
pub fn cmd(self, seq: u8) -> Command { pub fn cmd(self, seq: u8) -> Command {
Command::new(seq, self) Command::new(seq, self)
} }
pub fn take_req(&mut self) -> Option<Req> {
self.req_mut()
.map(|r| std::mem::replace(r, Req::none()))
.filter(|r| r.is_some())
}
pub fn get_req(&mut self) -> Option<Req> {
self.req_mut().map(|r| *r).filter(|r| r.is_some())
}
pub fn put_req(&mut self, req: Req) {
if let Some(r) = self.req_mut() {
*r = req;
}
}
fn req_mut(&mut self) -> Option<&mut Req> {
match self {
Self::QueueUpdate(_, _, req)
| Self::QueueAdd(_, _, req)
| Self::QueueInsert(_, _, _, req)
| Self::AddSong(_, req)
| Self::AddAlbum(_, req)
| Self::AddArtist(_, req)
| Self::AddCover(_, req)
| Self::ModifySong(_, req)
| Self::ModifyAlbum(_, req)
| Self::ModifyArtist(_, req)
| Self::Denied(req) => Some(req),
Self::Resume
| Self::Pause
| Self::Stop
| Self::NextSong
| Self::SyncDatabase(_, _, _)
| Self::QueueRemove(_)
| Self::QueueMove(_, _)
| Self::QueueMoveInto(_, _)
| Self::QueueGoto(_)
| Self::QueueShuffle(_)
| Self::QueueSetShuffle(_, _)
| Self::QueueUnshuffle(_)
| Self::RemoveSong(_)
| Self::RemoveAlbum(_)
| Self::RemoveArtist(_)
| Self::SetSongDuration(_, _)
| Self::TagSongFlagSet(_, _)
| Self::TagSongFlagUnset(_, _)
| Self::TagAlbumFlagSet(_, _)
| Self::TagAlbumFlagUnset(_, _)
| Self::TagArtistFlagSet(_, _)
| Self::TagArtistFlagUnset(_, _)
| Self::TagSongPropertySet(_, _, _)
| Self::TagSongPropertyUnset(_, _)
| Self::TagAlbumPropertySet(_, _, _)
| Self::TagAlbumPropertyUnset(_, _)
| Self::TagArtistPropertySet(_, _, _)
| Self::TagArtistPropertyUnset(_, _)
| Self::InitComplete
| Self::Save
| Self::ErrorInfo(_, _) => None,
}
}
} }
/// Should be stored in the same lock as the database /// Should be stored in the same lock as the database
pub struct Commander { pub struct Commander {
seq: u8, seq: u8,
} }
pub struct Requester {
req: u8,
}
impl Commander { impl Commander {
pub fn new(ff: bool) -> Self { pub fn new(ff: bool) -> Self {
Self { Self {
@ -72,16 +134,43 @@ impl Commander {
self.seq self.seq
} }
} }
#[derive(Clone, Debug)] impl Requester {
pub fn new() -> Self {
Self { req: 0 }
}
pub fn inc(&mut self) -> Req {
if self.req < 0xFFu8 {
self.req += 1;
} else {
self.req = 1;
}
Req(self.req)
}
}
/// A request ID, or None
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Req(u8);
impl Req {
pub fn none() -> Self {
Self(0)
}
pub fn is_none(self) -> bool {
self.0 == 0
}
pub fn is_some(self) -> bool {
self.0 != 0
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Action { pub enum Action {
Resume, Resume,
Pause, Pause,
Stop, Stop,
NextSong, NextSong,
SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>), SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>),
QueueUpdate(Vec<usize>, Queue), QueueUpdate(Vec<usize>, Queue, Req),
QueueAdd(Vec<usize>, Vec<Queue>), QueueAdd(Vec<usize>, Vec<Queue>, Req),
QueueInsert(Vec<usize>, usize, Vec<Queue>), QueueInsert(Vec<usize>, usize, Vec<Queue>, Req),
QueueRemove(Vec<usize>), QueueRemove(Vec<usize>),
/// Move an element from A to B /// Move an element from A to B
QueueMove(Vec<usize>, Vec<usize>), QueueMove(Vec<usize>, Vec<usize>),
@ -95,18 +184,18 @@ pub enum Action {
QueueUnshuffle(Vec<usize>), QueueUnshuffle(Vec<usize>),
/// .id field is ignored! /// .id field is ignored!
AddSong(Song), AddSong(Song, Req),
/// .id field is ignored! /// .id field is ignored!
AddAlbum(Album), AddAlbum(Album, Req),
/// .id field is ignored! /// .id field is ignored!
AddArtist(Artist), AddArtist(Artist, Req),
AddCover(Cover), AddCover(Cover, Req),
ModifySong(Song), ModifySong(Song, Req),
ModifyAlbum(Album), ModifyAlbum(Album, Req),
ModifyArtist(Artist, Req),
RemoveSong(SongId), RemoveSong(SongId),
RemoveAlbum(AlbumId), RemoveAlbum(AlbumId),
RemoveArtist(ArtistId), RemoveArtist(ArtistId),
ModifyArtist(Artist),
SetSongDuration(SongId, u64), SetSongDuration(SongId, u64),
/// Add the given Tag to the song's tags, if it isn't set already. /// Add the given Tag to the song's tags, if it isn't set already.
TagSongFlagSet(SongId, String), TagSongFlagSet(SongId, String),
@ -129,21 +218,25 @@ pub enum Action {
InitComplete, InitComplete,
Save, Save,
ErrorInfo(String, String), ErrorInfo(String, String),
/// The server denied a request or an action.
/// Contains the Request ID that was rejected, if there was a request ID.
Denied(Req),
} }
impl Command { impl Command {
pub fn send_to_server(self, db: &Database) -> Result<(), Self> { pub fn send_to_server(self, db: &Database, client: Option<u64>) -> Result<(), Self> {
if let Some(sender) = &db.command_sender { if let Some(sender) = &db.command_sender {
sender.send(self).unwrap(); sender.send((self, client)).unwrap();
Ok(()) Ok(())
} else { } else {
Err(self) Err(self)
} }
} }
pub fn send_to_server_or_apply(self, db: &mut Database) { pub fn send_to_server_or_apply(self, db: &mut Database, client: Option<u64>) {
if let Some(sender) = &db.command_sender { if let Some(sender) = &db.command_sender {
sender.send(self).unwrap(); sender.send((self, client)).unwrap();
} else { } else {
db.apply_command(self); db.apply_command(self, client);
} }
} }
} }
@ -164,7 +257,7 @@ impl Command {
pub fn run_server( pub fn run_server(
database: Arc<Mutex<Database>>, database: Arc<Mutex<Database>>,
addr_tcp: Option<SocketAddr>, addr_tcp: Option<SocketAddr>,
sender_sender: Option<Box<dyn FnOnce(mpsc::Sender<Command>)>>, sender_sender: Option<Box<dyn FnOnce(mpsc::Sender<(Command, Option<u64>)>)>>,
play_audio: bool, play_audio: bool,
) { ) {
run_server_caching_thread_opt(database, addr_tcp, sender_sender, None, play_audio) run_server_caching_thread_opt(database, addr_tcp, sender_sender, None, play_audio)
@ -172,7 +265,7 @@ pub fn run_server(
pub fn run_server_caching_thread_opt( pub fn run_server_caching_thread_opt(
database: Arc<Mutex<Database>>, database: Arc<Mutex<Database>>,
addr_tcp: Option<SocketAddr>, addr_tcp: Option<SocketAddr>,
sender_sender: Option<Box<dyn FnOnce(mpsc::Sender<Command>)>>, sender_sender: Option<Box<dyn FnOnce(mpsc::Sender<(Command, Option<u64>)>)>>,
caching_thread: Option<Box<dyn FnOnce(&mut crate::data::cache_manager::CacheManager)>>, caching_thread: Option<Box<dyn FnOnce(&mut crate::data::cache_manager::CacheManager)>>,
play_audio: bool, play_audio: bool,
) { ) {
@ -251,6 +344,7 @@ pub fn run_server_caching_thread_opt(
"control" => handle_one_connection_as_control( "control" => handle_one_connection_as_control(
&mut connection, &mut connection,
&command_sender, &command_sender,
None,
), ),
"get" => _ = handle_one_connection_as_get(db, &mut connection), "get" => _ = handle_one_connection_as_get(db, &mut connection),
_ => { _ => {
@ -312,13 +406,13 @@ pub fn run_server_caching_thread_opt(
} }
} }
} }
if let Ok(command) = command_receiver.recv_timeout(dur) { if let Ok((command, client)) = command_receiver.recv_timeout(dur) {
checkf = true; checkf = true;
#[cfg(feature = "playback")] #[cfg(feature = "playback")]
if let Some(player) = &mut player { if let Some(player) = &mut player {
player.handle_action(&command.action); player.handle_action(&command.action);
} }
database.lock().unwrap().apply_command(command); database.lock().unwrap().apply_command(command, client);
} }
} }
} }
@ -327,30 +421,36 @@ pub fn handle_one_connection_as_main(
db: Arc<Mutex<Database>>, db: Arc<Mutex<Database>>,
connection: &mut impl Read, connection: &mut impl Read,
mut send_to: (impl Write + Sync + Send + 'static), mut send_to: (impl Write + Sync + Send + 'static),
command_sender: &mpsc::Sender<Command>, command_sender: &mpsc::Sender<(Command, Option<u64>)>,
) -> Result<(), std::io::Error> { ) -> Result<(), std::io::Error> {
// sync database // sync database
let mut db = db.lock().unwrap(); let mut db = db.lock().unwrap();
db.init_connection(&mut send_to)?; db.init_connection(&mut send_to)?;
// keep the client in sync: // keep the client in sync:
// the db will send all updates to the client once it is added to update_endpoints // the db will send all updates to the client once it is added to update_endpoints
db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new( let udepid = db.update_endpoints_id;
db.update_endpoints_id += 1;
db.update_endpoints.push((
udepid,
UpdateEndpoint::Bytes(Box::new(
// try_clone is used here to split a TcpStream into Writer and Reader // try_clone is used here to split a TcpStream into Writer and Reader
send_to, send_to,
))); )),
));
// drop the mutex lock // drop the mutex lock
drop(db); drop(db);
handle_one_connection_as_control(connection, command_sender); handle_one_connection_as_control(connection, command_sender, Some(udepid));
Ok(()) Ok(())
} }
pub fn handle_one_connection_as_control( pub fn handle_one_connection_as_control(
connection: &mut impl Read, connection: &mut impl Read,
command_sender: &mpsc::Sender<Command>, command_sender: &mpsc::Sender<(Command, Option<u64>)>,
client: Option<u64>,
) { ) {
// read updates from the tcp stream and send them to the database, exit on EOF or Err // read updates from the tcp stream and send them to the database, exit on EOF or Err
loop { loop {
if let Ok(command) = Command::from_bytes(connection) { if let Ok(command) = Command::from_bytes(connection) {
command_sender.send(command).unwrap(); command_sender.send((command, client)).unwrap();
} else { } else {
break; break;
} }
@ -375,6 +475,7 @@ const BYTE_INIT_COMPLETE: u8 = 0b01_010_000;
const BYTE_SET_SONG_DURATION: u8 = 0b01_010_001; const BYTE_SET_SONG_DURATION: u8 = 0b01_010_001;
const BYTE_SAVE: u8 = 0b01_010_010; const BYTE_SAVE: u8 = 0b01_010_010;
const BYTE_ERRORINFO: u8 = 0b01_100_010; const BYTE_ERRORINFO: u8 = 0b01_100_010;
const BYTE_DENIED: u8 = 0b01_100_011;
const BYTE_QUEUE_UPDATE: u8 = 0b10_000_000; const BYTE_QUEUE_UPDATE: u8 = 0b10_000_000;
const BYTE_QUEUE_ADD: u8 = 0b10_000_001; const BYTE_QUEUE_ADD: u8 = 0b10_000_001;
@ -430,6 +531,21 @@ impl ToFromBytes for Command {
} }
} }
impl ToFromBytes for Req {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.0.to_bytes(s)?;
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(Self(ToFromBytes::from_bytes(s)?))
}
}
// impl ToFromBytes for Action { // impl ToFromBytes for Action {
impl Action { impl Action {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
@ -447,21 +563,24 @@ impl Action {
b.to_bytes(s)?; b.to_bytes(s)?;
c.to_bytes(s)?; c.to_bytes(s)?;
} }
Self::QueueUpdate(index, new_data) => { Self::QueueUpdate(index, new_data, req) => {
s.write_all(&[BYTE_QUEUE_UPDATE])?; s.write_all(&[BYTE_QUEUE_UPDATE])?;
index.to_bytes(s)?; index.to_bytes(s)?;
new_data.to_bytes(s)?; new_data.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::QueueAdd(index, new_data) => { Self::QueueAdd(index, new_data, req) => {
s.write_all(&[BYTE_QUEUE_ADD])?; s.write_all(&[BYTE_QUEUE_ADD])?;
index.to_bytes(s)?; index.to_bytes(s)?;
new_data.to_bytes(s)?; new_data.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::QueueInsert(index, pos, new_data) => { Self::QueueInsert(index, pos, new_data, req) => {
s.write_all(&[BYTE_QUEUE_INSERT])?; s.write_all(&[BYTE_QUEUE_INSERT])?;
index.to_bytes(s)?; index.to_bytes(s)?;
pos.to_bytes(s)?; pos.to_bytes(s)?;
new_data.to_bytes(s)?; new_data.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::QueueRemove(index) => { Self::QueueRemove(index) => {
s.write_all(&[BYTE_QUEUE_REMOVE])?; s.write_all(&[BYTE_QUEUE_REMOVE])?;
@ -497,40 +616,47 @@ impl Action {
s.write_all(&[SUBBYTE_ACTION_UNSHUFFLE])?; s.write_all(&[SUBBYTE_ACTION_UNSHUFFLE])?;
path.to_bytes(s)?; path.to_bytes(s)?;
} }
Self::AddSong(song) => { Self::AddSong(song, req) => {
s.write_all(&[BYTE_LIB_ADD])?; s.write_all(&[BYTE_LIB_ADD])?;
s.write_all(&[SUBBYTE_SONG])?; s.write_all(&[SUBBYTE_SONG])?;
song.to_bytes(s)?; song.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::AddAlbum(album) => { Self::AddAlbum(album, req) => {
s.write_all(&[BYTE_LIB_ADD])?; s.write_all(&[BYTE_LIB_ADD])?;
s.write_all(&[SUBBYTE_ALBUM])?; s.write_all(&[SUBBYTE_ALBUM])?;
album.to_bytes(s)?; album.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::AddArtist(artist) => { Self::AddArtist(artist, req) => {
s.write_all(&[BYTE_LIB_ADD])?; s.write_all(&[BYTE_LIB_ADD])?;
s.write_all(&[SUBBYTE_ARTIST])?; s.write_all(&[SUBBYTE_ARTIST])?;
artist.to_bytes(s)?; artist.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::AddCover(cover) => { Self::AddCover(cover, req) => {
s.write_all(&[BYTE_LIB_ADD])?; s.write_all(&[BYTE_LIB_ADD])?;
s.write_all(&[SUBBYTE_COVER])?; s.write_all(&[SUBBYTE_COVER])?;
cover.to_bytes(s)?; cover.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::ModifySong(song) => { Self::ModifySong(song, req) => {
s.write_all(&[BYTE_LIB_MODIFY])?; s.write_all(&[BYTE_LIB_MODIFY])?;
s.write_all(&[SUBBYTE_SONG])?; s.write_all(&[SUBBYTE_SONG])?;
song.to_bytes(s)?; song.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::ModifyAlbum(album) => { Self::ModifyAlbum(album, req) => {
s.write_all(&[BYTE_LIB_MODIFY])?; s.write_all(&[BYTE_LIB_MODIFY])?;
s.write_all(&[SUBBYTE_ALBUM])?; s.write_all(&[SUBBYTE_ALBUM])?;
album.to_bytes(s)?; album.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::ModifyArtist(artist) => { Self::ModifyArtist(artist, req) => {
s.write_all(&[BYTE_LIB_MODIFY])?; s.write_all(&[BYTE_LIB_MODIFY])?;
s.write_all(&[SUBBYTE_ARTIST])?; s.write_all(&[SUBBYTE_ARTIST])?;
artist.to_bytes(s)?; artist.to_bytes(s)?;
req.to_bytes(s)?;
} }
Self::RemoveSong(song) => { Self::RemoveSong(song) => {
s.write_all(&[BYTE_LIB_REMOVE])?; s.write_all(&[BYTE_LIB_REMOVE])?;
@ -636,6 +762,10 @@ impl Action {
t.to_bytes(s)?; t.to_bytes(s)?;
d.to_bytes(s)?; d.to_bytes(s)?;
} }
Self::Denied(req) => {
s.write_all(&[BYTE_DENIED])?;
req.to_bytes(s)?;
}
} }
Ok(()) Ok(())
} }
@ -654,9 +784,11 @@ impl Action {
BYTE_STOP => Self::Stop, BYTE_STOP => Self::Stop,
BYTE_NEXT_SONG => Self::NextSong, BYTE_NEXT_SONG => Self::NextSong,
BYTE_SYNC_DATABASE => Self::SyncDatabase(from_bytes!(), from_bytes!(), from_bytes!()), BYTE_SYNC_DATABASE => Self::SyncDatabase(from_bytes!(), from_bytes!(), from_bytes!()),
BYTE_QUEUE_UPDATE => Self::QueueUpdate(from_bytes!(), from_bytes!()), BYTE_QUEUE_UPDATE => Self::QueueUpdate(from_bytes!(), from_bytes!(), from_bytes!()),
BYTE_QUEUE_ADD => Self::QueueAdd(from_bytes!(), from_bytes!()), BYTE_QUEUE_ADD => Self::QueueAdd(from_bytes!(), from_bytes!(), from_bytes!()),
BYTE_QUEUE_INSERT => Self::QueueInsert(from_bytes!(), from_bytes!(), from_bytes!()), BYTE_QUEUE_INSERT => {
Self::QueueInsert(from_bytes!(), from_bytes!(), from_bytes!(), from_bytes!())
}
BYTE_QUEUE_REMOVE => Self::QueueRemove(from_bytes!()), BYTE_QUEUE_REMOVE => Self::QueueRemove(from_bytes!()),
BYTE_QUEUE_MOVE => Self::QueueMove(from_bytes!(), from_bytes!()), BYTE_QUEUE_MOVE => Self::QueueMove(from_bytes!(), from_bytes!()),
BYTE_QUEUE_MOVE_INTO => Self::QueueMoveInto(from_bytes!(), from_bytes!()), BYTE_QUEUE_MOVE_INTO => Self::QueueMoveInto(from_bytes!(), from_bytes!()),
@ -674,10 +806,10 @@ impl Action {
} }
}, },
BYTE_LIB_ADD => match s.read_byte()? { BYTE_LIB_ADD => match s.read_byte()? {
SUBBYTE_SONG => Self::AddSong(from_bytes!()), SUBBYTE_SONG => Self::AddSong(from_bytes!(), from_bytes!()),
SUBBYTE_ALBUM => Self::AddAlbum(from_bytes!()), SUBBYTE_ALBUM => Self::AddAlbum(from_bytes!(), from_bytes!()),
SUBBYTE_ARTIST => Self::AddArtist(from_bytes!()), SUBBYTE_ARTIST => Self::AddArtist(from_bytes!(), from_bytes!()),
SUBBYTE_COVER => Self::AddCover(from_bytes!()), SUBBYTE_COVER => Self::AddCover(from_bytes!(), from_bytes!()),
_ => { _ => {
eprintln!( eprintln!(
"[{}] unexpected byte when reading command:libAdd; stopping playback.", "[{}] unexpected byte when reading command:libAdd; stopping playback.",
@ -687,9 +819,9 @@ impl Action {
} }
}, },
BYTE_LIB_MODIFY => match s.read_byte()? { BYTE_LIB_MODIFY => match s.read_byte()? {
SUBBYTE_SONG => Self::ModifySong(from_bytes!()), SUBBYTE_SONG => Self::ModifySong(from_bytes!(), from_bytes!()),
SUBBYTE_ALBUM => Self::ModifyAlbum(from_bytes!()), SUBBYTE_ALBUM => Self::ModifyAlbum(from_bytes!(), from_bytes!()),
SUBBYTE_ARTIST => Self::ModifyArtist(from_bytes!()), SUBBYTE_ARTIST => Self::ModifyArtist(from_bytes!(), from_bytes!()),
_ => { _ => {
eprintln!( eprintln!(
"[{}] unexpected byte when reading command:libModify; stopping playback.", "[{}] unexpected byte when reading command:libModify; stopping playback.",
@ -751,6 +883,7 @@ impl Action {
BYTE_INIT_COMPLETE => Self::InitComplete, BYTE_INIT_COMPLETE => Self::InitComplete,
BYTE_SAVE => Self::Save, BYTE_SAVE => Self::Save,
BYTE_ERRORINFO => Self::ErrorInfo(from_bytes!(), from_bytes!()), BYTE_ERRORINFO => Self::ErrorInfo(from_bytes!(), from_bytes!()),
BYTE_DENIED => Self::Denied(from_bytes!()),
_ => { _ => {
eprintln!( eprintln!(
"[{}] unexpected byte when reading command; stopping playback.", "[{}] unexpected byte when reading command; stopping playback.",
@ -772,3 +905,61 @@ impl<T: Read> ReadByte for T {
Ok(b[0]) Ok(b[0])
} }
} }
#[test]
fn test_to_from_bytes() {
use crate::data::queue::QueueContent;
use std::io::Cursor;
for v in [
Action::Resume,
Action::Pause,
Action::Stop,
Action::NextSong,
Action::SyncDatabase(vec![], vec![], vec![]),
Action::QueueUpdate(vec![], QueueContent::Song(12).into(), Req::none()),
Action::QueueAdd(vec![], vec![], Req::none()),
Action::QueueInsert(vec![], 5, vec![], Req::none()),
Action::QueueRemove(vec![]),
Action::QueueMove(vec![], vec![]),
Action::QueueMoveInto(vec![], vec![]),
Action::QueueGoto(vec![]),
Action::QueueShuffle(vec![]),
Action::QueueSetShuffle(vec![], vec![]),
Action::QueueUnshuffle(vec![]),
// Action::AddSong(Song, Req),
// Action::AddAlbum(Album, Req),
// Action::AddArtist(Artist, Req),
// Action::AddCover(Cover, Req),
// Action::ModifySong(Song, Req),
// Action::ModifyAlbum(Album, Req),
// Action::ModifyArtist(Artist, Req),
// Action::RemoveSong(SongId),
// Action::RemoveAlbum(AlbumId),
// Action::RemoveArtist(ArtistId),
// Action::SetSongDuration(SongId, u64),
// Action::TagSongFlagSet(SongId, String),
// Action::TagSongFlagUnset(SongId, String),
// Action::TagAlbumFlagSet(AlbumId, String),
// Action::TagAlbumFlagUnset(AlbumId, String),
// Action::TagArtistFlagSet(ArtistId, String),
// Action::TagArtistFlagUnset(ArtistId, String),
// Action::TagSongPropertySet(SongId, String, String),
// Action::TagSongPropertyUnset(SongId, String),
// Action::TagAlbumPropertySet(AlbumId, String, String),
// Action::TagAlbumPropertyUnset(AlbumId, String),
// Action::TagArtistPropertySet(ArtistId, String, String),
// Action::TagArtistPropertyUnset(ArtistId, String),
Action::InitComplete,
Action::Save,
Action::ErrorInfo(format!("some error"), format!("with a message")),
Action::Denied(Req::none()),
] {
let v = v.cmd(0xFF);
assert_eq!(
v.action,
Command::from_bytes(&mut Cursor::new(v.to_bytes_vec()))
.unwrap()
.action
);
}
}

View File

@ -1,12 +1,12 @@
# compile for aarch64 linux # # compile for aarch64 linux
[build] [build]
pre-build = [ pre-build = [
"dpkg --add-architecture $CROSS_DEB_ARCH", "dpkg --add-architecture $CROSS_DEB_ARCH",
"apt-get update && apt-get --assume-yes install libasound2-dev libasound2-dev:$CROSS_DEB_ARCH" "apt-get update && apt-get --assume-yes install --force musl-dev libasound2-dev"
] ]
default-target = "aarch64-unknown-linux-gnu" default-target = "x86_64-unknown-linux-musl"
# # compile for aarch64 android # compile for aarch64 android
# [build] # [build]
# pre-build = [ # pre-build = [
# "dpkg --add-architecture $CROSS_DEB_ARCH", # "dpkg --add-architecture $CROSS_DEB_ARCH",

View File

@ -151,11 +151,11 @@ fn main() {
let cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap(); let cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap();
use musicdb_lib::server::Action::*; use musicdb_lib::server::Action::*;
match &cmd.action { match &cmd.action {
// ignore playback and queue commands // ignore playback and queue commands, and denials
Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..) Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..)
| QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..) | QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..)
| QueueGoto(..) | QueueShuffle(..) | QueueSetShuffle(..) | QueueGoto(..) | QueueShuffle(..) | QueueSetShuffle(..)
| QueueUnshuffle(..) => continue, | QueueUnshuffle(..) | Denied(..) => continue,
SyncDatabase(..) SyncDatabase(..)
| AddSong(..) | AddSong(..)
| AddAlbum(..) | AddAlbum(..)
@ -184,7 +184,7 @@ fn main() {
| Save | Save
| ErrorInfo(..) => (), | ErrorInfo(..) => (),
} }
database.lock().unwrap().apply_command(cmd); database.lock().unwrap().apply_command(cmd, None);
} }
}); });
} }

View File

@ -7,7 +7,7 @@ use musicdb_lib::data::database::Database;
use musicdb_lib::data::queue::{Queue, QueueContent, QueueFolder}; use musicdb_lib::data::queue::{Queue, QueueContent, QueueFolder};
use musicdb_lib::data::song::Song; use musicdb_lib::data::song::Song;
use musicdb_lib::data::SongId; use musicdb_lib::data::SongId;
use musicdb_lib::server::{Action, Command}; use musicdb_lib::server::{Action, Command, Req};
use rocket::response::content::RawHtml; use rocket::response::content::RawHtml;
use rocket::{get, routes, Config, State}; use rocket::{get, routes, Config, State};
@ -40,7 +40,7 @@ const HTML_END: &'static str = "</body></html>";
struct Data { struct Data {
db: Arc<Mutex<Database>>, db: Arc<Mutex<Database>>,
command_sender: mpsc::Sender<Command>, command_sender: mpsc::Sender<(Command, Option<u64>)>,
} }
#[get("/")] #[get("/")]
@ -258,7 +258,7 @@ fn gen_queue_html_impl(
fn queue_remove(data: &State<Data>, path: &str) { fn queue_remove(data: &State<Data>, path: &str) {
if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() { if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() {
data.command_sender data.command_sender
.send(Action::QueueRemove(path).cmd(0xFFu8)) .send((Action::QueueRemove(path).cmd(0xFFu8), None))
.unwrap(); .unwrap();
} }
} }
@ -266,7 +266,7 @@ fn queue_remove(data: &State<Data>, path: &str) {
fn queue_goto(data: &State<Data>, path: &str) { fn queue_goto(data: &State<Data>, path: &str) {
if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() { if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() {
data.command_sender data.command_sender
.send(Action::QueueGoto(path).cmd(0xFFu8)) .send((Action::QueueGoto(path).cmd(0xFFu8), None))
.unwrap(); .unwrap();
} }
} }
@ -274,27 +274,31 @@ fn queue_goto(data: &State<Data>, path: &str) {
#[get("/play")] #[get("/play")]
fn play(data: &State<Data>) { fn play(data: &State<Data>) {
data.command_sender data.command_sender
.send(Action::Resume.cmd(0xFFu8)) .send((Action::Resume.cmd(0xFFu8), None))
.unwrap(); .unwrap();
} }
#[get("/pause")] #[get("/pause")]
fn pause(data: &State<Data>) { fn pause(data: &State<Data>) {
data.command_sender.send(Action::Pause.cmd(0xFFu8)).unwrap(); data.command_sender
.send((Action::Pause.cmd(0xFFu8), None))
.unwrap();
} }
#[get("/stop")] #[get("/stop")]
fn stop(data: &State<Data>) { fn stop(data: &State<Data>) {
data.command_sender.send(Action::Stop.cmd(0xFFu8)).unwrap(); data.command_sender
.send((Action::Stop.cmd(0xFFu8), None))
.unwrap();
} }
#[get("/skip")] #[get("/skip")]
fn skip(data: &State<Data>) { fn skip(data: &State<Data>) {
data.command_sender data.command_sender
.send(Action::NextSong.cmd(0xFFu8)) .send((Action::NextSong.cmd(0xFFu8), None))
.unwrap(); .unwrap();
} }
#[get("/clear-queue")] #[get("/clear-queue")]
fn clear_queue(data: &State<Data>) { fn clear_queue(data: &State<Data>) {
data.command_sender data.command_sender
.send( .send((
Action::QueueUpdate( Action::QueueUpdate(
vec![], vec![],
QueueContent::Folder(QueueFolder { QueueContent::Folder(QueueFolder {
@ -304,16 +308,21 @@ fn clear_queue(data: &State<Data>) {
order: None, order: None,
}) })
.into(), .into(),
Req::none(),
) )
.cmd(0xFFu8), .cmd(0xFFu8),
) None,
))
.unwrap(); .unwrap();
} }
#[get("/add-song/<id>")] #[get("/add-song/<id>")]
fn add_song(data: &State<Data>, id: SongId) { fn add_song(data: &State<Data>, id: SongId) {
data.command_sender data.command_sender
.send(Action::QueueAdd(vec![], vec![QueueContent::Song(id).into()]).cmd(0xFFu8)) .send((
Action::QueueAdd(vec![], vec![QueueContent::Song(id).into()], Req::none()).cmd(0xFFu8),
None,
))
.unwrap(); .unwrap();
} }
@ -538,7 +547,7 @@ fn search(
pub async fn main( pub async fn main(
db: Arc<Mutex<Database>>, db: Arc<Mutex<Database>>,
command_sender: mpsc::Sender<Command>, command_sender: mpsc::Sender<(Command, Option<u64>)>,
addr: SocketAddr, addr: SocketAddr,
) { ) {
rocket::build() rocket::build()