feat(meta): resolve IMDb ids against series too

This commit is contained in:
Miguel Palhas
2026-08-23 18:59:35 +01:00
parent 1eac85c427
commit 3956617d41
5 changed files with 62 additions and 5 deletions
+10 -4
View File
@@ -9,8 +9,8 @@ use serde::de::DeserializeOwned;
use crate::cache::Cache;
use crate::error::{Error, Result};
use crate::model::{
ExternalIds, Movie, MovieSearchResult, RawExternalIds, RawFindPage, RawMovie, RawSearchPage,
RawSeason, RawSeries, RawSeriesSearchPage, Season, Series, SeriesSearchResult,
ExternalIds, FindResults, Movie, MovieSearchResult, RawExternalIds, RawFindPage, RawMovie,
RawSearchPage, RawSeason, RawSeries, RawSeriesSearchPage, Season, Series, SeriesSearchResult,
};
/// TMDB's v3 API root.
@@ -94,14 +94,20 @@ impl TmdbClient {
/// Resolve an `IMDb` title id through TMDB's external-id index.
///
/// TMDB answers with both kinds in one response, so one call surfaces
/// whichever the id names — the way a raw TMDB id resolves (§9.2).
///
/// # Errors
///
/// Any of [`Error`]; see its variants for what callers should distinguish.
pub async fn find_movie_by_imdb(&self, imdb_id: &str) -> Result<Vec<MovieSearchResult>> {
pub async fn find_by_imdb(&self, imdb_id: &str) -> Result<FindResults> {
let path = format!("find/{}", imdb_id.trim());
let params = [("external_source", "imdb_id".to_owned())];
let page: RawFindPage = self.get_json(&path, &params).await?;
Ok(page.movie_results.into_iter().map(Into::into).collect())
Ok(FindResults {
movies: page.movie_results.into_iter().map(Into::into).collect(),
series: page.tv_results.into_iter().map(Into::into).collect(),
})
}
/// Resolve a TVDB series id through TMDB's external-id index.
+1 -1
View File
@@ -24,5 +24,5 @@ mod model;
pub use client::{TmdbClient, TmdbClientBuilder, DEFAULT_BASE_URL, DEFAULT_CACHE_TTL};
pub use error::{Error, Result};
pub use model::{
Episode, ExternalIds, Movie, MovieSearchResult, Season, Series, SeriesSearchResult,
Episode, ExternalIds, FindResults, Movie, MovieSearchResult, Season, Series, SeriesSearchResult,
};
+9
View File
@@ -293,6 +293,15 @@ impl From<RawSeason> for Season {
}
}
/// What one `IMDb` id resolved to. An id names one title, so at most one of
/// the two lists is non-empty — but TMDB answers both kinds in the same
/// response, and both are surfaced rather than filtered here.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FindResults {
pub movies: Vec<MovieSearchResult>,
pub series: Vec<SeriesSearchResult>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct RawFindPage {
#[serde(default)]
+14
View File
@@ -0,0 +1,14 @@
{
"movie_results": [],
"tv_results": [
{
"id": 82728,
"name": "Bluey",
"original_name": "Bluey",
"original_language": "en",
"first_air_date": "2018-10-01",
"overview": "The slice-of-life adventures of an Australian cattle dog.",
"poster_path": "/58PmSsz6PEdlVscLE1tRJ7tknU.jpg"
}
]
}
+28
View File
@@ -19,6 +19,7 @@ const MOVIE_UNRELEASED: &str = include_str!("fixtures/movie_unreleased.json");
const MOVIE_THEATRICAL_ONLY: &str = include_str!("fixtures/movie_theatrical_only.json");
const MOVIE_FUTURE_DIGITAL: &str = include_str!("fixtures/movie_future_digital.json");
const SERIES_EXTERNAL_IDS: &str = include_str!("fixtures/series_external_ids.json");
const FIND_IMDB_SERIES: &str = include_str!("fixtures/find_imdb_series.json");
fn client(server: &MockServer) -> TmdbClient {
TmdbClient::builder("test-key")
@@ -483,6 +484,33 @@ async fn series_external_ids_carries_the_tvdb_id() {
assert_eq!(ids.tvdb_id, Some(361_391));
}
/// §9.2: a pasted `tt` id must resolve a series the same way it resolves a
/// movie. TMDB answers `/find` with both kinds in one response; the `tv_results`
/// half is what this reads.
#[tokio::test]
async fn find_by_imdb_resolves_series() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/3/find/tt7614372"))
.and(query_param("external_source", "imdb_id"))
.respond_with(ResponseTemplate::new(200).set_body_string(FIND_IMDB_SERIES))
.expect(1)
.mount(&server)
.await;
let results = client(&server)
.find_by_imdb("tt7614372")
.await
.expect("lookup succeeds");
assert!(results.movies.is_empty());
assert_eq!(results.series.len(), 1);
let series = &results.series[0];
assert_eq!(series.tmdb_id, 82_728);
assert_eq!(series.title, "Bluey");
assert_eq!(series.year(), Some(2018));
}
/// A series TMDB has no TVDB id for stays null rather than zero, so callers
/// can tell "unknown" from a real id and fall back to the text query.
#[tokio::test]