From 7b6d5953978b44a1cb55b120286d3786349fbe9c Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Thu, 17 Sep 2026 15:46:34 -0600 Subject: [PATCH] fix(radarr): model the download status as a DownloadStatus enum Radarr was the only tree still holding DownloadRecord.status as a bare String and comparing it against literals. It now carries its own DownloadStatus enum, shaped exactly like the Sonarr, Lidarr and Readarr copies, and is guarded with from_json_or_default so an unrecognised status degrades to Unknown instead of failing the whole queue fetch. The row-style decorator and get_movie_status compare against the enum variants. The status cell is never rendered in the Radarr tree, so no snapshot changes; the CLI JSON output is unchanged for every real value since the enum serializes to the same camelCase strings. --- .../radarr_handler_test_utils.rs | 6 +- src/models/radarr_models.rs | 33 +++++++++- src/models/radarr_models_tests.rs | 64 +++++++++++++++++-- src/network/radarr_network/library/mod.rs | 6 +- .../library/radarr_library_network_tests.rs | 9 +-- .../radarr_network_test_utils.rs | 9 +-- src/ui/radarr_ui/mod.rs | 6 +- src/ui/radarr_ui/radarr_ui_tests.rs | 22 +++++-- 8 files changed, 126 insertions(+), 29 deletions(-) diff --git a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs index 77a0382e..9eabb627 100644 --- a/src/handlers/radarr_handlers/radarr_handler_test_utils.rs +++ b/src/handlers/radarr_handlers/radarr_handler_test_utils.rs @@ -4,8 +4,8 @@ pub(in crate::handlers::radarr_handlers) mod utils { use crate::models::HorizontallyScrollableText; use crate::models::radarr_models::{ AddMovieBody, AddMovieOptions, AddMovieSearchResult, Collection, CollectionMovie, - DownloadRecord, IndexerSettings, MediaInfo, MinimumAvailability, Movie, MovieCollection, - MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList, + DownloadRecord, DownloadStatus, IndexerSettings, MediaInfo, MinimumAvailability, Movie, + MovieCollection, MovieFile, MovieMonitor, RadarrRelease, Rating, RatingsList, }; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, @@ -407,7 +407,7 @@ pub(in crate::handlers::radarr_handlers) mod utils { pub fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), - status: "downloading".to_owned(), + status: DownloadStatus::Downloading, id: 1, movie_id: 1, size: 3543348019, diff --git a/src/models/radarr_models.rs b/src/models/radarr_models.rs index 69481985..9e1a5984 100644 --- a/src/models/radarr_models.rs +++ b/src/models/radarr_models.rs @@ -154,7 +154,8 @@ pub struct DeleteMovieParams { #[serde(rename_all = "camelCase")] pub struct DownloadRecord { pub title: String, - pub status: String, + #[serde(deserialize_with = "super::from_json_or_default")] + pub status: DownloadStatus, #[serde(deserialize_with = "super::from_i64")] pub id: i64, #[serde(deserialize_with = "super::from_i64")] @@ -169,6 +170,36 @@ pub struct DownloadRecord { pub download_client: String, } +#[derive( + Serialize, + Deserialize, + Default, + PartialEq, + Eq, + Clone, + Copy, + Debug, + EnumIter, + Display, + EnumDisplayStyle, +)] +#[serde(rename_all = "camelCase")] +#[strum(serialize_all = "camelCase")] +pub enum DownloadStatus { + #[default] + Unknown, + Queued, + Paused, + Downloading, + Completed, + Failed, + Warning, + Delay, + #[display_style(name = "Download Client Unavailable")] + DownloadClientUnavailable, + Fallback, +} + #[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct DownloadsResponse { diff --git a/src/models/radarr_models_tests.rs b/src/models/radarr_models_tests.rs index 9a7e2ec2..430ce115 100644 --- a/src/models/radarr_models_tests.rs +++ b/src/models/radarr_models_tests.rs @@ -11,16 +11,16 @@ mod tests { Serdeable, radarr_models::{ AddMovieSearchResult, BlocklistItem, BlocklistResponse, Collection, Credit, CreditType, - DiskSpace, DownloadRecord, DownloadsResponse, Indexer, IndexerSettings, IndexerTestResult, - MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, QualityProfile, RadarrRelease, - RadarrSerdeable, RadarrTask, RadarrTaskName, Tag, Update, + DiskSpace, DownloadRecord, DownloadStatus, DownloadsResponse, Indexer, IndexerSettings, + IndexerTestResult, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, + QualityProfile, RadarrRelease, RadarrSerdeable, RadarrTask, RadarrTaskName, Tag, Update, }, servarr_models::{ HostConfig, Log, LogResponse, QueueEvent, RootFolder, SecurityConfig, SystemStatus, }, }; use crate::network::radarr_network::radarr_network_test_utils::test_utils::{ - MOVIE_JSON, collection, crew_credit, task, + MOVIE_JSON, collection, crew_credit, download_record, task, }; #[test] @@ -70,6 +70,40 @@ mod tests { assert_str_eq!(MovieMonitor::None.to_display_str(), "None"); } + #[test] + fn test_download_status_display() { + assert_str_eq!(DownloadStatus::Unknown.to_string(), "unknown"); + assert_str_eq!(DownloadStatus::Queued.to_string(), "queued"); + assert_str_eq!(DownloadStatus::Paused.to_string(), "paused"); + assert_str_eq!(DownloadStatus::Downloading.to_string(), "downloading"); + assert_str_eq!(DownloadStatus::Completed.to_string(), "completed"); + assert_str_eq!(DownloadStatus::Failed.to_string(), "failed"); + assert_str_eq!(DownloadStatus::Warning.to_string(), "warning"); + assert_str_eq!(DownloadStatus::Delay.to_string(), "delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_string(), + "downloadClientUnavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_string(), "fallback"); + } + + #[test] + fn test_download_status_to_display_str() { + assert_str_eq!(DownloadStatus::Unknown.to_display_str(), "Unknown"); + assert_str_eq!(DownloadStatus::Queued.to_display_str(), "Queued"); + assert_str_eq!(DownloadStatus::Paused.to_display_str(), "Paused"); + assert_str_eq!(DownloadStatus::Downloading.to_display_str(), "Downloading"); + assert_str_eq!(DownloadStatus::Completed.to_display_str(), "Completed"); + assert_str_eq!(DownloadStatus::Failed.to_display_str(), "Failed"); + assert_str_eq!(DownloadStatus::Warning.to_display_str(), "Warning"); + assert_str_eq!(DownloadStatus::Delay.to_display_str(), "Delay"); + assert_str_eq!( + DownloadStatus::DownloadClientUnavailable.to_display_str(), + "Download Client Unavailable" + ); + assert_str_eq!(DownloadStatus::Fallback.to_display_str(), "Fallback"); + } + #[test] fn test_radarr_history_event_type_display() { assert_str_eq!(RadarrHistoryEventType::Unknown.to_string(), "unknown"); @@ -230,7 +264,7 @@ mod tests { fn test_download_record_default_indexer_value() { let json = r#"{ "title": "test", - "status": "test", + "status": "downloading", "id": 0, "movieId": 0, "size": 0, @@ -239,7 +273,7 @@ mod tests { }"#; let expected_record = DownloadRecord { title: "test".to_owned(), - status: "test".to_owned(), + status: DownloadStatus::Downloading, id: 0, movie_id: 0, size: 0, @@ -693,6 +727,24 @@ mod tests { ); } + #[test] + fn test_download_record_status_deserialization_falls_back_to_default() { + let download_record_json = serde_json::to_string(&download_record()).unwrap(); + let expected = DownloadRecord { + status: DownloadStatus::default(), + ..download_record() + }; + + assert_eq!( + deserialize_with_field::(&download_record_json, "status", json!(2)), + expected + ); + assert_eq!( + deserialize_with_field::(&download_record_json, "status", json!("notAThing")), + expected + ); + } + fn deserialize_with_field(json: &str, field: &str, value: Value) -> T { let mut fixture_json: Value = serde_json::from_str(json).unwrap(); fixture_json[field] = value; diff --git a/src/network/radarr_network/library/mod.rs b/src/network/radarr_network/library/mod.rs index e373c58a..cbc1027d 100644 --- a/src/network/radarr_network/library/mod.rs +++ b/src/network/radarr_network/library/mod.rs @@ -1,6 +1,6 @@ use crate::models::radarr_models::{ AddMovieBody, AddMovieSearchResult, Credit, CreditType, DeleteMovieParams, DownloadRecord, - EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease, + DownloadStatus, EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, }; use crate::models::servarr_data::Notification; @@ -664,11 +664,11 @@ pub(in crate::network::radarr_network::library) fn get_movie_status( .iter() .find(|&download| download.movie_id == movie_id) { - if download.status == "downloading" { + if download.status == DownloadStatus::Downloading { return "Downloading".to_owned(); } - if download.status == "completed" { + if download.status == DownloadStatus::Completed { return "Awaiting Import".to_owned(); } } diff --git a/src/network/radarr_network/library/radarr_library_network_tests.rs b/src/network/radarr_network/library/radarr_library_network_tests.rs index 941f0108..ca4f44d4 100644 --- a/src/network/radarr_network/library/radarr_library_network_tests.rs +++ b/src/network/radarr_network/library/radarr_library_network_tests.rs @@ -1,8 +1,9 @@ #[cfg(test)] mod tests { use crate::models::radarr_models::{ - AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, EditMovieParams, - MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrReleaseDownloadBody, + AddMovieBody, AddMovieOptions, Credit, DeleteMovieParams, DownloadRecord, DownloadStatus, + EditMovieParams, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, + RadarrReleaseDownloadBody, }; use crate::models::servarr_data::Notification; use crate::models::servarr_data::radarr::modals::MovieDetailsModal; @@ -1199,7 +1200,7 @@ mod tests { false, &[DownloadRecord { movie_id: 1, - status: "downloading".to_owned(), + status: DownloadStatus::Downloading, ..DownloadRecord::default() }], 1 @@ -1215,7 +1216,7 @@ mod tests { false, &[DownloadRecord { movie_id: 1, - status: "completed".to_owned(), + status: DownloadStatus::Completed, ..DownloadRecord::default() }], 1 diff --git a/src/network/radarr_network/radarr_network_test_utils.rs b/src/network/radarr_network/radarr_network_test_utils.rs index 81f11f23..80a719db 100644 --- a/src/network/radarr_network/radarr_network_test_utils.rs +++ b/src/network/radarr_network/radarr_network_test_utils.rs @@ -2,9 +2,10 @@ pub mod test_utils { use crate::models::radarr_models::{ AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit, - CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability, - Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, RadarrHistoryEventType, - RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, RatingsList, + CreditType, DownloadRecord, DownloadStatus, DownloadsResponse, IndexerSettings, MediaInfo, + MinimumAvailability, Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrHistoryData, + RadarrHistoryEventType, RadarrHistoryItem, RadarrRelease, RadarrTask, RadarrTaskName, Rating, + RatingsList, }; use crate::models::servarr_models::{ Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder, @@ -339,7 +340,7 @@ pub mod test_utils { pub fn download_record() -> DownloadRecord { DownloadRecord { title: "Test Download Title".to_owned(), - status: "downloading".to_owned(), + status: DownloadStatus::Downloading, id: 1, movie_id: 1, size: 3543348019, diff --git a/src/ui/radarr_ui/mod.rs b/src/ui/radarr_ui/mod.rs index 79561e95..a841f003 100644 --- a/src/ui/radarr_ui/mod.rs +++ b/src/ui/radarr_ui/mod.rs @@ -1,7 +1,7 @@ use crate::app::App; use crate::logos::RADARR_LOGO; use crate::models::Route; -use crate::models::radarr_models::{DownloadRecord, Movie}; +use crate::models::radarr_models::{DownloadRecord, DownloadStatus, Movie}; use crate::models::servarr_data::radarr::radarr_data::RadarrData; use crate::models::servarr_models::{DiskSpace, RootFolder}; use crate::ui::DrawUi; @@ -230,11 +230,11 @@ fn decorate_with_row_style<'a>( .iter() .find(|&download| download.movie_id == movie.id) { - if download.status == "downloading" { + if download.status == DownloadStatus::Downloading { return row.downloading(); } - if download.status == "completed" { + if download.status == DownloadStatus::Completed { return row.awaiting_import(); } } diff --git a/src/ui/radarr_ui/radarr_ui_tests.rs b/src/ui/radarr_ui/radarr_ui_tests.rs index 4c0838f3..680269cc 100644 --- a/src/ui/radarr_ui/radarr_ui_tests.rs +++ b/src/ui/radarr_ui/radarr_ui_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use crate::models::radarr_models::{DownloadRecord, Movie}; + use crate::models::radarr_models::{DownloadRecord, DownloadStatus, Movie}; use pretty_assertions::assert_eq; use ratatui::widgets::{Cell, Row}; use rstest::rstest; @@ -50,8 +50,20 @@ mod tests { } #[rstest] - #[case(false, Some("downloading"), false, "", RowStyle::Downloading)] - #[case(false, Some("completed"), false, "", RowStyle::AwaitingImport)] + #[case( + false, + Some(DownloadStatus::Downloading), + false, + "", + RowStyle::Downloading + )] + #[case( + false, + Some(DownloadStatus::Completed), + false, + "", + RowStyle::AwaitingImport + )] #[case(false, None, false, "", RowStyle::UnmonitoredMissing)] #[case(false, None, true, "", RowStyle::Unreleased)] #[case(false, None, true, "released", RowStyle::Missing)] @@ -59,7 +71,7 @@ mod tests { #[case(true, None, true, "", RowStyle::Downloaded)] fn test_decorate_with_row_style( #[case] has_file: bool, - #[case] download_status: Option<&str>, + #[case] download_status: Option, #[case] is_monitored: bool, #[case] movie_status: String, #[case] expected_style: RowStyle, @@ -67,7 +79,7 @@ mod tests { let downloads_vec = if let Some(download_status) = download_status { vec![DownloadRecord { movie_id: 1, - status: download_status.to_owned(), + status: download_status, ..DownloadRecord::default() }] } else {