"""Blackmagic Design. BMD publishes their entire support catalogue as one JSON document, which makes them by far the best-behaved vendor in this fleet. One fetch resolves ATEM switchers and panels, Desktop Video (DeckLink), HyperDeck, the converter family and MultiView. Verified structure of an entry: { "name": "ATEM Switchers 10.4.1 Update", "date": "Last Thursday", # human, relative -- unusable "numericDate": 1788418810000, # ms epoch -- use this "desc": "This software update provides ...", "releaseNotesTitle": "Blackmagic ATEM Switchers 10.4.1", "relatedFamilies": ["atem-live-production-switchers"], "platforms": ["Mac OS X", "Windows"], "type": "Product Release", "urls": {"Windows": [{"major": 10, "minor": 4, "releaseNum": 1, "buildNum": 0, "beta": 255, "downloadId": "..."}]} } ``beta: 255`` marks a final release; anything lower is a beta build. Inventory usage: source: kind: blackmagic family: "ATEM Switchers" # matches the leading words of `name` """ from __future__ import annotations import re from datetime import datetime, timezone from ..models import Item, VersionCheck from ..versions import parse from .base import Context, error DOWNLOADS_URL = "https://www.blackmagicdesign.com/api/support/us/downloads.json" # Family label -> regex capturing the version out of the `name` field. # Verified against the live feed on 2026-09-08. FAMILY_PATTERNS: dict[str, str] = { "ATEM Switchers": r"^ATEM Switchers\s+([\d.]+)\s+Update$", "Desktop Video": r"^Desktop Video\s+([\d.]+)$", "Blackmagic Converters": r"^Blackmagic Converters\s+([\d.]+)\s+Update$", "HyperDeck": r"^HyperDeck\s+([\d.]+)\s+Update$", "MultiView": r"^Blackmagic Multi\s?[Vv]iew\s+([\d.]+)", "Camera": r"^Blackmagic Camera\s+([\d.]+)", "Videohub": r"^Videohub\s+([\d.]+)", "Web Presenter": r"^Web Presenter\s+([\d.]+)", "DaVinci Resolve": r"^DaVinci Resolve\s+([\d.]+)", } BETA_FINAL_SENTINEL = 255 def _load_catalogue(ctx: Context) -> list[dict]: def fetch() -> list[dict]: # The catalogue is ~1.6 MB; a 6 hour TTL is plenty and keeps us off # their servers during multi-run days. resp = ctx.http.get(DOWNLOADS_URL, ttl=6 * 3600) if not resp.ok: raise RuntimeError(f"HTTP {resp.status} from BMD downloads feed") data = resp.json() entries = data.get("downloads") if not isinstance(entries, list): raise RuntimeError("unexpected BMD payload: no 'downloads' list") return entries return ctx.memo("bmd:catalogue", fetch) def _is_beta(entry: dict) -> bool: for builds in (entry.get("urls") or {}).values(): if builds and isinstance(builds, list): beta = builds[0].get("beta") if beta is not None and beta != BETA_FINAL_SENTINEL: return True return False def _released(entry: dict) -> datetime | None: ms = entry.get("numericDate") if not isinstance(ms, (int, float)): return None try: return datetime.fromtimestamp(ms / 1000.0, tz=timezone.utc) except (OverflowError, OSError, ValueError): return None class BlackmagicSource: kind = "blackmagic" def check(self, item: Item, ctx: Context) -> VersionCheck: family = item.source.get("family") if not family: return error(item, "blackmagic source requires 'family'") pattern = item.source.get("pattern") or FAMILY_PATTERNS.get(family) if not pattern: return error( item, f"unknown BMD family {family!r}; set an explicit 'pattern' " f"or use one of: {', '.join(sorted(FAMILY_PATTERNS))}", ) try: catalogue = _load_catalogue(ctx) except RuntimeError as exc: return error(item, str(exc)) rx = re.compile(pattern) include_beta = bool(item.source.get("include_beta", False)) best: tuple | None = None for entry in catalogue: name = (entry.get("name") or "").strip() m = rx.match(name) if not m: continue if not include_beta and _is_beta(entry): continue ver = parse(m.group(1)) if not ver.valid: continue if best is None or ver > best[0]: best = (ver, entry) if best is None: return error(item, f"no releases matched pattern for family {family!r}") ver, entry = best notes = (entry.get("desc") or "").strip() or None title = entry.get("releaseNotesTitle") return VersionCheck( item_id=item.id, latest=ver.raw, released=_released(entry), status="ok", release_notes=notes, release_url=item.check_url or "https://www.blackmagicdesign.com/support", detail=title, )