Implemented basic stats functionality and started setting up menu

This commit is contained in:
2023-08-08 10:50:04 -06:00
parent 155675b596
commit 08ecdea1e0
8 changed files with 288 additions and 84 deletions
+1
View File
@@ -47,6 +47,7 @@ impl App {
pub async fn on_tick(&mut self) { pub async fn on_tick(&mut self) {
if self.tick_count % self.tick_until_poll == 0 { if self.tick_count % self.tick_until_poll == 0 {
self.dispatch(RadarrEvent::GetOverview).await; self.dispatch(RadarrEvent::GetOverview).await;
self.dispatch(RadarrEvent::GetStatus).await;
} }
self.tick_count += 1; self.tick_count += 1;
+5 -11
View File
@@ -1,16 +1,10 @@
use chrono::Duration;
use crate::network::radarr::DiskSpace; use crate::network::radarr::DiskSpace;
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct RadarrData { pub struct RadarrData {
pub free_space: u64, pub free_space: u64,
pub total_space: u64, pub total_space: u64,
} pub version: String,
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()
}
}
} }
+51
View File
@@ -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 = "⠀⠀⠀⠀⢀⣠⣠⣤⣄⣄⠀⠀⠀⠀⠀
⠀⠀⢠⠚⢫⣷⣷⣷⣷⣯⠟⠒⡄⠀⠀
⠀⢰⣗⣤⣜⢿⣿⢿⣿⢟⣥⣤⡻⡄⠀
⠀⣟⣾⣿⣿⡗⠁⠠⠈⣹⣿⣿⡯⣷
⠀⢿⣺⣿⣿⣇⠌⢀⢁⢼⣿⣿⡯⡟⠀
⠀⠘⣞⠋⢫⣾⣿⣶⣿⣷⠝⠛⣽⠃⠀
⠀⠁⠐⠤⣜⡿⢿⢿⢿⢟⣧⠔⠀⠀⠀
⠀⠀⠀⠀⠀⢉⠙⠋⠋⠉⠀⠀⠀⠀⠀
";
+1
View File
@@ -24,6 +24,7 @@ use crate::ui::ui;
mod app; mod app;
mod event; mod event;
mod handlers; mod handlers;
mod logos;
mod network; mod network;
mod ui; mod ui;
mod utils; mod utils;
+1
View File
@@ -11,6 +11,7 @@ pub(crate) mod radarr;
pub enum RadarrEvent { pub enum RadarrEvent {
HealthCheck, HealthCheck,
GetOverview, GetOverview,
GetStatus,
} }
pub struct Network<'a> { pub struct Network<'a> {
+98 -51
View File
@@ -1,3 +1,5 @@
use std::borrow::Borrow;
use anyhow::Result; use anyhow::Result;
use log::{debug, error}; use log::{debug, error};
use reqwest::RequestBuilder; use reqwest::RequestBuilder;
@@ -9,71 +11,116 @@ use crate::app::RadarrConfig;
use crate::network::{Network, RadarrEvent}; use crate::network::{Network, RadarrEvent};
impl RadarrEvent { impl RadarrEvent {
const fn resource(self) -> &'static str { const fn resource(self) -> &'static str {
match self { match self {
RadarrEvent::HealthCheck => "/health", RadarrEvent::HealthCheck => "/health",
RadarrEvent::GetOverview => "/diskspace" RadarrEvent::GetOverview => "/diskspace",
} RadarrEvent::GetStatus => "/system/status",
} }
}
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct DiskSpace { pub struct DiskSpace {
pub path: String, pub path: String,
pub label: String, pub label: String,
pub free_space: Number, pub free_space: Number,
pub total_space: Number pub total_space: Number,
}
#[derive(Deserialize, Debug)]
pub struct SystemStatus {
version: String,
} }
impl<'a> Network<'a> { impl<'a> Network<'a> {
pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) { pub async fn handle_radarr_event(&self, radarr_event: RadarrEvent) {
match radarr_event { match radarr_event {
RadarrEvent::HealthCheck => { RadarrEvent::HealthCheck => {
self.healthcheck(RadarrEvent::HealthCheck.resource()).await; self.healthcheck(RadarrEvent::HealthCheck.resource()).await;
} }
RadarrEvent::GetOverview => { RadarrEvent::GetOverview => match self.diskspace(RadarrEvent::GetOverview.resource()).await {
match self.diskspace(RadarrEvent::GetOverview.resource()).await { Ok(disk_space_vec) => {
Ok(disk_space_vec) => { let mut app = self.app.lock().await;
let mut app = self.app.lock().await; let DiskSpace {
app.data.radarr_data = RadarrData::from(&disk_space_vec[0]); free_space,
} total_space,
Err(e) => { ..
error!("Failed to fetch disk space. {:?}", e); } = &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();
}
} }
Err(e) => {
let mut app = self.app.lock().await; error!("Failed to fetch disk space. {:?}", e);
app.reset();
}
async fn healthcheck(&self, resource: &str) {
if let Err(e) = self.call_radarr_api(resource).await.send().await {
error!("Healthcheck failed. {:?}", 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<Vec<DiskSpace>> { let mut app = self.app.lock().await;
debug!("Handling diskspace event: {:?}", resource); app.reset();
}
Ok( async fn healthcheck(&self, resource: &str) {
self.call_radarr_api(resource) if let Err(e) = self.call_radarr_api(resource).await.send().await {
.await error!("Healthcheck failed. {:?}", e)
.send()
.await?
.json::<Vec<DiskSpace>>()
.await?
)
} }
}
async fn call_radarr_api(&self, resource: &str) -> RequestBuilder { async fn diskspace(&self, resource: &str) -> Result<Vec<DiskSpace>> {
debug!("Creating RequestBuilder for resource: {:?}", resource); debug!("Handling diskspace event: {:?}", 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)) Ok(
.header("X-Api-Key", api_token) self
} .call_radarr_api(resource)
} .await
.send()
.await?
.json::<Vec<DiskSpace>>()
.await?,
)
}
async fn status(&self, resource: &str) -> Result<SystemStatus> {
debug!("Handling system status event: {:?}", resource);
Ok(
self
.call_radarr_api(resource)
.await
.send()
.await?
.json::<SystemStatus>()
.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)
}
}
+84 -22
View File
@@ -1,31 +1,93 @@
use tui::backend::Backend; use tui::backend::Backend;
use tui::Frame; use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::layout::{Constraint, Direction, Layout}; use tui::style::Color::Cyan;
use tui::style::{Color, Style}; 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::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<B: Backend>(f: &mut Frame<B>, app: &App) { pub fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
let RadarrData { free_space, total_space } = app.data.radarr_data; let main_chunks = vertical_chunks(
let ratio = if total_space == 0 { vec![Constraint::Length(20), Constraint::Length(0)],
0f64 f.size(),
} else { );
1f64 - (free_space as f64 / total_space as f64)
};
let chunks = Layout::default() draw_context_row(f, app, main_chunks[0]);
.direction(Direction::Vertical) f.render_widget(Block::default().borders(Borders::ALL), main_chunks[1]);
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) }
.split(f.size());
let gauge = Gauge::default() fn draw_context_row<B: Backend>(f: &mut Frame<'_, B>, app: &App, area: Rect) {
.block(Block::default() let chunks = horizontal_chunks(
.title("Free Space") vec![
.borders(Borders::ALL)) Constraint::Percentage(23),
.gauge_style(Style::default().fg(Color::Cyan)) Constraint::Percentage(23),
.ratio(ratio); Constraint::Percentage(23),
Constraint::Percentage(23),
Constraint::Length(20),
],
area,
);
f.render_widget(gauge, chunks[0]); 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<B: Backend>(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<B: Backend>(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]);
}
+47
View File
@@ -0,0 +1,47 @@
use tui::layout::{Constraint, Direction, Layout, Rect};
pub fn horizontal_chunks(constraints: Vec<Constraint>, size: Rect) -> Vec<Rect> {
Layout::default()
.constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
&constraints,
))
.direction(Direction::Horizontal)
.split(size)
}
pub fn horizontal_chunks_with_margin(
constraints: Vec<Constraint>,
size: Rect,
margin: u16,
) -> Vec<Rect> {
Layout::default()
.constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
&constraints,
))
.direction(Direction::Horizontal)
.margin(margin)
.split(size)
}
pub fn vertical_chunks(constraints: Vec<Constraint>, size: Rect) -> Vec<Rect> {
Layout::default()
.constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
&constraints,
))
.direction(Direction::Vertical)
.split(size)
}
pub fn vertical_chunks_with_margin(
constraints: Vec<Constraint>,
size: Rect,
margin: u16,
) -> Vec<Rect> {
Layout::default()
.constraints(<Vec<Constraint> as AsRef<[Constraint]>>::as_ref(
&constraints,
))
.direction(Direction::Vertical)
.margin(margin)
.split(size)
}