From a8328d36365f7590a88cc9bb2b219f02ee480e17 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 19 Nov 2024 17:17:12 -0700 Subject: [PATCH] feat(network): Added support for fetching episode releases in Sonarr --- src/network/sonarr_network.rs | 62 ++++++- src/network/sonarr_network_tests.rs | 245 +++++++++++++++++++++++++++- 2 files changed, 297 insertions(+), 10 deletions(-) diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 45e298d..dd924a6 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -41,6 +41,7 @@ pub enum SonarrEvent { GetLogs(Option), GetQualityProfiles, GetQueuedEvents, + GetEpisodeReleases(Option), GetSeasonReleases(Option<(i64, i64)>), GetSecurityConfig, GetSeriesDetails(Option), @@ -63,7 +64,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetLogs(_) => "/log", SonarrEvent::GetQualityProfiles => "/qualityprofile", SonarrEvent::GetQueuedEvents => "/command", - SonarrEvent::GetSeasonReleases(_) => "/release", + SonarrEvent::GetSeasonReleases(_) | SonarrEvent::GetEpisodeReleases(_) => "/release", SonarrEvent::GetStatus => "/system/status", SonarrEvent::HealthCheck => "/health", SonarrEvent::ListSeries | SonarrEvent::GetSeriesDetails(_) => "/series", @@ -122,6 +123,10 @@ impl<'a, 'b> Network<'a, 'b> { .get_queued_sonarr_events() .await .map(SonarrSerdeable::from), + SonarrEvent::GetEpisodeReleases(params) => self + .get_episode_releases(params) + .await + .map(SonarrSerdeable::from), SonarrEvent::GetSeasonReleases(params) => self .get_season_releases(params) .await @@ -555,6 +560,61 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_episode_releases(&mut self, episode_id: Option) -> Result> { + let event = SonarrEvent::GetEpisodeReleases(None); + let id = self.extract_episode_id(episode_id).await; + + info!("Fetching releases for episode with ID: {id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(format!("episodeId={id}")), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |release_vec, mut app| { + if app.data.sonarr_data.season_details_modal.is_none() { + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + } + + if app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .is_none() + { + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal = Some(EpisodeDetailsModal::default()); + } + + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal + .as_mut() + .unwrap() + .episode_releases + .set_items(release_vec); + }) + .await + } + async fn get_season_releases( &mut self, series_season_id_tuple: Option<(i64, i64)>, diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index b57548a..4eb93fb 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -1,18 +1,11 @@ #[cfg(test)] mod test { - use std::fmt::Display; - use std::hash::Hash; use std::sync::Arc; use bimap::BiMap; use chrono::{DateTime, Utc}; use indoc::formatdoc; - use managarr_tree_widget::{Tree, TreeItem, TreeState}; use pretty_assertions::{assert_eq, assert_str_eq}; - use ratatui::buffer::Buffer; - use ratatui::layout::Rect; - use ratatui::text::ToText; - use ratatui::widgets::StatefulWidget; use reqwest::Client; use rstest::rstest; use serde_json::json; @@ -21,7 +14,7 @@ mod test { use tokio_util::sync::CancellationToken; use crate::app::App; - use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal; + use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal}; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::servarr_models::{ HostConfig, Indexer, IndexerField, Language, LogResponse, Quality, QualityProfile, @@ -161,7 +154,13 @@ mod test { } #[rstest] - fn test_resource_release(#[values(SonarrEvent::GetSeasonReleases(None))] event: SonarrEvent) { + fn test_resource_release( + #[values( + SonarrEvent::GetSeasonReleases(None), + SonarrEvent::GetEpisodeReleases(None) + )] + event: SonarrEvent, + ) { assert_str_eq!(event.resource(), "/release"); } @@ -1253,6 +1252,234 @@ mod test { } } + #[tokio::test] + async fn test_handle_get_episode_releases_event() { + let release_json = json!([{ + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }} + }]); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(release_json), + None, + SonarrEvent::GetEpisodeReleases(None), + None, + Some("episodeId=1"), + ) + .await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal = Some(EpisodeDetailsModal::default()); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::Releases(releases_vec) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_releases + .items, + vec![release()] + ); + assert_eq!(releases_vec, vec![release()]); + } + } + + #[tokio::test] + async fn test_handle_get_episode_releases_event_empty_episode_details_modal() { + let release_json = json!([{ + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }} + }]); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(release_json), + None, + SonarrEvent::GetEpisodeReleases(None), + None, + Some("episodeId=1"), + ) + .await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::Releases(releases_vec) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_releases + .items, + vec![release()] + ); + assert_eq!(releases_vec, vec![release()]); + } + } + + #[tokio::test] + #[should_panic(expected = "Season details have not been loaded")] + async fn test_handle_get_episode_releases_event_empty_season_details_modal_panics() { + let release_json = json!([{ + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }} + }]); + let (_async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(release_json), + None, + SonarrEvent::GetEpisodeReleases(None), + None, + Some("episodeId=1"), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + network + .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(None)) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_handle_get_episode_releases_event_uses_provided_series_id() { + let release_json = json!([{ + "guid": "1234", + "protocol": "torrent", + "age": 1, + "title": "Test Release", + "indexer": "kickass torrents", + "indexerId": 2, + "size": 1234, + "rejected": true, + "rejections": [ "Unknown quality profile", "Release is already mapped" ], + "seeders": 2, + "leechers": 1, + "languages": [ { "name": "English" } ], + "quality": { "quality": { "name": "Bluray-1080p" }} + }]); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(release_json), + None, + SonarrEvent::GetEpisodeReleases(None), + None, + Some("episodeId=2"), + ) + .await; + let mut season_details_modal = SeasonDetailsModal::default(); + season_details_modal.episodes.set_items(vec![episode()]); + app_arc.lock().await.data.sonarr_data.season_details_modal = Some(season_details_modal); + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_details_modal = Some(EpisodeDetailsModal::default()); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::Releases(releases_vec) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeReleases(Some(2))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_details_modal + .as_ref() + .unwrap() + .episode_releases + .items, + vec![release()] + ); + assert_eq!(releases_vec, vec![release()]); + } + } + #[tokio::test] async fn test_handle_get_season_releases_event() { let release_json = json!([{