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) {
|
||||
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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 event;
|
||||
mod handlers;
|
||||
mod logos;
|
||||
mod network;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
@@ -11,6 +11,7 @@ pub(crate) mod radarr;
|
||||
pub enum RadarrEvent {
|
||||
HealthCheck,
|
||||
GetOverview,
|
||||
GetStatus,
|
||||
}
|
||||
|
||||
pub struct Network<'a> {
|
||||
|
||||
+98
-51
@@ -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
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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