Restart page (23), force restarts, Tascam reconnect, ProdCom, Resi encoder
- Page 23 "Restart": hold a key 2 s to restart DVS (4 Macs), ProPresenter,
Ableton, Church Translate, ProdCom, ProdDash, captions, the bridge or the
Tascam connection even when the status page says it is fine ("force",
shared 60 s guard). RESTART PAGE key on page 19, STATUS PAGE back.
- Holds carry Companion's clock; ones queued while the watchdog was down
are dropped instead of firing together later.
- TASCAM hold always reconnects and checks again after 20 s: HUNG - POWER
CYCLE IT (pings, control port hung), NO NETWORK, or turns a disabled
connection on. Tascam pinged at its Companion address.
- INGEST: ProdCom (:24480), ProdDash's page (:24500), the caption screen;
DVS on Ingest restarts ProdCom afterwards. RESI: the encoder (10.1.10.202).
- Windows starts check the app really started (FIX FAILED otherwise); OBS's
start task runs elevated (OBS is "Run as administrator"); setup script
does that for any such app and gives sshd restart-on-failure.
- push_page.py checks what is on the target page, not its name (pages 22
and 97 are named "PAGE" but hold buttons); --which restart.
- Companion custom variables are written in the request body (no URL limit).
Reviewed by a 3-lens review with per-finding verification; 7 confirmed
issues fixed. Tested live: OBS hold opens OBS; Restart page TASCAM key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+67
-15
@@ -1,20 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import status-page.companionconfig into Companion's page 19 without clicking through the UI.
|
||||
"""Import the status page (19) or the Restart page (23) into Companion without clicking through the UI.
|
||||
|
||||
Does exactly what Import/Export -> Import -> "import into page 19" does, over the
|
||||
Does exactly what Import/Export -> Import -> "import into page N" does, over the
|
||||
same internal API the Companion web UI uses (tRPC over a WebSocket at /trpc):
|
||||
upload the file in chunks, check its SHA-1, then import it as a single page.
|
||||
|
||||
Safety: it refuses unless the target page is empty or already the status page
|
||||
(its name contains "status"), so it cannot overwrite one of your other pages.
|
||||
Safety: it looks at what is on the target page, not just its name. It refuses unless the
|
||||
page holds nothing but page up / page number / page down keys, or is already the page this
|
||||
file builds (same name). A page named "PAGE" can still hold real buttons (pages 22 and 97 do).
|
||||
|
||||
python3 push_page.py # na-ingest.local:8000, page and file from config.json
|
||||
python3 push_page.py # the status page, from config.json
|
||||
python3 push_page.py --which restart # the Restart page
|
||||
python3 push_page.py --host 10.6.3.222 --page 19 --file status-page.companionconfig
|
||||
|
||||
Stdlib only (a minimal WebSocket client is included). Written against Companion 5.0.7.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import gzip
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
@@ -116,6 +119,46 @@ class Trpc:
|
||||
return msg.get("result", {}).get("data")
|
||||
|
||||
|
||||
NAV_TYPES = ("pageup", "pagedown", "pagenum")
|
||||
NAV_MARKER = "STATUS nav key" # our coloured page number key (make_page.nav_key)
|
||||
|
||||
|
||||
def fetch_page(host, port, page):
|
||||
"""The page as Companion exports it ({"page": {"name", "controls": {row: {col: control}}}})."""
|
||||
with urllib.request.urlopen("http://%s:%d/int/export/page/%d" % (host, port, page), timeout=10) as r:
|
||||
raw = r.read()
|
||||
try:
|
||||
raw = gzip.decompress(raw)
|
||||
except OSError:
|
||||
pass
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def real_buttons(export):
|
||||
"""How many buttons a page holds besides its page up / number / down keys."""
|
||||
page = export.get("page") or {}
|
||||
n = 0
|
||||
for row in (page.get("controls") or {}).values():
|
||||
for ctl in (row or {}).values():
|
||||
if not ctl or ctl.get("type") in NAV_TYPES:
|
||||
continue
|
||||
if NAV_MARKER in ((ctl.get("options") or {}).get("notes") or ""):
|
||||
continue
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def safe_target(export, expected_name):
|
||||
"""(ok, reason). OK only for a page with nothing but nav keys, or the page this file builds."""
|
||||
name = ((export.get("page") or {}).get("name") or "")
|
||||
if name == expected_name:
|
||||
return True, "it is already %r" % name
|
||||
n = real_buttons(export)
|
||||
if n == 0:
|
||||
return True, "it holds only page keys"
|
||||
return False, "it is called %r and holds %d buttons" % (name, n)
|
||||
|
||||
|
||||
def page_name(host, port, page):
|
||||
url = "http://%s:%d/api/variable/internal/page_number_%d_name/value" % (host, port, page)
|
||||
with urllib.request.urlopen(url, timeout=5) as r:
|
||||
@@ -125,21 +168,30 @@ def page_name(host, port, page):
|
||||
def main():
|
||||
cfg = json.load(open(os.path.join(HERE, "config.json")))
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--which", choices=("status", "restart"), default="status")
|
||||
ap.add_argument("--host", default="na-ingest.local")
|
||||
ap.add_argument("--port", type=int, default=8000)
|
||||
ap.add_argument("--page", type=int, default=cfg["page"])
|
||||
ap.add_argument("--file", default=os.path.join(HERE, "status-page.companionconfig"))
|
||||
ap.add_argument("--force", action="store_true", help="overwrite the page whatever its name")
|
||||
ap.add_argument("--page", type=int)
|
||||
ap.add_argument("--file")
|
||||
ap.add_argument("--force", action="store_true", help="overwrite the page whatever is on it")
|
||||
args = ap.parse_args()
|
||||
|
||||
name = page_name(args.host, args.port, args.page)
|
||||
plain = name.replace("\\n", " ").strip()
|
||||
empty = plain.upper() in ("", "PAGE", "%d PAGE" % args.page)
|
||||
if not (empty or "status" in plain.lower() or args.force):
|
||||
sys.exit("page %d is called %r - not empty and not the status page; refusing (use --force)"
|
||||
% (args.page, plain))
|
||||
if args.which == "restart":
|
||||
page, expected = cfg["restart_page"]["page"], cfg["restart_page"]["page_name"]
|
||||
default_file = "restart-page.companionconfig"
|
||||
else:
|
||||
page, expected, default_file = cfg["page"], cfg["page_name"], "status-page.companionconfig"
|
||||
args.page = args.page or page
|
||||
args.file = args.file or os.path.join(HERE, default_file)
|
||||
|
||||
data = open(args.file, "rb").read()
|
||||
built = json.loads(gzip.decompress(data))
|
||||
if (built.get("page") or {}).get("name") != expected:
|
||||
sys.exit("%s builds page %r, not %r; refusing" % (os.path.basename(args.file),
|
||||
(built.get("page") or {}).get("name"), expected))
|
||||
ok, why = safe_target(fetch_page(args.host, args.port, args.page), expected)
|
||||
if not ok and not args.force:
|
||||
sys.exit("page %d: %s - refusing (use --force only if you mean to replace it)" % (args.page, why))
|
||||
plain = page_name(args.host, args.port, args.page).replace("\\n", " ").strip()
|
||||
ws = WebSocket(args.host, args.port, "/trpc")
|
||||
try:
|
||||
api = Trpc(ws)
|
||||
|
||||
Reference in New Issue
Block a user