Skip to content

CineInfo Sources

Domekologe edited this page Jul 28, 2026 · 2 revisions

CineInfo Sources

CineInfo Sources are the extension point that lets a third-party module add its own CineInfo data (ratings, providers, custom fields, ...) on top of the built-in TMDB lookup — without any change to the core. Where a provider pill only shows a small availability badge, a source feeds real fields into the CineInfo lookups themselves.

The core stays authoritative: sources are layered on top of the TMDB result, and with no source registered the whole mechanism is a zero-cost pass-through.

The two batch forms

Every source declares a single capability flag, and the orchestrator picks the fetch form automatically — there is no user setting for this:

supports_bulk The orchestrator calls Meaning Use for
False fetch_one(item, ctx) per item "einzeln nach und nach" — one request per item, looped with bounded concurrency Upstreams that answer one lookup per request (e.g. TMDB)
True fetch_many(items, ctx) per chunk "alles in einer Anfrage" — one request for up to max_bulk items Upstreams with a real batch endpoint

Both forms run through the same cache + rate-limit + de-duplication layer, so a source never implements batching, caching or throttling itself.

Writing a source

from ...cineinfo.source import CineInfoSource, QueryContext
from ...db import get_setting

class MySource(CineInfoSource):
    id = "myprovider"            # stable; also cache namespace + rate-limiter bucket
    label = "My Provider"
    supports_bulk = False        # ← the entire batch-form decision
    rate = 5.0                   # max upstream requests/second
    cache_ttl = 86400.0          # provider-cache TTL in seconds (0 = no cache)

    def is_enabled(self) -> bool:
        # Follow your own toggle: a disabled module stops contributing at
        # once. (Uninstall is handled by the item_id you pass below.)
        return get_setting("myprovider_enabled", "0") == "1"

    def fetch_one(self, item: dict, ctx: QueryContext) -> dict:
        # item carries a stable "key" plus lookup fields
        # (title / imdb_id / tmdb_id). ctx.country and ctx.ui_lang are resolved.
        # Return only the fields you actually know.
        ...
        return {"vote_average": 8.1, "myprovider_url": "https://..."}

A bulk source instead implements fetch_many(items, ctx) and returns {item["key"]: payload} for the whole chunk in one request.

Registering

From your module's register(app):

from ...cineinfo.registry import register_cineinfo_source
register_cineinfo_source(MySource(), item_id=MODULE_ID)

Register one instance per source. You can register several (e.g. one per batch form).

item_id is the id you already passed to register_thirdparty(). It is optional only so modules written against the first version of this API keep working — always pass it. Without it the source still works, but:

  • the Modulmanager cannot attribute it, so the module's capability line under- reports what it does (this is exactly why a CineInfo module used to show nothing but "1 × settings card"), and
  • unregister_module() cannot drop it, so an uninstalled module's source stays registered for the rest of the process' life — kept harmless only by its own is_enabled(), which is reading a setting that no longer exists.

With item_id passed, the module manager shows 1 × CineInfo source (or 2 × for a module registering both batch forms) and uninstalling really removes it.

Where it shows up

A registered source is not invisible plumbing:

  • Module manager lists it as 1 × CineInfo source among the module's capabilities (that is what item_id is for, see above).
  • Integrations → CineInfo → "Source order" lists it as a draggable row with a "Module" badge, next to the provider pills. That list is one setting (cineinfo_provider_order) holding two kinds of entry — ci:<source id> for a CineInfo source, ext:<name>/bare ids for provider pills — and each consumer reads only its own prefix. The position you drag a source to is the order enrich() applies it in: the first source that knows a field fills it, and the built-in TMDB base still beats every source. With nothing configured the old alphabetical-by-id order applies, unchanged.
  • Module Settings (under the Module Manager) and Integrations → Third Party both list the module's settings card even though its home is the CineInfo tab — same card, same API, same edit.

How the data lands

The core CineInfo endpoints (/api/tmdb/info, /api/tmdb/batch) call cineinfo.enrich(...), which runs every enabled source and field-merges its payload onto the TMDB base:

  • The built-in TMDB data wins. A source only fills fields TMDB is missing or left empty, plus any custom fields of its own.
  • Sources are applied in stable id order for deterministic results.
  • 0 / False are valid values and are preserved (not treated as "empty").

What you get for free

Cache-first (only cache-misses hit the network, via the shared provider_cache table), a per-source token-bucket rate limiter, in-flight de-duplication of concurrent identical lookups, bounded concurrency, per-query timeouts, and error isolation — a failing item or source never takes CineInfo down.

Reference implementation

See .examples/thirdparties/example_cineinfo_source/ for a complete, offline-safe module that registers one source of each batch form under the CineInfo settings tab. The plug-in system itself is documented in .examples/thirdparties/README.md.

Clone this wiki locally