Movie.status and AddMovieSearchResult.status were bare Strings compared against the literal "released" in the row-style decorator and in the movie details style function. They now carry a MovieStatus enum with the five values Radarr actually sends (tba, announced, inCinemas, released, deleted), shaped like MinimumAvailability and DownloadStatus, and guarded with from_json_or_default so an unrecognised status degrades to Tba instead of failing the whole fetch. Radarr never sends "Downloaded" as a movie status; its web UI derives that label from released plus hasFile. Every fixture that carried "Downloaded" now holds released, the value it was standing in for, so none of them degrade to the default through the guard. The status is never rendered as text in the Radarr tree, so no snapshot changes; the CLI JSON output is unchanged since the enum serializes to the same camelCase strings. The style_from_download_status tests now exercise the unreleased arm, which previously had no case, in place of a duplicated unmonitored-missing row.
268 lines
8.2 KiB
Rust
268 lines
8.2 KiB
Rust
use crate::app::App;
|
|
use crate::logos::RADARR_LOGO;
|
|
use crate::models::Route;
|
|
use crate::models::radarr_models::{DownloadRecord, DownloadStatus, Movie, MovieStatus};
|
|
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
|
|
use crate::models::servarr_models::{DiskSpace, RootFolder};
|
|
use crate::ui::DrawUi;
|
|
use crate::ui::draw_tabs;
|
|
use crate::ui::radarr_ui::blocklist::BlocklistUi;
|
|
use crate::ui::radarr_ui::collections::CollectionsUi;
|
|
use crate::ui::radarr_ui::downloads::DownloadsUi;
|
|
use crate::ui::radarr_ui::history::HistoryUi;
|
|
use crate::ui::radarr_ui::indexers::IndexersUi;
|
|
use crate::ui::radarr_ui::library::LibraryUi;
|
|
use crate::ui::radarr_ui::root_folders::RootFoldersUi;
|
|
use crate::ui::radarr_ui::system::SystemUi;
|
|
use crate::ui::styles::ManagarrStyle;
|
|
#[cfg(test)]
|
|
use crate::ui::ui_test_utils::test_utils::Utc;
|
|
use crate::ui::utils::{
|
|
borderless_block, extract_monitored_disk_space_vec, extract_monitored_root_folders, layout_block,
|
|
line_gauge_with_label, line_gauge_with_title, title_block,
|
|
};
|
|
use crate::ui::widgets::loading_block::LoadingBlock;
|
|
use crate::utils::convert_to_gb;
|
|
use chrono::Duration;
|
|
#[cfg(not(test))]
|
|
use chrono::Utc;
|
|
use ratatui::Frame;
|
|
use ratatui::layout::{Constraint, Layout, Rect};
|
|
use ratatui::prelude::Stylize;
|
|
use ratatui::text::Text;
|
|
use ratatui::widgets::{Paragraph, Row};
|
|
use std::{cmp, iter};
|
|
|
|
mod blocklist;
|
|
mod collections;
|
|
mod downloads;
|
|
mod history;
|
|
mod indexers;
|
|
mod library;
|
|
#[cfg(test)]
|
|
mod radarr_ui_tests;
|
|
mod radarr_ui_utils;
|
|
mod root_folders;
|
|
mod system;
|
|
|
|
pub(super) struct RadarrUi;
|
|
|
|
impl DrawUi for RadarrUi {
|
|
fn accepts(route: Route) -> bool {
|
|
matches!(route, Route::Radarr(_, _))
|
|
}
|
|
|
|
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|
let content_area = draw_tabs(f, area, "Movies", &app.data.radarr_data.main_tabs);
|
|
let route = app.get_current_route();
|
|
|
|
match route {
|
|
_ if LibraryUi::accepts(route) => LibraryUi::draw(f, app, content_area),
|
|
_ if CollectionsUi::accepts(route) => CollectionsUi::draw(f, app, content_area),
|
|
_ if DownloadsUi::accepts(route) => DownloadsUi::draw(f, app, content_area),
|
|
_ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area),
|
|
_ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area),
|
|
_ if SystemUi::accepts(route) => SystemUi::draw(f, app, content_area),
|
|
_ if BlocklistUi::accepts(route) => BlocklistUi::draw(f, app, content_area),
|
|
_ if HistoryUi::accepts(route) => HistoryUi::draw(f, app, content_area),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
fn draw_context_row(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
|
let [main_area, logo_area] =
|
|
Layout::horizontal([Constraint::Fill(0), Constraint::Length(20)]).areas(area);
|
|
|
|
let [stats_area, downloads_area] =
|
|
Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).areas(main_area);
|
|
|
|
draw_stats_context(f, app, stats_area);
|
|
draw_downloads_context(f, app, downloads_area);
|
|
draw_radarr_logo(f, logo_area);
|
|
}
|
|
}
|
|
|
|
fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
|
let block = title_block("Stats");
|
|
|
|
if !app.data.radarr_data.version.is_empty() {
|
|
f.render_widget(block, area);
|
|
let RadarrData {
|
|
root_folders,
|
|
disk_space_vec,
|
|
start_time,
|
|
..
|
|
} = &app.data.radarr_data;
|
|
let monitored_disk_space_vec = extract_monitored_disk_space_vec(app, disk_space_vec.clone());
|
|
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
|
|
|
|
let mut constraints = vec![
|
|
Constraint::Length(1),
|
|
Constraint::Length(1),
|
|
Constraint::Length(1),
|
|
];
|
|
|
|
constraints.append(
|
|
&mut iter::repeat_n(
|
|
Constraint::Length(1),
|
|
monitored_disk_space_vec.len() + monitored_root_folders.len() + 1,
|
|
)
|
|
.collect(),
|
|
);
|
|
|
|
let stat_item_areas = Layout::vertical(constraints).margin(1).split(area);
|
|
|
|
let version_paragraph = Paragraph::new(Text::from(format!(
|
|
"Radarr Version: {}",
|
|
app.data.radarr_data.version
|
|
)))
|
|
.block(borderless_block())
|
|
.bold();
|
|
|
|
let uptime = Utc::now() - start_time.to_owned();
|
|
let days = uptime.num_days();
|
|
let day_difference = uptime - Duration::days(days);
|
|
let hours = day_difference.num_hours();
|
|
let hour_difference = day_difference - Duration::hours(hours);
|
|
let minutes = hour_difference.num_minutes();
|
|
let seconds = (hour_difference - Duration::minutes(minutes)).num_seconds();
|
|
|
|
let uptime_paragraph = Paragraph::new(Text::from(format!(
|
|
"Uptime: {days}d {hours:0width$}:{minutes:0width$}:{seconds:0width$}",
|
|
width = 2
|
|
)))
|
|
.block(borderless_block())
|
|
.bold();
|
|
|
|
let storage = Paragraph::new(Text::from("Storage:")).block(borderless_block().bold());
|
|
let folders = Paragraph::new(Text::from("Root Folders:")).block(borderless_block().bold());
|
|
|
|
f.render_widget(version_paragraph, stat_item_areas[0]);
|
|
f.render_widget(uptime_paragraph, stat_item_areas[1]);
|
|
f.render_widget(storage, stat_item_areas[2]);
|
|
|
|
for i in 0..monitored_disk_space_vec.len() {
|
|
let DiskSpace {
|
|
path,
|
|
free_space,
|
|
total_space,
|
|
} = &monitored_disk_space_vec[i];
|
|
let title = if let Some(path) = path {
|
|
path
|
|
} else {
|
|
&format!("Disk {}", i + 1)
|
|
};
|
|
let ratio = if *total_space == 0 {
|
|
0f64
|
|
} else {
|
|
1f64 - (*free_space as f64 / *total_space as f64)
|
|
};
|
|
|
|
let space_gauge = line_gauge_with_label(title.as_str(), ratio);
|
|
|
|
f.render_widget(space_gauge, stat_item_areas[i + 3]);
|
|
}
|
|
|
|
f.render_widget(folders, stat_item_areas[monitored_disk_space_vec.len() + 3]);
|
|
|
|
let monitored_root_folders = extract_monitored_root_folders(app, root_folders.items.clone());
|
|
for i in 0..monitored_root_folders.len() {
|
|
let RootFolder {
|
|
path, free_space, ..
|
|
} = &monitored_root_folders[i];
|
|
let space: f64 = convert_to_gb(*free_space);
|
|
let root_folder_space = Paragraph::new(format!("{path}: {space:.2} GB free"))
|
|
.block(borderless_block())
|
|
.default_color();
|
|
|
|
f.render_widget(
|
|
root_folder_space,
|
|
stat_item_areas[i + monitored_disk_space_vec.len() + 4],
|
|
)
|
|
}
|
|
} else {
|
|
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
|
|
}
|
|
}
|
|
|
|
fn draw_downloads_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
|
let block = title_block("Downloads");
|
|
let downloads_vec = &app.data.radarr_data.downloads.items;
|
|
|
|
if !downloads_vec.is_empty() {
|
|
f.render_widget(block, area);
|
|
|
|
let max_items = ((((area.height as f32 / 2.0).floor() * 2.0) as i32) / 2) - 1;
|
|
let items = cmp::min(downloads_vec.len(), max_items.unsigned_abs() as usize);
|
|
let download_item_areas =
|
|
Layout::vertical(iter::repeat_n(Constraint::Length(2), items).collect::<Vec<Constraint>>())
|
|
.margin(1)
|
|
.split(area);
|
|
|
|
for i in 0..items {
|
|
let DownloadRecord {
|
|
title,
|
|
sizeleft,
|
|
size,
|
|
..
|
|
} = &downloads_vec[i];
|
|
let percent = if *size == 0 {
|
|
0.0
|
|
} else {
|
|
1f64 - (*sizeleft as f64 / *size as f64)
|
|
};
|
|
let download_gauge = line_gauge_with_title(title, percent);
|
|
|
|
f.render_widget(download_gauge, download_item_areas[i]);
|
|
}
|
|
} else {
|
|
f.render_widget(LoadingBlock::new(app.is_loading, block), area);
|
|
}
|
|
}
|
|
|
|
fn decorate_with_row_style<'a>(
|
|
downloads_vec: &[DownloadRecord],
|
|
movie: &Movie,
|
|
row: Row<'a>,
|
|
) -> Row<'a> {
|
|
if !movie.has_file {
|
|
if let Some(download) = downloads_vec
|
|
.iter()
|
|
.find(|&download| download.movie_id == movie.id)
|
|
{
|
|
if download.status == DownloadStatus::Downloading {
|
|
return row.downloading();
|
|
}
|
|
|
|
if download.status == DownloadStatus::Completed {
|
|
return row.awaiting_import();
|
|
}
|
|
}
|
|
|
|
if !movie.monitored {
|
|
return row.unmonitored_missing();
|
|
}
|
|
|
|
if movie.status != MovieStatus::Released {
|
|
return row.unreleased();
|
|
}
|
|
|
|
return row.missing();
|
|
}
|
|
|
|
if !movie.monitored {
|
|
row.unmonitored()
|
|
} else {
|
|
row.downloaded()
|
|
}
|
|
}
|
|
|
|
fn draw_radarr_logo(f: &mut Frame<'_>, area: Rect) {
|
|
let logo_text = Text::from(RADARR_LOGO);
|
|
let logo = Paragraph::new(logo_text)
|
|
.light_yellow()
|
|
.block(layout_block().default_color())
|
|
.centered();
|
|
f.render_widget(logo, area);
|
|
}
|