feat: Added initial Sonarr CLI support and the initial network handler setup for the TUI
This commit is contained in:
+15
-1
@@ -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()
|
||||
|
||||
@@ -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 +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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user