feat(readarr): fetch the blocklist and expose it on the CLI

This commit is contained in:
Dark-Alex-17 committed 2026-09-01 16:32:13 -06:00
1 parent 5a730f2f05
commit 5c593b3770
7 files changed
+334 -3

No files matched your search

@@ -0,0 +1,39 @@
use anyhow::Result;
use log::info;
use crate::models::Route;
use crate::models::readarr_models::{BlocklistItem, BlocklistResponse};
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
use crate::network::readarr_network::ReadarrEvent;
use crate::network::{Network, RequestMethod};
#[cfg(test)]
#[path = "readarr_blocklist_network_tests.rs"]
mod readarr_blocklist_network_tests;
impl Network<'_, '_> {
pub(in crate::network::readarr_network) async fn get_readarr_blocklist(
&mut self,
) -> Result<BlocklistResponse> {
info!("Fetching Readarr blocklist");
let event = ReadarrEvent::GetBlocklist;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| {
if !matches!(
app.get_current_route(),
Route::Readarr(ActiveReadarrBlock::BlocklistSortPrompt, _)
) {
let mut blocklist_vec: Vec<BlocklistItem> = blocklist_resp.records;
blocklist_vec.sort_by_key(|item| item.id);
app.data.readarr_data.blocklist.set_items(blocklist_vec);
app.data.readarr_data.blocklist.apply_sorting_toggle(false);
}
})
.await
}
}
@@ -0,0 +1,226 @@
#[cfg(test)]
mod tests {
use crate::models::readarr_models::{BlocklistItem, BlocklistResponse, ReadarrSerdeable};
use crate::models::servarr_data::readarr::readarr_data::ActiveReadarrBlock;
use crate::models::stateful_table::SortOption;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::readarr_network::ReadarrEvent;
use crate::network::readarr_network::readarr_network_test_utils::test_utils::{
AUTHOR_JSON, blocklist_item, stale_blocklist_item,
};
use pretty_assertions::assert_eq;
use rstest::rstest;
use serde_json::{Number, Value, json};
#[rstest]
#[tokio::test]
async fn test_handle_get_readarr_blocklist_event(
#[values(true, false)] use_custom_sorting: bool,
) {
let blocklist_json = json!({"records": [
{
"id": 456,
"authorId": 2001,
"bookIds": [42020],
"sourceTitle": "An Anthology",
"quality": { "quality": { "name": "AZW3" } },
"date": "2023-01-01T00:00:00Z",
"protocol": "usenet",
"indexer": "DrunkenSlug",
"message": "test message",
"author": serde_json::from_str::<Value>(AUTHOR_JSON).unwrap()
},
{
"id": 123,
"authorId": 1007,
"bookIds": [42018],
"sourceTitle": "z book",
"quality": { "quality": { "name": "AZW3" } },
"date": "2023-01-01T00:00:00Z",
"protocol": "usenet",
"indexer": "DrunkenSlug",
"message": "test message",
"author": serde_json::from_str::<Value>(AUTHOR_JSON).unwrap()
}
]});
let anthology_blocklist_item = BlocklistItem {
id: 456,
author_id: 2001,
book_ids: Some(vec![Number::from(42020)]),
source_title: "An Anthology".to_owned(),
..blocklist_item()
};
let z_book_blocklist_item = BlocklistItem {
id: 123,
author_id: 1007,
book_ids: Some(vec![Number::from(42018)]),
source_title: "z book".to_owned(),
..blocklist_item()
};
let expected_response = BlocklistResponse {
records: vec![
anthology_blocklist_item.clone(),
z_book_blocklist_item.clone(),
],
};
let mut expected_blocklist_items = vec![z_book_blocklist_item, anthology_blocklist_item];
let (mock, app, _server) = MockServarrApi::get()
.returns(blocklist_json)
.build_for(ReadarrEvent::GetBlocklist)
.await;
if use_custom_sorting {
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
a.source_title
.to_lowercase()
.cmp(&b.source_title.to_lowercase())
};
expected_blocklist_items.sort_by(cmp_fn);
let blocklist_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.readarr_data
.blocklist
.sorting(vec![blocklist_sort_option]);
}
{
let mut app = app.lock().await;
app.server_tabs.set_index(3);
app.data.readarr_data.blocklist.sort_asc = true;
app
.data
.readarr_data
.blocklist
.set_items(vec![stale_blocklist_item()]);
}
let mut network = test_network(&app);
let result = network
.handle_readarr_event(ReadarrEvent::GetBlocklist)
.await;
mock.assert_async().await;
let ReadarrSerdeable::BlocklistResponse(blocklist) = result.unwrap() else {
panic!("Expected BlocklistResponse")
};
assert_eq!(blocklist, expected_response);
let app = app.lock().await;
assert_eq!(
app.data.readarr_data.blocklist.items,
expected_blocklist_items
);
assert!(app.data.readarr_data.blocklist.sort_asc);
}
#[tokio::test]
async fn test_handle_get_readarr_blocklist_event_no_op_when_user_is_selecting_sort_options() {
let stale_blocklist_items = vec![stale_blocklist_item()];
let blocklist_json = json!({"records": [
{
"id": 456,
"authorId": 2001,
"bookIds": [42020],
"sourceTitle": "An Anthology",
"quality": { "quality": { "name": "AZW3" } },
"date": "2023-01-01T00:00:00Z",
"protocol": "usenet",
"indexer": "DrunkenSlug",
"message": "test message",
"author": serde_json::from_str::<Value>(AUTHOR_JSON).unwrap()
}
]});
let expected_response = BlocklistResponse {
records: vec![BlocklistItem {
id: 456,
author_id: 2001,
book_ids: Some(vec![Number::from(42020)]),
source_title: "An Anthology".to_owned(),
..blocklist_item()
}],
};
let (mock, app, _server) = MockServarrApi::get()
.returns(blocklist_json)
.build_for(ReadarrEvent::GetBlocklist)
.await;
{
let mut app = app.lock().await;
app.server_tabs.set_index(3);
app.data.readarr_data.blocklist.sort_asc = true;
app
.data
.readarr_data
.blocklist
.set_items(stale_blocklist_items.clone());
app.push_navigation_stack(ActiveReadarrBlock::BlocklistSortPrompt.into());
}
let mut network = test_network(&app);
let result = network
.handle_readarr_event(ReadarrEvent::GetBlocklist)
.await;
mock.assert_async().await;
let ReadarrSerdeable::BlocklistResponse(blocklist) = result.unwrap() else {
panic!("Expected BlocklistResponse")
};
assert_eq!(blocklist, expected_response);
let app = app.lock().await;
assert_eq!(app.data.readarr_data.blocklist.items, stale_blocklist_items);
assert!(app.data.readarr_data.blocklist.sort_asc);
}
#[tokio::test]
async fn test_handle_get_readarr_blocklist_event_failure() {
let stale_blocklist_items = vec![stale_blocklist_item()];
let blocklist_json = json!({"records": [
{
"id": 456,
"authorId": 2001,
"bookIds": [42020],
"sourceTitle": "An Anthology",
"quality": { "quality": { "name": "AZW3" } },
"date": "2023-01-01T00:00:00Z",
"protocol": "usenet",
"indexer": "DrunkenSlug",
"message": "test message",
"author": serde_json::from_str::<Value>(AUTHOR_JSON).unwrap()
}
]});
let (mock, app, _server) = MockServarrApi::get()
.returns(blocklist_json)
.status(500)
.build_for(ReadarrEvent::GetBlocklist)
.await;
{
let mut app = app.lock().await;
app.server_tabs.set_index(3);
app
.data
.readarr_data
.blocklist
.set_items(stale_blocklist_items.clone());
}
let mut network = test_network(&app);
let result = network
.handle_readarr_event(ReadarrEvent::GetBlocklist)
.await;
mock.assert_async().await;
assert_err!(result);
assert_eq!(
app.lock().await.data.readarr_data.blocklist.items,
stale_blocklist_items
);
}
}
+7
View File
@@ -10,6 +10,7 @@ use crate::models::readarr_models::{
use crate::models::servarr_models::{MetadataProfile, QualityProfile, ReleaseDownloadBody, Tag};
use crate::network::{Network, RequestMethod};
mod blocklist;
mod downloads;
mod history;
mod library;
@@ -40,6 +41,7 @@ pub enum ReadarrEvent {
GetAuthorDetails(i64),
GetAuthorHistory(i64),
GetAuthorReleases(i64),
GetBlocklist,
GetBookDetails(i64),
GetBookEditions(i64),
GetBookFiles(i64),
@@ -84,6 +86,7 @@ impl NetworkResource for ReadarrEvent {
| ReadarrEvent::ListAuthors
| ReadarrEvent::ToggleAuthorMonitoring(_) => "/author",
ReadarrEvent::SearchNewAuthor(_) => "/author/lookup",
ReadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
ReadarrEvent::DeleteBook(_)
| ReadarrEvent::GetBookDetails(_)
| ReadarrEvent::GetBooks(_)
@@ -183,6 +186,10 @@ impl Network<'_, '_> {
.get_author_releases(author_id)
.await
.map(ReadarrSerdeable::from),
ReadarrEvent::GetBlocklist => self
.get_readarr_blocklist()
.await
.map(ReadarrSerdeable::from),
ReadarrEvent::GetBookDetails(book_id) => self
.get_book_details(book_id)
.await
@@ -2,9 +2,10 @@
#[allow(dead_code)]
pub mod test_utils {
use crate::models::readarr_models::{
AddAuthorBody, AddAuthorOptions, AddAuthorSearchResult, Author, AuthorStatus, Book, BookFile,
DownloadRecord, DownloadStatus, DownloadsResponse, Edition, MonitorType, NewItemMonitorType,
ReadarrHistoryData, ReadarrHistoryEventType, ReadarrHistoryItem, ReadarrRelease,
AddAuthorBody, AddAuthorOptions, AddAuthorSearchResult, Author, AuthorStatus, BlocklistItem,
Book, BookFile, DownloadRecord, DownloadStatus, DownloadsResponse, Edition, MonitorType,
NewItemMonitorType, ReadarrHistoryData, ReadarrHistoryEventType, ReadarrHistoryItem,
ReadarrRelease,
};
use crate::models::servarr_models::{Quality, QualityWrapper, RootFolder};
use chrono::DateTime;
@@ -234,6 +235,30 @@ pub mod test_utils {
QualityWrapper { quality: quality() }
}
pub fn stale_blocklist_item() -> BlocklistItem {
BlocklistItem {
id: 99,
author_id: 99,
source_title: "Stale source title".to_owned(),
..BlocklistItem::default()
}
}
pub fn blocklist_item() -> BlocklistItem {
BlocklistItem {
id: 1,
author_id: 1,
book_ids: Some(vec![Number::from(1)]),
source_title: "Test source title".to_owned(),
quality: quality_wrapper(),
date: DateTime::from(DateTime::parse_from_rfc3339("2023-01-01T00:00:00Z").unwrap()),
protocol: "usenet".to_owned(),
indexer: "DrunkenSlug".to_owned(),
message: "test message".to_owned(),
author: serde_json::from_str(AUTHOR_JSON).unwrap(),
}
}
pub fn stale_release() -> ReadarrRelease {
ReadarrRelease {
guid: "stale-release-guid".to_owned(),
@@ -22,6 +22,7 @@ mod tests {
#[case(ReadarrEvent::ListAuthors, "/author")]
#[case(ReadarrEvent::ToggleAuthorMonitoring(1), "/author")]
#[case(ReadarrEvent::SearchNewAuthor(String::new()), "/author/lookup")]
#[case(ReadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
#[case(ReadarrEvent::DeleteBook(DeleteParams::default()), "/book")]
#[case(ReadarrEvent::GetBookDetails(1), "/book")]
#[case(ReadarrEvent::GetBooks(1), "/book")]