feat(network): Support for fetching Sonarr series history for a given series ID

This commit is contained in:
2024-11-20 14:54:16 -07:00
parent 5872a6ba72
commit 86d93377ac
5 changed files with 516 additions and 14 deletions
@@ -58,7 +58,6 @@ impl Default for SonarrData {
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)]
pub enum ActiveSonarrBlock { pub enum ActiveSonarrBlock {
#[default]
AddRootFolderPrompt, AddRootFolderPrompt,
AddSeriesAlreadyInLibrary, AddSeriesAlreadyInLibrary,
AddSeriesConfirmPrompt, AddSeriesConfirmPrompt,
@@ -95,6 +94,8 @@ pub enum ActiveSonarrBlock {
FilterHistoryError, FilterHistoryError,
FilterSeries, FilterSeries,
FilterSeriesError, FilterSeriesError,
FilterSeriesHistory,
FilterSeriesHistoryError,
History, History,
HistoryDetails, HistoryDetails,
HistorySortPrompt, HistorySortPrompt,
@@ -116,11 +117,15 @@ pub enum ActiveSonarrBlock {
SearchSeasonError, SearchSeasonError,
SearchSeries, SearchSeries,
SearchSeriesError, SearchSeriesError,
SearchSeriesHistory,
SearchSeriesHistoryError,
SeasonDetails, SeasonDetails,
SeasonHistory, SeasonHistory,
#[default]
Series, Series,
SeriesDetails, SeriesDetails,
SeriesHistory, SeriesHistory,
SeriesHistorySortPrompt,
SeriesSortPrompt, SeriesSortPrompt,
System, System,
SystemLogs, SystemLogs,
+2
View File
@@ -349,6 +349,7 @@ pub enum SonarrSerdeable {
SecurityConfig(SecurityConfig), SecurityConfig(SecurityConfig),
SeriesVec(Vec<Series>), SeriesVec(Vec<Series>),
Series(Series), Series(Series),
SonarrHistoryItems(Vec<SonarrHistoryItem>),
SonarrHistoryWrapper(SonarrHistoryWrapper), SonarrHistoryWrapper(SonarrHistoryWrapper),
SystemStatus(SystemStatus), SystemStatus(SystemStatus),
BlocklistResponse(BlocklistResponse), BlocklistResponse(BlocklistResponse),
@@ -382,6 +383,7 @@ serde_enum_from!(
SecurityConfig(SecurityConfig), SecurityConfig(SecurityConfig),
SeriesVec(Vec<Series>), SeriesVec(Vec<Series>),
Series(Series), Series(Series),
SonarrHistoryItems(Vec<SonarrHistoryItem>),
SonarrHistoryWrapper(SonarrHistoryWrapper), SonarrHistoryWrapper(SonarrHistoryWrapper),
SystemStatus(SystemStatus), SystemStatus(SystemStatus),
BlocklistResponse(BlocklistResponse), BlocklistResponse(BlocklistResponse),
+9 -11
View File
@@ -9,8 +9,8 @@ mod tests {
}, },
sonarr_models::{ sonarr_models::{
BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode,
IndexerSettings, Series, SeriesStatus, SeriesType, SonarrHistoryItem, SonarrHistoryWrapper, IndexerSettings, Series, SeriesStatus, SeriesType, SonarrHistoryItem, SonarrSerdeable,
SonarrSerdeable, SystemStatus, SystemStatus,
}, },
Serdeable, Serdeable,
}; };
@@ -169,18 +169,16 @@ mod tests {
} }
#[test] #[test]
fn test_sonarr_serdeable_from_sonarr_history_wrapper() { fn test_sonarr_serdeable_from_sonarr_history_items() {
let history_wrapper = SonarrHistoryWrapper { let history_items = vec![SonarrHistoryItem {
records: vec![SonarrHistoryItem { id: 1,
id: 1, ..SonarrHistoryItem::default()
..SonarrHistoryItem::default() }];
}], let sonarr_serdeable: SonarrSerdeable = history_items.clone().into();
};
let sonarr_serdeable: SonarrSerdeable = history_wrapper.clone().into();
assert_eq!( assert_eq!(
sonarr_serdeable, sonarr_serdeable,
SonarrSerdeable::SonarrHistoryWrapper(history_wrapper) SonarrSerdeable::SonarrHistoryItems(history_items)
); );
} }
+56 -1
View File
@@ -14,8 +14,9 @@ use crate::{
}, },
sonarr_models::{ sonarr_models::{
BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, IndexerSettings, Series, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, IndexerSettings, Series,
SonarrHistoryWrapper, SonarrSerdeable, SystemStatus, SonarrHistoryItem, SonarrHistoryWrapper, SonarrSerdeable, SystemStatus,
}, },
stateful_table::StatefulTable,
HorizontallyScrollableText, Route, Scrollable, ScrollableText, HorizontallyScrollableText, Route, Scrollable, ScrollableText,
}, },
network::RequestMethod, network::RequestMethod,
@@ -46,6 +47,7 @@ pub enum SonarrEvent {
GetSeasonReleases(Option<(i64, i64)>), GetSeasonReleases(Option<(i64, i64)>),
GetSecurityConfig, GetSecurityConfig,
GetSeriesDetails(Option<i64>), GetSeriesDetails(Option<i64>),
GetSeriesHistory(Option<i64>),
GetStatus, GetStatus,
HealthCheck, HealthCheck,
ListSeries, ListSeries,
@@ -67,6 +69,7 @@ impl NetworkResource for SonarrEvent {
SonarrEvent::GetQualityProfiles => "/qualityprofile", SonarrEvent::GetQualityProfiles => "/qualityprofile",
SonarrEvent::GetQueuedEvents => "/command", SonarrEvent::GetQueuedEvents => "/command",
SonarrEvent::GetSeasonReleases(_) | SonarrEvent::GetEpisodeReleases(_) => "/release", SonarrEvent::GetSeasonReleases(_) | SonarrEvent::GetEpisodeReleases(_) => "/release",
SonarrEvent::GetSeriesHistory(_) => "/history/series",
SonarrEvent::GetStatus => "/system/status", SonarrEvent::GetStatus => "/system/status",
SonarrEvent::HealthCheck => "/health", SonarrEvent::HealthCheck => "/health",
SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) => "/series", SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) => "/series",
@@ -145,6 +148,10 @@ impl<'a, 'b> Network<'a, 'b> {
.get_series_details(series_id) .get_series_details(series_id)
.await .await
.map(SonarrSerdeable::from), .map(SonarrSerdeable::from),
SonarrEvent::GetSeriesHistory(series_id) => self
.get_sonarr_series_history(series_id)
.await
.map(SonarrSerdeable::from),
SonarrEvent::GetStatus => self.get_sonarr_status().await.map(SonarrSerdeable::from), SonarrEvent::GetStatus => self.get_sonarr_status().await.map(SonarrSerdeable::from),
SonarrEvent::HealthCheck => self SonarrEvent::HealthCheck => self
.get_sonarr_healthcheck() .get_sonarr_healthcheck()
@@ -726,6 +733,54 @@ impl<'a, 'b> Network<'a, 'b> {
.await .await
} }
async fn get_sonarr_series_history(
&mut self,
series_id: Option<i64>,
) -> Result<Vec<SonarrHistoryItem>> {
let (id, series_id_param) = self.extract_series_id(series_id).await;
info!("Fetching Sonarr series history for series with ID: {id}");
let event = SonarrEvent::GetSeriesHistory(series_id);
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(series_id_param),
)
.await;
self
.handle_request::<(), Vec<SonarrHistoryItem>>(request_props, |mut history_vec, mut app| {
if app.data.sonarr_data.series_history.is_none() {
app.data.sonarr_data.series_history = Some(StatefulTable::default());
}
if !matches!(
app.get_current_route(),
Route::Sonarr(ActiveSonarrBlock::SeriesHistorySortPrompt, _)
) {
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.set_items(history_vec);
app
.data
.sonarr_data
.series_history
.as_mut()
.unwrap()
.apply_sorting_toggle(false);
}
})
.await
}
async fn list_series(&mut self) -> Result<Vec<Series>> { async fn list_series(&mut self) -> Result<Vec<Series>> {
info!("Fetching Sonarr library"); info!("Fetching Sonarr library");
let event = SonarrEvent::ListSeries; let event = SonarrEvent::ListSeries;
+443 -1
View File
@@ -155,6 +155,11 @@ mod test {
assert_str_eq!(event.resource(), "/indexer"); assert_str_eq!(event.resource(), "/indexer");
} }
#[rstest]
fn test_resource_history(#[values(SonarrEvent::GetSeriesHistory(None))] event: SonarrEvent) {
assert_str_eq!(event.resource(), "/history/series");
}
#[rstest] #[rstest]
fn test_resource_release( fn test_resource_release(
#[values( #[values(
@@ -999,7 +1004,6 @@ mod test {
.to_lowercase() .to_lowercase()
.cmp(&b.source_title.text.to_lowercase()) .cmp(&b.source_title.text.to_lowercase())
}; };
let history_sort_option = SortOption { let history_sort_option = SortOption {
name: "Source Title", name: "Source Title",
cmp_fn: Some(cmp_fn), cmp_fn: Some(cmp_fn),
@@ -2211,6 +2215,444 @@ mod test {
} }
} }
#[rstest]
#[tokio::test]
async fn test_handle_get_sonarr_series_history_event(
#[values(true, false)] use_custom_sorting: bool,
) {
let history_json = json!([{
"id": 123,
"sourceTitle": "z episode",
"episodeId": 1007,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
},
{
"id": 456,
"sourceTitle": "A Episode",
"episodeId": 2001,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
}]);
let response: Vec<SonarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let mut expected_history_items = vec![
SonarrHistoryItem {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
},
];
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(history_json),
None,
SonarrEvent::GetSeriesHistory(None),
None,
Some("seriesId=1"),
)
.await;
let mut series_history_table = StatefulTable {
sort_asc: true,
..StatefulTable::default()
};
if use_custom_sorting {
let cmp_fn = |a: &SonarrHistoryItem, b: &SonarrHistoryItem| {
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),
};
series_history_table.sorting(vec![history_sort_option]);
}
app_arc
.lock()
.await
.data
.sonarr_data
.series
.set_items(vec![series()]);
app_arc.lock().await.data.sonarr_data.series_history = Some(series_history_table);
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let SonarrSerdeable::SonarrHistoryItems(history_items) = network
.handle_sonarr_event(SonarrEvent::GetSeriesHistory(None))
.await
.unwrap()
{
async_server.assert_async().await;
assert!(app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.is_some());
assert_eq!(
app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.items,
expected_history_items
);
assert!(
app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.sort_asc
);
assert_eq!(history_items, response);
}
}
#[tokio::test]
async fn test_handle_get_sonarr_series_history_event_uses_provided_series_id() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z episode",
"episodeId": 1007,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
},
{
"id": 456,
"sourceTitle": "A Episode",
"episodeId": 2001,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
}]);
let response: Vec<SonarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![
SonarrHistoryItem {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
},
];
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(history_json),
None,
SonarrEvent::GetSeriesHistory(Some(2)),
None,
Some("seriesId=2"),
)
.await;
app_arc.lock().await.data.sonarr_data.series_history = Some(StatefulTable {
sort_asc: true,
..StatefulTable::default()
});
app_arc
.lock()
.await
.data
.sonarr_data
.series
.set_items(vec![series()]);
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let SonarrSerdeable::SonarrHistoryItems(history_items) = network
.handle_sonarr_event(SonarrEvent::GetSeriesHistory(Some(2)))
.await
.unwrap()
{
async_server.assert_async().await;
assert!(app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.is_some());
assert_eq!(
app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.items,
expected_history_items
);
assert!(
app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.sort_asc
);
assert_eq!(history_items, response);
}
}
#[tokio::test]
async fn test_handle_get_sonarr_series_history_event_empty_series_history_table() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z episode",
"episodeId": 1007,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
},
{
"id": 456,
"sourceTitle": "A Episode",
"episodeId": 2001,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
}]);
let response: Vec<SonarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let expected_history_items = vec![
SonarrHistoryItem {
id: 123,
episode_id: 1007,
source_title: "z episode".into(),
..history_item()
},
SonarrHistoryItem {
id: 456,
episode_id: 2001,
source_title: "A Episode".into(),
..history_item()
},
];
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(history_json),
None,
SonarrEvent::GetSeriesHistory(None),
None,
Some("seriesId=1"),
)
.await;
app_arc
.lock()
.await
.data
.sonarr_data
.series
.set_items(vec![series()]);
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let SonarrSerdeable::SonarrHistoryItems(history_items) = network
.handle_sonarr_event(SonarrEvent::GetSeriesHistory(None))
.await
.unwrap()
{
async_server.assert_async().await;
assert!(app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.is_some());
assert_eq!(
app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.items,
expected_history_items
);
assert!(
!app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.sort_asc
);
assert_eq!(history_items, response);
}
}
#[tokio::test]
async fn test_handle_get_sonarr_series_history_event_no_op_when_user_is_selecting_sort_options() {
let history_json = json!([{
"id": 123,
"sourceTitle": "z episode",
"episodeId": 1007,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
},
{
"id": 456,
"sourceTitle": "A Episode",
"episodeId": 2001,
"quality": { "quality": { "name": "Bluray-1080p" } },
"languages": [{"name": "English"}],
"date": "2024-02-10T07:28:45Z",
"eventType": "grabbed",
"data": {
"droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv",
"importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv"
}
}]);
let response: Vec<SonarrHistoryItem> = serde_json::from_value(history_json.clone()).unwrap();
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(history_json),
None,
SonarrEvent::GetSeriesHistory(None),
None,
Some("seriesId=1"),
)
.await;
let cmp_fn = |a: &SonarrHistoryItem, b: &SonarrHistoryItem| {
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),
};
let mut series_history_table = StatefulTable {
sort_asc: true,
..StatefulTable::default()
};
series_history_table.sorting(vec![history_sort_option]);
app_arc.lock().await.data.sonarr_data.series_history = Some(series_history_table);
app_arc
.lock()
.await
.data
.sonarr_data
.series
.set_items(vec![series()]);
app_arc
.lock()
.await
.push_navigation_stack(ActiveSonarrBlock::SeriesHistorySortPrompt.into());
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let SonarrSerdeable::SonarrHistoryItems(history_items) = network
.handle_sonarr_event(SonarrEvent::GetSeriesHistory(None))
.await
.unwrap()
{
async_server.assert_async().await;
assert!(app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.is_some());
assert!(app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.is_empty());
assert!(
app_arc
.lock()
.await
.data
.sonarr_data
.series_history
.as_ref()
.unwrap()
.sort_asc
);
assert_eq!(history_items, response);
}
}
#[tokio::test] #[tokio::test]
async fn test_handle_get_series_event_no_op_while_user_is_selecting_sort_options() { async fn test_handle_get_series_event_no_op_while_user_is_selecting_sort_options() {
let mut series_1: Value = serde_json::from_str(SERIES_JSON).unwrap(); let mut series_1: Value = serde_json::from_str(SERIES_JSON).unwrap();