Ingest and self-host company/network logos
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
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]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
69ce41d54f
commit
c03a75989d
@@ -32,7 +32,7 @@ from app.repositories import polymorphic
|
||||
from app.repositories import reference_repository as refs
|
||||
from app.repositories.provider_repository import get_or_create_provider, upsert_provider_record
|
||||
from app.services.metadata.matcher import match_entity
|
||||
from app.services.metadata.normalizer import NormalizedMovie, normalize_movie
|
||||
from app.services.metadata.normalizer import NormalizedMovie, company_logo_artwork, normalize_movie
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -68,6 +68,15 @@ async def _replace_movie_companies(
|
||||
country_code=country_code,
|
||||
)
|
||||
session.add(MovieCompany(movie_id=movie_id, company_id=company.id, role=ref.role))
|
||||
logo = company_logo_artwork(ref.logo_path)
|
||||
if logo is not None:
|
||||
await polymorphic.sync_artwork(
|
||||
session,
|
||||
entity_type="company",
|
||||
entity_id=company.id,
|
||||
provider_id=provider_id,
|
||||
artwork_list=[logo],
|
||||
)
|
||||
|
||||
|
||||
async def _replace_movie_credits(
|
||||
|
||||
@@ -19,6 +19,7 @@ from app.models.enums import IngestionJobStatus
|
||||
from app.models.provider import IngestionJob
|
||||
from app.providers.base import MetadataProvider
|
||||
from app.providers.tmdb import TMDbProvider
|
||||
from app.repositories.movie_repository import get_movie_companies
|
||||
from app.repositories.polymorphic import get_primary_artwork_ids
|
||||
from app.repositories.provider_repository import (
|
||||
create_ingestion_job,
|
||||
@@ -28,7 +29,7 @@ from app.repositories.provider_repository import (
|
||||
mark_job_running,
|
||||
mark_job_succeeded,
|
||||
)
|
||||
from app.repositories.tv_repository import get_seasons_for_series
|
||||
from app.repositories.tv_repository import get_seasons_for_series, get_series_companies
|
||||
from app.services.ingestion.movie_ingestion import ingest_movie
|
||||
from app.services.ingestion.person_ingestion import ingest_person
|
||||
from app.services.ingestion.series_ingestion import ingest_season, ingest_series
|
||||
@@ -185,3 +186,25 @@ async def get_primary_artwork_for_download(
|
||||
|
||||
async with session_scope() as session:
|
||||
return await get_primary_artwork_ids(session, entity_type, entity_ids)
|
||||
|
||||
|
||||
async def get_movie_company_ids(movie_id: uuid.UUID) -> list[uuid.UUID]:
|
||||
"""Companies (production companies) a just-ingested movie references,
|
||||
for cascading their logo into a download too — see
|
||||
app/workers/tasks.py:_cascade_artwork_downloads. Shared across many
|
||||
movies, so this fires on every movie referencing a given company, but
|
||||
download_artwork is idempotent (skips once local_path is set)."""
|
||||
|
||||
async with session_scope() as session:
|
||||
companies = await get_movie_companies(session, movie_id)
|
||||
return [company.id for company, _role in companies]
|
||||
|
||||
|
||||
async def get_series_company_ids(series_id: uuid.UUID) -> list[uuid.UUID]:
|
||||
"""Same as get_movie_company_ids, but for a series' companies *and*
|
||||
networks (get_series_companies with no company_type filter returns
|
||||
both)."""
|
||||
|
||||
async with session_scope() as session:
|
||||
companies = await get_series_companies(session, series_id)
|
||||
return [company.id for company, _role in companies]
|
||||
|
||||
@@ -34,6 +34,7 @@ from app.repositories.provider_repository import get_or_create_provider, upsert_
|
||||
from app.services.metadata.matcher import match_entity
|
||||
from app.services.metadata.normalizer import (
|
||||
NormalizedSeries,
|
||||
company_logo_artwork,
|
||||
normalize_season,
|
||||
normalize_series,
|
||||
)
|
||||
@@ -94,6 +95,15 @@ async def _replace_series_companies(
|
||||
country_code=country_code,
|
||||
)
|
||||
session.add(SeriesCompany(series_id=series_id, company_id=company.id, role=role))
|
||||
logo = company_logo_artwork(ref.logo_path)
|
||||
if logo is not None:
|
||||
await polymorphic.sync_artwork(
|
||||
session,
|
||||
entity_type="company",
|
||||
entity_id=company.id,
|
||||
provider_id=provider_id,
|
||||
artwork_list=[logo],
|
||||
)
|
||||
|
||||
|
||||
async def _replace_series_credits(
|
||||
|
||||
@@ -54,6 +54,7 @@ class NormalizedCompanyRef:
|
||||
name: str
|
||||
country_code: str | None = None
|
||||
role: str = "production"
|
||||
logo_path: str | None = None # TMDb's raw path, e.g. "/logo.png" — not a full URL yet
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -74,6 +75,27 @@ class NormalizedArtwork:
|
||||
is_primary: bool = False
|
||||
|
||||
|
||||
def company_logo_artwork(logo_path: str | None) -> NormalizedArtwork | None:
|
||||
"""A single-item artwork entry for a company/network's TMDb logo_path,
|
||||
or None if it doesn't have one. Shared by movie_ingestion.py and
|
||||
series_ingestion.py so the "how do we turn a relative TMDb path into a
|
||||
real image URL" logic stays in one place (this module), rather than the
|
||||
ingestion layer needing to know about TMDb's CDN itself."""
|
||||
|
||||
if not logo_path:
|
||||
return None
|
||||
return NormalizedArtwork(
|
||||
artwork_type="logo",
|
||||
url=_image_url_required(logo_path),
|
||||
provider_asset_id=logo_path,
|
||||
width=None,
|
||||
height=None,
|
||||
aspect_ratio=None,
|
||||
language=None,
|
||||
is_primary=True, # a company has exactly one logo ingested today
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NormalizedVideo:
|
||||
video_type: str
|
||||
@@ -173,6 +195,7 @@ def normalize_movie(raw: dict) -> NormalizedMovie:
|
||||
external_id=str(pc["id"]),
|
||||
name=pc["name"],
|
||||
country_code=pc.get("origin_country") or None,
|
||||
logo_path=pc.get("logo_path"),
|
||||
)
|
||||
for pc in raw.get("production_companies", [])
|
||||
]
|
||||
@@ -464,7 +487,10 @@ def normalize_series(raw: dict) -> NormalizedSeries:
|
||||
|
||||
networks = [
|
||||
NormalizedCompanyRef(
|
||||
external_id=str(n["id"]), name=n["name"], country_code=n.get("origin_country") or None
|
||||
external_id=str(n["id"]),
|
||||
name=n["name"],
|
||||
country_code=n.get("origin_country") or None,
|
||||
logo_path=n.get("logo_path"),
|
||||
)
|
||||
for n in raw.get("networks", [])
|
||||
]
|
||||
@@ -473,6 +499,7 @@ def normalize_series(raw: dict) -> NormalizedSeries:
|
||||
external_id=str(pc["id"]),
|
||||
name=pc["name"],
|
||||
country_code=pc.get("origin_country") or None,
|
||||
logo_path=pc.get("logo_path"),
|
||||
)
|
||||
for pc in raw.get("production_companies", [])
|
||||
]
|
||||
|
||||
@@ -22,8 +22,10 @@ from app.core.logging import get_logger
|
||||
from app.services.artwork.download import download_artwork
|
||||
from app.services.artwork.storage import get_artwork_storage
|
||||
from app.services.ingestion.runner import (
|
||||
get_movie_company_ids,
|
||||
get_or_create_ingestion_job,
|
||||
get_primary_artwork_for_download,
|
||||
get_series_company_ids,
|
||||
get_series_season_ids,
|
||||
get_series_season_numbers,
|
||||
run_movie_ingestion,
|
||||
@@ -52,6 +54,7 @@ async def _cascade_artwork_downloads(
|
||||
async def ingest_movie_task(ctx: dict[str, Any], job_id: str, external_id: str) -> str:
|
||||
movie_id = await run_movie_ingestion(uuid.UUID(job_id), external_id)
|
||||
await _cascade_artwork_downloads(ctx, "movie", [movie_id])
|
||||
await _cascade_artwork_downloads(ctx, "company", await get_movie_company_ids(movie_id))
|
||||
return str(movie_id)
|
||||
|
||||
|
||||
@@ -70,6 +73,7 @@ async def ingest_series_task(
|
||||
# creates, independent of include_seasons (which only controls whether
|
||||
# full per-season episode data gets fetched) -- so this always runs.
|
||||
await _cascade_artwork_downloads(ctx, "season", await get_series_season_ids(series_id))
|
||||
await _cascade_artwork_downloads(ctx, "company", await get_series_company_ids(series_id))
|
||||
|
||||
if include_seasons:
|
||||
# Fan out one ingest_season job per season this series has, using
|
||||
|
||||
Reference in New Issue
Block a user