diff --git a/CHANGELOG.md b/CHANGELOG.md index 1761ef9..2aadb1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v0.6.3 (2025-12-13) + +### Fix + +- Wrapped all Sonarr use of Language with Option to fix the 'null' array issue in the new Sonarr API + +## v0.6.2 (2025-12-12) + +### Fix + +- Fixed breaking Sonarr Episode file API calls after recent Sonarr API update + +### Refactor + +- Replaced all modulo usages of tick_until_poll with is_multiple_of + ## v0.6.1 (2025-09-02) ### Fix diff --git a/Cargo.toml b/Cargo.toml index 774ad50..d39823f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managarr" -version = "0.6.1" +version = "0.6.3" authors = ["Alex Clarke "] description = "A TUI and CLI to manage your Servarrs" keywords = ["managarr", "ratatui", "dashboard", "servarr", "tui"] diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8d0c6d2..5eb9c37 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, command}; +use clap::{command, Subcommand}; use clap_complete::Shell; use radarr::{RadarrCliHandler, RadarrCommand}; use sonarr::{SonarrCliHandler, SonarrCommand}; diff --git a/src/cli/radarr/add_command_handler.rs b/src/cli/radarr/add_command_handler.rs index f0e73bf..ac1596d 100644 --- a/src/cli/radarr/add_command_handler.rs +++ b/src/cli/radarr/add_command_handler.rs @@ -10,7 +10,7 @@ use crate::{ app::App, cli::{CliCommandHandler, Command}, models::radarr_models::{AddMovieBody, AddMovieOptions, MinimumAvailability, MovieMonitor}, - network::{NetworkTrait, radarr_network::RadarrEvent}, + network::{radarr_network::RadarrEvent, NetworkTrait}, }; #[cfg(test)] diff --git a/src/cli/radarr/get_command_handler.rs b/src/cli/radarr/get_command_handler.rs index 7233146..08016ea 100644 --- a/src/cli/radarr/get_command_handler.rs +++ b/src/cli/radarr/get_command_handler.rs @@ -1,13 +1,13 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, command}; +use clap::{command, Subcommand}; use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - network::{NetworkTrait, radarr_network::RadarrEvent}, + network::{radarr_network::RadarrEvent, NetworkTrait}, }; use super::RadarrCommand; diff --git a/src/cli/radarr/list_command_handler.rs b/src/cli/radarr/list_command_handler.rs index 4e613c9..3fd1575 100644 --- a/src/cli/radarr/list_command_handler.rs +++ b/src/cli/radarr/list_command_handler.rs @@ -1,13 +1,13 @@ use std::sync::Arc; use anyhow::Result; -use clap::{Subcommand, command}; +use clap::{command, Subcommand}; use tokio::sync::Mutex; use crate::{ app::App, cli::{CliCommandHandler, Command}, - network::{NetworkTrait, radarr_network::RadarrEvent}, + network::{radarr_network::RadarrEvent, NetworkTrait}, }; use super::RadarrCommand; diff --git a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs index 2a89195..06c6ada 100644 --- a/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs +++ b/src/handlers/sonarr_handlers/blocklist/blocklist_handler_tests.rs @@ -439,13 +439,25 @@ mod tests { let a_languages = a .languages .iter() - .map(|lang| lang.name.to_lowercase()) + .map(|lang| { + lang + .as_ref() + .unwrap_or(&Default::default()) + .name + .to_lowercase() + }) .collect::>() .join(", "); let b_languages = b .languages .iter() - .map(|lang| lang.name.to_lowercase()) + .map(|lang| { + lang + .as_ref() + .unwrap_or(&Default::default()) + .name + .to_lowercase() + }) .collect::>() .join(", "); @@ -601,10 +613,10 @@ mod tests { BlocklistItem { id: 3, source_title: "test 1".to_owned(), - languages: vec![Language { + languages: vec![Some(Language { id: 1, name: "telgu".to_owned(), - }], + })], quality: QualityWrapper { quality: Quality { name: "HD - 1080p".to_owned(), @@ -617,10 +629,10 @@ mod tests { BlocklistItem { id: 2, source_title: "test 2".to_owned(), - languages: vec![Language { + languages: vec![Some(Language { id: 3, name: "chinese".to_owned(), - }], + })], quality: QualityWrapper { quality: Quality { name: "SD - 720p".to_owned(), @@ -633,10 +645,10 @@ mod tests { BlocklistItem { id: 1, source_title: "test 3".to_owned(), - languages: vec![Language { + languages: vec![Some(Language { id: 1, name: "english".to_owned(), - }], + })], quality: QualityWrapper { quality: Quality { name: "HD - 1080p".to_owned(), diff --git a/src/handlers/sonarr_handlers/blocklist/mod.rs b/src/handlers/sonarr_handlers/blocklist/mod.rs index fa08c85..2989042 100644 --- a/src/handlers/sonarr_handlers/blocklist/mod.rs +++ b/src/handlers/sonarr_handlers/blocklist/mod.rs @@ -214,13 +214,25 @@ fn blocklist_sorting_options() -> Vec> { let a_languages = a .languages .iter() - .map(|lang| lang.name.to_lowercase()) + .map(|lang| { + lang + .as_ref() + .unwrap_or(&Default::default()) + .name + .to_lowercase() + }) .collect::>() .join(", "); let b_languages = b .languages .iter() - .map(|lang| lang.name.to_lowercase()) + .map(|lang| { + lang + .as_ref() + .unwrap_or(&Default::default()) + .name + .to_lowercase() + }) .collect::>() .join(", "); diff --git a/src/handlers/sonarr_handlers/history/history_handler_tests.rs b/src/handlers/sonarr_handlers/history/history_handler_tests.rs index c09d406..4a34d17 100644 --- a/src/handlers/sonarr_handlers/history/history_handler_tests.rs +++ b/src/handlers/sonarr_handlers/history/history_handler_tests.rs @@ -246,8 +246,19 @@ mod tests { id: 1, name: "_".to_owned(), }; - let language_a = &a.languages.first().unwrap_or(&default_language); - let language_b = &b.languages.first().unwrap_or(&default_language); + let default_language_option = Some(default_language.clone()); + let language_a = &a + .languages + .first() + .unwrap_or(&default_language_option) + .as_ref() + .unwrap_or(&default_language); + let language_b = &b + .languages + .first() + .unwrap_or(&default_language_option) + .as_ref() + .unwrap_or(&default_language); language_a.cmp(language_b) }; @@ -386,10 +397,10 @@ mod tests { id: 3, source_title: "test 1".into(), event_type: SonarrHistoryEventType::Grabbed, - languages: vec![Language { + languages: vec![Some(Language { id: 1, name: "telgu".to_owned(), - }], + })], quality: QualityWrapper { quality: Quality { name: "HD - 1080p".to_owned(), @@ -402,10 +413,10 @@ mod tests { id: 2, source_title: "test 2".into(), event_type: SonarrHistoryEventType::DownloadFolderImported, - languages: vec![Language { + languages: vec![Some(Language { id: 3, name: "chinese".to_owned(), - }], + })], quality: QualityWrapper { quality: Quality { name: "SD - 720p".to_owned(), @@ -418,10 +429,10 @@ mod tests { id: 1, source_title: "test 3".into(), event_type: SonarrHistoryEventType::EpisodeFileDeleted, - languages: vec![Language { + languages: vec![Some(Language { id: 1, name: "english".to_owned(), - }], + })], quality: QualityWrapper { quality: Quality { name: "HD - 1080p".to_owned(), diff --git a/src/handlers/sonarr_handlers/history/mod.rs b/src/handlers/sonarr_handlers/history/mod.rs index 58563d1..049de25 100644 --- a/src/handlers/sonarr_handlers/history/mod.rs +++ b/src/handlers/sonarr_handlers/history/mod.rs @@ -154,8 +154,19 @@ pub(in crate::handlers::sonarr_handlers) fn history_sorting_options() id: 1, name: "_".to_owned(), }; - let language_a = &a.languages.first().unwrap_or(&default_language); - let language_b = &b.languages.first().unwrap_or(&default_language); + let default_language_option = Some(default_language.clone()); + let language_a = &a + .languages + .first() + .unwrap_or(&default_language_option) + .as_ref() + .unwrap_or(&default_language); + let language_b = &b + .languages + .first() + .unwrap_or(&default_language_option) + .as_ref() + .unwrap_or(&default_language); language_a.cmp(language_b) }), diff --git a/src/handlers/sonarr_handlers/library/season_details_handler.rs b/src/handlers/sonarr_handlers/library/season_details_handler.rs index 84aeb53..193c3ff 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler.rs @@ -518,12 +518,17 @@ pub(in crate::handlers::sonarr_handlers::library) fn releases_sorting_options() SortOption { name: "Language", cmp_fn: Some(|a, b| { - let default_language_vec = vec![Language { + let default_language = Language { id: 1, name: "_".to_owned(), - }]; - let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0]; - let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0]; + }; + let default_language_vec = vec![Some(default_language.clone())]; + let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0] + .as_ref() + .unwrap_or(&default_language); + let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0] + .as_ref() + .unwrap_or(&default_language); language_a.cmp(language_b) }), diff --git a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs index c2d54d3..35b4c70 100644 --- a/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs +++ b/src/handlers/sonarr_handlers/library/season_details_handler_tests.rs @@ -1097,12 +1097,17 @@ mod tests { #[test] fn test_releases_sorting_options_language() { let expected_cmp_fn: fn(&SonarrRelease, &SonarrRelease) -> Ordering = |a, b| { - let default_language_vec = vec![Language { + let default_language = Language { id: 1, name: "_".to_owned(), - }]; - let language_a = &a.languages.as_ref().unwrap_or(&default_language_vec)[0]; - let language_b = &b.languages.as_ref().unwrap_or(&default_language_vec)[0]; + }; + let default_language_vec = vec![Some(default_language.clone())]; + let language_a = a.languages.as_ref().unwrap_or(&default_language_vec)[0] + .as_ref() + .unwrap_or(&default_language); + let language_b = b.languages.as_ref().unwrap_or(&default_language_vec)[0] + .as_ref() + .unwrap_or(&default_language); language_a.cmp(language_b) }; @@ -1141,10 +1146,10 @@ mod tests { size: 1, rejected: true, seeders: Some(Number::from(1)), - languages: Some(vec![Language { + languages: Some(vec![Some(Language { id: 1, name: "Language A".to_owned(), - }]), + })]), quality: QualityWrapper { quality: Quality { name: "Quality A".to_owned(), @@ -1160,10 +1165,10 @@ mod tests { size: 2, rejected: false, seeders: Some(Number::from(2)), - languages: Some(vec![Language { + languages: Some(vec![Some(Language { id: 2, name: "Language B".to_owned(), - }]), + })]), quality: QualityWrapper { quality: Quality { name: "Quality B".to_owned(), diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 1f22ccd..3dda720 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -84,7 +84,7 @@ pub struct BlocklistItem { pub series_title: Option, pub episode_ids: Vec, pub source_title: String, - pub languages: Vec, + pub languages: Vec>, pub quality: QualityWrapper, pub date: DateTime, pub protocol: String, @@ -509,7 +509,7 @@ pub struct SonarrHistoryItem { #[serde(deserialize_with = "super::from_i64")] pub episode_id: i64, pub quality: QualityWrapper, - pub languages: Vec, + pub languages: Vec>, pub date: DateTime, pub event_type: SonarrHistoryEventType, pub data: SonarrHistoryData, @@ -545,7 +545,7 @@ pub struct SonarrRelease { pub rejections: Option>, pub seeders: Option, pub leechers: Option, - pub languages: Option>, + pub languages: Option>>, pub quality: QualityWrapper, pub full_season: bool, } diff --git a/src/network/sonarr_network/sonarr_network_test_utils.rs b/src/network/sonarr_network/sonarr_network_test_utils.rs index c03b3c2..d8eb8f5 100644 --- a/src/network/sonarr_network/sonarr_network_test_utils.rs +++ b/src/network/sonarr_network/sonarr_network_test_utils.rs @@ -123,7 +123,7 @@ pub(in crate::network::sonarr_network) mod test_utils { series_title: None, episode_ids: vec![Number::from(1)], source_title: "Test Source Title".to_owned(), - languages: vec![language()], + languages: vec![Some(language())], quality: quality_wrapper(), date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), protocol: "usenet".to_owned(), @@ -206,7 +206,7 @@ pub(in crate::network::sonarr_network) mod test_utils { source_title: "Test source".into(), episode_id: 1, quality: quality_wrapper(), - languages: vec![language()], + languages: vec![Some(language())], date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), event_type: SonarrHistoryEventType::Grabbed, data: history_data(), @@ -377,7 +377,7 @@ pub(in crate::network::sonarr_network) mod test_utils { rejections: Some(rejections()), seeders: Some(Number::from(2)), leechers: Some(Number::from(1)), - languages: Some(vec![language()]), + languages: Some(vec![Some(language())]), quality: quality_wrapper(), full_season: false, } diff --git a/src/ui/sonarr_ui/blocklist/mod.rs b/src/ui/sonarr_ui/blocklist/mod.rs index 4deaa18..6eb7f07 100644 --- a/src/ui/sonarr_ui/blocklist/mod.rs +++ b/src/ui/sonarr_ui/blocklist/mod.rs @@ -90,7 +90,7 @@ fn draw_blocklist_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { let title = series_title.as_ref().unwrap_or(&String::new()).to_owned(); let languages_string = languages .iter() - .map(|lang| lang.name.to_owned()) + .map(|lang| lang.as_ref().unwrap_or(&Default::default()).name.to_owned()) .collect::>() .join(", "); diff --git a/src/ui/sonarr_ui/history/mod.rs b/src/ui/sonarr_ui/history/mod.rs index 18fe23d..20747ea 100644 --- a/src/ui/sonarr_ui/history/mod.rs +++ b/src/ui/sonarr_ui/history/mod.rs @@ -1,6 +1,7 @@ use crate::app::App; use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, HISTORY_BLOCKS}; +use crate::models::servarr_models::Language; use crate::models::sonarr_models::{SonarrHistoryEventType, SonarrHistoryItem}; use crate::ui::DrawUi; use crate::ui::styles::ManagarrStyle; @@ -77,7 +78,13 @@ fn draw_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from( languages .iter() - .map(|language| language.name.to_owned()) + .map(|language| { + language + .as_ref() + .unwrap_or(&Language::default()) + .name + .to_owned() + }) .collect::>() .join(","), ), diff --git a/src/ui/sonarr_ui/library/episode_details_ui.rs b/src/ui/sonarr_ui/library/episode_details_ui.rs index aa6d4a4..afc7dbf 100644 --- a/src/ui/sonarr_ui/library/episode_details_ui.rs +++ b/src/ui/sonarr_ui/library/episode_details_ui.rs @@ -1,6 +1,7 @@ use crate::app::App; use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, EPISODE_DETAILS_BLOCKS}; +use crate::models::servarr_models::Language; use crate::models::sonarr_models::{ DownloadRecord, DownloadStatus, Episode, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, }; @@ -289,7 +290,13 @@ fn draw_episode_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) Cell::from( languages .iter() - .map(|language| language.name.to_owned()) + .map(|language| { + language + .as_ref() + .unwrap_or(&Language::default()) + .name + .to_owned() + }) .collect::>() .join(","), ), @@ -450,7 +457,11 @@ fn draw_episode_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { }; let language = if languages.is_some() { - languages.clone().unwrap()[0].name.clone() + languages.clone().unwrap()[0] + .as_ref() + .unwrap_or(&Default::default()) + .name + .clone() } else { String::new() }; diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs index e5f47ee..aea91bd 100644 --- a/src/ui/sonarr_ui/library/season_details_ui.rs +++ b/src/ui/sonarr_ui/library/season_details_ui.rs @@ -280,7 +280,13 @@ fn draw_season_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from( languages .iter() - .map(|language| language.name.to_owned()) + .map(|language| { + language + .as_ref() + .unwrap_or(&Default::default()) + .name + .to_owned() + }) .collect::>() .join(","), ), @@ -402,7 +408,11 @@ fn draw_season_releases(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { }; let language = if languages.is_some() { - languages.clone().unwrap()[0].name.clone() + languages.clone().unwrap()[0] + .as_ref() + .unwrap_or(&Default::default()) + .name + .clone() } else { String::new() }; diff --git a/src/ui/sonarr_ui/library/series_details_ui.rs b/src/ui/sonarr_ui/library/series_details_ui.rs index de5aead..bcdaf45 100644 --- a/src/ui/sonarr_ui/library/series_details_ui.rs +++ b/src/ui/sonarr_ui/library/series_details_ui.rs @@ -324,7 +324,13 @@ fn draw_series_history_table(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) { Cell::from( languages .iter() - .map(|language| language.name.to_owned()) + .map(|language| { + language + .as_ref() + .unwrap_or(&Default::default()) + .name + .to_owned() + }) .collect::>() .join(","), ),