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) {
if self.tick_count % self.tick_until_poll == 0 {
self.dispatch(RadarrEvent::GetOverview).await;
self.dispatch(RadarrEvent::GetStatus).await;
}
self.tick_count += 1;
+5 -11
View File
@@ -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,
}
+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 event;
mod handlers;
mod logos;
mod network;
mod ui;
mod utils;
+1
View File
@@ -11,6 +11,7 @@ pub(crate) mod radarr;
pub enum RadarrEvent {
HealthCheck,
GetOverview,
GetStatus,
}
pub struct Network<'a> {
+98 -51
View File
@@ -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<Vec<DiskSpace>> {
debug!("Handling diskspace event: {:?}", resource);
let mut app = self.app.lock().await;
app.reset();
}
Ok(
self.call_radarr_api(resource)
.await
.send()
.await?
.json::<Vec<DiskSpace>>()
.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<Vec<DiskSpace>> {
debug!("Handling diskspace event: {:?}", resource);
app.client.get(format!("http://{}:{}/api/v3{}", host, port.unwrap_or(7878), resource))
.header("X-Api-Key", api_token)
}
}
Ok(
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::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<B: Backend>(f: &mut Frame<B>, 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<B: Backend>(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]);
}
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)
}