//! Sonarr API client library //! //! This crate provides a Rust client for the Sonarr API, allowing you to interact //! with Sonarr instances programmatically. pub mod error; pub use error::{ApiError, Result}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Clone)] pub struct SonarrClient { client: Client, base_url: String, api_key: String, } impl SonarrClient { pub fn new(base_url: String, api_key: String) -> Self { Self { client: Client::new(), base_url: base_url.trim_end_matches('/').to_string(), api_key, } } async fn get Deserialize<'de>>(&self, endpoint: &str) -> Result { let url = format!("{}/api/v3{}", self.base_url, endpoint); let response = self .client .get(&url) .header("X-Api-Key", &self.api_key) .send() .await?; if !response.status().is_success() { let status = response.status(); let error_text = response .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); return Err(ApiError::Generic { message: format!("HTTP {}: {}", status, error_text), }); } let text = response.text().await?; let result: T = serde_json::from_str(&text)?; Ok(result) } #[allow(dead_code)] async fn get_debug Deserialize<'de>>(&self, endpoint: &str) -> Result { let url = format!("{}/api/v3{}", self.base_url, endpoint); let response = self .client .get(&url) .header("X-Api-Key", &self.api_key) .send() .await?; if !response.status().is_success() { let status = response.status(); let error_text = response .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); return Err(ApiError::Generic { message: format!("Debug HTTP {}: {}", status, error_text), }); } let text = response.text().await?; let _ = std::fs::write(endpoint.replace("/", "_"), &text); let result: T = serde_json::from_str(&text)?; Ok(result) } #[allow(dead_code)] async fn post Deserialize<'de>>( &self, endpoint: &str, body: &T, ) -> Result { let url = format!("{}/api/v3{}", self.base_url, endpoint); let response = self .client .post(&url) .header("X-Api-Key", &self.api_key) .json(body) .send() .await?; if !response.status().is_success() { let status = response.status(); let error_text = response .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); return Err(ApiError::Generic { message: format!("POST HTTP {}: {}", status, error_text), }); } let text = response.text().await?; let result: R = serde_json::from_str(&text)?; Ok(result) } pub async fn get_system_status(&self) -> Result { self.get("/system/status").await } pub async fn get_series(&self) -> Result, ApiError> { self.get("/series").await } #[allow(dead_code)] pub async fn get_series_by_id(&self, id: u32) -> Result { self.get(&format!("/series/{}", id)).await } #[allow(dead_code)] pub async fn get_episodes( &self, series_id: Option, season_number: Option, ) -> Result, ApiError> { let mut query = Vec::new(); if let Some(id) = series_id { query.push(format!("seriesId={}", id)); } if let Some(season) = season_number { query.push(format!("seasonNumber={}", season)); } let query_string = if query.is_empty() { String::new() } else { format!("?{}", query.join("&")) }; self.get(&format!("/episode{}", query_string)).await } pub async fn get_calendar( &self, start: Option<&str>, end: Option<&str>, ) -> Result, ApiError> { let mut query = Vec::new(); if let Some(start_date) = start { query.push(format!("start={}", start_date)); } if let Some(end_date) = end { query.push(format!("end={}", end_date)); } let query_string = if query.is_empty() { String::new() } else { format!("?{}", query.join("&")) }; self.get(&format!("/calendar{}", query_string)).await } pub async fn get_queue(&self) -> Result { self.get("/queue").await } pub async fn get_history(&self) -> Result { self.get("/history").await } #[allow(dead_code)] pub async fn get_missing_episodes(&self) -> Result { self.get("/wanted/missing").await } pub async fn get_health(&self) -> Result, ApiError> { self.get("/health").await } pub async fn search_series(&self, term: &str) -> Result, ApiError> { self.get(&format!( "/series/lookup?term={}", urlencoding::encode(term) )) .await } #[allow(dead_code)] pub async fn add_series(&self, series: &Series) -> Result { self.post("/series", series).await } } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SystemStatus { pub app_name: Option, pub instance_name: Option, pub version: Option, pub build_time: chrono::DateTime, pub is_debug: bool, pub is_production: bool, pub is_admin: bool, pub is_user_interactive: bool, pub startup_path: Option, pub app_data: Option, pub os_name: Option, pub os_version: Option, pub is_net_core: bool, pub is_linux: bool, pub is_osx: bool, pub is_windows: bool, pub is_docker: bool, pub mode: String, pub branch: Option, pub authentication: String, pub sqlite_version: Option, pub migration_version: i32, pub url_base: Option, pub runtime_version: Option, pub runtime_name: Option, pub start_time: chrono::DateTime, pub package_version: Option, pub package_author: Option, pub package_update_mechanism: String, pub package_update_mechanism_message: Option, pub database_version: Option, pub database_type: String, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Series { pub id: Option, pub title: Option, pub alternate_titles: Option>, pub sort_title: Option, pub status: String, pub ended: Option, pub profile_name: Option, pub overview: Option, pub next_airing: Option>, pub previous_airing: Option>, pub network: Option, pub air_time: Option, pub images: Option>, pub original_language: Option, pub remote_poster: Option, pub seasons: Option>, pub year: u32, pub path: Option, pub quality_profile_id: u32, pub season_folder: bool, pub monitored: bool, pub monitor_new_items: String, pub use_scene_numbering: bool, pub runtime: u32, pub tvdb_id: u32, pub tv_rage_id: u32, pub tv_maze_id: u32, pub tmdb_id: u32, pub first_aired: Option>, pub last_aired: Option>, pub series_type: String, pub clean_title: Option, pub imdb_id: Option, pub title_slug: Option, pub root_folder_path: Option, pub folder: Option, pub certification: Option, pub genres: Option>, pub tags: Option>, pub added: chrono::DateTime, pub add_options: Option, pub ratings: Option, pub statistics: Option, pub episodes_changed: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AlternateTitle { pub title: Option, pub season_number: Option, pub scene_season_number: Option, pub scene_origin: Option, pub comment: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MediaCover { pub cover_type: String, pub url: Option, pub remote_url: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Language { pub id: u32, pub name: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Season { pub season_number: i32, pub monitored: bool, pub statistics: Option, pub images: Option>, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AddSeriesOptions { pub ignore_episodes_with_files: bool, pub ignore_episodes_without_files: bool, pub monitor: String, pub search_for_missing_episodes: bool, pub search_for_cutoff_unmet_episodes: bool, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Ratings { pub votes: i32, pub value: f64, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SeriesStatistics { pub season_count: i32, pub episode_file_count: i32, pub episode_count: i32, pub total_episode_count: i32, pub size_on_disk: i64, pub release_groups: Option>, pub percent_of_episodes: f64, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SeasonStatistics { pub next_airing: Option>, pub previous_airing: Option>, pub episode_file_count: i32, pub episode_count: i32, pub total_episode_count: i32, pub size_on_disk: i64, pub release_groups: Option>, pub percent_of_episodes: f64, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Episode { pub id: u32, pub series_id: u32, pub tvdb_id: u32, pub episode_file_id: u32, pub season_number: i32, pub episode_number: i32, pub title: Option, pub air_date: Option, pub air_date_utc: Option>, pub last_search_time: Option>, pub runtime: i32, pub finale_type: Option, pub overview: Option, pub episode_file: Option, pub has_file: bool, pub monitored: bool, pub absolute_episode_number: Option, pub scene_absolute_episode_number: Option, pub scene_episode_number: Option, pub scene_season_number: Option, pub unverified_scene_numbering: bool, pub end_time: Option>, pub grab_date: Option>, pub series: Option, pub images: Option>, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EpisodeFile { pub id: u32, pub series_id: u32, pub season_number: i32, pub relative_path: Option, pub path: Option, pub size: i64, pub date_added: chrono::DateTime, pub scene_name: Option, pub release_group: Option, pub languages: Option>, pub quality: Option, pub custom_formats: Option>, pub custom_format_score: i32, pub indexer_flags: Option, pub release_type: Option, pub media_info: Option, pub quality_cutoff_not_met: bool, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Quality { pub quality: QualityDefinition, pub revision: Revision, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct QualityDefinition { pub id: u32, pub name: Option, pub source: String, pub resolution: i32, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Revision { pub version: i32, pub real: i32, pub is_repack: bool, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CustomFormat { pub id: u32, pub name: Option, pub include_custom_format_when_renaming: Option, pub specifications: Option>>, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MediaInfo { pub id: u32, pub audio_bitrate: i64, pub audio_channels: f64, pub audio_codec: Option, pub audio_languages: Option, pub audio_stream_count: i32, pub video_bit_depth: i32, pub video_bitrate: i64, pub video_codec: Option, pub video_fps: f64, pub video_dynamic_range: Option, pub video_dynamic_range_type: Option, pub resolution: Option, pub run_time: Option, pub scan_type: Option, pub subtitles: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct QueuePagingResource { pub page: i32, pub page_size: i32, pub sort_key: Option, pub sort_direction: String, pub total_records: i32, pub records: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct QueueItem { pub id: u32, pub series_id: Option, pub episode_id: Option, pub season_number: Option, pub series: Option, pub episode: Option, pub languages: Option>, pub quality: Option, pub custom_formats: Option>, pub custom_format_score: i32, pub size: f64, pub title: Option, pub estimated_completion_time: Option>, pub added: Option>, pub status: String, pub tracked_download_status: Option, pub tracked_download_state: Option, pub status_messages: Option>, pub error_message: Option, pub download_id: Option, pub protocol: String, pub download_client: Option, pub download_client_has_post_import_category: bool, pub indexer: Option, pub output_path: Option, pub episode_has_file: bool, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct StatusMessage { pub title: Option, pub messages: Option>, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct HistoryPagingResource { pub page: i32, pub page_size: i32, pub sort_key: Option, pub sort_direction: String, pub total_records: i32, pub records: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct HistoryItem { pub id: u32, pub episode_id: u32, pub series_id: u32, pub source_title: Option, pub languages: Option>, pub quality: Option, pub custom_formats: Option>, pub custom_format_score: i32, pub quality_cutoff_not_met: bool, pub date: chrono::DateTime, pub download_id: Option, pub event_type: Option, pub data: Option>>, pub episode: Option, pub series: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EpisodePagingResource { pub page: i32, pub page_size: i32, pub sort_key: Option, pub sort_direction: String, pub total_records: i32, pub records: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct HealthResource { pub id: Option, pub source: Option, #[serde(rename = "type")] pub health_type: String, pub message: Option, pub wiki_url: Option, }