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
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:
co-authored by
Claude Sonnet 5
parent
567b4ef068
commit
74ebad379a
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
*,
|
||||
|
||||
@@ -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,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user