refactor: move and add functions

This commit is contained in:
Thomas Frans
2023-05-27 19:08:45 +02:00
committed by Henrik Friedrichsen
parent 04cbe8ac20
commit c36d3cf272
9 changed files with 183 additions and 133 deletions

View File

@@ -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<String>) -> Result<Credentials, String> {
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<Library>,
/// The music queue which controls playback order.
queue: Arc<Queue>,
/// Internally shared
spotify: Spotify,
/// The configuration provided in the config file.
config: Arc<Config>,
configuration: Arc<Config>,
/// 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<Cursive>,
/// The theme used to draw the user interface.
theme: Rc<Theme>,
}
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<PathBuf>,
configuration_file_path: Option<PathBuf>,
) -> Result<Self, String> {
set_configuration_base_path(configuration_base_path);
pub fn new(configuration_file_path: Option<String>) -> Result<Self, String> {
// 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<Config>| 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) {

View File

@@ -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<RespotCredentials, String> {
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<String>) -> Result<RespotCredentials, String> {
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<RespotCredentials, String> {
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| {

View File

@@ -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<ConfigValues>,
/// 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<String>) -> 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
}
}

View File

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

View File

@@ -42,7 +42,7 @@ pub struct Library {
}
impl Library {
pub fn new(ev: &EventManager, spotify: Spotify, cfg: Arc<Config>) -> Self {
pub fn new(ev: EventManager, spotify: Spotify, cfg: Arc<Config>) -> 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,
};

View File

@@ -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::<PathBuf>("basepath").cloned());
// Create the application.
let mut application = Application::new(
matches.get_one::<PathBuf>("basepath").cloned(),
matches.get_one::<PathBuf>("config").cloned(),
)?;
let mut application = Application::new(matches.get_one::<String>("config").cloned())?;
// Start the application event loop.
application.run()

View File

@@ -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<St
let path = crate::utils::cache_path_for_url(u.to_string());
if !path.exists() {
if let Err(e) = crate::utils::download(u, path.clone()) {
error!("Failed to download cover: {}", e);
log::error!("Failed to download cover: {}", e);
}
}
n.icon(path.to_str().unwrap());
@@ -521,6 +521,6 @@ pub fn send_notification(summary_txt: &str, body_txt: &str, cover_url: Option<St
#[cfg(all(unix, not(target_os = "macos")))]
info!("Created notification: {}", handle.id());
}
Err(e) => error!("Failed to send notification cover: {}", e),
Err(e) => log::error!("Failed to send notification cover: {}", e),
}
}

View File

@@ -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<Theme>,
}
impl Layout {
pub fn new<T: IntoBoxedView>(status: T, ev: &events::EventManager, theme: Theme) -> Layout {
pub fn new<T: IntoBoxedView>(status: T, ev: &events::EventManager, theme: Rc<Theme>) -> Layout {
let style = ColorStyle::new(
ColorType::Color(*theme.palette.custom("cmdline_bg").unwrap()),
ColorType::Color(*theme.palette.custom("cmdline").unwrap()),

View File

@@ -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<CursiveRunner<Cursive>, Box<dyn std::error::Error>> {
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)
}