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.
This commit is contained in:
Dark-Alex-17 committed 2026-09-17 15:46:49 -06:00
1 parent 25da032f02
commit 7b6d595397
8 files changed
+126 -29

No files matched your search

@@ -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,
+32 -1
View File
@@ -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 {
+58 -6
View File
@@ -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::<DownloadRecord>(&download_record_json, "status", json!(2)),
expected
);
assert_eq!(
deserialize_with_field::<DownloadRecord>(&download_record_json, "status", json!("notAThing")),
expected
);
}
fn deserialize_with_field<T: DeserializeOwned>(json: &str, field: &str, value: Value) -> T {
let mut fixture_json: Value = serde_json::from_str(json).unwrap();
fixture_json[field] = value;
+3 -3
View File
@@ -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();
}
}
@@ -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
@@ -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,
+3 -3
View File
@@ -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();
}
}
+17 -5
View File
@@ -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<DownloadStatus>,
#[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 {