mirror of
https://github.com/talwat/lowfi
synced 2026-04-28 21:03:18 +00:00
feat: logging (#127)
* feat: add basic logging for error codes on tracks * fix: refine log printing to make sure nothing overlaps * docs: add comments for logger
This commit is contained in:
parent
5efecea14e
commit
4891e626e1
16
Cargo.toml
16
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"
|
||||
|
||||
@ -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<Handle> {
|
||||
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,
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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<String>,
|
||||
|
||||
/// 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));
|
||||
|
||||
17
src/ui.rs
17
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<Update>),
|
||||
|
||||
#[error("error sending a log: {0}")]
|
||||
LogSend(#[from] tokio::sync::mpsc::error::SendError<String>),
|
||||
|
||||
#[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<Update>,
|
||||
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 {
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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<String>);
|
||||
|
||||
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<String>,
|
||||
|
||||
/// 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<Self> {
|
||||
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(())
|
||||
|
||||
@ -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<String>,
|
||||
content: Vec<String>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user