From c36d3cf272a82a09180e53100a09e39f5fa52bed Mon Sep 17 00:00:00 2001 From: Thomas Frans Date: Sat, 27 May 2023 19:08:45 +0200 Subject: [PATCH] refactor: move and add functions --- src/application.rs | 198 ++++++++++++++++++------------------------ src/authentication.rs | 62 ++++++++++++- src/config.rs | 10 ++- src/lib.rs | 1 - src/library.rs | 4 +- src/main.rs | 10 ++- src/queue.rs | 12 +-- src/ui/layout.rs | 5 +- src/ui/mod.rs | 14 +++ 9 files changed, 183 insertions(+), 133 deletions(-) diff --git a/src/application.rs b/src/application.rs index 285d04f..f1d0901 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,12 +1,11 @@ -use crate::{command, ipc, mpris, queue, spotify}; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::rc::Rc; use std::sync::Arc; use cursive::event::EventTrigger; +use cursive::theme::Theme; use cursive::traits::Nameable; use cursive::{Cursive, CursiveRunner}; -use librespot_core::authentication::Credentials; -use librespot_core::cache::Cache; use log::{error, info, trace}; #[cfg(unix)] @@ -14,27 +13,20 @@ use signal_hook::{consts::SIGHUP, consts::SIGTERM, iterator::Signals}; use crate::command::{Command, JumpMode}; use crate::commands::CommandManager; -use crate::config::{self, cache_path, set_configuration_base_path, Config}; +use crate::config::{cache_path, Config}; use crate::events::{Event, EventManager}; use crate::ext_traits::CursiveExt; +use crate::ipc::IpcSocket; use crate::library::Library; +use crate::queue::Queue; use crate::spotify::{PlayerEvent, Spotify}; use crate::ui::contextmenu::ContextMenu; +use crate::ui::create_cursive; use crate::{authentication, ui}; +use crate::{command, ipc, queue, spotify}; -fn credentials_prompt(error_message: Option) -> Result { - if let Some(message) = error_message { - let mut siv = cursive::default(); - let dialog = cursive::views::Dialog::around(cursive::views::TextView::new(format!( - "Connection error:\n{message}" - ))) - .button("Ok", |s| s.quit()); - siv.add_layer(dialog); - siv.run(); - } - - authentication::create_credentials() -} +#[cfg(feature = "mpris")] +use crate::mpris::{self, MprisManager}; /// Set up the global logger to log to `filename`. pub fn setup_logging(filename: &Path) -> Result<(), fern::InitError> { @@ -77,14 +69,24 @@ lazy_static!( pub struct Application { /// The Spotify library, which is obtained from the Spotify API using rspotify. library: Arc, + /// The music queue which controls playback order. + queue: Arc, /// Internally shared spotify: Spotify, /// The configuration provided in the config file. - config: Arc, + configuration: Arc, /// Internally shared event_manager: EventManager, + /// An IPC implementation using the D-Bus MPRIS protocol, used to control and inspect ncspot. + #[cfg(feature = "mpris")] + mpris_manager: MprisManager, + /// An IPC implementation using a Unix domain socket, used to control and inspect ncspot. + #[cfg(unix)] + ipc: IpcSocket, /// The object to render to the terminal. cursive: CursiveRunner, + /// The theme used to draw the user interface. + theme: Rc, } impl Application { @@ -94,97 +96,75 @@ impl Application { /// /// * `configuration_base_path` - Path to the configuration directory /// * `configuration_file_path` - Relative path to the configuration file inside the base path - pub fn new( - configuration_base_path: Option, - configuration_file_path: Option, - ) -> Result { - set_configuration_base_path(configuration_base_path); + pub fn new(configuration_file_path: Option) -> Result { + // Things here may cause the process to abort; we must do them before creating curses + // windows otherwise the error message will not be seen by a user - // Things here may cause the process to abort; we must do them before creating curses windows - // otherwise the error message will not be seen by a user - let config = Arc::new(Config::new( - configuration_file_path.unwrap_or("config.toml".into()), - )); - - let mut credentials = { - let cache = Cache::new(Some(config::cache_path("librespot")), None, None, None) - .expect("Could not create librespot cache"); - let cached_credentials = cache.credentials(); - match cached_credentials { - Some(c) => { - info!("Using cached credentials"); - c - } - None => { - info!("Attempting to resolve credentials via username/password commands"); - let creds = config.values().credentials.clone().unwrap_or_default(); - - match (creds.username_cmd, creds.password_cmd) { - (Some(username_cmd), Some(password_cmd)) => { - authentication::credentials_eval(&username_cmd, &password_cmd)? - } - _ => credentials_prompt(None)?, - } - } - } - }; - - while let Err(error) = spotify::Spotify::test_credentials(credentials.clone()) { - let error_msg = format!("{error}"); - credentials = credentials_prompt(Some(error_msg))?; - } + let configuration = Arc::new(Config::new(configuration_file_path)); + let credentials = authentication::get_credentials(&configuration)?; + let theme = configuration.build_theme(); println!("Connecting to Spotify.."); // DON'T USE STDOUT AFTER THIS CALL! - let backend = cursive::backends::try_default().map_err(|e| e.to_string())?; - let buffered_backend = Box::new(cursive_buffered_backend::BufferedBackend::new(backend)); + let mut cursive = create_cursive().map_err(|error| error.to_string())?; - let mut cursive = cursive::CursiveRunner::new(cursive::Cursive::new(), buffered_backend); - cursive.set_window_title("ncspot"); + cursive.set_theme(theme.clone()); let event_manager = EventManager::new(cursive.cb_sink().clone()); - let spotify = spotify::Spotify::new(event_manager.clone(), credentials, config.clone()); + let spotify = + spotify::Spotify::new(event_manager.clone(), credentials, configuration.clone()); let library = Arc::new(Library::new( - &event_manager, + event_manager.clone(), spotify.clone(), - config.clone(), + configuration.clone(), )); - Ok(Self { - library, - spotify, - config, - event_manager, - cursive, - }) - } - - pub fn run(&mut self) -> Result<(), String> { - let theme = self.config.build_theme(); - self.cursive.set_theme(theme.clone()); - let queue = Arc::new(queue::Queue::new( - self.spotify.clone(), - self.config.clone(), - self.library.clone(), + spotify.clone(), + configuration.clone(), + library.clone(), )); #[cfg(feature = "mpris")] let mpris_manager = mpris::MprisManager::new( - self.event_manager.clone(), + event_manager.clone(), queue.clone(), - self.library.clone(), - self.spotify.clone(), + library.clone(), + spotify.clone(), ); + #[cfg(unix)] + let ipc = ipc::IpcSocket::new( + ASYNC_RUNTIME.handle(), + cache_path("ncspot.sock"), + event_manager.clone(), + ) + .map_err(|e| e.to_string())?; + + Ok(Self { + library, + queue, + spotify, + configuration, + event_manager, + #[cfg(feature = "mpris")] + mpris_manager, + #[cfg(unix)] + ipc, + cursive, + theme: Rc::new(theme), + }) + } + + pub fn run(&mut self) -> Result<(), String> { let mut cmd_manager = CommandManager::new( self.spotify.clone(), - queue.clone(), + self.queue.clone(), self.library.clone(), - self.config.clone(), + self.configuration.clone(), self.event_manager.clone(), ); @@ -196,31 +176,35 @@ impl Application { let search = ui::search::SearchView::new( self.event_manager.clone(), - queue.clone(), + self.queue.clone(), self.library.clone(), ); - let libraryview = ui::library::LibraryView::new(queue.clone(), self.library.clone()); + let libraryview = ui::library::LibraryView::new(self.queue.clone(), self.library.clone()); - let queueview = ui::queue::QueueView::new(queue.clone(), self.library.clone()); + let queueview = ui::queue::QueueView::new(self.queue.clone(), self.library.clone()); #[cfg(feature = "cover")] - let coverview = - ui::cover::CoverView::new(queue.clone(), self.library.clone(), &self.config); + let coverview = ui::cover::CoverView::new( + self.queue.clone(), + self.library.clone(), + &self.configuration, + ); - let status = ui::statusbar::StatusBar::new(queue.clone(), Arc::clone(&self.library)); + let status = ui::statusbar::StatusBar::new(self.queue.clone(), Arc::clone(&self.library)); - let mut layout = ui::layout::Layout::new(status, &self.event_manager, theme) - .screen("search", search.with_name("search")) - .screen("library", libraryview.with_name("library")) - .screen("queue", queueview); + let mut layout = + ui::layout::Layout::new(status, &self.event_manager, Rc::clone(&self.theme)) + .screen("search", search.with_name("search")) + .screen("library", libraryview.with_name("library")) + .screen("queue", queueview); #[cfg(feature = "cover")] layout.add_screen("cover", coverview.with_name("cover")); // initial screen is library let initial_screen = self - .config + .configuration .values() .initial_screen .clone() @@ -235,8 +219,8 @@ impl Application { let cmd_key = |cfg: Arc| cfg.values().command_key.unwrap_or(':'); { - let c = self.config.clone(); - let config_clone = Arc::clone(&self.config); + let c = self.configuration.clone(); + let config_clone = Arc::clone(&self.configuration); self.cursive.set_on_post_event( EventTrigger::from_fn(move |event| { event == &cursive::event::Event::Char(cmd_key(c.clone())) @@ -316,16 +300,6 @@ impl Application { let mut signals = Signals::new([SIGTERM, SIGHUP]).expect("could not register signal handler"); - #[cfg(unix)] - let ipc = { - ipc::IpcSocket::new( - ASYNC_RUNTIME.handle(), - cache_path("ncspot.sock"), - self.event_manager.clone(), - ) - .map_err(|e| e.to_string())? - }; - // cursive event loop while self.cursive.is_running() { self.cursive.step(); @@ -345,17 +319,17 @@ impl Application { self.spotify.update_status(state.clone()); #[cfg(feature = "mpris")] - mpris_manager.update(); + self.mpris_manager.update(); #[cfg(unix)] - ipc.publish(&state, queue.get_current()); + self.ipc.publish(&state, self.queue.get_current()); if state == PlayerEvent::FinishedTrack { - queue.next(false); + self.queue.next(false); } } Event::Queue(event) => { - queue.handle_event(event); + self.queue.handle_event(event); } Event::SessionDied => self.spotify.start_worker(None), Event::IpcInput(input) => match command::parse(&input) { diff --git a/src/authentication.rs b/src/authentication.rs index 3121f77..34ee144 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -3,13 +3,71 @@ use std::process::Command; use cursive::traits::Resizable; use cursive::view::Nameable; use cursive::views::*; -use cursive::{Cursive, CursiveExt}; +use cursive::Cursive; use librespot_core::authentication::Credentials as RespotCredentials; +use librespot_core::cache::Cache; use librespot_protocol::authentication::AuthenticationType; +use log::info; + +use crate::config::{self, Config}; +use crate::spotify::Spotify; +use crate::ui::create_cursive; + +/// Get credentials for use with librespot. This first tries to get cached credentials. If no cached +/// credentials are available, it will either try to get them from the user configured commands, or +/// if that fails, it will prompt the user on stdout. +pub fn get_credentials(configuration: &Config) -> Result { + let mut credentials = { + let cache = Cache::new(Some(config::cache_path("librespot")), None, None, None) + .expect("Could not create librespot cache"); + let cached_credentials = cache.credentials(); + match cached_credentials { + Some(c) => { + info!("Using cached credentials"); + c + } + None => { + info!("Attempting to resolve credentials via username/password commands"); + let creds = configuration + .values() + .credentials + .clone() + .unwrap_or_default(); + + match (creds.username_cmd, creds.password_cmd) { + (Some(username_cmd), Some(password_cmd)) => { + credentials_eval(&username_cmd, &password_cmd)? + } + _ => credentials_prompt(None)?, + } + } + } + }; + + while let Err(error) = Spotify::test_credentials(credentials.clone()) { + let error_msg = format!("{error}"); + credentials = credentials_prompt(Some(error_msg))?; + } + Ok(credentials) +} + +fn credentials_prompt(error_message: Option) -> Result { + if let Some(message) = error_message { + let mut siv = create_cursive().unwrap(); + let dialog = cursive::views::Dialog::around(cursive::views::TextView::new(format!( + "Connection error:\n{message}" + ))) + .button("Ok", |s| s.quit()); + siv.add_layer(dialog); + siv.run(); + } + + create_credentials() +} pub fn create_credentials() -> Result { - let mut login_cursive = Cursive::default(); + let mut login_cursive = create_cursive().unwrap(); let info_buf = TextContent::new("Please login to Spotify\n"); let info_view = Dialog::around(TextView::new_with_content(info_buf)) .button("Login", move |s| { diff --git a/src/config.rs b/src/config.rs index 556b031..1dfedfe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -182,7 +182,7 @@ lazy_static! { /// The complete configuration (state + user configuration) of ncspot. pub struct Config { /// The configuration file path. - filename: PathBuf, + filename: String, /// Configuration set by the user, read only. values: RwLock, /// Runtime state which can't be edited by the user, read/write. @@ -191,8 +191,10 @@ pub struct Config { impl Config { /// Generate the configuration from the user configuration file and the runtime state file. - pub fn new(filename: PathBuf) -> Self { - let values = load(&filename.to_string_lossy()).unwrap_or_else(|e| { + /// `filename` can be used to look for a differently named configuration file. + pub fn new(filename: Option) -> Self { + let filename = filename.unwrap_or("config.toml".to_owned()); + let values = load(&filename).unwrap_or_else(|e| { eprintln!("could not load config: {e}"); process::exit(1); }); @@ -256,7 +258,7 @@ impl Config { /// Reload the configuration file. pub fn reload(&self) { - let cfg = load(&self.filename.to_string_lossy()).expect("could not reload config"); + let cfg = load(&self.filename).expect("could not reload config"); *self.values.write().expect("can't writelock config values") = cfg } } diff --git a/src/lib.rs b/src/lib.rs index b3b8a87..c2c349a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,6 @@ pub fn program_arguments() -> clap::Command { .short('c') .long("config") .value_name("FILE") - .value_parser(PathBufValueParser::new()) .help("Filename of config file in basepath") .default_value("config.toml"), ) diff --git a/src/library.rs b/src/library.rs index af83416..a216d8e 100644 --- a/src/library.rs +++ b/src/library.rs @@ -42,7 +42,7 @@ pub struct Library { } impl Library { - pub fn new(ev: &EventManager, spotify: Spotify, cfg: Arc) -> Self { + pub fn new(ev: EventManager, spotify: Spotify, cfg: Arc) -> Self { let current_user = spotify.api.current_user(); let user_id = current_user.as_ref().map(|u| u.id.id().to_string()); let display_name = current_user.as_ref().and_then(|u| u.display_name.clone()); @@ -56,7 +56,7 @@ impl Library { is_done: Arc::new(RwLock::new(false)), user_id, display_name, - ev: ev.clone(), + ev, spotify, cfg, }; diff --git a/src/main.rs b/src/main.rs index ea53c82..df5d14e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ extern crate serde; use std::path::PathBuf; use application::{setup_logging, Application}; +use config::set_configuration_base_path; use ncspot::program_arguments; mod application; @@ -51,11 +52,12 @@ fn main() -> Result<(), String> { setup_logging(filename).expect("logger could not be initialized"); } + // Set the configuration base path. All configuration files are read/written relative to this + // path. + set_configuration_base_path(matches.get_one::("basepath").cloned()); + // Create the application. - let mut application = Application::new( - matches.get_one::("basepath").cloned(), - matches.get_one::("config").cloned(), - )?; + let mut application = Application::new(matches.get_one::("config").cloned())?; // Start the application event loop. application.run() diff --git a/src/queue.rs b/src/queue.rs index dd83307..9f7afe8 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,14 +1,14 @@ use std::cmp::Ordering; use std::sync::{Arc, RwLock}; -use log::{debug, error, info}; +use log::{debug, info}; #[cfg(feature = "notify")] use notify_rust::{Hint, Notification, Urgency}; use rand::prelude::*; use strum_macros::Display; -use crate::config::{Config, NotificationFormat, PlaybackState}; +use crate::config::{Config, PlaybackState}; use crate::library::Library; use crate::model::playable::Playable; use crate::spotify::PlayerEvent; @@ -321,10 +321,10 @@ impl Queue { .notification_format .clone() .unwrap_or_default(); - let default_title = NotificationFormat::default().title.unwrap(); + let default_title = crate::config::NotificationFormat::default().title.unwrap(); let title = format.title.unwrap_or_else(|| default_title.clone()); - let default_body = NotificationFormat::default().body.unwrap(); + let default_body = crate::config::NotificationFormat::default().body.unwrap(); let body = format.body.unwrap_or_else(|| default_body.clone()); let summary_txt = Playable::format(track, &title, &self.library); @@ -503,7 +503,7 @@ pub fn send_notification(summary_txt: &str, body_txt: &str, cover_url: Option error!("Failed to send notification cover: {}", e), + Err(e) => log::error!("Failed to send notification cover: {}", e), } } diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 9081fa6..9658ca4 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::rc::Rc; use std::time::{Duration, SystemTime}; use cursive::align::HAlign; @@ -30,11 +31,11 @@ pub struct Layout { screenchange: bool, last_size: Vec2, ev: events::EventManager, - theme: Theme, + theme: Rc, } impl Layout { - pub fn new(status: T, ev: &events::EventManager, theme: Theme) -> Layout { + pub fn new(status: T, ev: &events::EventManager, theme: Rc) -> Layout { let style = ColorStyle::new( ColorType::Color(*theme.palette.custom("cmdline_bg").unwrap()), ColorType::Color(*theme.palette.custom("cmdline").unwrap()), diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7b9be56..e8783f7 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,6 @@ +use cursive::{Cursive, CursiveRunner}; +use ncspot::BIN_NAME; + pub mod album; pub mod artist; pub mod browse; @@ -19,3 +22,14 @@ pub mod tabview; #[cfg(feature = "cover")] pub mod cover; + +/// Create a CursiveRunner which implements the drawing logic and event loop. +pub fn create_cursive() -> Result, Box> { + let backend = cursive::backends::try_default()?; + let buffered_backend = Box::new(cursive_buffered_backend::BufferedBackend::new(backend)); + let mut cursive_runner = CursiveRunner::new(cursive::Cursive::new(), buffered_backend); + + cursive_runner.set_window_title(BIN_NAME); + + Ok(cursive_runner) +}