Added full support for managing the blocklist

This commit is contained in:
2024-02-15 16:20:03 -07:00
parent d869647dd8
commit 6cadf70c1e
42 changed files with 2004 additions and 123 deletions
Generated
+1 -1
View File
@@ -863,7 +863,7 @@ dependencies = [
[[package]]
name = "managarr"
version = "0.0.33"
version = "0.0.34"
dependencies = [
"anyhow",
"backtrace",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "managarr"
version = "0.0.33"
version = "0.0.34"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI to manage your Servarrs"
keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"]
+4 -1
View File
@@ -36,7 +36,7 @@ pleasant as possible!
### Radarr
- [x] View your library, downloads, collections
- [x] View your library, downloads, collections, and blocklist
- [x] View details of a specific movie including description, history, downloaded file info, or the credits
- [x] View details of any collection and the movies in them
- [x] Search your library or collections
@@ -48,6 +48,7 @@ pleasant as possible!
- [x] Edit your movies, collections, and indexers
- [x] Manage your tags
- [x] Manage your root folders
- [x] Manage your blocklist
- [ ] Manage your quality profiles
- [ ] Manage your quality definitions
- [x] View and browse logs, tasks, events queues, and updates
@@ -144,6 +145,8 @@ tautulli:
![logs](screenshots/logs.png)
![new_movie_search](screenshots/new_movie_search.png)
![add_new_movie](screenshots/add_new_movie.png)
![collection_details](screenshots/collection_details.png)
![indexers](screenshots/indexers.png)
## Dependencies
* [ratatui](https://github.com/tui-rs-revival/ratatui)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 176 KiB

+5
View File
@@ -15,6 +15,7 @@ generate_keybindings! {
left,
right,
backspace,
clear,
search,
settings,
filter,
@@ -67,6 +68,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::Backspace,
desc: "backspace",
},
clear: KeyBinding {
key: Key::Char('c'),
desc: "clear",
},
search: KeyBinding {
key: Key::Char('s'),
desc: "search",
+1
View File
@@ -13,6 +13,7 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")]
#[case(DEFAULT_KEYBINDINGS.clear, Key::Char('c'), "clear")]
#[case(DEFAULT_KEYBINDINGS.search, Key::Char('s'), "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('s'), "settings")]
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")]
+5
View File
@@ -11,6 +11,11 @@ mod radarr_tests;
impl<'a> App<'a> {
pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) {
match active_radarr_block {
ActiveRadarrBlock::Blocklist => {
self
.dispatch_network_event(RadarrEvent::GetBlocklist.into())
.await;
}
ActiveRadarrBlock::Collections => {
self
.dispatch_network_event(RadarrEvent::GetCollections.into())
+20 -10
View File
@@ -21,14 +21,6 @@ pub static LIBRARY_CONTEXT_CLUES: [ContextClue; 10] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
];
pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
@@ -43,6 +35,25 @@ pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter"),
];
pub static DOWNLOADS_CONTEXT_CLUES: [ContextClue; 2] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
];
pub static BLOCKLIST_CONTEXT_CLUES: [ContextClue; 5] = [
(
DEFAULT_KEYBINDINGS.refresh,
DEFAULT_KEYBINDINGS.refresh.desc,
),
(DEFAULT_KEYBINDINGS.sort, DEFAULT_KEYBINDINGS.sort.desc),
(DEFAULT_KEYBINDINGS.submit, "details"),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
(DEFAULT_KEYBINDINGS.clear, "clear blocklist"),
];
pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.desc),
@@ -52,8 +63,7 @@ pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [
),
];
pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 7] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.submit, "edit indexer"),
(
DEFAULT_KEYBINDINGS.settings,
+48 -22
View File
@@ -4,7 +4,7 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
ADD_MOVIE_SEARCH_RESULTS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES,
COLLECTION_DETAILS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
@@ -67,22 +67,6 @@ mod tests {
assert_eq!(library_context_clues_iter.next(), None);
}
#[test]
fn test_downloads_context_clues() {
let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter();
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
assert_eq!(downloads_context_clues_iter.next(), None);
}
#[test]
fn test_collections_context_clues() {
let mut collections_context_clues = COLLECTIONS_CONTEXT_CLUES.iter();
@@ -129,6 +113,53 @@ mod tests {
assert_eq!(collections_context_clues.next(), None);
}
#[test]
fn test_downloads_context_clues() {
let mut downloads_context_clues_iter = DOWNLOADS_CONTEXT_CLUES.iter();
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = downloads_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
assert_eq!(downloads_context_clues_iter.next(), None);
}
#[test]
fn test_blocklist_context_clues() {
let mut blocklist_context_clues_iter = BLOCKLIST_CONTEXT_CLUES.iter();
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.refresh);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.refresh.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.sort);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.sort.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "details");
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.delete);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.delete.desc);
let (key_binding, description) = blocklist_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.clear);
assert_str_eq!(*description, "clear blocklist");
assert_eq!(blocklist_context_clues_iter.next(), None);
}
#[test]
fn test_root_folders_context_clues() {
let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter();
@@ -156,11 +187,6 @@ mod tests {
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.add);
assert_str_eq!(*description, DEFAULT_KEYBINDINGS.add.desc);
let (key_binding, description) = indexers_context_clues_iter.next().unwrap();
assert_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "edit indexer");
+17
View File
@@ -12,6 +12,23 @@ mod tests {
use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent;
#[tokio::test]
async fn test_dispatch_by_blocklist_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
app
.dispatch_by_radarr_block(&ActiveRadarrBlock::Blocklist)
.await;
assert!(app.is_loading);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetBlocklist.into()
);
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.tick_count, 0);
}
#[tokio::test]
async fn test_dispatch_by_collections_block() {
let (mut app, mut sync_network_rx) = construct_app_unit();
@@ -0,0 +1,707 @@
#[cfg(test)]
mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::blocklist::{blocklist_sorting_options, BlocklistHandler};
use crate::handlers::KeyEventHandler;
use crate::models::radarr_models::{BlocklistItem, Language, Movie, Quality, QualityWrapper};
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption;
use chrono::DateTime;
use pretty_assertions::{assert_eq, assert_str_eq};
use std::cmp::Ordering;
use strum::IntoEnumIterator;
mod test_handle_scroll_up_and_down {
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use crate::models::radarr_models::BlocklistItem;
use crate::{simple_stateful_iterable_vec, test_iterable_scroll};
use super::*;
test_iterable_scroll!(
test_blocklist_scroll,
BlocklistHandler,
blocklist,
simple_stateful_iterable_vec!(BlocklistItem, String, source_title),
ActiveRadarrBlock::Blocklist,
None,
source_title,
to_string
);
#[rstest]
fn test_blocklist_sort_scroll(
#[values(DEFAULT_KEYBINDINGS.up.key, DEFAULT_KEYBINDINGS.down.key)] key: Key,
) {
let blocklist_field_vec = sort_options();
let mut app = App::default();
app.data.radarr_data.blocklist.sorting(sort_options());
if key == Key::Up {
for i in (0..blocklist_field_vec.len()).rev() {
BlocklistHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[i]
);
}
} else {
for i in 0..blocklist_field_vec.len() {
BlocklistHandler::with(
&key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[(i + 1) % blocklist_field_vec.len()]
);
}
}
}
}
mod test_handle_home_end {
use super::*;
use crate::models::radarr_models::BlocklistItem;
use crate::{extended_stateful_iterable_vec, test_iterable_home_and_end};
use pretty_assertions::{assert_eq, assert_str_eq};
test_iterable_home_and_end!(
test_blocklist_home_and_end,
BlocklistHandler,
blocklist,
extended_stateful_iterable_vec!(BlocklistItem, String, source_title),
ActiveRadarrBlock::Blocklist,
None,
source_title,
to_string
);
#[test]
fn test_blocklist_sort_home_end() {
let blocklist_field_vec = sort_options();
let mut app = App::default();
app.data.radarr_data.blocklist.sorting(sort_options());
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.end.key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[blocklist_field_vec.len() - 1]
);
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.home.key,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app
.data
.radarr_data
.blocklist
.sort
.as_ref()
.unwrap()
.current_selection(),
&blocklist_field_vec[0]
);
}
}
mod test_handle_delete {
use super::*;
use crate::assert_delete_prompt;
use pretty_assertions::assert_eq;
const DELETE_KEY: Key = DEFAULT_KEYBINDINGS.delete.key;
#[test]
fn test_delete_blocklist_item_prompt() {
assert_delete_prompt!(
BlocklistHandler,
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt
);
}
}
mod test_handle_left_right_action {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[test]
fn test_blocklist_tab_left() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(3);
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
);
}
#[test]
fn test_blocklist_tab_right() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(3);
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::RootFolders.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into()
);
}
#[rstest]
fn test_blocklist_left_right_prompt_toggle(
#[values(
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt
)]
active_radarr_block: ActiveRadarrBlock,
#[values(DEFAULT_KEYBINDINGS.left.key, DEFAULT_KEYBINDINGS.right.key)] key: Key,
) {
let mut app = App::default();
BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle();
assert!(app.data.radarr_data.prompt_confirm);
BlocklistHandler::with(&key, &mut app, &active_radarr_block, &None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
}
}
mod test_handle_submit {
use crate::network::radarr_network::RadarrEvent;
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
const SUBMIT_KEY: Key = DEFAULT_KEYBINDINGS.submit.key;
#[rstest]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
RadarrEvent::DeleteBlocklistItem
)]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
RadarrEvent::ClearBlocklist
)]
fn test_blocklist_prompt_confirm_submit(
#[case] base_route: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock,
#[case] expected_action: RadarrEvent,
) {
let mut app = App::default();
app.data.radarr_data.prompt_confirm = true;
app.push_navigation_stack(base_route.into());
app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle();
assert!(app.data.radarr_data.prompt_confirm);
assert_eq!(
app.data.radarr_data.prompt_confirm_action,
Some(expected_action)
);
assert_eq!(app.get_current_route(), &base_route.into());
}
#[rstest]
fn test_blocklist_prompt_decline_submit(
#[values(
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt
)]
prompt_block: ActiveRadarrBlock,
) {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(prompt_block.into());
BlocklistHandler::with(&SUBMIT_KEY, &mut app, &prompt_block, &None).handle();
assert!(!app.data.radarr_data.prompt_confirm);
assert_eq!(app.data.radarr_data.prompt_confirm_action, None);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
}
#[test]
fn test_blocklist_sort_prompt_submit() {
let mut app = App::default();
app.data.radarr_data.blocklist.sort_asc = true;
app.data.radarr_data.blocklist.sorting(sort_options());
app.data.radarr_data.blocklist.set_items(blocklist_vec());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
let mut expected_vec = blocklist_vec();
expected_vec.sort_by(|a, b| a.id.cmp(&b.id));
expected_vec.reverse();
BlocklistHandler::with(
&SUBMIT_KEY,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(app.data.radarr_data.blocklist.items, expected_vec);
}
}
mod test_handle_esc {
use super::*;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
use pretty_assertions::assert_eq;
use rstest::rstest;
const ESC_KEY: Key = DEFAULT_KEYBINDINGS.esc.key;
#[rstest]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::DeleteBlocklistItemPrompt
)]
#[case(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt
)]
fn test_blocklist_prompt_blocks_esc(
#[case] base_block: ActiveRadarrBlock,
#[case] prompt_block: ActiveRadarrBlock,
) {
let mut app = App::default();
app.push_navigation_stack(base_block.into());
app.push_navigation_stack(prompt_block.into());
app.data.radarr_data.prompt_confirm = true;
BlocklistHandler::with(&ESC_KEY, &mut app, &prompt_block, &None).handle();
assert_eq!(app.get_current_route(), &base_block.into());
assert!(!app.data.radarr_data.prompt_confirm);
}
#[test]
fn test_esc_blocklist_item_details() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into());
BlocklistHandler::with(
&ESC_KEY,
&mut app,
&ActiveRadarrBlock::BlocklistItemDetails,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
}
#[test]
fn test_blocklist_sort_prompt_block_esc() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
BlocklistHandler::with(
&ESC_KEY,
&mut app,
&ActiveRadarrBlock::BlocklistSortPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
}
#[test]
fn test_default_esc() {
let mut app = App::default();
app.error = "test error".to_owned().into();
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
app.push_navigation_stack(ActiveRadarrBlock::Blocklist.into());
DownloadsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::Blocklist, &None).handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert!(app.error.text.is_empty());
}
}
mod test_handle_key_char {
use super::*;
use crate::assert_refresh_key;
use pretty_assertions::assert_eq;
#[test]
fn test_refresh_blocklist_key() {
assert_refresh_key!(BlocklistHandler, ActiveRadarrBlock::Blocklist);
}
#[test]
fn test_clear_blocklist_key() {
let mut app = App::default();
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.clear.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into()
);
}
#[test]
fn test_sort_key() {
let mut app = App::default();
BlocklistHandler::with(
&DEFAULT_KEYBINDINGS.sort.key,
&mut app,
&ActiveRadarrBlock::Blocklist,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::BlocklistSortPrompt.into()
);
assert_eq!(
app.data.radarr_data.blocklist.sort.as_ref().unwrap().items,
blocklist_sorting_options()
);
assert!(!app.data.radarr_data.blocklist.sort_asc);
}
}
#[test]
fn test_blocklist_sorting_options_movie_title() {
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
a.movie
.title
.text
.to_lowercase()
.cmp(&b.movie.title.text.to_lowercase())
};
let mut expected_blocklist_vec = blocklist_vec();
expected_blocklist_vec.sort_by(expected_cmp_fn);
let sort_option = blocklist_sorting_options()[0].clone();
let mut sorted_blocklist_vec = blocklist_vec();
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
assert_str_eq!(sort_option.name, "Movie Title");
}
#[test]
fn test_blocklist_sorting_options_source_title() {
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
a.source_title
.to_lowercase()
.cmp(&b.source_title.to_lowercase())
};
let mut expected_blocklist_vec = blocklist_vec();
expected_blocklist_vec.sort_by(expected_cmp_fn);
let sort_option = blocklist_sorting_options()[1].clone();
let mut sorted_blocklist_vec = blocklist_vec();
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
assert_str_eq!(sort_option.name, "Source Title");
}
#[test]
fn test_blocklist_sorting_options_languages() {
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
let a_languages = a
.languages
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
let b_languages = b
.languages
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
a_languages.cmp(&b_languages)
};
let mut expected_blocklist_vec = blocklist_vec();
expected_blocklist_vec.sort_by(expected_cmp_fn);
let sort_option = blocklist_sorting_options()[2].clone();
let mut sorted_blocklist_vec = blocklist_vec();
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
assert_str_eq!(sort_option.name, "Languages");
}
#[test]
fn test_blocklist_sorting_options_quality() {
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
a.quality
.quality
.name
.to_lowercase()
.cmp(&b.quality.quality.name.to_lowercase())
};
let mut expected_blocklist_vec = blocklist_vec();
expected_blocklist_vec.sort_by(expected_cmp_fn);
let sort_option = blocklist_sorting_options()[3].clone();
let mut sorted_blocklist_vec = blocklist_vec();
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
assert_str_eq!(sort_option.name, "Quality");
}
#[test]
fn test_blocklist_sorting_options_custom_formats() {
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering = |a, b| {
let a_custom_formats = a
.custom_formats
.as_ref()
.unwrap_or(&Vec::new())
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
let b_custom_formats = b
.custom_formats
.as_ref()
.unwrap_or(&Vec::new())
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
a_custom_formats.cmp(&b_custom_formats)
};
let mut expected_blocklist_vec = blocklist_vec();
expected_blocklist_vec.sort_by(expected_cmp_fn);
let sort_option = blocklist_sorting_options()[4].clone();
let mut sorted_blocklist_vec = blocklist_vec();
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
assert_str_eq!(sort_option.name, "Formats");
}
#[test]
fn test_blocklist_sorting_options_date() {
let expected_cmp_fn: fn(&BlocklistItem, &BlocklistItem) -> Ordering =
|a, b| a.date.cmp(&b.date);
let mut expected_blocklist_vec = blocklist_vec();
expected_blocklist_vec.sort_by(expected_cmp_fn);
let sort_option = blocklist_sorting_options()[5].clone();
let mut sorted_blocklist_vec = blocklist_vec();
sorted_blocklist_vec.sort_by(sort_option.cmp_fn.unwrap());
assert_eq!(sorted_blocklist_vec, expected_blocklist_vec);
assert_str_eq!(sort_option.name, "Date");
}
fn blocklist_vec() -> Vec<BlocklistItem> {
vec![
BlocklistItem {
id: 3,
source_title: "test 1".to_owned(),
languages: vec![Language {
name: "telgu".to_owned(),
}],
quality: QualityWrapper {
quality: Quality {
name: "HD - 1080p".to_owned(),
},
},
custom_formats: Some(vec![Language {
name: "nikki".to_owned(),
}]),
date: DateTime::from(DateTime::parse_from_rfc3339("2024-01-10T07:28:45Z").unwrap()),
movie: Movie {
title: "test 3".into(),
..Movie::default()
},
..BlocklistItem::default()
},
BlocklistItem {
id: 2,
source_title: "test 2".to_owned(),
languages: vec![Language {
name: "chinese".to_owned(),
}],
quality: QualityWrapper {
quality: Quality {
name: "SD - 720p".to_owned(),
},
},
custom_formats: Some(vec![
Language {
name: "alex".to_owned(),
},
Language {
name: "English".to_owned(),
},
]),
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
movie: Movie {
title: "test 2".into(),
..Movie::default()
},
..BlocklistItem::default()
},
BlocklistItem {
id: 1,
source_title: "test 3".to_owned(),
languages: vec![Language {
name: "english".to_owned(),
}],
quality: QualityWrapper {
quality: Quality {
name: "HD - 1080p".to_owned(),
},
},
custom_formats: Some(vec![Language {
name: "English".to_owned(),
}]),
date: DateTime::from(DateTime::parse_from_rfc3339("2024-03-10T07:28:45Z").unwrap()),
movie: Movie {
title: "test 1".into(),
..Movie::default()
},
..BlocklistItem::default()
},
]
}
fn sort_options() -> Vec<SortOption<BlocklistItem>> {
vec![SortOption {
name: "Test 1",
cmp_fn: Some(|a, b| {
b.source_title
.to_lowercase()
.cmp(&a.source_title.to_lowercase())
}),
}]
}
#[test]
fn test_blocklist_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if BLOCKLIST_BLOCKS.contains(&active_radarr_block) {
assert!(BlocklistHandler::accepts(&active_radarr_block));
} else {
assert!(!BlocklistHandler::accepts(&active_radarr_block));
}
})
}
}
@@ -0,0 +1,284 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::App;
use crate::event::Key;
use crate::handlers::radarr_handlers::handle_change_tab_left_right_keys;
use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler};
use crate::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::stateful_table::SortOption;
use crate::models::Scrollable;
use crate::network::radarr_network::RadarrEvent;
#[cfg(test)]
#[path = "blocklist_handler_tests.rs"]
mod blocklist_handler_tests;
pub(super) struct BlocklistHandler<'a, 'b> {
key: &'a Key,
app: &'a mut App<'b>,
active_radarr_block: &'a ActiveRadarrBlock,
_context: &'a Option<ActiveRadarrBlock>,
}
impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for BlocklistHandler<'a, 'b> {
fn accepts(active_block: &'a ActiveRadarrBlock) -> bool {
BLOCKLIST_BLOCKS.contains(active_block)
}
fn with(
key: &'a Key,
app: &'a mut App<'b>,
active_block: &'a ActiveRadarrBlock,
context: &'a Option<ActiveRadarrBlock>,
) -> Self {
BlocklistHandler {
key,
app,
active_radarr_block: active_block,
_context: context,
}
}
fn get_key(&self) -> &Key {
self.key
}
fn handle_scroll_up(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_up(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_up(),
_ => (),
}
}
fn handle_scroll_down(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_down(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_down(),
_ => (),
}
}
fn handle_home(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_top(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_to_top(),
_ => (),
}
}
fn handle_end(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => self.app.data.radarr_data.blocklist.scroll_to_bottom(),
ActiveRadarrBlock::BlocklistSortPrompt => self
.app
.data
.radarr_data
.blocklist
.sort
.as_mut()
.unwrap()
.scroll_to_bottom(),
_ => (),
}
}
fn handle_delete(&mut self) {
if self.active_radarr_block == &ActiveRadarrBlock::Blocklist {
self
.app
.push_navigation_stack(ActiveRadarrBlock::DeleteBlocklistItemPrompt.into());
}
}
fn handle_left_right_action(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::Blocklist => handle_change_tab_left_right_keys(self.app, self.key),
ActiveRadarrBlock::DeleteBlocklistItemPrompt
| ActiveRadarrBlock::BlocklistClearAllItemsPrompt => handle_prompt_toggle(self.app, self.key),
_ => {}
}
}
fn handle_submit(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::DeleteBlocklistItem);
}
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
if self.app.data.radarr_data.prompt_confirm {
self.app.data.radarr_data.prompt_confirm_action = Some(RadarrEvent::ClearBlocklist);
}
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::BlocklistSortPrompt => {
self
.app
.data
.radarr_data
.blocklist
.items
.sort_by(|a, b| a.id.cmp(&b.id));
self.app.data.radarr_data.blocklist.apply_sorting();
self.app.pop_navigation_stack();
}
ActiveRadarrBlock::Blocklist => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::BlocklistItemDetails.into());
}
_ => (),
}
}
fn handle_esc(&mut self) {
match self.active_radarr_block {
ActiveRadarrBlock::DeleteBlocklistItemPrompt
| ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false;
}
ActiveRadarrBlock::BlocklistItemDetails | ActiveRadarrBlock::BlocklistSortPrompt => {
self.app.pop_navigation_stack();
}
_ => handle_clear_errors(self.app),
}
}
fn handle_char_key_event(&mut self) {
let key = self.key;
if self.active_radarr_block == &ActiveRadarrBlock::Blocklist {
match self.key {
_ if *key == DEFAULT_KEYBINDINGS.refresh.key => {
self.app.should_refresh = true;
}
_ if *key == DEFAULT_KEYBINDINGS.clear.key => {
self
.app
.push_navigation_stack(ActiveRadarrBlock::BlocklistClearAllItemsPrompt.into());
}
_ if *key == DEFAULT_KEYBINDINGS.sort.key => {
self
.app
.data
.radarr_data
.blocklist
.sorting(blocklist_sorting_options());
self
.app
.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
}
_ => (),
}
}
}
}
fn blocklist_sorting_options() -> Vec<SortOption<BlocklistItem>> {
vec![
SortOption {
name: "Movie Title",
cmp_fn: Some(|a, b| {
a.movie
.title
.text
.to_lowercase()
.cmp(&b.movie.title.text.to_lowercase())
}),
},
SortOption {
name: "Source Title",
cmp_fn: Some(|a, b| {
a.source_title
.to_lowercase()
.cmp(&b.source_title.to_lowercase())
}),
},
SortOption {
name: "Languages",
cmp_fn: Some(|a, b| {
let a_languages = a
.languages
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
let b_languages = b
.languages
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
a_languages.cmp(&b_languages)
}),
},
SortOption {
name: "Quality",
cmp_fn: Some(|a, b| {
a.quality
.quality
.name
.to_lowercase()
.cmp(&b.quality.quality.name.to_lowercase())
}),
},
SortOption {
name: "Formats",
cmp_fn: Some(|a, b| {
let a_custom_formats = a
.custom_formats
.as_ref()
.unwrap_or(&Vec::new())
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
let b_custom_formats = b
.custom_formats
.as_ref()
.unwrap_or(&Vec::new())
.iter()
.map(|lang| lang.name.to_lowercase())
.collect::<Vec<String>>()
.join(", ");
a_custom_formats.cmp(&b_custom_formats)
}),
},
SortOption {
name: "Date",
cmp_fn: Some(|a, b| a.date.cmp(&b.date)),
},
]
}
@@ -267,7 +267,7 @@ mod tests {
#[test]
fn test_collections_tab_left() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(2);
app.data.radarr_data.main_tabs.set_index(1);
CollectionsHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
@@ -279,18 +279,15 @@ mod tests {
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
&ActiveRadarrBlock::Movies.into()
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
}
#[test]
fn test_collections_tab_right() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(2);
app.data.radarr_data.main_tabs.set_index(1);
CollectionsHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
@@ -302,11 +299,11 @@ mod tests {
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::RootFolders.into()
&ActiveRadarrBlock::Downloads.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into()
&ActiveRadarrBlock::Downloads.into()
);
}
@@ -832,6 +829,26 @@ mod tests {
assert!(!app.data.radarr_data.prompt_confirm);
}
#[test]
fn test_collections_sort_prompt_block_esc() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Collections.into());
app.push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into());
CollectionsHandler::with(
&ESC_KEY,
&mut app,
&ActiveRadarrBlock::CollectionsSortPrompt,
&None,
)
.handle();
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
);
}
#[test]
fn test_default_esc() {
let mut app = App::default();
@@ -287,6 +287,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for CollectionsHandler<'
self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false;
}
ActiveRadarrBlock::CollectionsSortPrompt => {
self.app.pop_navigation_stack();
}
_ => {
self.app.data.radarr_data.collections.reset_search();
self.app.data.radarr_data.collections.reset_filter();
@@ -74,7 +74,7 @@ mod tests {
#[test]
fn test_downloads_tab_left() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(1);
app.data.radarr_data.main_tabs.set_index(2);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
@@ -84,26 +84,6 @@ mod tests {
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Movies.into()
);
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
}
#[test]
fn test_downloads_tab_right() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(1);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Collections.into()
@@ -114,6 +94,29 @@ mod tests {
);
}
#[test]
fn test_downloads_tab_right() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(2);
DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
&mut app,
&ActiveRadarrBlock::Downloads,
&None,
)
.handle();
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Blocklist.into()
);
}
#[rstest]
fn test_downloads_left_right_prompt_toggle(
#[values(
@@ -78,7 +78,7 @@ mod tests {
#[test]
fn test_indexers_tab_left() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(4);
app.data.radarr_data.main_tabs.set_index(5);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
@@ -101,7 +101,7 @@ mod tests {
#[test]
fn test_indexers_tab_right() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(4);
app.data.radarr_data.main_tabs.set_index(5);
IndexersHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
@@ -312,11 +312,11 @@ mod tests {
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Downloads.into()
&ActiveRadarrBlock::Collections.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
&ActiveRadarrBlock::Collections.into()
);
}
@@ -776,6 +776,23 @@ mod tests {
assert!(!app.data.radarr_data.prompt_confirm);
}
#[test]
fn test_movies_sort_prompt_block_esc() {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
app.push_navigation_stack(ActiveRadarrBlock::MoviesSortPrompt.into());
LibraryHandler::with(
&ESC_KEY,
&mut app,
&ActiveRadarrBlock::MoviesSortPrompt,
&None,
)
.handle();
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
}
#[test]
fn test_default_esc() {
let mut app = App::default();
@@ -298,6 +298,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for LibraryHandler<'a, '
self.app.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false;
}
ActiveRadarrBlock::MoviesSortPrompt => {
self.app.pop_navigation_stack();
}
_ => {
self.app.data.radarr_data.movies.reset_search();
self.app.data.radarr_data.movies.reset_filter();
+5
View File
@@ -1,4 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::handlers::radarr_handlers::blocklist::BlocklistHandler;
use crate::handlers::radarr_handlers::collections::CollectionsHandler;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
use crate::handlers::radarr_handlers::indexers::IndexersHandler;
@@ -9,6 +10,7 @@ use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::{App, Key};
mod blocklist;
mod collections;
mod downloads;
mod indexers;
@@ -54,6 +56,9 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveRadarrBlock> for RadarrHandler<'a, 'b
RootFoldersHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle()
}
_ if BlocklistHandler::accepts(self.active_radarr_block) => {
BlocklistHandler::with(self.key, self.app, self.active_radarr_block, self.context).handle()
}
_ => self.handle_key_event(),
}
}
@@ -12,12 +12,13 @@ mod tests {
use crate::test_handler_delegation;
#[rstest]
#[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Downloads)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections)]
#[case(2, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)]
#[case(3, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Indexers)]
#[case(4, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(5, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
#[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)]
#[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)]
#[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)]
#[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)]
#[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
fn test_radarr_handler_change_tab_left_right_keys(
#[case] index: usize,
#[case] left_block: ActiveRadarrBlock,
@@ -68,6 +69,7 @@ mod tests {
fn test_delegates_library_blocks_to_library_handler(
#[values(
ActiveRadarrBlock::Movies,
ActiveRadarrBlock::MoviesSortPrompt,
ActiveRadarrBlock::SearchMovie,
ActiveRadarrBlock::SearchMovieError,
ActiveRadarrBlock::FilterMovies,
@@ -112,6 +114,7 @@ mod tests {
#[values(
ActiveRadarrBlock::Collections,
ActiveRadarrBlock::SearchCollection,
ActiveRadarrBlock::CollectionsSortPrompt,
ActiveRadarrBlock::SearchCollectionError,
ActiveRadarrBlock::FilterCollections,
ActiveRadarrBlock::FilterCollectionsError,
@@ -189,6 +192,24 @@ mod tests {
);
}
#[rstest]
fn test_delegates_blocklist_blocks_to_blocklist_handler(
#[values(
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::BlocklistItemDetails,
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
ActiveRadarrBlock::BlocklistSortPrompt
)]
active_radarr_block: ActiveRadarrBlock,
) {
test_handler_delegation!(
RadarrHandler,
ActiveRadarrBlock::Blocklist,
active_radarr_block
);
}
#[test]
fn test_radarr_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
@@ -123,7 +123,7 @@ mod tests {
#[test]
fn test_root_folders_tab_left() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(3);
app.data.radarr_data.main_tabs.set_index(4);
RootFoldersHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
@@ -135,18 +135,18 @@ mod tests {
assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Collections.into()
&ActiveRadarrBlock::Blocklist.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Collections.into()
&ActiveRadarrBlock::Blocklist.into()
);
}
#[test]
fn test_root_folders_tab_right() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(3);
app.data.radarr_data.main_tabs.set_index(4);
RootFoldersHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
@@ -422,11 +422,6 @@ mod tests {
let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::System.into());
app.push_navigation_stack(ActiveRadarrBlock::SystemUpdates.into());
app
.data
.radarr_data
.queued_events
.set_items(vec![QueueEvent::default()]);
SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemUpdates, &None)
.handle();
@@ -22,7 +22,7 @@ mod tests {
#[test]
fn test_system_tab_left() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(5);
app.data.radarr_data.main_tabs.set_index(6);
SystemHandler::with(
&DEFAULT_KEYBINDINGS.left.key,
@@ -42,7 +42,7 @@ mod tests {
#[test]
fn test_system_tab_right() {
let mut app = App::default();
app.data.radarr_data.main_tabs.set_index(5);
app.data.radarr_data.main_tabs.set_index(6);
SystemHandler::with(
&DEFAULT_KEYBINDINGS.right.key,
+23
View File
@@ -54,6 +54,29 @@ pub struct AddRootFolderBody {
pub path: String,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct BlocklistResponse {
pub records: Vec<BlocklistItem>,
}
#[derive(Default, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BlocklistItem {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub movie_id: i64,
pub source_title: String,
pub languages: Vec<Language>,
pub quality: QualityWrapper,
pub custom_formats: Option<Vec<Language>>,
pub date: DateTime<Utc>,
pub protocol: String,
pub indexer: String,
pub message: String,
pub movie: Movie,
}
#[derive(Deserialize, Derivative, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Collection {
+27 -7
View File
@@ -1,13 +1,13 @@
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::{
COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES,
};
use crate::models::radarr_models::{
AddMovieSearchResult, Collection, CollectionMovie, DiskSpace, DownloadRecord, Indexer,
IndexerSettings, Movie, QueueEvent, RootFolder, Task,
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DiskSpace, DownloadRecord,
Indexer, IndexerSettings, Movie, QueueEvent, RootFolder, Task,
};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
@@ -40,6 +40,7 @@ pub struct RadarrData<'a> {
pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>,
pub downloads: StatefulTable<DownloadRecord>,
pub indexers: StatefulTable<Indexer>,
pub blocklist: StatefulTable<BlocklistItem>,
pub quality_profile_map: BiMap<i64, String>,
pub tags_map: BiMap<i64, String>,
pub collections: StatefulTable<Collection>,
@@ -91,6 +92,7 @@ impl<'a> Default for RadarrData<'a> {
selected_block: BlockSelectionState::default(),
downloads: StatefulTable::default(),
indexers: StatefulTable::default(),
blocklist: StatefulTable::default(),
quality_profile_map: BiMap::default(),
tags_map: BiMap::default(),
collections: StatefulTable::default(),
@@ -122,6 +124,12 @@ impl<'a> Default for RadarrData<'a> {
help: String::new(),
contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)),
},
TabRoute {
title: "Collections",
route: ActiveRadarrBlock::Collections.into(),
help: String::new(),
contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)),
},
TabRoute {
title: "Downloads",
route: ActiveRadarrBlock::Downloads.into(),
@@ -129,10 +137,10 @@ impl<'a> Default for RadarrData<'a> {
contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)),
},
TabRoute {
title: "Collections",
route: ActiveRadarrBlock::Collections.into(),
title: "Blocklist",
route: ActiveRadarrBlock::Blocklist.into(),
help: String::new(),
contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)),
contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)),
},
TabRoute {
title: "Root Folders",
@@ -213,11 +221,16 @@ pub enum ActiveRadarrBlock {
AddMovieEmptySearchResults,
AddRootFolderPrompt,
AutomaticallySearchMoviePrompt,
Blocklist,
BlocklistClearAllItemsPrompt,
BlocklistItemDetails,
BlocklistSortPrompt,
Collections,
CollectionsSortPrompt,
CollectionDetails,
Cast,
Crew,
DeleteBlocklistItemPrompt,
DeleteDownloadPrompt,
DeleteIndexerPrompt,
DeleteMoviePrompt,
@@ -323,6 +336,13 @@ pub static ROOT_FOLDERS_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::AddRootFolderPrompt,
ActiveRadarrBlock::DeleteRootFolderPrompt,
];
pub static BLOCKLIST_BLOCKS: [ActiveRadarrBlock; 5] = [
ActiveRadarrBlock::Blocklist,
ActiveRadarrBlock::BlocklistItemDetails,
ActiveRadarrBlock::DeleteBlocklistItemPrompt,
ActiveRadarrBlock::BlocklistClearAllItemsPrompt,
ActiveRadarrBlock::BlocklistSortPrompt,
];
pub static ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::AddMovieSearchInput,
ActiveRadarrBlock::AddMovieSearchResults,
@@ -6,8 +6,8 @@ mod tests {
use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::{
COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
INDEXERS_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES,
};
@@ -64,6 +64,7 @@ mod tests {
assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
assert!(radarr_data.downloads.items.is_empty());
assert!(radarr_data.indexers.items.is_empty());
assert!(radarr_data.blocklist.items.is_empty());
assert!(radarr_data.quality_profile_map.is_empty());
assert!(radarr_data.tags_map.is_empty());
assert!(radarr_data.collections.items.is_empty());
@@ -89,7 +90,7 @@ mod tests {
assert!(!radarr_data.delete_movie_files);
assert!(!radarr_data.add_list_exclusion);
assert_eq!(radarr_data.main_tabs.tabs.len(), 6);
assert_eq!(radarr_data.main_tabs.tabs.len(), 7);
assert_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!(
@@ -102,58 +103,69 @@ mod tests {
Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES))
);
assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Downloads");
assert_str_eq!(radarr_data.main_tabs.tabs[1].title, "Collections");
assert_eq!(
radarr_data.main_tabs.tabs[1].route,
ActiveRadarrBlock::Downloads.into()
ActiveRadarrBlock::Collections.into()
);
assert!(radarr_data.main_tabs.tabs[1].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[1].contextual_help,
Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES))
Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES))
);
assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Collections");
assert_str_eq!(radarr_data.main_tabs.tabs[2].title, "Downloads");
assert_eq!(
radarr_data.main_tabs.tabs[2].route,
ActiveRadarrBlock::Collections.into()
ActiveRadarrBlock::Downloads.into()
);
assert!(radarr_data.main_tabs.tabs[2].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[2].contextual_help,
Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES))
Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES))
);
assert_str_eq!(radarr_data.main_tabs.tabs[3].title, "Root Folders");
assert_str_eq!(radarr_data.main_tabs.tabs[3].title, "Blocklist");
assert_eq!(
radarr_data.main_tabs.tabs[3].route,
ActiveRadarrBlock::RootFolders.into()
ActiveRadarrBlock::Blocklist.into()
);
assert!(radarr_data.main_tabs.tabs[3].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[3].contextual_help,
Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES))
Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES))
);
assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Indexers");
assert_str_eq!(radarr_data.main_tabs.tabs[4].title, "Root Folders");
assert_eq!(
radarr_data.main_tabs.tabs[4].route,
ActiveRadarrBlock::Indexers.into()
ActiveRadarrBlock::RootFolders.into()
);
assert!(radarr_data.main_tabs.tabs[4].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[4].contextual_help,
Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES))
Some(build_context_clue_string(&ROOT_FOLDERS_CONTEXT_CLUES))
);
assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "System");
assert_str_eq!(radarr_data.main_tabs.tabs[5].title, "Indexers");
assert_eq!(
radarr_data.main_tabs.tabs[5].route,
ActiveRadarrBlock::System.into()
ActiveRadarrBlock::Indexers.into()
);
assert!(radarr_data.main_tabs.tabs[5].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[5].contextual_help,
Some(build_context_clue_string(&INDEXERS_CONTEXT_CLUES))
);
assert_str_eq!(radarr_data.main_tabs.tabs[6].title, "System");
assert_eq!(
radarr_data.main_tabs.tabs[6].route,
ActiveRadarrBlock::System.into()
);
assert!(radarr_data.main_tabs.tabs[6].help.is_empty());
assert_eq!(
radarr_data.main_tabs.tabs[6].contextual_help,
Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES))
);
@@ -246,10 +258,10 @@ mod tests {
use pretty_assertions::assert_eq;
use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, COLLECTIONS_BLOCKS,
COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS,
DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS,
EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, BLOCKLIST_BLOCKS,
COLLECTIONS_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS,
DELETE_MOVIE_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS,
EDIT_COLLECTION_SELECTION_BLOCKS, EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_BLOCKS,
EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS,
INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS,
MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
@@ -296,6 +308,16 @@ mod tests {
assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteRootFolderPrompt));
}
#[test]
fn test_blocklist_blocks_contents() {
assert_eq!(BLOCKLIST_BLOCKS.len(), 5);
assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::Blocklist));
assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistItemDetails));
assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::DeleteBlocklistItemPrompt));
assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistClearAllItemsPrompt));
assert!(BLOCKLIST_BLOCKS.contains(&ActiveRadarrBlock::BlocklistSortPrompt));
}
#[test]
fn test_add_movie_blocks_contents() {
assert_eq!(ADD_MOVIE_BLOCKS.len(), 10);
+5 -1
View File
@@ -143,7 +143,11 @@ impl<'a, 'b> Network<'a, 'b> {
.put(uri)
.json(&body.unwrap_or_default())
.header("X-Api-Key", api_token),
RequestMethod::Delete => self.client.delete(uri).header("X-Api-Key", api_token),
RequestMethod::Delete => self
.client
.delete(uri)
.json(&body.unwrap_or_default())
.header("X-Api-Key", api_token),
}
}
}
+98 -5
View File
@@ -9,11 +9,11 @@ use urlencoding::encode;
use crate::app::RadarrConfig;
use crate::models::radarr_models::{
AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie,
CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer,
IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, MovieHistoryItem,
QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task,
Update,
AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, BlocklistResponse, Collection,
CollectionMovie, CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse,
Indexer, IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody,
MovieHistoryItem, QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder,
SystemStatus, Tag, Task, Update,
};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
@@ -33,6 +33,8 @@ mod radarr_network_tests;
pub enum RadarrEvent {
AddMovie,
AddRootFolder,
ClearBlocklist,
DeleteBlocklistItem,
DeleteDownload,
DeleteIndexer,
DeleteMovie,
@@ -42,6 +44,7 @@ pub enum RadarrEvent {
EditCollection,
EditIndexer,
EditMovie,
GetBlocklist,
GetCollections,
GetDownloads,
GetIndexers,
@@ -75,6 +78,9 @@ pub enum RadarrEvent {
impl RadarrEvent {
const fn resource(self) -> &'static str {
match self {
RadarrEvent::ClearBlocklist => "/blocklist/bulk",
RadarrEvent::DeleteBlocklistItem => "/blocklist",
RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection",
RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue",
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => {
@@ -125,6 +131,8 @@ impl<'a, 'b> Network<'a, 'b> {
match radarr_event {
RadarrEvent::AddMovie => self.add_movie().await,
RadarrEvent::AddRootFolder => self.add_root_folder().await,
RadarrEvent::ClearBlocklist => self.clear_blocklist().await,
RadarrEvent::DeleteBlocklistItem => self.delete_blocklist_item().await,
RadarrEvent::DeleteDownload => self.delete_download().await,
RadarrEvent::DeleteIndexer => self.delete_indexer().await,
RadarrEvent::DeleteMovie => self.delete_movie().await,
@@ -134,6 +142,7 @@ impl<'a, 'b> Network<'a, 'b> {
RadarrEvent::EditCollection => self.edit_collection().await,
RadarrEvent::EditIndexer => self.edit_indexer().await,
RadarrEvent::EditMovie => self.edit_movie().await,
RadarrEvent::GetBlocklist => self.get_blocklist().await,
RadarrEvent::GetCollections => self.get_collections().await,
RadarrEvent::GetDownloads => self.get_downloads().await,
RadarrEvent::GetIndexers => self.get_indexers().await,
@@ -319,6 +328,64 @@ impl<'a, 'b> Network<'a, 'b> {
.await;
}
async fn clear_blocklist(&mut self) {
info!("Clearing Radarr blocklist");
let ids = self
.app
.lock()
.await
.data
.radarr_data
.blocklist
.items
.iter()
.map(|item| item.id)
.collect::<Vec<i64>>();
let request_props = self
.radarr_request_props_from(
RadarrEvent::ClearBlocklist.resource(),
RequestMethod::Delete,
Some(json!({"ids": ids})),
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await;
}
async fn delete_blocklist_item(&mut self) {
let blocklist_item_id = self
.app
.lock()
.await
.data
.radarr_data
.blocklist
.current_selection()
.id;
info!("Deleting Radarr blocklist item for item with id: {blocklist_item_id}");
let request_props = self
.radarr_request_props_from(
format!(
"{}/{blocklist_item_id}",
RadarrEvent::DeleteBlocklistItem.resource()
)
.as_str(),
RequestMethod::Delete,
None::<()>,
)
.await;
self
.handle_request::<(), ()>(request_props, |_, _| ())
.await;
}
async fn delete_download(&mut self) {
let download_id = self
.app
@@ -794,6 +861,32 @@ impl<'a, 'b> Network<'a, 'b> {
.await;
}
async fn get_blocklist(&mut self) {
info!("Fetching blocklist");
let request_props = self
.radarr_request_props_from(
RadarrEvent::GetBlocklist.resource(),
RequestMethod::Get,
None::<()>,
)
.await;
self
.handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| {
if !matches!(
app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::BlocklistSortPrompt, _)
) {
let mut blocklist_vec = blocklist_resp.records;
blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.radarr_data.blocklist.set_items(blocklist_vec);
app.data.radarr_data.blocklist.apply_sorting_toggle(false);
}
})
.await;
}
async fn get_collections(&mut self) {
info!("Fetching Radarr collections");
+348 -2
View File
@@ -13,8 +13,8 @@ mod test {
use tokio_util::sync::CancellationToken;
use crate::models::radarr_models::{
CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile,
Quality, QualityWrapper, Rating, RatingsList,
BlocklistItem, CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability,
Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::SortOption;
@@ -186,6 +186,9 @@ mod test {
}
#[rstest]
#[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")]
#[case(RadarrEvent::DeleteBlocklistItem, "/blocklist")]
#[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
#[case(RadarrEvent::GetLogs, "/log")]
#[case(RadarrEvent::SearchNewMovie, "/movie/lookup")]
#[case(RadarrEvent::GetMovieCredits, "/credit")]
@@ -1302,6 +1305,271 @@ mod test {
);
}
#[rstest]
#[tokio::test]
async fn test_handle_get_blocklist_event(#[values(true, false)] use_custom_sorting: bool) {
let blocklist_json = json!({"records": [{
"id": 123,
"movieId": 1007,
"sourceTitle": "z movie",
"languages": [{"name": "English"}],
"quality": {"quality": {"name": "HD - 1080p"}},
"customFormats": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"protocol": "usenet",
"indexer": "DrunkenSlug (Prowlarr)",
"message": "test message",
"movie": {
"id": 1007,
"title": "z movie",
"tmdbId": 1234,
"originalLanguage": {"name": "English"},
"sizeOnDisk": 3543348019i64,
"status": "Downloaded",
"overview": "Blah blah blah",
"path": "/nfs/movies",
"studio": "21st Century Alex",
"genres": ["cool", "family", "fun"],
"year": 2023,
"monitored": true,
"hasFile": true,
"runtime": 120,
"qualityProfileId": 2222,
"minimumAvailability": "announced",
"certification": "R",
"tags": [1],
"ratings": {
"imdb": {"value": 9.9},
"tmdb": {"value": 9.9},
"rottenTomatoes": {"value": 9.9}
},
},
}, {
"id": 456,
"movieId": 2001,
"sourceTitle": "A Movie",
"languages": [{"name": "English"}],
"quality": {"quality": {"name": "HD - 1080p"}},
"customFormats": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"protocol": "usenet",
"indexer": "DrunkenSlug (Prowlarr)",
"message": "test message",
"movie": {
"id": 2001,
"title": "A Movie",
"tmdbId": 1234,
"originalLanguage": {"name": "English"},
"sizeOnDisk": 3543348019i64,
"status": "Downloaded",
"overview": "Blah blah blah",
"path": "/nfs/movies",
"studio": "21st Century Alex",
"genres": ["cool", "family", "fun"],
"year": 2023,
"monitored": true,
"hasFile": true,
"runtime": 120,
"qualityProfileId": 2222,
"minimumAvailability": "announced",
"certification": "R",
"tags": [1],
"ratings": {
"imdb": {"value": 9.9},
"tmdb": {"value": 9.9},
"rottenTomatoes": {"value": 9.9}
},
},
}]});
let mut expected_blocklist = vec![
BlocklistItem {
id: 123,
movie_id: 1007,
source_title: "z movie".into(),
movie: Movie {
id: 1007,
title: "z movie".into(),
movie_file: None,
collection: None,
..movie()
},
..blocklist_item()
},
BlocklistItem {
id: 456,
movie_id: 2001,
source_title: "A Movie".into(),
movie: Movie {
id: 2001,
title: "A Movie".into(),
movie_file: None,
collection: None,
..movie()
},
..blocklist_item()
},
];
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(blocklist_json),
None,
RadarrEvent::GetBlocklist.resource(),
)
.await;
app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true;
if use_custom_sorting {
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
a.source_title
.to_lowercase()
.cmp(&b.source_title.to_lowercase())
};
expected_blocklist.sort_by(cmp_fn);
let blocklist_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app_arc
.lock()
.await
.data
.radarr_data
.blocklist
.sorting(vec![blocklist_sort_option]);
}
let mut network = Network::new(&app_arc, CancellationToken::new());
network.handle_radarr_event(RadarrEvent::GetBlocklist).await;
async_server.assert_async().await;
assert_eq!(
app_arc.lock().await.data.radarr_data.blocklist.items,
expected_blocklist
);
assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc);
}
#[tokio::test]
async fn test_handle_get_blocklist_event_no_op_when_user_is_selecting_sort_options() {
let blocklist_json = json!({"records": [{
"id": 123,
"movieId": 1007,
"sourceTitle": "z movie",
"languages": [{"name": "English"}],
"quality": {"quality": {"name": "HD - 1080p"}},
"customFormats": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"protocol": "usenet",
"indexer": "DrunkenSlug (Prowlarr)",
"message": "test message",
"movie": {
"id": 1007,
"title": "z movie",
"tmdbId": 1234,
"originalLanguage": {"name": "English"},
"sizeOnDisk": 3543348019i64,
"status": "Downloaded",
"overview": "Blah blah blah",
"path": "/nfs/movies",
"studio": "21st Century Alex",
"genres": ["cool", "family", "fun"],
"year": 2023,
"monitored": true,
"hasFile": true,
"runtime": 120,
"qualityProfileId": 2222,
"minimumAvailability": "announced",
"certification": "R",
"tags": [1],
"ratings": {
"imdb": {"value": 9.9},
"tmdb": {"value": 9.9},
"rottenTomatoes": {"value": 9.9}
},
},
}, {
"id": 456,
"movieId": 2001,
"sourceTitle": "A Movie",
"languages": [{"name": "English"}],
"quality": {"quality": {"name": "HD - 1080p"}},
"customFormats": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"protocol": "usenet",
"indexer": "DrunkenSlug (Prowlarr)",
"message": "test message",
"movie": {
"id": 2001,
"title": "A Movie",
"tmdbId": 1234,
"originalLanguage": {"name": "English"},
"sizeOnDisk": 3543348019i64,
"status": "Downloaded",
"overview": "Blah blah blah",
"path": "/nfs/movies",
"studio": "21st Century Alex",
"genres": ["cool", "family", "fun"],
"year": 2023,
"monitored": true,
"hasFile": true,
"runtime": 120,
"qualityProfileId": 2222,
"minimumAvailability": "announced",
"certification": "R",
"tags": [1],
"ratings": {
"imdb": {"value": 9.9},
"tmdb": {"value": 9.9},
"rottenTomatoes": {"value": 9.9}
},
},
}]});
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Get,
None,
Some(blocklist_json),
None,
RadarrEvent::GetBlocklist.resource(),
)
.await;
app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true;
app_arc
.lock()
.await
.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
a.source_title
.to_lowercase()
.cmp(&b.source_title.to_lowercase())
};
let blocklist_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app_arc
.lock()
.await
.data
.radarr_data
.blocklist
.sorting(vec![blocklist_sort_option]);
let mut network = Network::new(&app_arc, CancellationToken::new());
network.handle_radarr_event(RadarrEvent::GetBlocklist).await;
async_server.assert_async().await;
assert!(app_arc
.lock()
.await
.data
.radarr_data
.blocklist
.items
.is_empty());
assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc);
}
#[rstest]
#[tokio::test]
async fn test_handle_get_collections_event(#[values(true, false)] use_custom_sorting: bool) {
@@ -2154,6 +2422,68 @@ mod test {
assert!(!app_arc.lock().await.data.radarr_data.add_list_exclusion);
}
#[tokio::test]
async fn test_handle_clear_blocklist_event() {
let blocklist_items = vec![
BlocklistItem {
id: 1,
..blocklist_item()
},
BlocklistItem {
id: 2,
..blocklist_item()
},
BlocklistItem {
id: 3,
..blocklist_item()
},
];
let expected_request_json = json!({ "ids": [1, 2, 3]});
let (async_server, app_arc, _server) = mock_radarr_api(
RequestMethod::Delete,
Some(expected_request_json),
None,
None,
RadarrEvent::ClearBlocklist.resource(),
)
.await;
app_arc
.lock()
.await
.data
.radarr_data
.blocklist
.set_items(blocklist_items);
let mut network = Network::new(&app_arc, CancellationToken::new());
network
.handle_radarr_event(RadarrEvent::ClearBlocklist)
.await;
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_delete_blocklist_item_event() {
let resource = format!("{}/1", RadarrEvent::DeleteBlocklistItem.resource());
let (async_server, app_arc, _server) =
mock_radarr_api(RequestMethod::Delete, None, None, None, &resource).await;
app_arc
.lock()
.await
.data
.radarr_data
.blocklist
.set_items(vec![blocklist_item()]);
let mut network = Network::new(&app_arc, CancellationToken::new());
network
.handle_radarr_event(RadarrEvent::DeleteBlocklistItem)
.await;
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_delete_download_event() {
let resource = format!("{}/1", RadarrEvent::DeleteDownload.resource());
@@ -3311,6 +3641,22 @@ mod test {
}
}
fn blocklist_item() -> BlocklistItem {
BlocklistItem {
id: 1,
movie_id: 1,
source_title: "z movie".to_owned(),
languages: vec![language()],
quality: quality_wrapper(),
custom_formats: Some(vec![language()]),
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
protocol: "usenet".to_owned(),
indexer: "DrunkenSlug (Prowlarr)".to_owned(),
message: "test message".to_owned(),
movie: movie(),
}
}
fn collection() -> Collection {
Collection {
id: 123,
@@ -0,0 +1,18 @@
#[cfg(test)]
mod tests {
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::ui::radarr_ui::blocklist::BlocklistUi;
use crate::ui::DrawUi;
use strum::IntoEnumIterator;
#[test]
fn test_blocklist_ui_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
if BLOCKLIST_BLOCKS.contains(&active_radarr_block) {
assert!(BlocklistUi::accepts(active_radarr_block.into()));
} else {
assert!(!BlocklistUi::accepts(active_radarr_block.into()));
}
});
}
}
+201
View File
@@ -0,0 +1,201 @@
use crate::app::App;
use crate::models::radarr_models::BlocklistItem;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, BLOCKLIST_BLOCKS};
use crate::models::Route;
use crate::ui::styles::ManagarrStyle;
use crate::ui::utils::{get_width_from_percentage, layout_block_top_border};
use crate::ui::widgets::confirmation_prompt::ConfirmationPrompt;
use crate::ui::widgets::managarr_table::ManagarrTable;
use crate::ui::widgets::message::Message;
use crate::ui::widgets::popup::{Popup, Size};
use crate::ui::DrawUi;
use ratatui::layout::{Alignment, Constraint, Rect};
use ratatui::style::{Style, Stylize};
use ratatui::text::{Line, Text};
use ratatui::widgets::{Cell, Row};
use ratatui::Frame;
#[cfg(test)]
#[path = "blocklist_ui_tests.rs"]
mod blocklist_ui_tests;
pub(super) struct BlocklistUi;
impl DrawUi for BlocklistUi {
fn accepts(route: Route) -> bool {
if let Route::Radarr(active_radarr_block, _) = route {
return BLOCKLIST_BLOCKS.contains(&active_radarr_block);
}
false
}
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
match active_radarr_block {
ActiveRadarrBlock::Blocklist | ActiveRadarrBlock::BlocklistSortPrompt => {
draw_blocklist_table(f, app, area)
}
ActiveRadarrBlock::BlocklistItemDetails => {
draw_blocklist_table(f, app, area);
draw_blocklist_item_details_popup(f, app);
}
ActiveRadarrBlock::DeleteBlocklistItemPrompt => {
let prompt = format!(
"Do you want to remove this item from your blocklist: \n{}?",
app
.data
.radarr_data
.blocklist
.current_selection()
.source_title
);
let confirmation_prompt = ConfirmationPrompt::new()
.title("Remove Item from Blocklist")
.prompt(&prompt)
.yes_no_value(app.data.radarr_data.prompt_confirm);
draw_blocklist_table(f, app, area);
f.render_widget(Popup::new(confirmation_prompt).size(Size::Prompt), f.size());
}
ActiveRadarrBlock::BlocklistClearAllItemsPrompt => {
let confirmation_prompt = ConfirmationPrompt::new()
.title("Clear Blocklist")
.prompt("Do you want to clear your blocklist?")
.yes_no_value(app.data.radarr_data.prompt_confirm);
draw_blocklist_table(f, app, area);
f.render_widget(
Popup::new(confirmation_prompt).size(Size::SmallPrompt),
f.size(),
);
}
_ => (),
}
}
}
}
fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
if let Route::Radarr(active_radarr_block, _) = *app.get_current_route() {
let current_selection = if app.data.radarr_data.blocklist.items.is_empty() {
BlocklistItem::default()
} else {
app.data.radarr_data.blocklist.current_selection().clone()
};
let blocklist_table_footer = app
.data
.radarr_data
.main_tabs
.get_active_tab_contextual_help();
let blocklist_row_mapping = |blocklist_item: &BlocklistItem| {
let BlocklistItem {
source_title,
languages,
quality,
custom_formats,
date,
movie,
..
} = blocklist_item;
movie.title.scroll_left_or_reset(
get_width_from_percentage(area, 20),
current_selection == *blocklist_item,
app.tick_count % app.ticks_until_scroll == 0,
);
let languages_string = languages
.iter()
.map(|lang| lang.name.to_owned())
.collect::<Vec<String>>()
.join(", ");
let custom_formats_string = if let Some(formats) = custom_formats.as_ref() {
formats
.iter()
.map(|cf| cf.name.to_owned())
.collect::<Vec<String>>()
.join(", ")
} else {
"".to_owned()
};
Row::new(vec![
Cell::from(movie.title.to_string()),
Cell::from(source_title.to_owned()),
Cell::from(languages_string),
Cell::from(quality.quality.name.to_owned()),
Cell::from(custom_formats_string),
Cell::from(date.to_string()),
])
.primary()
};
let blocklist_table = ManagarrTable::new(
Some(&mut app.data.radarr_data.blocklist),
blocklist_row_mapping,
)
.block(layout_block_top_border())
.loading(app.is_loading)
.footer(blocklist_table_footer)
.sorting(active_radarr_block == ActiveRadarrBlock::BlocklistSortPrompt)
.headers([
"Movie Title",
"Source Title",
"Languages",
"Quality",
"Formats",
"Date",
])
.constraints([
Constraint::Percentage(20),
Constraint::Percentage(35),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(10),
Constraint::Percentage(15),
]);
f.render_widget(blocklist_table, area);
}
}
fn draw_blocklist_item_details_popup(f: &mut Frame<'_>, app: &mut App<'_>) {
let current_selection = if app.data.radarr_data.blocklist.items.is_empty() {
BlocklistItem::default()
} else {
app.data.radarr_data.blocklist.current_selection().clone()
};
let BlocklistItem {
source_title,
protocol,
indexer,
message,
..
} = current_selection;
let text = Text::from(vec![
Line::from(vec![
"Name: ".bold().secondary(),
source_title.to_owned().secondary(),
]),
Line::from(vec![
"Protocol: ".bold().secondary(),
protocol.to_owned().secondary(),
]),
Line::from(vec![
"Indexer: ".bold().secondary(),
indexer.to_owned().secondary(),
]),
Line::from(vec![
"Message: ".bold().secondary(),
message.to_owned().secondary(),
]),
]);
let message = Message::new(text)
.title("Details")
.style(Style::new().secondary())
.alignment(Alignment::Left);
f.render_widget(Popup::new(message).size(Size::NarrowMessage), f.size());
}
+3
View File
@@ -13,6 +13,7 @@ use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie, RootFolder}
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::Route;
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::indexers::IndexersUi;
@@ -27,6 +28,7 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::DrawUi;
use crate::utils::convert_to_gb;
mod blocklist;
mod collections;
mod downloads;
mod indexers;
@@ -57,6 +59,7 @@ impl DrawUi for RadarrUi {
_ 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),
_ => (),
}
}
+8 -1
View File
@@ -14,6 +14,7 @@ pub struct Message<'a> {
text: Text<'a>,
title: &'a str,
style: Style,
alignment: Alignment,
}
impl<'a> Message<'a> {
@@ -25,6 +26,7 @@ impl<'a> Message<'a> {
text: message.into(),
title: "Error",
style: Style::new().failure().bold(),
alignment: Alignment::Center,
}
}
@@ -38,10 +40,15 @@ impl<'a> Message<'a> {
self
}
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
fn render_message(self, area: Rect, buf: &mut Buffer) {
Paragraph::new(self.text)
.style(self.style)
.alignment(Alignment::Center)
.alignment(self.alignment)
.block(title_block_centered(self.title).style(self.style))
.wrap(Wrap { trim: true })
.render(area, buf);
+16
View File
@@ -3,6 +3,7 @@ mod tests {
use crate::ui::styles::ManagarrStyle;
use crate::ui::widgets::message::Message;
use pretty_assertions::{assert_eq, assert_str_eq};
use ratatui::layout::Alignment;
use ratatui::style::{Style, Stylize};
use ratatui::text::Text;
@@ -15,6 +16,7 @@ mod tests {
assert_eq!(message.text, Text::from(test_message));
assert_str_eq!(message.title, "Error");
assert_eq!(message.style, Style::new().failure().bold());
assert_eq!(message.alignment, Alignment::Center);
}
#[test]
@@ -27,6 +29,7 @@ mod tests {
assert_str_eq!(message.title, title);
assert_eq!(message.text, Text::from(test_message));
assert_eq!(message.style, Style::new().failure().bold());
assert_eq!(message.alignment, Alignment::Center);
}
#[test]
@@ -39,5 +42,18 @@ mod tests {
assert_eq!(message.style, style);
assert_eq!(message.text, Text::from(test_message));
assert_str_eq!(message.title, "Error");
assert_eq!(message.alignment, Alignment::Center);
}
#[test]
fn test_message_alignment() {
let test_message = "This is a message";
let message = Message::new(test_message).alignment(Alignment::Left);
assert_eq!(message.alignment, Alignment::Left);
assert_eq!(message.text, Text::from(test_message));
assert_str_eq!(message.title, "Error");
assert_eq!(message.style, Style::new().failure().bold());
}
}
+4
View File
@@ -10,9 +10,11 @@ use ratatui::widgets::{Block, Clear, Paragraph, Widget};
mod popup_tests;
pub enum Size {
SmallPrompt,
Prompt,
LargePrompt,
Message,
NarrowMessage,
LargeMessage,
InputBox,
Dropdown,
@@ -24,9 +26,11 @@ pub enum Size {
impl Size {
pub fn to_percent(&self) -> (u16, u16) {
match self {
Size::SmallPrompt => (20, 20),
Size::Prompt => (35, 35),
Size::LargePrompt => (70, 45),
Size::Message => (25, 8),
Size::NarrowMessage => (50, 20),
Size::LargeMessage => (25, 25),
Size::InputBox => (30, 13),
Size::Dropdown => (20, 30),
+2
View File
@@ -6,9 +6,11 @@ mod tests {
#[test]
fn test_dimensions_to_percent() {
assert_eq!(Size::SmallPrompt.to_percent(), (20, 20));
assert_eq!(Size::Prompt.to_percent(), (35, 35));
assert_eq!(Size::LargePrompt.to_percent(), (70, 45));
assert_eq!(Size::Message.to_percent(), (25, 8));
assert_eq!(Size::NarrowMessage.to_percent(), (50, 20));
assert_eq!(Size::LargeMessage.to_percent(), (25, 25));
assert_eq!(Size::InputBox.to_percent(), (30, 13));
assert_eq!(Size::Dropdown.to_percent(), (20, 30));