149 lines
5.0 KiB
Python
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,
|
|
)
|