From 08ecdea1e0f19b109a37587453bfc4344b7aad4f Mon Sep 17 00:00:00 2001 From: Dark-Alex-17 Date: Tue, 8 Aug 2023 10:50:04 -0600 Subject: [PATCH] Implemented basic stats functionality and started setting up menu --- src/app/mod.rs | 1 + src/app/radarr.rs | 16 ++--- src/logos.rs | 51 +++++++++++++++ src/main.rs | 1 + src/network/mod.rs | 1 + src/network/radarr.rs | 149 +++++++++++++++++++++++++++--------------- src/ui/mod.rs | 106 +++++++++++++++++++++++------- src/ui/utils.rs | 47 +++++++++++++ 8 files changed, 288 insertions(+), 84 deletions(-) create mode 100644 src/logos.rs create mode 100644 src/ui/utils.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index 9d98bd2..d3b74a0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -47,6 +47,7 @@ impl App { pub async fn on_tick(&mut self) { if self.tick_count % self.tick_until_poll == 0 { self.dispatch(RadarrEvent::GetOverview).await; + self.dispatch(RadarrEvent::GetStatus).await; } self.tick_count += 1; diff --git a/src/app/radarr.rs b/src/app/radarr.rs index 45b09ab..760de04 100644 --- a/src/app/radarr.rs +++ b/src/app/radarr.rs @@ -1,16 +1,10 @@ +use chrono::Duration; + use crate::network::radarr::DiskSpace; #[derive(Default, Debug)] pub struct RadarrData { - pub free_space: u64, - pub total_space: u64, -} - -impl From<&DiskSpace> for RadarrData { - fn from(disk_space: &DiskSpace) -> Self { - RadarrData { - free_space: disk_space.free_space.as_u64().unwrap(), - total_space: disk_space.total_space.as_u64().unwrap() - } - } + pub free_space: u64, + pub total_space: u64, + pub version: String, } diff --git a/src/logos.rs b/src/logos.rs new file mode 100644 index 0000000..73e0fcf --- /dev/null +++ b/src/logos.rs @@ -0,0 +1,51 @@ +pub const RADARR_LOGO: &str = "⠀⣠⣶⢶⣶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⣿⡇⠀⠈⠙⠻⢿⣶⣤⡀⠀⠀⠀⠀ +⠀⣿⡇⠀⠀⠀⠀⠀⠈⠙⠻⢷⣦⡄⠀ +⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢉⠻⠀ +⠀⣿⡇⠀⠀⠀⠀⠀⢀⣠⣴⣾⠿⠀⠀ +⠀⢿⡇⠀⠀⣀⣤⣶⡿⠛⠉⠀⠀⠀⠀ +⠀⠀⠰⠶⡿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀ +"; +pub const SONARR_LOGO: &str = "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⢀⣄⠙⠻⠟⠋⣤⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⢸⣿⠆⢾⡗⢸⣿⡇⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠈⠋⣠⣴⣦⣄⠛⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ +"; +pub const BAZARR_LOGO: &str = "⠀⠀⠀⠀⠀⠀⠀⣀⠠⠄⠠⠄⣀⠀⠀⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⡠⢊⣀⣀⣀⣀⣀⣀⡑⢄⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠐⢸⣿⣿⣿⣿⣿⣿⣿⣿⡇⠂⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠠⠸⣿⣶⣒⣒⣒⣒⣶⣿⠇⠄⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠑⢌⠉⠉⠉⠉⠉⠉⡠⠊⠀⠀⠀⠀⠀ +⠀⠀⠀⠀⠀⠀⠀⠉⠒⠂⠐⠒⠉⠀⠀⠀⠀⠀⠀⠀ +"; +pub const READARR_LOGO: &str = "⠀⠀⠀⠀⠀⣀⣠⣤⣄⣀⠀⠀⠀⠀⠀ +⠀⠀⢀⡴⠛⠉⠀⠀⠀⠉⠛⢦⡀⠀⠀ +⠀⢠⣯⣄⣀⣐⠻⣿⠟⣂⣀⣠⣽⡄⠀ +⠀⣾⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⣷⠀ +⠀⢿⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⡿⠀ +⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀ +⠀⠀⠈⠳⣬⣙⠻⠿⠟⣋⣥⠞⠁⠀⠀ +⠀⠀⠀⠀⠀⠉⠙⠛⠋⠉⠀⠀⠀⠀⠀ +"; +pub const LIDARR_LOGO: &str = "⠀⠀⠀⣠⣴⣶⡿⠻⣿⣶⣦⣄⠀⠀⠀ +⠀⢠⣾⠟⠋⠀⠀⢀⣀⠀⠙⠻⣷⡄⠀ +⢠⣿⠋⠀⣴⠃⠀⢸⣿⣿⣦⡀⠙⣿⡄ +⣾⡟⠀⢸⠃⠀⠀⠀⠈⠉⠉⠁⠀⢹⣷ +⢿⣧⠀⠈⠀⠀⠀⠀⠀⠀⢀⡟⠀⣸⡿ +⠘⣿⣄⠀⠻⣿⣿⡇⠀⢀⠞⠀⣠⣿⠃ +⠀⠘⢿⣦⣄⠀⠉⠁⠀⠀⣠⣴⡿⠃⠀ +⠀⠀⠀⠉⠻⠿⢿⡆⡾⠿⠟⠉⠀⠀⠀ +"; +pub const PROWLARR_LOGO: &str = "⠀⠀⠀⠀⢀⣠⣠⣤⣄⣄⠀⠀⠀⠀⠀ +⠀⠀⢠⠚⢫⣷⣷⣷⣷⣯⠟⠒⡄⠀⠀ +⠀⢰⣗⣤⣜⢿⣿⢿⣿⢟⣥⣤⡻⡄⠀ +⠀⣟⣾⣿⣿⡗⠁⠠⠈⣹⣿⣿⡯⣷ +⠀⢿⣺⣿⣿⣇⠌⢀⢁⢼⣿⣿⡯⡟⠀ +⠀⠘⣞⠋⢫⣾⣿⣶⣿⣷⠝⠛⣽⠃⠀ +⠀⠁⠐⠤⣜⡿⢿⢿⢿⢟⣧⠔⠀⠀⠀ +⠀⠀⠀⠀⠀⢉⠙⠋⠋⠉⠀⠀⠀⠀⠀ +"; diff --git a/src/main.rs b/src/main.rs index fe13d10..2c93b0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ use crate::ui::ui; mod app; mod event; mod handlers; +mod logos; mod network; mod ui; mod utils; diff --git a/src/network/mod.rs b/src/network/mod.rs index a1ffc4b..4a5d059 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -11,6 +11,7 @@ pub(crate) mod radarr; pub enum RadarrEvent { HealthCheck, GetOverview, + GetStatus, } pub struct Network<'a> { diff --git a/src/network/radarr.rs b/src/network/radarr.rs index 81e2cc3..7881833 100644 --- a/src/network/radarr.rs +++ b/src/network/radarr.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + use anyhow::Result; use log::{debug, error}; use reqwest::RequestBuilder; @@ -9,71 +11,116 @@ use crate::app::RadarrConfig; use crate::network::{Network, RadarrEvent}; impl RadarrEvent { - const fn resource(self) -> &'static str { - match self { - RadarrEvent::HealthCheck => "/health", - RadarrEvent::GetOverview => "/diskspace" - } + const fn resource(self) -> &'static str { + match self { + RadarrEvent::HealthCheck => "/health", + RadarrEvent::GetOverview => "/diskspace", + RadarrEvent::GetStatus => "/system/status", } + } } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct DiskSpace { - pub path: String, - pub label: String, - pub free_space: Number, - pub total_space: Number + pub path: String, + pub label: String, + pub free_space: Number, + pub total_space: Number, +} + +#[derive(Deserialize, Debug)] +pub struct SystemStatus { + version: String, } impl<'a> Network<'a> { - pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) { - match radarr_event { - RadarrEvent::HealthCheck => { - self.healthcheck(RadarrEvent::HealthCheck.resource()).await; - } - RadarrEvent::GetOverview => { - match self.diskspace(RadarrEvent::GetOverview.resource()).await { - Ok(disk_space_vec) => { - let mut app = self.app.lock().await; - app.data.radarr_data = RadarrData::from(&disk_space_vec[0]); - } - Err(e) => { - error!("Failed to fetch disk space. {:?}", e); - } - } - } + pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) { + match radarr_event { + RadarrEvent::HealthCheck => { + self.healthcheck(RadarrEvent::HealthCheck.resource()).await; + } + RadarrEvent::GetOverview => match self.diskspace(RadarrEvent::GetOverview.resource()).await { + Ok(disk_space_vec) => { + let mut app = self.app.lock().await; + let DiskSpace { + free_space, + total_space, + .. + } = &disk_space_vec[0]; + app.data.radarr_data.free_space = free_space.as_u64().unwrap(); + app.data.radarr_data.total_space = total_space.as_u64().unwrap(); } - - let mut app = self.app.lock().await; - app.reset(); - } - - async fn healthcheck(&self, resource: &str) { - if let Err(e) = self.call_radarr_api(resource).await.send().await { - error!("Healthcheck failed. {:?}", e) + Err(e) => { + error!("Failed to fetch disk space. {:?}", e); } + }, + RadarrEvent::GetStatus => match self.status(RadarrEvent::GetStatus.resource()).await { + Ok(system_status) => { + let mut app = self.app.lock().await; + app.data.radarr_data.version = system_status.version; + } + Err(e) => { + error!("Failed to fetch system status. {:?}", e); + } + }, } - async fn diskspace(&self, resource: &str) -> Result> { - debug!("Handling diskspace event: {:?}", resource); + let mut app = self.app.lock().await; + app.reset(); + } - Ok( - self.call_radarr_api(resource) - .await - .send() - .await? - .json::>() - .await? - ) + async fn healthcheck(&self, resource: &str) { + if let Err(e) = self.call_radarr_api(resource).await.send().await { + error!("Healthcheck failed. {:?}", e) } + } - async fn call_radarr_api(&self, resource: &str) -> RequestBuilder { - debug!("Creating RequestBuilder for resource: {:?}", resource); - let app = self.app.lock().await; - let RadarrConfig { host, port, api_token } = &app.config.radarr; + async fn diskspace(&self, resource: &str) -> Result> { + debug!("Handling diskspace event: {:?}", resource); - app.client.get(format!("http://{}:{}/api/v3{}", host, port.unwrap_or(7878), resource)) - .header("X-Api-Key", api_token) - } -} \ No newline at end of file + Ok( + self + .call_radarr_api(resource) + .await + .send() + .await? + .json::>() + .await?, + ) + } + + async fn status(&self, resource: &str) -> Result { + debug!("Handling system status event: {:?}", resource); + + Ok( + self + .call_radarr_api(resource) + .await + .send() + .await? + .json::() + .await?, + ) + } + + async fn call_radarr_api(&self, resource: &str) -> RequestBuilder { + debug!("Creating RequestBuilder for resource: {:?}", resource); + let app = self.app.lock().await; + let RadarrConfig { + host, + port, + api_token, + } = &app.config.radarr; + + app + .client + .get(format!( + "http://{}:{}/api/v3{}", + host, + port.unwrap_or(7878), + resource + )) + .header("X-Api-Key", api_token) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index fd78b6a..1ea3e3c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,31 +1,93 @@ use tui::backend::Backend; -use tui::Frame; -use tui::layout::{Constraint, Direction, Layout}; +use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; +use tui::style::Color::Cyan; use tui::style::{Color, Style}; -use tui::widgets::{Block, Borders, Gauge}; +use tui::text::{Spans, Text}; +use tui::widgets::{Block, Borders, Gauge, LineGauge, Paragraph}; +use tui::{symbols, Frame}; -use crate::app::App; use crate::app::radarr::RadarrData; +use crate::app::App; +use crate::logos::{ + BAZARR_LOGO, LIDARR_LOGO, PROWLARR_LOGO, RADARR_LOGO, READARR_LOGO, SONARR_LOGO, +}; +use crate::ui::utils::{ + horizontal_chunks, horizontal_chunks_with_margin, vertical_chunks, vertical_chunks_with_margin, +}; + +mod utils; pub fn ui(f: &mut Frame, app: &App) { - let RadarrData { free_space, total_space } = app.data.radarr_data; - let ratio = if total_space == 0 { - 0f64 - } else { - 1f64 - (free_space as f64 / total_space as f64) - }; + let main_chunks = vertical_chunks( + vec![Constraint::Length(20), Constraint::Length(0)], + f.size(), + ); - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) - .split(f.size()); + draw_context_row(f, app, main_chunks[0]); + f.render_widget(Block::default().borders(Borders::ALL), main_chunks[1]); +} - let gauge = Gauge::default() - .block(Block::default() - .title("Free Space") - .borders(Borders::ALL)) - .gauge_style(Style::default().fg(Color::Cyan)) - .ratio(ratio); +fn draw_context_row(f: &mut Frame<'_, B>, app: &App, area: Rect) { + let chunks = horizontal_chunks( + vec![ + Constraint::Percentage(23), + Constraint::Percentage(23), + Constraint::Percentage(23), + Constraint::Percentage(23), + Constraint::Length(20), + ], + area, + ); - f.render_widget(gauge, chunks[0]); -} \ No newline at end of file + draw_stats(f, app, chunks[0]); + f.render_widget(Block::default().borders(Borders::ALL), chunks[1]); + f.render_widget(Block::default().borders(Borders::ALL), chunks[2]); + f.render_widget(Block::default().borders(Borders::ALL), chunks[3]); + + draw_logo(f, chunks[4]); +} + +fn draw_stats(f: &mut Frame<'_, B>, app: &App, area: Rect) { + let RadarrData { + free_space, + total_space, + .. + } = app.data.radarr_data; + let ratio = if total_space == 0 { + 0f64 + } else { + 1f64 - (free_space as f64 / total_space as f64) + }; + + let base_block = Block::default().title("Stats").borders(Borders::ALL); + f.render_widget(base_block, area); + + let chunks = + vertical_chunks_with_margin(vec![Constraint::Length(1), Constraint::Min(2)], area, 1); + + let version = Paragraph::new(Text::from(format!( + "Version: {}", + app.data.radarr_data.version + ))) + .block(Block::default()); + f.render_widget(version, chunks[0]); + + let space_gauge = LineGauge::default() + .block(Block::default().title("Storage:")) + .gauge_style(Style::default().fg(Cyan)) + .line_set(symbols::line::THICK) + .ratio(ratio) + .label(Spans::from(format!("{:.0}%", ratio * 100.0))); + f.render_widget(space_gauge, chunks[1]); +} + +fn draw_logo(f: &mut Frame<'_, B>, area: Rect) { + let chunks = vertical_chunks( + vec![Constraint::Percentage(60), Constraint::Percentage(40)], + area, + ); + let logo = Paragraph::new(Text::from(RADARR_LOGO)) + .block(Block::default()) + .alignment(Alignment::Center); + f.render_widget(logo, chunks[0]); +} diff --git a/src/ui/utils.rs b/src/ui/utils.rs new file mode 100644 index 0000000..e06cd40 --- /dev/null +++ b/src/ui/utils.rs @@ -0,0 +1,47 @@ +use tui::layout::{Constraint, Direction, Layout, Rect}; + +pub fn horizontal_chunks(constraints: Vec, size: Rect) -> Vec { + Layout::default() + .constraints( as AsRef<[Constraint]>>::as_ref( + &constraints, + )) + .direction(Direction::Horizontal) + .split(size) +} + +pub fn horizontal_chunks_with_margin( + constraints: Vec, + size: Rect, + margin: u16, +) -> Vec { + Layout::default() + .constraints( as AsRef<[Constraint]>>::as_ref( + &constraints, + )) + .direction(Direction::Horizontal) + .margin(margin) + .split(size) +} + +pub fn vertical_chunks(constraints: Vec, size: Rect) -> Vec { + Layout::default() + .constraints( as AsRef<[Constraint]>>::as_ref( + &constraints, + )) + .direction(Direction::Vertical) + .split(size) +} + +pub fn vertical_chunks_with_margin( + constraints: Vec, + size: Rect, + margin: u16, +) -> Vec { + Layout::default() + .constraints( as AsRef<[Constraint]>>::as_ref( + &constraints, + )) + .direction(Direction::Vertical) + .margin(margin) + .split(size) +}