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]] [[package]]
name = "managarr" name = "managarr"
version = "0.0.33" version = "0.0.34"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"backtrace", "backtrace",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "managarr" name = "managarr"
version = "0.0.33" version = "0.0.34"
authors = ["Alex Clarke <alex.j.tusa@gmail.com>"] authors = ["Alex Clarke <alex.j.tusa@gmail.com>"]
description = "A TUI to manage your Servarrs" description = "A TUI to manage your Servarrs"
keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"] keywords = ["managarr", "tui-rs", "dashboard", "servarr", "tui"]
+4 -1
View File
@@ -36,7 +36,7 @@ pleasant as possible!
### Radarr ### 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 a specific movie including description, history, downloaded file info, or the credits
- [x] View details of any collection and the movies in them - [x] View details of any collection and the movies in them
- [x] Search your library or collections - [x] Search your library or collections
@@ -48,6 +48,7 @@ pleasant as possible!
- [x] Edit your movies, collections, and indexers - [x] Edit your movies, collections, and indexers
- [x] Manage your tags - [x] Manage your tags
- [x] Manage your root folders - [x] Manage your root folders
- [x] Manage your blocklist
- [ ] Manage your quality profiles - [ ] Manage your quality profiles
- [ ] Manage your quality definitions - [ ] Manage your quality definitions
- [x] View and browse logs, tasks, events queues, and updates - [x] View and browse logs, tasks, events queues, and updates
@@ -144,6 +145,8 @@ tautulli:
![logs](screenshots/logs.png) ![logs](screenshots/logs.png)
![new_movie_search](screenshots/new_movie_search.png) ![new_movie_search](screenshots/new_movie_search.png)
![add_new_movie](screenshots/add_new_movie.png) ![add_new_movie](screenshots/add_new_movie.png)
![collection_details](screenshots/collection_details.png)
![indexers](screenshots/indexers.png)
## Dependencies ## Dependencies
* [ratatui](https://github.com/tui-rs-revival/ratatui) * [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, left,
right, right,
backspace, backspace,
clear,
search, search,
settings, settings,
filter, filter,
@@ -67,6 +68,10 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
key: Key::Backspace, key: Key::Backspace,
desc: "backspace", desc: "backspace",
}, },
clear: KeyBinding {
key: Key::Char('c'),
desc: "clear",
},
search: KeyBinding { search: KeyBinding {
key: Key::Char('s'), key: Key::Char('s'),
desc: "search", desc: "search",
+1
View File
@@ -13,6 +13,7 @@ mod test {
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")] #[case(DEFAULT_KEYBINDINGS.left, Key::Left, "left")]
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")] #[case(DEFAULT_KEYBINDINGS.right, Key::Right, "right")]
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, "backspace")] #[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.search, Key::Char('s'), "search")]
#[case(DEFAULT_KEYBINDINGS.settings, Key::Char('s'), "settings")] #[case(DEFAULT_KEYBINDINGS.settings, Key::Char('s'), "settings")]
#[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")] #[case(DEFAULT_KEYBINDINGS.filter, Key::Char('f'), "filter")]
+5
View File
@@ -11,6 +11,11 @@ mod radarr_tests;
impl<'a> App<'a> { impl<'a> App<'a> {
pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) { pub(super) async fn dispatch_by_radarr_block(&mut self, active_radarr_block: &ActiveRadarrBlock) {
match active_radarr_block { match active_radarr_block {
ActiveRadarrBlock::Blocklist => {
self
.dispatch_network_event(RadarrEvent::GetBlocklist.into())
.await;
}
ActiveRadarrBlock::Collections => { ActiveRadarrBlock::Collections => {
self self
.dispatch_network_event(RadarrEvent::GetCollections.into()) .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"), (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] = [ pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [
(DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc), (DEFAULT_KEYBINDINGS.search, DEFAULT_KEYBINDINGS.search.desc),
(DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc), (DEFAULT_KEYBINDINGS.edit, DEFAULT_KEYBINDINGS.edit.desc),
@@ -43,6 +35,25 @@ pub static COLLECTIONS_CONTEXT_CLUES: [ContextClue; 8] = [
(DEFAULT_KEYBINDINGS.esc, "cancel filter"), (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] = [ pub static ROOT_FOLDERS_CONTEXT_CLUES: [ContextClue; 3] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc), (DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.delete, DEFAULT_KEYBINDINGS.delete.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] = [ pub static INDEXERS_CONTEXT_CLUES: [ContextClue; 6] = [
(DEFAULT_KEYBINDINGS.add, DEFAULT_KEYBINDINGS.add.desc),
(DEFAULT_KEYBINDINGS.submit, "edit indexer"), (DEFAULT_KEYBINDINGS.submit, "edit indexer"),
( (
DEFAULT_KEYBINDINGS.settings, DEFAULT_KEYBINDINGS.settings,
+48 -22
View File
@@ -4,7 +4,7 @@ mod tests {
use crate::app::key_binding::DEFAULT_KEYBINDINGS; use crate::app::key_binding::DEFAULT_KEYBINDINGS;
use crate::app::radarr::radarr_context_clues::{ 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, COLLECTION_DETAILS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES, LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_CONTEXT_CLUES,
MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_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); 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] #[test]
fn test_collections_context_clues() { fn test_collections_context_clues() {
let mut collections_context_clues = COLLECTIONS_CONTEXT_CLUES.iter(); let mut collections_context_clues = COLLECTIONS_CONTEXT_CLUES.iter();
@@ -129,6 +113,53 @@ mod tests {
assert_eq!(collections_context_clues.next(), None); 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] #[test]
fn test_root_folders_context_clues() { fn test_root_folders_context_clues() {
let mut root_folders_context_clues_iter = ROOT_FOLDERS_CONTEXT_CLUES.iter(); 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(); 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_eq!(*key_binding, DEFAULT_KEYBINDINGS.submit);
assert_str_eq!(*description, "edit indexer"); assert_str_eq!(*description, "edit indexer");
+17
View File
@@ -12,6 +12,23 @@ mod tests {
use crate::network::radarr_network::RadarrEvent; use crate::network::radarr_network::RadarrEvent;
use crate::network::NetworkEvent; 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] #[tokio::test]
async fn test_dispatch_by_collections_block() { async fn test_dispatch_by_collections_block() {
let (mut app, mut sync_network_rx) = construct_app_unit(); 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] #[test]
fn test_collections_tab_left() { fn test_collections_tab_left() {
let mut app = App::default(); 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( CollectionsHandler::with(
&DEFAULT_KEYBINDINGS.left.key, &DEFAULT_KEYBINDINGS.left.key,
@@ -279,18 +279,15 @@ mod tests {
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Downloads.into() &ActiveRadarrBlock::Movies.into()
);
assert_eq!(
app.get_current_route(),
&ActiveRadarrBlock::Downloads.into()
); );
assert_eq!(app.get_current_route(), &ActiveRadarrBlock::Movies.into());
} }
#[test] #[test]
fn test_collections_tab_right() { fn test_collections_tab_right() {
let mut app = App::default(); 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( CollectionsHandler::with(
&DEFAULT_KEYBINDINGS.right.key, &DEFAULT_KEYBINDINGS.right.key,
@@ -302,11 +299,11 @@ mod tests {
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::RootFolders.into() &ActiveRadarrBlock::Downloads.into()
); );
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::RootFolders.into() &ActiveRadarrBlock::Downloads.into()
); );
} }
@@ -832,6 +829,26 @@ mod tests {
assert!(!app.data.radarr_data.prompt_confirm); 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] #[test]
fn test_default_esc() { fn test_default_esc() {
let mut app = App::default(); 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.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false; 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_search();
self.app.data.radarr_data.collections.reset_filter(); self.app.data.radarr_data.collections.reset_filter();
@@ -74,7 +74,7 @@ mod tests {
#[test] #[test]
fn test_downloads_tab_left() { fn test_downloads_tab_left() {
let mut app = App::default(); 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( DownloadsHandler::with(
&DEFAULT_KEYBINDINGS.left.key, &DEFAULT_KEYBINDINGS.left.key,
@@ -84,26 +84,6 @@ mod tests {
) )
.handle(); .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!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Collections.into() &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] #[rstest]
fn test_downloads_left_right_prompt_toggle( fn test_downloads_left_right_prompt_toggle(
#[values( #[values(
@@ -78,7 +78,7 @@ mod tests {
#[test] #[test]
fn test_indexers_tab_left() { fn test_indexers_tab_left() {
let mut app = App::default(); 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( IndexersHandler::with(
&DEFAULT_KEYBINDINGS.left.key, &DEFAULT_KEYBINDINGS.left.key,
@@ -101,7 +101,7 @@ mod tests {
#[test] #[test]
fn test_indexers_tab_right() { fn test_indexers_tab_right() {
let mut app = App::default(); 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( IndexersHandler::with(
&DEFAULT_KEYBINDINGS.right.key, &DEFAULT_KEYBINDINGS.right.key,
@@ -312,11 +312,11 @@ mod tests {
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Downloads.into() &ActiveRadarrBlock::Collections.into()
); );
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::Downloads.into() &ActiveRadarrBlock::Collections.into()
); );
} }
@@ -776,6 +776,23 @@ mod tests {
assert!(!app.data.radarr_data.prompt_confirm); 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] #[test]
fn test_default_esc() { fn test_default_esc() {
let mut app = App::default(); 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.pop_navigation_stack();
self.app.data.radarr_data.prompt_confirm = false; 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_search();
self.app.data.radarr_data.movies.reset_filter(); self.app.data.radarr_data.movies.reset_filter();
+5
View File
@@ -1,4 +1,5 @@
use crate::app::key_binding::DEFAULT_KEYBINDINGS; 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::collections::CollectionsHandler;
use crate::handlers::radarr_handlers::downloads::DownloadsHandler; use crate::handlers::radarr_handlers::downloads::DownloadsHandler;
use crate::handlers::radarr_handlers::indexers::IndexersHandler; 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::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::{App, Key}; use crate::{App, Key};
mod blocklist;
mod collections; mod collections;
mod downloads; mod downloads;
mod indexers; 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) RootFoldersHandler::with(self.key, self.app, self.active_radarr_block, self.context)
.handle() .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(), _ => self.handle_key_event(),
} }
} }
@@ -12,12 +12,13 @@ mod tests {
use crate::test_handler_delegation; use crate::test_handler_delegation;
#[rstest] #[rstest]
#[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Downloads)] #[case(0, ActiveRadarrBlock::System, ActiveRadarrBlock::Collections)]
#[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Collections)] #[case(1, ActiveRadarrBlock::Movies, ActiveRadarrBlock::Downloads)]
#[case(2, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)] #[case(2, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Blocklist)]
#[case(3, ActiveRadarrBlock::Collections, ActiveRadarrBlock::Indexers)] #[case(3, ActiveRadarrBlock::Downloads, ActiveRadarrBlock::RootFolders)]
#[case(4, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)] #[case(4, ActiveRadarrBlock::Blocklist, ActiveRadarrBlock::Indexers)]
#[case(5, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)] #[case(5, ActiveRadarrBlock::RootFolders, ActiveRadarrBlock::System)]
#[case(6, ActiveRadarrBlock::Indexers, ActiveRadarrBlock::Movies)]
fn test_radarr_handler_change_tab_left_right_keys( fn test_radarr_handler_change_tab_left_right_keys(
#[case] index: usize, #[case] index: usize,
#[case] left_block: ActiveRadarrBlock, #[case] left_block: ActiveRadarrBlock,
@@ -68,6 +69,7 @@ mod tests {
fn test_delegates_library_blocks_to_library_handler( fn test_delegates_library_blocks_to_library_handler(
#[values( #[values(
ActiveRadarrBlock::Movies, ActiveRadarrBlock::Movies,
ActiveRadarrBlock::MoviesSortPrompt,
ActiveRadarrBlock::SearchMovie, ActiveRadarrBlock::SearchMovie,
ActiveRadarrBlock::SearchMovieError, ActiveRadarrBlock::SearchMovieError,
ActiveRadarrBlock::FilterMovies, ActiveRadarrBlock::FilterMovies,
@@ -112,6 +114,7 @@ mod tests {
#[values( #[values(
ActiveRadarrBlock::Collections, ActiveRadarrBlock::Collections,
ActiveRadarrBlock::SearchCollection, ActiveRadarrBlock::SearchCollection,
ActiveRadarrBlock::CollectionsSortPrompt,
ActiveRadarrBlock::SearchCollectionError, ActiveRadarrBlock::SearchCollectionError,
ActiveRadarrBlock::FilterCollections, ActiveRadarrBlock::FilterCollections,
ActiveRadarrBlock::FilterCollectionsError, 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] #[test]
fn test_radarr_handler_accepts() { fn test_radarr_handler_accepts() {
ActiveRadarrBlock::iter().for_each(|active_radarr_block| { ActiveRadarrBlock::iter().for_each(|active_radarr_block| {
@@ -123,7 +123,7 @@ mod tests {
#[test] #[test]
fn test_root_folders_tab_left() { fn test_root_folders_tab_left() {
let mut app = App::default(); 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( RootFoldersHandler::with(
&DEFAULT_KEYBINDINGS.left.key, &DEFAULT_KEYBINDINGS.left.key,
@@ -135,18 +135,18 @@ mod tests {
assert_eq!( assert_eq!(
app.data.radarr_data.main_tabs.get_active_route(), app.data.radarr_data.main_tabs.get_active_route(),
&ActiveRadarrBlock::Collections.into() &ActiveRadarrBlock::Blocklist.into()
); );
assert_eq!( assert_eq!(
app.get_current_route(), app.get_current_route(),
&ActiveRadarrBlock::Collections.into() &ActiveRadarrBlock::Blocklist.into()
); );
} }
#[test] #[test]
fn test_root_folders_tab_right() { fn test_root_folders_tab_right() {
let mut app = App::default(); 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( RootFoldersHandler::with(
&DEFAULT_KEYBINDINGS.right.key, &DEFAULT_KEYBINDINGS.right.key,
@@ -422,11 +422,6 @@ mod tests {
let mut app = App::default(); let mut app = App::default();
app.push_navigation_stack(ActiveRadarrBlock::System.into()); app.push_navigation_stack(ActiveRadarrBlock::System.into());
app.push_navigation_stack(ActiveRadarrBlock::SystemUpdates.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) SystemDetailsHandler::with(&ESC_KEY, &mut app, &ActiveRadarrBlock::SystemUpdates, &None)
.handle(); .handle();
@@ -22,7 +22,7 @@ mod tests {
#[test] #[test]
fn test_system_tab_left() { fn test_system_tab_left() {
let mut app = App::default(); 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( SystemHandler::with(
&DEFAULT_KEYBINDINGS.left.key, &DEFAULT_KEYBINDINGS.left.key,
@@ -42,7 +42,7 @@ mod tests {
#[test] #[test]
fn test_system_tab_right() { fn test_system_tab_right() {
let mut app = App::default(); 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( SystemHandler::with(
&DEFAULT_KEYBINDINGS.right.key, &DEFAULT_KEYBINDINGS.right.key,
+23
View File
@@ -54,6 +54,29 @@ pub struct AddRootFolderBody {
pub path: String, 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)] #[derive(Deserialize, Derivative, Default, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Collection { pub struct Collection {
+27 -7
View File
@@ -1,13 +1,13 @@
use crate::app::context_clues::build_context_clue_string; use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_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, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
}; };
use crate::models::radarr_models::{ use crate::models::radarr_models::{
AddMovieSearchResult, Collection, CollectionMovie, DiskSpace, DownloadRecord, Indexer, AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DiskSpace, DownloadRecord,
IndexerSettings, Movie, QueueEvent, RootFolder, Task, Indexer, IndexerSettings, Movie, QueueEvent, RootFolder, Task,
}; };
use crate::models::servarr_data::radarr::modals::{ use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
@@ -40,6 +40,7 @@ pub struct RadarrData<'a> {
pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>, pub selected_block: BlockSelectionState<'a, ActiveRadarrBlock>,
pub downloads: StatefulTable<DownloadRecord>, pub downloads: StatefulTable<DownloadRecord>,
pub indexers: StatefulTable<Indexer>, pub indexers: StatefulTable<Indexer>,
pub blocklist: StatefulTable<BlocklistItem>,
pub quality_profile_map: BiMap<i64, String>, pub quality_profile_map: BiMap<i64, String>,
pub tags_map: BiMap<i64, String>, pub tags_map: BiMap<i64, String>,
pub collections: StatefulTable<Collection>, pub collections: StatefulTable<Collection>,
@@ -91,6 +92,7 @@ impl<'a> Default for RadarrData<'a> {
selected_block: BlockSelectionState::default(), selected_block: BlockSelectionState::default(),
downloads: StatefulTable::default(), downloads: StatefulTable::default(),
indexers: StatefulTable::default(), indexers: StatefulTable::default(),
blocklist: StatefulTable::default(),
quality_profile_map: BiMap::default(), quality_profile_map: BiMap::default(),
tags_map: BiMap::default(), tags_map: BiMap::default(),
collections: StatefulTable::default(), collections: StatefulTable::default(),
@@ -122,6 +124,12 @@ impl<'a> Default for RadarrData<'a> {
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)), 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 { TabRoute {
title: "Downloads", title: "Downloads",
route: ActiveRadarrBlock::Downloads.into(), route: ActiveRadarrBlock::Downloads.into(),
@@ -129,10 +137,10 @@ impl<'a> Default for RadarrData<'a> {
contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&DOWNLOADS_CONTEXT_CLUES)),
}, },
TabRoute { TabRoute {
title: "Collections", title: "Blocklist",
route: ActiveRadarrBlock::Collections.into(), route: ActiveRadarrBlock::Blocklist.into(),
help: String::new(), help: String::new(),
contextual_help: Some(build_context_clue_string(&COLLECTIONS_CONTEXT_CLUES)), contextual_help: Some(build_context_clue_string(&BLOCKLIST_CONTEXT_CLUES)),
}, },
TabRoute { TabRoute {
title: "Root Folders", title: "Root Folders",
@@ -213,11 +221,16 @@ pub enum ActiveRadarrBlock {
AddMovieEmptySearchResults, AddMovieEmptySearchResults,
AddRootFolderPrompt, AddRootFolderPrompt,
AutomaticallySearchMoviePrompt, AutomaticallySearchMoviePrompt,
Blocklist,
BlocklistClearAllItemsPrompt,
BlocklistItemDetails,
BlocklistSortPrompt,
Collections, Collections,
CollectionsSortPrompt, CollectionsSortPrompt,
CollectionDetails, CollectionDetails,
Cast, Cast,
Crew, Crew,
DeleteBlocklistItemPrompt,
DeleteDownloadPrompt, DeleteDownloadPrompt,
DeleteIndexerPrompt, DeleteIndexerPrompt,
DeleteMoviePrompt, DeleteMoviePrompt,
@@ -323,6 +336,13 @@ pub static ROOT_FOLDERS_BLOCKS: [ActiveRadarrBlock; 3] = [
ActiveRadarrBlock::AddRootFolderPrompt, ActiveRadarrBlock::AddRootFolderPrompt,
ActiveRadarrBlock::DeleteRootFolderPrompt, 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] = [ pub static ADD_MOVIE_BLOCKS: [ActiveRadarrBlock; 10] = [
ActiveRadarrBlock::AddMovieSearchInput, ActiveRadarrBlock::AddMovieSearchInput,
ActiveRadarrBlock::AddMovieSearchResults, ActiveRadarrBlock::AddMovieSearchResults,
@@ -6,8 +6,8 @@ mod tests {
use crate::app::context_clues::build_context_clue_string; use crate::app::context_clues::build_context_clue_string;
use crate::app::radarr::radarr_context_clues::{ use crate::app::radarr::radarr_context_clues::{
COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES, INDEXERS_CONTEXT_CLUES, BLOCKLIST_CONTEXT_CLUES, COLLECTIONS_CONTEXT_CLUES, DOWNLOADS_CONTEXT_CLUES,
LIBRARY_CONTEXT_CLUES, MANUAL_MOVIE_SEARCH_CONTEXTUAL_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, MANUAL_MOVIE_SEARCH_CONTEXT_CLUES, MOVIE_DETAILS_CONTEXT_CLUES, ROOT_FOLDERS_CONTEXT_CLUES,
SYSTEM_CONTEXT_CLUES, SYSTEM_CONTEXT_CLUES,
}; };
@@ -64,6 +64,7 @@ mod tests {
assert_eq!(radarr_data.selected_block, BlockSelectionState::default()); assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
assert!(radarr_data.downloads.items.is_empty()); assert!(radarr_data.downloads.items.is_empty());
assert!(radarr_data.indexers.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.quality_profile_map.is_empty());
assert!(radarr_data.tags_map.is_empty()); assert!(radarr_data.tags_map.is_empty());
assert!(radarr_data.collections.items.is_empty()); assert!(radarr_data.collections.items.is_empty());
@@ -89,7 +90,7 @@ mod tests {
assert!(!radarr_data.delete_movie_files); assert!(!radarr_data.delete_movie_files);
assert!(!radarr_data.add_list_exclusion); 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_str_eq!(radarr_data.main_tabs.tabs[0].title, "Library");
assert_eq!( assert_eq!(
@@ -102,58 +103,69 @@ mod tests {
Some(build_context_clue_string(&LIBRARY_CONTEXT_CLUES)) 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!( assert_eq!(
radarr_data.main_tabs.tabs[1].route, radarr_data.main_tabs.tabs[1].route,
ActiveRadarrBlock::Downloads.into() ActiveRadarrBlock::Collections.into()
); );
assert!(radarr_data.main_tabs.tabs[1].help.is_empty()); assert!(radarr_data.main_tabs.tabs[1].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[1].contextual_help, 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!( assert_eq!(
radarr_data.main_tabs.tabs[2].route, radarr_data.main_tabs.tabs[2].route,
ActiveRadarrBlock::Collections.into() ActiveRadarrBlock::Downloads.into()
); );
assert!(radarr_data.main_tabs.tabs[2].help.is_empty()); assert!(radarr_data.main_tabs.tabs[2].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[2].contextual_help, 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!( assert_eq!(
radarr_data.main_tabs.tabs[3].route, radarr_data.main_tabs.tabs[3].route,
ActiveRadarrBlock::RootFolders.into() ActiveRadarrBlock::Blocklist.into()
); );
assert!(radarr_data.main_tabs.tabs[3].help.is_empty()); assert!(radarr_data.main_tabs.tabs[3].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[3].contextual_help, 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!( assert_eq!(
radarr_data.main_tabs.tabs[4].route, radarr_data.main_tabs.tabs[4].route,
ActiveRadarrBlock::Indexers.into() ActiveRadarrBlock::RootFolders.into()
); );
assert!(radarr_data.main_tabs.tabs[4].help.is_empty()); assert!(radarr_data.main_tabs.tabs[4].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[4].contextual_help, 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!( assert_eq!(
radarr_data.main_tabs.tabs[5].route, radarr_data.main_tabs.tabs[5].route,
ActiveRadarrBlock::System.into() ActiveRadarrBlock::Indexers.into()
); );
assert!(radarr_data.main_tabs.tabs[5].help.is_empty()); assert!(radarr_data.main_tabs.tabs[5].help.is_empty());
assert_eq!( assert_eq!(
radarr_data.main_tabs.tabs[5].contextual_help, 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)) Some(build_context_clue_string(&SYSTEM_CONTEXT_CLUES))
); );
@@ -246,10 +258,10 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::models::servarr_data::radarr::radarr_data::{ use crate::models::servarr_data::radarr::radarr_data::{
ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, COLLECTIONS_BLOCKS, ActiveRadarrBlock, ADD_MOVIE_BLOCKS, ADD_MOVIE_SELECTION_BLOCKS, BLOCKLIST_BLOCKS,
COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS, COLLECTIONS_BLOCKS, COLLECTION_DETAILS_BLOCKS, DELETE_MOVIE_BLOCKS,
DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS, EDIT_COLLECTION_SELECTION_BLOCKS, DELETE_MOVIE_SELECTION_BLOCKS, DOWNLOADS_BLOCKS, EDIT_COLLECTION_BLOCKS,
EDIT_INDEXER_BLOCKS, EDIT_INDEXER_NZB_SELECTION_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, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, EDIT_MOVIE_BLOCKS, EDIT_MOVIE_SELECTION_BLOCKS,
INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS, INDEXERS_BLOCKS, INDEXER_SETTINGS_BLOCKS, INDEXER_SETTINGS_SELECTION_BLOCKS, LIBRARY_BLOCKS,
MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS, MOVIE_DETAILS_BLOCKS, ROOT_FOLDERS_BLOCKS, SYSTEM_DETAILS_BLOCKS,
@@ -296,6 +308,16 @@ mod tests {
assert!(ROOT_FOLDERS_BLOCKS.contains(&ActiveRadarrBlock::DeleteRootFolderPrompt)); 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] #[test]
fn test_add_movie_blocks_contents() { fn test_add_movie_blocks_contents() {
assert_eq!(ADD_MOVIE_BLOCKS.len(), 10); assert_eq!(ADD_MOVIE_BLOCKS.len(), 10);
+5 -1
View File
@@ -143,7 +143,11 @@ impl<'a, 'b> Network<'a, 'b> {
.put(uri) .put(uri)
.json(&body.unwrap_or_default()) .json(&body.unwrap_or_default())
.header("X-Api-Key", api_token), .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::app::RadarrConfig;
use crate::models::radarr_models::{ use crate::models::radarr_models::{
AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, Collection, CollectionMovie, AddMovieBody, AddMovieSearchResult, AddOptions, AddRootFolderBody, BlocklistResponse, Collection,
CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse, Indexer, CollectionMovie, CommandBody, Credit, CreditType, DiskSpace, DownloadRecord, DownloadsResponse,
IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody, MovieHistoryItem, Indexer, IndexerSettings, IndexerTestResult, LogResponse, Movie, MovieCommandBody,
QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder, SystemStatus, Tag, Task, MovieHistoryItem, QualityProfile, QueueEvent, Release, ReleaseDownloadBody, RootFolder,
Update, SystemStatus, Tag, Task, Update,
}; };
use crate::models::servarr_data::radarr::modals::{ use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem, AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
@@ -33,6 +33,8 @@ mod radarr_network_tests;
pub enum RadarrEvent { pub enum RadarrEvent {
AddMovie, AddMovie,
AddRootFolder, AddRootFolder,
ClearBlocklist,
DeleteBlocklistItem,
DeleteDownload, DeleteDownload,
DeleteIndexer, DeleteIndexer,
DeleteMovie, DeleteMovie,
@@ -42,6 +44,7 @@ pub enum RadarrEvent {
EditCollection, EditCollection,
EditIndexer, EditIndexer,
EditMovie, EditMovie,
GetBlocklist,
GetCollections, GetCollections,
GetDownloads, GetDownloads,
GetIndexers, GetIndexers,
@@ -75,6 +78,9 @@ pub enum RadarrEvent {
impl RadarrEvent { impl RadarrEvent {
const fn resource(self) -> &'static str { const fn resource(self) -> &'static str {
match self { match self {
RadarrEvent::ClearBlocklist => "/blocklist/bulk",
RadarrEvent::DeleteBlocklistItem => "/blocklist",
RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection", RadarrEvent::GetCollections | RadarrEvent::EditCollection => "/collection",
RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue", RadarrEvent::GetDownloads | RadarrEvent::DeleteDownload => "/queue",
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => { RadarrEvent::GetIndexers | RadarrEvent::EditIndexer | RadarrEvent::DeleteIndexer => {
@@ -125,6 +131,8 @@ impl<'a, 'b> Network<'a, 'b> {
match radarr_event { match radarr_event {
RadarrEvent::AddMovie => self.add_movie().await, RadarrEvent::AddMovie => self.add_movie().await,
RadarrEvent::AddRootFolder => self.add_root_folder().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::DeleteDownload => self.delete_download().await,
RadarrEvent::DeleteIndexer => self.delete_indexer().await, RadarrEvent::DeleteIndexer => self.delete_indexer().await,
RadarrEvent::DeleteMovie => self.delete_movie().await, RadarrEvent::DeleteMovie => self.delete_movie().await,
@@ -134,6 +142,7 @@ impl<'a, 'b> Network<'a, 'b> {
RadarrEvent::EditCollection => self.edit_collection().await, RadarrEvent::EditCollection => self.edit_collection().await,
RadarrEvent::EditIndexer => self.edit_indexer().await, RadarrEvent::EditIndexer => self.edit_indexer().await,
RadarrEvent::EditMovie => self.edit_movie().await, RadarrEvent::EditMovie => self.edit_movie().await,
RadarrEvent::GetBlocklist => self.get_blocklist().await,
RadarrEvent::GetCollections => self.get_collections().await, RadarrEvent::GetCollections => self.get_collections().await,
RadarrEvent::GetDownloads => self.get_downloads().await, RadarrEvent::GetDownloads => self.get_downloads().await,
RadarrEvent::GetIndexers => self.get_indexers().await, RadarrEvent::GetIndexers => self.get_indexers().await,
@@ -319,6 +328,64 @@ impl<'a, 'b> Network<'a, 'b> {
.await; .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) { async fn delete_download(&mut self) {
let download_id = self let download_id = self
.app .app
@@ -794,6 +861,32 @@ impl<'a, 'b> Network<'a, 'b> {
.await; .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) { async fn get_collections(&mut self) {
info!("Fetching Radarr collections"); info!("Fetching Radarr collections");
+348 -2
View File
@@ -13,8 +13,8 @@ mod test {
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::models::radarr_models::{ use crate::models::radarr_models::{
CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability, Monitor, MovieFile, BlocklistItem, CollectionMovie, IndexerField, Language, MediaInfo, MinimumAvailability,
Quality, QualityWrapper, Rating, RatingsList, Monitor, MovieFile, Quality, QualityWrapper, Rating, RatingsList,
}; };
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock; use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::SortOption; use crate::models::stateful_table::SortOption;
@@ -186,6 +186,9 @@ mod test {
} }
#[rstest] #[rstest]
#[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")]
#[case(RadarrEvent::DeleteBlocklistItem, "/blocklist")]
#[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
#[case(RadarrEvent::GetLogs, "/log")] #[case(RadarrEvent::GetLogs, "/log")]
#[case(RadarrEvent::SearchNewMovie, "/movie/lookup")] #[case(RadarrEvent::SearchNewMovie, "/movie/lookup")]
#[case(RadarrEvent::GetMovieCredits, "/credit")] #[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] #[rstest]
#[tokio::test] #[tokio::test]
async fn test_handle_get_collections_event(#[values(true, false)] use_custom_sorting: bool) { 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); 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] #[tokio::test]
async fn test_handle_delete_download_event() { async fn test_handle_delete_download_event() {
let resource = format!("{}/1", RadarrEvent::DeleteDownload.resource()); 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 { fn collection() -> Collection {
Collection { Collection {
id: 123, 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::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::Route; use crate::models::Route;
use crate::ui::draw_tabs; use crate::ui::draw_tabs;
use crate::ui::radarr_ui::blocklist::BlocklistUi;
use crate::ui::radarr_ui::collections::CollectionsUi; use crate::ui::radarr_ui::collections::CollectionsUi;
use crate::ui::radarr_ui::downloads::DownloadsUi; use crate::ui::radarr_ui::downloads::DownloadsUi;
use crate::ui::radarr_ui::indexers::IndexersUi; use crate::ui::radarr_ui::indexers::IndexersUi;
@@ -27,6 +28,7 @@ use crate::ui::widgets::loading_block::LoadingBlock;
use crate::ui::DrawUi; use crate::ui::DrawUi;
use crate::utils::convert_to_gb; use crate::utils::convert_to_gb;
mod blocklist;
mod collections; mod collections;
mod downloads; mod downloads;
mod indexers; mod indexers;
@@ -57,6 +59,7 @@ impl DrawUi for RadarrUi {
_ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area), _ if IndexersUi::accepts(route) => IndexersUi::draw(f, app, content_area),
_ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area), _ if RootFoldersUi::accepts(route) => RootFoldersUi::draw(f, app, content_area),
_ if SystemUi::accepts(route) => SystemUi::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>, text: Text<'a>,
title: &'a str, title: &'a str,
style: Style, style: Style,
alignment: Alignment,
} }
impl<'a> Message<'a> { impl<'a> Message<'a> {
@@ -25,6 +26,7 @@ impl<'a> Message<'a> {
text: message.into(), text: message.into(),
title: "Error", title: "Error",
style: Style::new().failure().bold(), style: Style::new().failure().bold(),
alignment: Alignment::Center,
} }
} }
@@ -38,10 +40,15 @@ impl<'a> Message<'a> {
self self
} }
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
fn render_message(self, area: Rect, buf: &mut Buffer) { fn render_message(self, area: Rect, buf: &mut Buffer) {
Paragraph::new(self.text) Paragraph::new(self.text)
.style(self.style) .style(self.style)
.alignment(Alignment::Center) .alignment(self.alignment)
.block(title_block_centered(self.title).style(self.style)) .block(title_block_centered(self.title).style(self.style))
.wrap(Wrap { trim: true }) .wrap(Wrap { trim: true })
.render(area, buf); .render(area, buf);
+16
View File
@@ -3,6 +3,7 @@ mod tests {
use crate::ui::styles::ManagarrStyle; use crate::ui::styles::ManagarrStyle;
use crate::ui::widgets::message::Message; use crate::ui::widgets::message::Message;
use pretty_assertions::{assert_eq, assert_str_eq}; use pretty_assertions::{assert_eq, assert_str_eq};
use ratatui::layout::Alignment;
use ratatui::style::{Style, Stylize}; use ratatui::style::{Style, Stylize};
use ratatui::text::Text; use ratatui::text::Text;
@@ -15,6 +16,7 @@ mod tests {
assert_eq!(message.text, Text::from(test_message)); assert_eq!(message.text, Text::from(test_message));
assert_str_eq!(message.title, "Error"); assert_str_eq!(message.title, "Error");
assert_eq!(message.style, Style::new().failure().bold()); assert_eq!(message.style, Style::new().failure().bold());
assert_eq!(message.alignment, Alignment::Center);
} }
#[test] #[test]
@@ -27,6 +29,7 @@ mod tests {
assert_str_eq!(message.title, title); assert_str_eq!(message.title, title);
assert_eq!(message.text, Text::from(test_message)); assert_eq!(message.text, Text::from(test_message));
assert_eq!(message.style, Style::new().failure().bold()); assert_eq!(message.style, Style::new().failure().bold());
assert_eq!(message.alignment, Alignment::Center);
} }
#[test] #[test]
@@ -39,5 +42,18 @@ mod tests {
assert_eq!(message.style, style); assert_eq!(message.style, style);
assert_eq!(message.text, Text::from(test_message)); assert_eq!(message.text, Text::from(test_message));
assert_str_eq!(message.title, "Error"); 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; mod popup_tests;
pub enum Size { pub enum Size {
SmallPrompt,
Prompt, Prompt,
LargePrompt, LargePrompt,
Message, Message,
NarrowMessage,
LargeMessage, LargeMessage,
InputBox, InputBox,
Dropdown, Dropdown,
@@ -24,9 +26,11 @@ pub enum Size {
impl Size { impl Size {
pub fn to_percent(&self) -> (u16, u16) { pub fn to_percent(&self) -> (u16, u16) {
match self { match self {
Size::SmallPrompt => (20, 20),
Size::Prompt => (35, 35), Size::Prompt => (35, 35),
Size::LargePrompt => (70, 45), Size::LargePrompt => (70, 45),
Size::Message => (25, 8), Size::Message => (25, 8),
Size::NarrowMessage => (50, 20),
Size::LargeMessage => (25, 25), Size::LargeMessage => (25, 25),
Size::InputBox => (30, 13), Size::InputBox => (30, 13),
Size::Dropdown => (20, 30), Size::Dropdown => (20, 30),
+2
View File
@@ -6,9 +6,11 @@ mod tests {
#[test] #[test]
fn test_dimensions_to_percent() { fn test_dimensions_to_percent() {
assert_eq!(Size::SmallPrompt.to_percent(), (20, 20));
assert_eq!(Size::Prompt.to_percent(), (35, 35)); assert_eq!(Size::Prompt.to_percent(), (35, 35));
assert_eq!(Size::LargePrompt.to_percent(), (70, 45)); assert_eq!(Size::LargePrompt.to_percent(), (70, 45));
assert_eq!(Size::Message.to_percent(), (25, 8)); 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::LargeMessage.to_percent(), (25, 25));
assert_eq!(Size::InputBox.to_percent(), (30, 13)); assert_eq!(Size::InputBox.to_percent(), (30, 13));
assert_eq!(Size::Dropdown.to_percent(), (20, 30)); assert_eq!(Size::Dropdown.to_percent(), (20, 30));