feat(network): Support for fetching all episode files for a given series
This commit is contained in:
@@ -19,7 +19,7 @@ use crate::{
|
|||||||
HorizontallyScrollableText, ScrollableText, TabRoute, TabState,
|
HorizontallyScrollableText, ScrollableText, TabRoute, TabState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use crate::models::sonarr_models::EpisodeFile;
|
||||||
use super::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
use super::sonarr_data::{ActiveSonarrBlock, SonarrData};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -310,6 +310,7 @@ impl Default for EpisodeDetailsModal {
|
|||||||
|
|
||||||
pub struct SeasonDetailsModal {
|
pub struct SeasonDetailsModal {
|
||||||
pub episodes: StatefulTable<Episode>,
|
pub episodes: StatefulTable<Episode>,
|
||||||
|
pub episode_files: StatefulTable<EpisodeFile>,
|
||||||
pub episode_details_modal: Option<EpisodeDetailsModal>,
|
pub episode_details_modal: Option<EpisodeDetailsModal>,
|
||||||
pub season_history: StatefulTable<SonarrHistoryItem>,
|
pub season_history: StatefulTable<SonarrHistoryItem>,
|
||||||
pub season_releases: StatefulTable<SonarrRelease>,
|
pub season_releases: StatefulTable<SonarrRelease>,
|
||||||
@@ -321,6 +322,7 @@ impl Default for SeasonDetailsModal {
|
|||||||
SeasonDetailsModal {
|
SeasonDetailsModal {
|
||||||
episodes: StatefulTable::default(),
|
episodes: StatefulTable::default(),
|
||||||
episode_details_modal: None,
|
episode_details_modal: None,
|
||||||
|
episode_files: StatefulTable::default(),
|
||||||
season_releases: StatefulTable::default(),
|
season_releases: StatefulTable::default(),
|
||||||
season_history: StatefulTable::default(),
|
season_history: StatefulTable::default(),
|
||||||
season_details_tabs: TabState::new(vec![
|
season_details_tabs: TabState::new(vec![
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(season_details_modal.episodes.is_empty());
|
assert!(season_details_modal.episodes.is_empty());
|
||||||
assert!(season_details_modal.episode_details_modal.is_none());
|
assert!(season_details_modal.episode_details_modal.is_none());
|
||||||
|
assert!(season_details_modal.episode_files.is_empty());
|
||||||
assert!(season_details_modal.season_releases.is_empty());
|
assert!(season_details_modal.season_releases.is_empty());
|
||||||
assert!(season_details_modal.season_history.is_empty());
|
assert!(season_details_modal.season_history.is_empty());
|
||||||
|
|
||||||
|
|||||||
@@ -176,11 +176,14 @@ impl Display for Episode {
|
|||||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EpisodeFile {
|
pub struct EpisodeFile {
|
||||||
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
|
pub id: i64,
|
||||||
pub relative_path: String,
|
pub relative_path: String,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
#[serde(deserialize_with = "super::from_i64")]
|
#[serde(deserialize_with = "super::from_i64")]
|
||||||
pub size: i64,
|
pub size: i64,
|
||||||
pub language: Language,
|
pub language: Language,
|
||||||
|
pub quality: QualityWrapper,
|
||||||
pub date_added: DateTime<Utc>,
|
pub date_added: DateTime<Utc>,
|
||||||
pub media_info: Option<MediaInfo>,
|
pub media_info: Option<MediaInfo>,
|
||||||
}
|
}
|
||||||
@@ -626,6 +629,7 @@ pub enum SonarrSerdeable {
|
|||||||
DiskSpaces(Vec<DiskSpace>),
|
DiskSpaces(Vec<DiskSpace>),
|
||||||
Episode(Episode),
|
Episode(Episode),
|
||||||
Episodes(Vec<Episode>),
|
Episodes(Vec<Episode>),
|
||||||
|
EpisodeFiles(Vec<EpisodeFile>),
|
||||||
HostConfig(HostConfig),
|
HostConfig(HostConfig),
|
||||||
IndexerSettings(IndexerSettings),
|
IndexerSettings(IndexerSettings),
|
||||||
Indexers(Vec<Indexer>),
|
Indexers(Vec<Indexer>),
|
||||||
@@ -669,6 +673,7 @@ serde_enum_from!(
|
|||||||
DiskSpaces(Vec<DiskSpace>),
|
DiskSpaces(Vec<DiskSpace>),
|
||||||
Episode(Episode),
|
Episode(Episode),
|
||||||
Episodes(Vec<Episode>),
|
Episodes(Vec<Episode>),
|
||||||
|
EpisodeFiles(Vec<EpisodeFile>),
|
||||||
HostConfig(HostConfig),
|
HostConfig(HostConfig),
|
||||||
IndexerSettings(IndexerSettings),
|
IndexerSettings(IndexerSettings),
|
||||||
Indexers(Vec<Indexer>),
|
Indexers(Vec<Indexer>),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
sonarr_models::{
|
sonarr_models::{
|
||||||
AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse,
|
AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse,
|
||||||
Episode, IndexerSettings, Series, SeriesMonitor, SeriesStatus, SeriesType,
|
Episode, EpisodeFile, IndexerSettings, Series, SeriesMonitor, SeriesStatus, SeriesType,
|
||||||
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask,
|
SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask,
|
||||||
SonarrTaskName, SystemStatus,
|
SonarrTaskName, SystemStatus,
|
||||||
},
|
},
|
||||||
@@ -236,6 +236,21 @@ mod tests {
|
|||||||
assert_eq!(sonarr_serdeable, SonarrSerdeable::Episodes(episodes));
|
assert_eq!(sonarr_serdeable, SonarrSerdeable::Episodes(episodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sonarr_serdeable_from_episode_files() {
|
||||||
|
let episode_files = vec![EpisodeFile {
|
||||||
|
id: 1,
|
||||||
|
..EpisodeFile::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
let sonarr_serdeable: SonarrSerdeable = episode_files.clone().into();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sonarr_serdeable,
|
||||||
|
SonarrSerdeable::EpisodeFiles(episode_files)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sonarr_serdeable_from_host_config() {
|
fn test_sonarr_serdeable_from_host_config() {
|
||||||
let host_config = HostConfig {
|
let host_config = HostConfig {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ use crate::{
|
|||||||
sonarr_models::{
|
sonarr_models::{
|
||||||
AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, BlocklistItem, BlocklistResponse,
|
AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, BlocklistItem, BlocklistResponse,
|
||||||
DeleteSeriesParams, DownloadRecord, DownloadsResponse, EditSeriesParams, Episode,
|
DeleteSeriesParams, DownloadRecord, DownloadsResponse, EditSeriesParams, Episode,
|
||||||
IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem, SonarrHistoryWrapper,
|
EpisodeFile, IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem,
|
||||||
SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, SonarrTaskName,
|
SonarrHistoryWrapper, SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask,
|
||||||
SystemStatus,
|
SonarrTaskName, SystemStatus,
|
||||||
},
|
},
|
||||||
stateful_table::StatefulTable,
|
stateful_table::StatefulTable,
|
||||||
HorizontallyScrollableText, Route, Scrollable, ScrollableText,
|
HorizontallyScrollableText, Route, Scrollable, ScrollableText,
|
||||||
@@ -62,6 +62,7 @@ pub enum SonarrEvent {
|
|||||||
GetIndexers,
|
GetIndexers,
|
||||||
GetEpisodeDetails(Option<i64>),
|
GetEpisodeDetails(Option<i64>),
|
||||||
GetEpisodes(Option<i64>),
|
GetEpisodes(Option<i64>),
|
||||||
|
GetEpisodeFiles(Option<i64>),
|
||||||
GetEpisodeHistory(Option<i64>),
|
GetEpisodeHistory(Option<i64>),
|
||||||
GetLanguageProfiles,
|
GetLanguageProfiles,
|
||||||
GetLogs(Option<u64>),
|
GetLogs(Option<u64>),
|
||||||
@@ -104,7 +105,7 @@ impl NetworkResource for SonarrEvent {
|
|||||||
SonarrEvent::GetAllIndexerSettings | SonarrEvent::EditAllIndexerSettings(_) => {
|
SonarrEvent::GetAllIndexerSettings | SonarrEvent::EditAllIndexerSettings(_) => {
|
||||||
"/config/indexer"
|
"/config/indexer"
|
||||||
}
|
}
|
||||||
SonarrEvent::DeleteEpisodeFile(_) => "/episodefile",
|
SonarrEvent::GetEpisodeFiles(_) | SonarrEvent::DeleteEpisodeFile(_) => "/episodefile",
|
||||||
SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
|
SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
|
||||||
SonarrEvent::GetDownloads | SonarrEvent::DeleteDownload(_) => "/queue",
|
SonarrEvent::GetDownloads | SonarrEvent::DeleteDownload(_) => "/queue",
|
||||||
SonarrEvent::GetEpisodes(_) | SonarrEvent::GetEpisodeDetails(_) => "/episode",
|
SonarrEvent::GetEpisodes(_) | SonarrEvent::GetEpisodeDetails(_) => "/episode",
|
||||||
@@ -225,6 +226,10 @@ impl<'a, 'b> Network<'a, 'b> {
|
|||||||
.get_episodes(series_id)
|
.get_episodes(series_id)
|
||||||
.await
|
.await
|
||||||
.map(SonarrSerdeable::from),
|
.map(SonarrSerdeable::from),
|
||||||
|
SonarrEvent::GetEpisodeFiles(series_id) => self
|
||||||
|
.get_episode_files(series_id)
|
||||||
|
.await
|
||||||
|
.map(SonarrSerdeable::from),
|
||||||
SonarrEvent::GetEpisodeDetails(episode_id) => self
|
SonarrEvent::GetEpisodeDetails(episode_id) => self
|
||||||
.get_episode_details(episode_id)
|
.get_episode_details(episode_id)
|
||||||
.await
|
.await
|
||||||
@@ -1420,6 +1425,39 @@ impl<'a, 'b> Network<'a, 'b> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_episode_files(&mut self, series_id: Option<i64>) -> Result<Vec<EpisodeFile>> {
|
||||||
|
let event = SonarrEvent::GetEpisodeFiles(series_id);
|
||||||
|
let (id, series_id_param) = self.extract_series_id(series_id).await;
|
||||||
|
info!("Fetching episodes files 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<EpisodeFile>>(request_props, |episode_file_vec, mut app| {
|
||||||
|
if app.data.sonarr_data.season_details_modal.is_none() {
|
||||||
|
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
app
|
||||||
|
.data
|
||||||
|
.sonarr_data
|
||||||
|
.season_details_modal
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.episode_files
|
||||||
|
.set_items(episode_file_vec);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_sonarr_episode_history(
|
async fn get_sonarr_episode_history(
|
||||||
&mut self,
|
&mut self,
|
||||||
episode_id: Option<i64>,
|
episode_id: Option<i64>,
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ mod test {
|
|||||||
"airDateUtc": "2024-02-10T07:28:45Z",
|
"airDateUtc": "2024-02-10T07:28:45Z",
|
||||||
"overview": "Okay so this one time at band camp...",
|
"overview": "Okay so this one time at band camp...",
|
||||||
"episodeFile": {
|
"episodeFile": {
|
||||||
|
"id": 1,
|
||||||
"relativePath": "/season 1/episode 1.mkv",
|
"relativePath": "/season 1/episode 1.mkv",
|
||||||
"path": "/nfs/tv/series/season 1/episode 1.mkv",
|
"path": "/nfs/tv/series/season 1/episode 1.mkv",
|
||||||
"size": 3543348019,
|
"size": 3543348019,
|
||||||
@@ -265,10 +266,20 @@ mod test {
|
|||||||
assert_str_eq!(event.resource(), "/release");
|
assert_str_eq!(event.resource(), "/release");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
fn test_resource_episode_file(
|
||||||
|
#[values(
|
||||||
|
SonarrEvent::GetEpisodeFiles(None),
|
||||||
|
SonarrEvent::DeleteEpisodeFile(None)
|
||||||
|
)]
|
||||||
|
event: SonarrEvent,
|
||||||
|
) {
|
||||||
|
assert_str_eq!(event.resource(), "/episodefile");
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")]
|
#[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")]
|
||||||
#[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")]
|
#[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")]
|
||||||
#[case(SonarrEvent::DeleteEpisodeFile(None), "/episodefile")]
|
|
||||||
#[case(SonarrEvent::HealthCheck, "/health")]
|
#[case(SonarrEvent::HealthCheck, "/health")]
|
||||||
#[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
|
#[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
|
||||||
#[case(SonarrEvent::GetDiskSpace, "/diskspace")]
|
#[case(SonarrEvent::GetDiskSpace, "/diskspace")]
|
||||||
@@ -2589,6 +2600,162 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_episode_files_event() {
|
||||||
|
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||||
|
RequestMethod::Get,
|
||||||
|
None,
|
||||||
|
Some(json!([episode_file()])),
|
||||||
|
None,
|
||||||
|
SonarrEvent::GetEpisodeFiles(None),
|
||||||
|
None,
|
||||||
|
Some("seriesId=1"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
app_arc.lock().await.data.sonarr_data.season_details_modal =
|
||||||
|
Some(SeasonDetailsModal::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());
|
||||||
|
|
||||||
|
if let SonarrSerdeable::EpisodeFiles(episode_files) = network
|
||||||
|
.handle_sonarr_event(SonarrEvent::GetEpisodeFiles(None))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
async_server.assert_async().await;
|
||||||
|
assert_eq!(
|
||||||
|
app_arc
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.data
|
||||||
|
.sonarr_data
|
||||||
|
.season_details_modal
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.episode_files
|
||||||
|
.items,
|
||||||
|
vec![episode_file()]
|
||||||
|
);
|
||||||
|
assert_eq!(episode_files, vec![episode_file()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_episode_files_event_empty_season_details_modal() {
|
||||||
|
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||||
|
RequestMethod::Get,
|
||||||
|
None,
|
||||||
|
Some(json!([episode_file()])),
|
||||||
|
None,
|
||||||
|
SonarrEvent::GetEpisodeFiles(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::EpisodeFiles(episode_files) = network
|
||||||
|
.handle_sonarr_event(SonarrEvent::GetEpisodeFiles(None))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
async_server.assert_async().await;
|
||||||
|
assert!(app_arc
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.data
|
||||||
|
.sonarr_data
|
||||||
|
.season_details_modal
|
||||||
|
.is_some());
|
||||||
|
assert_eq!(
|
||||||
|
app_arc
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.data
|
||||||
|
.sonarr_data
|
||||||
|
.season_details_modal
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.episode_files
|
||||||
|
.items,
|
||||||
|
vec![episode_file()]
|
||||||
|
);
|
||||||
|
assert_eq!(episode_files, vec![episode_file()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_handle_get_episode_files_event_uses_provided_series_id() {
|
||||||
|
let episode_file = EpisodeFile {
|
||||||
|
id: 2,
|
||||||
|
..episode_file()
|
||||||
|
};
|
||||||
|
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||||
|
RequestMethod::Get,
|
||||||
|
None,
|
||||||
|
Some(json!([episode_file.clone()])),
|
||||||
|
None,
|
||||||
|
SonarrEvent::GetEpisodeFiles(Some(2)),
|
||||||
|
None,
|
||||||
|
Some("seriesId=2"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
app_arc.lock().await.data.sonarr_data.season_details_modal =
|
||||||
|
Some(SeasonDetailsModal::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());
|
||||||
|
|
||||||
|
if let SonarrSerdeable::EpisodeFiles(episode_files) = network
|
||||||
|
.handle_sonarr_event(SonarrEvent::GetEpisodeFiles(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_files
|
||||||
|
.items,
|
||||||
|
vec![episode_file.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(episode_files, vec![episode_file]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_handle_get_sonarr_host_config_event() {
|
async fn test_handle_get_sonarr_host_config_event() {
|
||||||
let host_config_response = json!({
|
let host_config_response = json!({
|
||||||
@@ -7104,9 +7271,11 @@ mod test {
|
|||||||
|
|
||||||
fn episode_file() -> EpisodeFile {
|
fn episode_file() -> EpisodeFile {
|
||||||
EpisodeFile {
|
EpisodeFile {
|
||||||
|
id: 1,
|
||||||
relative_path: "/season 1/episode 1.mkv".to_owned(),
|
relative_path: "/season 1/episode 1.mkv".to_owned(),
|
||||||
path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(),
|
path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(),
|
||||||
size: 3543348019,
|
size: 3543348019,
|
||||||
|
quality: quality_wrapper(),
|
||||||
language: language(),
|
language: language(),
|
||||||
date_added: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
|
date_added: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
|
||||||
media_info: Some(media_info()),
|
media_info: Some(media_info()),
|
||||||
|
|||||||
Reference in New Issue
Block a user