1. test_ingestion_cache_invalidation.py: the job was created through
the test's db_session fixture, whose commit() only ends a savepoint
that gets rolled back at teardown (see tests/conftest.py) -- it's
never a real commit. _run_ingestion looks the job up through its
own session_scope() (a genuinely separate connection), which could
never see it, so the very first _require_job call raised "job
vanished mid-run" every time. Worse, test_failed_ingestion_does_not_
invalidate_the_cache still passed, coincidentally -- it expected a
RuntimeError and got one, just the wrong one, masking the bug.
Fixed by having _make_job create the job through session_scope()
itself, matching test_ingestion_retry.py's existing pattern.
2. Same class of bug as the video-collision fix in the last commit:
FAKE_MOVIE's fixed imdb_id ("tt0133093"), not just its video, also
collides across two different fake movies sharing it unmodified --
uq_external_id_source is a real unique constraint on
(provider_id, entity_type, external_id). Fixed by also overriding
imdb_id on the second movie in both affected tests.
All caught by running the actual integration suite against Postgres --
these paths were previously only collect-only'd locally (no local DB).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
FAKE_MOVIE carries a fixed provider_video_id ("abc123"); two different
fake movies both spreading it unmodified and getting ingested in the
same test collide on the real uq_video_provider_source unique
constraint (a video id belongs to exactly one entity). Found by
actually running the tests against Postgres on the devbox -- these
paths were only ever collect-only'd locally before now (no local DB).
Affected: test_movie_recommendations_only_include_already_ingested_movies
and test_ingest_collection_only_links_already_ingested_movies, both of
which ingest two FAKE_MOVIE-derived movies in one test. Fixed by
overriding videos to empty on the second movie in each.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
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]>
Collections were bare stubs (name only) lazily created the moment a
movie's belongs_to_collection reference was seen. Adds a real
ingest_collection pipeline: TMDb's /collection/{id} for overview and
poster/backdrop artwork, plus ordered CollectionMovie membership --
linking only movies already ingested into Aurora, the same
never-fabricate-a-stub rule movie recommendations already follow.
get_or_create_collection now refreshes name/overview on an existing
stub instead of only creating, guarded so a bare movie-stub call
(overview=None) never clobbers real data a full ingestion already
filled in.
New POST /api/v1/admin/ingest/collection endpoint and
ingest_collection_task; GET /api/v1/collections/{id} needed no changes
since its read path (collection_service.py) already assembled the full
detail shape.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
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]>
movie_service.get_movie_detail() hardcoded companies[].logo_path = None
instead of looking up the company's logo artwork, and tv_service's
networks[]/companies[] never set logo_path at all -- both just built
CompanyBriefOut without the artwork lookup that company_service.py (and
_season_brief for posters) already does correctly. Ingestion and storage
were working the whole time; the API layer just never read the result.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
CompanyBriefOut.logo_path and CompanyDetailOut.logo_path have existed
in the API schemas since Core 0.1's first pass, but nothing ever
actually populated them -- TMDb's production_companies/networks
entries carry a logo_path field that the normalizer simply never read,
so every response has silently returned null there.
- NormalizedCompanyRef now captures logo_path from both movie
production_companies and series networks/production_companies.
- normalizer.company_logo_artwork() builds the single-image artwork
entry for it (a company has exactly one logo ingested, so it's
always is_primary) -- centralized there rather than duplicated in
both ingestion modules, matching how movie/series artwork URLs are
already built in one place.
- movie_ingestion.py and series_ingestion.py sync it via the same
sync_artwork() the rest of section 10's self-hosting already uses,
so it gets the same re-ingestion-safe upsert behavior for free.
- tasks.py cascades a download_artwork_task per referenced company
after movie/series ingestion, same fan-out pattern as
movie/series/season/person artwork.
Co-Authored-By: Claude Sonnet 5 <[email protected]>