feat(sonarr_network): Added support for fetching episodes for a specified series to the network events

This commit is contained in:
2024-11-15 14:57:19 -07:00
parent 295cd56a1f
commit 6dffc90e92
5 changed files with 348 additions and 7 deletions
+2 -2
View File
@@ -279,11 +279,11 @@ impl From<()> for SonarrSerdeable {
serde_enum_from!(
SonarrSerdeable {
Value(Value),
Episodes(Vec<Episode>),
Episodes(Vec<Episode>),
SeriesVec(Vec<Series>),
SystemStatus(SystemStatus),
BlocklistResponse(BlocklistResponse),
LogResponse(LogResponse),
LogResponse(LogResponse),
}
);
+2 -2
View File
@@ -12,7 +12,7 @@ mod stateful_tree_tests;
#[derive(Default)]
pub struct StatefulTree<T>
where
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display,
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display + PartialEq + Eq,
{
pub state: TreeState,
pub items: Vec<TreeItem<T>>,
@@ -20,7 +20,7 @@ where
impl<T> StatefulTree<T>
where
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display,
T: ToText + Hash + Clone + PartialEq + Eq + Debug + Default + Display + PartialEq + Eq,
{
pub fn set_items(&mut self, items: Vec<TreeItem<T>>) {
self.items = items;
+74 -1
View File
@@ -1,11 +1,16 @@
use std::collections::BTreeMap;
use anyhow::Result;
use log::info;
use managarr_tree_widget::TreeItem;
use serde_json::{json, Value};
use crate::{
models::{
servarr_data::sonarr::sonarr_data::ActiveSonarrBlock,
sonarr_models::{BlocklistResponse, LogResponse, Series, SonarrSerdeable, SystemStatus},
sonarr_models::{
BlocklistResponse, Episode, LogResponse, Series, SonarrSerdeable, SystemStatus,
},
HorizontallyScrollableText, Route, Scrollable,
},
network::RequestMethod,
@@ -21,6 +26,7 @@ pub enum SonarrEvent {
ClearBlocklist,
DeleteBlocklistItem(Option<i64>),
GetBlocklist,
GetEpisodes(Option<i64>),
GetLogs(Option<u64>),
GetStatus,
HealthCheck,
@@ -33,6 +39,7 @@ impl NetworkResource for SonarrEvent {
SonarrEvent::ClearBlocklist => "/blocklist/bulk",
SonarrEvent::DeleteBlocklistItem(_) => "/blocklist",
SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
SonarrEvent::GetEpisodes(_) => "/episode",
SonarrEvent::GetLogs(_) => "/log",
SonarrEvent::GetStatus => "/system/status",
SonarrEvent::HealthCheck => "/health",
@@ -62,6 +69,10 @@ impl<'a, 'b> Network<'a, 'b> {
.await
.map(SonarrSerdeable::from),
SonarrEvent::GetBlocklist => self.get_sonarr_blocklist().await.map(SonarrSerdeable::from),
SonarrEvent::GetEpisodes(series_id) => self
.get_episodes(series_id)
.await
.map(SonarrSerdeable::from),
SonarrEvent::GetLogs(events) => self
.get_sonarr_logs(events)
.await
@@ -175,6 +186,51 @@ impl<'a, 'b> Network<'a, 'b> {
.await
}
async fn get_episodes(&mut self, series_id: Option<i64>) -> Result<Vec<Episode>> {
let event = SonarrEvent::GetEpisodes(series_id);
let (id, series_id_param) = self.extract_series_id(series_id).await;
info!("Fetching episodes for Sonarr series with ID: {id}");
let request_props = self
.request_props_from(
event,
RequestMethod::Get,
None::<()>,
None,
Some(series_id_param),
)
.await;
self
.handle_request::<(), Vec<Episode>>(request_props, |mut episode_vec, mut app| {
episode_vec.sort_by(|a, b| a.id.cmp(&b.id));
let mut seasons = BTreeMap::new();
for episode in episode_vec {
seasons
.entry(episode.season_number)
.or_insert_with(Vec::new)
.push(episode);
}
let tree = seasons
.into_iter()
.map(|(season, episodes_vec)| {
let marker_episode = Episode {
title: Some(format!("Season {season}")),
..Episode::default()
};
let children = episodes_vec.into_iter().map(TreeItem::new_leaf).collect();
TreeItem::new(marker_episode, children).expect("All item identifiers must be unique")
})
.collect();
app.data.sonarr_data.episodes.set_items(tree);
})
.await
}
async fn get_sonarr_logs(&mut self, events: Option<u64>) -> Result<LogResponse> {
info!("Fetching Sonarr logs");
let event = SonarrEvent::GetLogs(events);
@@ -259,4 +315,21 @@ impl<'a, 'b> Network<'a, 'b> {
})
.await
}
async fn extract_series_id(&mut self, series_id: Option<i64>) -> (i64, String) {
let series_id = if let Some(id) = series_id {
id
} else {
self
.app
.lock()
.await
.data
.sonarr_data
.series
.current_selection()
.id
};
(series_id, format!("seriesId={series_id}"))
}
}
+269 -1
View File
@@ -1,17 +1,23 @@
#[cfg(test)]
mod test {
use std::sync::Arc;
use chrono::{DateTime, Utc};
use managarr_tree_widget::TreeItem;
use pretty_assertions::{assert_eq, assert_str_eq};
use reqwest::Client;
use rstest::rstest;
use serde_json::json;
use serde_json::{Number, Value};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::app::App;
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::sonarr_models::{BlocklistItem, Language, LogResponse};
use crate::models::sonarr_models::{BlocklistItem, Episode, Language, LogResponse};
use crate::models::sonarr_models::{BlocklistResponse, Quality};
use crate::models::sonarr_models::{QualityWrapper, SystemStatus};
use crate::models::stateful_table::StatefulTable;
use crate::models::HorizontallyScrollableText;
use crate::models::{sonarr_models::SonarrSerdeable, stateful_table::SortOption};
@@ -80,6 +86,7 @@ mod test {
#[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")]
#[case(SonarrEvent::HealthCheck, "/health")]
#[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
#[case(SonarrEvent::GetEpisodes(None), "/episode")]
#[case(SonarrEvent::GetLogs(Some(500)), "/log")]
#[case(SonarrEvent::GetStatus, "/system/status")]
fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) {
@@ -278,6 +285,190 @@ mod test {
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_get_episodes_event() {
let episodes_json = json!([
{
"id": 2,
"seriesId": 1,
"tvdbId": 1234,
"episodeFileId": 2,
"seasonNumber": 2,
"episodeNumber": 2,
"title": "Something cool",
"airDateUtc": "2024-02-10T07:28:45Z",
"overview": "Okay so this one time at band camp...",
"hasFile": true,
"monitored": true
},
{
"id": 1,
"seriesId": 1,
"tvdbId": 1234,
"episodeFileId": 1,
"seasonNumber": 1,
"episodeNumber": 1,
"title": "Something cool",
"airDateUtc": "2024-02-10T07:28:45Z",
"overview": "Okay so this one time at band camp...",
"hasFile": true,
"monitored": true
}
]);
let marker_episode_1 = Episode {
title: Some("Season 1".to_owned()),
..Episode::default()
};
let marker_episode_2 = Episode {
title: Some("Season 2".to_owned()),
..Episode::default()
};
let episode_1 = episode();
let episode_2 = Episode {
id: 2,
episode_file_id: 2,
season_number: 2,
episode_number: 2,
..episode()
};
let expected_episodes = vec![episode_2.clone(), episode_1.clone()];
let expected_tree = vec![
TreeItem::new(
marker_episode_1,
vec![TreeItem::new_leaf(episode_1.clone())],
)
.unwrap(),
TreeItem::new(
marker_episode_2,
vec![TreeItem::new_leaf(episode_2.clone())],
)
.unwrap(),
];
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(episodes_json),
None,
SonarrEvent::GetEpisodes(None),
None,
Some("seriesId=1"),
)
.await;
app_arc
.lock()
.await
.data
.sonarr_data
.series
.set_items(vec![Series {
id: 1,
..Series::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let SonarrSerdeable::Episodes(episodes) = network
.handle_sonarr_event(SonarrEvent::GetEpisodes(None))
.await
.unwrap()
{
async_server.assert_async().await;
assert_eq!(
app_arc.lock().await.data.sonarr_data.episodes.items,
expected_tree
);
assert_eq!(episodes, expected_episodes);
}
}
#[tokio::test]
async fn test_handle_get_episodes_event_uses_provided_series_id() {
let episodes_json = json!([
{
"id": 2,
"seriesId": 2,
"tvdbId": 1234,
"episodeFileId": 2,
"seasonNumber": 2,
"episodeNumber": 2,
"title": "Something cool",
"airDateUtc": "2024-02-10T07:28:45Z",
"overview": "Okay so this one time at band camp...",
"hasFile": true,
"monitored": true
},
{
"id": 1,
"seriesId": 2,
"tvdbId": 1234,
"episodeFileId": 1,
"seasonNumber": 1,
"episodeNumber": 1,
"title": "Something cool",
"airDateUtc": "2024-02-10T07:28:45Z",
"overview": "Okay so this one time at band camp...",
"hasFile": true,
"monitored": true
}
]);
let marker_episode_1 = Episode {
title: Some("Season 1".to_owned()),
..Episode::default()
};
let marker_episode_2 = Episode {
title: Some("Season 2".to_owned()),
..Episode::default()
};
let episode_1 = Episode {
series_id: 2,
..episode()
};
let episode_2 = Episode {
id: 2,
episode_file_id: 2,
season_number: 2,
episode_number: 2,
series_id: 2,
..episode()
};
let expected_episodes = vec![episode_2.clone(), episode_1.clone()];
let expected_tree = vec![
TreeItem::new(
marker_episode_1,
vec![TreeItem::new_leaf(episode_1.clone())],
)
.unwrap(),
TreeItem::new(
marker_episode_2,
vec![TreeItem::new_leaf(episode_2.clone())],
)
.unwrap(),
];
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(episodes_json),
None,
SonarrEvent::GetEpisodes(None),
None,
Some("seriesId=2"),
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
if let SonarrSerdeable::Episodes(episodes) = network
.handle_sonarr_event(SonarrEvent::GetEpisodes(Some(2)))
.await
.unwrap()
{
async_server.assert_async().await;
assert_eq!(
app_arc.lock().await.data.sonarr_data.episodes.items,
expected_tree
);
assert_eq!(episodes, expected_episodes);
}
}
#[tokio::test]
async fn test_handle_get_sonarr_logs_event() {
let expected_logs = vec![
@@ -591,6 +782,65 @@ mod test {
}
}
#[tokio::test]
async fn test_extract_series_id() {
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc
.lock()
.await
.data
.sonarr_data
.series
.set_items(vec![Series {
id: 1,
..Series::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let (id, series_id_param) = network.extract_series_id(None).await;
assert_eq!(id, 1);
assert_str_eq!(series_id_param, "seriesId=1");
}
#[tokio::test]
async fn test_extract_series_id_uses_provided_id() {
let app_arc = Arc::new(Mutex::new(App::default()));
app_arc
.lock()
.await
.data
.sonarr_data
.series
.set_items(vec![Series {
id: 1,
..Series::default()
}]);
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let (id, series_id_param) = network.extract_series_id(Some(2)).await;
assert_eq!(id, 2);
assert_str_eq!(series_id_param, "seriesId=2");
}
#[tokio::test]
async fn test_extract_series_id_filtered_series() {
let app_arc = Arc::new(Mutex::new(App::default()));
let mut filtered_series = StatefulTable::default();
filtered_series.set_filtered_items(vec![Series {
id: 1,
..Series::default()
}]);
app_arc.lock().await.data.sonarr_data.series = filtered_series;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let (id, series_id_param) = network.extract_series_id(None).await;
assert_eq!(id, 1);
assert_str_eq!(series_id_param, "seriesId=1");
}
fn blocklist_item() -> BlocklistItem {
BlocklistItem {
id: 1,
@@ -606,6 +856,24 @@ mod test {
}
}
fn episode() -> Episode {
Episode {
id: 1,
series_id: 1,
tvdb_id: 1234,
episode_file_id: 1,
season_number: 1,
episode_number: 1,
title: Some("Something cool".to_owned()),
air_date_utc: Some(DateTime::from(
DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap(),
)),
overview: Some("Okay so this one time at band camp...".to_owned()),
has_file: true,
monitored: true,
}
}
fn language() -> Language {
Language {
name: "English".to_owned(),