Files

149 lines
5.0 KiB
Python

"""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,
)