feat(network): Support for listing disk space on a Sonarr instance

This commit is contained in:
2024-11-22 15:54:11 -07:00
parent d96316577a
commit a881d1f33a
17 changed files with 185 additions and 39 deletions
+1 -1
View File
@@ -213,7 +213,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
RadarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
+1 -1
View File
@@ -179,7 +179,7 @@ impl<'a> App<'a> {
.dispatch_network_event(RadarrEvent::GetDownloads.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetOverview.into())
.dispatch_network_event(RadarrEvent::GetDiskSpace.into())
.await;
self
.dispatch_network_event(RadarrEvent::GetStatus.into())
+2 -2
View File
@@ -511,7 +511,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
RadarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
@@ -544,7 +544,7 @@ mod tests {
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
RadarrEvent::GetOverview.into()
RadarrEvent::GetDiskSpace.into()
);
assert_eq!(
sync_network_rx.recv().await.unwrap(),
+12
View File
@@ -42,6 +42,11 @@ pub enum SonarrDeleteCommand {
#[arg(long, help = "The ID of the root folder to delete", required = true)]
root_folder_id: i64,
},
#[command(about = "Delete the tag with the specified ID")]
Tag {
#[arg(long, help = "The ID of the tag to delete", required = true)]
tag_id: i64,
},
}
impl From<SonarrDeleteCommand> for Command {
@@ -99,6 +104,13 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrDeleteCommand> for SonarrDeleteComm
.await?;
serde_json::to_string_pretty(&resp)?
}
SonarrDeleteCommand::Tag { tag_id } => {
let resp = self
.network
.handle_network_event(SonarrEvent::DeleteTag(tag_id).into())
.await?;
serde_json::to_string_pretty(&resp)?
}
};
Ok(resp)
@@ -156,6 +156,31 @@ mod tests {
assert_eq!(delete_command, expected_args);
}
}
#[test]
fn test_delete_tag_requires_arguments() {
let result = Cli::command().try_get_matches_from(["managarr", "sonarr", "delete", "tag"]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn test_delete_tag_success() {
let expected_args = SonarrDeleteCommand::Tag { tag_id: 1 };
let result = Cli::try_parse_from(["managarr", "sonarr", "delete", "tag", "--tag-id", "1"]);
assert!(result.is_ok());
if let Some(Command::Sonarr(SonarrCommand::Delete(delete_command))) = result.unwrap().command
{
assert_eq!(delete_command, expected_args);
}
}
}
mod handler {
@@ -283,5 +308,31 @@ mod tests {
assert!(result.is_ok());
}
#[tokio::test]
async fn test_handle_delete_tag_command() {
let expected_tag_id = 1;
let mut mock_network = MockNetworkTrait::new();
mock_network
.expect_handle_network_event()
.with(eq::<NetworkEvent>(
SonarrEvent::DeleteTag(expected_tag_id).into(),
))
.times(1)
.returning(|_| {
Ok(Serdeable::Sonarr(SonarrSerdeable::Value(
json!({"testResponse": "response"}),
)))
});
let app_arc = Arc::new(Mutex::new(App::default()));
let delete_tag_command = SonarrDeleteCommand::Tag { tag_id: 1 };
let result =
SonarrDeleteCommandHandler::with(&app_arc, delete_tag_command, &mut mock_network)
.handle()
.await;
assert!(result.is_ok());
}
}
}
+2 -11
View File
@@ -10,8 +10,8 @@ use strum_macros::EnumIter;
use crate::{models::HorizontallyScrollableText, serde_enum_from};
use super::servarr_models::{
HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper, QueueEvent, Release,
RootFolder, SecurityConfig, Tag,
DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper,
QueueEvent, Release, RootFolder, SecurityConfig, Tag,
};
use super::{EnumDisplayStyle, Serdeable};
@@ -149,15 +149,6 @@ pub struct DeleteMovieParams {
pub add_list_exclusion: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DiskSpace {
#[serde(deserialize_with = "super::from_i64")]
pub free_space: i64,
#[serde(deserialize_with = "super::from_i64")]
pub total_space: i64,
}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DownloadRecord {
@@ -6,14 +6,14 @@ use crate::app::radarr::radarr_context_clues::{
SYSTEM_CONTEXT_CLUES,
};
use crate::models::radarr_models::{
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DiskSpace, DownloadRecord,
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord,
IndexerSettings, Movie, Task,
};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, IndexerTestResultModalItem,
MovieDetailsModal,
};
use crate::models::servarr_models::{Indexer, QueueEvent, RootFolder};
use crate::models::servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder};
use crate::models::stateful_list::StatefulList;
use crate::models::stateful_table::StatefulTable;
use crate::models::{
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
use strum::EnumIter;
use crate::models::{
servarr_models::{Indexer, QueueEvent, RootFolder},
servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder},
sonarr_models::{
BlocklistItem, DownloadRecord, IndexerSettings, Season, Series, SonarrHistoryItem,
},
@@ -21,6 +21,7 @@ mod sonarr_data_tests;
pub struct SonarrData {
pub blocklist: StatefulTable<BlocklistItem>,
pub downloads: StatefulTable<DownloadRecord>,
pub disk_space_vec: Vec<DiskSpace>,
pub edit_root_folder: Option<HorizontallyScrollableText>,
pub history: StatefulTable<SonarrHistoryItem>,
pub indexers: StatefulTable<Indexer>,
@@ -43,6 +44,7 @@ impl Default for SonarrData {
SonarrData {
blocklist: StatefulTable::default(),
downloads: StatefulTable::default(),
disk_space_vec: Vec::new(),
edit_root_folder: None,
history: StatefulTable::default(),
indexers: StatefulTable::default(),
@@ -36,6 +36,7 @@ mod tests {
assert!(sonarr_data.blocklist.is_empty());
assert!(sonarr_data.downloads.is_empty());
assert!(sonarr_data.disk_space_vec.is_empty());
assert!(sonarr_data.edit_root_folder.is_none());
assert!(sonarr_data.history.is_empty());
assert!(sonarr_data.indexers.is_empty());
+9
View File
@@ -74,6 +74,15 @@ impl Display for CertificateValidation {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DiskSpace {
#[serde(deserialize_with = "super::from_i64")]
pub free_space: i64,
#[serde(deserialize_with = "super::from_i64")]
pub total_space: i64,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct HostConfig {
+4 -2
View File
@@ -11,8 +11,8 @@ use crate::serde_enum_from;
use super::{
servarr_models::{
HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper, QueueEvent,
Release, RootFolder, SecurityConfig, Tag,
DiskSpace, HostConfig, Indexer, Language, LogResponse, QualityProfile, QualityWrapper,
QueueEvent, Release, RootFolder, SecurityConfig, Tag,
},
EnumDisplayStyle, HorizontallyScrollableText, Serdeable,
};
@@ -394,6 +394,7 @@ pub struct SonarrHistoryItem {
pub enum SonarrSerdeable {
Value(Value),
DownloadsResponse(DownloadsResponse),
DiskSpaces(Vec<DiskSpace>),
Episode(Episode),
Episodes(Vec<Episode>),
HostConfig(HostConfig),
@@ -431,6 +432,7 @@ serde_enum_from!(
SonarrSerdeable {
Value(Value),
DownloadsResponse(DownloadsResponse),
DiskSpaces(Vec<DiskSpace>),
Episode(Episode),
Episodes(Vec<Episode>),
HostConfig(HostConfig),
+14 -2
View File
@@ -5,8 +5,8 @@ mod tests {
use crate::models::{
servarr_models::{
HostConfig, Indexer, Log, LogResponse, QualityProfile, QueueEvent, Release, RootFolder,
SecurityConfig, Tag,
DiskSpace, HostConfig, Indexer, Log, LogResponse, QualityProfile, QueueEvent, Release,
RootFolder, SecurityConfig, Tag,
},
sonarr_models::{
BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, Episode,
@@ -291,6 +291,18 @@ mod tests {
);
}
#[test]
fn test_sonarr_serdeable_from_disk_spaces() {
let disk_spaces = vec![DiskSpace {
free_space: 1,
total_space: 1,
}];
let sonarr_serdeable: SonarrSerdeable = disk_spaces.clone().into();
assert_eq!(sonarr_serdeable, SonarrSerdeable::DiskSpaces(disk_spaces));
}
#[test]
fn test_sonarr_serdeable_from_log_response() {
let log_response = LogResponse {
+8 -8
View File
@@ -8,7 +8,7 @@ use urlencoding::encode;
use crate::models::radarr_models::{
AddMovieBody, AddMovieSearchResult, AddOptions, BlocklistResponse, Collection, CollectionMovie,
CommandBody, Credit, CreditType, DeleteMovieParams, DiskSpace, DownloadRecord, DownloadsResponse,
CommandBody, Credit, CreditType, DeleteMovieParams, DownloadRecord, DownloadsResponse,
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings, IndexerTestResult,
Movie, MovieCommandBody, MovieHistoryItem, RadarrSerdeable, ReleaseDownloadBody, SystemStatus,
Task, TaskName, Update,
@@ -19,8 +19,8 @@ use crate::models::servarr_data::radarr::modals::{
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_models::{
AddRootFolderBody, HostConfig, Indexer, LogResponse, QualityProfile, QueueEvent, Release,
RootFolder, SecurityConfig, Tag,
AddRootFolderBody, DiskSpace, HostConfig, Indexer, LogResponse, QualityProfile, QueueEvent,
Release, RootFolder, SecurityConfig, Tag,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText};
@@ -61,7 +61,7 @@ pub enum RadarrEvent {
GetMovieDetails(Option<i64>),
GetMovieHistory(Option<i64>),
GetMovies,
GetOverview,
GetDiskSpace,
GetQualityProfiles,
GetQueuedEvents,
GetReleases(Option<i64>),
@@ -107,7 +107,7 @@ impl NetworkResource for RadarrEvent {
RadarrEvent::SearchNewMovie(_) => "/movie/lookup",
RadarrEvent::GetMovieCredits(_) => "/credit",
RadarrEvent::GetMovieHistory(_) => "/history/movie",
RadarrEvent::GetOverview => "/diskspace",
RadarrEvent::GetDiskSpace => "/diskspace",
RadarrEvent::GetQualityProfiles => "/qualityprofile",
RadarrEvent::GetReleases(_) | RadarrEvent::DownloadRelease(_) => "/release",
RadarrEvent::AddRootFolder(_)
@@ -220,7 +220,7 @@ impl<'a, 'b> Network<'a, 'b> {
.await
.map(RadarrSerdeable::from),
RadarrEvent::GetMovies => self.get_movies().await.map(RadarrSerdeable::from),
RadarrEvent::GetOverview => self.get_diskspace().await.map(RadarrSerdeable::from),
RadarrEvent::GetDiskSpace => self.get_radarr_diskspace().await.map(RadarrSerdeable::from),
RadarrEvent::GetQualityProfiles => self
.get_radarr_quality_profiles()
.await
@@ -1368,9 +1368,9 @@ impl<'a, 'b> Network<'a, 'b> {
.await
}
async fn get_diskspace(&mut self) -> Result<Vec<DiskSpace>> {
async fn get_radarr_diskspace(&mut self) -> Result<Vec<DiskSpace>> {
info!("Fetching Radarr disk space");
let event = RadarrEvent::GetOverview;
let event = RadarrEvent::GetDiskSpace;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
+4 -4
View File
@@ -222,7 +222,7 @@ mod test {
#[case(RadarrEvent::SearchNewMovie(None), "/movie/lookup")]
#[case(RadarrEvent::GetMovieCredits(None), "/credit")]
#[case(RadarrEvent::GetMovieHistory(None), "/history/movie")]
#[case(RadarrEvent::GetOverview, "/diskspace")]
#[case(RadarrEvent::GetDiskSpace, "/diskspace")]
#[case(RadarrEvent::GetQualityProfiles, "/qualityprofile")]
#[case(RadarrEvent::GetStatus, "/system/status")]
#[case(RadarrEvent::GetTasks, "/system/task")]
@@ -262,7 +262,7 @@ mod test {
}
#[tokio::test]
async fn test_handle_get_diskspace_event() {
async fn test_handle_get_radarr_diskspace_event() {
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
@@ -277,7 +277,7 @@ mod test {
}
])),
None,
RadarrEvent::GetOverview,
RadarrEvent::GetDiskSpace,
None,
None,
)
@@ -295,7 +295,7 @@ mod test {
];
if let RadarrSerdeable::DiskSpaces(disk_space) = network
.handle_radarr_event(RadarrEvent::GetOverview)
.handle_radarr_event(RadarrEvent::GetDiskSpace)
.await
.unwrap()
{
+20 -2
View File
@@ -10,8 +10,8 @@ use crate::{
sonarr_data::ActiveSonarrBlock,
},
servarr_models::{
AddRootFolderBody, HostConfig, Indexer, LogResponse, QualityProfile, QueueEvent, Release,
RootFolder, SecurityConfig, Tag,
AddRootFolderBody, DiskSpace, HostConfig, Indexer, LogResponse, QualityProfile, QueueEvent,
Release, RootFolder, SecurityConfig, Tag,
},
sonarr_models::{
BlocklistResponse, DownloadRecord, DownloadsResponse, Episode, IndexerSettings, Series,
@@ -49,6 +49,7 @@ pub enum SonarrEvent {
GetEpisodes(Option<i64>),
GetEpisodeHistory(Option<i64>),
GetLogs(Option<u64>),
GetDiskSpace,
GetQualityProfiles,
GetQueuedEvents,
GetRootFolders,
@@ -77,6 +78,7 @@ impl NetworkResource for SonarrEvent {
SonarrEvent::GetHostConfig | SonarrEvent::GetSecurityConfig => "/config/host",
SonarrEvent::GetIndexers | SonarrEvent::DeleteIndexer(_) => "/indexer",
SonarrEvent::GetLogs(_) => "/log",
SonarrEvent::GetDiskSpace => "/diskspace",
SonarrEvent::GetQualityProfiles => "/qualityprofile",
SonarrEvent::GetQueuedEvents => "/command",
SonarrEvent::GetRootFolders
@@ -163,6 +165,7 @@ impl<'a, 'b> Network<'a, 'b> {
.get_sonarr_logs(events)
.await
.map(SonarrSerdeable::from),
SonarrEvent::GetDiskSpace => self.get_sonarr_diskspace().await.map(SonarrSerdeable::from),
SonarrEvent::GetQualityProfiles => self
.get_sonarr_quality_profiles()
.await
@@ -843,6 +846,21 @@ impl<'a, 'b> Network<'a, 'b> {
.await
}
async fn get_sonarr_diskspace(&mut self) -> Result<Vec<DiskSpace>> {
info!("Fetching Sonarr disk space");
let event = SonarrEvent::GetDiskSpace;
let request_props = self
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
.await;
self
.handle_request::<(), Vec<DiskSpace>>(request_props, |disk_space_vec, mut app| {
app.data.sonarr_data.disk_space_vec = disk_space_vec;
})
.await
}
async fn get_sonarr_quality_profiles(&mut self) -> Result<Vec<QualityProfile>> {
info!("Fetching Sonarr quality profiles");
let event = SonarrEvent::GetQualityProfiles;
+49 -1
View File
@@ -17,7 +17,7 @@ mod test {
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,
DiskSpace, HostConfig, Indexer, IndexerField, Language, LogResponse, Quality, QualityProfile,
QualityWrapper, QueueEvent, Release, RootFolder, SecurityConfig, Tag,
};
use crate::models::sonarr_models::SystemStatus;
@@ -212,6 +212,7 @@ mod test {
#[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")]
#[case(SonarrEvent::HealthCheck, "/health")]
#[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
#[case(SonarrEvent::GetDiskSpace, "/diskspace")]
#[case(SonarrEvent::GetSeriesHistory(None), "/history/series")]
#[case(SonarrEvent::GetLogs(Some(500)), "/log")]
#[case(SonarrEvent::GetQualityProfiles, "/qualityprofile")]
@@ -784,6 +785,53 @@ mod test {
}
}
#[tokio::test]
async fn test_handle_get_sonarr_diskspace_event() {
let (async_server, app_arc, _server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(json!([
{
"freeSpace": 1111,
"totalSpace": 2222,
},
{
"freeSpace": 3333,
"totalSpace": 4444
}
])),
None,
SonarrEvent::GetDiskSpace,
None,
None,
)
.await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
let disk_space_vec = vec![
DiskSpace {
free_space: 1111,
total_space: 2222,
},
DiskSpace {
free_space: 3333,
total_space: 4444,
},
];
if let SonarrSerdeable::DiskSpaces(disk_space) = network
.handle_sonarr_event(SonarrEvent::GetDiskSpace)
.await
.unwrap()
{
async_server.assert_async().await;
assert_eq!(
app_arc.lock().await.data.sonarr_data.disk_space_vec,
disk_space_vec
);
assert_eq!(disk_space, disk_space_vec);
}
}
#[tokio::test]
async fn test_handle_get_sonarr_healthcheck_event() {
let (async_server, app_arc, _server) = mock_servarr_api(
+2 -2
View File
@@ -9,9 +9,9 @@ use ratatui::Frame;
use crate::app::App;
use crate::logos::RADARR_LOGO;
use crate::models::radarr_models::{DiskSpace, DownloadRecord, Movie};
use crate::models::radarr_models::{DownloadRecord, Movie};
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::servarr_models::RootFolder;
use crate::models::servarr_models::{DiskSpace, RootFolder};
use crate::models::Route;
use crate::ui::draw_tabs;
use crate::ui::radarr_ui::blocklist::BlocklistUi;