diff --git a/Cargo.toml b/Cargo.toml index 096c924..867dce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,19 +66,3 @@ indicatif = { version = "0.18.4", optional = true } [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2.182" - -[lints.clippy] -all = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } -nursery = { level = "warn", priority = -1 } - -unwrap_in_result = "warn" -missing_docs_in_private_items = "warn" - -missing_errors_doc = "allow" -missing_panics_doc = "allow" -must_use_candidate = "allow" -cast_precision_loss = "allow" -cast_sign_loss = "allow" -cast_possible_truncation = "allow" -struct_excessive_bools = "allow" diff --git a/src/downloader.rs b/src/downloader.rs index 255dc3b..9891008 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -5,7 +5,7 @@ use std::{ time::Duration, }; -use crate::tracks; +use crate::{tracks, ui::interface::Logger}; use reqwest::Client; use tokio::sync::mpsc; @@ -68,20 +68,60 @@ pub struct Downloader { /// The list of tracks to download from. tracks: tracks::List, + /// Whether to log more debug information. + debug: bool, + /// The [`reqwest`] client to use for downloads. client: Client, /// The RNG generator to use. rng: fastrand::Rng, + + /// Logger, to report any download errors to the UI. + logger: Logger, } impl Downloader { + /// Handles an error encountered by the downloader. + /// + /// If the error isn't a timeout, it will also stall for some arbitrary error timeout. + async fn handle_error(&mut self, error: tracks::Error) -> crate::Result<()> { + const ERROR_TIMEOUT: Duration = Duration::from_secs(1); + + PROGRESS.store(0, atomic::Ordering::Relaxed); + + let track_suffix = if let Some(x) = error.track.as_ref() { + format!(" track: {x}") + } else { + String::new() + }; + + match &error.kind { + tracks::error::Kind::Request(x) => { + if let Some(status) = x.status() { + let message = + format!("track list error code: {}{track_suffix}", status.as_u16()); + self.logger.info(message).await?; + } + } + error if self.debug => { + let message = format!("debug: error: {}{track_suffix}", error); + self.logger.info(message).await?; + } + _ => {} + } + + if !error.timeout() { + tokio::time::sleep(ERROR_TIMEOUT).await; + } + + Ok(()) + } + /// Actually runs the downloader, consuming it and beginning /// the cycle of downloading tracks and reporting to the /// rest of the program. async fn run(mut self) -> crate::Result<()> { - const ERROR_TIMEOUT: Duration = Duration::from_secs(1); - loop { let result = self .tracks @@ -90,18 +130,19 @@ impl Downloader { match result { Ok(track) => { + if self.debug { + self.logger + .info(format!("debug: adding {} to queue", track.display)) + .await?; + } + self.queue.send(track).await?; if LOADING.load(atomic::Ordering::Relaxed) { self.tx.send(crate::Message::Loaded).await?; LOADING.store(false, atomic::Ordering::Relaxed); } } - Err(error) => { - PROGRESS.store(0, atomic::Ordering::Relaxed); - if !error.timeout() { - tokio::time::sleep(ERROR_TIMEOUT).await; - } - } + Err(error) => self.handle_error(error).await?, } } } @@ -146,8 +187,8 @@ impl crate::Tasks { /// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`]. pub fn downloader( &mut self, - size: usize, - timeout: u64, + args: &crate::Args, + logger: Logger, tracks: tracks::List, ) -> crate::Result { let client = Client::builder() @@ -156,11 +197,13 @@ impl crate::Tasks { "/", env!("CARGO_PKG_VERSION") )) - .timeout(Duration::from_secs(timeout)) + .timeout(Duration::from_secs(args.timeout)) .build()?; - let (qtx, qrx) = mpsc::channel(size - 1); + let (qtx, qrx) = mpsc::channel(args.buffer_size as usize - 1); let downloader = Downloader { + debug: args.debug, + logger, queue: qtx, tx: self.tx(), tracks, diff --git a/src/player.rs b/src/player.rs index b2d736a..40ac008 100644 --- a/src/player.rs +++ b/src/player.rs @@ -111,9 +111,10 @@ impl Player { let volume = PersistentVolume::load().await?; sink.set_volume(volume.float()); + let ui = tasks.ui(state, &args).await?; let player = Self { - ui: tasks.ui(state, &args).await?, - downloader: tasks.downloader(args.buffer_size as usize, args.timeout, list)?, + downloader: tasks.downloader(&args, ui.logger(), list)?, + ui, waiter: tasks.waiter(Arc::clone(&sink)), bookmarks: Bookmarks::load().await?, current: Current::default(), diff --git a/src/tracks/list.rs b/src/tracks/list.rs index b94bbe2..0e4ef44 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -28,8 +28,8 @@ pub struct List { #[allow(dead_code)] pub name: String, - /// Just the raw file, but seperated by `/n` (newlines). - /// `lines[0]` is the base/heaeder, with the rest being tracks. + /// Just the raw file, but separated by `/n` (newlines). + /// `lines[0]` is the base/header, with the rest being tracks. pub lines: Vec, /// The file path which the list was read from. @@ -95,7 +95,13 @@ impl List { let result = tokio::fs::read(path.clone()).await.track(x)?; result.into() } else { - let response = client.get(path.clone()).send().await.track(track)?; + let response = client + .get(path.clone()) + .send() + .await? + .error_for_status() + .track(track)?; + let Some(progress) = progress else { let bytes = response.bytes().await.track(track)?; return Ok((bytes, path)); diff --git a/src/ui.rs b/src/ui.rs index fbc4e64..9780cd1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2,7 +2,7 @@ use std::sync::Arc; -use crate::player::Current; +use crate::{player::Current, ui::interface::Logger}; use tokio::{sync::broadcast, time::Instant}; pub mod environment; @@ -34,6 +34,9 @@ pub enum Error { #[error("sharing state between backend and frontend failed: {0}")] StateSend(#[from] tokio::sync::broadcast::error::SendError), + #[error("error sending a log: {0}")] + LogSend(#[from] tokio::sync::mpsc::error::SendError), + #[error("you can't disable the UI without MPRIS!")] RejectedDisable, @@ -114,6 +117,9 @@ pub struct Handle { /// The MPRIS server, which is more or less a handle to the actual MPRIS thread. #[cfg(feature = "mpris")] pub mpris: mpris::Server, + + /// Logger which can be used to log important events. + logger: Logger, } impl Handle { @@ -122,6 +128,11 @@ impl Handle { self.updater.send(update)?; Ok(()) } + + /// Creates a new handle to log events. + pub fn logger(&self) -> Logger { + self.logger.clone() + } } /// The main UI process, which will both render the UI to the terminal @@ -134,11 +145,9 @@ impl Handle { /// and `params` specifies aesthetic options that are specified by the user. pub async fn run( mut updater: broadcast::Receiver, + mut interface: Interface, mut state: State, - params: interface::Params, ) -> Result<()> { - let mut interface = Interface::new(params)?; - loop { if let Ok(message) = updater.try_recv() { match message { diff --git a/src/ui/init.rs b/src/ui/init.rs index fe02613..3ce8833 100644 --- a/src/ui/init.rs +++ b/src/ui/init.rs @@ -13,13 +13,17 @@ impl crate::Tasks { let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?; let params = interface::Params::try_from(args)?; + let interface = interface::Interface::new(params)?; + let logger = interface.logger.clone(); + if params.enabled { - self.spawn(ui::run(urx, state, params)); + self.spawn(ui::run(urx, interface, state)); self.spawn(input::listen(self.tx())); } Ok(ui::Handle { updater: utx, + logger, #[cfg(feature = "mpris")] mpris, }) diff --git a/src/ui/interface.rs b/src/ui/interface.rs index 02a6e44..1170f75 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -13,8 +13,21 @@ pub mod window; pub use clock::Clock; pub use titlebar::TitleBar; +use tokio::sync::mpsc; pub use window::Window; +/// Wrapper around [`mpsc::Sender`] which provides a nice API for logging. +#[derive(Clone)] +pub struct Logger(mpsc::Sender); + +impl Logger { + /// Send an informational log. + pub async fn info(&self, message: String) -> ui::Result<()> { + self.0.send(message).await?; + Ok(()) + } +} + /// UI-specific parameters and options. #[derive(Copy, Clone, Debug)] pub struct Params { @@ -93,6 +106,13 @@ pub struct Interface { /// The interface parameters that control smaller /// aesthetic features and options. params: Params, + + /// Receiver for any truly import live time logs about tracks. + pub logs: mpsc::Receiver, + + /// An instance of the logger which can be cloned to give tasks + /// access to logging. + pub(super) logger: Logger, } impl Default for Interface { @@ -106,10 +126,13 @@ impl Interface { /// Creates a new interface. pub fn new(params: Params) -> ui::Result { let mut window = Window::new(params.width, params.borderless, false, true); + let (sender, logs) = mpsc::channel(8); Ok(Self { clock: params.clock.then(|| Clock::new(&mut window)), interval: tokio::time::interval(params.delta), + logger: Logger(sender), + logs, window, params, }) @@ -148,7 +171,8 @@ impl Interface { } let menu = self.menu(state); - self.window.draw(stdout().lock(), menu)?; + self.window + .draw(stdout().lock(), self.logs.try_recv().ok(), menu)?; self.interval.tick().await; Ok(()) diff --git a/src/ui/interface/window.rs b/src/ui/interface/window.rs index b7ed718..cfae245 100644 --- a/src/ui/interface/window.rs +++ b/src/ui/interface/window.rs @@ -99,14 +99,22 @@ impl Window { } /// Actually draws the window, with each element in `content` being on a new line. + /// + /// If `log` is [`Some`], then it will also print it after clearing, but before the lowfi window. pub fn draw( &mut self, mut writer: impl std::io::Write, + log: Option, content: Vec, ) -> ui::Result<()> { let (rendered, height) = self.render(content)?; + crossterm::queue!(writer, Clear(ClearType::FromCursorDown), MoveToColumn(0))?; - crossterm::execute!( + if let Some(log) = log { + crossterm::queue!(writer, Print(log), Print("\n"), MoveToColumn(0))?; + } + + crossterm::queue!( writer, Clear(ClearType::FromCursorDown), MoveToColumn(0), @@ -115,6 +123,7 @@ impl Window { MoveUp(height - 1), )?; + writer.flush()?; Ok(()) } }