Add GET /movies/{id}/recommendations
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

Ingestion already stored movie_recommendations edges from TMDb's
recommendations list (linking only to movies already ingested into
Aurora, per the never-fabricate-a-stub rule), but nothing exposed them.
Adds the repository lookup, service assembly, and route to read them
back in ranked order.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-05 08:16:18 -05:00
co-authored by Claude Sonnet 5
parent 73488a0d80
commit 4af4ba40ef
6 changed files with 106 additions and 2 deletions
+17 -1
View File
@@ -10,7 +10,13 @@ from app.api.deps import cached_response, not_found
from app.core.database import get_db
from app.core.redis import get_redis
from app.core.security import enforce_rate_limit
from app.schemas.movie import MovieCreditsOut, MovieDetailOut, MovieImagesOut, MovieVideosOut
from app.schemas.movie import (
MovieCreditsOut,
MovieDetailOut,
MovieImagesOut,
MovieRecommendationsOut,
MovieVideosOut,
)
from app.services import movie_service
from app.services.cache import build_key
@@ -62,3 +68,13 @@ async def get_movie_videos(
return await movie_service.get_movie_videos(session, aurora_id)
except movie_service.MovieNotFoundError as exc:
raise not_found("movie", aurora_id) from exc
@router.get("/{aurora_id}/recommendations", response_model=MovieRecommendationsOut)
async def get_movie_recommendations(
aurora_id: str, session: AsyncSession = Depends(get_db)
) -> MovieRecommendationsOut:
try:
return await movie_service.get_movie_recommendations(session, aurora_id)
except movie_service.MovieNotFoundError as exc:
raise not_found("movie", aurora_id) from exc
+18 -1
View File
@@ -11,7 +11,7 @@ from sqlalchemy.orm import selectinload
from app.models.company import Company, MovieCompany
from app.models.genre import Genre, MovieGenre
from app.models.keyword import Keyword, MovieKeyword
from app.models.movie import Movie
from app.models.movie import Movie, MovieRecommendation
async def get_movie_by_id(session: AsyncSession, movie_id: uuid.UUID) -> Movie | None:
@@ -59,3 +59,20 @@ async def get_movie_companies(
)
result = await session.execute(stmt)
return [(company, role) for company, role in result.all()]
async def get_movie_recommendation_ids(
session: AsyncSession, movie_id: uuid.UUID
) -> list[uuid.UUID]:
"""Aurora ids of movies TMDb recommends alongside this one, ordered by
rank (section 15) -- only ever movies already ingested into Aurora, since
`ingest_movie` (movie_ingestion.py) links these edges purely by
external-id lookup and never creates a stub for an unseen movie."""
stmt = (
select(MovieRecommendation.recommended_movie_id)
.where(MovieRecommendation.movie_id == movie_id)
.order_by(MovieRecommendation.rank.asc().nulls_last())
)
result = await session.execute(stmt)
return list(result.scalars().all())
+5
View File
@@ -78,3 +78,8 @@ class MovieImagesOut(BaseModel):
class MovieVideosOut(BaseModel):
id: str
videos: list[VideoOut] = []
class MovieRecommendationsOut(BaseModel):
id: str
recommendations: list[MovieSummaryOut] = []
+13
View File
@@ -14,6 +14,7 @@ from app.schemas.movie import (
MovieCreditsOut,
MovieDetailOut,
MovieImagesOut,
MovieRecommendationsOut,
MovieSummaryOut,
MovieVideosOut,
)
@@ -123,6 +124,18 @@ async def get_movie_videos(session: AsyncSession, movie_aurora_id: str) -> Movie
return MovieVideosOut(id=aurora_id("movie", str(movie.id)), videos=[to_video_out(v) for v in videos])
async def get_movie_recommendations(
session: AsyncSession, movie_aurora_id: str
) -> MovieRecommendationsOut:
movie_id = await _resolve_movie_id(movie_aurora_id)
movie = await movie_repository.get_movie_by_id(session, movie_id)
if movie is None:
raise MovieNotFoundError(movie_aurora_id)
recommended_ids = await movie_repository.get_movie_recommendation_ids(session, movie_id)
summaries = await build_movie_summaries(session, recommended_ids)
return MovieRecommendationsOut(id=aurora_id("movie", str(movie.id)), recommendations=summaries)
async def build_movie_summary(session: AsyncSession, movie_id: uuid.UUID) -> MovieSummaryOut | None:
movie = await movie_repository.get_movie_by_id(session, movie_id)
if movie is None: