Implemented basic stats functionality and started setting up menu
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
+3
-9
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = "⠀⠀⠀⠀⢀⣠⣠⣤⣄⣄⠀⠀⠀⠀⠀
|
||||||
|
⠀⠀⢠⠚⢫⣷⣷⣷⣷⣯⠟⠒⡄⠀⠀
|
||||||
|
⠀⢰⣗⣤⣜⢿⣿⢿⣿⢟⣥⣤⡻⡄⠀
|
||||||
|
⠀⣟⣾⣿⣿⡗⠁⠠⠈⣹⣿⣿⡯⣷
|
||||||
|
⠀⢿⣺⣿⣿⣇⠌⢀⢁⢼⣿⣿⡯⡟⠀
|
||||||
|
⠀⠘⣞⠋⢫⣾⣿⣶⣿⣷⠝⠛⣽⠃⠀
|
||||||
|
⠀⠁⠐⠤⣜⡿⢿⢿⢿⢟⣧⠔⠀⠀⠀
|
||||||
|
⠀⠀⠀⠀⠀⢉⠙⠋⠋⠉⠀⠀⠀⠀⠀
|
||||||
|
";
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
+55
-8
@@ -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;
|
||||||
@@ -12,7 +14,8 @@ 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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +26,12 @@ 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> {
|
||||||
@@ -32,17 +40,30 @@ impl<'a> Network<'a> {
|
|||||||
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;
|
||||||
app.data.radarr_data = RadarrData::from(&disk_space_vec[0]);
|
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();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to fetch disk space. {:?}", 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);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut app = self.app.lock().await;
|
let mut app = self.app.lock().await;
|
||||||
@@ -59,21 +80,47 @@ impl<'a> Network<'a> {
|
|||||||
debug!("Handling diskspace event: {:?}", resource);
|
debug!("Handling diskspace event: {:?}", resource);
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
self.call_radarr_api(resource)
|
self
|
||||||
|
.call_radarr_api(resource)
|
||||||
.await
|
.await
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.json::<Vec<DiskSpace>>()
|
.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?
|
.await?
|
||||||
|
.json::<SystemStatus>()
|
||||||
|
.await?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn call_radarr_api(&self, resource: &str) -> RequestBuilder {
|
async fn call_radarr_api(&self, resource: &str) -> RequestBuilder {
|
||||||
debug!("Creating RequestBuilder for resource: {:?}", resource);
|
debug!("Creating RequestBuilder for resource: {:?}", resource);
|
||||||
let app = self.app.lock().await;
|
let app = self.app.lock().await;
|
||||||
let RadarrConfig { host, port, api_token } = &app.config.radarr;
|
let RadarrConfig {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
api_token,
|
||||||
|
} = &app.config.radarr;
|
||||||
|
|
||||||
app.client.get(format!("http://{}:{}/api/v3{}", host, port.unwrap_or(7878), resource))
|
app
|
||||||
|
.client
|
||||||
|
.get(format!(
|
||||||
|
"http://{}:{}/api/v3{}",
|
||||||
|
host,
|
||||||
|
port.unwrap_or(7878),
|
||||||
|
resource
|
||||||
|
))
|
||||||
.header("X-Api-Key", api_token)
|
.header("X-Api-Key", api_token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+78
-16
@@ -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(
|
||||||
|
vec![Constraint::Length(20), Constraint::Length(0)],
|
||||||
|
f.size(),
|
||||||
|
);
|
||||||
|
|
||||||
|
draw_context_row(f, app, main_chunks[0]);
|
||||||
|
f.render_widget(Block::default().borders(Borders::ALL), main_chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
let ratio = if total_space == 0 {
|
||||||
0f64
|
0f64
|
||||||
} else {
|
} else {
|
||||||
1f64 - (free_space as f64 / total_space as f64)
|
1f64 - (free_space as f64 / total_space as f64)
|
||||||
};
|
};
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let base_block = Block::default().title("Stats").borders(Borders::ALL);
|
||||||
.direction(Direction::Vertical)
|
f.render_widget(base_block, area);
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
|
||||||
.split(f.size());
|
|
||||||
|
|
||||||
let gauge = Gauge::default()
|
let chunks =
|
||||||
.block(Block::default()
|
vertical_chunks_with_margin(vec![Constraint::Length(1), Constraint::Min(2)], area, 1);
|
||||||
.title("Free Space")
|
|
||||||
.borders(Borders::ALL))
|
|
||||||
.gauge_style(Style::default().fg(Color::Cyan))
|
|
||||||
.ratio(ratio);
|
|
||||||
|
|
||||||
f.render_widget(gauge, chunks[0]);
|
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]);
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user