feat: TUI support for Lidarr library
This commit is contained in:
@@ -8,6 +8,7 @@ mod tests {
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::app::{App, AppConfig, Data, ServarrConfig, interpolate_env_vars};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::LidarrData;
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::{HorizontallyScrollableText, TabRoute};
|
||||
@@ -185,6 +186,7 @@ mod tests {
|
||||
..SonarrData::default()
|
||||
};
|
||||
let data = Data {
|
||||
lidarr_data: LidarrData::default(),
|
||||
radarr_data,
|
||||
sonarr_data,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::app::App;
|
||||
use crate::app::key_binding::{DEFAULT_KEYBINDINGS, KeyBinding};
|
||||
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
|
||||
use crate::app::radarr::radarr_context_clues::RadarrContextClueProvider;
|
||||
use crate::app::sonarr::sonarr_context_clues::SonarrContextClueProvider;
|
||||
use crate::models::Route;
|
||||
@@ -21,6 +22,7 @@ impl ContextClueProvider for ServarrContextClueProvider {
|
||||
match app.get_current_route() {
|
||||
Route::Radarr(_, _) => RadarrContextClueProvider::get_context_clues(app),
|
||||
Route::Sonarr(_, _) => SonarrContextClueProvider::get_context_clues(app),
|
||||
Route::Lidarr(_, _) => LidarrContextClueProvider::get_context_clues(app),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::app::App;
|
||||
use crate::app::context_clues::{ContextClue, ContextClueProvider};
|
||||
use crate::models::Route;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_context_clues_tests.rs"]
|
||||
mod lidarr_context_clues_tests;
|
||||
|
||||
pub(in crate::app) struct LidarrContextClueProvider;
|
||||
|
||||
impl ContextClueProvider for LidarrContextClueProvider {
|
||||
fn get_context_clues(app: &mut App<'_>) -> Option<&'static [ContextClue]> {
|
||||
let Route::Lidarr(active_lidarr_block, _context_option) = app.get_current_route() else {
|
||||
panic!("LidarrContextClueProvider::get_context_clues called with non-Lidarr route");
|
||||
};
|
||||
|
||||
match active_lidarr_block {
|
||||
_ => app
|
||||
.data
|
||||
.lidarr_data
|
||||
.main_tabs
|
||||
.get_active_route_contextual_help(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::context_clues::ContextClueProvider;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
use crate::app::lidarr::lidarr_context_clues::LidarrContextClueProvider;
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{
|
||||
ActiveLidarrBlock, ARTISTS_CONTEXT_CLUES,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
|
||||
#[test]
|
||||
fn test_artists_context_clues() {
|
||||
let mut artists_context_clues_iter = ARTISTS_CONTEXT_CLUES.iter();
|
||||
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc
|
||||
)
|
||||
);
|
||||
assert_some_eq_x!(
|
||||
artists_context_clues_iter.next(),
|
||||
&(DEFAULT_KEYBINDINGS.esc, "cancel filter")
|
||||
);
|
||||
assert_none!(artists_context_clues_iter.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "LidarrContextClueProvider::get_context_clues called with non-Lidarr route"
|
||||
)]
|
||||
fn test_lidarr_context_clue_provider_get_context_clues_non_lidarr_route() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::default().into());
|
||||
|
||||
LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::Artists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_artists_sort_prompt_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::ArtistsSortPrompt.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_search_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::SearchArtists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_context_clue_provider_filter_artists_block() {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveLidarrBlock::FilterArtists.into());
|
||||
|
||||
let context_clues = LidarrContextClueProvider::get_context_clues(&mut app);
|
||||
|
||||
assert_some_eq_x!(context_clues, &ARTISTS_CONTEXT_CLUES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::network::NetworkEvent;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_by_lidarr_block_artists() {
|
||||
let (tx, mut rx) = mpsc::channel::<NetworkEvent>(500);
|
||||
let mut app = App::test_default();
|
||||
app.network_tx = Some(tx);
|
||||
|
||||
app.dispatch_by_lidarr_block(&ActiveLidarrBlock::Artists).await;
|
||||
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetQualityProfiles.into()
|
||||
);
|
||||
assert_eq!(
|
||||
rx.recv().await.unwrap(),
|
||||
LidarrEvent::GetMetadataProfiles.into()
|
||||
);
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::GetTags.into());
|
||||
assert_eq!(rx.recv().await.unwrap(), LidarrEvent::ListArtists.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use crate::{
|
||||
models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
network::lidarr_network::LidarrEvent,
|
||||
};
|
||||
|
||||
use super::App;
|
||||
|
||||
pub(in crate::app) mod lidarr_context_clues;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_tests.rs"]
|
||||
mod lidarr_tests;
|
||||
|
||||
impl App<'_> {
|
||||
pub(super) async fn dispatch_by_lidarr_block(&mut self, active_lidarr_block: &ActiveLidarrBlock) {
|
||||
match active_lidarr_block {
|
||||
ActiveLidarrBlock::Artists => {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::ListArtists.into())
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.check_for_lidarr_prompt_action().await;
|
||||
self.reset_tick_count();
|
||||
}
|
||||
|
||||
async fn check_for_lidarr_prompt_action(&mut self) {
|
||||
if self.data.lidarr_data.prompt_confirm {
|
||||
self.data.lidarr_data.prompt_confirm = false;
|
||||
if let Some(lidarr_event) = self.data.lidarr_data.prompt_confirm_action.take() {
|
||||
self.dispatch_network_event(lidarr_event.into()).await;
|
||||
self.should_refresh = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn lidarr_on_tick(&mut self, active_lidarr_block: ActiveLidarrBlock) {
|
||||
if self.is_first_render {
|
||||
self.refresh_lidarr_metadata().await;
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
self.is_first_render = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if self.should_refresh {
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
self.refresh_lidarr_metadata().await;
|
||||
}
|
||||
|
||||
if self.is_routing {
|
||||
if !self.should_refresh {
|
||||
self.cancellation_token.cancel();
|
||||
} else {
|
||||
self.dispatch_by_lidarr_block(&active_lidarr_block).await;
|
||||
}
|
||||
}
|
||||
|
||||
if self.tick_count.is_multiple_of(self.tick_until_poll) {
|
||||
self.refresh_lidarr_metadata().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh_lidarr_metadata(&mut self) {
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetQualityProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetMetadataProfiles.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetTags.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetRootFolders.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDownloads(500).into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetDiskSpace.into())
|
||||
.await;
|
||||
self
|
||||
.dispatch_network_event(LidarrEvent::GetStatus.into())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -13,7 +13,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use veil::Redact;
|
||||
|
||||
use crate::cli::Command;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LidarrData};
|
||||
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, RadarrData};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||
use crate::models::servarr_models::KeybindingItem;
|
||||
@@ -26,6 +26,7 @@ mod app_tests;
|
||||
pub mod context_clues;
|
||||
pub mod key_binding;
|
||||
mod key_binding_tests;
|
||||
pub mod lidarr;
|
||||
pub mod radarr;
|
||||
pub mod sonarr;
|
||||
|
||||
@@ -197,6 +198,7 @@ impl App<'_> {
|
||||
match self.get_current_route() {
|
||||
Route::Radarr(active_radarr_block, _) => self.radarr_on_tick(active_radarr_block).await,
|
||||
Route::Sonarr(active_sonarr_block, _) => self.sonarr_on_tick(active_sonarr_block).await,
|
||||
Route::Lidarr(active_lidarr_block, _) => self.lidarr_on_tick(active_lidarr_block).await,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -299,6 +301,7 @@ impl App<'_> {
|
||||
pub fn test_default_fully_populated() -> Self {
|
||||
App {
|
||||
data: Data {
|
||||
lidarr_data: LidarrData::test_default_fully_populated(),
|
||||
radarr_data: RadarrData::test_default_fully_populated(),
|
||||
sonarr_data: SonarrData::test_default_fully_populated(),
|
||||
},
|
||||
@@ -329,6 +332,7 @@ impl App<'_> {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Data<'a> {
|
||||
pub lidarr_data: LidarrData<'a>,
|
||||
pub radarr_data: RadarrData<'a>,
|
||||
pub sonarr_data: SonarrData<'a>,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use serde_json::Number;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::handlers::lidarr_handlers::library::{LibraryHandler, artists_sorting_options};
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::models::lidarr_models::{Artist, ArtistStatistics, ArtistStatus};
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS};
|
||||
|
||||
#[test]
|
||||
fn test_library_handler_accepts() {
|
||||
for lidarr_block in ActiveLidarrBlock::iter() {
|
||||
if LIBRARY_BLOCKS.contains(&lidarr_block) {
|
||||
assert!(LibraryHandler::accepts(lidarr_block));
|
||||
} else {
|
||||
assert!(!LibraryHandler::accepts(lidarr_block));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_name() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.artist_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.artist_name.text.to_lowercase())
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[0].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_type() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase()
|
||||
.cmp(
|
||||
&b.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase(),
|
||||
)
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[1].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_status() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.status
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.cmp(&b.status.to_string().to_lowercase())
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[2].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Status");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_quality_profile() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering =
|
||||
|a, b| a.quality_profile_id.cmp(&b.quality_profile_id);
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[3].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Quality Profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_metadata_profile() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering =
|
||||
|a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id);
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[4].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Metadata Profile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_albums() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.album_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count))
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[5].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Albums");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_tracks() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.track_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count))
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[6].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Tracks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_size() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.size_on_disk)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk))
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[7].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Size");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_monitored() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| a.monitored.cmp(&b.monitored);
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[8].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Monitored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artists_sorting_options_tags() {
|
||||
let expected_cmp_fn: fn(&Artist, &Artist) -> Ordering = |a, b| {
|
||||
let a_str = a
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let b_str = b
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
a_str.cmp(&b_str)
|
||||
};
|
||||
let mut expected_artists_vec = artists_vec();
|
||||
expected_artists_vec.sort_by(expected_cmp_fn);
|
||||
|
||||
let sort_option = artists_sorting_options()[9].clone();
|
||||
let mut sorted_artists_vec = artists_vec();
|
||||
sorted_artists_vec.sort_by(sort_option.cmp_fn.unwrap());
|
||||
|
||||
assert_eq!(sorted_artists_vec, expected_artists_vec);
|
||||
assert_str_eq!(sort_option.name, "Tags");
|
||||
}
|
||||
|
||||
fn artists_vec() -> Vec<Artist> {
|
||||
vec![
|
||||
Artist {
|
||||
id: 3,
|
||||
artist_name: "Test Artist 1".into(),
|
||||
artist_type: Some("Group".to_owned()),
|
||||
status: ArtistStatus::Ended,
|
||||
quality_profile_id: 1,
|
||||
metadata_profile_id: 1,
|
||||
monitored: false,
|
||||
tags: vec![Number::from(1), Number::from(2)],
|
||||
statistics: Some(ArtistStatistics {
|
||||
album_count: 5,
|
||||
track_count: 50,
|
||||
size_on_disk: 789,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
},
|
||||
Artist {
|
||||
id: 2,
|
||||
artist_name: "Test Artist 2".into(),
|
||||
artist_type: Some("Solo".to_owned()),
|
||||
status: ArtistStatus::Continuing,
|
||||
quality_profile_id: 2,
|
||||
metadata_profile_id: 2,
|
||||
monitored: false,
|
||||
tags: vec![Number::from(1), Number::from(3)],
|
||||
statistics: Some(ArtistStatistics {
|
||||
album_count: 10,
|
||||
track_count: 100,
|
||||
size_on_disk: 456,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
},
|
||||
Artist {
|
||||
id: 1,
|
||||
artist_name: "Test Artist 3".into(),
|
||||
artist_type: None,
|
||||
status: ArtistStatus::Deleted,
|
||||
quality_profile_id: 3,
|
||||
metadata_profile_id: 3,
|
||||
monitored: true,
|
||||
tags: vec![Number::from(2), Number::from(3)],
|
||||
statistics: Some(ArtistStatistics {
|
||||
album_count: 3,
|
||||
track_count: 30,
|
||||
size_on_disk: 123,
|
||||
..ArtistStatistics::default()
|
||||
}),
|
||||
..Artist::default()
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
use crate::{
|
||||
app::App,
|
||||
event::Key,
|
||||
handlers::{KeyEventHandler, handle_clear_errors},
|
||||
matches_key,
|
||||
models::{
|
||||
lidarr_models::Artist,
|
||||
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS},
|
||||
stateful_table::SortOption,
|
||||
},
|
||||
};
|
||||
|
||||
use super::handle_change_tab_left_right_keys;
|
||||
use crate::handlers::table_handler::{TableHandlingConfig, handle_table};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "library_handler_tests.rs"]
|
||||
mod library_handler_tests;
|
||||
|
||||
pub(super) struct LibraryHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
_context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LibraryHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
let artists_table_handling_config =
|
||||
TableHandlingConfig::new(ActiveLidarrBlock::Artists.into())
|
||||
.sorting_block(ActiveLidarrBlock::ArtistsSortPrompt.into())
|
||||
.sort_options(artists_sorting_options())
|
||||
.searching_block(ActiveLidarrBlock::SearchArtists.into())
|
||||
.search_error_block(ActiveLidarrBlock::SearchArtistsError.into())
|
||||
.search_field_fn(|artist| &artist.artist_name.text)
|
||||
.filtering_block(ActiveLidarrBlock::FilterArtists.into())
|
||||
.filter_error_block(ActiveLidarrBlock::FilterArtistsError.into())
|
||||
.filter_field_fn(|artist| &artist.artist_name.text);
|
||||
|
||||
if !handle_table(
|
||||
self,
|
||||
|app| &mut app.data.lidarr_data.artists,
|
||||
artists_table_handling_config,
|
||||
) {
|
||||
self.handle_key_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(active_block: ActiveLidarrBlock) -> bool {
|
||||
LIBRARY_BLOCKS.contains(&active_block)
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> LibraryHandler<'a, 'b> {
|
||||
LibraryHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
_context: context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
!self.app.is_loading && !self.app.data.lidarr_data.artists.is_empty()
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::Artists {
|
||||
handle_change_tab_left_right_keys(self.app, self.key);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {}
|
||||
|
||||
fn handle_esc(&mut self) {
|
||||
handle_clear_errors(self.app);
|
||||
}
|
||||
|
||||
fn handle_char_key_event(&mut self) {
|
||||
let key = self.key;
|
||||
if self.active_lidarr_block == ActiveLidarrBlock::Artists && matches_key!(refresh, key) {
|
||||
self.app.should_refresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
fn artists_sorting_options() -> Vec<SortOption<Artist>> {
|
||||
vec![
|
||||
SortOption {
|
||||
name: "Name",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.artist_name
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.artist_name.text.to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Type",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase()
|
||||
.cmp(
|
||||
&b.artist_type
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_lowercase(),
|
||||
)
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Status",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.status
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.cmp(&b.status.to_string().to_lowercase())
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Quality Profile",
|
||||
cmp_fn: Some(|a, b| a.quality_profile_id.cmp(&b.quality_profile_id)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Metadata Profile",
|
||||
cmp_fn: Some(|a, b| a.metadata_profile_id.cmp(&b.metadata_profile_id)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Albums",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.album_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.album_count))
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Tracks",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.track_count)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.track_count))
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Size",
|
||||
cmp_fn: Some(|a, b| {
|
||||
a.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.size_on_disk)
|
||||
.cmp(&b.statistics.as_ref().map_or(0, |stats| stats.size_on_disk))
|
||||
}),
|
||||
},
|
||||
SortOption {
|
||||
name: "Monitored",
|
||||
cmp_fn: Some(|a, b| a.monitored.cmp(&b.monitored)),
|
||||
},
|
||||
SortOption {
|
||||
name: "Tags",
|
||||
cmp_fn: Some(|a, b| {
|
||||
let a_str = a
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let b_str = b
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| tag.as_i64().unwrap().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
a_str.cmp(&b_str)
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::handlers::KeyEventHandler;
|
||||
use crate::handlers::lidarr_handlers::LidarrHandler;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_handler_accepts() {
|
||||
for lidarr_block in ActiveLidarrBlock::iter() {
|
||||
assert!(LidarrHandler::accepts(lidarr_block));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
use library::LibraryHandler;
|
||||
|
||||
use crate::{
|
||||
app::App, event::Key, matches_key, models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock,
|
||||
};
|
||||
|
||||
use super::KeyEventHandler;
|
||||
|
||||
mod library;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_handler_tests.rs"]
|
||||
mod lidarr_handler_tests;
|
||||
|
||||
pub(super) struct LidarrHandler<'a, 'b> {
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_lidarr_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveLidarrBlock> for LidarrHandler<'a, 'b> {
|
||||
fn handle(&mut self) {
|
||||
match self.active_lidarr_block {
|
||||
_ if LibraryHandler::accepts(self.active_lidarr_block) => {
|
||||
LibraryHandler::new(self.key, self.app, self.active_lidarr_block, self.context).handle();
|
||||
}
|
||||
_ => self.handle_key_event(),
|
||||
}
|
||||
}
|
||||
|
||||
fn accepts(_active_block: ActiveLidarrBlock) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn ignore_special_keys(&self) -> bool {
|
||||
self.app.ignore_special_keys_for_textbox_input
|
||||
}
|
||||
|
||||
fn new(
|
||||
key: Key,
|
||||
app: &'a mut App<'b>,
|
||||
active_block: ActiveLidarrBlock,
|
||||
context: Option<ActiveLidarrBlock>,
|
||||
) -> LidarrHandler<'a, 'b> {
|
||||
LidarrHandler {
|
||||
key,
|
||||
app,
|
||||
active_lidarr_block: active_block,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key(&self) -> Key {
|
||||
self.key
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_scroll_up(&mut self) {}
|
||||
|
||||
fn handle_scroll_down(&mut self) {}
|
||||
|
||||
fn handle_home(&mut self) {}
|
||||
|
||||
fn handle_end(&mut self) {}
|
||||
|
||||
fn handle_delete(&mut self) {}
|
||||
|
||||
fn handle_left_right_action(&mut self) {}
|
||||
|
||||
fn handle_submit(&mut self) {}
|
||||
|
||||
fn handle_esc(&mut self) {}
|
||||
|
||||
fn handle_char_key_event(&mut self) {}
|
||||
|
||||
fn app_mut(&mut self) -> &mut App<'b> {
|
||||
self.app
|
||||
}
|
||||
|
||||
fn current_route(&self) -> crate::models::Route {
|
||||
self.app.get_current_route()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_change_tab_left_right_keys(app: &mut App<'_>, key: Key) {
|
||||
let key_ref = key;
|
||||
match key_ref {
|
||||
_ if matches_key!(left, key, app.ignore_special_keys_for_textbox_input) => {
|
||||
app.data.lidarr_data.main_tabs.previous();
|
||||
app.pop_and_push_navigation_stack(app.data.lidarr_data.main_tabs.get_active_route());
|
||||
}
|
||||
_ if matches_key!(right, key, app.ignore_special_keys_for_textbox_input) => {
|
||||
app.data.lidarr_data.main_tabs.next();
|
||||
app.pop_and_push_navigation_stack(app.data.lidarr_data.main_tabs.get_active_route());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use lidarr_handlers::LidarrHandler;
|
||||
use radarr_handlers::RadarrHandler;
|
||||
use sonarr_handlers::SonarrHandler;
|
||||
|
||||
@@ -15,6 +16,7 @@ use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{HorizontallyScrollableText, Route};
|
||||
|
||||
mod keybinding_handler;
|
||||
mod lidarr_handlers;
|
||||
mod radarr_handlers;
|
||||
mod sonarr_handlers;
|
||||
|
||||
@@ -125,6 +127,9 @@ pub fn handle_events(key: Key, app: &mut App<'_>) {
|
||||
Route::Sonarr(active_sonarr_block, context) => {
|
||||
SonarrHandler::new(key, app, active_sonarr_block, context).handle()
|
||||
}
|
||||
Route::Lidarr(active_lidarr_block, context) => {
|
||||
LidarrHandler::new(key, app, active_lidarr_block, context).handle()
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -187,6 +192,9 @@ fn handle_prompt_toggle(app: &mut App<'_>, key: Key) {
|
||||
Route::Sonarr(_, _) => {
|
||||
app.data.sonarr_data.prompt_confirm = !app.data.sonarr_data.prompt_confirm
|
||||
}
|
||||
Route::Lidarr(_, _) => {
|
||||
app.data.lidarr_data.prompt_confirm = !app.data.lidarr_data.prompt_confirm
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
|
||||
+102
-2
@@ -1,7 +1,9 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use derivative::Derivative;
|
||||
use enum_display_style_derive::EnumDisplayStyle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
use strum::EnumIter;
|
||||
|
||||
use super::{HorizontallyScrollableText, Serdeable};
|
||||
use crate::serde_enum_from;
|
||||
@@ -15,7 +17,6 @@ mod lidarr_models_tests;
|
||||
pub struct Artist {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub mb_id: String,
|
||||
pub artist_name: HorizontallyScrollableText,
|
||||
pub foreign_artist_id: String,
|
||||
pub status: ArtistStatus,
|
||||
@@ -35,8 +36,20 @@ pub struct Artist {
|
||||
pub statistics: Option<ArtistStatistics>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug)]
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
strum::Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum ArtistStatus {
|
||||
#[default]
|
||||
Continuing,
|
||||
@@ -74,6 +87,86 @@ pub struct ArtistStatistics {
|
||||
|
||||
impl Eq for ArtistStatistics {}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct MetadataProfile {
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<(&i64, &String)> for MetadataProfile {
|
||||
fn from(value: (&i64, &String)) -> Self {
|
||||
MetadataProfile {
|
||||
id: *value.0,
|
||||
name: value.1.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadRecord {
|
||||
pub title: String,
|
||||
pub status: DownloadStatus,
|
||||
#[serde(deserialize_with = "super::from_i64")]
|
||||
pub id: i64,
|
||||
pub album_id: Option<Number>,
|
||||
pub artist_id: Option<Number>,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub size: f64,
|
||||
#[serde(deserialize_with = "super::from_f64")]
|
||||
pub sizeleft: f64,
|
||||
pub output_path: Option<HorizontallyScrollableText>,
|
||||
#[serde(default)]
|
||||
pub indexer: String,
|
||||
pub download_client: Option<String>,
|
||||
}
|
||||
|
||||
impl Eq for DownloadRecord {}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
EnumIter,
|
||||
strum::Display,
|
||||
EnumDisplayStyle,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[strum(serialize_all = "camelCase")]
|
||||
pub enum DownloadStatus {
|
||||
#[default]
|
||||
Unknown,
|
||||
Queued,
|
||||
Paused,
|
||||
Downloading,
|
||||
Completed,
|
||||
Failed,
|
||||
Warning,
|
||||
Delay,
|
||||
#[display_style(name = "Download Client Unavailable")]
|
||||
DownloadClientUnavailable,
|
||||
Fallback,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadsResponse {
|
||||
pub records: Vec<DownloadRecord>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemStatus {
|
||||
pub version: String,
|
||||
pub start_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<LidarrSerdeable> for Serdeable {
|
||||
fn from(value: LidarrSerdeable) -> Serdeable {
|
||||
Serdeable::Lidarr(value)
|
||||
@@ -83,6 +176,13 @@ impl From<LidarrSerdeable> for Serdeable {
|
||||
serde_enum_from!(
|
||||
LidarrSerdeable {
|
||||
Artists(Vec<Artist>),
|
||||
DiskSpaces(Vec<super::servarr_models::DiskSpace>),
|
||||
DownloadsResponse(DownloadsResponse),
|
||||
MetadataProfiles(Vec<MetadataProfile>),
|
||||
QualityProfiles(Vec<super::servarr_models::QualityProfile>),
|
||||
RootFolders(Vec<super::servarr_models::RootFolder>),
|
||||
SystemStatus(SystemStatus),
|
||||
Tags(Vec<super::servarr_models::Tag>),
|
||||
Value(Value),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -85,7 +85,6 @@ mod tests {
|
||||
let artist: Artist = serde_json::from_value(artist_json).unwrap();
|
||||
|
||||
assert_eq!(artist.id, 1);
|
||||
assert_str_eq!(artist.mb_id, "test-mb-id");
|
||||
assert_str_eq!(artist.artist_name.text, "Test Artist");
|
||||
assert_str_eq!(artist.foreign_artist_id, "test-foreign-id");
|
||||
assert_eq!(artist.status, ArtistStatus::Continuing);
|
||||
|
||||
@@ -1,20 +1,133 @@
|
||||
use bimap::BiMap;
|
||||
use chrono::{DateTime, Utc};
|
||||
use strum::EnumIter;
|
||||
#[cfg(test)]
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::models::Route;
|
||||
use crate::models::{
|
||||
Route, TabRoute, TabState,
|
||||
lidarr_models::{Artist, DownloadRecord},
|
||||
servarr_models::{DiskSpace, RootFolder},
|
||||
stateful_table::StatefulTable,
|
||||
};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_data_tests.rs"]
|
||||
mod lidarr_data_tests;
|
||||
|
||||
pub struct LidarrData<'a> {
|
||||
pub artists: StatefulTable<Artist>,
|
||||
pub disk_space_vec: Vec<DiskSpace>,
|
||||
pub downloads: StatefulTable<DownloadRecord>,
|
||||
pub main_tabs: TabState,
|
||||
pub metadata_profile_map: BiMap<i64, String>,
|
||||
pub prompt_confirm: bool,
|
||||
pub prompt_confirm_action: Option<LidarrEvent>,
|
||||
pub quality_profile_map: BiMap<i64, String>,
|
||||
pub root_folders: StatefulTable<RootFolder>,
|
||||
pub selected_block: crate::models::BlockSelectionState<'a, ActiveLidarrBlock>,
|
||||
pub start_time: DateTime<Utc>,
|
||||
pub tags_map: BiMap<i64, String>,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl LidarrData<'_> {
|
||||
pub fn reset_sorting(&mut self) {
|
||||
self.artists.sorting(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for LidarrData<'a> {
|
||||
fn default() -> LidarrData<'a> {
|
||||
LidarrData {
|
||||
artists: StatefulTable::default(),
|
||||
disk_space_vec: Vec::new(),
|
||||
downloads: StatefulTable::default(),
|
||||
metadata_profile_map: BiMap::new(),
|
||||
prompt_confirm: false,
|
||||
prompt_confirm_action: None,
|
||||
quality_profile_map: BiMap::new(),
|
||||
root_folders: StatefulTable::default(),
|
||||
selected_block: crate::models::BlockSelectionState::default(),
|
||||
start_time: Utc::now(),
|
||||
tags_map: BiMap::new(),
|
||||
version: String::new(),
|
||||
main_tabs: TabState::new(vec![
|
||||
TabRoute {
|
||||
title: "Library".to_string(),
|
||||
route: ActiveLidarrBlock::Artists.into(),
|
||||
contextual_help: Some(&ARTISTS_CONTEXT_CLUES),
|
||||
config: None,
|
||||
},
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl LidarrData<'_> {
|
||||
pub fn test_default_fully_populated() -> Self {
|
||||
use crate::models::lidarr_models::{Artist, DownloadRecord};
|
||||
use crate::models::servarr_models::{DiskSpace, RootFolder};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
|
||||
let mut lidarr_data = LidarrData::default();
|
||||
lidarr_data.artists.set_items(vec![Artist::default()]);
|
||||
lidarr_data.artists.sorting(vec![SortOption {
|
||||
name: "Name",
|
||||
cmp_fn: Some(|a: &Artist, b: &Artist| a.artist_name.text.cmp(&b.artist_name.text)),
|
||||
}]);
|
||||
lidarr_data.quality_profile_map = BiMap::from_iter([(1i64, "Lossless".to_owned())]);
|
||||
lidarr_data.metadata_profile_map = BiMap::from_iter([(1i64, "Standard".to_owned())]);
|
||||
lidarr_data.tags_map = BiMap::from_iter([(1i64, "usenet".to_owned())]);
|
||||
lidarr_data.disk_space_vec = vec![DiskSpace {
|
||||
free_space: 50000000000,
|
||||
total_space: 100000000000,
|
||||
}];
|
||||
lidarr_data.downloads.set_items(vec![DownloadRecord::default()]);
|
||||
lidarr_data.root_folders.set_items(vec![RootFolder::default()]);
|
||||
lidarr_data.version = "1.0.0".to_owned();
|
||||
|
||||
lidarr_data
|
||||
}
|
||||
}
|
||||
|
||||
use crate::app::context_clues::ContextClue;
|
||||
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
|
||||
|
||||
pub static ARTISTS_CONTEXT_CLUES: [ContextClue; 5] = [
|
||||
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
|
||||
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
|
||||
(DEFAULT_KEYBINDINGS.filter, DEFAULT_KEYBINDINGS.filter.desc),
|
||||
(
|
||||
DEFAULT_KEYBINDINGS.refresh,
|
||||
DEFAULT_KEYBINDINGS.refresh.desc,
|
||||
),
|
||||
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)]
|
||||
#[cfg_attr(test, derive(Display, EnumString))]
|
||||
pub enum ActiveLidarrBlock {
|
||||
#[default]
|
||||
Artists,
|
||||
ArtistsSortPrompt,
|
||||
SearchArtists,
|
||||
SearchArtistsError,
|
||||
FilterArtists,
|
||||
FilterArtistsError,
|
||||
}
|
||||
|
||||
pub static LIBRARY_BLOCKS: [ActiveLidarrBlock; 6] = [
|
||||
ActiveLidarrBlock::Artists,
|
||||
ActiveLidarrBlock::ArtistsSortPrompt,
|
||||
ActiveLidarrBlock::SearchArtists,
|
||||
ActiveLidarrBlock::SearchArtistsError,
|
||||
ActiveLidarrBlock::FilterArtists,
|
||||
ActiveLidarrBlock::FilterArtistsError,
|
||||
];
|
||||
|
||||
impl From<ActiveLidarrBlock> for Route {
|
||||
fn from(active_lidarr_block: ActiveLidarrBlock) -> Route {
|
||||
Route::Lidarr(active_lidarr_block, None)
|
||||
|
||||
@@ -174,9 +174,25 @@ where
|
||||
}
|
||||
|
||||
pub fn set_filtered_items(&mut self, filtered_items: Vec<T>) {
|
||||
let items_len = filtered_items.len();
|
||||
self.filtered_items = Some(filtered_items);
|
||||
|
||||
let preserved_selection = self
|
||||
.filtered_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.selected())
|
||||
.map_or(0, |i| {
|
||||
if i > 0 && i < items_len {
|
||||
i
|
||||
} else if i >= items_len && items_len > 0 {
|
||||
items_len - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
|
||||
let mut filtered_state: TableState = Default::default();
|
||||
filtered_state.select(Some(0));
|
||||
filtered_state.select(Some(preserved_selection));
|
||||
self.filtered_state = Some(filtered_state);
|
||||
}
|
||||
|
||||
|
||||
@@ -390,6 +390,47 @@ mod tests {
|
||||
assert_some_eq_x!(&filtered_stateful_table.filtered_items, &filtered_items_vec);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_set_filtered_items_preserves_selection() {
|
||||
let filtered_items_vec = vec!["Test 1", "Test 2", "Test 3"];
|
||||
let mut filtered_stateful_table: StatefulTable<&str> = StatefulTable::default();
|
||||
|
||||
filtered_stateful_table.set_filtered_items(filtered_items_vec.clone());
|
||||
filtered_stateful_table
|
||||
.filtered_state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.select(Some(1));
|
||||
|
||||
filtered_stateful_table.set_filtered_items(filtered_items_vec.clone());
|
||||
|
||||
assert_some_eq_x!(
|
||||
filtered_stateful_table
|
||||
.filtered_state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.selected(),
|
||||
1
|
||||
);
|
||||
|
||||
filtered_stateful_table
|
||||
.filtered_state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.select(Some(5));
|
||||
|
||||
filtered_stateful_table.set_filtered_items(filtered_items_vec);
|
||||
|
||||
assert_some_eq_x!(
|
||||
filtered_stateful_table
|
||||
.filtered_state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.selected(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_current_selection() {
|
||||
let mut stateful_table = create_test_stateful_table();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{Artist, LidarrSerdeable};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_artists_event() {
|
||||
let artists_json = json!([{
|
||||
"id": 1,
|
||||
"mbId": "test-mb-id",
|
||||
"artistName": "Test Artist",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"path": "/music/test-artist",
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"genres": [],
|
||||
"tags": [],
|
||||
"added": "2023-01-01T00:00:00Z"
|
||||
}]);
|
||||
let response: Vec<Artist> = serde_json::from_value(artists_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(artists_json)
|
||||
.build_for(LidarrEvent::ListArtists)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::ListArtists).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Artists(artists) = result.unwrap() else {
|
||||
panic!("Expected Artists");
|
||||
};
|
||||
|
||||
assert_eq!(artists, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.artists.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use crate::models::lidarr_models::Artist;
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::Route;
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_library_network_tests.rs"]
|
||||
mod lidarr_library_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn list_artists(&mut self) -> Result<Vec<Artist>> {
|
||||
info!("Fetching Lidarr artists");
|
||||
let event = LidarrEvent::ListArtists;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Artist>>(request_props, |mut artists_vec, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Lidarr(ActiveLidarrBlock::ArtistsSortPrompt, _)
|
||||
) {
|
||||
artists_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.lidarr_data.artists.set_items(artists_vec);
|
||||
app.data.lidarr_data.artists.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{Artist, LidarrSerdeable};
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use crate::network::{NetworkEvent, NetworkResource, lidarr_network::LidarrEvent};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
|
||||
#[rstest]
|
||||
#[case(LidarrEvent::GetDiskSpace, "/diskspace")]
|
||||
#[case(LidarrEvent::GetDownloads(500), "/queue")]
|
||||
#[case(LidarrEvent::GetMetadataProfiles, "/metadataprofile")]
|
||||
#[case(LidarrEvent::GetQualityProfiles, "/qualityprofile")]
|
||||
#[case(LidarrEvent::GetRootFolders, "/rootfolder")]
|
||||
#[case(LidarrEvent::GetStatus, "/system/status")]
|
||||
#[case(LidarrEvent::GetTags, "/tag")]
|
||||
#[case(LidarrEvent::HealthCheck, "/health")]
|
||||
#[case(LidarrEvent::ListArtists, "/artist")]
|
||||
fn test_resource(#[case] event: LidarrEvent, #[case] expected_uri: &str) {
|
||||
@@ -21,52 +25,4 @@ mod tests {
|
||||
NetworkEvent::from(LidarrEvent::HealthCheck)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_lidarr_healthcheck_event() {
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.build_for(LidarrEvent::HealthCheck)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let _ = network.handle_lidarr_event(LidarrEvent::HealthCheck).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_list_artists_event() {
|
||||
let artists_json = json!([{
|
||||
"id": 1,
|
||||
"mbId": "test-mb-id",
|
||||
"artistName": "Test Artist",
|
||||
"foreignArtistId": "test-foreign-id",
|
||||
"status": "continuing",
|
||||
"path": "/music/test-artist",
|
||||
"qualityProfileId": 1,
|
||||
"metadataProfileId": 1,
|
||||
"monitored": true,
|
||||
"genres": [],
|
||||
"tags": [],
|
||||
"added": "2023-01-01T00:00:00Z"
|
||||
}]);
|
||||
let response: Vec<Artist> = serde_json::from_value(artists_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(artists_json)
|
||||
.build_for(LidarrEvent::ListArtists)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::ListArtists).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Artists(artists) = result.unwrap() else {
|
||||
panic!("Expected Artists");
|
||||
};
|
||||
|
||||
assert_eq!(artists, response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use super::{Network, NetworkEvent, NetworkResource};
|
||||
use crate::{
|
||||
models::lidarr_models::{Artist, LidarrSerdeable},
|
||||
network::RequestMethod,
|
||||
};
|
||||
use super::{NetworkEvent, NetworkResource};
|
||||
use crate::models::lidarr_models::LidarrSerdeable;
|
||||
use crate::network::Network;
|
||||
|
||||
mod library;
|
||||
mod system;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_network_tests.rs"]
|
||||
@@ -13,6 +13,13 @@ mod lidarr_network_tests;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum LidarrEvent {
|
||||
GetDiskSpace,
|
||||
GetDownloads(u64),
|
||||
GetMetadataProfiles,
|
||||
GetQualityProfiles,
|
||||
GetRootFolders,
|
||||
GetStatus,
|
||||
GetTags,
|
||||
HealthCheck,
|
||||
ListArtists,
|
||||
}
|
||||
@@ -20,6 +27,13 @@ pub enum LidarrEvent {
|
||||
impl NetworkResource for LidarrEvent {
|
||||
fn resource(&self) -> &'static str {
|
||||
match &self {
|
||||
LidarrEvent::GetDiskSpace => "/diskspace",
|
||||
LidarrEvent::GetDownloads(_) => "/queue",
|
||||
LidarrEvent::GetMetadataProfiles => "/metadataprofile",
|
||||
LidarrEvent::GetQualityProfiles => "/qualityprofile",
|
||||
LidarrEvent::GetRootFolders => "/rootfolder",
|
||||
LidarrEvent::GetStatus => "/system/status",
|
||||
LidarrEvent::GetTags => "/tag",
|
||||
LidarrEvent::HealthCheck => "/health",
|
||||
LidarrEvent::ListArtists => "/artist",
|
||||
}
|
||||
@@ -38,6 +52,31 @@ impl Network<'_, '_> {
|
||||
lidarr_event: LidarrEvent,
|
||||
) -> Result<LidarrSerdeable> {
|
||||
match lidarr_event {
|
||||
LidarrEvent::GetDiskSpace => self
|
||||
.get_lidarr_diskspace()
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetDownloads(count) => self
|
||||
.get_lidarr_downloads(count)
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetMetadataProfiles => self
|
||||
.get_lidarr_metadata_profiles()
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetQualityProfiles => self
|
||||
.get_lidarr_quality_profiles()
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetRootFolders => self
|
||||
.get_lidarr_root_folders()
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetStatus => self
|
||||
.get_lidarr_status()
|
||||
.await
|
||||
.map(LidarrSerdeable::from),
|
||||
LidarrEvent::GetTags => self.get_lidarr_tags().await.map(LidarrSerdeable::from),
|
||||
LidarrEvent::HealthCheck => self
|
||||
.get_lidarr_healthcheck()
|
||||
.await
|
||||
@@ -45,30 +84,4 @@ impl Network<'_, '_> {
|
||||
LidarrEvent::ListArtists => self.list_artists().await.map(LidarrSerdeable::from),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_lidarr_healthcheck(&mut self) -> Result<()> {
|
||||
info!("Performing Lidarr health check");
|
||||
let event = LidarrEvent::HealthCheck;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_artists(&mut self) -> Result<Vec<Artist>> {
|
||||
info!("Fetching Lidarr artists");
|
||||
let event = LidarrEvent::ListArtists;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Artist>>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::lidarr_models::{
|
||||
DownloadsResponse, LidarrSerdeable, MetadataProfile, SystemStatus,
|
||||
};
|
||||
use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_lidarr_healthcheck_event() {
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.build_for(LidarrEvent::HealthCheck)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let _ = network.handle_lidarr_event(LidarrEvent::HealthCheck).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_metadata_profiles_event() {
|
||||
let metadata_profiles_json = json!([{
|
||||
"id": 1,
|
||||
"name": "Standard"
|
||||
}]);
|
||||
let response: Vec<MetadataProfile> =
|
||||
serde_json::from_value(metadata_profiles_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(metadata_profiles_json)
|
||||
.build_for(LidarrEvent::GetMetadataProfiles)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetMetadataProfiles)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::MetadataProfiles(metadata_profiles) = result.unwrap() else {
|
||||
panic!("Expected MetadataProfiles");
|
||||
};
|
||||
|
||||
assert_eq!(metadata_profiles, response);
|
||||
assert_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.metadata_profile_map
|
||||
.get_by_left(&1),
|
||||
Some(&"Standard".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_quality_profiles_event() {
|
||||
let quality_profiles_json = json!([{
|
||||
"id": 1,
|
||||
"name": "Lossless"
|
||||
}]);
|
||||
let response: Vec<QualityProfile> =
|
||||
serde_json::from_value(quality_profiles_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(quality_profiles_json)
|
||||
.build_for(LidarrEvent::GetQualityProfiles)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetQualityProfiles)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::QualityProfiles(quality_profiles) = result.unwrap() else {
|
||||
panic!("Expected QualityProfiles");
|
||||
};
|
||||
|
||||
assert_eq!(quality_profiles, response);
|
||||
assert_eq!(
|
||||
app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.lidarr_data
|
||||
.quality_profile_map
|
||||
.get_by_left(&1),
|
||||
Some(&"Lossless".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_tags_event() {
|
||||
let tags_json = json!([{
|
||||
"id": 1,
|
||||
"label": "usenet"
|
||||
}]);
|
||||
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(tags_json)
|
||||
.build_for(LidarrEvent::GetTags)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::GetTags).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::Tags(tags) = result.unwrap() else {
|
||||
panic!("Expected Tags");
|
||||
};
|
||||
|
||||
assert_eq!(tags, response);
|
||||
assert_eq!(
|
||||
app.lock().await.data.lidarr_data.tags_map.get_by_left(&1),
|
||||
Some(&"usenet".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_diskspace_event() {
|
||||
let diskspace_json = json!([{
|
||||
"freeSpace": 50000000000i64,
|
||||
"totalSpace": 100000000000i64
|
||||
}]);
|
||||
let response: Vec<DiskSpace> = serde_json::from_value(diskspace_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(diskspace_json)
|
||||
.build_for(LidarrEvent::GetDiskSpace)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::GetDiskSpace).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::DiskSpaces(disk_spaces) = result.unwrap() else {
|
||||
panic!("Expected DiskSpaces");
|
||||
};
|
||||
|
||||
assert_eq!(disk_spaces, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.disk_space_vec.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_downloads_event() {
|
||||
let downloads_json = json!({
|
||||
"records": [{
|
||||
"title": "Test Album",
|
||||
"status": "downloading",
|
||||
"id": 1,
|
||||
"size": 100.0,
|
||||
"sizeleft": 50.0,
|
||||
"indexer": "test-indexer"
|
||||
}]
|
||||
});
|
||||
let response: DownloadsResponse = serde_json::from_value(downloads_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(downloads_json)
|
||||
.query("pageSize=500")
|
||||
.build_for(LidarrEvent::GetDownloads(500))
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetDownloads(500))
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::DownloadsResponse(downloads_response) = result.unwrap() else {
|
||||
panic!("Expected DownloadsResponse");
|
||||
};
|
||||
|
||||
assert_eq!(downloads_response, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.downloads.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_root_folders_event() {
|
||||
let root_folders_json = json!([{
|
||||
"id": 1,
|
||||
"path": "/music",
|
||||
"accessible": true,
|
||||
"freeSpace": 50000000000i64
|
||||
}]);
|
||||
let response: Vec<RootFolder> = serde_json::from_value(root_folders_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(root_folders_json)
|
||||
.build_for(LidarrEvent::GetRootFolders)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network
|
||||
.handle_lidarr_event(LidarrEvent::GetRootFolders)
|
||||
.await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::RootFolders(root_folders) = result.unwrap() else {
|
||||
panic!("Expected RootFolders");
|
||||
};
|
||||
|
||||
assert_eq!(root_folders, response);
|
||||
assert!(!app.lock().await.data.lidarr_data.root_folders.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_status_event() {
|
||||
let status_json = json!({
|
||||
"version": "1.0.0",
|
||||
"startTime": "2023-01-01T00:00:00Z"
|
||||
});
|
||||
let response: SystemStatus = serde_json::from_value(status_json.clone()).unwrap();
|
||||
let (mock, app, _server) = MockServarrApi::get()
|
||||
.returns(status_json)
|
||||
.build_for(LidarrEvent::GetStatus)
|
||||
.await;
|
||||
app.lock().await.server_tabs.set_index(2);
|
||||
let mut network = test_network(&app);
|
||||
|
||||
let result = network.handle_lidarr_event(LidarrEvent::GetStatus).await;
|
||||
|
||||
mock.assert_async().await;
|
||||
|
||||
let LidarrSerdeable::SystemStatus(status) = result.unwrap() else {
|
||||
panic!("Expected SystemStatus");
|
||||
};
|
||||
|
||||
assert_eq!(status, response);
|
||||
assert_eq!(app.lock().await.data.lidarr_data.version, "1.0.0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
use crate::models::lidarr_models::{DownloadsResponse, MetadataProfile, SystemStatus};
|
||||
use crate::models::servarr_models::{DiskSpace, QualityProfile, RootFolder, Tag};
|
||||
use crate::network::lidarr_network::LidarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_system_network_tests.rs"]
|
||||
mod lidarr_system_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_healthcheck(&mut self) -> Result<()> {
|
||||
info!("Performing Lidarr health check");
|
||||
let event = LidarrEvent::HealthCheck;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_metadata_profiles(
|
||||
&mut self,
|
||||
) -> Result<Vec<MetadataProfile>> {
|
||||
info!("Fetching Lidarr metadata profiles");
|
||||
let event = LidarrEvent::GetMetadataProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<MetadataProfile>>(request_props, |metadata_profiles, mut app| {
|
||||
app.data.lidarr_data.metadata_profile_map = metadata_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_quality_profiles(
|
||||
&mut self,
|
||||
) -> Result<Vec<QualityProfile>> {
|
||||
info!("Fetching Lidarr quality profiles");
|
||||
let event = LidarrEvent::GetQualityProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
|
||||
app.data.lidarr_data.quality_profile_map = quality_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_tags(&mut self) -> Result<Vec<Tag>> {
|
||||
info!("Fetching Lidarr tags");
|
||||
let event = LidarrEvent::GetTags;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
|
||||
app.data.lidarr_data.tags_map = tags_vec
|
||||
.into_iter()
|
||||
.map(|tag| (tag.id, tag.label))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_diskspace(
|
||||
&mut self,
|
||||
) -> Result<Vec<DiskSpace>> {
|
||||
info!("Fetching Lidarr disk space");
|
||||
let event = LidarrEvent::GetDiskSpace;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<DiskSpace>>(request_props, |disk_space_vec, mut app| {
|
||||
app.data.lidarr_data.disk_space_vec = disk_space_vec;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_downloads(
|
||||
&mut self,
|
||||
count: u64,
|
||||
) -> Result<DownloadsResponse> {
|
||||
info!("Fetching Lidarr downloads");
|
||||
let event = LidarrEvent::GetDownloads(count);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("pageSize={count}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
|
||||
app
|
||||
.data
|
||||
.lidarr_data
|
||||
.downloads
|
||||
.set_items(queue_response.records);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_root_folders(
|
||||
&mut self,
|
||||
) -> Result<Vec<RootFolder>> {
|
||||
info!("Fetching Lidarr root folders");
|
||||
let event = LidarrEvent::GetRootFolders;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
|
||||
app.data.lidarr_data.root_folders.set_items(root_folders);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::lidarr_network) async fn get_lidarr_status(
|
||||
&mut self,
|
||||
) -> Result<SystemStatus> {
|
||||
info!("Fetching Lidarr system status");
|
||||
let event = LidarrEvent::GetStatus;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), SystemStatus>(request_props, |system_status, mut app| {
|
||||
app.data.lidarr_data.version = system_status.version;
|
||||
app.data.lidarr_data.start_time = system_status.start_time;
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn add_sonarr_series(
|
||||
&mut self,
|
||||
mut add_series_body: AddSeriesBody,
|
||||
) -> anyhow::Result<Value> {
|
||||
) -> Result<Value> {
|
||||
info!("Adding new series to Sonarr");
|
||||
let event = SonarrEvent::AddSeries(AddSeriesBody::default());
|
||||
if let Some(tag_input_str) = add_series_body.tag_input_string.as_ref() {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS};
|
||||
use crate::models::Route;
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::lidarr_ui::library::LibraryUi;
|
||||
|
||||
#[test]
|
||||
fn test_library_ui_accepts() {
|
||||
for lidarr_block in ActiveLidarrBlock::iter() {
|
||||
if LIBRARY_BLOCKS.contains(&lidarr_block) {
|
||||
assert!(LibraryUi::accepts(Route::Lidarr(lidarr_block, None)));
|
||||
} else {
|
||||
assert!(!LibraryUi::accepts(Route::Lidarr(lidarr_block, None)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Rect},
|
||||
widgets::{Cell, Row},
|
||||
};
|
||||
|
||||
use crate::ui::widgets::managarr_table::ManagarrTable;
|
||||
use crate::utils::convert_to_gb;
|
||||
use crate::{
|
||||
app::App,
|
||||
models::{
|
||||
Route,
|
||||
lidarr_models::{Artist, ArtistStatus},
|
||||
servarr_data::lidarr::lidarr_data::{ActiveLidarrBlock, LIBRARY_BLOCKS},
|
||||
},
|
||||
ui::{
|
||||
DrawUi,
|
||||
styles::ManagarrStyle,
|
||||
utils::{get_width_from_percentage, layout_block_top_border},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "library_ui_tests.rs"]
|
||||
mod library_ui_tests;
|
||||
|
||||
pub(super) struct LibraryUi;
|
||||
|
||||
impl DrawUi for LibraryUi {
|
||||
fn accepts(route: Route) -> bool {
|
||||
if let Route::Lidarr(active_lidarr_block, _) = route {
|
||||
return LIBRARY_BLOCKS.contains(&active_lidarr_block);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
draw_library(f, app, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
if let Route::Lidarr(active_lidarr_block, _) = app.get_current_route() {
|
||||
let current_selection = if !app.data.lidarr_data.artists.items.is_empty() {
|
||||
app.data.lidarr_data.artists.current_selection().clone()
|
||||
} else {
|
||||
Artist::default()
|
||||
};
|
||||
let quality_profile_map = &app.data.lidarr_data.quality_profile_map;
|
||||
let metadata_profile_map = &app.data.lidarr_data.metadata_profile_map;
|
||||
let tags_map = &app.data.lidarr_data.tags_map;
|
||||
let content = Some(&mut app.data.lidarr_data.artists);
|
||||
|
||||
let artists_table_row_mapping = |artist: &Artist| {
|
||||
artist.artist_name.scroll_left_or_reset(
|
||||
get_width_from_percentage(area, 25),
|
||||
*artist == current_selection,
|
||||
app.ui_scroll_tick_count == 0,
|
||||
);
|
||||
let monitored = if artist.monitored { "🏷" } else { "" };
|
||||
let artist_type = artist.artist_type.clone().unwrap_or_default();
|
||||
let size = artist
|
||||
.statistics
|
||||
.as_ref()
|
||||
.map_or(0f64, |stats| convert_to_gb(stats.size_on_disk));
|
||||
let quality_profile = quality_profile_map
|
||||
.get_by_left(&artist.quality_profile_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let metadata_profile = metadata_profile_map
|
||||
.get_by_left(&artist.metadata_profile_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let albums = artist
|
||||
.statistics
|
||||
.as_ref()
|
||||
.map_or(0, |stats| stats.album_count);
|
||||
let tracks = artist
|
||||
.statistics
|
||||
.as_ref()
|
||||
.map_or(String::new(), |stats| {
|
||||
format!("{}/{}", stats.track_file_count, stats.total_track_count)
|
||||
});
|
||||
let tags = artist
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag_id| {
|
||||
let id = tag_id.as_i64()?;
|
||||
tags_map.get_by_left(&id).cloned()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
decorate_artist_row_with_style(
|
||||
artist,
|
||||
Row::new(vec![
|
||||
Cell::from(artist.artist_name.to_string()),
|
||||
Cell::from(artist_type),
|
||||
Cell::from(artist.status.to_display_str()),
|
||||
Cell::from(quality_profile),
|
||||
Cell::from(metadata_profile),
|
||||
Cell::from(albums.to_string()),
|
||||
Cell::from(tracks),
|
||||
Cell::from(format!("{size:.2} GB")),
|
||||
Cell::from(monitored.to_owned()),
|
||||
Cell::from(tags),
|
||||
]),
|
||||
)
|
||||
};
|
||||
let artists_table = ManagarrTable::new(content, artists_table_row_mapping)
|
||||
.block(layout_block_top_border())
|
||||
.loading(app.is_loading)
|
||||
.sorting(active_lidarr_block == ActiveLidarrBlock::ArtistsSortPrompt)
|
||||
.searching(active_lidarr_block == ActiveLidarrBlock::SearchArtists)
|
||||
.filtering(active_lidarr_block == ActiveLidarrBlock::FilterArtists)
|
||||
.search_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::SearchArtistsError)
|
||||
.filter_produced_empty_results(active_lidarr_block == ActiveLidarrBlock::FilterArtistsError)
|
||||
.headers([
|
||||
"Name",
|
||||
"Type",
|
||||
"Status",
|
||||
"Quality Profile",
|
||||
"Metadata Profile",
|
||||
"Albums",
|
||||
"Tracks",
|
||||
"Size",
|
||||
"Monitored",
|
||||
"Tags",
|
||||
])
|
||||
.constraints([
|
||||
Constraint::Percentage(22),
|
||||
Constraint::Percentage(8),
|
||||
Constraint::Percentage(8),
|
||||
Constraint::Percentage(12),
|
||||
Constraint::Percentage(12),
|
||||
Constraint::Percentage(6),
|
||||
Constraint::Percentage(8),
|
||||
Constraint::Percentage(7),
|
||||
Constraint::Percentage(6),
|
||||
Constraint::Percentage(11),
|
||||
]);
|
||||
|
||||
if [
|
||||
ActiveLidarrBlock::SearchArtists,
|
||||
ActiveLidarrBlock::FilterArtists,
|
||||
]
|
||||
.contains(&active_lidarr_block)
|
||||
{
|
||||
artists_table.show_cursor(f, area);
|
||||
}
|
||||
|
||||
f.render_widget(artists_table, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn decorate_artist_row_with_style<'a>(artist: &Artist, row: Row<'a>) -> Row<'a> {
|
||||
if !artist.monitored {
|
||||
return row.unmonitored();
|
||||
}
|
||||
|
||||
match artist.status {
|
||||
ArtistStatus::Ended => {
|
||||
if let Some(ref stats) = artist.statistics {
|
||||
return if stats.track_file_count == stats.total_track_count && stats.total_track_count > 0 {
|
||||
row.downloaded()
|
||||
} else {
|
||||
row.missing()
|
||||
};
|
||||
}
|
||||
row.indeterminate()
|
||||
}
|
||||
ArtistStatus::Continuing => {
|
||||
if let Some(ref stats) = artist.statistics {
|
||||
return if stats.track_file_count == stats.total_track_count && stats.total_track_count > 0 {
|
||||
row.unreleased()
|
||||
} else {
|
||||
row.missing()
|
||||
};
|
||||
}
|
||||
row.indeterminate()
|
||||
}
|
||||
_ => row.indeterminate(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::models::servarr_data::lidarr::lidarr_data::ActiveLidarrBlock;
|
||||
use crate::models::Route;
|
||||
use crate::ui::DrawUi;
|
||||
use crate::ui::lidarr_ui::LidarrUi;
|
||||
|
||||
#[test]
|
||||
fn test_lidarr_ui_accepts() {
|
||||
for lidarr_block in ActiveLidarrBlock::iter() {
|
||||
assert!(LidarrUi::accepts(Route::Lidarr(lidarr_block, None)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
use std::{cmp, iter};
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::ui::ui_test_utils::test_utils::Utc;
|
||||
use chrono::Duration;
|
||||
#[cfg(not(test))]
|
||||
use chrono::Utc;
|
||||
use library::LibraryUi;
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
text::Text,
|
||||
widgets::Paragraph,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
logos::LIDARR_LOGO,
|
||||
models::{
|
||||
Route,
|
||||
lidarr_models::DownloadRecord,
|
||||
servarr_data::lidarr::lidarr_data::LidarrData,
|
||||
servarr_models::{DiskSpace, RootFolder},
|
||||
},
|
||||
utils::convert_to_gb,
|
||||
};
|
||||
|
||||
use super::{
|
||||
DrawUi, draw_tabs,
|
||||
styles::ManagarrStyle,
|
||||
utils::{borderless_block, layout_block, line_gauge_with_label, line_gauge_with_title, title_block},
|
||||
widgets::loading_block::LoadingBlock,
|
||||
};
|
||||
|
||||
mod library;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lidarr_ui_tests.rs"]
|
||||
mod lidarr_ui_tests;
|
||||
|
||||
pub(super) struct LidarrUi;
|
||||
|
||||
impl DrawUi for LidarrUi {
|
||||
fn accepts(route: Route) -> bool {
|
||||
matches!(route, Route::Lidarr(_, _))
|
||||
}
|
||||
|
||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||
let content_area = draw_tabs(f, area, "Artists", &app.data.lidarr_data.main_tabs);
|
||||
let route = app.get_current_route();
|
||||
|
||||
match route {
|
||||
_ if LibraryUi::accepts(route) => LibraryUi::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_lidarr_logo(f, logo_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_stats_context(f: &mut Frame<'_>, app: &App<'_>, area: Rect) {
|
||||
let block = title_block("Stats");
|
||||
|
||||
if !app.data.lidarr_data.version.is_empty() {
|
||||
f.render_widget(block, area);
|
||||
let LidarrData {
|
||||
root_folders,
|
||||
disk_space_vec,
|
||||
start_time,
|
||||
..
|
||||
} = &app.data.lidarr_data;
|
||||
|
||||
let mut constraints = vec![
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
];
|
||||
|
||||
constraints.append(
|
||||
&mut iter::repeat_n(
|
||||
Constraint::Length(1),
|
||||
disk_space_vec.len() + root_folders.items.len() + 1,
|
||||
)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let stat_item_areas = Layout::vertical(constraints).margin(1).split(area);
|
||||
|
||||
let version_paragraph = Paragraph::new(Text::from(format!(
|
||||
"Lidarr Version: {}",
|
||||
app.data.lidarr_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..disk_space_vec.len() {
|
||||
let DiskSpace {
|
||||
free_space,
|
||||
total_space,
|
||||
} = &disk_space_vec[i];
|
||||
let title = 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[disk_space_vec.len() + 3]);
|
||||
|
||||
for i in 0..root_folders.items.len() {
|
||||
let RootFolder {
|
||||
path, free_space, ..
|
||||
} = &root_folders.items[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();
|
||||
|
||||
f.render_widget(
|
||||
root_folder_space,
|
||||
stat_item_areas[i + 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.lidarr_data.downloads.items;
|
||||
|
||||
if !downloads_vec.is_empty() {
|
||||
f.render_widget(block, area);
|
||||
|
||||
let max_items = ((((area.height as f64 / 2.0).floor() * 2.0) as i64) / 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.0
|
||||
} else {
|
||||
1f64 - (*sizeleft / *size)
|
||||
};
|
||||
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 draw_lidarr_logo(f: &mut Frame<'_>, area: Rect) {
|
||||
let logo_text = Text::from(LIDARR_LOGO);
|
||||
let logo = Paragraph::new(logo_text)
|
||||
.light_green()
|
||||
.block(layout_block().default())
|
||||
.centered();
|
||||
f.render_widget(logo, area);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Tabs;
|
||||
use ratatui::widgets::Wrap;
|
||||
use ratatui::widgets::{Clear, Row};
|
||||
use lidarr_ui::LidarrUi;
|
||||
use sonarr_ui::SonarrUi;
|
||||
use utils::layout_block;
|
||||
|
||||
@@ -27,6 +28,7 @@ use crate::ui::widgets::managarr_table::ManagarrTable;
|
||||
use crate::ui::widgets::popup::Size;
|
||||
|
||||
mod builtin_themes;
|
||||
mod lidarr_ui;
|
||||
mod radarr_ui;
|
||||
mod sonarr_ui;
|
||||
mod styles;
|
||||
@@ -86,6 +88,10 @@ pub fn ui(f: &mut Frame<'_>, app: &mut App<'_>) {
|
||||
SonarrUi::draw_context_row(f, app, context_area);
|
||||
SonarrUi::draw(f, app, table_area);
|
||||
}
|
||||
route if LidarrUi::accepts(route) => {
|
||||
LidarrUi::draw_context_row(f, app, context_area);
|
||||
LidarrUi::draw(f, app, table_area);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user