Sonarr v3 shim for Jellyseerr (#84)
This commit was merged in pull request #84.
This commit is contained in:
@@ -8,7 +8,10 @@ use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::cache::Cache;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::model::{Movie, MovieSearchResult, RawFindPage, RawMovie, RawSearchPage};
|
||||
use crate::model::{
|
||||
Movie, MovieSearchResult, RawFindPage, RawMovie, RawSearchPage, RawSeason, RawSeries,
|
||||
RawSeriesSearchPage, Season, Series, SeriesSearchResult,
|
||||
};
|
||||
|
||||
/// TMDB's v3 API root.
|
||||
pub const DEFAULT_BASE_URL: &str = "https://api.themoviedb.org/3/";
|
||||
@@ -101,6 +104,18 @@ impl TmdbClient {
|
||||
Ok(page.movie_results.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
/// Resolve a TVDB series id through TMDB's external-id index.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Any of [`Error`]; see its variants for what callers should distinguish.
|
||||
pub async fn find_series_by_tvdb(&self, tvdb_id: u32) -> Result<Vec<SeriesSearchResult>> {
|
||||
let path = format!("find/{tvdb_id}");
|
||||
let params = [("external_source", "tvdb_id".to_owned())];
|
||||
let page: RawFindPage = self.get_json(&path, ¶ms).await?;
|
||||
Ok(page.tv_results.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
/// Full detail for one movie, including the digital release date.
|
||||
///
|
||||
/// One HTTP call: release dates come back appended to the same response
|
||||
@@ -116,6 +131,43 @@ impl TmdbClient {
|
||||
Ok(raw.into())
|
||||
}
|
||||
|
||||
/// Search TMDB for TV series by title.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Any of [`Error`]; see its variants for what callers should distinguish.
|
||||
pub async fn search_series(&self, query: &str) -> Result<Vec<SeriesSearchResult>> {
|
||||
let params = [
|
||||
("query", query.trim().to_owned()),
|
||||
("include_adult", "false".to_owned()),
|
||||
];
|
||||
let page: RawSeriesSearchPage = self.get_json("search/tv", ¶ms).await?;
|
||||
Ok(page.results.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
/// Full detail for one TV series, including its season summaries.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`Error::NotFound`] when TMDB has no such id, otherwise any of [`Error`].
|
||||
pub async fn series(&self, tmdb_id: u32) -> Result<Series> {
|
||||
let path = format!("tv/{tmdb_id}");
|
||||
let params = [("append_to_response", "external_ids".to_owned())];
|
||||
let raw: RawSeries = self.get_json(&path, ¶ms).await?;
|
||||
Ok(raw.into())
|
||||
}
|
||||
|
||||
/// Episodes in one TV season.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`Error::NotFound`] when TMDB has no such season, otherwise any of [`Error`].
|
||||
pub async fn season(&self, tmdb_id: u32, season_number: u32) -> Result<Season> {
|
||||
let path = format!("tv/{tmdb_id}/season/{season_number}");
|
||||
let raw: RawSeason = self.get_json(&path, &[]).await?;
|
||||
Ok(raw.into())
|
||||
}
|
||||
|
||||
/// Drop every cached response. For a user-initiated "refresh metadata".
|
||||
pub fn clear_cache(&self) {
|
||||
self.cache.clear();
|
||||
|
||||
@@ -23,4 +23,4 @@ mod model;
|
||||
|
||||
pub use client::{TmdbClient, TmdbClientBuilder, DEFAULT_BASE_URL, DEFAULT_CACHE_TTL};
|
||||
pub use error::{Error, Result};
|
||||
pub use model::{Movie, MovieSearchResult};
|
||||
pub use model::{Episode, Movie, MovieSearchResult, Season, Series, SeriesSearchResult};
|
||||
|
||||
@@ -97,6 +97,62 @@ impl Movie {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SeriesSearchResult {
|
||||
pub tmdb_id: u32,
|
||||
pub title: String,
|
||||
pub original_language: String,
|
||||
pub first_air_date: Option<NaiveDate>,
|
||||
pub overview: Option<String>,
|
||||
pub poster_path: Option<String>,
|
||||
}
|
||||
|
||||
impl SeriesSearchResult {
|
||||
#[must_use]
|
||||
pub fn year(&self) -> Option<i32> {
|
||||
self.first_air_date.map(|date| date.year())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Series {
|
||||
pub tmdb_id: u32,
|
||||
pub tvdb_id: Option<u32>,
|
||||
pub title: String,
|
||||
pub original_language: String,
|
||||
pub first_air_date: Option<NaiveDate>,
|
||||
pub status: String,
|
||||
pub overview: Option<String>,
|
||||
pub poster_path: Option<String>,
|
||||
pub seasons: Vec<SeasonSummary>,
|
||||
}
|
||||
|
||||
impl Series {
|
||||
#[must_use]
|
||||
pub fn year(&self) -> Option<i32> {
|
||||
self.first_air_date.map(|date| date.year())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SeasonSummary {
|
||||
pub number: u32,
|
||||
pub episode_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Season {
|
||||
pub number: u32,
|
||||
pub episodes: Vec<Episode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Episode {
|
||||
pub number: u32,
|
||||
pub title: String,
|
||||
pub air_date: Option<NaiveDate>,
|
||||
}
|
||||
|
||||
// --- TMDB wire types -------------------------------------------------------
|
||||
//
|
||||
// Private on purpose. TMDB's field names stop here.
|
||||
@@ -107,10 +163,135 @@ pub(crate) struct RawSearchPage {
|
||||
pub(crate) results: Vec<RawSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawSeriesSearchPage {
|
||||
#[serde(default)]
|
||||
pub(crate) results: Vec<RawSeriesSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawSeriesSearchResult {
|
||||
id: u32,
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
original_language: String,
|
||||
#[serde(default)]
|
||||
first_air_date: Option<String>,
|
||||
#[serde(default)]
|
||||
overview: Option<String>,
|
||||
#[serde(default)]
|
||||
poster_path: Option<String>,
|
||||
}
|
||||
|
||||
impl From<RawSeriesSearchResult> for SeriesSearchResult {
|
||||
fn from(raw: RawSeriesSearchResult) -> Self {
|
||||
Self {
|
||||
tmdb_id: raw.id,
|
||||
title: raw.name,
|
||||
original_language: raw.original_language,
|
||||
first_air_date: raw.first_air_date.as_deref().and_then(parse_date),
|
||||
overview: non_empty(raw.overview),
|
||||
poster_path: non_empty(raw.poster_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawSeries {
|
||||
id: u32,
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
original_language: String,
|
||||
#[serde(default)]
|
||||
first_air_date: Option<String>,
|
||||
#[serde(default)]
|
||||
status: String,
|
||||
#[serde(default)]
|
||||
overview: Option<String>,
|
||||
#[serde(default)]
|
||||
poster_path: Option<String>,
|
||||
#[serde(default)]
|
||||
seasons: Vec<RawSeasonSummary>,
|
||||
#[serde(default)]
|
||||
external_ids: Option<RawExternalIds>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawExternalIds {
|
||||
tvdb_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawSeasonSummary {
|
||||
season_number: u32,
|
||||
#[serde(default)]
|
||||
episode_count: u32,
|
||||
}
|
||||
|
||||
impl From<RawSeries> for Series {
|
||||
fn from(raw: RawSeries) -> Self {
|
||||
Self {
|
||||
tmdb_id: raw.id,
|
||||
tvdb_id: raw.external_ids.and_then(|ids| ids.tvdb_id),
|
||||
title: raw.name,
|
||||
original_language: raw.original_language,
|
||||
first_air_date: raw.first_air_date.as_deref().and_then(parse_date),
|
||||
status: raw.status,
|
||||
overview: non_empty(raw.overview),
|
||||
poster_path: non_empty(raw.poster_path),
|
||||
seasons: raw
|
||||
.seasons
|
||||
.into_iter()
|
||||
.map(|season| SeasonSummary {
|
||||
number: season.season_number,
|
||||
episode_count: season.episode_count,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawSeason {
|
||||
season_number: u32,
|
||||
#[serde(default)]
|
||||
episodes: Vec<RawEpisode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawEpisode {
|
||||
episode_number: u32,
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
air_date: Option<String>,
|
||||
}
|
||||
|
||||
impl From<RawSeason> for Season {
|
||||
fn from(raw: RawSeason) -> Self {
|
||||
Self {
|
||||
number: raw.season_number,
|
||||
episodes: raw
|
||||
.episodes
|
||||
.into_iter()
|
||||
.map(|episode| Episode {
|
||||
number: episode.episode_number,
|
||||
title: episode.name,
|
||||
air_date: episode.air_date.as_deref().and_then(parse_date),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawFindPage {
|
||||
#[serde(default)]
|
||||
pub(crate) movie_results: Vec<RawSearchResult>,
|
||||
#[serde(default)]
|
||||
pub(crate) tv_results: Vec<RawSeriesSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user