#![cfg_attr(nightly, feature(test))] #[cfg(nightly)] extern crate test; mod tui; use std::{ffi::OsString, fs::OpenOptions, io, path::PathBuf}; use ratatui::{backend::CrosstermBackend, Terminal}; use structopt::StructOpt; use musichoard::{ external::{ database::json::{backend::JsonDatabaseFileBackend, JsonDatabase}, library::beets::{ executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor}, BeetsLibrary, }, }, interface::{ database::{IDatabase, NullDatabase}, library::{ILibrary, NullLibrary}, }, MusicHoardBuilder, NoDatabase, NoLibrary, }; use tui::{App, EventChannel, EventHandler, EventListener, Tui, Ui}; #[derive(StructOpt)] struct Opt { #[structopt(flatten)] lib_opt: LibOpt, #[structopt(flatten)] db_opt: DbOpt, } #[derive(StructOpt)] struct LibOpt { #[structopt(long = "ssh", help = "Beets SSH URI")] beets_ssh_uri: Option, #[structopt(long = "beets", help = "Beets config file path")] beets_config_file_path: Option, #[structopt(long = "no-library", help = "Do not connect to the library")] no_library: bool, } #[derive(StructOpt)] struct DbOpt { #[structopt( long = "database", help = "Database file path", default_value = "database.json" )] database_file_path: PathBuf, #[structopt(long = "no-database", help = "Do not read from/write to the database")] no_database: bool, } fn with(builder: MusicHoardBuilder) { let music_hoard = builder.build().expect("failed to initialise MusicHoard"); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); let terminal = Terminal::new(backend).expect("failed to initialise terminal"); let channel = EventChannel::new(); let listener = EventListener::new(channel.sender()); let handler = EventHandler::new(channel.receiver()); let app = App::new(music_hoard); let ui = Ui; // Run the TUI application. Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); } fn with_database( db_opt: DbOpt, builder: MusicHoardBuilder, ) { if db_opt.no_database { with(builder.set_database(NullDatabase)); } else { // Create an empty database file if it does not exist. match OpenOptions::new() .write(true) .create_new(true) .open(&db_opt.database_file_path) { Ok(f) => { drop(f); JsonDatabase::new(JsonDatabaseFileBackend::new(&db_opt.database_file_path)) .save(&vec![]) .expect("failed to create empty database"); } Err(e) => match e.kind() { io::ErrorKind::AlreadyExists => {} _ => panic!("failed to access database file"), }, } let db_exec = JsonDatabaseFileBackend::new(&db_opt.database_file_path); with(builder.set_database(JsonDatabase::new(db_exec))); }; } fn with_library(lib_opt: LibOpt, db_opt: DbOpt, builder: MusicHoardBuilder) { if lib_opt.no_library { with_database(db_opt, builder.set_library(NullLibrary)); } else if let Some(uri) = lib_opt.beets_ssh_uri { let uri = uri.into_string().expect("invalid SSH URI"); let beets_config_file_path = lib_opt .beets_config_file_path .map(|s| s.into_string()) .transpose() .expect("failed to extract beets config file path"); let lib_exec = BeetsLibrarySshExecutor::new(uri) .expect("failed to initialise beets") .config(beets_config_file_path); with_database(db_opt, builder.set_library(BeetsLibrary::new(lib_exec))); } else { let lib_exec = BeetsLibraryProcessExecutor::default().config(lib_opt.beets_config_file_path); with_database(db_opt, builder.set_library(BeetsLibrary::new(lib_exec))); } } fn main() { let opt = Opt::from_args(); let builder = MusicHoardBuilder::default(); with_library(opt.lib_opt, opt.db_opt, builder); } #[cfg(test)] #[macro_use] mod testmod;