From 3093ec1a250f8afa6f4b4b3bc239e983aece6713 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 4 Oct 2023 13:57:55 +0200 Subject: [PATCH] lib_dir is no longer saved in dbfile --- musicdb-client/Cargo.toml | 1 + musicdb-client/src/gui.rs | 2 +- musicdb-client/src/main.rs | 239 +++++-------------------------- musicdb-lib/src/data/database.rs | 23 ++- musicdb-lib/src/server/mod.rs | 8 +- musicdb-server/Cargo.toml | 1 + musicdb-server/src/main.rs | 176 +++++------------------ musicdb-server/src/web.rs | 22 ++- 8 files changed, 108 insertions(+), 364 deletions(-) diff --git a/musicdb-client/Cargo.toml b/musicdb-client/Cargo.toml index d18b41c..022ff0c 100755 --- a/musicdb-client/Cargo.toml +++ b/musicdb-client/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { version = "4.4.6", features = ["derive"] } directories = "5.0.1" musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } regex = "1.9.3" diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index a0fc164..9d03432 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -198,7 +198,7 @@ impl Gui { | Command::Pause | Command::Stop | Command::Save - | Command::SetLibraryDirectory(..) => {} + | Command::InitComplete => {} Command::NextSong | Command::QueueUpdate(..) | Command::QueueAdd(..) diff --git a/musicdb-client/src/main.rs b/musicdb-client/src/main.rs index 34892e7..dd468af 100755 --- a/musicdb-client/src/main.rs +++ b/musicdb-client/src/main.rs @@ -1,5 +1,4 @@ use std::{ - eprintln, fs, io::{BufReader, Write}, net::{SocketAddr, TcpStream}, path::PathBuf, @@ -8,15 +7,12 @@ use std::{ time::Duration, }; +use clap::{Parser, Subcommand}; use gui::GuiEvent; use musicdb_lib::{ data::{ - album::Album, - artist::Artist, - database::{ClientIo, Cover, Database}, - queue::QueueContent, - song::Song, - CoverId, DatabaseLocation, GeneralData, SongId, + database::{ClientIo, Database}, + CoverId, SongId, }, load::ToFromBytes, player::Player, @@ -44,12 +40,24 @@ mod gui_text; mod gui_wrappers; mod textcfg; -#[derive(Clone, Copy)] +#[derive(Parser, Debug)] +struct Args { + /// the address to be used for the tcp connection to the server + addr: SocketAddr, + /// what to do + #[command(subcommand)] + mode: Mode, +} + +#[derive(Subcommand, Debug, Clone)] enum Mode { - Cli, + #[cfg(feature = "speedy2d")] + /// graphical user interface Gui, - SyncPlayer, - SyncPlayerWithoutData, + /// play in sync with the server, but load the songs from a local copy of the lib-dir + SyncplayerLocal { lib_dir: PathBuf }, + /// play in sync with the server, and fetch the songs from it too. slower than the local variant for obvious reasons + SyncplayerNetwork, } fn get_config_file_path() -> PathBuf { @@ -57,36 +65,15 @@ fn get_config_file_path() -> PathBuf { .unwrap() .config_dir() .to_path_buf() - // if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") { - // let mut config_home: PathBuf = config_home.into(); - // config_home.push("musicdb-client"); - // config_home - // } else if let Ok(home) = std::env::var("HOME") { - // let mut config_home: PathBuf = home.into(); - // config_home.push(".config"); - // config_home.push("musicdb-client"); - // config_home - // } else { - // eprintln!("No config directory!"); - // std::process::exit(24); - // } } fn main() { - let mut args = std::env::args().skip(1); - let mode = match args.next().as_ref().map(|v| v.trim()) { - Some("cli") => Mode::Cli, - Some("gui") => Mode::Gui, - Some("syncplayer") => Mode::SyncPlayer, - Some("syncplayernd") => Mode::SyncPlayerWithoutData, - _ => { - println!("Run with argument !"); - return; - } - }; - let addr = args.next().unwrap_or("127.0.0.1:26314".to_string()); - let addr = addr.parse::().unwrap(); + // parse args + let args = Args::parse(); + // start + let addr = args.addr; let mut con = TcpStream::connect(addr).unwrap(); + let mode = args.mode; writeln!(con, "main").unwrap(); let database = Arc::new(Mutex::new(Database::new_clientside())); #[cfg(feature = "speedy2d")] @@ -95,16 +82,21 @@ fn main() { #[cfg(feature = "speedy2d")] let sender = Arc::clone(&update_gui_sender); let con_thread = { + let mode = mode.clone(); let database = Arc::clone(&database); let mut con = con.try_clone().unwrap(); // this is all you need to keep the db in sync thread::spawn(move || { - let mut player = if matches!(mode, Mode::SyncPlayer | Mode::SyncPlayerWithoutData) { - Some(Player::new().unwrap()) + let mut player = + if matches!(mode, Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork) { + Some(Player::new().unwrap()) + } else { + None + }; + if let Mode::SyncplayerLocal { lib_dir } = mode { + let mut db = database.lock().unwrap(); + db.lib_directory = lib_dir; } else { - None - }; - if matches!(mode, Mode::SyncPlayerWithoutData) { let mut db = database.lock().unwrap(); let client_con: Box = Box::new(TcpStream::connect(addr).unwrap()); db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new( @@ -114,7 +106,7 @@ fn main() { loop { if let Some(player) = &mut player { let mut db = database.lock().unwrap(); - if !db.lib_directory.as_os_str().is_empty() { + if db.is_client_init() { player.update(&mut db); } } @@ -131,15 +123,8 @@ fn main() { }) }; match mode { - Mode::Cli => { - Looper { - con: &mut con, - database: &database, - } - .cmd_loop(); - } + #[cfg(feature = "speedy2d")] Mode::Gui => { - #[cfg(feature = "speedy2d")] { let occasional_refresh_sender = Arc::clone(&sender); thread::spawn(move || loop { @@ -159,162 +144,12 @@ fn main() { ) }; } - Mode::SyncPlayer | Mode::SyncPlayerWithoutData => { + Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork => { con_thread.join().unwrap(); } } } -struct Looper<'a> { - pub con: &'a mut TcpStream, - pub database: &'a Arc>, -} -impl<'a> Looper<'a> { - pub fn cmd_loop(&mut self) { - loop { - println!(); - let line = self.read_line(" > enter a command (help for help)"); - let line = line.trim(); - match line { - "resume" => Command::Resume, - "pause" => Command::Pause, - "stop" => Command::Stop, - "next" => Command::NextSong, - "set-lib-dir" => { - let line = self.read_line("Enter the new (absolute) library directory, or leave empty to abort"); - if !line.is_empty() { - Command::SetLibraryDirectory(line.into()) - } else { - continue; - } - }, - "add-song" => { - let song = Song { - id: 0, - location: self.read_line("The songs file is located, relative to the library root, at...").into(), - title: self.read_line("The songs title is..."), - album: self.read_line_ido("The song is part of the album with the id... (empty for None)"), - artist: self.read_line_id("The song is made by the artist with the id..."), - more_artists: accumulate(|| self.read_line_ido("The song is made with support by other artist, one of which has the id... (will ask repeatedly; leave empty once done)")), - cover: self.read_line_ido("The song should use the cover with the id... (empty for None - will default to album or artist cover, if available)"), - general: GeneralData::default(), - cached_data: Arc::new(Mutex::new(None)), - }; - println!("You are about to add the following song to the database:"); - println!(" + {song}"); - if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" { - Command::AddSong(song) - } else { - println!("[-] Aborted - no event will be sent to the database."); - continue; - } - }, - "update-song" => { - let song_id = self.read_line_id("The ID of the song is..."); - if let Some(mut song) = self.database.lock().unwrap().get_song(&song_id).cloned() { - println!("You are now editing the song {song}."); - loop { - match self.read_line("What do you want to edit? (title/album/artist/location or done)").to_lowercase().trim() { - "done" => break, - "title" => { - println!("prev: '{}'", song.title); - song.title = self.read_line(""); - } - "album" => { - println!("prev: '{}'", song.album.map_or(String::new(), |v| v.to_string())); - song.album = self.read_line_ido(""); - } - "artist" => { - println!("prev: '{}'", song.artist); - song.artist = self.read_line_id(""); - } - "location" => { - println!("prev: '{:?}'", song.location); - song.location = self.read_line("").into(); - } - _ => println!("[-] must be title/album/artist/location or done"), - } - } - println!("You are about to update the song:"); - println!(" + {song}"); - if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" { - Command::ModifySong(song) - } else { - println!("[-] Aborted - no event will be sent to the database."); - continue; - } - } else { - println!("[-] No song with that ID found, aborting."); - continue; - } - } - "queue-clear" => Command::QueueUpdate(vec![], QueueContent::Folder(0, vec![], String::new()).into()), - "queue-add-to-end" => Command::QueueAdd(vec![], QueueContent::Song(self.read_line_id("The ID of the song that should be added to the end of the queue is...")).into()), - "save" => Command::Save, - "status" => { - let db = self.database.lock().unwrap(); - println!("DB contains {} songs:", db.songs().len()); - for song in db.songs().values() { - println!("> [{}]: {}", song.id, song); - } - println!("Queue: {:?}, then {:?}", db.queue.get_current(), db.queue.get_next()); - continue; - } - "exit" => { - println!("<< goodbye"); - break; - } - _ => { - println!("Type 'exit' to exit, 'status' to see the db, 'resume', 'pause', 'stop', 'next', 'queue-clear', 'queue-add-to-end', 'add-song', 'add-album', 'add-artist', 'update-song', 'update-album', 'update-artist', 'set-lib-dir', or 'save' to control playback or update the db."); - continue; - } - } - .to_bytes(self.con) - .unwrap(); - } - } - pub fn read_line(&mut self, q: &str) -> String { - loop { - if !q.is_empty() { - println!("{q}"); - } - let mut line = String::new(); - std::io::stdin().read_line(&mut line).unwrap(); - while line.ends_with('\n') || line.ends_with('\r') { - line.pop(); - } - if line.trim() == "#" { - self.cmd_loop(); - } else { - return line; - } - } - } - - pub fn read_line_id(&mut self, q: &str) -> u64 { - loop { - if let Ok(v) = self.read_line(q).trim().parse() { - return v; - } else { - println!("[-] Must be a positive integer."); - } - } - } - pub fn read_line_ido(&mut self, q: &str) -> Option { - loop { - let line = self.read_line(q); - let line = line.trim(); - if line.is_empty() { - return None; - } - if let Ok(v) = line.parse() { - return Some(v); - } else { - println!("[-] Must be a positive integer or nothing for None."); - } - } - } -} pub fn accumulate Option, T>(mut f: F) -> Vec { let mut o = vec![]; loop { diff --git a/musicdb-lib/src/data/database.rs b/musicdb-lib/src/data/database.rs index 0622780..8ddb26b 100755 --- a/musicdb-lib/src/data/database.rs +++ b/musicdb-lib/src/data/database.rs @@ -38,6 +38,8 @@ pub struct Database { pub command_sender: Option>, pub remote_server_as_song_file_source: Option>>>>, + /// only relevant for clients. true if init is done + client_is_init: bool, } pub trait ClientIo: Read + Write + Send {} impl ClientIo for T {} @@ -60,6 +62,9 @@ impl Database { pub fn is_client(&self) -> bool { self.db_file.as_os_str().is_empty() } + pub fn is_client_init(&self) -> bool { + self.client_is_init + } pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf { self.lib_directory.join(&location.rel_path) } @@ -220,7 +225,7 @@ impl Database { } // since this is so easy to check for, it comes last. // this allows clients to find out when init_connection is done. - Command::SetLibraryDirectory(self.lib_directory.clone()).to_bytes(con)?; + Command::InitComplete.to_bytes(con)?; // 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. // we just need to handle commands (receive from the connection). @@ -341,8 +346,8 @@ impl Database { Command::RemoveArtist(artist) => { _ = self.remove_artist(artist); } - Command::SetLibraryDirectory(new_dir) => { - self.lib_directory = new_dir; + Command::InitComplete => { + self.client_is_init = true; } } } @@ -368,6 +373,7 @@ impl Database { playing: false, command_sender: None, remote_server_as_song_file_source: None, + client_is_init: false, } } pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self { @@ -385,13 +391,12 @@ impl Database { playing: false, command_sender: None, remote_server_as_song_file_source: None, + client_is_init: false, } } - pub fn load_database(path: PathBuf) -> Result { + pub fn load_database(path: PathBuf, lib_directory: PathBuf) -> Result { let mut file = BufReader::new(File::open(&path)?); eprintln!("[info] loading library from {file:?}"); - let lib_directory = ToFromBytes::from_bytes(&mut file)?; - eprintln!("[info] library directory is {lib_directory:?}"); Ok(Self { db_file: path, lib_directory, @@ -406,6 +411,7 @@ impl Database { playing: false, command_sender: None, remote_server_as_song_file_source: None, + client_is_init: false, }) } /// saves the database's contents. save path can be overridden @@ -425,7 +431,6 @@ impl Database { .truncate(true) .create(true) .open(&path)?; - self.lib_directory.to_bytes(&mut file)?; self.artists.to_bytes(&mut file)?; self.albums.to_bytes(&mut file)?; self.songs.to_bytes(&mut file)?; @@ -433,6 +438,10 @@ impl Database { Ok(path) } pub fn broadcast_update(&mut self, update: &Command) { + match update { + Command::InitComplete => return, + _ => {} + } let mut remove = vec![]; let mut bytes = None; let mut arc = None; diff --git a/musicdb-lib/src/server/mod.rs b/musicdb-lib/src/server/mod.rs index f4ed84d..e4672e9 100755 --- a/musicdb-lib/src/server/mod.rs +++ b/musicdb-lib/src/server/mod.rs @@ -4,7 +4,6 @@ use std::{ eprintln, io::{BufRead, BufReader, Read, Write}, net::{SocketAddr, TcpListener}, - path::PathBuf, sync::{mpsc, Arc, Mutex}, thread, time::Duration, @@ -51,7 +50,7 @@ pub enum Command { RemoveAlbum(AlbumId), RemoveArtist(ArtistId), ModifyArtist(Artist), - SetLibraryDirectory(PathBuf), + InitComplete, } impl Command { pub fn send_to_server(self, db: &Database) -> Result<(), Self> { @@ -277,9 +276,8 @@ impl ToFromBytes for Command { s.write_all(&[0b11011100])?; artist.to_bytes(s)?; } - Self::SetLibraryDirectory(path) => { + Self::InitComplete => { s.write_all(&[0b00110001])?; - path.to_bytes(s)?; } } Ok(()) @@ -325,7 +323,7 @@ impl ToFromBytes for Command { 0b11010011 => Self::RemoveAlbum(ToFromBytes::from_bytes(s)?), 0b11011100 => Self::RemoveArtist(ToFromBytes::from_bytes(s)?), 0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?), - 0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?), + 0b00110001 => Self::InitComplete, _ => { eprintln!("unexpected byte when reading command; stopping playback."); Self::Stop diff --git a/musicdb-server/Cargo.toml b/musicdb-server/Cargo.toml index 5bfead5..221704f 100755 --- a/musicdb-server/Cargo.toml +++ b/musicdb-server/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] axum = { version = "0.6.19", features = ["headers"] } +clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.28" headers = "0.3.8" musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } diff --git a/musicdb-server/src/main.rs b/musicdb-server/src/main.rs index 5785f8b..82e36b3 100755 --- a/musicdb-server/src/main.rs +++ b/musicdb-server/src/main.rs @@ -1,166 +1,68 @@ mod web; use std::{ + net::SocketAddr, path::PathBuf, process::exit, sync::{Arc, Mutex}, thread, }; +use clap::Parser; use musicdb_lib::server::run_server; use musicdb_lib::data::database::Database; -/* - -# Exit codes - -0 => exited as requested by the user -1 => exit after printing help message -3 => error parsing cli arguments -10 => tried to start with a path that caused some io::Error -11 => tried to start with a path that does not exist (--init prevents this) - -*/ +#[derive(Parser, Debug)] +struct Args { + /// The file which contains information about the songs in your library + #[arg()] + dbfile: 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, + /// 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, +} #[tokio::main] async fn main() { // parse args - let mut args = std::env::args().skip(1); - let mut tcp_addr = None; - let mut web_addr = None; - let mut lib_dir_for_init = None; - let database = if let Some(path_s) = args.next() { - loop { - if let Some(arg) = args.next() { - if arg.starts_with("--") { - match &arg[2..] { - "init" => { - if let Some(lib_dir) = args.next() { - lib_dir_for_init = Some(lib_dir); - } else { - eprintln!( - "[EXIT] -missing argument: --init " - ); - exit(3); - } - } - "tcp" => { - if let Some(addr) = args.next() { - if let Ok(addr) = addr.parse() { - tcp_addr = Some(addr) - } else { - eprintln!( - "[EXIT] -bad argument: --tcp : couldn't parse " - ); - exit(3); - } - } else { - eprintln!( - "[EXIT] -missing argument: --tcp " - ); - exit(3); - } - } - "web" => { - if let Some(addr) = args.next() { - if let Ok(addr) = addr.parse() { - web_addr = Some(addr) - } else { - eprintln!( - "[EXIT] -bad argument: --web : couldn't parse " - ); - exit(3); - } - } else { - eprintln!( - "[EXIT] -missing argument: --web " - ); - exit(3); - } - } - o => { - eprintln!( - "[EXIT] -Unknown long argument --{o}" - ); - exit(3); - } - } - } else if arg.starts_with("-") { - match &arg[1..] { - o => { - eprintln!( - "[EXIT] -Unknown short argument -{o}" - ); - exit(3); - } - } - } else { - eprintln!( - "[EXIT] -Argument didn't start with - or -- ({arg})." - ); - exit(3); - } - } else { - break; - } - } - let path = PathBuf::from(&path_s); - match path.try_exists() { - Ok(exists) => { - if let Some(lib_directory) = lib_dir_for_init { - Database::new_empty(path, lib_directory.into()) - } else if exists { - Database::load_database(path).unwrap() - } else { - eprintln!( - "[EXIT] -The provided path does not exist." - ); - exit(11); - } - } - Err(e) => { - eprintln!( - "[EXIT] -Error getting information about the provided path '{path_s}': {e}" - ); - exit(10); - } - } + let args = Args::parse(); + let database = if args.init { + Database::new_empty(args.dbfile, args.lib_dir) } else { - eprintln!( - "[EXIT] -musicdb-server - help -musicdb-server <...> -options: - --init - --tcp - --web -this help was shown because no arguments were provided." - ); - exit(1); + match Database::load_database(args.dbfile.clone(), args.lib_dir.clone()) { + Ok(db) => db, + Err(e) => { + eprintln!("Couldn't load database!"); + eprintln!(" dbfile: {:?}", args.dbfile); + eprintln!(" libdir: {:?}", args.lib_dir); + eprintln!(" err: {}", e); + exit(1); + } + } }; // database can be shared by multiple threads using Arc> let database = Arc::new(Mutex::new(database)); - if tcp_addr.is_some() || web_addr.is_some() { - if let Some(addr) = web_addr { + if args.tcp.is_some() || args.web.is_some() { + if let Some(addr) = &args.web { let (s, mut r) = tokio::sync::mpsc::channel(2); let db = Arc::clone(&database); - thread::spawn(move || run_server(database, tcp_addr, Some(s))); + thread::spawn(move || run_server(database, args.tcp, Some(s))); if let Some(sender) = r.recv().await { - web::main(db, sender, addr).await; + web::main(db, sender, *addr).await; } } else { - run_server(database, tcp_addr, None); + run_server(database, args.tcp, None); } } else { eprintln!("nothing to do, not starting the server."); diff --git a/musicdb-server/src/web.rs b/musicdb-server/src/web.rs index 0b1cbf3..6222003 100755 --- a/musicdb-server/src/web.rs +++ b/musicdb-server/src/web.rs @@ -438,7 +438,7 @@ async fn sse_handler( .collect::(), ) } - Command::Save | Command::SetLibraryDirectory(_) => return Poll::Pending, + Command::Save | Command::InitComplete => return Poll::Pending, })) } else { return Poll::Pending; @@ -673,17 +673,15 @@ fn build_queue_content_build( HtmlPart::Plain(v) => html.push_str(v), HtmlPart::Insert(key) => match key.as_str() { "path" => html.push_str(&path), - "content" => { - build_queue_content_build( - db, - state, - html, - &inner, - format!("{path}-0"), - current, - true, - ) - } + "content" => build_queue_content_build( + db, + state, + html, + &inner, + format!("{path}-0"), + current, + true, + ), _ => {} }, }