Add now-playing and upcoming movie endpoints
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

Buildable now that release_dates exists: GET /movies/now-playing and
/movies/upcoming filter on release_type="theatrical" per region, windowed
around today. A movie can carry more than one theatrical-type row in the
same region (limited release then wide release both map to "theatrical"
per normalizer._RELEASE_TYPE_MAP) -- collapsed to one row per movie via a
grouped subquery rather than joining ReleaseDate directly, which would
duplicate the movie in the page and inflate the total count.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-06 04:45:09 -05:00
co-authored by Claude Sonnet 5
parent 567b4ef068
commit 74ebad379a
5 changed files with 301 additions and 1 deletions
+20
View File
@@ -54,6 +54,26 @@ async def list_movies(
raise HTTPException(status.HTTP_422_UNPROCESSABLE_CONTENT, str(exc)) from exc
@router.get("/now-playing", response_model=MovieListOut)
async def now_playing_movies(
region: str = Query("US", min_length=2, max_length=2, description="ISO 3166-1 alpha-2 country"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=50),
session: AsyncSession = Depends(get_db),
) -> MovieListOut:
return await movie_service.list_now_playing(session, region=region, page=page, page_size=page_size)
@router.get("/upcoming", response_model=MovieListOut)
async def upcoming_movies(
region: str = Query("US", min_length=2, max_length=2, description="ISO 3166-1 alpha-2 country"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=50),
session: AsyncSession = Depends(get_db),
) -> MovieListOut:
return await movie_service.list_upcoming(session, region=region, page=page, page_size=page_size)
@router.get("/{aurora_id}", response_model=MovieDetailOut)
async def get_movie(
aurora_id: str,
+86 -1
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import uuid
from datetime import date, timedelta
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,7 +13,13 @@ from app.models.company import Company, MovieCompany
from app.models.enums import MovieStatus
from app.models.genre import Genre, MovieGenre
from app.models.keyword import Keyword, MovieKeyword
from app.models.movie import Movie, MovieAlternateTitle, MovieRecommendation
from app.models.movie import Movie, MovieAlternateTitle, MovieRecommendation, ReleaseDate
# How far back "now playing" looks for a theatrical release -- TMDb's own
# now_playing window is roughly a month; a movie ages out of "now playing"
# once its theatrical run is this old, whether or not it has any release
# still in the future (that's what "upcoming" is for).
_NOW_PLAYING_WINDOW_DAYS = 45
_MOVIE_SORT_COLUMNS = {
"popularity": Movie.popularity,
@@ -98,6 +105,84 @@ async def get_movie_recommendation_ids(
return list(result.scalars().all())
async def list_now_playing(
session: AsyncSession, *, region: str, limit: int, offset: int
) -> tuple[list[Movie], int]:
"""Movies with a theatrical release in `region` within the last
_NOW_PLAYING_WINDOW_DAYS, most recent first. A movie can have more than
one theatrical-type release_date row in the same region (a limited
release then a wide release both map to release_type="theatrical" --
see normalizer._RELEASE_TYPE_MAP), so this collapses to each movie's
latest one via a grouped subquery rather than joining ReleaseDate
directly, which would duplicate the movie in the page and inflate the
window-function total_count."""
today = date.today()
window_start = today - timedelta(days=_NOW_PLAYING_WINDOW_DAYS)
latest_release = (
select(
ReleaseDate.entity_id.label("movie_id"),
func.max(ReleaseDate.release_date).label("release_date"),
)
.where(
ReleaseDate.entity_type == "movie",
ReleaseDate.country_code == region,
ReleaseDate.release_type == "theatrical",
ReleaseDate.release_date <= today,
ReleaseDate.release_date >= window_start,
)
.group_by(ReleaseDate.entity_id)
.subquery()
)
stmt = (
select(Movie, func.count().over().label("total_count"))
.join(latest_release, latest_release.c.movie_id == Movie.id)
.order_by(latest_release.c.release_date.desc(), Movie.id.asc())
.limit(limit)
.offset(offset)
)
rows = (await session.execute(stmt)).all()
if not rows:
return [], 0
return [row[0] for row in rows], rows[0].total_count
async def list_upcoming(
session: AsyncSession, *, region: str, limit: int, offset: int
) -> tuple[list[Movie], int]:
"""Movies with a future theatrical release in `region`, soonest first
-- see list_now_playing for why this collapses to one row per movie
(here, the earliest upcoming date) rather than joining ReleaseDate
directly."""
today = date.today()
earliest_release = (
select(
ReleaseDate.entity_id.label("movie_id"),
func.min(ReleaseDate.release_date).label("release_date"),
)
.where(
ReleaseDate.entity_type == "movie",
ReleaseDate.country_code == region,
ReleaseDate.release_type == "theatrical",
ReleaseDate.release_date > today,
)
.group_by(ReleaseDate.entity_id)
.subquery()
)
stmt = (
select(Movie, func.count().over().label("total_count"))
.join(earliest_release, earliest_release.c.movie_id == Movie.id)
.order_by(earliest_release.c.release_date.asc(), Movie.id.asc())
.limit(limit)
.offset(offset)
)
rows = (await session.execute(stmt)).all()
if not rows:
return [], 0
return [row[0] for row in rows], rows[0].total_count
async def list_movies(
session: AsyncSession,
*,
+24
View File
@@ -192,6 +192,30 @@ async def build_movie_summaries(
return out
async def list_now_playing(
session: AsyncSession, *, region: str, page: int, page_size: int
) -> MovieListOut:
movies, total = await movie_repository.list_now_playing(
session, region=region.upper(), limit=page_size, offset=(page - 1) * page_size
)
results = [await _movie_summary_from_row(session, m) for m in movies]
total_pages = (total + page_size - 1) // page_size if total else 0
return MovieListOut(
page=page, page_size=page_size, total_results=total, total_pages=total_pages, results=results
)
async def list_upcoming(session: AsyncSession, *, region: str, page: int, page_size: int) -> MovieListOut:
movies, total = await movie_repository.list_upcoming(
session, region=region.upper(), limit=page_size, offset=(page - 1) * page_size
)
results = [await _movie_summary_from_row(session, m) for m in movies]
total_pages = (total + page_size - 1) // page_size if total else 0
return MovieListOut(
page=page, page_size=page_size, total_results=total, total_pages=total_pages, results=results
)
async def list_movies(
session: AsyncSession,
*,