mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 14:13:53 +01:00
sequence numbers
This commit is contained in:
parent
d02646406d
commit
c3622aca30
@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
musicdb-lib = { path = "../musicdb-lib" }
|
musicdb-lib = { path = "../musicdb-lib", default-features = false }
|
||||||
clap = { version = "4.4.6", features = ["derive"] }
|
clap = { version = "4.4.6", features = ["derive"] }
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
regex = "1.9.3"
|
regex = "1.9.3"
|
||||||
@ -16,7 +16,7 @@ musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true }
|
|||||||
uianimator = "0.1.1"
|
uianimator = "0.1.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["gui", "playback", "merscfg"]
|
default = ["gui", "default-playback"]
|
||||||
# gui:
|
# gui:
|
||||||
# enables the gui modes
|
# enables the gui modes
|
||||||
# merscfg:
|
# merscfg:
|
||||||
@ -26,6 +26,9 @@ default = ["gui", "playback", "merscfg"]
|
|||||||
# 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", "speedy2d"]
|
merscfg = ["mers", "gui"]
|
||||||
mers = ["musicdb-mers"]
|
mers = ["musicdb-mers"]
|
||||||
playback = ["musicdb-lib/playback"]
|
playback = []
|
||||||
|
default-playback = ["playback", "musicdb-lib/default-playback"]
|
||||||
|
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]
|
||||||
|
playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"]
|
||||||
|
@ -16,7 +16,7 @@ use musicdb_lib::{
|
|||||||
AlbumId, ArtistId, CoverId, SongId,
|
AlbumId, ArtistId, CoverId, SongId,
|
||||||
},
|
},
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
server::{get, Command},
|
server::{get, Action},
|
||||||
};
|
};
|
||||||
use speedy2d::{
|
use speedy2d::{
|
||||||
color::Color,
|
color::Color,
|
||||||
@ -363,56 +363,57 @@ impl Gui {
|
|||||||
Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"),
|
Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"),
|
||||||
}
|
}
|
||||||
database.lock().unwrap().update_endpoints.push(
|
database.lock().unwrap().update_endpoints.push(
|
||||||
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
|
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| {
|
||||||
Command::Resume
|
match &cmd.action {
|
||||||
| Command::Pause
|
Action::Resume
|
||||||
| Command::Stop
|
| Action::Pause
|
||||||
| Command::Save
|
| Action::Stop
|
||||||
| Command::InitComplete => {}
|
| Action::Save
|
||||||
Command::NextSong
|
| Action::InitComplete => {}
|
||||||
| Command::QueueUpdate(..)
|
Action::NextSong
|
||||||
| Command::QueueAdd(..)
|
| Action::QueueUpdate(..)
|
||||||
| Command::QueueInsert(..)
|
| Action::QueueAdd(..)
|
||||||
| Command::QueueRemove(..)
|
| Action::QueueInsert(..)
|
||||||
| Command::QueueMove(..)
|
| Action::QueueRemove(..)
|
||||||
| Command::QueueMoveInto(..)
|
| Action::QueueMove(..)
|
||||||
| Command::QueueGoto(..)
|
| Action::QueueMoveInto(..)
|
||||||
| Command::QueueShuffle(..)
|
| Action::QueueGoto(..)
|
||||||
| Command::QueueSetShuffle(..)
|
| Action::QueueShuffle(..)
|
||||||
| Command::QueueUnshuffle(..) => {
|
| Action::QueueSetShuffle(..)
|
||||||
|
| Action::QueueUnshuffle(..) => {
|
||||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||||
_ = s.send_event(GuiEvent::UpdatedQueue);
|
_ = s.send_event(GuiEvent::UpdatedQueue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::SyncDatabase(..)
|
Action::SyncDatabase(..)
|
||||||
| Command::AddSong(_)
|
| Action::AddSong(_)
|
||||||
| Command::AddAlbum(_)
|
| Action::AddAlbum(_)
|
||||||
| Command::AddArtist(_)
|
| Action::AddArtist(_)
|
||||||
| Command::AddCover(_)
|
| Action::AddCover(_)
|
||||||
| Command::ModifySong(_)
|
| Action::ModifySong(_)
|
||||||
| Command::ModifyAlbum(_)
|
| Action::ModifyAlbum(_)
|
||||||
| Command::ModifyArtist(_)
|
| Action::ModifyArtist(_)
|
||||||
| Command::RemoveSong(_)
|
| Action::RemoveSong(_)
|
||||||
| Command::RemoveAlbum(_)
|
| Action::RemoveAlbum(_)
|
||||||
| Command::RemoveArtist(_)
|
| Action::RemoveArtist(_)
|
||||||
| Command::TagSongFlagSet(..)
|
| Action::TagSongFlagSet(..)
|
||||||
| Command::TagSongFlagUnset(..)
|
| Action::TagSongFlagUnset(..)
|
||||||
| Command::TagAlbumFlagSet(..)
|
| Action::TagAlbumFlagSet(..)
|
||||||
| Command::TagAlbumFlagUnset(..)
|
| Action::TagAlbumFlagUnset(..)
|
||||||
| Command::TagArtistFlagSet(..)
|
| Action::TagArtistFlagSet(..)
|
||||||
| Command::TagArtistFlagUnset(..)
|
| Action::TagArtistFlagUnset(..)
|
||||||
| Command::TagSongPropertySet(..)
|
| Action::TagSongPropertySet(..)
|
||||||
| Command::TagSongPropertyUnset(..)
|
| Action::TagSongPropertyUnset(..)
|
||||||
| Command::TagAlbumPropertySet(..)
|
| Action::TagAlbumPropertySet(..)
|
||||||
| Command::TagAlbumPropertyUnset(..)
|
| Action::TagAlbumPropertyUnset(..)
|
||||||
| Command::TagArtistPropertySet(..)
|
| Action::TagArtistPropertySet(..)
|
||||||
| Command::TagArtistPropertyUnset(..)
|
| Action::TagArtistPropertyUnset(..)
|
||||||
| Command::SetSongDuration(..) => {
|
| Action::SetSongDuration(..) => {
|
||||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||||
_ = s.send_event(GuiEvent::UpdatedLibrary);
|
_ = s.send_event(GuiEvent::UpdatedLibrary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::ErrorInfo(t, d) => {
|
Action::ErrorInfo(t, d) => {
|
||||||
let (t, d) = (t.clone(), d.clone());
|
let (t, d) = (t.clone(), d.clone());
|
||||||
notif_sender_two
|
notif_sender_two
|
||||||
.send(Box::new(move |_| {
|
.send(Box::new(move |_| {
|
||||||
@ -442,6 +443,7 @@ impl Gui {
|
|||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
let no_animations = false;
|
let no_animations = false;
|
||||||
@ -1191,7 +1193,7 @@ pub enum GuiAction {
|
|||||||
ShowNotification(Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>),
|
ShowNotification(Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>),
|
||||||
/// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc)
|
/// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc)
|
||||||
Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>),
|
Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>),
|
||||||
SendToServer(Command),
|
SendToServer(Action),
|
||||||
ContextMenu(Option<(Vec<Box<dyn GuiElem>>)>),
|
ContextMenu(Option<(Vec<Box<dyn GuiElem>>)>),
|
||||||
/// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one.
|
/// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one.
|
||||||
ResetKeyboardFocus,
|
ResetKeyboardFocus,
|
||||||
@ -1304,10 +1306,17 @@ impl Gui {
|
|||||||
self.keybinds.insert(bind, action.with_priority(priority));
|
self.keybinds.insert(bind, action.with_priority(priority));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GuiAction::SendToServer(cmd) => {
|
GuiAction::SendToServer(action) => {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
eprintln!("[DEBUG] Sending command to server: {cmd:?}");
|
eprintln!("[DEBUG] Sending command to server: {action:?}");
|
||||||
if let Err(e) = cmd.to_bytes(&mut self.connection) {
|
if let Err(e) = self
|
||||||
|
.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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1551,7 +1560,7 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
| Dragging::Queue(Ok(_))
|
| Dragging::Queue(Ok(_))
|
||||||
| Dragging::Queues(_) => (),
|
| Dragging::Queues(_) => (),
|
||||||
Dragging::Queue(Err(path)) => {
|
Dragging::Queue(Err(path)) => {
|
||||||
self.exec_gui_action(GuiAction::SendToServer(Command::QueueRemove(path)))
|
self.exec_gui_action(GuiAction::SendToServer(Action::QueueRemove(path)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::{song::Song, ArtistId},
|
data::{song::Song, ArtistId},
|
||||||
server::Command,
|
server::Action,
|
||||||
};
|
};
|
||||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
|
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ impl GuiElem for EditorForSongs {
|
|||||||
song.album = None;
|
song.album = None;
|
||||||
}
|
}
|
||||||
info.actions
|
info.actions
|
||||||
.push(GuiAction::SendToServer(Command::ModifySong(song)));
|
.push(GuiAction::SendToServer(Action::ModifySong(song)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::SetArtist(name, id) => {
|
Event::SetArtist(name, id) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::sync::{atomic::AtomicBool, Arc};
|
||||||
|
|
||||||
use musicdb_lib::server::Command;
|
use musicdb_lib::server::Action;
|
||||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
|
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -28,9 +28,9 @@ impl PlayPause {
|
|||||||
if let Some(song) = db.get_song(song_id) {
|
if let Some(song) = db.get_song(song_id) {
|
||||||
vec![GuiAction::SendToServer(
|
vec![GuiAction::SendToServer(
|
||||||
if song.general.tags.iter().any(|v| v == "Fav") {
|
if song.general.tags.iter().any(|v| v == "Fav") {
|
||||||
Command::TagSongFlagUnset(*song_id, "Fav".to_owned())
|
Action::TagSongFlagUnset(*song_id, "Fav".to_owned())
|
||||||
} else {
|
} else {
|
||||||
Command::TagSongFlagSet(*song_id, "Fav".to_owned())
|
Action::TagSongFlagSet(*song_id, "Fav".to_owned())
|
||||||
},
|
},
|
||||||
)]
|
)]
|
||||||
} else {
|
} else {
|
||||||
@ -48,7 +48,7 @@ impl PlayPause {
|
|||||||
),
|
),
|
||||||
to_zero: Button::new(
|
to_zero: Button::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.26, 0.01), (0.49, 0.99))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.26, 0.01), (0.49, 0.99))),
|
||||||
|_| vec![GuiAction::SendToServer(Command::Stop)],
|
|_| vec![GuiAction::SendToServer(Action::Stop)],
|
||||||
[Panel::with_background(
|
[Panel::with_background(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.2, 0.2), (0.8, 0.8))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.2, 0.2), (0.8, 0.8))),
|
||||||
(),
|
(),
|
||||||
@ -59,9 +59,9 @@ impl PlayPause {
|
|||||||
GuiElemCfg::at(Rectangle::from_tuples((0.51, 0.01), (0.74, 0.99))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.51, 0.01), (0.74, 0.99))),
|
||||||
|btn| {
|
|btn| {
|
||||||
vec![GuiAction::SendToServer(if btn.children[0].is_playing {
|
vec![GuiAction::SendToServer(if btn.children[0].is_playing {
|
||||||
Command::Pause
|
Action::Pause
|
||||||
} else {
|
} else {
|
||||||
Command::Resume
|
Action::Resume
|
||||||
})]
|
})]
|
||||||
},
|
},
|
||||||
[PlayPauseDisplay::new(GuiElemCfg::at(
|
[PlayPauseDisplay::new(GuiElemCfg::at(
|
||||||
@ -70,7 +70,7 @@ impl PlayPause {
|
|||||||
),
|
),
|
||||||
to_end: Button::new(
|
to_end: Button::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.76, 0.01), (0.99, 0.99))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.76, 0.01), (0.99, 0.99))),
|
||||||
|_| vec![GuiAction::SendToServer(Command::NextSong)],
|
|_| vec![GuiAction::SendToServer(Action::NextSong)],
|
||||||
[NextSongShape::new(GuiElemCfg::at(Rectangle::from_tuples(
|
[NextSongShape::new(GuiElemCfg::at(Rectangle::from_tuples(
|
||||||
(0.2, 0.2),
|
(0.2, 0.2),
|
||||||
(0.8, 0.8),
|
(0.8, 0.8),
|
||||||
|
@ -5,7 +5,7 @@ use musicdb_lib::{
|
|||||||
song::Song,
|
song::Song,
|
||||||
AlbumId, ArtistId,
|
AlbumId, ArtistId,
|
||||||
},
|
},
|
||||||
server::Command,
|
server::Action,
|
||||||
};
|
};
|
||||||
use speedy2d::{
|
use speedy2d::{
|
||||||
color::Color,
|
color::Color,
|
||||||
@ -404,8 +404,8 @@ impl GuiElem for QueueEmptySpaceDragHandler {
|
|||||||
dragged_add_to_queue(
|
dragged_add_to_queue(
|
||||||
dragged,
|
dragged,
|
||||||
(),
|
(),
|
||||||
|_, q| Command::QueueAdd(vec![], q),
|
|_, q| Action::QueueAdd(vec![], q),
|
||||||
|_, q| Command::QueueMoveInto(q, vec![]),
|
|_, q| Action::QueueMoveInto(q, vec![]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -563,7 +563,7 @@ impl GuiElem for QueueSong {
|
|||||||
if self.mouse && button == MouseButton::Left {
|
if self.mouse && button == MouseButton::Left {
|
||||||
self.mouse = false;
|
self.mouse = false;
|
||||||
if e.take() && !self.always_copy {
|
if e.take() && !self.always_copy {
|
||||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
vec![GuiAction::SendToServer(Action::QueueGoto(
|
||||||
self.path.clone(),
|
self.path.clone(),
|
||||||
))]
|
))]
|
||||||
} else {
|
} else {
|
||||||
@ -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() {
|
||||||
Command::QueueInsert(p, if insert_below { j + 1 } else { j }, q)
|
Action::QueueInsert(p, if insert_below { j + 1 } else { j }, q)
|
||||||
} else {
|
} else {
|
||||||
Command::QueueAdd(p, q)
|
Action::QueueAdd(p, q)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
move |mut p, q| {
|
move |mut p, q| {
|
||||||
@ -642,7 +642,7 @@ impl GuiElem for QueueSong {
|
|||||||
*l += 1;
|
*l += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::QueueMove(q, p)
|
Action::QueueMove(q, p)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -787,9 +787,9 @@ impl GuiElem for QueueFolder {
|
|||||||
// Panel::with_background(GuiElemCfg::default(), (), Color::DARK_GRAY),
|
// Panel::with_background(GuiElemCfg::default(), (), Color::DARK_GRAY),
|
||||||
// )]))];
|
// )]))];
|
||||||
return vec![GuiAction::SendToServer(if self.queue.order.is_some() {
|
return vec![GuiAction::SendToServer(if self.queue.order.is_some() {
|
||||||
Command::QueueUnshuffle(self.path.clone())
|
Action::QueueUnshuffle(self.path.clone())
|
||||||
} else {
|
} else {
|
||||||
Command::QueueShuffle(self.path.clone())
|
Action::QueueShuffle(self.path.clone())
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
vec![]
|
vec![]
|
||||||
@ -798,7 +798,7 @@ impl GuiElem for QueueFolder {
|
|||||||
if self.mouse && button == MouseButton::Left {
|
if self.mouse && button == MouseButton::Left {
|
||||||
self.mouse = false;
|
self.mouse = false;
|
||||||
if e.take() && !self.always_copy {
|
if e.take() && !self.always_copy {
|
||||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
vec![GuiAction::SendToServer(Action::QueueGoto(
|
||||||
self.path.clone(),
|
self.path.clone(),
|
||||||
))]
|
))]
|
||||||
} else {
|
} else {
|
||||||
@ -826,8 +826,8 @@ impl GuiElem for QueueFolder {
|
|||||||
dragged_add_to_queue(
|
dragged_add_to_queue(
|
||||||
dragged,
|
dragged,
|
||||||
self.path.clone(),
|
self.path.clone(),
|
||||||
|p, q| Command::QueueAdd(p, q),
|
|p, q| Action::QueueAdd(p, q),
|
||||||
|p, q| Command::QueueMoveInto(q, p),
|
|p, q| Action::QueueMoveInto(q, p),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
dragged_add_to_queue(
|
dragged_add_to_queue(
|
||||||
@ -835,9 +835,9 @@ 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);
|
||||||
Command::QueueInsert(p, j, q)
|
Action::QueueInsert(p, j, q)
|
||||||
},
|
},
|
||||||
|p, q| Command::QueueMove(q, p),
|
|p, q| Action::QueueMove(q, p),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -903,10 +903,10 @@ 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| Command::QueueInsert(p, j, q),
|
|(p, j), q| Action::QueueInsert(p, j, q),
|
||||||
|(mut p, j), q| {
|
|(mut p, j), q| {
|
||||||
p.push(j);
|
p.push(j);
|
||||||
Command::QueueMove(q, p)
|
Action::QueueMove(q, p)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1037,7 +1037,7 @@ impl GuiElem for QueueLoop {
|
|||||||
if self.mouse && button == MouseButton::Left {
|
if self.mouse && button == MouseButton::Left {
|
||||||
self.mouse = false;
|
self.mouse = false;
|
||||||
if e.take() && !self.always_copy {
|
if e.take() && !self.always_copy {
|
||||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
vec![GuiAction::SendToServer(Action::QueueGoto(
|
||||||
self.path.clone(),
|
self.path.clone(),
|
||||||
))]
|
))]
|
||||||
} else {
|
} else {
|
||||||
@ -1066,8 +1066,8 @@ impl GuiElem for QueueLoop {
|
|||||||
dragged_add_to_queue(
|
dragged_add_to_queue(
|
||||||
dragged,
|
dragged,
|
||||||
p,
|
p,
|
||||||
|p, q| Command::QueueAdd(p, q),
|
|p, q| Action::QueueAdd(p, q),
|
||||||
|p, q| Command::QueueMoveInto(q, p),
|
|p, q| Action::QueueMoveInto(q, p),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
@ -1078,8 +1078,8 @@ impl GuiElem for QueueLoop {
|
|||||||
fn dragged_add_to_queue<T: 'static>(
|
fn dragged_add_to_queue<T: 'static>(
|
||||||
dragged: Dragging,
|
dragged: Dragging,
|
||||||
data: T,
|
data: T,
|
||||||
f_queues: impl FnOnce(T, Vec<Queue>) -> Command + 'static,
|
f_queues: impl FnOnce(T, Vec<Queue>) -> Action + 'static,
|
||||||
f_queue_by_path: impl FnOnce(T, Vec<usize>) -> Command + 'static,
|
f_queue_by_path: impl FnOnce(T, Vec<usize>) -> Action + 'static,
|
||||||
) -> Vec<GuiAction> {
|
) -> Vec<GuiAction> {
|
||||||
match dragged {
|
match dragged {
|
||||||
Dragging::Artist(id) => {
|
Dragging::Artist(id) => {
|
||||||
|
@ -2,7 +2,7 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::queue::{QueueContent, QueueFolder},
|
data::queue::{QueueContent, QueueFolder},
|
||||||
server::Command,
|
server::Action,
|
||||||
};
|
};
|
||||||
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};
|
||||||
@ -120,15 +120,13 @@ impl GuiScreen {
|
|||||||
button_clear_queue: Button::new(
|
button_clear_queue: Button::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))),
|
||||||
|_| {
|
|_| {
|
||||||
vec![GuiAction::SendToServer(
|
vec![GuiAction::SendToServer(Action::QueueUpdate(
|
||||||
musicdb_lib::server::Command::QueueUpdate(
|
|
||||||
vec![],
|
vec![],
|
||||||
musicdb_lib::data::queue::QueueContent::Folder(
|
musicdb_lib::data::queue::QueueContent::Folder(
|
||||||
musicdb_lib::data::queue::QueueFolder::default(),
|
musicdb_lib::data::queue::QueueFolder::default(),
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
),
|
))]
|
||||||
)]
|
|
||||||
},
|
},
|
||||||
[Label::new(
|
[Label::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
@ -305,9 +303,9 @@ impl GuiElem for GuiScreen {
|
|||||||
if key == ' ' && !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && e.take() {
|
if key == ' ' && !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && e.take() {
|
||||||
vec![GuiAction::Build(Box::new(|db| {
|
vec![GuiAction::Build(Box::new(|db| {
|
||||||
vec![GuiAction::SendToServer(if db.playing {
|
vec![GuiAction::SendToServer(if db.playing {
|
||||||
Command::Pause
|
Action::Pause
|
||||||
} else {
|
} else {
|
||||||
Command::Resume
|
Action::Resume
|
||||||
})]
|
})]
|
||||||
}))]
|
}))]
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
||||||
|
|
||||||
use musicdb_lib::server::Command;
|
use musicdb_lib::server::Action;
|
||||||
use speedy2d::{
|
use speedy2d::{
|
||||||
color::Color,
|
color::Color,
|
||||||
dimen::Vec2,
|
dimen::Vec2,
|
||||||
@ -450,7 +450,7 @@ impl SettingsContent {
|
|||||||
),
|
),
|
||||||
save_button: Button::new(
|
save_button: Button::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
|_| vec![GuiAction::SendToServer(Command::Save)],
|
|_| vec![GuiAction::SendToServer(Action::Save)],
|
||||||
[Label::new(
|
[Label::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
"Server: Save Changes".to_string(),
|
"Server: Save Changes".to_string(),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
// #![allow(unused)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufReader, Write},
|
io::{BufReader, Write},
|
||||||
net::{SocketAddr, TcpStream},
|
net::{SocketAddr, TcpStream},
|
||||||
@ -12,7 +14,7 @@ use gui::GuiEvent;
|
|||||||
#[cfg(feature = "playback")]
|
#[cfg(feature = "playback")]
|
||||||
use musicdb_lib::data::cache_manager::CacheManager;
|
use musicdb_lib::data::cache_manager::CacheManager;
|
||||||
#[cfg(feature = "playback")]
|
#[cfg(feature = "playback")]
|
||||||
use musicdb_lib::player::{playback_rs::PlayerBackendPlaybackRs, Player};
|
use musicdb_lib::player::{Player, PlayerBackendFeat};
|
||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::{
|
data::{
|
||||||
database::{ClientIo, Database},
|
database::{ClientIo, Database},
|
||||||
@ -152,7 +154,7 @@ fn main() {
|
|||||||
cm.set_cache_songs_count(20);
|
cm.set_cache_songs_count(20);
|
||||||
cache_manager = Some(cm);
|
cache_manager = Some(cm);
|
||||||
Some(Player::new_client(
|
Some(Player::new_client(
|
||||||
PlayerBackendPlaybackRs::new_without_command_sending().unwrap(),
|
PlayerBackendFeat::new_without_command_sending().unwrap(),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -186,21 +188,22 @@ fn main() {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
loop {
|
loop {
|
||||||
let update = Command::from_bytes(&mut con).unwrap();
|
let command = Command::from_bytes(&mut con).unwrap();
|
||||||
let mut db = database.lock().unwrap();
|
let mut db = database.lock().unwrap();
|
||||||
|
let action = db.seq.recv(command);
|
||||||
#[cfg(feature = "playback")]
|
#[cfg(feature = "playback")]
|
||||||
if let Some(player) = &mut player {
|
if let Some(player) = &mut player {
|
||||||
player.handle_command(&update);
|
player.handle_action(&action);
|
||||||
}
|
}
|
||||||
#[allow(unused_labels)]
|
#[allow(unused_labels)]
|
||||||
'feature_if: {
|
'feature_if: {
|
||||||
#[cfg(any(feature = "mers", feature = "merscfg"))]
|
#[cfg(any(feature = "mers", feature = "merscfg"))]
|
||||||
if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() {
|
if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() {
|
||||||
db.apply_command(update.clone());
|
db.apply_command(action.clone());
|
||||||
action(update);
|
action(action);
|
||||||
break 'feature_if;
|
break 'feature_if;
|
||||||
}
|
}
|
||||||
db.apply_command(update);
|
db.apply_action_unchecked_seq(action);
|
||||||
}
|
}
|
||||||
#[cfg(feature = "playback")]
|
#[cfg(feature = "playback")]
|
||||||
if let Some(player) = &mut player {
|
if let Some(player) = &mut player {
|
||||||
|
@ -13,8 +13,9 @@ rodio = { version = "0.20.1", optional = true }
|
|||||||
sysinfo = "0.30.12"
|
sysinfo = "0.30.12"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# default = ["playback"]
|
default = []
|
||||||
playback = ["playback-via-playback-rs"]
|
playback = []
|
||||||
# playback = ["playback-via-rodio"]
|
default-playback = ["playback-via-playback-rs"]
|
||||||
playback-via-playback-rs = ["dep:playback-rs"]
|
# default-playback = ["playback-via-rodio"]
|
||||||
playback-via-rodio = ["dep:rodio"]
|
playback-via-playback-rs = ["playback", "dep:playback-rs"]
|
||||||
|
playback-via-rodio = ["playback", "dep:rodio"]
|
||||||
|
@ -11,7 +11,10 @@ use std::{
|
|||||||
use colorize::AnsiColor;
|
use colorize::AnsiColor;
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
|
|
||||||
use crate::{load::ToFromBytes, server::Command};
|
use crate::{
|
||||||
|
load::ToFromBytes,
|
||||||
|
server::{Action, Command, Commander},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
album::Album,
|
album::Album,
|
||||||
@ -22,6 +25,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
|
pub seq: Commander,
|
||||||
/// the directory that contains the dbfile, backups, statistics, ...
|
/// the directory that contains the dbfile, backups, statistics, ...
|
||||||
pub db_dir: PathBuf,
|
pub db_dir: PathBuf,
|
||||||
/// the path to the file used to save/load the data. empty if database is in client mode.
|
/// the path to the file used to save/load the data. empty if database is in client mode.
|
||||||
@ -495,75 +499,93 @@ impl Database {
|
|||||||
|
|
||||||
pub fn init_connection<T: Write>(&self, con: &mut T) -> Result<(), std::io::Error> {
|
pub fn init_connection<T: Write>(&self, con: &mut T) -> Result<(), std::io::Error> {
|
||||||
// TODO! this is slow because it clones everything - there has to be a better way...
|
// TODO! this is slow because it clones everything - there has to be a better way...
|
||||||
Command::SyncDatabase(
|
self.seq
|
||||||
|
.pack(Action::SyncDatabase(
|
||||||
self.artists().iter().map(|v| v.1.clone()).collect(),
|
self.artists().iter().map(|v| v.1.clone()).collect(),
|
||||||
self.albums().iter().map(|v| v.1.clone()).collect(),
|
self.albums().iter().map(|v| v.1.clone()).collect(),
|
||||||
self.songs().iter().map(|v| v.1.clone()).collect(),
|
self.songs().iter().map(|v| v.1.clone()).collect(),
|
||||||
)
|
))
|
||||||
|
.to_bytes(con)?;
|
||||||
|
self.seq
|
||||||
|
.pack(Action::QueueUpdate(vec![], self.queue.clone()))
|
||||||
.to_bytes(con)?;
|
.to_bytes(con)?;
|
||||||
Command::QueueUpdate(vec![], self.queue.clone()).to_bytes(con)?;
|
|
||||||
if self.playing {
|
if self.playing {
|
||||||
Command::Resume.to_bytes(con)?;
|
self.seq.pack(Action::Resume).to_bytes(con)?;
|
||||||
}
|
}
|
||||||
// this allows clients to find out when init_connection is done.
|
// this allows clients to find out when init_connection is done.
|
||||||
Command::InitComplete.to_bytes(con)?;
|
self.seq.pack(Action::InitComplete).to_bytes(con)?;
|
||||||
// is initialized now - client can receive updates after this point.
|
// is initialized now - client can receive updates after this point.
|
||||||
// NOTE: Don't write to connection anymore - the db will dispatch updates on its own.
|
// NOTE: Don't write to connection anymore - the db will dispatch updates on its own.
|
||||||
// we just need to handle commands (receive from the connection).
|
// we just need to handle commands (receive from the connection).
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_command(&mut self, mut command: Command) {
|
/// `apply_action_unchecked_seq(command.action)` if `command.seq` is correct or `0xFF`
|
||||||
|
pub fn apply_command(&mut self, command: Command) {
|
||||||
|
if command.seq != self.seq.seq() && command.seq != 0xFF {
|
||||||
|
eprintln!(
|
||||||
|
"Invalid sequence number: got {} but expected {}.",
|
||||||
|
command.seq,
|
||||||
|
self.seq.seq()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.apply_action_unchecked_seq(command.action)
|
||||||
|
}
|
||||||
|
pub fn apply_action_unchecked_seq(&mut self, mut action: Action) {
|
||||||
if !self.is_client() {
|
if !self.is_client() {
|
||||||
if let Command::ErrorInfo(t, _) = &mut command {
|
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,
|
||||||
// BUT only the server can set the Title of the ErrorInfo.
|
// BUT only the server can set the Title of the ErrorInfo.
|
||||||
t.clear();
|
t.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// some commands shouldn't be broadcast. these will broadcast a different command in their specific implementation.
|
// some commands shouldn't be broadcast. these will broadcast a different command in their specific implementation.
|
||||||
match &command {
|
match &action {
|
||||||
// Will broadcast `QueueSetShuffle`
|
// Will broadcast `QueueSetShuffle`
|
||||||
Command::QueueShuffle(_) => (),
|
Action::QueueShuffle(_) => (),
|
||||||
|
Action::NextSong if self.queue.is_almost_empty() => (),
|
||||||
|
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
|
// since db.update_endpoints is empty for clients, this won't cause unwanted back and forth
|
||||||
_ => self.broadcast_update(&command),
|
_ => action = self.broadcast_update(action),
|
||||||
}
|
}
|
||||||
match command {
|
match action {
|
||||||
Command::Resume => self.playing = true,
|
Action::Resume => self.playing = true,
|
||||||
Command::Pause => self.playing = false,
|
Action::Pause => self.playing = false,
|
||||||
Command::Stop => self.playing = false,
|
Action::Stop => self.playing = false,
|
||||||
Command::NextSong => {
|
Action::NextSong => {
|
||||||
if !Queue::advance_index_db(self) {
|
if !Queue::advance_index_db(self) {
|
||||||
// end of queue
|
// end of queue
|
||||||
self.apply_command(Command::Pause);
|
self.apply_action_unchecked_seq(Action::Pause);
|
||||||
self.queue.init();
|
self.queue.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Save => {
|
Action::Save => {
|
||||||
if let Err(e) = self.save_database(None) {
|
if let Err(e) = self.save_database(None) {
|
||||||
eprintln!("[{}] Couldn't save: {e}", "ERR!".red());
|
eprintln!("[{}] Couldn't save: {e}", "ERR!".red());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::SyncDatabase(a, b, c) => self.sync(a, b, c),
|
Action::SyncDatabase(a, b, c) => self.sync(a, b, c),
|
||||||
Command::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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::QueueRemove(index) => {
|
Action::QueueRemove(index) => {
|
||||||
self.queue.remove_by_index(&index, 0);
|
self.queue.remove_by_index(&index, 0);
|
||||||
}
|
}
|
||||||
Command::QueueMove(index_from, mut index_to) => 'queue_move: {
|
Action::QueueMove(index_from, mut index_to) => 'queue_move: {
|
||||||
if index_to.len() == 0 || index_to.starts_with(&index_from) {
|
if index_to.len() == 0 || index_to.starts_with(&index_from) {
|
||||||
break 'queue_move;
|
break 'queue_move;
|
||||||
}
|
}
|
||||||
@ -605,7 +627,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::QueueMoveInto(index_from, mut parent_to) => 'queue_move_into: {
|
Action::QueueMoveInto(index_from, mut parent_to) => 'queue_move_into: {
|
||||||
if parent_to.starts_with(&index_from) {
|
if parent_to.starts_with(&index_from) {
|
||||||
break 'queue_move_into;
|
break 'queue_move_into;
|
||||||
}
|
}
|
||||||
@ -628,8 +650,8 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::QueueGoto(index) => Queue::set_index_db(self, &index),
|
Action::QueueGoto(index) => Queue::set_index_db(self, &index),
|
||||||
Command::QueueShuffle(path) => {
|
Action::QueueShuffle(path) => {
|
||||||
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
||||||
if let QueueContent::Folder(QueueFolder {
|
if let QueueContent::Folder(QueueFolder {
|
||||||
index: _,
|
index: _,
|
||||||
@ -640,7 +662,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_command(Command::QueueSetShuffle(path, ord));
|
self.apply_action_unchecked_seq(Action::QueueSetShuffle(path, ord));
|
||||||
} else {
|
} else {
|
||||||
eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!");
|
eprintln!("(QueueShuffle) QueueElement at {path:?} not a folder!");
|
||||||
}
|
}
|
||||||
@ -648,7 +670,7 @@ impl Database {
|
|||||||
eprintln!("(QueueShuffle) No QueueElement at {path:?}");
|
eprintln!("(QueueShuffle) No QueueElement at {path:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::QueueSetShuffle(path, ord) => {
|
Action::QueueSetShuffle(path, ord) => {
|
||||||
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
||||||
if let QueueContent::Folder(QueueFolder {
|
if let QueueContent::Folder(QueueFolder {
|
||||||
index,
|
index,
|
||||||
@ -681,7 +703,7 @@ impl Database {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::QueueUnshuffle(path) => {
|
Action::QueueUnshuffle(path) => {
|
||||||
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
if let Some(elem) = self.queue.get_item_at_index_mut(&path, 0) {
|
||||||
if let QueueContent::Folder(QueueFolder {
|
if let QueueContent::Folder(QueueFolder {
|
||||||
index,
|
index,
|
||||||
@ -697,77 +719,77 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::AddSong(song) => {
|
Action::AddSong(song) => {
|
||||||
self.add_song_new(song);
|
self.add_song_new(song);
|
||||||
}
|
}
|
||||||
Command::AddAlbum(album) => {
|
Action::AddAlbum(album) => {
|
||||||
self.add_album_new(album);
|
self.add_album_new(album);
|
||||||
}
|
}
|
||||||
Command::AddArtist(artist) => {
|
Action::AddArtist(artist) => {
|
||||||
self.add_artist_new(artist);
|
self.add_artist_new(artist);
|
||||||
}
|
}
|
||||||
Command::AddCover(cover) => _ = self.add_cover_new(cover),
|
Action::AddCover(cover) => _ = self.add_cover_new(cover),
|
||||||
Command::ModifySong(song) => {
|
Action::ModifySong(song) => {
|
||||||
_ = self.update_song(song);
|
_ = self.update_song(song);
|
||||||
}
|
}
|
||||||
Command::ModifyAlbum(album) => {
|
Action::ModifyAlbum(album) => {
|
||||||
_ = self.update_album(album);
|
_ = self.update_album(album);
|
||||||
}
|
}
|
||||||
Command::ModifyArtist(artist) => {
|
Action::ModifyArtist(artist) => {
|
||||||
_ = self.update_artist(artist);
|
_ = self.update_artist(artist);
|
||||||
}
|
}
|
||||||
Command::RemoveSong(song) => {
|
Action::RemoveSong(song) => {
|
||||||
_ = self.remove_song(song);
|
_ = self.remove_song(song);
|
||||||
}
|
}
|
||||||
Command::RemoveAlbum(album) => {
|
Action::RemoveAlbum(album) => {
|
||||||
_ = self.remove_album(album);
|
_ = self.remove_album(album);
|
||||||
}
|
}
|
||||||
Command::RemoveArtist(artist) => {
|
Action::RemoveArtist(artist) => {
|
||||||
_ = self.remove_artist(artist);
|
_ = self.remove_artist(artist);
|
||||||
}
|
}
|
||||||
Command::TagSongFlagSet(id, tag) => {
|
Action::TagSongFlagSet(id, tag) => {
|
||||||
if let Some(v) = self.get_song_mut(&id) {
|
if let Some(v) = self.get_song_mut(&id) {
|
||||||
if !v.general.tags.contains(&tag) {
|
if !v.general.tags.contains(&tag) {
|
||||||
v.general.tags.push(tag);
|
v.general.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagSongFlagUnset(id, tag) => {
|
Action::TagSongFlagUnset(id, tag) => {
|
||||||
if let Some(v) = self.get_song_mut(&id) {
|
if let Some(v) = self.get_song_mut(&id) {
|
||||||
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
||||||
v.general.tags.remove(i);
|
v.general.tags.remove(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagAlbumFlagSet(id, tag) => {
|
Action::TagAlbumFlagSet(id, tag) => {
|
||||||
if let Some(v) = self.albums.get_mut(&id) {
|
if let Some(v) = self.albums.get_mut(&id) {
|
||||||
if !v.general.tags.contains(&tag) {
|
if !v.general.tags.contains(&tag) {
|
||||||
v.general.tags.push(tag);
|
v.general.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagAlbumFlagUnset(id, tag) => {
|
Action::TagAlbumFlagUnset(id, tag) => {
|
||||||
if let Some(v) = self.albums.get_mut(&id) {
|
if let Some(v) = self.albums.get_mut(&id) {
|
||||||
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
||||||
v.general.tags.remove(i);
|
v.general.tags.remove(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagArtistFlagSet(id, tag) => {
|
Action::TagArtistFlagSet(id, tag) => {
|
||||||
if let Some(v) = self.artists.get_mut(&id) {
|
if let Some(v) = self.artists.get_mut(&id) {
|
||||||
if !v.general.tags.contains(&tag) {
|
if !v.general.tags.contains(&tag) {
|
||||||
v.general.tags.push(tag);
|
v.general.tags.push(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagArtistFlagUnset(id, tag) => {
|
Action::TagArtistFlagUnset(id, tag) => {
|
||||||
if let Some(v) = self.artists.get_mut(&id) {
|
if let Some(v) = self.artists.get_mut(&id) {
|
||||||
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
if let Some(i) = v.general.tags.iter().position(|v| v == &tag) {
|
||||||
v.general.tags.remove(i);
|
v.general.tags.remove(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagSongPropertySet(id, key, val) => {
|
Action::TagSongPropertySet(id, key, val) => {
|
||||||
if let Some(v) = self.get_song_mut(&id) {
|
if let Some(v) = self.get_song_mut(&id) {
|
||||||
let new = format!("{key}{val}");
|
let new = format!("{key}{val}");
|
||||||
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
||||||
@ -777,13 +799,13 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagSongPropertyUnset(id, key) => {
|
Action::TagSongPropertyUnset(id, key) => {
|
||||||
if let Some(v) = self.get_song_mut(&id) {
|
if let Some(v) = self.get_song_mut(&id) {
|
||||||
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
||||||
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagAlbumPropertySet(id, key, val) => {
|
Action::TagAlbumPropertySet(id, key, val) => {
|
||||||
if let Some(v) = self.albums.get_mut(&id) {
|
if let Some(v) = self.albums.get_mut(&id) {
|
||||||
let new = format!("{key}{val}");
|
let new = format!("{key}{val}");
|
||||||
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
||||||
@ -793,13 +815,13 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagAlbumPropertyUnset(id, key) => {
|
Action::TagAlbumPropertyUnset(id, key) => {
|
||||||
if let Some(v) = self.albums.get_mut(&id) {
|
if let Some(v) = self.albums.get_mut(&id) {
|
||||||
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
||||||
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagArtistPropertySet(id, key, val) => {
|
Action::TagArtistPropertySet(id, key, val) => {
|
||||||
if let Some(v) = self.artists.get_mut(&id) {
|
if let Some(v) = self.artists.get_mut(&id) {
|
||||||
let new = format!("{key}{val}");
|
let new = format!("{key}{val}");
|
||||||
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
if let Some(v) = v.general.tags.iter_mut().find(|v| v.starts_with(&key)) {
|
||||||
@ -809,21 +831,21 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::TagArtistPropertyUnset(id, key) => {
|
Action::TagArtistPropertyUnset(id, key) => {
|
||||||
if let Some(v) = self.artists.get_mut(&id) {
|
if let Some(v) = self.artists.get_mut(&id) {
|
||||||
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
let tags = std::mem::replace(&mut v.general.tags, vec![]);
|
||||||
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
v.general.tags = tags.into_iter().filter(|v| !v.starts_with(&key)).collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::SetSongDuration(id, duration) => {
|
Action::SetSongDuration(id, duration) => {
|
||||||
if let Some(song) = self.get_song_mut(&id) {
|
if let Some(song) = self.get_song_mut(&id) {
|
||||||
song.duration_millis = duration;
|
song.duration_millis = duration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::InitComplete => {
|
Action::InitComplete => {
|
||||||
self.client_is_init = true;
|
self.client_is_init = true;
|
||||||
}
|
}
|
||||||
Command::ErrorInfo(..) => {}
|
Action::ErrorInfo(..) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -842,6 +864,7 @@ impl Database {
|
|||||||
/// A client database doesn't need any storage paths and won't perform autosaves.
|
/// A client database doesn't need any storage paths and won't perform autosaves.
|
||||||
pub fn new_clientside() -> Self {
|
pub fn new_clientside() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
seq: Commander::new(true),
|
||||||
db_dir: PathBuf::new(),
|
db_dir: PathBuf::new(),
|
||||||
db_file: PathBuf::new(),
|
db_file: PathBuf::new(),
|
||||||
lib_directory: PathBuf::new(),
|
lib_directory: PathBuf::new(),
|
||||||
@ -862,6 +885,7 @@ impl Database {
|
|||||||
pub fn new_empty_in_dir(dir: PathBuf, lib_dir: PathBuf) -> Self {
|
pub fn new_empty_in_dir(dir: PathBuf, lib_dir: PathBuf) -> Self {
|
||||||
let path = dir.join("dbfile");
|
let path = dir.join("dbfile");
|
||||||
Self {
|
Self {
|
||||||
|
seq: Commander::new(false),
|
||||||
db_dir: dir,
|
db_dir: dir,
|
||||||
db_file: path,
|
db_file: path,
|
||||||
lib_directory: lib_dir,
|
lib_directory: lib_dir,
|
||||||
@ -887,6 +911,7 @@ impl Database {
|
|||||||
let mut file = BufReader::new(File::open(&path)?);
|
let mut file = BufReader::new(File::open(&path)?);
|
||||||
eprintln!("[{}] loading library from {file:?}", "INFO".cyan());
|
eprintln!("[{}] loading library from {file:?}", "INFO".cyan());
|
||||||
let s = Self {
|
let s = Self {
|
||||||
|
seq: Commander::new(false),
|
||||||
db_dir: dir,
|
db_dir: dir,
|
||||||
db_file: path,
|
db_file: path,
|
||||||
lib_directory,
|
lib_directory,
|
||||||
@ -948,11 +973,15 @@ impl Database {
|
|||||||
self.times_data_modified = None;
|
self.times_data_modified = None;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
pub fn broadcast_update(&mut self, update: &Command) {
|
pub fn broadcast_update(&mut self, update: Action) -> Action {
|
||||||
match update {
|
match update {
|
||||||
Command::InitComplete => return,
|
Action::InitComplete => return update,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
if !self.is_client() {
|
||||||
|
self.seq.inc();
|
||||||
|
}
|
||||||
|
let update = self.seq.pack(update);
|
||||||
let mut remove = vec![];
|
let mut remove = vec![];
|
||||||
let mut bytes = None;
|
let mut bytes = None;
|
||||||
let mut arc = None;
|
let mut arc = None;
|
||||||
@ -974,7 +1003,7 @@ impl Database {
|
|||||||
remove.push(i);
|
remove.push(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UpdateEndpoint::Custom(func) => func(update),
|
UpdateEndpoint::Custom(func) => func(&update),
|
||||||
UpdateEndpoint::CustomArc(func) => {
|
UpdateEndpoint::CustomArc(func) => {
|
||||||
if arc.is_none() {
|
if arc.is_none() {
|
||||||
arc = Some(Arc::new(update.clone()));
|
arc = Some(Arc::new(update.clone()));
|
||||||
@ -999,6 +1028,7 @@ impl Database {
|
|||||||
self.update_endpoints.remove(i);
|
self.update_endpoints.remove(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>) {
|
||||||
self.modified_data();
|
self.modified_data();
|
||||||
|
@ -64,6 +64,39 @@ impl Queue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
if !self.enabled {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match &self.content {
|
||||||
|
QueueContent::Song(_) => false,
|
||||||
|
QueueContent::Folder(folder) => folder.content.iter().all(|v| v.is_empty()),
|
||||||
|
QueueContent::Loop(_total, _done, inner) => inner.is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// returns true if there is at most one song in the queue
|
||||||
|
pub fn is_almost_empty(&self) -> bool {
|
||||||
|
self.is_almost_empty_int() < 2
|
||||||
|
}
|
||||||
|
fn is_almost_empty_int(&self) -> u8 {
|
||||||
|
if !self.enabled {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
match &self.content {
|
||||||
|
QueueContent::Song(_) => 1,
|
||||||
|
QueueContent::Folder(folder) => {
|
||||||
|
let mut o = 0;
|
||||||
|
for v in folder.content.iter() {
|
||||||
|
o += v.is_almost_empty_int();
|
||||||
|
if o >= 2 {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o
|
||||||
|
}
|
||||||
|
QueueContent::Loop(_total, _done, inner) => inner.is_almost_empty_int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
if !self.enabled {
|
if !self.enabled {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -2,12 +2,16 @@
|
|||||||
pub mod playback_rs;
|
pub mod playback_rs;
|
||||||
#[cfg(feature = "playback-via-rodio")]
|
#[cfg(feature = "playback-via-rodio")]
|
||||||
pub mod rodio;
|
pub mod rodio;
|
||||||
|
#[cfg(feature = "playback-via-playback-rs")]
|
||||||
|
pub type PlayerBackendFeat<T> = playback_rs::PlayerBackendPlaybackRs<T>;
|
||||||
|
#[cfg(feature = "playback-via-rodio")]
|
||||||
|
pub type PlayerBackendFeat<T> = rodio::PlayerBackendRodio<T>;
|
||||||
|
|
||||||
use std::{collections::HashMap, ffi::OsStr, sync::Arc};
|
use std::{collections::HashMap, ffi::OsStr, sync::Arc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{database::Database, song::CachedData, SongId},
|
data::{database::Database, song::CachedData, SongId},
|
||||||
server::Command,
|
server::Action,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Player<T: PlayerBackend<SongCustomData>> {
|
pub struct Player<T: PlayerBackend<SongCustomData>> {
|
||||||
@ -94,11 +98,11 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
|||||||
allow_sending_commands: false,
|
allow_sending_commands: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn handle_command(&mut self, command: &Command) {
|
pub fn handle_action(&mut self, action: &Action) {
|
||||||
match command {
|
match action {
|
||||||
Command::Resume => self.resume(),
|
Action::Resume => self.resume(),
|
||||||
Command::Pause => self.pause(),
|
Action::Pause => self.pause(),
|
||||||
Command::Stop => self.stop(),
|
Action::Stop => self.stop(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,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_command(Command::NextSong);
|
db.apply_action_unchecked_seq(Action::NextSong);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,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_command(Command::SetSongDuration(id, dur))
|
db.apply_action_unchecked_seq(Action::SetSongDuration(id, dur))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(song) = db.get_song(&id) {
|
} else if let Some(song) = db.get_song(&id) {
|
||||||
@ -165,21 +169,21 @@ 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_command(Command::SetSongDuration(id, dur))
|
db.apply_action_unchecked_seq(Action::SetSongDuration(id, dur))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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_command(Command::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_command(Command::NextSong);
|
db.apply_action_unchecked_seq(Action::NextSong);
|
||||||
}
|
}
|
||||||
self.backend.clear();
|
self.backend.clear();
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,10 @@ use std::{ffi::OsStr, io::Cursor, path::Path, sync::Arc, time::Duration};
|
|||||||
|
|
||||||
use playback_rs::Hint;
|
use playback_rs::Hint;
|
||||||
|
|
||||||
use crate::{data::SongId, server::Command};
|
use crate::{
|
||||||
|
data::SongId,
|
||||||
|
server::{Action, Command},
|
||||||
|
};
|
||||||
|
|
||||||
use super::PlayerBackend;
|
use super::PlayerBackend;
|
||||||
|
|
||||||
@ -52,10 +55,13 @@ 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(Command::ErrorInfo(
|
s.send(
|
||||||
|
Action::ErrorInfo(
|
||||||
format!("Couldn't decode song #{id}!"),
|
format!("Couldn't decode song #{id}!"),
|
||||||
format!("Error: {e}"),
|
format!("Error: {e}"),
|
||||||
))
|
)
|
||||||
|
.cmd(0xFFu8),
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -95,18 +101,21 @@ 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(Command::ErrorInfo(
|
s.send(
|
||||||
|
Action::ErrorInfo(
|
||||||
format!("Couldn't play song #{id}!"),
|
format!("Couldn't play song #{id}!"),
|
||||||
format!("Error: {e}"),
|
format!("Error: {e}"),
|
||||||
))
|
)
|
||||||
|
.cmd(0xFFu8),
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
s.send(Command::NextSong).unwrap();
|
s.send(Action::NextSong.cmd(0xFFu8)).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(Command::NextSong).unwrap();
|
s.send(Action::NextSong.cmd(0xFFu8)).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@ use std::{ffi::OsStr, sync::Arc};
|
|||||||
use rc_u8_reader::ArcU8Reader;
|
use rc_u8_reader::ArcU8Reader;
|
||||||
use rodio::{decoder::DecoderError, Decoder, OutputStream, OutputStreamHandle, Sink, Source};
|
use rodio::{decoder::DecoderError, Decoder, OutputStream, OutputStreamHandle, Sink, Source};
|
||||||
|
|
||||||
use crate::{data::SongId, server::Command};
|
use crate::{
|
||||||
|
data::SongId,
|
||||||
|
server::{Action, Command},
|
||||||
|
};
|
||||||
|
|
||||||
use super::PlayerBackend;
|
use super::PlayerBackend;
|
||||||
|
|
||||||
@ -57,10 +60,13 @@ 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(Command::ErrorInfo(
|
s.send(
|
||||||
|
Action::ErrorInfo(
|
||||||
format!("Couldn't decode song #{id}!"),
|
format!("Couldn't decode song #{id}!"),
|
||||||
format!("Error: '{e}'"),
|
format!("Error: '{e}'"),
|
||||||
))
|
)
|
||||||
|
.cmd(0xFFu8),
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,54 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Command {
|
pub struct Command {
|
||||||
|
/// when sending to the server, this should be the most recent sequence number,
|
||||||
|
/// or `0xFF` to indicate that the action should be performed regardless of if the sequence number would be up to date or not.
|
||||||
|
/// when receiving from the server, this contains the most recent sequence number. It is never `0xFF`.
|
||||||
|
/// used to avoid issues due to desynchronization
|
||||||
|
pub seq: u8,
|
||||||
|
pub action: Action,
|
||||||
|
}
|
||||||
|
impl Command {
|
||||||
|
pub fn new(seq: u8, action: Action) -> Self {
|
||||||
|
Self { seq, action }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Action {
|
||||||
|
pub fn cmd(self, seq: u8) -> Command {
|
||||||
|
Command::new(seq, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Should be stored in the same lock as the database
|
||||||
|
pub struct Commander {
|
||||||
|
seq: u8,
|
||||||
|
}
|
||||||
|
impl Commander {
|
||||||
|
pub fn new(ff: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
seq: if ff { 0xFFu8 } else { 0u8 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn inc(&mut self) {
|
||||||
|
if self.seq < 0xFEu8 {
|
||||||
|
self.seq += 1;
|
||||||
|
} else {
|
||||||
|
self.seq = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn pack(&self, action: Action) -> Command {
|
||||||
|
Command::new(self.seq, action)
|
||||||
|
}
|
||||||
|
pub fn recv(&mut self, command: Command) -> Action {
|
||||||
|
self.seq = command.seq;
|
||||||
|
command.action
|
||||||
|
}
|
||||||
|
pub fn seq(&self) -> u8 {
|
||||||
|
self.seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Action {
|
||||||
Resume,
|
Resume,
|
||||||
Pause,
|
Pause,
|
||||||
Stop,
|
Stop,
|
||||||
@ -269,7 +316,7 @@ pub fn run_server_caching_thread_opt(
|
|||||||
checkf = true;
|
checkf = true;
|
||||||
#[cfg(feature = "playback")]
|
#[cfg(feature = "playback")]
|
||||||
if let Some(player) = &mut player {
|
if let Some(player) = &mut player {
|
||||||
player.handle_command(&command);
|
player.handle_action(&command.action);
|
||||||
}
|
}
|
||||||
database.lock().unwrap().apply_command(command);
|
database.lock().unwrap().apply_command(command);
|
||||||
}
|
}
|
||||||
@ -365,6 +412,26 @@ const SUBBYTE_TAG_ARTIST_PROPERTY_SET: u8 = 0b10_100_010;
|
|||||||
const SUBBYTE_TAG_ARTIST_PROPERTY_UNSET: u8 = 0b10_100_100;
|
const SUBBYTE_TAG_ARTIST_PROPERTY_UNSET: u8 = 0b10_100_100;
|
||||||
|
|
||||||
impl ToFromBytes for Command {
|
impl ToFromBytes for Command {
|
||||||
|
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
|
||||||
|
where
|
||||||
|
T: Write,
|
||||||
|
{
|
||||||
|
s.write_all(&[self.seq])?;
|
||||||
|
self.action.to_bytes(s)
|
||||||
|
}
|
||||||
|
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
|
||||||
|
where
|
||||||
|
T: Read,
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
seq: ToFromBytes::from_bytes(s)?,
|
||||||
|
action: Action::from_bytes(s)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl ToFromBytes for 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>
|
||||||
where
|
where
|
||||||
T: Write,
|
T: Write,
|
||||||
|
@ -16,6 +16,9 @@ rocket = { version = "0.5.0", optional = true }
|
|||||||
html-escape = { version = "0.2.13", optional = true }
|
html-escape = { version = "0.2.13", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["website", "playback"]
|
default = ["website", "default-playback"]
|
||||||
website = ["dep:tokio", "dep:rocket", "dep:html-escape"]
|
website = ["dep:tokio", "dep:rocket", "dep:html-escape"]
|
||||||
playback = ["musicdb-lib/playback"]
|
playback = []
|
||||||
|
default-playback = ["playback", "musicdb-lib/default-playback"]
|
||||||
|
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]
|
||||||
|
playback-via-rodio = ["playback", "musicdb-lib/playback-via-rodio"]
|
||||||
|
@ -149,8 +149,8 @@ fn main() {
|
|||||||
writeln!(con, "main").unwrap();
|
writeln!(con, "main").unwrap();
|
||||||
loop {
|
loop {
|
||||||
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::Command::*;
|
use musicdb_lib::server::Action::*;
|
||||||
match &cmd {
|
match &cmd.action {
|
||||||
// ignore playback and queue commands
|
// ignore playback and queue commands
|
||||||
Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..)
|
Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..)
|
||||||
| QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..)
|
| QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..)
|
||||||
|
@ -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::Command;
|
use musicdb_lib::server::{Action, Command};
|
||||||
use rocket::response::content::RawHtml;
|
use rocket::response::content::RawHtml;
|
||||||
use rocket::{get, routes, Config, State};
|
use rocket::{get, routes, Config, State};
|
||||||
|
|
||||||
@ -258,37 +258,44 @@ 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(Command::QueueRemove(path))
|
.send(Action::QueueRemove(path).cmd(0xFFu8))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[get("/queue-goto/<path>")]
|
#[get("/queue-goto/<path>")]
|
||||||
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.send(Command::QueueGoto(path)).unwrap();
|
data.command_sender
|
||||||
|
.send(Action::QueueGoto(path).cmd(0xFFu8))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/play")]
|
#[get("/play")]
|
||||||
fn play(data: &State<Data>) {
|
fn play(data: &State<Data>) {
|
||||||
data.command_sender.send(Command::Resume).unwrap();
|
data.command_sender
|
||||||
|
.send(Action::Resume.cmd(0xFFu8))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
#[get("/pause")]
|
#[get("/pause")]
|
||||||
fn pause(data: &State<Data>) {
|
fn pause(data: &State<Data>) {
|
||||||
data.command_sender.send(Command::Pause).unwrap();
|
data.command_sender.send(Action::Pause.cmd(0xFFu8)).unwrap();
|
||||||
}
|
}
|
||||||
#[get("/stop")]
|
#[get("/stop")]
|
||||||
fn stop(data: &State<Data>) {
|
fn stop(data: &State<Data>) {
|
||||||
data.command_sender.send(Command::Stop).unwrap();
|
data.command_sender.send(Action::Stop.cmd(0xFFu8)).unwrap();
|
||||||
}
|
}
|
||||||
#[get("/skip")]
|
#[get("/skip")]
|
||||||
fn skip(data: &State<Data>) {
|
fn skip(data: &State<Data>) {
|
||||||
data.command_sender.send(Command::NextSong).unwrap();
|
data.command_sender
|
||||||
|
.send(Action::NextSong.cmd(0xFFu8))
|
||||||
|
.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(Command::QueueUpdate(
|
.send(
|
||||||
|
Action::QueueUpdate(
|
||||||
vec![],
|
vec![],
|
||||||
QueueContent::Folder(QueueFolder {
|
QueueContent::Folder(QueueFolder {
|
||||||
index: 0,
|
index: 0,
|
||||||
@ -297,17 +304,16 @@ fn clear_queue(data: &State<Data>) {
|
|||||||
order: None,
|
order: None,
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
))
|
)
|
||||||
|
.cmd(0xFFu8),
|
||||||
|
)
|
||||||
.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(Command::QueueAdd(
|
.send(Action::QueueAdd(vec![], vec![QueueContent::Song(id).into()]).cmd(0xFFu8))
|
||||||
vec![],
|
|
||||||
vec![QueueContent::Song(id).into()],
|
|
||||||
))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user