Add movie/series ratings and watchlist

Builds on the accounts foundation: PUT/DELETE a rating (0.5-10.0) or a
watchlist entry on any movie or series, all scoped to the bearer token's
own user, plus GET /account/ratings|watchlist/{movies,series} to list
them back. Both are polymorphic entity_type/entity_id tables, same shape
as watch_provider_offerings and trending_entries, so movie and series
share one repository/router each rather than duplicating per type.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-06 10:47:41 -05:00
co-authored by Claude Sonnet 5
parent a38c2074a6
commit 0ab31a314c
20 changed files with 1043 additions and 6 deletions
+83 -1
View File
@@ -9,7 +9,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.ids import aurora_id, parse_aurora_id
from app.models.enums import ArtworkType, MovieStatus
from app.models.movie import Movie
from app.repositories import movie_repository, polymorphic, trending_repository
from app.repositories import (
movie_repository,
polymorphic,
rating_repository,
trending_repository,
watchlist_repository,
)
from app.schemas.account import RatingOut
from app.schemas.common import CollectionBriefOut, CompanyBriefOut, GenreOut, KeywordOut
from app.schemas.movie import (
MovieCreditsOut,
@@ -20,6 +27,8 @@ from app.schemas.movie import (
MovieSummaryOut,
MovieVideosOut,
MovieWatchProvidersOut,
RatedMovieListOut,
RatedMovieOut,
)
from app.services.common import (
build_watch_providers_out,
@@ -209,6 +218,79 @@ async def build_movie_summaries(
return out
async def rate_movie(
session: AsyncSession, movie_aurora_id: str, *, user_id: uuid.UUID, value: float
) -> RatingOut:
movie_id = await _resolve_movie_id(movie_aurora_id)
if await movie_repository.get_movie_by_id(session, movie_id) is None:
raise MovieNotFoundError(movie_aurora_id)
rating = await rating_repository.upsert_rating(
session, user_id=user_id, entity_type="movie", entity_id=movie_id, value=value
)
return RatingOut(value=rating.value, created_at=rating.created_at, updated_at=rating.updated_at)
async def unrate_movie(session: AsyncSession, movie_aurora_id: str, *, user_id: uuid.UUID) -> None:
movie_id = await _resolve_movie_id(movie_aurora_id)
if await movie_repository.get_movie_by_id(session, movie_id) is None:
raise MovieNotFoundError(movie_aurora_id)
await rating_repository.delete_rating(session, user_id=user_id, entity_type="movie", entity_id=movie_id)
async def list_rated_movies(
session: AsyncSession, *, user_id: uuid.UUID, page: int, page_size: int
) -> RatedMovieListOut:
ratings, total = await rating_repository.list_rated(
session, user_id=user_id, entity_type="movie", limit=page_size, offset=(page - 1) * page_size
)
results: list[RatedMovieOut] = []
for rating in ratings:
summary = await build_movie_summary(session, rating.entity_id)
if summary is not None:
results.append(RatedMovieOut(**summary.model_dump(), rating=rating.value))
total_pages = (total + page_size - 1) // page_size if total else 0
return RatedMovieListOut(
page=page, page_size=page_size, total_results=total, total_pages=total_pages, results=results
)
async def add_movie_to_watchlist(
session: AsyncSession, movie_aurora_id: str, *, user_id: uuid.UUID
) -> None:
movie_id = await _resolve_movie_id(movie_aurora_id)
if await movie_repository.get_movie_by_id(session, movie_id) is None:
raise MovieNotFoundError(movie_aurora_id)
await watchlist_repository.add_entry(session, user_id=user_id, entity_type="movie", entity_id=movie_id)
async def remove_movie_from_watchlist(
session: AsyncSession, movie_aurora_id: str, *, user_id: uuid.UUID
) -> None:
movie_id = await _resolve_movie_id(movie_aurora_id)
if await movie_repository.get_movie_by_id(session, movie_id) is None:
raise MovieNotFoundError(movie_aurora_id)
await watchlist_repository.remove_entry(session, user_id=user_id, entity_type="movie", entity_id=movie_id)
async def list_watchlist_movies(
session: AsyncSession, *, user_id: uuid.UUID, page: int, page_size: int
) -> MovieListOut:
entries, total = await watchlist_repository.list_entries(
session, user_id=user_id, entity_type="movie", limit=page_size, offset=(page - 1) * page_size
)
results = await build_movie_summaries(session, [entry.entity_id for entry in entries])
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_now_playing(
session: AsyncSession, *, region: str, page: int, page_size: int
) -> MovieListOut: