mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 05:43:53 +01:00
aaaaa
This commit is contained in:
parent
85c79a21e1
commit
d3a1facba0
@ -12,7 +12,7 @@ directories = "5.0.1"
|
||||
regex = "1.9.3"
|
||||
speedy2d = { version = "1.12.0", optional = true }
|
||||
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"
|
||||
|
||||
[features]
|
||||
@ -26,8 +26,8 @@ default = ["gui", "default-playback"]
|
||||
# playback:
|
||||
# enables syncplayer modes, where the client mirrors the server's playback
|
||||
gui = ["speedy2d"]
|
||||
merscfg = ["mers", "gui"]
|
||||
mers = ["musicdb-mers"]
|
||||
# merscfg = ["mers", "gui"]
|
||||
# mers = ["musicdb-mers"]
|
||||
playback = []
|
||||
default-playback = ["playback", "musicdb-lib/default-playback"]
|
||||
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]
|
||||
|
@ -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]
|
||||
pre-build = [
|
||||
"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"
|
||||
|
@ -362,90 +362,124 @@ impl Gui {
|
||||
}
|
||||
Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"),
|
||||
}
|
||||
database.lock().unwrap().update_endpoints.push(
|
||||
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| {
|
||||
match &cmd.action {
|
||||
Action::Resume
|
||||
| Action::Pause
|
||||
| Action::Stop
|
||||
| Action::Save
|
||||
| Action::InitComplete => {}
|
||||
Action::NextSong
|
||||
| Action::QueueUpdate(..)
|
||||
| Action::QueueAdd(..)
|
||||
| Action::QueueInsert(..)
|
||||
| Action::QueueRemove(..)
|
||||
| Action::QueueMove(..)
|
||||
| Action::QueueMoveInto(..)
|
||||
| Action::QueueGoto(..)
|
||||
| Action::QueueShuffle(..)
|
||||
| Action::QueueSetShuffle(..)
|
||||
| Action::QueueUnshuffle(..) => {
|
||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||
_ = s.send_event(GuiEvent::UpdatedQueue);
|
||||
{
|
||||
let mut db = database.lock().unwrap();
|
||||
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::Pause
|
||||
| Action::Stop
|
||||
| Action::Save
|
||||
| Action::InitComplete => {}
|
||||
Action::NextSong
|
||||
| Action::QueueUpdate(..)
|
||||
| Action::QueueAdd(..)
|
||||
| Action::QueueInsert(..)
|
||||
| Action::QueueRemove(..)
|
||||
| Action::QueueMove(..)
|
||||
| Action::QueueMoveInto(..)
|
||||
| Action::QueueGoto(..)
|
||||
| Action::QueueShuffle(..)
|
||||
| Action::QueueSetShuffle(..)
|
||||
| Action::QueueUnshuffle(..) => {
|
||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||
_ = s.send_event(GuiEvent::UpdatedQueue);
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::SyncDatabase(..)
|
||||
| Action::AddSong(_)
|
||||
| Action::AddAlbum(_)
|
||||
| Action::AddArtist(_)
|
||||
| Action::AddCover(_)
|
||||
| Action::ModifySong(_)
|
||||
| Action::ModifyAlbum(_)
|
||||
| Action::ModifyArtist(_)
|
||||
| Action::RemoveSong(_)
|
||||
| Action::RemoveAlbum(_)
|
||||
| Action::RemoveArtist(_)
|
||||
| Action::TagSongFlagSet(..)
|
||||
| Action::TagSongFlagUnset(..)
|
||||
| Action::TagAlbumFlagSet(..)
|
||||
| Action::TagAlbumFlagUnset(..)
|
||||
| Action::TagArtistFlagSet(..)
|
||||
| Action::TagArtistFlagUnset(..)
|
||||
| Action::TagSongPropertySet(..)
|
||||
| Action::TagSongPropertyUnset(..)
|
||||
| Action::TagAlbumPropertySet(..)
|
||||
| Action::TagAlbumPropertyUnset(..)
|
||||
| Action::TagArtistPropertySet(..)
|
||||
| Action::TagArtistPropertyUnset(..)
|
||||
| Action::SetSongDuration(..) => {
|
||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||
_ = s.send_event(GuiEvent::UpdatedLibrary);
|
||||
Action::SyncDatabase(..)
|
||||
| Action::AddSong(_, _)
|
||||
| Action::AddAlbum(_, _)
|
||||
| Action::AddArtist(_, _)
|
||||
| Action::AddCover(_, _)
|
||||
| Action::ModifySong(_, _)
|
||||
| Action::ModifyAlbum(_, _)
|
||||
| Action::ModifyArtist(_, _)
|
||||
| Action::RemoveSong(_)
|
||||
| Action::RemoveAlbum(_)
|
||||
| Action::RemoveArtist(_)
|
||||
| Action::TagSongFlagSet(..)
|
||||
| Action::TagSongFlagUnset(..)
|
||||
| Action::TagAlbumFlagSet(..)
|
||||
| Action::TagAlbumFlagUnset(..)
|
||||
| Action::TagArtistFlagSet(..)
|
||||
| Action::TagArtistFlagUnset(..)
|
||||
| Action::TagSongPropertySet(..)
|
||||
| Action::TagSongPropertyUnset(..)
|
||||
| Action::TagAlbumPropertySet(..)
|
||||
| Action::TagAlbumPropertyUnset(..)
|
||||
| Action::TagArtistPropertySet(..)
|
||||
| Action::TagArtistPropertyUnset(..)
|
||||
| Action::SetSongDuration(..) => {
|
||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||
_ = s.send_event(GuiEvent::UpdatedLibrary);
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::ErrorInfo(t, d) => {
|
||||
let (t, d) = (t.clone(), d.clone());
|
||||
notif_sender_two
|
||||
.send(Box::new(move |_| {
|
||||
(
|
||||
Box::new(Panel::with_background(
|
||||
GuiElemCfg::default(),
|
||||
[Label::new(
|
||||
Action::ErrorInfo(t, d) => {
|
||||
let (t, d) = (t.clone(), d.clone());
|
||||
notif_sender_two
|
||||
.send(Box::new(move |_| {
|
||||
(
|
||||
Box::new(Panel::with_background(
|
||||
GuiElemCfg::default(),
|
||||
if t.is_empty() {
|
||||
format!("Server message\n{d}")
|
||||
} else {
|
||||
format!("Server error ({t})\n{d}")
|
||||
},
|
||||
Color::WHITE,
|
||||
None,
|
||||
Vec2::new(0.5, 0.5),
|
||||
)],
|
||||
Color::from_rgba(0.0, 0.0, 0.0, 0.8),
|
||||
)),
|
||||
if t.is_empty() {
|
||||
NotifInfo::new(Duration::from_secs(2))
|
||||
} else {
|
||||
NotifInfo::new(Duration::from_secs(5))
|
||||
.with_highlight(Color::RED)
|
||||
},
|
||||
)
|
||||
}))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
})),
|
||||
);
|
||||
[Label::new(
|
||||
GuiElemCfg::default(),
|
||||
if t.is_empty() {
|
||||
format!("Server message\n{d}")
|
||||
} else {
|
||||
format!("Server error ({t})\n{d}")
|
||||
},
|
||||
Color::WHITE,
|
||||
None,
|
||||
Vec2::new(0.5, 0.5),
|
||||
)],
|
||||
Color::from_rgba(0.0, 0.0, 0.0, 0.8),
|
||||
)),
|
||||
if t.is_empty() {
|
||||
NotifInfo::new(Duration::from_secs(2))
|
||||
} else {
|
||||
NotifInfo::new(Duration::from_secs(5))
|
||||
.with_highlight(Color::RED)
|
||||
},
|
||||
)
|
||||
}))
|
||||
.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;
|
||||
Gui {
|
||||
event_sender,
|
||||
@ -1307,16 +1341,10 @@ impl Gui {
|
||||
}
|
||||
}
|
||||
GuiAction::SendToServer(action) => {
|
||||
let command = self.database.lock().unwrap().seq.pack(action);
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("[DEBUG] Sending command to server: {action:?}");
|
||||
if let Err(e) = self
|
||||
.database
|
||||
.lock()
|
||||
.unwrap()
|
||||
.seq
|
||||
.pack(action)
|
||||
.to_bytes(&mut self.connection)
|
||||
{
|
||||
eprintln!("[DEBUG] Sending command to server: {command:?}");
|
||||
if let Err(e) = command.to_bytes(&mut self.connection) {
|
||||
eprintln!("Error sending command to server: {e}");
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::time::Instant;
|
||||
|
||||
use musicdb_lib::{
|
||||
data::{song::Song, ArtistId},
|
||||
server::Action,
|
||||
server::{Action, Req},
|
||||
};
|
||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
|
||||
|
||||
@ -189,7 +189,10 @@ impl GuiElem for EditorForSongs {
|
||||
song.album = None;
|
||||
}
|
||||
info.actions
|
||||
.push(GuiAction::SendToServer(Action::ModifySong(song)));
|
||||
.push(GuiAction::SendToServer(Action::ModifySong(
|
||||
song,
|
||||
Req::none(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
Event::SetArtist(name, id) => {
|
||||
|
@ -5,7 +5,7 @@ use musicdb_lib::{
|
||||
song::Song,
|
||||
AlbumId, ArtistId,
|
||||
},
|
||||
server::Action,
|
||||
server::{Action, Req},
|
||||
};
|
||||
use speedy2d::{
|
||||
color::Color,
|
||||
@ -404,7 +404,7 @@ impl GuiElem for QueueEmptySpaceDragHandler {
|
||||
dragged_add_to_queue(
|
||||
dragged,
|
||||
(),
|
||||
|_, q| Action::QueueAdd(vec![], q),
|
||||
|_, q| Action::QueueAdd(vec![], q, Req::none()),
|
||||
|_, q| Action::QueueMoveInto(q, vec![]),
|
||||
)
|
||||
}
|
||||
@ -631,9 +631,9 @@ impl GuiElem for QueueSong {
|
||||
self.path.clone(),
|
||||
move |mut p: Vec<usize>, q| {
|
||||
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 {
|
||||
Action::QueueAdd(p, q)
|
||||
Action::QueueAdd(p, q, Req::none())
|
||||
}
|
||||
},
|
||||
move |mut p, q| {
|
||||
@ -826,7 +826,7 @@ impl GuiElem for QueueFolder {
|
||||
dragged_add_to_queue(
|
||||
dragged,
|
||||
self.path.clone(),
|
||||
|p, q| Action::QueueAdd(p, q),
|
||||
|p, q| Action::QueueAdd(p, q, Req::none()),
|
||||
|p, q| Action::QueueMoveInto(q, p),
|
||||
)
|
||||
} else {
|
||||
@ -835,7 +835,7 @@ impl GuiElem for QueueFolder {
|
||||
self.path.clone(),
|
||||
|mut p, q| {
|
||||
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),
|
||||
)
|
||||
@ -903,7 +903,7 @@ impl GuiElem for QueueIndentEnd {
|
||||
dragged_add_to_queue(
|
||||
dragged,
|
||||
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| {
|
||||
p.push(j);
|
||||
Action::QueueMove(q, p)
|
||||
@ -1066,7 +1066,7 @@ impl GuiElem for QueueLoop {
|
||||
dragged_add_to_queue(
|
||||
dragged,
|
||||
p,
|
||||
|p, q| Action::QueueAdd(p, q),
|
||||
|p, q| Action::QueueAdd(p, q, Req::none()),
|
||||
|p, q| Action::QueueMoveInto(q, p),
|
||||
)
|
||||
} else {
|
||||
|
@ -2,7 +2,7 @@ use std::time::Instant;
|
||||
|
||||
use musicdb_lib::{
|
||||
data::queue::{QueueContent, QueueFolder},
|
||||
server::Action,
|
||||
server::{Action, Req},
|
||||
};
|
||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D};
|
||||
use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator};
|
||||
@ -126,6 +126,7 @@ impl GuiScreen {
|
||||
musicdb_lib::data::queue::QueueFolder::default(),
|
||||
)
|
||||
.into(),
|
||||
Req::none(),
|
||||
))]
|
||||
},
|
||||
[Label::new(
|
||||
|
@ -203,7 +203,7 @@ fn main() {
|
||||
action(action);
|
||||
break 'feature_if;
|
||||
}
|
||||
db.apply_action_unchecked_seq(action);
|
||||
db.apply_action_unchecked_seq(action, None);
|
||||
}
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(player) = &mut player {
|
||||
|
@ -4,7 +4,7 @@ use crate::load::ToFromBytes;
|
||||
|
||||
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Album {
|
||||
pub id: AlbumId,
|
||||
pub name: String,
|
||||
|
@ -4,7 +4,7 @@ use crate::load::ToFromBytes;
|
||||
|
||||
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Artist {
|
||||
pub id: ArtistId,
|
||||
pub name: String,
|
||||
|
@ -13,7 +13,7 @@ use rand::thread_rng;
|
||||
|
||||
use crate::{
|
||||
load::ToFromBytes,
|
||||
server::{Action, Command, Commander},
|
||||
server::{Action, Command, Commander, Req},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -45,10 +45,11 @@ pub struct Database {
|
||||
pub queue: Queue,
|
||||
/// 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.
|
||||
pub update_endpoints: Vec<UpdateEndpoint>,
|
||||
pub update_endpoints: Vec<(u64, UpdateEndpoint)>,
|
||||
pub update_endpoints_id: u64,
|
||||
/// true if a song is/should be playing
|
||||
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:
|
||||
Option<Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>>,
|
||||
/// only relevant for clients. true if init is done
|
||||
@ -65,7 +66,7 @@ pub enum UpdateEndpoint {
|
||||
Bytes(Box<dyn Write + Sync + Send>),
|
||||
CmdChannel(mpsc::Sender<Arc<Command>>),
|
||||
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>),
|
||||
}
|
||||
|
||||
@ -507,7 +508,7 @@ impl Database {
|
||||
))
|
||||
.to_bytes(con)?;
|
||||
self.seq
|
||||
.pack(Action::QueueUpdate(vec![], self.queue.clone()))
|
||||
.pack(Action::QueueUpdate(vec![], self.queue.clone(), Req::none()))
|
||||
.to_bytes(con)?;
|
||||
if self.playing {
|
||||
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`
|
||||
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 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!(
|
||||
"Invalid sequence number: got {} but expected {}.",
|
||||
command.seq,
|
||||
@ -530,9 +552,9 @@ impl Database {
|
||||
);
|
||||
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 let Action::ErrorInfo(t, _) = &mut action {
|
||||
// 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::Resume if self.playing => (),
|
||||
// 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 {
|
||||
Action::Resume => self.playing = true,
|
||||
@ -557,7 +579,7 @@ impl Database {
|
||||
Action::NextSong => {
|
||||
if !Queue::advance_index_db(self) {
|
||||
// end of queue
|
||||
self.apply_action_unchecked_seq(Action::Pause);
|
||||
self.apply_action_unchecked_seq(Action::Pause, client);
|
||||
self.queue.init();
|
||||
}
|
||||
}
|
||||
@ -567,17 +589,17 @@ impl Database {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
*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) {
|
||||
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) {
|
||||
v.insert(new_data, pos, false);
|
||||
}
|
||||
@ -662,7 +684,7 @@ impl Database {
|
||||
{
|
||||
let mut ord: Vec<usize> = (0..content.len()).collect();
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
Action::AddAlbum(album) => {
|
||||
Action::AddAlbum(album, _) => {
|
||||
self.add_album_new(album);
|
||||
}
|
||||
Action::AddArtist(artist) => {
|
||||
Action::AddArtist(artist, _) => {
|
||||
self.add_artist_new(artist);
|
||||
}
|
||||
Action::AddCover(cover) => _ = self.add_cover_new(cover),
|
||||
Action::ModifySong(song) => {
|
||||
Action::AddCover(cover, _) => _ = self.add_cover_new(cover),
|
||||
Action::ModifySong(song, _) => {
|
||||
_ = self.update_song(song);
|
||||
}
|
||||
Action::ModifyAlbum(album) => {
|
||||
Action::ModifyAlbum(album, _) => {
|
||||
_ = self.update_album(album);
|
||||
}
|
||||
Action::ModifyArtist(artist) => {
|
||||
Action::ModifyArtist(artist, _) => {
|
||||
_ = self.update_artist(artist);
|
||||
}
|
||||
Action::RemoveSong(song) => {
|
||||
@ -846,6 +868,7 @@ impl Database {
|
||||
self.client_is_init = true;
|
||||
}
|
||||
Action::ErrorInfo(..) => {}
|
||||
Action::Denied(..) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -875,6 +898,7 @@ impl Database {
|
||||
custom_files: None,
|
||||
queue: QueueContent::Folder(QueueFolder::default()).into(),
|
||||
update_endpoints: vec![],
|
||||
update_endpoints_id: 0,
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
remote_server_as_song_file_source: None,
|
||||
@ -896,6 +920,7 @@ impl Database {
|
||||
custom_files: None,
|
||||
queue: QueueContent::Folder(QueueFolder::default()).into(),
|
||||
update_endpoints: vec![],
|
||||
update_endpoints_id: 0,
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
remote_server_as_song_file_source: None,
|
||||
@ -922,6 +947,7 @@ impl Database {
|
||||
custom_files: None,
|
||||
queue: QueueContent::Folder(QueueFolder::default()).into(),
|
||||
update_endpoints: vec![],
|
||||
update_endpoints_id: 0,
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
remote_server_as_song_file_source: None,
|
||||
@ -973,7 +999,7 @@ impl Database {
|
||||
self.times_data_modified = None;
|
||||
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 {
|
||||
Action::InitComplete => return update,
|
||||
_ => {}
|
||||
@ -981,11 +1007,36 @@ impl Database {
|
||||
if !self.is_client() {
|
||||
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 bytes = 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 {
|
||||
UpdateEndpoint::Bytes(writer) => {
|
||||
if bytes.is_none() {
|
||||
@ -1008,7 +1059,7 @@ impl Database {
|
||||
if arc.is_none() {
|
||||
arc = Some(Arc::new(update.clone()));
|
||||
}
|
||||
func(arc.as_ref().unwrap())
|
||||
func(Arc::clone(arc.as_ref().unwrap()))
|
||||
}
|
||||
UpdateEndpoint::CustomBytes(func) => {
|
||||
if bytes.is_none() {
|
||||
@ -1028,6 +1079,9 @@ impl Database {
|
||||
self.update_endpoints.remove(i);
|
||||
}
|
||||
}
|
||||
if let Some(req) = req {
|
||||
update.action.put_req(req);
|
||||
}
|
||||
update.action
|
||||
}
|
||||
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 data: Arc<Mutex<(bool, Option<(Instant, Vec<u8>)>)>>,
|
||||
}
|
||||
impl PartialEq for Cover {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.location == other.location
|
||||
}
|
||||
}
|
||||
impl Cover {
|
||||
pub fn get_bytes_from_file<O>(
|
||||
&self,
|
||||
|
@ -17,13 +17,13 @@ pub type AlbumId = u64;
|
||||
pub type ArtistId = u64;
|
||||
pub type CoverId = u64;
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
#[derive(Clone, Default, Debug, PartialEq)]
|
||||
/// general data for songs, albums and artists
|
||||
pub struct GeneralData {
|
||||
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
|
||||
pub struct DatabaseLocation {
|
||||
pub rel_path: PathBuf,
|
||||
|
@ -4,18 +4,18 @@ use crate::load::ToFromBytes;
|
||||
|
||||
use super::{database::Database, SongId};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Queue {
|
||||
enabled: bool,
|
||||
content: QueueContent,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum QueueContent {
|
||||
Song(SongId),
|
||||
Folder(QueueFolder),
|
||||
Loop(usize, usize, Box<Queue>),
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct QueueFolder {
|
||||
pub index: usize,
|
||||
pub content: Vec<Queue>,
|
||||
|
@ -17,7 +17,7 @@ use super::{
|
||||
AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Song {
|
||||
pub id: SongId,
|
||||
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 {
|
||||
fn clone(&self) -> Self {
|
||||
Self(Arc::clone(&self.0))
|
||||
|
@ -126,7 +126,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
pub fn update_uncache_opt(&mut self, db: &mut Database, allow_uncaching: bool) {
|
||||
if self.allow_sending_commands {
|
||||
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);
|
||||
if self.allow_sending_commands && load_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) {
|
||||
@ -169,21 +169,27 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
self.backend.next(db.playing, load_duration);
|
||||
if self.allow_sending_commands && load_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 {
|
||||
// only show an error if the user tries to play the song.
|
||||
// otherwise, the error might be spammed.
|
||||
if self.allow_sending_commands && db.playing {
|
||||
db.apply_action_unchecked_seq(Action::ErrorInfo(
|
||||
format!("Couldn't load bytes for song {id}"),
|
||||
format!(
|
||||
"Song: {}\nby {:?} on {:?}",
|
||||
song.title, song.artist, song.album
|
||||
db.apply_action_unchecked_seq(
|
||||
Action::ErrorInfo(
|
||||
format!("Couldn't load bytes for song {id}"),
|
||||
format!(
|
||||
"Song: {}\nby {:?} on {:?}",
|
||||
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();
|
||||
}
|
||||
|
@ -13,12 +13,12 @@ pub struct PlayerBackendPlaybackRs<T> {
|
||||
player: playback_rs::Player,
|
||||
current: 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> {
|
||||
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>> {
|
||||
Self::new_with_optional_command_sending(Some(command_sender))
|
||||
}
|
||||
@ -26,7 +26,7 @@ impl<T> PlayerBackendPlaybackRs<T> {
|
||||
Self::new_with_optional_command_sending(None)
|
||||
}
|
||||
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>> {
|
||||
Ok(Self {
|
||||
player: playback_rs::Player::new(None)?,
|
||||
@ -55,13 +55,14 @@ impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
if let Some(s) = &self.command_sender {
|
||||
s.send(
|
||||
s.send((
|
||||
Action::ErrorInfo(
|
||||
format!("Couldn't decode song #{id}!"),
|
||||
format!("Error: {e}"),
|
||||
)
|
||||
.cmd(0xFFu8),
|
||||
)
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
None
|
||||
@ -101,21 +102,22 @@ impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> {
|
||||
if let Some(song) = song {
|
||||
if let Err(e) = self.player.play_song_now(song, None) {
|
||||
if let Some(s) = &self.command_sender {
|
||||
s.send(
|
||||
s.send((
|
||||
Action::ErrorInfo(
|
||||
format!("Couldn't play song #{id}!"),
|
||||
format!("Error: {e}"),
|
||||
)
|
||||
.cmd(0xFFu8),
|
||||
)
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
s.send(Action::NextSong.cmd(0xFFu8)).unwrap();
|
||||
s.send((Action::NextSong.cmd(0xFFu8), None)).unwrap();
|
||||
}
|
||||
} else {
|
||||
self.player.set_playing(play);
|
||||
}
|
||||
} else if let Some(s) = &self.command_sender {
|
||||
s.send(Action::NextSong.cmd(0xFFu8)).unwrap();
|
||||
s.send((Action::NextSong.cmd(0xFFu8), None)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ pub struct PlayerBackendRodio<T> {
|
||||
stopped: bool,
|
||||
current: Option<(SongId, Arc<Vec<u8>>, Option<u128>, 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> {
|
||||
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>> {
|
||||
Self::new_with_optional_command_sending(Some(command_sender))
|
||||
}
|
||||
@ -32,7 +32,7 @@ impl<T> PlayerBackendRodio<T> {
|
||||
Self::new_with_optional_command_sending(None)
|
||||
}
|
||||
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>> {
|
||||
let (output_stream, output_stream_handle) = rodio::OutputStream::try_default()?;
|
||||
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));
|
||||
if let Err(e) = &decoder {
|
||||
if let Some(s) = &self.command_sender {
|
||||
s.send(
|
||||
s.send((
|
||||
Action::ErrorInfo(
|
||||
format!("Couldn't decode song #{id}!"),
|
||||
format!("Error: '{e}'"),
|
||||
)
|
||||
.cmd(0xFFu8),
|
||||
)
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -43,11 +43,73 @@ impl Action {
|
||||
pub fn cmd(self, seq: u8) -> Command {
|
||||
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
|
||||
pub struct Commander {
|
||||
seq: u8,
|
||||
}
|
||||
pub struct Requester {
|
||||
req: u8,
|
||||
}
|
||||
impl Commander {
|
||||
pub fn new(ff: bool) -> Self {
|
||||
Self {
|
||||
@ -72,16 +134,43 @@ impl Commander {
|
||||
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 {
|
||||
Resume,
|
||||
Pause,
|
||||
Stop,
|
||||
NextSong,
|
||||
SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>),
|
||||
QueueUpdate(Vec<usize>, Queue),
|
||||
QueueAdd(Vec<usize>, Vec<Queue>),
|
||||
QueueInsert(Vec<usize>, usize, Vec<Queue>),
|
||||
QueueUpdate(Vec<usize>, Queue, Req),
|
||||
QueueAdd(Vec<usize>, Vec<Queue>, Req),
|
||||
QueueInsert(Vec<usize>, usize, Vec<Queue>, Req),
|
||||
QueueRemove(Vec<usize>),
|
||||
/// Move an element from A to B
|
||||
QueueMove(Vec<usize>, Vec<usize>),
|
||||
@ -95,18 +184,18 @@ pub enum Action {
|
||||
QueueUnshuffle(Vec<usize>),
|
||||
|
||||
/// .id field is ignored!
|
||||
AddSong(Song),
|
||||
AddSong(Song, Req),
|
||||
/// .id field is ignored!
|
||||
AddAlbum(Album),
|
||||
AddAlbum(Album, Req),
|
||||
/// .id field is ignored!
|
||||
AddArtist(Artist),
|
||||
AddCover(Cover),
|
||||
ModifySong(Song),
|
||||
ModifyAlbum(Album),
|
||||
AddArtist(Artist, Req),
|
||||
AddCover(Cover, Req),
|
||||
ModifySong(Song, Req),
|
||||
ModifyAlbum(Album, Req),
|
||||
ModifyArtist(Artist, Req),
|
||||
RemoveSong(SongId),
|
||||
RemoveAlbum(AlbumId),
|
||||
RemoveArtist(ArtistId),
|
||||
ModifyArtist(Artist),
|
||||
SetSongDuration(SongId, u64),
|
||||
/// Add the given Tag to the song's tags, if it isn't set already.
|
||||
TagSongFlagSet(SongId, String),
|
||||
@ -129,21 +218,25 @@ pub enum Action {
|
||||
InitComplete,
|
||||
Save,
|
||||
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 {
|
||||
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 {
|
||||
sender.send(self).unwrap();
|
||||
sender.send((self, client)).unwrap();
|
||||
Ok(())
|
||||
} else {
|
||||
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 {
|
||||
sender.send(self).unwrap();
|
||||
sender.send((self, client)).unwrap();
|
||||
} else {
|
||||
db.apply_command(self);
|
||||
db.apply_command(self, client);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -164,7 +257,7 @@ impl Command {
|
||||
pub fn run_server(
|
||||
database: Arc<Mutex<Database>>,
|
||||
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,
|
||||
) {
|
||||
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(
|
||||
database: Arc<Mutex<Database>>,
|
||||
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)>>,
|
||||
play_audio: bool,
|
||||
) {
|
||||
@ -251,6 +344,7 @@ pub fn run_server_caching_thread_opt(
|
||||
"control" => handle_one_connection_as_control(
|
||||
&mut connection,
|
||||
&command_sender,
|
||||
None,
|
||||
),
|
||||
"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;
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(player) = &mut player {
|
||||
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>>,
|
||||
connection: &mut impl Read,
|
||||
mut send_to: (impl Write + Sync + Send + 'static),
|
||||
command_sender: &mpsc::Sender<Command>,
|
||||
command_sender: &mpsc::Sender<(Command, Option<u64>)>,
|
||||
) -> 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,
|
||||
)));
|
||||
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
|
||||
send_to,
|
||||
)),
|
||||
));
|
||||
// drop the mutex lock
|
||||
drop(db);
|
||||
handle_one_connection_as_control(connection, command_sender);
|
||||
handle_one_connection_as_control(connection, command_sender, Some(udepid));
|
||||
Ok(())
|
||||
}
|
||||
pub fn handle_one_connection_as_control(
|
||||
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
|
||||
loop {
|
||||
if let Ok(command) = Command::from_bytes(connection) {
|
||||
command_sender.send(command).unwrap();
|
||||
command_sender.send((command, client)).unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@ -375,6 +475,7 @@ const BYTE_INIT_COMPLETE: u8 = 0b01_010_000;
|
||||
const BYTE_SET_SONG_DURATION: u8 = 0b01_010_001;
|
||||
const BYTE_SAVE: u8 = 0b01_010_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_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 Action {
|
||||
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||
@ -447,21 +563,24 @@ impl Action {
|
||||
b.to_bytes(s)?;
|
||||
c.to_bytes(s)?;
|
||||
}
|
||||
Self::QueueUpdate(index, new_data) => {
|
||||
Self::QueueUpdate(index, new_data, req) => {
|
||||
s.write_all(&[BYTE_QUEUE_UPDATE])?;
|
||||
index.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])?;
|
||||
index.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])?;
|
||||
index.to_bytes(s)?;
|
||||
pos.to_bytes(s)?;
|
||||
new_data.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::QueueRemove(index) => {
|
||||
s.write_all(&[BYTE_QUEUE_REMOVE])?;
|
||||
@ -497,40 +616,47 @@ impl Action {
|
||||
s.write_all(&[SUBBYTE_ACTION_UNSHUFFLE])?;
|
||||
path.to_bytes(s)?;
|
||||
}
|
||||
Self::AddSong(song) => {
|
||||
Self::AddSong(song, req) => {
|
||||
s.write_all(&[BYTE_LIB_ADD])?;
|
||||
s.write_all(&[SUBBYTE_SONG])?;
|
||||
song.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::AddAlbum(album) => {
|
||||
Self::AddAlbum(album, req) => {
|
||||
s.write_all(&[BYTE_LIB_ADD])?;
|
||||
s.write_all(&[SUBBYTE_ALBUM])?;
|
||||
album.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::AddArtist(artist) => {
|
||||
Self::AddArtist(artist, req) => {
|
||||
s.write_all(&[BYTE_LIB_ADD])?;
|
||||
s.write_all(&[SUBBYTE_ARTIST])?;
|
||||
artist.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::AddCover(cover) => {
|
||||
Self::AddCover(cover, req) => {
|
||||
s.write_all(&[BYTE_LIB_ADD])?;
|
||||
s.write_all(&[SUBBYTE_COVER])?;
|
||||
cover.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::ModifySong(song) => {
|
||||
Self::ModifySong(song, req) => {
|
||||
s.write_all(&[BYTE_LIB_MODIFY])?;
|
||||
s.write_all(&[SUBBYTE_SONG])?;
|
||||
song.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::ModifyAlbum(album) => {
|
||||
Self::ModifyAlbum(album, req) => {
|
||||
s.write_all(&[BYTE_LIB_MODIFY])?;
|
||||
s.write_all(&[SUBBYTE_ALBUM])?;
|
||||
album.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::ModifyArtist(artist) => {
|
||||
Self::ModifyArtist(artist, req) => {
|
||||
s.write_all(&[BYTE_LIB_MODIFY])?;
|
||||
s.write_all(&[SUBBYTE_ARTIST])?;
|
||||
artist.to_bytes(s)?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
Self::RemoveSong(song) => {
|
||||
s.write_all(&[BYTE_LIB_REMOVE])?;
|
||||
@ -636,6 +762,10 @@ impl Action {
|
||||
t.to_bytes(s)?;
|
||||
d.to_bytes(s)?;
|
||||
}
|
||||
Self::Denied(req) => {
|
||||
s.write_all(&[BYTE_DENIED])?;
|
||||
req.to_bytes(s)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -654,9 +784,11 @@ impl Action {
|
||||
BYTE_STOP => Self::Stop,
|
||||
BYTE_NEXT_SONG => Self::NextSong,
|
||||
BYTE_SYNC_DATABASE => Self::SyncDatabase(from_bytes!(), from_bytes!(), from_bytes!()),
|
||||
BYTE_QUEUE_UPDATE => Self::QueueUpdate(from_bytes!(), from_bytes!()),
|
||||
BYTE_QUEUE_ADD => Self::QueueAdd(from_bytes!(), from_bytes!()),
|
||||
BYTE_QUEUE_INSERT => Self::QueueInsert(from_bytes!(), from_bytes!(), from_bytes!()),
|
||||
BYTE_QUEUE_UPDATE => Self::QueueUpdate(from_bytes!(), 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!(), from_bytes!())
|
||||
}
|
||||
BYTE_QUEUE_REMOVE => Self::QueueRemove(from_bytes!()),
|
||||
BYTE_QUEUE_MOVE => Self::QueueMove(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()? {
|
||||
SUBBYTE_SONG => Self::AddSong(from_bytes!()),
|
||||
SUBBYTE_ALBUM => Self::AddAlbum(from_bytes!()),
|
||||
SUBBYTE_ARTIST => Self::AddArtist(from_bytes!()),
|
||||
SUBBYTE_COVER => Self::AddCover(from_bytes!()),
|
||||
SUBBYTE_SONG => Self::AddSong(from_bytes!(), from_bytes!()),
|
||||
SUBBYTE_ALBUM => Self::AddAlbum(from_bytes!(), from_bytes!()),
|
||||
SUBBYTE_ARTIST => Self::AddArtist(from_bytes!(), from_bytes!()),
|
||||
SUBBYTE_COVER => Self::AddCover(from_bytes!(), from_bytes!()),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"[{}] unexpected byte when reading command:libAdd; stopping playback.",
|
||||
@ -687,9 +819,9 @@ impl Action {
|
||||
}
|
||||
},
|
||||
BYTE_LIB_MODIFY => match s.read_byte()? {
|
||||
SUBBYTE_SONG => Self::ModifySong(from_bytes!()),
|
||||
SUBBYTE_ALBUM => Self::ModifyAlbum(from_bytes!()),
|
||||
SUBBYTE_ARTIST => Self::ModifyArtist(from_bytes!()),
|
||||
SUBBYTE_SONG => Self::ModifySong(from_bytes!(), from_bytes!()),
|
||||
SUBBYTE_ALBUM => Self::ModifyAlbum(from_bytes!(), from_bytes!()),
|
||||
SUBBYTE_ARTIST => Self::ModifyArtist(from_bytes!(), from_bytes!()),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"[{}] unexpected byte when reading command:libModify; stopping playback.",
|
||||
@ -751,6 +883,7 @@ impl Action {
|
||||
BYTE_INIT_COMPLETE => Self::InitComplete,
|
||||
BYTE_SAVE => Self::Save,
|
||||
BYTE_ERRORINFO => Self::ErrorInfo(from_bytes!(), from_bytes!()),
|
||||
BYTE_DENIED => Self::Denied(from_bytes!()),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"[{}] unexpected byte when reading command; stopping playback.",
|
||||
@ -772,3 +905,61 @@ impl<T: Read> ReadByte for T {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
# compile for aarch64 linux
|
||||
# # compile for aarch64 linux
|
||||
[build]
|
||||
pre-build = [
|
||||
"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]
|
||||
# pre-build = [
|
||||
# "dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||
|
@ -151,11 +151,11 @@ fn main() {
|
||||
let cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap();
|
||||
use musicdb_lib::server::Action::*;
|
||||
match &cmd.action {
|
||||
// ignore playback and queue commands
|
||||
// ignore playback and queue commands, and denials
|
||||
Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..)
|
||||
| QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..)
|
||||
| QueueGoto(..) | QueueShuffle(..) | QueueSetShuffle(..)
|
||||
| QueueUnshuffle(..) => continue,
|
||||
| QueueUnshuffle(..) | Denied(..) => continue,
|
||||
SyncDatabase(..)
|
||||
| AddSong(..)
|
||||
| AddAlbum(..)
|
||||
@ -184,7 +184,7 @@ fn main() {
|
||||
| Save
|
||||
| ErrorInfo(..) => (),
|
||||
}
|
||||
database.lock().unwrap().apply_command(cmd);
|
||||
database.lock().unwrap().apply_command(cmd, None);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use musicdb_lib::data::database::Database;
|
||||
use musicdb_lib::data::queue::{Queue, QueueContent, QueueFolder};
|
||||
use musicdb_lib::data::song::Song;
|
||||
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::{get, routes, Config, State};
|
||||
|
||||
@ -40,7 +40,7 @@ const HTML_END: &'static str = "</body></html>";
|
||||
|
||||
struct Data {
|
||||
db: Arc<Mutex<Database>>,
|
||||
command_sender: mpsc::Sender<Command>,
|
||||
command_sender: mpsc::Sender<(Command, Option<u64>)>,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
@ -258,7 +258,7 @@ fn gen_queue_html_impl(
|
||||
fn queue_remove(data: &State<Data>, path: &str) {
|
||||
if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() {
|
||||
data.command_sender
|
||||
.send(Action::QueueRemove(path).cmd(0xFFu8))
|
||||
.send((Action::QueueRemove(path).cmd(0xFFu8), None))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@ -266,7 +266,7 @@ fn queue_remove(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() {
|
||||
data.command_sender
|
||||
.send(Action::QueueGoto(path).cmd(0xFFu8))
|
||||
.send((Action::QueueGoto(path).cmd(0xFFu8), None))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@ -274,27 +274,31 @@ fn queue_goto(data: &State<Data>, path: &str) {
|
||||
#[get("/play")]
|
||||
fn play(data: &State<Data>) {
|
||||
data.command_sender
|
||||
.send(Action::Resume.cmd(0xFFu8))
|
||||
.send((Action::Resume.cmd(0xFFu8), None))
|
||||
.unwrap();
|
||||
}
|
||||
#[get("/pause")]
|
||||
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")]
|
||||
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")]
|
||||
fn skip(data: &State<Data>) {
|
||||
data.command_sender
|
||||
.send(Action::NextSong.cmd(0xFFu8))
|
||||
.send((Action::NextSong.cmd(0xFFu8), None))
|
||||
.unwrap();
|
||||
}
|
||||
#[get("/clear-queue")]
|
||||
fn clear_queue(data: &State<Data>) {
|
||||
data.command_sender
|
||||
.send(
|
||||
.send((
|
||||
Action::QueueUpdate(
|
||||
vec![],
|
||||
QueueContent::Folder(QueueFolder {
|
||||
@ -304,16 +308,21 @@ fn clear_queue(data: &State<Data>) {
|
||||
order: None,
|
||||
})
|
||||
.into(),
|
||||
Req::none(),
|
||||
)
|
||||
.cmd(0xFFu8),
|
||||
)
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[get("/add-song/<id>")]
|
||||
fn add_song(data: &State<Data>, id: SongId) {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -538,7 +547,7 @@ fn search(
|
||||
|
||||
pub async fn main(
|
||||
db: Arc<Mutex<Database>>,
|
||||
command_sender: mpsc::Sender<Command>,
|
||||
command_sender: mpsc::Sender<(Command, Option<u64>)>,
|
||||
addr: SocketAddr,
|
||||
) {
|
||||
rocket::build()
|
||||
|
Loading…
Reference in New Issue
Block a user