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

View File

@ -12,7 +12,7 @@ directories = "5.0.1"
regex = "1.9.3"
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"]

View File

@ -1,6 +1,13 @@
# [build]
# pre-build = [
# "dpkg --add-architecture $CROSS_DEB_ARCH",
# "apt-get update && apt-get --assume-yes install libasound2-dev libasound2-dev:$CROSS_DEB_ARCH"
# ]
# default-target = "aarch64-unknown-linux-gnu"
[build]
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"

View File

@ -362,9 +362,14 @@ 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 {
{
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
@ -386,13 +391,13 @@ impl Gui {
}
}
Action::SyncDatabase(..)
| Action::AddSong(_)
| Action::AddAlbum(_)
| Action::AddArtist(_)
| Action::AddCover(_)
| Action::ModifySong(_)
| Action::ModifyAlbum(_)
| Action::ModifyArtist(_)
| Action::AddSong(_, _)
| Action::AddAlbum(_, _)
| Action::AddArtist(_, _)
| Action::AddCover(_, _)
| Action::ModifySong(_, _)
| Action::ModifyAlbum(_, _)
| Action::ModifyArtist(_, _)
| Action::RemoveSong(_)
| Action::RemoveAlbum(_)
| Action::RemoveArtist(_)
@ -443,9 +448,38 @@ impl Gui {
}))
.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}");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -126,7 +126,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
pub fn update_uncache_opt(&mut self, db: &mut Database, allow_uncaching: bool) {
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(
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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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