Matches each mirrored Plex movie/show to TMDB by its stored canonical guid, fetches once (append_to_response), and PERSISTS the enrichment onto the row — served from the DB, never a live TMDB call at serve time. Additive: the Plex-sourced imdb_rating/rating are untouched; tmdb_rating is the distinct TMDb community score (ratings scope = TMDb + Plex's IMDb/audience, no OMDb/RT). - config.py: tmdb_api_key + api_base/language/interval/batch defaults. - sysconfig SPECS: tmdb_api_key (secret, Fernet) + tmdb_language, auto-rendered in the admin Config page; read like the Plex token. - migration 0071 (ADD-only nullable, reversible): plex_items + plex_shows gain tmdb_id / tmdb_enriched_at (watermark) / tmdb_rating / tmdb_vote_count / budget / revenue / keywords / crew / studios / production_countries / recommendations / backdrops; plex_people gains tmdb_person_id (the S2 foundation for S6 person bios). No watermark reset needed. - app/plex/tmdb.py: TmdbClient (mirrors PlexClient; throttled; 404->None, transient->TmdbError), parse_tmdb / apply_tmdb_detail (one-parser rule), guid_to_tmdb_id (tmdb: direct, imdb:/tvdb: via /find), enrich_batch (movies then shows, transient error leaves the watermark NULL to retry, definitive no-match still stamps so it isn't re-fetched), _link_people (upserts tmdb_person_id by name), serve_block (recommendations resolved to library rows by guid), run_enrich (no-op without a key). - scheduler: tmdb_enrich nightly job (default 120 min, no YouTube quota), runtime-tunable + "run now" from the Scheduler dashboard. - detail endpoints emit `tmdb` (movies + shows) so it's curl-verifiable now; the FE render lands in S5. Note: _link_people inserts plex_people rows for TMDB cast/crew; unreferenced crew (composers etc., not in cast_names/directors) get pruned by the orphan sweep — acceptable, the person page is cast-driven and cast rows survive. Gate: ruff + py_compile green; backend test lane FULL suite 387 passed (incl. migration drift downgrade->re-upgrade + test_plex_tmdb 9 tests). Frontend unchanged since S1. Not merged (epic branch, stacks on S1).
53 lines
2.1 KiB
Python
53 lines
2.1 KiB
Python
"""plex TMDB enrichment columns (E5 S2)
|
|
|
|
Additive TMDB-sourced fields on plex_items + plex_shows — keywords, full crew, budget/revenue, TMDb
|
|
community score, production companies/countries, recommendations, and a backdrop-path pool — plus a
|
|
`tmdb_id` cache and a `tmdb_enriched_at` watermark. Filled by the new TMDB enrichment job (matched by
|
|
the stored canonical `guid`), SERVED FROM THE ROW (never a live TMDB call). Distinct from the existing
|
|
Plex-sourced `imdb_rating`/`rating`, which are left untouched. Plus `plex_people.tmdb_person_id` (the S2
|
|
foundation for S6's person bios). ADD-only, all nullable → no table rewrite, no ACCESS EXCLUSIVE lock.
|
|
No watermark reset needed: `tmdb_enriched_at` starts NULL, so every eligible row is picked up once.
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision: str = "0071_plex_tmdb_fields"
|
|
down_revision: Union[str, None] = "0070_fold_maintenance_batch"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
_TABLES = ("plex_items", "plex_shows")
|
|
|
|
# (name, type) — same set on both movie/leaf rows and show rows. budget/revenue stay NULL for shows.
|
|
_COLS: tuple[tuple[str, sa.types.TypeEngine], ...] = (
|
|
("tmdb_id", sa.Integer()),
|
|
("tmdb_enriched_at", sa.DateTime(timezone=True)),
|
|
("tmdb_rating", sa.Float()),
|
|
("tmdb_vote_count", sa.Integer()),
|
|
("budget", sa.BigInteger()),
|
|
("revenue", sa.BigInteger()),
|
|
("keywords", postgresql.JSONB()),
|
|
("crew", postgresql.JSONB()),
|
|
("studios", postgresql.JSONB()),
|
|
("production_countries", postgresql.JSONB()),
|
|
("recommendations", postgresql.JSONB()),
|
|
("backdrops", postgresql.JSONB()),
|
|
)
|
|
|
|
|
|
def upgrade() -> None:
|
|
for t in _TABLES:
|
|
for name, type_ in _COLS:
|
|
op.add_column(t, sa.Column(name, type_, nullable=True))
|
|
op.add_column("plex_people", sa.Column("tmdb_person_id", sa.Integer(), nullable=True))
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_column("plex_people", "tmdb_person_id")
|
|
for t in _TABLES:
|
|
for name, _ in reversed(_COLS):
|
|
op.drop_column(t, name)
|