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]>
This commit is contained in:
NixReaper
2026-09-04 17:34:03 -05:00
co-authored by Claude Sonnet 5
commit c20df1c503
111 changed files with 10041 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
"""SQLAlchemy models.
Import every model module here so `Base.metadata` is fully populated for
Alembic autogeneration and `Base.metadata.create_all()` (used only in
tests) regardless of which module happens to trigger the import first.
"""
from app.models.artwork import Artwork
from app.models.base import Base
from app.models.collection import Collection, CollectionMovie, CollectionRelation, CollectionSeries
from app.models.company import Company, MovieCompany, SeriesCompany
from app.models.credit import Credit
from app.models.external_id import ExternalID
from app.models.genre import Genre, MovieGenre, SeriesGenre
from app.models.keyword import Keyword, MovieKeyword, SeriesKeyword
from app.models.language_country import Certification, Country, Language
from app.models.movie import (
Movie,
MovieAlternateTitle,
MovieProductionCountry,
MovieRecommendation,
MovieSpokenLanguage,
ReleaseDate,
)
from app.models.person import Person
from app.models.provider import IngestionJob, Provider, ProviderRecord
from app.models.tv import (
Episode,
Season,
Series,
SeriesAlternateTitle,
SeriesProductionCountry,
SeriesSpokenLanguage,
)
from app.models.video import Video
__all__ = [
"Base",
"Artwork",
"Collection",
"CollectionMovie",
"CollectionSeries",
"CollectionRelation",
"Company",
"MovieCompany",
"SeriesCompany",
"Credit",
"ExternalID",
"Genre",
"MovieGenre",
"SeriesGenre",
"Keyword",
"MovieKeyword",
"SeriesKeyword",
"Language",
"Country",
"Certification",
"Movie",
"MovieAlternateTitle",
"MovieSpokenLanguage",
"MovieProductionCountry",
"ReleaseDate",
"MovieRecommendation",
"Person",
"Provider",
"ProviderRecord",
"IngestionJob",
"Series",
"SeriesAlternateTitle",
"SeriesSpokenLanguage",
"SeriesProductionCountry",
"Season",
"Episode",
"Video",
]
+52
View File
@@ -0,0 +1,52 @@
"""Artwork as a first-class entity (section 10) — never a `poster_url`
column on media tables, so provenance/licensing can be tracked per asset."""
from __future__ import annotations
import uuid
from sqlalchemy import Boolean, Float, ForeignKey, Index, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.enums import ArtworkType
from app.models.provider import Provider
class Artwork(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "artwork"
__table_args__ = (
Index("ix_artwork_entity", "entity_type", "entity_id", "artwork_type"),
)
# Polymorphic target: "movie", "series", "season", "episode", "person",
# "company", "collection".
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
entity_id: Mapped[uuid.UUID] = mapped_column(nullable=False)
artwork_type: Mapped[ArtworkType] = mapped_column(nullable=False)
language_code: Mapped[str | None] = mapped_column(
ForeignKey("languages.code"), nullable=True
)
width: Mapped[int | None] = mapped_column(Integer, nullable=True)
height: Mapped[int | None] = mapped_column(Integer, nullable=True)
aspect_ratio: Mapped[float | None] = mapped_column(Float, nullable=True)
vote_average: Mapped[float | None] = mapped_column(Float, nullable=True)
is_primary: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
provider_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("providers.id", ondelete="CASCADE"), nullable=False
)
provider_asset_id: Mapped[str | None] = mapped_column(String(200), nullable=True)
original_url: Mapped[str] = mapped_column(String(1000), nullable=False)
local_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
checksum: Mapped[str | None] = mapped_column(String(128), nullable=True)
attribution: Mapped[str | None] = mapped_column(Text, nullable=True)
license: Mapped[str | None] = mapped_column(Text, nullable=True)
provider: Mapped[Provider] = relationship()
__all__ = ["Artwork"]
+80
View File
@@ -0,0 +1,80 @@
"""Declarative base and shared mixins for all Aurora Core models."""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, MetaData, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from uuid6 import uuid7
from app.models.enums import (
ArtworkType,
CompanyType,
IngestionJobStatus,
MatchConfidence,
MovieStatus,
SeriesStatus,
VideoType,
)
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
def _enum_column(enum_cls: type) -> Enum:
"""Store our str-Enum members by `.value` (lowercase snake_case), not
`.name`, which is what SQLAlchemy's Enum type does by default. Without
this, the DB would store e.g. "RELEASED" while every part of the
codebase (schemas, ingestion, API filters) works with "released"."""
return Enum(
enum_cls,
values_callable=lambda obj: [e.value for e in obj],
name=f"{enum_cls.__name__.lower()}_enum",
)
class Base(DeclarativeBase):
"""Base class for all ORM models. Naming convention keeps Alembic's
autogenerated constraint names stable across databases."""
metadata = MetaData(naming_convention=NAMING_CONVENTION)
type_annotation_map = {
MovieStatus: _enum_column(MovieStatus),
SeriesStatus: _enum_column(SeriesStatus),
ArtworkType: _enum_column(ArtworkType),
VideoType: _enum_column(VideoType),
CompanyType: _enum_column(CompanyType),
IngestionJobStatus: _enum_column(IngestionJobStatus),
MatchConfidence: _enum_column(MatchConfidence),
}
class UUIDPrimaryKeyMixin:
"""UUIDv7 primary key. Time-ordered so it stays btree-friendly and can
double as a creation-order cursor, unlike a random UUIDv4."""
id: Mapped[uuid.UUID] = mapped_column(
primary_key=True,
default=lambda: uuid.UUID(str(uuid7())),
)
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
+59
View File
@@ -0,0 +1,59 @@
"""Collections/franchises as first-class entities (e.g. "Alien", "MCU")."""
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
class Collection(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "collections"
name: Mapped[str] = mapped_column(String(300), nullable=False, index=True)
overview: Mapped[str | None] = mapped_column(Text, nullable=True)
slug: Mapped[str] = mapped_column(String(350), unique=True, nullable=False)
class CollectionMovie(Base):
__tablename__ = "collection_movies"
collection_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("collections.id", ondelete="CASCADE"), primary_key=True
)
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
class CollectionSeries(Base):
__tablename__ = "collection_series"
collection_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("collections.id", ondelete="CASCADE"), primary_key=True
)
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), primary_key=True
)
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
class CollectionRelation(Base):
"""Relationships between collections (e.g. shared universe)."""
__tablename__ = "collection_relations"
collection_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("collections.id", ondelete="CASCADE"), primary_key=True
)
related_collection_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("collections.id", ondelete="CASCADE"), primary_key=True
)
relation_type: Mapped[str] = mapped_column(String(50), default="related", nullable=False)
__all__ = ["Collection", "CollectionMovie", "CollectionSeries", "CollectionRelation"]
+62
View File
@@ -0,0 +1,62 @@
"""Production companies, studios, networks, and distributors.
Modeled as a single `Company` table distinguished by `company_type` per the
spec (section 8), rather than a separate Network entity — a TV network and a
studio share the same shape (name, logo, country, parent company)."""
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.enums import CompanyType
class Company(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "companies"
name: Mapped[str] = mapped_column(String(300), nullable=False, index=True)
company_type: Mapped[CompanyType] = mapped_column(
default=CompanyType.PRODUCTION_COMPANY, nullable=False
)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
country_code: Mapped[str | None] = mapped_column(
ForeignKey("countries.code"), nullable=True
)
headquarters: Mapped[str | None] = mapped_column(String(300), nullable=True)
homepage: Mapped[str | None] = mapped_column(String(500), nullable=True)
parent_company_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("companies.id", ondelete="SET NULL"), nullable=True
)
parent_company: Mapped[Company | None] = relationship(remote_side="Company.id")
class MovieCompany(Base):
__tablename__ = "movie_companies"
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
company_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("companies.id", ondelete="CASCADE"), primary_key=True
)
role: Mapped[str | None] = mapped_column(String(50), nullable=True)
class SeriesCompany(Base):
__tablename__ = "series_companies"
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), primary_key=True
)
company_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("companies.id", ondelete="CASCADE"), primary_key=True
)
role: Mapped[str | None] = mapped_column(String(50), nullable=True)
__all__ = ["Company", "MovieCompany", "SeriesCompany"]
+50
View File
@@ -0,0 +1,50 @@
"""Flexible, normalized credit system (section 7).
A `Credit` links a `Person` to any media entity (movie, series, episode)
with a department/job/character, instead of duplicating cast/crew as text
blobs on the media tables.
"""
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, Index, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.person import Person
from app.models.provider import Provider
class Credit(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "credits"
__table_args__ = (Index("ix_credits_entity", "entity_type", "entity_id"),)
person_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("people.id", ondelete="CASCADE"), nullable=False
)
# Polymorphic target: "movie", "series", "episode".
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
entity_id: Mapped[uuid.UUID] = mapped_column(nullable=False)
# "Acting", "Directing", "Writing", "Production", "Camera", "Sound",
# "Editing", "Art", "Costume & Make-Up", "Crew", "Creator", ...
department: Mapped[str] = mapped_column(String(100), nullable=False)
job: Mapped[str | None] = mapped_column(String(150), nullable=True)
character: Mapped[str | None] = mapped_column(String(300), nullable=True)
billing_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
is_guest: Mapped[bool] = mapped_column(default=False, nullable=False)
episode_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
provider_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("providers.id", ondelete="SET NULL"), nullable=True
)
person: Mapped[Person] = relationship()
provider: Mapped[Provider | None] = relationship()
__all__ = ["Credit"]
+84
View File
@@ -0,0 +1,84 @@
"""Shared enumerations used across Aurora Core models."""
from __future__ import annotations
import enum
class MovieStatus(enum.StrEnum):
RUMORED = "rumored"
PLANNED = "planned"
IN_PRODUCTION = "in_production"
POST_PRODUCTION = "post_production"
RELEASED = "released"
CANCELED = "canceled"
class SeriesStatus(enum.StrEnum):
PLANNED = "planned"
PILOT = "pilot"
IN_PRODUCTION = "in_production"
RETURNING_SERIES = "returning_series"
ENDED = "ended"
CANCELED = "canceled"
class ArtworkType(enum.StrEnum):
POSTER = "poster"
BACKDROP = "backdrop"
LOGO = "logo"
PROFILE = "profile"
STILL = "still"
BANNER = "banner"
THUMBNAIL = "thumbnail"
class VideoType(enum.StrEnum):
TRAILER = "trailer"
TEASER = "teaser"
CLIP = "clip"
FEATURETTE = "featurette"
BEHIND_THE_SCENES = "behind_the_scenes"
INTERVIEW = "interview"
OTHER = "other"
class CompanyType(enum.StrEnum):
PRODUCTION_COMPANY = "production_company"
NETWORK = "network"
STUDIO = "studio"
DISTRIBUTOR = "distributor"
class EntityType(enum.StrEnum):
"""Kinds of entity that polymorphic tables (credits, artwork, videos,
external IDs, release dates, provider records) can attach to."""
MOVIE = "movie"
SERIES = "series"
SEASON = "season"
EPISODE = "episode"
PERSON = "person"
COMPANY = "company"
COLLECTION = "collection"
class IngestionJobStatus(enum.StrEnum):
PENDING = "pending"
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"
RETRYING = "retrying"
class MatchConfidence(enum.StrEnum):
"""How confident the entity matcher was when linking a provider record
to an Aurora entity. Anything below HIGH should be reviewable rather
than silently merged — see docs/architecture.md."""
EXACT = "exact"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
AMBIGUOUS = "ambiguous"
NEW = "new"
+45
View File
@@ -0,0 +1,45 @@
"""Generalized external identifier system.
Links an Aurora entity (any type) to its identifier in an external provider
(TMDb, IMDb, TVDb, Wikidata, ...). External IDs are references, never
primary keys — see app/core/ids.py and docs/architecture.md.
"""
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.provider import Provider
class ExternalID(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "external_ids"
__table_args__ = (
# A provider's ID for a given entity type must be unique (one Aurora
# entity per provider-native ID).
UniqueConstraint(
"provider_id", "entity_type", "external_id", name="uq_external_id_source"
),
# An Aurora entity has at most one mapping per provider.
UniqueConstraint(
"entity_type", "entity_id", "provider_id", name="uq_external_id_entity_provider"
),
)
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
entity_id: Mapped[uuid.UUID] = mapped_column(nullable=False)
provider_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("providers.id", ondelete="CASCADE"), nullable=False
)
external_id: Mapped[str] = mapped_column(String(200), nullable=False)
url: Mapped[str | None] = mapped_column(String(500), nullable=True)
provider: Mapped[Provider] = relationship()
__all__ = ["ExternalID"]
+42
View File
@@ -0,0 +1,42 @@
"""Genres and their movie/series associations."""
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, UUIDPrimaryKeyMixin
class Genre(Base, UUIDPrimaryKeyMixin):
__tablename__ = "genres"
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
class MovieGenre(Base):
__tablename__ = "movie_genres"
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
genre_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("genres.id", ondelete="CASCADE"), primary_key=True
)
class SeriesGenre(Base):
__tablename__ = "series_genres"
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), primary_key=True
)
genre_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("genres.id", ondelete="CASCADE"), primary_key=True
)
__all__ = ["Genre", "MovieGenre", "SeriesGenre"]
+42
View File
@@ -0,0 +1,42 @@
"""Keywords/tags and their movie/series associations."""
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, UUIDPrimaryKeyMixin
class Keyword(Base, UUIDPrimaryKeyMixin):
__tablename__ = "keywords"
name: Mapped[str] = mapped_column(String(150), unique=True, nullable=False)
slug: Mapped[str] = mapped_column(String(150), unique=True, nullable=False)
class MovieKeyword(Base):
__tablename__ = "movie_keywords"
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
keyword_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("keywords.id", ondelete="CASCADE"), primary_key=True
)
class SeriesKeyword(Base):
__tablename__ = "series_keywords"
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), primary_key=True
)
keyword_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("keywords.id", ondelete="CASCADE"), primary_key=True
)
__all__ = ["Keyword", "MovieKeyword", "SeriesKeyword"]
+46
View File
@@ -0,0 +1,46 @@
"""Reference data: languages, countries, certifications."""
from __future__ import annotations
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
class Language(Base):
"""ISO 639-1 language codes (e.g. 'en', 'ja')."""
__tablename__ = "languages"
code: Mapped[str] = mapped_column(String(10), primary_key=True)
english_name: Mapped[str] = mapped_column(String(200), nullable=False)
native_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
class Country(Base):
"""ISO 3166-1 alpha-2 country codes (e.g. 'US', 'GB')."""
__tablename__ = "countries"
code: Mapped[str] = mapped_column(String(2), primary_key=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
class Certification(Base, UUIDPrimaryKeyMixin, TimestampMixin):
"""Content rating within a country's rating system, e.g. US/MPA/PG-13."""
__tablename__ = "certifications"
__table_args__ = (
UniqueConstraint("country_code", "system", "rating", name="uq_certification_scope"),
)
country_code: Mapped[str] = mapped_column(ForeignKey("countries.code"), nullable=False)
system: Mapped[str] = mapped_column(String(50), nullable=False) # e.g. "MPA", "BBFC"
rating: Mapped[str] = mapped_column(String(20), nullable=False) # e.g. "PG-13"
meaning: Mapped[str | None] = mapped_column(Text, nullable=True)
country: Mapped[Country] = relationship()
__all__ = ["Language", "Country", "Certification"]
+127
View File
@@ -0,0 +1,127 @@
"""Movie entity and its directly-owned satellite tables (section 4)."""
from __future__ import annotations
import uuid
from datetime import date
from sqlalchemy import BigInteger, Date, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.collection import Collection
from app.models.enums import MovieStatus
class Movie(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "movies"
title: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
original_title: Mapped[str | None] = mapped_column(String(500), nullable=True)
# Title with leading articles ("The", "A") stripped, for stable sorting.
sort_title: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
overview: Mapped[str | None] = mapped_column(Text, nullable=True)
tagline: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[MovieStatus] = mapped_column(default=MovieStatus.RELEASED, nullable=False)
original_language_code: Mapped[str | None] = mapped_column(
ForeignKey("languages.code"), nullable=True
)
primary_release_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
runtime_minutes: Mapped[int | None] = mapped_column(Integer, nullable=True)
budget: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
revenue: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
popularity: Mapped[float | None] = mapped_column(Float, nullable=True)
vote_average: Mapped[float | None] = mapped_column(Float, nullable=True)
vote_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
adult: Mapped[bool] = mapped_column(default=False, nullable=False)
collection_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("collections.id", ondelete="SET NULL"), nullable=True
)
# Best-known authoritative source for this record at creation time. Full
# field-level provenance lives in provider_records; this is a coarse
# pointer for quick display. See docs/architecture.md.
source_provider_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("providers.id", ondelete="SET NULL"), nullable=True
)
collection: Mapped[Collection | None] = relationship()
class MovieAlternateTitle(Base, UUIDPrimaryKeyMixin):
__tablename__ = "movie_alternate_titles"
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), nullable=False, index=True
)
title: Mapped[str] = mapped_column(String(500), nullable=False)
country_code: Mapped[str | None] = mapped_column(ForeignKey("countries.code"), nullable=True)
type: Mapped[str | None] = mapped_column(String(50), nullable=True) # e.g. "working_title"
class MovieSpokenLanguage(Base):
__tablename__ = "movie_spoken_languages"
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
language_code: Mapped[str] = mapped_column(
ForeignKey("languages.code"), primary_key=True
)
class MovieProductionCountry(Base):
__tablename__ = "movie_production_countries"
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
country_code: Mapped[str] = mapped_column(ForeignKey("countries.code"), primary_key=True)
class ReleaseDate(Base, UUIDPrimaryKeyMixin):
"""Per-country/per-type release dates and certifications for a movie."""
__tablename__ = "release_dates"
entity_type: Mapped[str] = mapped_column(String(30), default="movie", nullable=False)
entity_id: Mapped[uuid.UUID] = mapped_column(nullable=False, index=True)
country_code: Mapped[str] = mapped_column(ForeignKey("countries.code"), nullable=False)
release_date: Mapped[date] = mapped_column(Date, nullable=False)
# "theatrical", "digital", "physical", "premiere", "tv"
release_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
certification_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("certifications.id", ondelete="SET NULL"), nullable=True
)
note: Mapped[str | None] = mapped_column(Text, nullable=True)
class MovieRecommendation(Base):
"""Recommendations/similar-title edges between movies."""
__tablename__ = "movie_recommendations"
movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
recommended_movie_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("movies.id", ondelete="CASCADE"), primary_key=True
)
rank: Mapped[int | None] = mapped_column(Integer, nullable=True)
source: Mapped[str | None] = mapped_column(String(50), nullable=True)
__all__ = [
"Movie",
"MovieAlternateTitle",
"MovieSpokenLanguage",
"MovieProductionCountry",
"ReleaseDate",
"MovieRecommendation",
]
+32
View File
@@ -0,0 +1,32 @@
"""People — first-class Aurora entities (section 6)."""
from __future__ import annotations
from datetime import date
from sqlalchemy import JSON, Date, Float, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
JSONVariant = JSONB().with_variant(JSON(), "sqlite")
class Person(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "people"
name: Mapped[str] = mapped_column(String(300), nullable=False, index=True)
aliases: Mapped[list[str]] = mapped_column(JSONVariant, default=list)
biography: Mapped[str | None] = mapped_column(Text, nullable=True)
birthday: Mapped[date | None] = mapped_column(Date, nullable=True)
deathday: Mapped[date | None] = mapped_column(Date, nullable=True)
birthplace: Mapped[str | None] = mapped_column(String(300), nullable=True)
# Free-text, only populated when a source legitimately provides it —
# never inferred.
gender: Mapped[str | None] = mapped_column(String(30), nullable=True)
known_for_department: Mapped[str | None] = mapped_column(String(100), nullable=True)
popularity: Mapped[float | None] = mapped_column(Float, nullable=True)
__all__ = ["Person"]
+108
View File
@@ -0,0 +1,108 @@
"""Provider abstraction and provenance tables.
`Provider` identifies a metadata source (TMDb, IMDb dataset export, ...).
`ProviderRecord` is the provenance ledger: one row per (provider, entity
type, provider-native ID) linking to the Aurora entity it was matched to,
retaining the raw payload and match confidence so ambiguous matches stay
reviewable instead of being silently merged (see docs/architecture.md).
`IngestionJob` tracks async ingestion work so jobs are observable, retryable,
and protected against duplicate processing via `idempotency_key`.
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.enums import IngestionJobStatus, MatchConfidence
JSONVariant = JSONB().with_variant(JSON(), "sqlite")
class Provider(Base, UUIDPrimaryKeyMixin, TimestampMixin):
"""A metadata data source. Providers are replaceable adapters, never
Aurora's identity system — see app/providers/base.py."""
__tablename__ = "providers"
slug: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) # "tmdb", "imdb"
name: Mapped[str] = mapped_column(String(200), nullable=False)
base_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
is_active: Mapped[bool] = mapped_column(default=True, nullable=False)
config: Mapped[dict] = mapped_column(JSONVariant, default=dict)
class ProviderRecord(Base, UUIDPrimaryKeyMixin, TimestampMixin):
"""Provenance ledger row: this provider's native record, and what Aurora
entity (if any) it has been matched to."""
__tablename__ = "provider_records"
__table_args__ = (
UniqueConstraint(
"provider_id", "entity_type", "external_id", name="uq_provider_record_source"
),
)
provider_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("providers.id", ondelete="CASCADE"), nullable=False
)
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
external_id: Mapped[str] = mapped_column(String(200), nullable=False)
# Polymorphic reference to the Aurora entity this record resolved to.
# No DB-level FK (the target table varies by entity_type); integrity is
# enforced by the entity matcher / repositories, not the schema.
aurora_entity_id: Mapped[uuid.UUID | None] = mapped_column(nullable=True)
raw_data: Mapped[dict] = mapped_column(JSONVariant, default=dict)
source_version: Mapped[str | None] = mapped_column(String(100), nullable=True)
match_confidence: Mapped[MatchConfidence] = mapped_column(
default=MatchConfidence.NEW, nullable=False
)
match_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
ingestion_job_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("ingestion_jobs.id", ondelete="SET NULL"), nullable=True
)
last_synced_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
provider: Mapped[Provider] = relationship()
class IngestionJob(Base, UUIDPrimaryKeyMixin, TimestampMixin):
"""A unit of asynchronous ingestion work processed by workers."""
__tablename__ = "ingestion_jobs"
__table_args__ = (UniqueConstraint("idempotency_key", name="uq_ingestion_job_idempotency"),)
provider_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("providers.id", ondelete="CASCADE"), nullable=False
)
job_type: Mapped[str] = mapped_column(String(50), nullable=False) # "movie_sync", "full_sync"
entity_type: Mapped[str | None] = mapped_column(String(30), nullable=True)
external_id: Mapped[str | None] = mapped_column(String(200), nullable=True)
idempotency_key: Mapped[str] = mapped_column(String(300), nullable=False)
status: Mapped[IngestionJobStatus] = mapped_column(
default=IngestionJobStatus.PENDING, nullable=False
)
attempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
max_attempts: Mapped[int] = mapped_column(Integer, default=5, nullable=False)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
error: Mapped[str | None] = mapped_column(Text, nullable=True)
result_aurora_id: Mapped[uuid.UUID | None] = mapped_column(nullable=True)
provider: Mapped[Provider] = relationship()
__all__ = ["Provider", "ProviderRecord", "IngestionJob"]
+118
View File
@@ -0,0 +1,118 @@
"""Television model: Series -> Seasons -> Episodes (section 5)."""
from __future__ import annotations
import uuid
from datetime import date
from sqlalchemy import Date, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.enums import SeriesStatus
class Series(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "series"
title: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
original_title: Mapped[str | None] = mapped_column(String(500), nullable=True)
sort_title: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
overview: Mapped[str | None] = mapped_column(Text, nullable=True)
tagline: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[SeriesStatus] = mapped_column(
default=SeriesStatus.RETURNING_SERIES, nullable=False
)
original_language_code: Mapped[str | None] = mapped_column(
ForeignKey("languages.code"), nullable=True
)
first_air_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True)
last_air_date: Mapped[date | None] = mapped_column(Date, nullable=True)
number_of_seasons: Mapped[int | None] = mapped_column(Integer, nullable=True)
number_of_episodes: Mapped[int | None] = mapped_column(Integer, nullable=True)
popularity: Mapped[float | None] = mapped_column(nullable=True)
vote_average: Mapped[float | None] = mapped_column(nullable=True)
vote_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
source_provider_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("providers.id", ondelete="SET NULL"), nullable=True
)
class SeriesAlternateTitle(Base, UUIDPrimaryKeyMixin):
__tablename__ = "series_alternate_titles"
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), nullable=False, index=True
)
title: Mapped[str] = mapped_column(String(500), nullable=False)
country_code: Mapped[str | None] = mapped_column(ForeignKey("countries.code"), nullable=True)
type: Mapped[str | None] = mapped_column(String(50), nullable=True)
class SeriesSpokenLanguage(Base):
__tablename__ = "series_spoken_languages"
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), primary_key=True
)
language_code: Mapped[str] = mapped_column(ForeignKey("languages.code"), primary_key=True)
class SeriesProductionCountry(Base):
__tablename__ = "series_production_countries"
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), primary_key=True
)
country_code: Mapped[str] = mapped_column(ForeignKey("countries.code"), primary_key=True)
class Season(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "seasons"
__table_args__ = (UniqueConstraint("series_id", "season_number", name="uq_season_number"),)
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), nullable=False, index=True
)
season_number: Mapped[int] = mapped_column(Integer, nullable=False)
title: Mapped[str | None] = mapped_column(String(500), nullable=True)
overview: Mapped[str | None] = mapped_column(Text, nullable=True)
air_date: Mapped[date | None] = mapped_column(Date, nullable=True)
episode_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
class Episode(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "episodes"
__table_args__ = (
UniqueConstraint("season_id", "episode_number", name="uq_episode_number"),
)
season_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("seasons.id", ondelete="CASCADE"), nullable=False, index=True
)
# Denormalized for direct filtering/joins without hopping through seasons.
series_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("series.id", ondelete="CASCADE"), nullable=False, index=True
)
season_number: Mapped[int] = mapped_column(Integer, nullable=False)
episode_number: Mapped[int] = mapped_column(Integer, nullable=False)
title: Mapped[str | None] = mapped_column(String(500), nullable=True)
overview: Mapped[str | None] = mapped_column(Text, nullable=True)
air_date: Mapped[date | None] = mapped_column(Date, nullable=True)
runtime_minutes: Mapped[int | None] = mapped_column(Integer, nullable=True)
__all__ = [
"Series",
"SeriesAlternateTitle",
"SeriesSpokenLanguage",
"SeriesProductionCountry",
"Season",
"Episode",
]
+50
View File
@@ -0,0 +1,50 @@
"""Videos/trailers as normalized entities (section 11).
Aurora never downloads copyrighted video media — only stores the
provider's reference/URL and metadata about it.
"""
from __future__ import annotations
import uuid
from datetime import date
from sqlalchemy import Boolean, Date, ForeignKey, Index, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from app.models.enums import VideoType
from app.models.provider import Provider
class Video(Base, UUIDPrimaryKeyMixin, TimestampMixin):
__tablename__ = "videos"
__table_args__ = (
Index("ix_videos_entity", "entity_type", "entity_id"),
UniqueConstraint("provider_id", "provider_video_id", name="uq_video_provider_source"),
)
# Polymorphic target: "movie", "series", "season", "episode".
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
entity_id: Mapped[uuid.UUID] = mapped_column(nullable=False)
video_type: Mapped[VideoType] = mapped_column(nullable=False)
provider_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("providers.id", ondelete="CASCADE"), nullable=False
)
provider_video_id: Mapped[str] = mapped_column(String(200), nullable=False)
site: Mapped[str | None] = mapped_column(String(100), nullable=True) # "YouTube", "Vimeo"
url: Mapped[str] = mapped_column(String(1000), nullable=False)
language_code: Mapped[str | None] = mapped_column(
ForeignKey("languages.code"), nullable=True
)
country_code: Mapped[str | None] = mapped_column(ForeignKey("countries.code"), nullable=True)
is_official: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
published_at: Mapped[date | None] = mapped_column(Date, nullable=True)
provider: Mapped[Provider] = relationship()
__all__ = ["Video"]