69ce41d54f41ab66480f2864a870052cfe3df600
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c4bfe6cf4 |
Fix sync_artwork clobbering is_primary via duplicate list entries
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
TMDb's images.posters/backdrops alternates commonly include the *same* image as the top-level poster_path/backdrop_path -- so normalize_movie legitimately produces the same provider_asset_id twice in one artwork_list: once from the top-level field (is_primary=True), once from the general alternates loop (is_primary=False, the default). sync_artwork matched both occurrences to the same existing DB row by provider_asset_id and did a plain overwrite each time, so whichever occurrence it processed last won -- the non-primary duplicate always came second in list order, so it silently un-set is_primary right after the first occurrence had just set it. Caught by re-ingesting The Matrix on a devbox with the artwork download feature live: the movie ingested successfully, but zero download jobs were ever enqueued because get_primary_artwork_ids found nothing marked is_primary -- the flag had been clobbered back to false immediately after being set. Fixed by resetting is_primary on all existing rows up front (so a provider dropping its "primary" pick on an image actually clears, not just adds) and accumulating with OR within the batch instead of a plain overwrite, so any primary occurrence in one sync sticks regardless of processing order. Also dedupes brand-new rows within the same batch the same way, closing the equivalent bug for images that don't have an existing row yet. Co-Authored-By: Claude Sonnet 5 <[email protected]> |
||
|
|
fef914db36 |
Add artwork self-hosting: local-disk download worker + storage
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
Section 10: every image API responses reference has pointed straight at TMDb's CDN since day one -- Aurora never actually downloaded or served artwork itself, despite storing dimensions/checksum/local_path columns for exactly that. Closes the gap: - is_primary was declared on the Artwork model but never actually set anywhere -- every row defaulted to False. Now the normalizer marks the one image each provider designates as *the* image for an entity (TMDb's poster_path/backdrop_path/profile_path) as primary, distinct from the dozens of alternates also ingested, including correctly marking an existing row primary (not inserting a duplicate) when that image already appears in the alternates list. - app/services/artwork/: an ArtworkStorage interface with a LocalDiskStorage implementation (checksum-named files, single-host deployment via a Docker volume shared between api and worker -- see docs/architecture.md for the S3/R2 upgrade path), and a download pipeline with content-type validation, a size cap, and path-traversal protection. - After movie/series/person ingestion succeeds, tasks.py cascades one download_artwork_task per primary artwork id via arq's ctx['redis'] (same fan-out pattern as the season cascade) -- fire-and-forget, not tracked in ingestion_jobs, since a failed image cache is a known-safe fallback to the provider URL, not a user-facing failure. - app/main.py serves MEDIA_ROOT at MEDIA_URL_PREFIX; local_path is preferred over original_url once set (already-existing fallback logic in app/services/common.py:artwork_url, now actually exercised). Also fixes a real interaction bug this surfaced: every ingestion pipeline deleted and recreated all of an entity's Artwork rows on each re-sync, which would have silently wiped local_path/checksum on every re-ingestion, forcing a full re-download every time and defeating the point of caching locally at all. app/repositories/polymorphic.py's new sync_artwork upserts by provider_asset_id (TMDb's stable per-image path) instead, preserving downloaded copies across re-sync, and replaces three near-identical delete+recreate helpers that used to live separately in movie_ingestion.py, series_ingestion.py, and person_ingestion.py. Co-Authored-By: Claude Sonnet 5 <[email protected]> |
||
|
|
cd4dd98adb |
Make failed ingestion jobs actually retry, not just say "retrying"
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
arq only auto-retries a job on its own Retry exception (or CancelledError) -- never on an arbitrary exception a task raises. _run_ingestion caught failures, recorded them via mark_job_failed (which sets ingestion_jobs.status to "retrying" when attempts hasn't hit max_attempts), and then just re-raised the original exception -- which arq treats as a terminal failure. The DB said "retrying" forever; nothing ever put the job back on the queue. Confirmed live: cascading Better Call Saul's 6 seasons hit the get_or_create race fixed in the previous commit on 4 of them, and the worker's own shutdown summary showed "4 failed, 0 retries" -- every one of those was permanently lost despite max_attempts=5 suggesting otherwise. _run_ingestion now raises arq.Retry (capped exponential backoff) when mark_job_failed left the job in RETRYING state, so arq actually requeues it; only once attempts are exhausted does the original exception propagate as a genuine terminal failure. Co-Authored-By: Claude Sonnet 5 <[email protected]> |
||
|
|
64169c4aee |
Fix test factory Provider collision from ingestion-cascade tests
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
tests/integration/factories.py:make_provider did a raw INSERT assuming no "tmdb" Provider row existed yet. That held as long as every test used the db_session fixture's isolated, rolled-back transaction -- but the new ingestion-cascade tests call get_or_create_ingestion_job, which really commits via session_scope()'s own connection, outside that per-test isolation. Once that ran, every later test using the raw-insert factory hit "duplicate key value violates unique constraint uq_providers_slug". Fixed by using the app's own get_or_create_provider in the factory instead of a bare insert, which is also just correct: provider slugs are unique in production too, so the test helper should behave the same way real ingestion code does rather than assume a pristine table. Co-Authored-By: Claude Sonnet 5 <[email protected]> |
||
|
|
b4d1e8f755 |
Add opt-in season cascade to series ingestion
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
Ingesting a series only ever produced season stubs -- getting actual episodes meant calling POST /admin/ingest/season once per season by hand, which is exactly what manual end-to-end testing kept requiring for every multi-season show. POST /api/v1/admin/ingest/series now takes include_seasons (default false, since a show can have dozens of seasons and that's dozens of TMDb calls fired from one request). When true, ingest_series_task fans out one ingest_season job per season once the series succeeds, using arq's own ctx['redis'] connection for the job (the same pattern arq's docs recommend for job chaining) rather than a second unrelated connection. Each fanned-out season job still goes through get_or_create_ingestion_job, so re-running a cascaded series ingestion reuses the same season job rows instead of creating duplicates. Co-Authored-By: Claude Sonnet 5 <[email protected]> |
||
|
|
ec6b5bdd7d |
Surface series/episode credits in person filmography
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
/people/{id}/credits only ever looked at entity_type == "movie",
silently dropping the series-level cast/crew and episode-level
guest-star Credit rows that series_ingestion.py has been writing all
along -- a person ingested via a TV show would show an empty or
movie-only filmography no matter how much TV work was actually in the
database.
Regular series credits map straight to a filmography row, same shape
as movies. Guest-star episode credits are aggregated to one row per
(series, department/job/character) with an episode_count, rather than
one row per episode -- a prolific guest actor would otherwise flood
the list with dozens of near-identical entries. Added
tv_repository.get_series_by_ids/get_episodes_by_ids (bulk, mirroring
the existing movie_repository pattern) to support it.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
||
|
|
3acf00e9f0 |
Fix test bug: await inside a generator expression passed to all()
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
Python turns a generator expression containing await into an async generator (since it's evaluated inside an async def), which the built-in all() can't consume -- it raised "TypeError: 'async_generator' object is not iterable" rather than testing anything. Replaced with a plain loop. Found by actually running the new integration tests. Co-Authored-By: Claude Sonnet 5 <[email protected]> |
||
|
|
3f6332559a |
Add series/season/episode ingestion pipeline
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
Movie and person ingestion existed, but the series/season/episode API
endpoints had no way to actually populate them from TMDb -- the
biggest remaining gap in Core 0.1's television scope.
- normalize_series/normalize_season in the metadata normalizer,
including two TMDb quirks that would otherwise silently produce
empty data: /tv nests keywords and alternative_titles under
"results" (not "keywords"/"titles" like /movie), and series-level
creators come from a separate created_by array, not the credits
cast/crew lists.
- ingest_series upserts the Series row plus season *stubs* (number,
title, air date, poster -- everything TMDb inlines on /tv/{id});
ingest_season fetches and upserts one season's full episode list,
and requires the series to already exist (TMDb has no single
endpoint for a whole series' episodes, so this has to be a separate
per-season call).
- Wired through the same job-lifecycle path as movie/person ingestion
(runner.py, arq tasks, admin endpoints), with the shared boilerplate
between them factored into runner._run_ingestion and
admin.py:_enqueue rather than copy-pasted a third and fourth time.
- Unit tests for both normalizers (runnable without a DB) and
integration tests for the full ingest_series -> ingest_season flow,
including the series-must-exist-first error and idempotency on
re-ingestion.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
||
|
|
c20df1c503 |
Initial Aurora Core 0.1 implementation
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
Production-grade media metadata backend per docs/aurora-core-spec.md: Aurora-owned UUIDv7 identity system, normalized movie/TV/people/credits schema, provider abstraction with a TMDb adapter, an idempotent async ingestion pipeline (normalize -> match -> upsert -> provenance), a Postgres FTS/trigram search layer, a stateless FastAPI v1 API with Redis caching/rate-limiting, arq workers, dev/prod Docker Compose topologies, unit + integration + load tests, CI, and architecture/ database/API/ERD documentation. Co-Authored-By: Claude Sonnet 5 <[email protected]> |