mirror of
https://github.com/talwat/lowfi
synced 2026-05-30 04:13:16 +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]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
libc = "0.2.182"
|
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,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tracks;
|
use crate::{tracks, ui::interface::Logger};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@ -68,20 +68,60 @@ pub struct Downloader {
|
|||||||
/// The list of tracks to download from.
|
/// The list of tracks to download from.
|
||||||
tracks: tracks::List,
|
tracks: tracks::List,
|
||||||
|
|
||||||
|
/// Whether to log more debug information.
|
||||||
|
debug: bool,
|
||||||
|
|
||||||
/// The [`reqwest`] client to use for downloads.
|
/// The [`reqwest`] client to use for downloads.
|
||||||
client: Client,
|
client: Client,
|
||||||
|
|
||||||
/// The RNG generator to use.
|
/// The RNG generator to use.
|
||||||
rng: fastrand::Rng,
|
rng: fastrand::Rng,
|
||||||
|
|
||||||
|
/// Logger, to report any download errors to the UI.
|
||||||
|
logger: Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Downloader {
|
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
|
/// Actually runs the downloader, consuming it and beginning
|
||||||
/// the cycle of downloading tracks and reporting to the
|
/// the cycle of downloading tracks and reporting to the
|
||||||
/// rest of the program.
|
/// rest of the program.
|
||||||
async fn run(mut self) -> crate::Result<()> {
|
async fn run(mut self) -> crate::Result<()> {
|
||||||
const ERROR_TIMEOUT: Duration = Duration::from_secs(1);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let result = self
|
let result = self
|
||||||
.tracks
|
.tracks
|
||||||
@ -90,18 +130,19 @@ impl Downloader {
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(track) => {
|
Ok(track) => {
|
||||||
|
if self.debug {
|
||||||
|
self.logger
|
||||||
|
.info(format!("debug: adding {} to queue", track.display))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
self.queue.send(track).await?;
|
self.queue.send(track).await?;
|
||||||
if LOADING.load(atomic::Ordering::Relaxed) {
|
if LOADING.load(atomic::Ordering::Relaxed) {
|
||||||
self.tx.send(crate::Message::Loaded).await?;
|
self.tx.send(crate::Message::Loaded).await?;
|
||||||
LOADING.store(false, atomic::Ordering::Relaxed);
|
LOADING.store(false, atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => self.handle_error(error).await?,
|
||||||
PROGRESS.store(0, atomic::Ordering::Relaxed);
|
|
||||||
if !error.timeout() {
|
|
||||||
tokio::time::sleep(ERROR_TIMEOUT).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,8 +187,8 @@ impl crate::Tasks {
|
|||||||
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
|
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
|
||||||
pub fn downloader(
|
pub fn downloader(
|
||||||
&mut self,
|
&mut self,
|
||||||
size: usize,
|
args: &crate::Args,
|
||||||
timeout: u64,
|
logger: Logger,
|
||||||
tracks: tracks::List,
|
tracks: tracks::List,
|
||||||
) -> crate::Result<Handle> {
|
) -> crate::Result<Handle> {
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
@ -156,11 +197,13 @@ impl crate::Tasks {
|
|||||||
"/",
|
"/",
|
||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
))
|
))
|
||||||
.timeout(Duration::from_secs(timeout))
|
.timeout(Duration::from_secs(args.timeout))
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let (qtx, qrx) = mpsc::channel(size - 1);
|
let (qtx, qrx) = mpsc::channel(args.buffer_size as usize - 1);
|
||||||
let downloader = Downloader {
|
let downloader = Downloader {
|
||||||
|
debug: args.debug,
|
||||||
|
logger,
|
||||||
queue: qtx,
|
queue: qtx,
|
||||||
tx: self.tx(),
|
tx: self.tx(),
|
||||||
tracks,
|
tracks,
|
||||||
|
|||||||
@ -111,9 +111,10 @@ impl Player {
|
|||||||
let volume = PersistentVolume::load().await?;
|
let volume = PersistentVolume::load().await?;
|
||||||
sink.set_volume(volume.float());
|
sink.set_volume(volume.float());
|
||||||
|
|
||||||
|
let ui = tasks.ui(state, &args).await?;
|
||||||
let player = Self {
|
let player = Self {
|
||||||
ui: tasks.ui(state, &args).await?,
|
downloader: tasks.downloader(&args, ui.logger(), list)?,
|
||||||
downloader: tasks.downloader(args.buffer_size as usize, args.timeout, list)?,
|
ui,
|
||||||
waiter: tasks.waiter(Arc::clone(&sink)),
|
waiter: tasks.waiter(Arc::clone(&sink)),
|
||||||
bookmarks: Bookmarks::load().await?,
|
bookmarks: Bookmarks::load().await?,
|
||||||
current: Current::default(),
|
current: Current::default(),
|
||||||
|
|||||||
@ -28,8 +28,8 @@ pub struct List {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/// Just the raw file, but seperated by `/n` (newlines).
|
/// Just the raw file, but separated by `/n` (newlines).
|
||||||
/// `lines[0]` is the base/heaeder, with the rest being tracks.
|
/// `lines[0]` is the base/header, with the rest being tracks.
|
||||||
pub lines: Vec<String>,
|
pub lines: Vec<String>,
|
||||||
|
|
||||||
/// The file path which the list was read from.
|
/// 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)?;
|
let result = tokio::fs::read(path.clone()).await.track(x)?;
|
||||||
result.into()
|
result.into()
|
||||||
} else {
|
} 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 Some(progress) = progress else {
|
||||||
let bytes = response.bytes().await.track(track)?;
|
let bytes = response.bytes().await.track(track)?;
|
||||||
return Ok((bytes, path));
|
return Ok((bytes, path));
|
||||||
|
|||||||
17
src/ui.rs
17
src/ui.rs
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::player::Current;
|
use crate::{player::Current, ui::interface::Logger};
|
||||||
use tokio::{sync::broadcast, time::Instant};
|
use tokio::{sync::broadcast, time::Instant};
|
||||||
|
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
@ -34,6 +34,9 @@ pub enum Error {
|
|||||||
#[error("sharing state between backend and frontend failed: {0}")]
|
#[error("sharing state between backend and frontend failed: {0}")]
|
||||||
StateSend(#[from] tokio::sync::broadcast::error::SendError<Update>),
|
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!")]
|
#[error("you can't disable the UI without MPRIS!")]
|
||||||
RejectedDisable,
|
RejectedDisable,
|
||||||
|
|
||||||
@ -114,6 +117,9 @@ pub struct Handle {
|
|||||||
/// The MPRIS server, which is more or less a handle to the actual MPRIS thread.
|
/// The MPRIS server, which is more or less a handle to the actual MPRIS thread.
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
pub mpris: mpris::Server,
|
pub mpris: mpris::Server,
|
||||||
|
|
||||||
|
/// Logger which can be used to log important events.
|
||||||
|
logger: Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
@ -122,6 +128,11 @@ impl Handle {
|
|||||||
self.updater.send(update)?;
|
self.updater.send(update)?;
|
||||||
Ok(())
|
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
|
/// 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.
|
/// and `params` specifies aesthetic options that are specified by the user.
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
mut updater: broadcast::Receiver<Update>,
|
mut updater: broadcast::Receiver<Update>,
|
||||||
|
mut interface: Interface,
|
||||||
mut state: State,
|
mut state: State,
|
||||||
params: interface::Params,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut interface = Interface::new(params)?;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(message) = updater.try_recv() {
|
if let Ok(message) = updater.try_recv() {
|
||||||
match message {
|
match message {
|
||||||
|
|||||||
@ -13,13 +13,17 @@ impl crate::Tasks {
|
|||||||
let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?;
|
let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?;
|
||||||
|
|
||||||
let params = interface::Params::try_from(args)?;
|
let params = interface::Params::try_from(args)?;
|
||||||
|
let interface = interface::Interface::new(params)?;
|
||||||
|
let logger = interface.logger.clone();
|
||||||
|
|
||||||
if params.enabled {
|
if params.enabled {
|
||||||
self.spawn(ui::run(urx, state, params));
|
self.spawn(ui::run(urx, interface, state));
|
||||||
self.spawn(input::listen(self.tx()));
|
self.spawn(input::listen(self.tx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ui::Handle {
|
Ok(ui::Handle {
|
||||||
updater: utx,
|
updater: utx,
|
||||||
|
logger,
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
mpris,
|
mpris,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -13,8 +13,21 @@ pub mod window;
|
|||||||
|
|
||||||
pub use clock::Clock;
|
pub use clock::Clock;
|
||||||
pub use titlebar::TitleBar;
|
pub use titlebar::TitleBar;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
pub use window::Window;
|
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.
|
/// UI-specific parameters and options.
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
@ -93,6 +106,13 @@ pub struct Interface {
|
|||||||
/// The interface parameters that control smaller
|
/// The interface parameters that control smaller
|
||||||
/// aesthetic features and options.
|
/// aesthetic features and options.
|
||||||
params: Params,
|
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 {
|
impl Default for Interface {
|
||||||
@ -106,10 +126,13 @@ impl Interface {
|
|||||||
/// Creates a new interface.
|
/// Creates a new interface.
|
||||||
pub fn new(params: Params) -> ui::Result<Self> {
|
pub fn new(params: Params) -> ui::Result<Self> {
|
||||||
let mut window = Window::new(params.width, params.borderless, false, true);
|
let mut window = Window::new(params.width, params.borderless, false, true);
|
||||||
|
let (sender, logs) = mpsc::channel(8);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
clock: params.clock.then(|| Clock::new(&mut window)),
|
clock: params.clock.then(|| Clock::new(&mut window)),
|
||||||
interval: tokio::time::interval(params.delta),
|
interval: tokio::time::interval(params.delta),
|
||||||
|
logger: Logger(sender),
|
||||||
|
logs,
|
||||||
window,
|
window,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@ -148,7 +171,8 @@ impl Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let menu = self.menu(state);
|
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;
|
self.interval.tick().await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -99,14 +99,22 @@ impl Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Actually draws the window, with each element in `content` being on a new line.
|
/// 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(
|
pub fn draw(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut writer: impl std::io::Write,
|
mut writer: impl std::io::Write,
|
||||||
|
log: Option<String>,
|
||||||
content: Vec<String>,
|
content: Vec<String>,
|
||||||
) -> ui::Result<()> {
|
) -> ui::Result<()> {
|
||||||
let (rendered, height) = self.render(content)?;
|
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,
|
writer,
|
||||||
Clear(ClearType::FromCursorDown),
|
Clear(ClearType::FromCursorDown),
|
||||||
MoveToColumn(0),
|
MoveToColumn(0),
|
||||||
@ -115,6 +123,7 @@ impl Window {
|
|||||||
MoveUp(height - 1),
|
MoveUp(height - 1),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
writer.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user