Add movies/series filmography to company detail
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

GET /companies/{id} showed only the company's own info. Adds movies
and series lists (any role: production, distribution, network),
ordered by release/first-air date, read straight off the existing
movie_companies/series_companies junction rows -- no separate sync
needed since those only ever exist as a side effect of ingesting the
movie or series itself.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-05 09:34:31 -05:00
co-authored by Claude Sonnet 5
parent 606d2532c4
commit 6956bf7406
6 changed files with 144 additions and 2 deletions
+25 -1
View File
@@ -7,9 +7,33 @@ import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.company import Company
from app.models.company import Company, MovieCompany, SeriesCompany
from app.models.movie import Movie
from app.models.tv import Series
async def get_company_by_id(session: AsyncSession, company_id: uuid.UUID) -> Company | None:
result = await session.execute(select(Company).where(Company.id == company_id))
return result.scalar_one_or_none()
async def get_company_movies(session: AsyncSession, company_id: uuid.UUID) -> list[Movie]:
stmt = (
select(Movie)
.join(MovieCompany, MovieCompany.movie_id == Movie.id)
.where(MovieCompany.company_id == company_id)
.order_by(Movie.primary_release_date.asc().nulls_last())
)
result = await session.execute(stmt)
return list(result.scalars().all())
async def get_company_series(session: AsyncSession, company_id: uuid.UUID) -> list[Series]:
stmt = (
select(Series)
.join(SeriesCompany, SeriesCompany.series_id == Series.id)
.where(SeriesCompany.company_id == company_id)
.order_by(Series.first_air_date.asc().nulls_last())
)
result = await session.execute(stmt)
return list(result.scalars().all())
+6
View File
@@ -6,6 +6,9 @@ from datetime import datetime
from pydantic import BaseModel
from app.schemas.movie import MovieSummaryOut
from app.schemas.tv import SeriesSummaryOut
class CompanyDetailOut(BaseModel):
id: str
@@ -20,5 +23,8 @@ class CompanyDetailOut(BaseModel):
parent_company_id: str | None = None
external_ids: dict[str, str] = {}
movies: list[MovieSummaryOut] = []
series: list[SeriesSummaryOut] = []
created_at: datetime
updated_at: datetime
+6
View File
@@ -12,6 +12,8 @@ from app.repositories import company_repository, polymorphic
from app.schemas.common import CompanyBriefOut
from app.schemas.company import CompanyDetailOut
from app.services.common import primary_artwork_path
from app.services.movie_service import build_movie_summaries
from app.services.tv_service import build_series_summaries
class CompanyNotFoundError(Exception):
@@ -33,6 +35,8 @@ async def get_company_detail(session: AsyncSession, company_aurora_id: str) -> C
logos = await polymorphic.get_artwork(session, "company", company_id, ArtworkType.LOGO)
external_ids = await polymorphic.get_external_ids(session, "company", company_id)
movies = await company_repository.get_company_movies(session, company_id)
series = await company_repository.get_company_series(session, company_id)
return CompanyDetailOut(
id=aurora_id("company", str(company.id)),
@@ -49,6 +53,8 @@ async def get_company_detail(session: AsyncSession, company_aurora_id: str) -> C
else None
),
external_ids=external_ids,
movies=await build_movie_summaries(session, [m.id for m in movies]),
series=await build_series_summaries(session, [s.id for s in series]),
created_at=company.created_at,
updated_at=company.updated_at,
)
+7
View File
@@ -160,6 +160,13 @@ member movies purely by external-id lookup, the same never-fabricate-a-stub
rule `recommendations` follows, so a franchise fills in as more of its
movies get ingested rather than all at once.
Company detail also carries `movies` and `series` lists — everything that
company is credited on (any role: production, distribution, network),
ordered by release/first-air date. No never-fabricate-a-stub caveat here:
a `MovieCompany`/`SeriesCompany` row is only ever created as a side effect
of actually ingesting that movie or series, so there's no separate
"company's known filmography" TMDb ever needs linking after the fact.
## Administrative (section 17: "separately protected")
Require header `X-API-Key: <one of AURORA_ADMIN_API_KEYS>`. Never exposed to
+6 -1
View File
@@ -57,7 +57,12 @@ else carries `job`. Series-level "Creator" credits and episode
A single `companies` table serves production companies, studios, networks,
and distributors, distinguished by `company_type` (section 8) — a TV network
and a movie studio have the same shape (name, logo, country, parent
company), so a separate `Network` entity would just duplicate columns.
company), so a separate `Network` entity would just duplicate columns. A
company's own filmography (`GET /api/v1/companies/{id}`'s `movies`/`series`
lists) is read straight off `movie_companies`/`series_companies` — no
separate sync step, since those rows only ever exist as a side effect of
ingesting the movie or series itself (`app/repositories/company_repository.py`).
`collections` (franchises) are first-class, with their own artwork and
`collection_relations` for shared-universe links between collections
(section 9). A collection starts as a bare name-only stub the first time
@@ -0,0 +1,94 @@
"""Integration test for company filmography (section 17): GET /companies/{id}
listing the movies/series a studio/network has produced, the same pattern
collections use for their member movies. Exercised through
company_service.get_company_detail against real 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.company import Company
from app.services import company_service
from app.services.ingestion.movie_ingestion import ingest_movie
from app.services.ingestion.series_ingestion import ingest_series
from tests.integration.test_movie_ingestion import FakeTMDbProvider
from tests.integration.test_series_ingestion import FakeTMDbSeriesProvider
_SHARED_STUDIO = {"id": 555, "name": "Shared Studio", "origin_country": "US"}
def _movie_payload(**overrides: Any) -> dict[str, Any]:
base = {
"id": 9200,
"title": "Untitled",
"status": "Released",
"production_companies": [_SHARED_STUDIO],
"release_date": "2020-01-01",
}
base.update(overrides)
return base
def _series_payload(**overrides: Any) -> dict[str, Any]:
base = {
"id": 9200,
"name": "Untitled",
"status": "Ended",
"production_companies": [_SHARED_STUDIO],
"first_air_date": "2020-01-01",
}
base.update(overrides)
return base
@pytest.mark.asyncio
async def test_company_detail_lists_its_movies_and_series_by_release_date(
db_session: AsyncSession,
):
later_movie = _movie_payload(id=9201, title="Studio Movie 1", release_date="2021-01-01")
earlier_movie = _movie_payload(id=9202, title="Studio Movie 2", release_date="2019-01-01")
show = _series_payload(id=9203, name="Studio Show", first_air_date="2020-06-01")
await ingest_movie(db_session, FakeTMDbProvider(later_movie), "9201")
await ingest_movie(db_session, FakeTMDbProvider(earlier_movie), "9202")
await ingest_series(db_session, FakeTMDbSeriesProvider(show, {}), "9203")
await db_session.commit()
company = (
await db_session.execute(select(Company).where(Company.name == "Shared Studio"))
).scalar_one()
detail = await company_service.get_company_detail(
db_session, aurora_id("company", str(company.id))
)
# Ordered by release date ascending, not ingestion order.
assert [m.title for m in detail.movies] == ["Studio Movie 2", "Studio Movie 1"]
assert [s.title for s in detail.series] == ["Studio Show"]
@pytest.mark.asyncio
async def test_company_detail_series_list_is_empty_for_a_movie_only_studio(
db_session: AsyncSession,
):
movie = _movie_payload(
id=9204, title="Lone Movie", production_companies=[{"id": 556, "name": "Solo Studio"}]
)
await ingest_movie(db_session, FakeTMDbProvider(movie), "9204")
await db_session.commit()
company = (
await db_session.execute(select(Company).where(Company.name == "Solo Studio"))
).scalar_one()
detail = await company_service.get_company_detail(
db_session, aurora_id("company", str(company.id))
)
assert len(detail.movies) == 1
assert detail.series == []