feat: Created a History tab in the Radarr UI and created a list history command and mark-history-item-as-failed command for Radarr

This commit is contained in:
2026-01-13 12:35:54 -07:00
parent 0172253d20
commit ad9e2b3671
52 changed files with 2265 additions and 84 deletions
+63
View File
@@ -0,0 +1,63 @@
use crate::models::Route;
use crate::models::radarr_models::RadarrHistoryWrapper;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::network::radarr_network::RadarrEvent;
use crate::network::{Network, RequestMethod};
use anyhow::Result;
use log::info;
use serde_json::Value;
#[cfg(test)]
#[path = "radarr_history_network_tests.rs"]
mod radarr_history_network_tests;
impl Network<'_, '_> {
pub(in crate::network::radarr_network) async fn get_radarr_history(
&mut self,
events: u64,
) -> Result<RadarrHistoryWrapper> {
info!("Fetching all Radarr history events");
let event = RadarrEvent::GetHistory(events);
let params = format!("pageSize={events}&sortDirection=descending&sortKey=date");
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
.await;
self
.handle_request::<(), RadarrHistoryWrapper>(request_props, |history_response, mut app| {
if !matches!(
app.get_current_route(),
Route::Radarr(ActiveRadarrBlock::HistorySortPrompt, _)
) {
let mut history_vec = history_response.records;
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
app.data.radarr_data.history.set_items(history_vec);
app.data.radarr_data.history.apply_sorting_toggle(false);
}
})
.await
}
pub(in crate::network::radarr_network) async fn mark_radarr_history_item_as_failed(
&mut self,
history_item_id: i64,
) -> Result<Value> {
info!("Marking the Radarr history item with ID: {history_item_id} as 'failed'");
let event = RadarrEvent::MarkHistoryItemAsFailed(history_item_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Post,
None,
Some(format!("/{history_item_id}")),
None,
)
.await;
self
.handle_request::<(), Value>(request_props, |_, _| ())
.await
}
}
@@ -0,0 +1,196 @@
#[cfg(test)]
mod tests {
use crate::models::radarr_models::{RadarrHistoryItem, RadarrHistoryWrapper, RadarrSerdeable};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::SortOption;
use crate::network::network_tests::test_utils::{MockServarrApi, test_network};
use crate::network::radarr_network::RadarrEvent;
use crate::network::radarr_network::radarr_network_test_utils::test_utils::radarr_history_item;
use pretty_assertions::assert_eq;
use rstest::rstest;
use serde_json::json;
#[rstest]
#[tokio::test]
async fn test_handle_get_radarr_history_event(#[values(true, false)] use_custom_sorting: bool) {
let history_json = json!({"records": [{
"id": 123,
"sourceTitle": "z movie",
"movieId": 1007,
"quality": { "quality": { "name": "HD - 1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2022-12-30T07:37:56Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS",
"downloadClient": "transmission",
}
},
{
"id": 456,
"sourceTitle": "A Movie",
"movieId": 2001,
"quality": { "quality": { "name": "HD - 1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2022-12-30T07:37:56Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS",
"downloadClient": "transmission",
}
}]});
let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap();
let mut expected_history_items = vec![
RadarrHistoryItem {
id: 123,
movie_id: 1007,
source_title: "z movie".into(),
..radarr_history_item()
},
RadarrHistoryItem {
id: 456,
movie_id: 2001,
source_title: "A Movie".into(),
..radarr_history_item()
},
];
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("pageSize=500&sortDirection=descending&sortKey=date")
.build_for(RadarrEvent::GetHistory(500))
.await;
app.lock().await.data.radarr_data.history.sort_asc = true;
if use_custom_sorting {
let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
expected_history_items.sort_by(cmp_fn);
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.radarr_data
.history
.sorting(vec![history_sort_option]);
}
let mut network = test_network(&app);
let RadarrSerdeable::HistoryWrapper(history) = network
.handle_radarr_event(RadarrEvent::GetHistory(500))
.await
.unwrap()
else {
panic!("Expected HistoryWrapper")
};
mock.assert_async().await;
assert_eq!(
app.lock().await.data.radarr_data.history.items,
expected_history_items
);
assert!(app.lock().await.data.radarr_data.history.sort_asc);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_get_radarr_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!({"records": [{
"id": 123,
"sourceTitle": "z movie",
"movieId": 1007,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS"
}
},
{
"id": 456,
"sourceTitle": "A Movie",
"movieId": 2001,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{ "id": 1, "name": "English" }],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"indexer": "DrunkenSlug (Prowlarr)",
"releaseGroup": "SPARKS"
}
}]});
let response: RadarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap();
let (mock, app, _server) = MockServarrApi::get()
.returns(history_json)
.query("pageSize=500&sortDirection=descending&sortKey=date")
.build_for(RadarrEvent::GetHistory(500))
.await;
app.lock().await.data.radarr_data.history.sort_asc = true;
app
.lock()
.await
.push_navigation_stack(ActiveRadarrBlock::HistorySortPrompt.into());
let cmp_fn = |a: &RadarrHistoryItem, b: &RadarrHistoryItem| {
a.source_title
.text
.to_lowercase()
.cmp(&b.source_title.text.to_lowercase())
};
let history_sort_option = SortOption {
name: "Source Title",
cmp_fn: Some(cmp_fn),
};
app
.lock()
.await
.data
.radarr_data
.history
.sorting(vec![history_sort_option]);
let mut network = test_network(&app);
let RadarrSerdeable::HistoryWrapper(history) = network
.handle_radarr_event(RadarrEvent::GetHistory(500))
.await
.unwrap()
else {
panic!("Expected HistoryWrapper")
};
mock.assert_async().await;
assert_is_empty!(app.lock().await.data.radarr_data.history);
assert!(app.lock().await.data.radarr_data.history.sort_asc);
assert_eq!(history, response);
}
#[tokio::test]
async fn test_handle_mark_radarr_history_item_as_failed_event() {
let expected_history_item_id = 1;
let (mock, app, _server) = MockServarrApi::post()
.returns(json!({}))
.path("/1")
.build_for(RadarrEvent::MarkHistoryItemAsFailed(
expected_history_item_id,
))
.await;
let mut network = test_network(&app);
let result = network
.handle_radarr_event(RadarrEvent::MarkHistoryItemAsFailed(
expected_history_item_id,
))
.await;
mock.assert_async().await;
assert_ok!(result);
}
}
+13
View File
@@ -16,6 +16,7 @@ use super::NetworkResource;
mod blocklist;
mod collections;
mod downloads;
mod history;
mod indexers;
mod library;
mod root_folders;
@@ -47,10 +48,12 @@ pub enum RadarrEvent {
GetBlocklist,
GetCollections,
GetDownloads(u64),
GetHistory(u64),
GetHostConfig,
GetIndexers,
GetAllIndexerSettings,
GetLogs(u64),
MarkHistoryItemAsFailed(i64),
GetMovieCredits(i64),
GetMovieDetails(i64),
GetMovieHistory(i64),
@@ -86,7 +89,9 @@ impl NetworkResource for RadarrEvent {
RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection",
RadarrEvent::GetDownloads(_) | RadarrEvent::DeleteDownload(_) => "/queue",
RadarrEvent::GetHistory(_) => "/history",
RadarrEvent::GetHostConfig | RadarrEvent::GetSecurityConfig => "/config/host",
RadarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => {
"/indexer"
}
@@ -199,6 +204,10 @@ impl Network<'_, '_> {
.get_radarr_downloads(count)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetHistory(events) => self
.get_radarr_history(events)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetHostConfig => self
.get_radarr_host_config()
.await
@@ -208,6 +217,10 @@ impl Network<'_, '_> {
.get_radarr_logs(events)
.await
.map(RadarrSerdeable::from),
RadarrEvent::MarkHistoryItemAsFailed(history_item_id) => self
.mark_radarr_history_item_as_failed(history_item_id)
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetMovieCredits(movie_id) => {
self.get_credits(movie_id).await.map(RadarrSerdeable::from)
}
@@ -3,8 +3,8 @@ pub mod test_utils {
use crate::models::radarr_models::{
AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit,
CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability,
Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName,
Rating, RatingsList,
Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, RadarrHistoryEventType,
RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, RatingsList,
};
use crate::models::servarr_models::{
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
@@ -313,6 +313,24 @@ pub mod test_utils {
}
}
pub fn radarr_history_item() -> RadarrHistoryItem {
RadarrHistoryItem {
id: 1,
source_title: HorizontallyScrollableText::from("Test"),
movie_id: 1,
quality: quality_wrapper(),
languages: vec![language()],
date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()),
event_type: RadarrHistoryEventType::Grabbed,
data: RadarrHistoryData {
indexer: Some("DrunkenSlug (Prowlarr)".to_owned()),
release_group: Some("SPARKS".to_owned()),
download_client: Some("transmission".to_owned()),
..RadarrHistoryData::default()
},
}
}
pub fn download_record() -> DownloadRecord {
DownloadRecord {
title: "Test Download Title".to_owned(),
@@ -136,7 +136,9 @@ mod test {
#[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")]
#[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")]
#[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
#[case(RadarrEvent::GetHistory(500), "/history")]
#[case(RadarrEvent::GetLogs(500), "/log")]
#[case(RadarrEvent::MarkHistoryItemAsFailed(1), "/history/failed")]
#[case(RadarrEvent::SearchNewMovie(String::new()), "/movie/lookup")]
#[case(RadarrEvent::GetMovieCredits(0), "/credit")]
#[case(RadarrEvent::GetMovieHistory(0), "/history/movie")]