mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-09 21:13:54 +01:00
add more modes to client and change server cli
server can now source its database and files from another server, but it will have its own queue and appears as separate to clients. the client now has gui-syncplayer-{local,network} modes which show the gui and also play the songs. using a syncplayer-network mode now automatically enables the cache manager, which should make waiting for songs to load less frequent.
This commit is contained in:
parent
7d6d6d295b
commit
61110f5f4a
@ -76,7 +76,7 @@ musicdb-client 0.0.0.0:26002 gui
|
||||
To start the server:
|
||||
|
||||
```sh
|
||||
musicdb-server ~/my_dbdir ~/music --tcp 0.0.0.0:26002
|
||||
musicdb-server --tcp 0.0.0.0:26002 --play-audio local ~/my_dbdir ~/music
|
||||
```
|
||||
|
||||
A simple script can start the server and then the client:
|
||||
@ -84,7 +84,7 @@ A simple script can start the server and then the client:
|
||||
```sh
|
||||
# if the server is already running, this command will fail since 0.0.0.0:26002 is already in use,
|
||||
# and you will never end up with 2+ servers running at the same time
|
||||
musicdb-server ~/my_dbdir ~/music --tcp 0.0.0.0:26002 &
|
||||
musicdb-server --tcp 0.0.0.0:26002 --play-audio local ~/my_dbdir ~/music &
|
||||
# wait for the server to load (on most systems, this should never take more than 0.1 seconds, but just in case...)
|
||||
sleep 1
|
||||
# now start the client
|
||||
|
@ -17,9 +17,9 @@ musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true }
|
||||
uianimator = "0.1.1"
|
||||
|
||||
[features]
|
||||
default = ["gui", "mers", "merscfg"]
|
||||
default = ["gui", "playback"]
|
||||
# gui:
|
||||
# enables the gui mode
|
||||
# enables the gui modes
|
||||
# merscfg:
|
||||
# allows using mers to configure the gui
|
||||
# mers:
|
||||
|
@ -9,7 +9,12 @@ use std::{
|
||||
};
|
||||
|
||||
use musicdb_lib::{
|
||||
data::{database::Database, queue::Queue, song::Song, AlbumId, ArtistId, CoverId, SongId},
|
||||
data::{
|
||||
database::{ClientIo, Database},
|
||||
queue::Queue,
|
||||
song::Song,
|
||||
AlbumId, ArtistId, CoverId, SongId,
|
||||
},
|
||||
load::ToFromBytes,
|
||||
server::{get, Command},
|
||||
};
|
||||
@ -79,9 +84,11 @@ pub fn hotkey_select_songs(modifiers: &ModifiersState, key: Option<VirtualKeyCod
|
||||
pub fn main(
|
||||
database: Arc<Mutex<Database>>,
|
||||
connection: TcpStream,
|
||||
get_con: get::Client<TcpStream>,
|
||||
get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
|
||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
||||
after_db_cmd: &Arc<Mutex<Option<Box<dyn FnMut(Command) + Send + Sync + 'static>>>>,
|
||||
#[cfg(feature = "merscfg")] after_db_cmd: &Arc<
|
||||
Mutex<Option<Box<dyn FnMut(Command) + Send + Sync + 'static>>>,
|
||||
>,
|
||||
) {
|
||||
let config_dir = super::get_config_file_path();
|
||||
let config_file = config_dir.join("config_gui.toml");
|
||||
@ -219,7 +226,7 @@ pub fn main(
|
||||
font,
|
||||
Arc::clone(&database),
|
||||
connection,
|
||||
Arc::new(Mutex::new(get_con)),
|
||||
get_con,
|
||||
event_sender_arc,
|
||||
Arc::new(sender),
|
||||
line_height,
|
||||
@ -264,6 +271,7 @@ pub fn main(
|
||||
#[cfg(feature = "merscfg")]
|
||||
merscfg: crate::merscfg::MersCfg::new(config_dir.join("dynamic_config.mers"), database),
|
||||
},
|
||||
#[cfg(feature = "merscfg")]
|
||||
after_db_cmd,
|
||||
));
|
||||
}
|
||||
@ -284,7 +292,7 @@ pub struct Gui {
|
||||
pub event_sender: Arc<UserEventSender<GuiEvent>>,
|
||||
pub database: Arc<Mutex<Database>>,
|
||||
pub connection: TcpStream,
|
||||
pub get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||
pub get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
|
||||
pub gui: GuiScreen,
|
||||
pub notif_sender:
|
||||
Sender<Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>>,
|
||||
@ -316,7 +324,7 @@ impl Gui {
|
||||
font: Font,
|
||||
database: Arc<Mutex<Database>>,
|
||||
connection: TcpStream,
|
||||
get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||
get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
|
||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
||||
event_sender: Arc<UserEventSender<GuiEvent>>,
|
||||
line_height: f32,
|
||||
@ -1158,7 +1166,7 @@ pub struct DrawInfo<'a> {
|
||||
/// true if `info.pos.contains(info.mouse_pos)`.
|
||||
pub mouse_pos_in_bounds: bool,
|
||||
pub helper: Option<&'a mut WindowHelper<GuiEvent>>,
|
||||
pub get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||
pub get_con: Arc<Mutex<get::Client<Box<dyn ClientIo + 'static>>>>,
|
||||
pub covers: &'a mut HashMap<CoverId, GuiServerImage>,
|
||||
pub custom_images: &'a mut HashMap<String, GuiServerImage>,
|
||||
pub has_keyboard_focus: bool,
|
||||
@ -1529,18 +1537,6 @@ impl WindowHandler<GuiEvent> for Gui {
|
||||
scancode: KeyScancode,
|
||||
) {
|
||||
helper.request_redraw();
|
||||
// handle keybinds unless settings are open, opening or closing
|
||||
if self.gui.settings.0 == false && self.gui.settings.1.is_none() {
|
||||
if let Some(key) = virtual_key_code {
|
||||
let keybind = KeyBinding::new(&self.modifiers, key);
|
||||
if let Some(action) = self.keybinds.get(&keybind) {
|
||||
for a in self.key_actions.get(action).execute() {
|
||||
self.exec_gui_action(a);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(VirtualKeyCode::Tab) = virtual_key_code {
|
||||
if !(self.modifiers.ctrl() || self.modifiers.alt() || self.modifiers.logo()) {
|
||||
self.gui._keyboard_move_focus(self.modifiers.shift(), false);
|
||||
@ -1578,6 +1574,18 @@ impl WindowHandler<GuiEvent> for Gui {
|
||||
scancode: KeyScancode,
|
||||
) {
|
||||
helper.request_redraw();
|
||||
// handle keybinds unless settings are open, opening or closing
|
||||
if self.gui.settings.0 == false && self.gui.settings.1.is_none() {
|
||||
if let Some(key) = virtual_key_code {
|
||||
let keybind = KeyBinding::new(&self.modifiers, key);
|
||||
if let Some(action) = self.keybinds.get(&keybind) {
|
||||
for a in self.key_actions.get(action).execute() {
|
||||
self.exec_gui_action(a);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for a in self.gui._keyboard_event(
|
||||
&mut |e, a| {
|
||||
if e.config().keyboard_events_focus {
|
||||
@ -1663,7 +1671,10 @@ pub enum GuiServerImage {
|
||||
}
|
||||
#[allow(unused)]
|
||||
impl GuiServerImage {
|
||||
pub fn new_cover(id: CoverId, get_con: Arc<Mutex<get::Client<TcpStream>>>) -> Self {
|
||||
pub fn new_cover<T: ClientIo + 'static>(
|
||||
id: CoverId,
|
||||
get_con: Arc<Mutex<get::Client<T>>>,
|
||||
) -> Self {
|
||||
Self::Loading(std::thread::spawn(move || {
|
||||
get_con
|
||||
.lock()
|
||||
@ -1673,7 +1684,10 @@ impl GuiServerImage {
|
||||
.and_then(|v| v.ok())
|
||||
}))
|
||||
}
|
||||
pub fn new_custom_file(file: String, get_con: Arc<Mutex<get::Client<TcpStream>>>) -> Self {
|
||||
pub fn new_custom_file<T: ClientIo + 'static>(
|
||||
file: String,
|
||||
get_con: Arc<Mutex<get::Client<T>>>,
|
||||
) -> Self {
|
||||
Self::Loading(std::thread::spawn(move || {
|
||||
get_con
|
||||
.lock()
|
||||
|
@ -10,7 +10,9 @@ use clap::{Parser, Subcommand};
|
||||
#[cfg(feature = "speedy2d")]
|
||||
use gui::GuiEvent;
|
||||
#[cfg(feature = "playback")]
|
||||
use musicdb_lib::player::Player;
|
||||
use musicdb_lib::data::cache_manager::CacheManager;
|
||||
#[cfg(feature = "playback")]
|
||||
use musicdb_lib::player::{rodio::PlayerBackendRodio, Player};
|
||||
use musicdb_lib::{
|
||||
data::{
|
||||
database::{ClientIo, Database},
|
||||
@ -69,9 +71,17 @@ struct Args {
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum Mode {
|
||||
#[cfg(feature = "speedy2d")]
|
||||
/// graphical user interface
|
||||
#[cfg(feature = "speedy2d")]
|
||||
Gui,
|
||||
/// graphical user interface + syncplayer using local files (syncplayer-network can be enabled in settings)
|
||||
#[cfg(feature = "speedy2d")]
|
||||
#[cfg(feature = "playback")]
|
||||
GuiSyncplayerLocal { lib_dir: PathBuf },
|
||||
/// graphical user interface + syncplayer (syncplayer-network can be toggled in settings)
|
||||
#[cfg(feature = "speedy2d")]
|
||||
#[cfg(feature = "playback")]
|
||||
GuiSyncplayerNetwork,
|
||||
/// play in sync with the server, but load the songs from a local copy of the lib-dir
|
||||
#[cfg(feature = "playback")]
|
||||
SyncplayerLocal { lib_dir: PathBuf },
|
||||
@ -112,6 +122,7 @@ fn main() {
|
||||
Mutex<Option<Box<dyn FnMut(Command) + Send + Sync + 'static>>>,
|
||||
> = Arc::new(Mutex::new(None));
|
||||
let con_thread = {
|
||||
#[cfg(any(feature = "mers", feature = "merscfg"))]
|
||||
let mers_after_db_updated_action = Arc::clone(&mers_after_db_updated_action);
|
||||
let mode = mode.clone();
|
||||
let database = Arc::clone(&database);
|
||||
@ -119,22 +130,52 @@ fn main() {
|
||||
// this is all you need to keep the db in sync
|
||||
thread::spawn(move || {
|
||||
#[cfg(feature = "playback")]
|
||||
let mut player =
|
||||
if matches!(mode, Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork) {
|
||||
Some(Player::new().unwrap().without_sending_commands())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(not(feature = "speedy2d"))]
|
||||
let is_syncplayer =
|
||||
matches!(mode, Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork);
|
||||
#[cfg(feature = "playback")]
|
||||
#[cfg(feature = "speedy2d")]
|
||||
let is_syncplayer = matches!(
|
||||
mode,
|
||||
Mode::SyncplayerLocal { .. }
|
||||
| Mode::SyncplayerNetwork
|
||||
| Mode::GuiSyncplayerLocal { .. }
|
||||
| Mode::GuiSyncplayerNetwork
|
||||
);
|
||||
#[cfg(feature = "playback")]
|
||||
let mut cache_manager = None;
|
||||
#[cfg(feature = "playback")]
|
||||
let mut player = if is_syncplayer {
|
||||
let cm = CacheManager::new(Arc::clone(&database));
|
||||
cache_manager = Some(cm);
|
||||
Some(Player::new_client(
|
||||
PlayerBackendRodio::new_without_command_sending().unwrap(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[allow(unused_labels)]
|
||||
'ifstatementworkaround: {
|
||||
// use if+break instead of if-else because we can't #[cfg(feature)] the if statement,
|
||||
// since we want the else part to run if the feature is disabled
|
||||
#[cfg(feature = "playback")]
|
||||
if let Mode::SyncplayerLocal { lib_dir } = mode {
|
||||
let lib_dir = match &mode {
|
||||
Mode::SyncplayerLocal { lib_dir } => Some(lib_dir.clone()),
|
||||
#[cfg(feature = "speedy2d")]
|
||||
Mode::GuiSyncplayerLocal { lib_dir } => Some(lib_dir.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(lib_dir) = lib_dir {
|
||||
let mut db = database.lock().unwrap();
|
||||
db.lib_directory = lib_dir;
|
||||
break 'ifstatementworkaround;
|
||||
}
|
||||
#[cfg(feature = "speedy2d")]
|
||||
if matches!(mode, Mode::Gui) {
|
||||
// gui does this in the main thread
|
||||
break 'ifstatementworkaround;
|
||||
}
|
||||
let mut db = database.lock().unwrap();
|
||||
let client_con: Box<dyn ClientIo> = Box::new(TcpStream::connect(addr).unwrap());
|
||||
db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new(
|
||||
@ -142,26 +183,27 @@ fn main() {
|
||||
)));
|
||||
}
|
||||
loop {
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(player) = &mut player {
|
||||
let mut db = database.lock().unwrap();
|
||||
if db.is_client_init() {
|
||||
// command_sender does nothing. if a song finishes, we don't want to move to the next song, we want to wait for the server to send the NextSong event.
|
||||
player.update(&mut db, &Arc::new(|_| {}));
|
||||
}
|
||||
}
|
||||
let update = Command::from_bytes(&mut con).unwrap();
|
||||
let mut db = database.lock().unwrap();
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(player) = &mut player {
|
||||
player.handle_command(&update);
|
||||
}
|
||||
#[cfg(any(feature = "mers", feature = "merscfg"))]
|
||||
if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() {
|
||||
database.lock().unwrap().apply_command(update.clone());
|
||||
action(update);
|
||||
} else {
|
||||
database.lock().unwrap().apply_command(update);
|
||||
#[allow(unused_labels)]
|
||||
'feature_if: {
|
||||
#[cfg(any(feature = "mers", feature = "merscfg"))]
|
||||
if let Some(action) = &mut *mers_after_db_updated_action.lock().unwrap() {
|
||||
db.apply_command(update.clone());
|
||||
action(update);
|
||||
break 'feature_if;
|
||||
}
|
||||
db.apply_command(update);
|
||||
}
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(player) = &mut player {
|
||||
player.update_dont_uncache(&mut *db);
|
||||
}
|
||||
drop(db);
|
||||
#[cfg(feature = "speedy2d")]
|
||||
if let Some(v) = &*update_gui_sender.lock().unwrap() {
|
||||
v.send_event(GuiEvent::Refresh).unwrap();
|
||||
@ -171,8 +213,31 @@ fn main() {
|
||||
};
|
||||
match mode {
|
||||
#[cfg(feature = "speedy2d")]
|
||||
Mode::Gui => {
|
||||
Mode::Gui | Mode::GuiSyncplayerLocal { .. } | Mode::GuiSyncplayerNetwork => {
|
||||
{
|
||||
let get_con: Arc<
|
||||
Mutex<musicdb_lib::server::get::Client<Box<dyn ClientIo + 'static>>>,
|
||||
> = Arc::new(Mutex::new(
|
||||
musicdb_lib::server::get::Client::new(BufReader::new(Box::new(
|
||||
TcpStream::connect(addr).expect("opening get client connection"),
|
||||
)
|
||||
as _))
|
||||
.expect("initializing get client connection"),
|
||||
));
|
||||
'anotherifstatement: {
|
||||
#[cfg(feature = "playback")]
|
||||
if let Mode::GuiSyncplayerLocal { lib_dir } = mode {
|
||||
database.lock().unwrap().lib_directory = lib_dir;
|
||||
break 'anotherifstatement;
|
||||
}
|
||||
#[cfg(feature = "playback")]
|
||||
if let Mode::GuiSyncplayerNetwork = mode {
|
||||
break 'anotherifstatement;
|
||||
}
|
||||
// if not using syncplayer-local
|
||||
database.lock().unwrap().remote_server_as_song_file_source =
|
||||
Some(Arc::clone(&get_con));
|
||||
}
|
||||
let occasional_refresh_sender = Arc::clone(&sender);
|
||||
thread::spawn(move || loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
@ -183,10 +248,7 @@ fn main() {
|
||||
gui::main(
|
||||
database,
|
||||
con,
|
||||
musicdb_lib::server::get::Client::new(BufReader::new(
|
||||
TcpStream::connect(addr).expect("opening get client connection"),
|
||||
))
|
||||
.expect("initializing get client connection"),
|
||||
get_con,
|
||||
sender,
|
||||
#[cfg(feature = "merscfg")]
|
||||
&mers_after_db_updated_action,
|
||||
|
@ -12,5 +12,5 @@ rodio = { version = "0.18.0", optional = true }
|
||||
sysinfo = "0.30.12"
|
||||
|
||||
[features]
|
||||
default = ["playback"]
|
||||
# default = ["playback"]
|
||||
playback = ["dep:rodio"]
|
||||
|
@ -163,7 +163,7 @@ impl CacheManager {
|
||||
Err(true) => {
|
||||
break;
|
||||
}
|
||||
Ok(()) => {
|
||||
Ok(true) => {
|
||||
eprintln!(
|
||||
"[{}] CacheManager :: Start caching bytes for song '{}'.",
|
||||
"INFO".cyan(),
|
||||
@ -172,6 +172,7 @@ impl CacheManager {
|
||||
sleep_short = true;
|
||||
break;
|
||||
}
|
||||
Ok(false) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,15 +89,16 @@ impl CachedData {
|
||||
}
|
||||
/// If no data is cached yet and no caching thread is running, starts a thread to cache the data.
|
||||
pub fn cache_data_start_thread(&self, db: &Database, song: &Song) -> bool {
|
||||
self.cache_data_start_thread_or_say_already_running(db, song)
|
||||
.is_ok()
|
||||
self.cache_data_start_thread_or_say_already_running(db, song) == Ok(true)
|
||||
}
|
||||
/// Ok(true) => thread started,
|
||||
/// Ok(false) => data was already loaded
|
||||
pub fn cache_data_start_thread_or_say_already_running(
|
||||
&self,
|
||||
db: &Database,
|
||||
song: &Song,
|
||||
) -> Result<(), bool> {
|
||||
self.get_data_or_start_thread_and_say_already_running(db, |_| (), || (), song)
|
||||
) -> Result<bool, bool> {
|
||||
self.get_data_or_start_thread_and_say_already_running(db, |_| false, || true, song)
|
||||
}
|
||||
/// gets the data if available, or, if no thread is running, starts a thread to get the data.
|
||||
/// if a thread is running, was started, or recently encountered an error, `None` is returned, otherwise `Some(data)`.
|
||||
@ -128,7 +129,7 @@ impl CachedData {
|
||||
) -> Result<T, bool> {
|
||||
let mut cd = self.0.lock().unwrap();
|
||||
match cd.0.as_mut() {
|
||||
Err(Some(i)) if i.elapsed().as_secs_f32() > 60.0 => return Err(false),
|
||||
Err(Some(i)) if i.elapsed().as_secs_f32() < 60.0 => return Err(false),
|
||||
Err(_) => (),
|
||||
Ok(Err(t)) => {
|
||||
if t.is_finished() {
|
||||
|
@ -10,6 +10,7 @@ use crate::{
|
||||
pub struct Player<T: PlayerBackend<SongCustomData>> {
|
||||
cached: HashMap<SongId, CachedData>,
|
||||
pub backend: T,
|
||||
allow_sending_commands: bool,
|
||||
}
|
||||
|
||||
pub struct SongCustomData {
|
||||
@ -80,6 +81,14 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
Self {
|
||||
cached: HashMap::new(),
|
||||
backend,
|
||||
allow_sending_commands: true,
|
||||
}
|
||||
}
|
||||
pub fn new_client(backend: T) -> Self {
|
||||
Self {
|
||||
cached: HashMap::new(),
|
||||
backend,
|
||||
allow_sending_commands: false,
|
||||
}
|
||||
}
|
||||
pub fn handle_command(&mut self, command: &Command) {
|
||||
@ -108,8 +117,10 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
self.update_uncache_opt(db, false)
|
||||
}
|
||||
pub fn update_uncache_opt(&mut self, db: &mut Database, allow_uncaching: bool) {
|
||||
if self.backend.song_finished() {
|
||||
db.apply_command(Command::NextSong);
|
||||
if self.allow_sending_commands {
|
||||
if self.allow_sending_commands && self.backend.song_finished() {
|
||||
db.apply_command(Command::NextSong);
|
||||
}
|
||||
}
|
||||
|
||||
let queue_current_song = db.queue.get_current_song().copied();
|
||||
@ -125,7 +136,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
.next_song()
|
||||
.is_some_and(|(_, _, t)| t.load_duration);
|
||||
self.backend.next(db.playing, load_duration);
|
||||
if load_duration {
|
||||
if self.allow_sending_commands && load_duration {
|
||||
if let Some(dur) = self.backend.current_song_duration() {
|
||||
db.apply_command(Command::SetSongDuration(id, dur))
|
||||
}
|
||||
@ -149,7 +160,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
SongCustomData { load_duration },
|
||||
);
|
||||
self.backend.next(db.playing, load_duration);
|
||||
if load_duration {
|
||||
if self.allow_sending_commands && load_duration {
|
||||
if let Some(dur) = self.backend.current_song_duration() {
|
||||
db.apply_command(Command::SetSongDuration(id, dur))
|
||||
}
|
||||
@ -157,7 +168,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
} else {
|
||||
// only show an error if the user tries to play the song.
|
||||
// otherwise, the error might be spammed.
|
||||
if db.playing {
|
||||
if self.allow_sending_commands && db.playing {
|
||||
db.apply_command(Command::ErrorInfo(
|
||||
format!("Couldn't load bytes for song {id}"),
|
||||
format!(
|
||||
@ -204,7 +215,7 @@ impl<T: PlayerBackend<SongCustomData>> Player<T> {
|
||||
if db.playing {
|
||||
self.backend.resume();
|
||||
// if we can't resume (i.e. there is no song), send `Pause` command
|
||||
if !self.backend.playing() {
|
||||
if self.allow_sending_commands && !self.backend.playing() {
|
||||
db.apply_command(Command::Pause);
|
||||
}
|
||||
} else {
|
||||
|
@ -16,12 +16,20 @@ 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: std::sync::mpsc::Sender<Command>,
|
||||
command_sender: Option<std::sync::mpsc::Sender<Command>>,
|
||||
}
|
||||
|
||||
impl<T> PlayerBackendRodio<T> {
|
||||
pub fn new(
|
||||
command_sender: std::sync::mpsc::Sender<Command>,
|
||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Self::new_with_optional_command_sending(Some(command_sender))
|
||||
}
|
||||
pub fn new_without_command_sending() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Self::new_with_optional_command_sending(None)
|
||||
}
|
||||
pub fn new_with_optional_command_sending(
|
||||
command_sender: Option<std::sync::mpsc::Sender<Command>>,
|
||||
) -> 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)?;
|
||||
@ -48,12 +56,13 @@ impl<T> PlayerBackend<T> for PlayerBackendRodio<T> {
|
||||
) {
|
||||
let decoder = decoder_from_bytes(Arc::clone(&bytes));
|
||||
if let Err(e) = &decoder {
|
||||
self.command_sender
|
||||
.send(Command::ErrorInfo(
|
||||
if let Some(s) = &self.command_sender {
|
||||
s.send(Command::ErrorInfo(
|
||||
format!("Couldn't decode song #{id}!"),
|
||||
format!("Error: '{e}'"),
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
self.next = Some((id, bytes, decoder.ok(), custom_data));
|
||||
}
|
||||
|
@ -229,28 +229,49 @@ pub fn handle_one_connection_as_get(
|
||||
}
|
||||
}
|
||||
"custom-file" => {
|
||||
if let Some(bytes) = request.next().and_then(|path| {
|
||||
let db = db.lock().unwrap();
|
||||
let mut parent = match &db.custom_files {
|
||||
None => None,
|
||||
Some(None) => Some(db.lib_directory.clone()),
|
||||
Some(Some(p)) => Some(p.clone()),
|
||||
};
|
||||
// check for malicious paths [TODO: Improve]
|
||||
if Path::new(path).is_absolute() {
|
||||
parent = None;
|
||||
}
|
||||
if let Some(parent) = parent {
|
||||
let path = parent.join(path);
|
||||
if path.starts_with(parent) {
|
||||
fs::read(path).ok()
|
||||
if let Some(bytes) =
|
||||
request.next().and_then(|path| 'load_custom_file_data: {
|
||||
let db = db.lock().unwrap();
|
||||
let mut parent = match &db.custom_files {
|
||||
None => {
|
||||
if let Some(con) = &db.remote_server_as_song_file_source {
|
||||
if let Ok(Ok(data)) =
|
||||
con.lock().unwrap().custom_file(path)
|
||||
{
|
||||
break 'load_custom_file_data Some(data);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
// if a remote source is present, this means we should ignore it. if no remote source is present, use the lib_dir.
|
||||
Some(None) => {
|
||||
if db.remote_server_as_song_file_source.is_none() {
|
||||
Some(db.lib_directory.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Some(Some(p)) => Some(p.clone()),
|
||||
};
|
||||
// check for malicious paths [TODO: Improve]
|
||||
if Path::new(path).is_absolute() {
|
||||
parent = None;
|
||||
}
|
||||
if let Some(parent) = parent {
|
||||
let path = parent.join(path);
|
||||
if path.starts_with(parent) {
|
||||
fs::read(path).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
})
|
||||
{
|
||||
writeln!(connection.get_mut(), "len: {}", bytes.len())?;
|
||||
connection.get_mut().write_all(&bytes)?;
|
||||
} else {
|
||||
|
@ -1,12 +1,18 @@
|
||||
pub mod get;
|
||||
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
io::{BufRead as _, BufReader, Read, Write},
|
||||
net::{SocketAddr, TcpListener},
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use colorize::AnsiColor;
|
||||
|
||||
#[cfg(feature = "playback")]
|
||||
use crate::player::Player;
|
||||
use crate::server::get::handle_one_connection_as_get;
|
||||
use crate::{
|
||||
data::{
|
||||
album::Album,
|
||||
@ -18,15 +24,6 @@ use crate::{
|
||||
},
|
||||
load::ToFromBytes,
|
||||
};
|
||||
#[cfg(feature = "playback")]
|
||||
use crate::{player::Player, server::get::handle_one_connection_as_get};
|
||||
#[cfg(feature = "playback")]
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
net::{SocketAddr, TcpListener},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Command {
|
||||
@ -117,33 +114,45 @@ impl Command {
|
||||
/// a) initialize new connections using db.init_connection() to synchronize the new client
|
||||
/// b) handle the decoding of messages using Command::from_bytes()
|
||||
/// c) re-encode all received messages using Command::to_bytes_vec(), send them to the db, and send them to all your clients.
|
||||
#[cfg(feature = "playback")]
|
||||
pub fn run_server(
|
||||
database: Arc<Mutex<Database>>,
|
||||
addr_tcp: Option<SocketAddr>,
|
||||
sender_sender: Option<Box<dyn FnOnce(mpsc::Sender<Command>)>>,
|
||||
play_audio: bool,
|
||||
) {
|
||||
run_server_caching_thread_opt(database, addr_tcp, sender_sender, None)
|
||||
run_server_caching_thread_opt(database, addr_tcp, sender_sender, None, play_audio)
|
||||
}
|
||||
#[cfg(feature = "playback")]
|
||||
pub fn run_server_caching_thread_opt(
|
||||
database: Arc<Mutex<Database>>,
|
||||
addr_tcp: Option<SocketAddr>,
|
||||
sender_sender: Option<Box<dyn FnOnce(mpsc::Sender<Command>)>>,
|
||||
caching_thread: Option<Box<dyn FnOnce(&mut crate::data::cache_manager::CacheManager)>>,
|
||||
play_audio: bool,
|
||||
) {
|
||||
#[cfg(not(feature = "playback"))]
|
||||
if play_audio {
|
||||
panic!("Can't run the server: cannot play audio because the `playback` feature was disabled when compiling, but `play_audio` was set to `true`!");
|
||||
}
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
data::cache_manager::CacheManager,
|
||||
player::{rodio::PlayerBackendRodio, PlayerBackend},
|
||||
};
|
||||
use crate::data::cache_manager::CacheManager;
|
||||
#[cfg(feature = "playback")]
|
||||
use crate::player::{rodio::PlayerBackendRodio, PlayerBackend};
|
||||
|
||||
// commands sent to this will be handeled later in this function in an infinite loop.
|
||||
// these commands are sent to the database asap.
|
||||
let (command_sender, command_receiver) = mpsc::channel();
|
||||
|
||||
let mut player = Player::new(PlayerBackendRodio::new(command_sender.clone()).unwrap());
|
||||
#[cfg(feature = "playback")]
|
||||
let mut player = if play_audio {
|
||||
Some(Player::new(
|
||||
PlayerBackendRodio::new(command_sender.clone()).unwrap(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[allow(unused)]
|
||||
let cache_manager = if let Some(func) = caching_thread {
|
||||
let mut cm = CacheManager::new(Arc::clone(&database));
|
||||
func(&mut cm);
|
||||
@ -203,7 +212,12 @@ pub fn run_server_caching_thread_opt(
|
||||
}
|
||||
}
|
||||
}
|
||||
let song_done_polling = player.backend.song_finished_polling();
|
||||
#[cfg(feature = "playback")]
|
||||
let song_done_polling = player
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.backend.song_finished_polling());
|
||||
#[cfg(not(feature = "playback"))]
|
||||
let song_done_polling = false;
|
||||
let (dur, check_every) = if song_done_polling {
|
||||
(Duration::from_millis(50), 200)
|
||||
} else {
|
||||
@ -213,16 +227,23 @@ pub fn run_server_caching_thread_opt(
|
||||
let mut checkf = true;
|
||||
loop {
|
||||
check += 1;
|
||||
if check >= check_every || checkf || player.backend.song_finished() {
|
||||
#[cfg(feature = "playback")]
|
||||
let song_finished = player.as_ref().is_some_and(|p| p.backend.song_finished());
|
||||
#[cfg(not(feature = "playback"))]
|
||||
let song_finished = false;
|
||||
if check >= check_every || checkf || song_finished {
|
||||
check = 0;
|
||||
checkf = false;
|
||||
// at the start and once after every command sent to the server,
|
||||
let mut db = database.lock().unwrap();
|
||||
// update the player
|
||||
if cache_manager.is_some() {
|
||||
player.update_dont_uncache(&mut db);
|
||||
} else {
|
||||
player.update(&mut db);
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(player) = &mut player {
|
||||
if cache_manager.is_some() {
|
||||
player.update_dont_uncache(&mut db);
|
||||
} else {
|
||||
player.update(&mut db);
|
||||
}
|
||||
}
|
||||
// autosave if necessary
|
||||
if let Some((first, last)) = db.times_data_modified {
|
||||
@ -236,7 +257,10 @@ pub fn run_server_caching_thread_opt(
|
||||
}
|
||||
if let Ok(command) = command_receiver.recv_timeout(dur) {
|
||||
checkf = true;
|
||||
player.handle_command(&command);
|
||||
#[cfg(feature = "playback")]
|
||||
if let Some(player) = &mut player {
|
||||
player.handle_command(&command);
|
||||
}
|
||||
database.lock().unwrap().apply_command(command);
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
musicdb-lib = { path = "../musicdb-lib", features = ["playback"] }
|
||||
musicdb-lib = { path = "../musicdb-lib" }
|
||||
clap = { version = "4.4.6", features = ["derive"] }
|
||||
headers = "0.3.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@ -16,5 +16,6 @@ rocket = { version = "0.5.0", optional = true }
|
||||
html-escape = { version = "0.2.13", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["website"]
|
||||
default = ["website", "playback"]
|
||||
website = ["dep:tokio", "dep:rocket", "dep:html-escape"]
|
||||
playback = ["musicdb-lib/playback"]
|
||||
|
@ -2,28 +2,20 @@
|
||||
mod web;
|
||||
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
io::{BufReader, Write},
|
||||
net::{SocketAddr, TcpStream},
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use musicdb_lib::server::run_server_caching_thread_opt;
|
||||
use clap::{Parser, Subcommand};
|
||||
use musicdb_lib::{load::ToFromBytes, server::run_server_caching_thread_opt};
|
||||
|
||||
use musicdb_lib::data::database::Database;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
/// The directory which contains information about the songs in your library
|
||||
#[arg()]
|
||||
db_dir: PathBuf,
|
||||
/// The path containing your actual library.
|
||||
#[arg()]
|
||||
lib_dir: PathBuf,
|
||||
/// skip reading the dbfile (because it doesn't exist yet)
|
||||
#[arg(long)]
|
||||
init: bool,
|
||||
/// optional address for tcp connections to the server
|
||||
#[arg(long)]
|
||||
tcp: Option<SocketAddr>,
|
||||
@ -31,8 +23,15 @@ struct Args {
|
||||
/// requires the `assets/` folder to be present!
|
||||
#[arg(long)]
|
||||
web: Option<SocketAddr>,
|
||||
/// play audio instead of acting like a server
|
||||
#[arg(long)]
|
||||
play_audio: bool,
|
||||
|
||||
/// allow clients to access files in this directory, or the lib_dir if not specified.
|
||||
///
|
||||
/// if source=remote and this is not specified, the remote server's files will be used.
|
||||
/// if source=remote and a path is specified, the local files will be used.
|
||||
/// if source=remote and no path is specified, custom files are disabled even if the remote server has them enabled.
|
||||
#[arg(long)]
|
||||
custom_files: Option<Option<PathBuf>>,
|
||||
|
||||
@ -45,28 +44,150 @@ struct Args {
|
||||
/// Only does something if `--advanced-cache` is used. CacheManager will cache the current, next, ..., songs in the queue, but at most this many songs.
|
||||
#[arg(long, value_name = "number_of_songs", default_value_t = 10)]
|
||||
advanced_cache_song_lookahead_limit: u32,
|
||||
|
||||
// db and song file source
|
||||
#[command(subcommand)]
|
||||
source: Source,
|
||||
}
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Source {
|
||||
Local {
|
||||
/// The directory which contains information about the songs in your library
|
||||
#[arg()]
|
||||
db_dir: PathBuf,
|
||||
/// The path containing your actual library.
|
||||
#[arg()]
|
||||
lib_dir: PathBuf,
|
||||
/// skip reading the dbfile (because it doesn't exist yet)
|
||||
#[arg(long)]
|
||||
init: bool,
|
||||
},
|
||||
Remote {
|
||||
/// The address of another musicdb-server from where to load the songs
|
||||
#[arg()]
|
||||
addr: SocketAddr,
|
||||
},
|
||||
}
|
||||
// struct Args {
|
||||
// /// The directory which contains information about the songs in your library
|
||||
// #[arg()]
|
||||
// db_dir: PathBuf,
|
||||
// /// The path containing your actual library.
|
||||
// #[arg()]
|
||||
// lib_dir: PathBuf,
|
||||
// /// skip reading the dbfile (because it doesn't exist yet)
|
||||
// #[arg(long)]
|
||||
// init: bool,
|
||||
// /// optional address for tcp connections to the server
|
||||
// #[arg(long)]
|
||||
// tcp: Option<SocketAddr>,
|
||||
// /// optional address on which to start a website which can be used on devices without `musicdb-client` to control playback.
|
||||
// /// requires the `assets/` folder to be present!
|
||||
// #[arg(long)]
|
||||
// web: Option<SocketAddr>,
|
||||
|
||||
// /// allow clients to access files in this directory, or the lib_dir if not specified.
|
||||
// #[arg(long)]
|
||||
// custom_files: Option<Option<PathBuf>>,
|
||||
|
||||
// /// Use an extra background thread to cache more songs ahead of time. Useful for remote filesystems or very slow disks. If more than this many MiB of system memory are available, cache more songs.
|
||||
// #[arg(long, value_name = "max_avail_mem_in_mib")]
|
||||
// advanced_cache: Option<u64>,
|
||||
// /// Only does something if `--advanced-cache` is used. If available system memory drops below this amount (in MiB), remove songs from cache.
|
||||
// #[arg(long, value_name = "min_avail_mem_in_mib", default_value_t = 1024)]
|
||||
// advanced_cache_min_mem: u64,
|
||||
// /// Only does something if `--advanced-cache` is used. CacheManager will cache the current, next, ..., songs in the queue, but at most this many songs.
|
||||
// #[arg(long, value_name = "number_of_songs", default_value_t = 10)]
|
||||
// advanced_cache_song_lookahead_limit: u32,
|
||||
// }
|
||||
|
||||
fn main() {
|
||||
// parse args
|
||||
let args = Args::parse();
|
||||
let mut database = if args.init {
|
||||
Database::new_empty_in_dir(args.db_dir, args.lib_dir)
|
||||
} else {
|
||||
match Database::load_database_from_dir(args.db_dir.clone(), args.lib_dir.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
eprintln!("Couldn't load database!");
|
||||
eprintln!(" dbfile: {:?}", args.db_dir);
|
||||
eprintln!(" libdir: {:?}", args.lib_dir);
|
||||
eprintln!(" err: {}", e);
|
||||
exit(1);
|
||||
let mut remote_source_addr = None;
|
||||
let mut database = match args.source {
|
||||
Source::Local {
|
||||
db_dir,
|
||||
lib_dir,
|
||||
init,
|
||||
} => {
|
||||
if init {
|
||||
Database::new_empty_in_dir(db_dir, lib_dir)
|
||||
} else {
|
||||
match Database::load_database_from_dir(db_dir.clone(), lib_dir.clone()) {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
eprintln!("Couldn't load database!");
|
||||
eprintln!(" dbfile: {:?}", db_dir);
|
||||
eprintln!(" libdir: {:?}", lib_dir);
|
||||
eprintln!(" err: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Source::Remote { addr } => {
|
||||
let mut db = Database::new_clientside();
|
||||
db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new(
|
||||
musicdb_lib::server::get::Client::new(BufReader::new(Box::new(
|
||||
TcpStream::connect(&addr).unwrap(),
|
||||
) as _))
|
||||
.unwrap(),
|
||||
)));
|
||||
remote_source_addr = Some(addr);
|
||||
db
|
||||
}
|
||||
};
|
||||
database.custom_files = args.custom_files;
|
||||
// database can be shared by multiple threads using Arc<Mutex<_>>
|
||||
let database = Arc::new(Mutex::new(database));
|
||||
// thread to communicate with the remote server
|
||||
if let Some(addr) = remote_source_addr {
|
||||
let database = Arc::clone(&database);
|
||||
std::thread::spawn(move || {
|
||||
let mut con = TcpStream::connect(addr).unwrap();
|
||||
writeln!(con, "main").unwrap();
|
||||
loop {
|
||||
let cmd = musicdb_lib::server::Command::from_bytes(&mut con).unwrap();
|
||||
use musicdb_lib::server::Command::*;
|
||||
match &cmd {
|
||||
// ignore playback and queue commands
|
||||
Resume | Pause | Stop | NextSong | QueueUpdate(..) | QueueAdd(..)
|
||||
| QueueInsert(..) | QueueRemove(..) | QueueMove(..) | QueueMoveInto(..)
|
||||
| QueueGoto(..) | QueueShuffle(..) | QueueSetShuffle(..)
|
||||
| QueueUnshuffle(..) => continue,
|
||||
SyncDatabase(..)
|
||||
| AddSong(..)
|
||||
| AddAlbum(..)
|
||||
| AddArtist(..)
|
||||
| AddCover(..)
|
||||
| ModifySong(..)
|
||||
| ModifyAlbum(..)
|
||||
| RemoveSong(..)
|
||||
| RemoveAlbum(..)
|
||||
| RemoveArtist(..)
|
||||
| ModifyArtist(..)
|
||||
| SetSongDuration(..)
|
||||
| TagSongFlagSet(..)
|
||||
| TagSongFlagUnset(..)
|
||||
| TagAlbumFlagSet(..)
|
||||
| TagAlbumFlagUnset(..)
|
||||
| TagArtistFlagSet(..)
|
||||
| TagArtistFlagUnset(..)
|
||||
| TagSongPropertySet(..)
|
||||
| TagSongPropertyUnset(..)
|
||||
| TagAlbumPropertySet(..)
|
||||
| TagAlbumPropertyUnset(..)
|
||||
| TagArtistPropertySet(..)
|
||||
| TagArtistPropertyUnset(..)
|
||||
| InitComplete
|
||||
| Save
|
||||
| ErrorInfo(..) => (),
|
||||
}
|
||||
database.lock().unwrap().apply_command(cmd);
|
||||
}
|
||||
});
|
||||
}
|
||||
if args.tcp.is_some() || args.web.is_some() {
|
||||
let mem_min = args.advanced_cache_min_mem;
|
||||
let cache_limit = args.advanced_cache_song_lookahead_limit;
|
||||
@ -84,6 +205,7 @@ fn main() {
|
||||
},
|
||||
) as _
|
||||
}),
|
||||
args.play_audio,
|
||||
);
|
||||
};
|
||||
if let Some(addr) = &args.web {
|
||||
|
Loading…
Reference in New Issue
Block a user