feat: Added initial Sonarr CLI support and the initial network handler setup for the TUI

This commit is contained in:
2024-11-10 21:23:55 -07:00
parent b6f5b9d08c
commit 60d61b9e31
28 changed files with 2419 additions and 761 deletions
+15 -1
View File
@@ -6,8 +6,11 @@ use radarr_models::RadarrSerdeable;
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Number;
use servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use sonarr_models::SonarrSerdeable;
pub mod radarr_models;
pub mod servarr_data;
pub mod sonarr_models;
pub mod stateful_list;
pub mod stateful_table;
@@ -20,7 +23,7 @@ mod model_tests;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Route {
Radarr(ActiveRadarrBlock, Option<ActiveRadarrBlock>),
Sonarr,
Sonarr(ActiveSonarrBlock, Option<ActiveSonarrBlock>),
Readarr,
Lidarr,
Whisparr,
@@ -33,6 +36,7 @@ pub enum Route {
#[serde(untagged)]
pub enum Serdeable {
Radarr(RadarrSerdeable),
Sonarr(SonarrSerdeable),
}
pub trait Scrollable {
@@ -359,6 +363,16 @@ where
)))
}
pub fn from_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: Deserializer<'de>,
{
let num: Number = Deserialize::deserialize(deserializer)?;
num.as_f64().ok_or(de::Error::custom(format!(
"Unable to convert Number to f64: {num:?}"
)))
}
pub fn strip_non_search_characters(input: &str) -> String {
Regex::new(r"[^a-zA-Z0-9.,/'\-:\s]")
.unwrap()
+8
View File
@@ -10,6 +10,7 @@ mod tests {
use serde::de::IntoDeserializer;
use serde_json::to_string;
use crate::models::from_f64;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::{from_i64, strip_non_search_characters};
use crate::models::{
@@ -649,6 +650,13 @@ mod tests {
);
}
#[test]
fn test_from_f64() {
let deserializer: F64Deserializer<ValueError> = 1f64.into_deserializer();
assert_eq!(from_f64(deserializer), Ok(1.0));
}
#[test]
fn test_horizontally_scrollable_serialize() {
let text = HorizontallyScrollableText::from("Test");
+1
View File
@@ -1 +1,2 @@
pub mod radarr;
pub mod sonarr;
@@ -19,6 +19,14 @@ mod tests {
use crate::assert_movie_info_tabs_reset;
use crate::models::BlockSelectionState;
#[test]
fn test_from_active_radarr_block_to_route() {
assert_eq!(
Route::from(ActiveRadarrBlock::AddMoviePrompt),
Route::Radarr(ActiveRadarrBlock::AddMoviePrompt, None)
);
}
#[test]
fn test_from_tuple_to_route_with_context() {
assert_eq!(
@@ -60,7 +68,7 @@ mod tests {
assert_eq!(radarr_data.disk_space_vec, Vec::new());
assert!(radarr_data.version.is_empty());
assert_eq!(radarr_data.start_time, <DateTime<Utc>>::default());
assert!(radarr_data.movies.items.is_empty());
assert!(radarr_data.movies.is_empty());
assert_eq!(radarr_data.selected_block, BlockSelectionState::default());
assert!(radarr_data.downloads.items.is_empty());
assert!(radarr_data.indexers.items.is_empty());
+1
View File
@@ -0,0 +1 @@
pub mod sonarr_data;
@@ -0,0 +1,43 @@
use chrono::{DateTime, Utc};
use strum::EnumIter;
use crate::models::{sonarr_models::Series, stateful_table::StatefulTable, Route};
#[cfg(test)]
#[path = "sonarr_data_tests.rs"]
mod sonarr_data_tests;
pub struct SonarrData {
pub version: String,
pub start_time: DateTime<Utc>,
pub series: StatefulTable<Series>,
}
impl Default for SonarrData {
fn default() -> SonarrData {
SonarrData {
version: String::new(),
start_time: DateTime::default(),
series: StatefulTable::default(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, EnumIter)]
pub enum ActiveSonarrBlock {
#[default]
Series,
SeriesSortPrompt,
}
impl From<ActiveSonarrBlock> for Route {
fn from(active_sonarr_block: ActiveSonarrBlock) -> Route {
Route::Sonarr(active_sonarr_block, None)
}
}
impl From<(ActiveSonarrBlock, Option<ActiveSonarrBlock>)> for Route {
fn from(value: (ActiveSonarrBlock, Option<ActiveSonarrBlock>)) -> Route {
Route::Sonarr(value.0, value.1)
}
}
@@ -0,0 +1,42 @@
#[cfg(test)]
mod tests {
mod sonarr_data_tests {
use chrono::{DateTime, Utc};
use crate::models::{
servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, SonarrData},
Route,
};
#[test]
fn test_from_active_sonarr_block_to_route() {
assert_eq!(
Route::from(ActiveSonarrBlock::SeriesSortPrompt),
Route::Sonarr(ActiveSonarrBlock::SeriesSortPrompt, None)
);
}
#[test]
fn test_from_tuple_to_route_with_context() {
assert_eq!(
Route::from((
ActiveSonarrBlock::SeriesSortPrompt,
Some(ActiveSonarrBlock::Series)
)),
Route::Sonarr(
ActiveSonarrBlock::SeriesSortPrompt,
Some(ActiveSonarrBlock::Series),
)
);
}
#[test]
fn test_sonarr_data_defaults() {
let sonarr_data = SonarrData::default();
assert!(sonarr_data.version.is_empty());
assert_eq!(sonarr_data.start_time, <DateTime<Utc>>::default());
assert!(sonarr_data.series.is_empty());
}
}
}
+207
View File
@@ -0,0 +1,207 @@
use std::fmt::{Display, Formatter};
use chrono::{DateTime, Utc};
use clap::ValueEnum;
use derivative::Derivative;
use serde::{Deserialize, Serialize};
use serde_json::{json, Number, Value};
use strum::EnumIter;
use crate::serde_enum_from;
use super::{HorizontallyScrollableText, Serdeable};
#[cfg(test)]
#[path = "sonarr_models_tests.rs"]
mod sonarr_models_tests;
#[derive(Derivative, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[derivative(Default)]
pub struct Rating {
#[serde(deserialize_with = "super::from_i64")]
pub votes: i64,
#[serde(deserialize_with = "super::from_f64")]
pub value: f64,
}
impl Eq for Rating {}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Season {
#[serde(deserialize_with = "super::from_i64")]
pub season_number: i64,
pub monitored: bool,
pub statistics: SeasonStatistics,
}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SeasonStatistics {
pub next_airing: Option<DateTime<Utc>>,
pub previous_airing: Option<DateTime<Utc>>,
#[serde(deserialize_with = "super::from_i64")]
pub episode_file_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub episode_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub total_episode_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub size_on_disk: i64,
#[serde(deserialize_with = "super::from_f64")]
pub percent_of_episodes: f64,
}
impl Eq for SeasonStatistics {}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Series {
#[serde(deserialize_with = "super::from_i64")]
pub id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub tvdb_id: i64,
pub title: HorizontallyScrollableText,
#[serde(deserialize_with = "super::from_i64")]
pub quality_profile_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub language_profile_id: i64,
#[serde(deserialize_with = "super::from_i64")]
pub runtime: i64,
#[serde(deserialize_with = "super::from_i64")]
pub year: i64,
pub monitored: bool,
pub series_type: SeriesType,
pub path: String,
pub genres: Vec<String>,
pub tags: Vec<Number>,
pub ratings: Rating,
pub ended: bool,
pub status: SeriesStatus,
pub overview: String,
pub network: Option<String>,
pub season_folder: bool,
pub certification: Option<String>,
pub statistics: Option<SeriesStatistics>,
pub seasons: Option<Vec<Season>>,
}
#[derive(
Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter, ValueEnum,
)]
#[serde(rename_all = "camelCase")]
pub enum SeriesType {
#[default]
Standard,
Daily,
Anime,
}
impl Display for SeriesType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let series_type = match self {
SeriesType::Standard => "standard",
SeriesType::Daily => "daily",
SeriesType::Anime => "anime",
};
write!(f, "{series_type}")
}
}
impl SeriesType {
pub fn to_display_str<'a>(self) -> &'a str {
match self {
SeriesType::Standard => "Standard",
SeriesType::Daily => "Daily",
SeriesType::Anime => "Anime",
}
}
}
#[derive(Derivative, Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SeriesStatistics {
#[serde(deserialize_with = "super::from_i64")]
pub season_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub episode_file_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub episode_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub total_episode_count: i64,
#[serde(deserialize_with = "super::from_i64")]
pub size_on_disk: i64,
#[serde(deserialize_with = "super::from_f64")]
pub percent_of_episodes: f64,
}
impl Eq for SeriesStatistics {}
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy, Debug, EnumIter)]
#[serde(rename_all = "camelCase")]
pub enum SeriesStatus {
#[default]
Continuing,
Ended,
Upcoming,
Deleted,
}
impl Display for SeriesStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let series_status = match self {
SeriesStatus::Continuing => "continuing",
SeriesStatus::Ended => "ended",
SeriesStatus::Upcoming => "upcoming",
SeriesStatus::Deleted => "deleted",
};
write!(f, "{series_status}")
}
}
impl SeriesStatus {
pub fn to_display_str<'a>(self) -> &'a str {
match self {
SeriesStatus::Continuing => "Continuing",
SeriesStatus::Ended => "Ended",
SeriesStatus::Upcoming => "Upcoming",
SeriesStatus::Deleted => "Deleted",
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum SonarrSerdeable {
Value(Value),
SeriesVec(Vec<Series>),
SystemStatus(SystemStatus),
}
impl From<SonarrSerdeable> for Serdeable {
fn from(value: SonarrSerdeable) -> Serdeable {
Serdeable::Sonarr(value)
}
}
impl From<()> for SonarrSerdeable {
fn from(_: ()) -> Self {
SonarrSerdeable::Value(json!({}))
}
}
serde_enum_from!(
SonarrSerdeable {
Value(Value),
SeriesVec(Vec<Series>),
SystemStatus(SystemStatus),
}
);
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SystemStatus {
pub version: String,
pub start_time: DateTime<Utc>,
}
+92
View File
@@ -0,0 +1,92 @@
#[cfg(test)]
mod tests {
use pretty_assertions::{assert_eq, assert_str_eq};
use serde_json::json;
use crate::models::{
sonarr_models::{Series, SeriesStatus, SeriesType, SonarrSerdeable, SystemStatus},
Serdeable,
};
#[test]
fn test_series_status_display() {
assert_str_eq!(SeriesStatus::Continuing.to_string(), "continuing");
assert_str_eq!(SeriesStatus::Ended.to_string(), "ended");
assert_str_eq!(SeriesStatus::Upcoming.to_string(), "upcoming");
assert_str_eq!(SeriesStatus::Deleted.to_string(), "deleted");
}
#[test]
fn test_series_status_to_display_str() {
assert_str_eq!(SeriesStatus::Continuing.to_display_str(), "Continuing");
assert_str_eq!(SeriesStatus::Ended.to_display_str(), "Ended");
assert_str_eq!(SeriesStatus::Upcoming.to_display_str(), "Upcoming");
assert_str_eq!(SeriesStatus::Deleted.to_display_str(), "Deleted");
}
#[test]
fn test_series_type_display() {
assert_str_eq!(SeriesType::Standard.to_string(), "standard");
assert_str_eq!(SeriesType::Daily.to_string(), "daily");
assert_str_eq!(SeriesType::Anime.to_string(), "anime");
}
#[test]
fn test_series_type_to_display_str() {
assert_str_eq!(SeriesType::Standard.to_display_str(), "Standard");
assert_str_eq!(SeriesType::Daily.to_display_str(), "Daily");
assert_str_eq!(SeriesType::Anime.to_display_str(), "Anime");
}
#[test]
fn test_sonarr_serdeable_from() {
let sonarr_serdeable = SonarrSerdeable::Value(json!({}));
let serdeable: Serdeable = Serdeable::from(sonarr_serdeable.clone());
assert_eq!(serdeable, Serdeable::Sonarr(sonarr_serdeable));
}
#[test]
fn test_sonarr_serdeable_from_unit() {
let sonarr_serdeable = SonarrSerdeable::from(());
assert_eq!(sonarr_serdeable, SonarrSerdeable::Value(json!({})));
}
#[test]
fn test_sonarr_serdeable_from_value() {
let value = json!({"test": "test"});
let sonarr_serdeable: SonarrSerdeable = value.clone().into();
assert_eq!(sonarr_serdeable, SonarrSerdeable::Value(value));
}
#[test]
fn test_sonarr_serdeable_from_series() {
let series = vec![Series {
id: 1,
..Series::default()
}];
let sonarr_serdeable: SonarrSerdeable = series.clone().into();
assert_eq!(sonarr_serdeable, SonarrSerdeable::SeriesVec(series));
}
#[test]
fn test_sonarr_serdeable_from_system_status() {
let system_status = SystemStatus {
version: "1".to_owned(),
..SystemStatus::default()
};
let sonarr_serdeable: SonarrSerdeable = system_status.clone().into();
assert_eq!(
sonarr_serdeable,
SonarrSerdeable::SystemStatus(system_status)
);
}
}