Add GET /collections browse endpoint; fix embedded collection poster_path
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
Completes browse/discover across all five entity types: movies, series, people, companies, and now collections. No filter (no categorical field like company_type exists for a collection) -- just page/page_size, always alphabetical by name. While adding poster_path to CollectionBriefOut for the browse listing, found the same class of bug fixed earlier for company logo_path: movie_service's embedded `collection` field never looked up its poster artwork either, so it stayed null even after ingest_collection populated one. Fixed alongside since it's the same schema field this feature was already touching. Also removed PageMeta from schemas/common.py -- scaffolding from early spec work that was never actually wired into any endpoint; all five browse endpoints shipped this session use a flat page/page_size/ total_results/total_pages shape instead. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4cf3556433
commit
f1ac072794
@@ -2,13 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import not_found
|
||||
from app.core.database import get_db
|
||||
from app.core.security import enforce_rate_limit
|
||||
from app.schemas.collection import CollectionDetailOut
|
||||
from app.schemas.collection import CollectionDetailOut, CollectionListOut
|
||||
from app.services import collection_service
|
||||
|
||||
router = APIRouter(
|
||||
@@ -16,6 +16,15 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=CollectionListOut)
|
||||
async def list_collections(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=50),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> CollectionListOut:
|
||||
return await collection_service.list_collections(session, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get("/{aurora_id}", response_model=CollectionDetailOut)
|
||||
async def get_collection(
|
||||
aurora_id: str, session: AsyncSession = Depends(get_db)
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.collection import Collection, CollectionMovie, CollectionSeries
|
||||
@@ -37,3 +37,24 @@ async def get_collection_series(session: AsyncSession, collection_id: uuid.UUID)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_collections(
|
||||
session: AsyncSession, *, limit: int, offset: int
|
||||
) -> tuple[list[Collection], int]:
|
||||
"""Browse collections -- see movie_repository.list_movies for the
|
||||
window-function total-count approach this mirrors. Collections have no
|
||||
categorical field to filter by and no popularity/date to sort by, so
|
||||
this always orders alphabetically by name (with an id tiebreak for
|
||||
stable pagination)."""
|
||||
|
||||
stmt = (
|
||||
select(Collection, func.count().over().label("total_count"))
|
||||
.order_by(Collection.name.asc(), Collection.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
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.common import CollectionBriefOut
|
||||
from app.schemas.movie import MovieSummaryOut
|
||||
from app.schemas.tv import SeriesSummaryOut
|
||||
|
||||
@@ -22,3 +23,11 @@ class CollectionDetailOut(BaseModel):
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class CollectionListOut(BaseModel):
|
||||
page: int
|
||||
page_size: int
|
||||
total_results: int
|
||||
total_pages: int
|
||||
results: list[CollectionBriefOut] = []
|
||||
|
||||
@@ -101,13 +101,7 @@ class CompanyBriefOut(BaseModel):
|
||||
class CollectionBriefOut(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class PageMeta(BaseModel):
|
||||
page: int
|
||||
page_size: int
|
||||
total: int
|
||||
total_pages: int
|
||||
poster_path: str | None = None
|
||||
|
||||
|
||||
class ErrorDetail(BaseModel):
|
||||
|
||||
@@ -7,9 +7,10 @@ import uuid
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.ids import aurora_id, parse_aurora_id
|
||||
from app.models.collection import Collection
|
||||
from app.models.enums import ArtworkType
|
||||
from app.repositories import collection_repository, polymorphic
|
||||
from app.schemas.collection import CollectionDetailOut
|
||||
from app.schemas.collection import CollectionDetailOut, CollectionListOut
|
||||
from app.schemas.common import CollectionBriefOut
|
||||
from app.services.common import primary_artwork_path
|
||||
from app.services.movie_service import build_movie_summaries
|
||||
@@ -55,13 +56,24 @@ async def get_collection_detail(
|
||||
)
|
||||
|
||||
|
||||
async def _collection_summary_from_row(
|
||||
session: AsyncSession, collection: Collection
|
||||
) -> CollectionBriefOut:
|
||||
posters = await polymorphic.get_artwork(session, "collection", collection.id, ArtworkType.POSTER)
|
||||
return CollectionBriefOut(
|
||||
id=aurora_id("collection", str(collection.id)),
|
||||
name=collection.name,
|
||||
poster_path=primary_artwork_path(posters, ArtworkType.POSTER),
|
||||
)
|
||||
|
||||
|
||||
async def build_collection_summary(
|
||||
session: AsyncSession, collection_id: uuid.UUID
|
||||
) -> CollectionBriefOut | None:
|
||||
collection = await collection_repository.get_collection_by_id(session, collection_id)
|
||||
if collection is None:
|
||||
return None
|
||||
return CollectionBriefOut(id=aurora_id("collection", str(collection.id)), name=collection.name)
|
||||
return await _collection_summary_from_row(session, collection)
|
||||
|
||||
|
||||
async def build_collection_summaries(
|
||||
@@ -73,3 +85,15 @@ async def build_collection_summaries(
|
||||
if summary is not None:
|
||||
out.append(summary)
|
||||
return out
|
||||
|
||||
|
||||
async def list_collections(session: AsyncSession, *, page: int, page_size: int) -> CollectionListOut:
|
||||
collections, total = await collection_repository.list_collections(
|
||||
session, limit=page_size, offset=(page - 1) * page_size
|
||||
)
|
||||
results = [await _collection_summary_from_row(session, c) for c in collections]
|
||||
total_pages = (total + page_size - 1) // page_size if total else 0
|
||||
|
||||
return CollectionListOut(
|
||||
page=page, page_size=page_size, total_results=total, total_pages=total_pages, results=results
|
||||
)
|
||||
|
||||
@@ -54,6 +54,17 @@ async def get_movie_detail(session: AsyncSession, movie_aurora_id: str) -> Movie
|
||||
|
||||
cast, crew = split_credits(credits_)
|
||||
|
||||
collection_out: CollectionBriefOut | None = None
|
||||
if movie.collection is not None:
|
||||
collection_posters = await polymorphic.get_artwork(
|
||||
session, "collection", movie.collection.id, ArtworkType.POSTER
|
||||
)
|
||||
collection_out = CollectionBriefOut(
|
||||
id=aurora_id("collection", str(movie.collection.id)),
|
||||
name=movie.collection.name,
|
||||
poster_path=primary_artwork_path(collection_posters, ArtworkType.POSTER),
|
||||
)
|
||||
|
||||
company_out: list[CompanyBriefOut] = []
|
||||
for company, _role in companies:
|
||||
logos = await polymorphic.get_artwork(session, "company", company.id, ArtworkType.LOGO)
|
||||
@@ -87,13 +98,7 @@ async def get_movie_detail(session: AsyncSession, movie_aurora_id: str) -> Movie
|
||||
cast=cast,
|
||||
crew=crew,
|
||||
companies=company_out,
|
||||
collection=(
|
||||
CollectionBriefOut(
|
||||
id=aurora_id("collection", str(movie.collection.id)), name=movie.collection.name
|
||||
)
|
||||
if movie.collection
|
||||
else None
|
||||
),
|
||||
collection=collection_out,
|
||||
images=to_images_out(artwork),
|
||||
videos=[to_video_out(v) for v in videos],
|
||||
external_ids=external_ids,
|
||||
|
||||
Reference in New Issue
Block a user