Expose certifications and per-country release dates
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s

TMDb's release_dates/content_ratings blocks were already fetched via
append_to_response but discarded at normalization -- every downstream
layer (ingestion write, repository read, schema, service) was unbuilt.
Adds the full pipeline: NormalizedReleaseDate + get_or_create_certification,
a shared polymorphic sync/read (movie and series both use it), and
MovieDetailOut/SeriesDetailOut certification + release_dates fields.

Series content_ratings has no date of its own, so each rating is anchored
to the show's first_air_date with release_type="tv".

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-06 04:07:18 -05:00
co-authored by Claude Sonnet 5
parent 340cadd99d
commit e1a36ce227
15 changed files with 510 additions and 5 deletions
+134
View File
@@ -0,0 +1,134 @@
"""Integration tests for release-dates/certifications (section 17,
additive): TMDb's release_dates/content_ratings blocks were already being
fetched via append_to_response, but dropped at the normalizer step -- every
downstream layer (ingestion write, repository read, schema, service) was
unbuilt. Mirrors test_movie_ingestion.py/test_series_ingestion.py's fake
provider pattern to exercise the full write -> read path against
PostgreSQL."""
from __future__ import annotations
from typing import Any
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.ids import aurora_id
from app.models.language_country import Certification
from app.models.movie import ReleaseDate
from app.providers.base import MetadataProvider
from app.services import movie_service, tv_service
from app.services.ingestion.movie_ingestion import ingest_movie
from app.services.ingestion.series_ingestion import ingest_series
from tests.unit.test_normalizer import FAKE_MOVIE, FAKE_SERIES
class FakeTMDbProvider(MetadataProvider):
slug = "tmdb"
name = "The Movie Database (TMDb)"
def __init__(self, movie_payload: dict[str, Any] | None = None, series_payload: dict[str, Any] | None = None) -> None:
self._movie_payload = movie_payload
self._series_payload = series_payload
async def get_movie(self, external_id: str) -> dict[str, Any]:
assert self._movie_payload is not None
return self._movie_payload
async def get_series(self, external_id: str) -> dict[str, Any]:
assert self._series_payload is not None
return self._series_payload
async def get_season(self, series_external_id: str, season_number: int) -> dict[str, Any]:
raise NotImplementedError
async def get_person(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def get_collection(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def search_movie(self, query: str) -> list[dict[str, Any]]:
raise NotImplementedError
@pytest.mark.asyncio
async def test_ingest_movie_stores_release_dates_and_certifications(db_session: AsyncSession):
movie_id = await ingest_movie(db_session, FakeTMDbProvider(movie_payload=FAKE_MOVIE), "603")
await db_session.commit()
rows = (
(await db_session.execute(select(ReleaseDate).where(ReleaseDate.entity_id == movie_id)))
.scalars()
.all()
)
assert len(rows) == 4
us_theatrical = next(r for r in rows if r.country_code == "US" and r.release_type == "theatrical")
cert = await db_session.get(Certification, us_theatrical.certification_id)
assert cert.system == "MPA"
assert cert.rating == "R"
@pytest.mark.asyncio
async def test_ingest_movie_reingestion_replaces_release_dates(db_session: AsyncSession):
"""Wholesale replace on re-sync, same as credits/videos -- re-running
ingestion must not accumulate duplicate release-date rows."""
provider = FakeTMDbProvider(movie_payload=FAKE_MOVIE)
movie_id = await ingest_movie(db_session, provider, "603")
await db_session.commit()
movie_id_again = await ingest_movie(db_session, provider, "603")
await db_session.commit()
assert movie_id == movie_id_again
rows = (
(await db_session.execute(select(ReleaseDate).where(ReleaseDate.entity_id == movie_id)))
.scalars()
.all()
)
assert len(rows) == 4
@pytest.mark.asyncio
async def test_movie_detail_surfaces_certification_and_release_dates(db_session: AsyncSession):
movie_id = await ingest_movie(db_session, FakeTMDbProvider(movie_payload=FAKE_MOVIE), "603")
await db_session.commit()
detail = await movie_service.get_movie_detail(db_session, aurora_id("movie", str(movie_id)))
assert detail.certification == "R" # US MPA rating, the default badge
assert len(detail.release_dates) == 4
us_theatrical = next(
rd for rd in detail.release_dates if rd.country_code == "US" and rd.release_type == "theatrical"
)
assert us_theatrical.certification.system == "MPA"
assert us_theatrical.certification.rating == "R"
@pytest.mark.asyncio
async def test_ingest_series_anchors_content_ratings_to_first_air_date(db_session: AsyncSession):
series_id = await ingest_series(db_session, FakeTMDbProvider(series_payload=FAKE_SERIES), "1396")
await db_session.commit()
rows = (
(await db_session.execute(select(ReleaseDate).where(ReleaseDate.entity_id == series_id)))
.scalars()
.all()
)
assert len(rows) == 2
assert all(r.release_type == "tv" for r in rows)
assert all(str(r.release_date) == "2008-01-20" for r in rows)
@pytest.mark.asyncio
async def test_series_detail_surfaces_certification(db_session: AsyncSession):
series_id = await ingest_series(db_session, FakeTMDbProvider(series_payload=FAKE_SERIES), "1396")
await db_session.commit()
detail = await tv_service.get_series_detail(db_session, aurora_id("series", str(series_id)))
assert detail.certification == "TV-MA"
assert len(detail.release_dates) == 2
+88
View File
@@ -89,6 +89,39 @@ FAKE_MOVIE = {
"alternative_titles": {"titles": [{"title": "Matrix", "iso_3166_1": "US"}]},
"external_ids": {"imdb_id": "tt0133093"},
"recommendations": {"results": [{"id": 604}]},
"release_dates": {
"results": [
{
"iso_3166_1": "US",
"release_dates": [
{
"certification": "R",
"release_date": "1999-03-31T00:00:00.000Z",
"type": 3,
"note": "",
},
{
"certification": "",
"release_date": "1999-03-01T00:00:00.000Z",
"type": 1,
"note": "Premiere",
},
],
},
{
"iso_3166_1": "GB",
"release_dates": [
{"certification": "15", "release_date": "1999-06-11T00:00:00.000Z", "type": 3}
],
},
{
"iso_3166_1": "MX",
"release_dates": [
{"certification": "B", "release_date": "1999-04-02T00:00:00.000Z", "type": 3}
],
},
]
},
}
FAKE_PERSON = {
@@ -192,6 +225,40 @@ def test_normalize_movie_maps_collection_and_recommendations():
assert movie.recommendation_external_ids == ["604"]
def test_normalize_movie_maps_release_dates_and_certification():
movie = normalize_movie(FAKE_MOVIE)
assert len(movie.release_dates) == 4
us_theatrical = next(
rd for rd in movie.release_dates if rd.country_code == "US" and rd.release_type == "theatrical"
)
assert us_theatrical.release_date == date(1999, 3, 31)
assert us_theatrical.certification == ("MPA", "R")
us_premiere = next(rd for rd in movie.release_dates if rd.release_type == "premiere")
assert us_premiere.certification is None
assert us_premiere.note == "Premiere"
gb_entry = next(rd for rd in movie.release_dates if rd.country_code == "GB")
assert gb_entry.certification == ("BBFC", "15")
mx_entry = next(rd for rd in movie.release_dates if rd.country_code == "MX")
# MX has no entry in the small known certification-system map -- falls
# back to the country code itself rather than fabricating a body name.
assert mx_entry.certification == ("MX", "B")
def test_normalize_movie_release_dates_skip_entries_without_a_date():
minimal = {
**FAKE_MOVIE,
"release_dates": {
"results": [{"iso_3166_1": "US", "release_dates": [{"certification": "R", "release_date": ""}]}]
},
}
movie = normalize_movie(minimal)
assert movie.release_dates == []
def test_normalize_movie_handles_missing_optional_sections():
minimal = {"id": 1, "title": "Minimal Movie"}
movie = normalize_movie(minimal)
@@ -264,6 +331,9 @@ FAKE_SERIES = {
"keywords": {"results": [{"id": 1, "name": "drug dealer"}]},
"alternative_titles": {"results": [{"title": "Breaking Bad", "iso_3166_1": "US"}]},
"external_ids": {"imdb_id": "tt0903747", "tvdb_id": 81189},
"content_ratings": {
"results": [{"iso_3166_1": "US", "rating": "TV-MA"}, {"iso_3166_1": "GB", "rating": "15"}]
},
"seasons": [
{
"season_number": 1,
@@ -376,6 +446,24 @@ def test_normalize_series_external_ids_include_tvdb():
assert series.external_ids["tvdb"] == "81189"
def test_normalize_series_anchors_content_ratings_to_first_air_date():
# TMDb's /tv content_ratings carries no date of its own -- each rating
# is anchored to the show's first_air_date (release_type="tv") rather
# than left with no release_date at all.
series = normalize_series(FAKE_SERIES)
assert len(series.release_dates) == 2
us = next(rd for rd in series.release_dates if rd.country_code == "US")
assert us.release_date == date(2008, 1, 20)
assert us.release_type == "tv"
assert us.certification == ("MPA", "TV-MA")
def test_normalize_series_content_ratings_skipped_without_first_air_date():
no_air_date = {**FAKE_SERIES, "first_air_date": None}
series = normalize_series(no_air_date)
assert series.release_dates == []
def test_normalize_season_maps_episodes():
season = normalize_season(FAKE_SEASON)
assert season.season_number == 1