Add watch providers (where to stream/rent/buy)
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
New TMDb integration end to end: append_to_response "watch/providers", normalizer parsing grouped by country/monetization type, a WatchProvider catalog entry (external-id keyed, same get_or_create pattern as Company/ Collection/Person) + polymorphic WatchProviderOffering table (new migration 0002), ingestion write (including provider logos via the existing Artwork table), and API exposure -- embedded on movie/series detail plus a dedicated /watch-providers sub-route, shaped to mirror TMDb's own response for easy porting. Note: this data comes from JustWatch via TMDb and carries its own attribution requirement separate from TMDb's general one -- flagged in docs/api.md, must be satisfied before showing this in the frontend. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
74ebad379a
commit
6c95e18aad
@@ -33,6 +33,7 @@ from app.models.tv import (
|
||||
SeriesSpokenLanguage,
|
||||
)
|
||||
from app.models.video import Video
|
||||
from app.models.watch_provider import WatchProvider, WatchProviderOffering
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
@@ -72,4 +73,6 @@ __all__ = [
|
||||
"Season",
|
||||
"Episode",
|
||||
"Video",
|
||||
"WatchProvider",
|
||||
"WatchProviderOffering",
|
||||
]
|
||||
|
||||
@@ -61,6 +61,7 @@ class EntityType(enum.StrEnum):
|
||||
PERSON = "person"
|
||||
COMPANY = "company"
|
||||
COLLECTION = "collection"
|
||||
WATCH_PROVIDER = "watch_provider"
|
||||
|
||||
|
||||
class IngestionJobStatus(enum.StrEnum):
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Where-to-watch offerings (JustWatch data via TMDb's watch/providers
|
||||
endpoint) -- section 17, additive.
|
||||
|
||||
Attribution note: TMDb's own terms require this specific data to be
|
||||
sourced back to JustWatch, not just TMDb itself -- see docs/api.md."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import ForeignKey, Index, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.models.base import Base, UUIDPrimaryKeyMixin
|
||||
|
||||
|
||||
class WatchProvider(Base, UUIDPrimaryKeyMixin):
|
||||
"""A streaming/rental/purchase service catalog entry (Netflix, Apple TV,
|
||||
etc.) -- TMDb's own provider_id links here via ExternalID, same pattern
|
||||
as Company/Collection/Person. Its logo lives in the shared polymorphic
|
||||
Artwork table (entity_type="watch_provider"), same as a Company logo."""
|
||||
|
||||
__tablename__ = "watch_providers"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
|
||||
|
||||
class WatchProviderOffering(Base, UUIDPrimaryKeyMixin):
|
||||
"""One (entity, country, monetization type, provider) row -- where a
|
||||
movie/series can be streamed/rented/bought/watched free/with ads in a
|
||||
given region. Polymorphic across movie/series, same shape as
|
||||
ReleaseDate."""
|
||||
|
||||
__tablename__ = "watch_provider_offerings"
|
||||
__table_args__ = (
|
||||
Index("ix_watch_provider_offerings_entity", "entity_type", "entity_id"),
|
||||
)
|
||||
|
||||
entity_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
||||
entity_id: Mapped[uuid.UUID] = mapped_column(nullable=False)
|
||||
|
||||
country_code: Mapped[str] = mapped_column(ForeignKey("countries.code"), nullable=False)
|
||||
# "flatrate" (subscription) | "rent" | "buy" | "ads" | "free"
|
||||
monetization_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
watch_provider_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("watch_providers.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
display_priority: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
# TMDb/JustWatch's deep link for this (entity, country) -- identical
|
||||
# across every provider row for the same country, denormalized here
|
||||
# rather than a separate link-per-country table since it's cheap and
|
||||
# keeps the read path to a single table.
|
||||
link: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
watch_provider: Mapped[WatchProvider] = relationship()
|
||||
|
||||
|
||||
__all__ = ["WatchProvider", "WatchProviderOffering"]
|
||||
Reference in New Issue
Block a user