- schema.py: every setting with type, default, help, owner screen and the rules a config must pass; tests/test_schema.py keeps it in step with the code (every key the code reads is declared, every default matches, check types and fix kinds agree, the glossary covers every word a key can show, today's config still builds today's pages apart from the key notes) - status_panel.py: VERSION + started.json; SIGTERM lets a running fix finish and starts nothing new (RESTARTING SOON); fixes-running.json and live-now.json for the settings page; state.json keeps the ALARMS override, acknowledgements, auto-fix tries and fix times across restarts, keyed by what a check is about rather than its position; 'test' and 'resurvey' commands; queued commands dropped at start; overnight windows; a HyperDeck switched off is always switched back on; the spotify fix kind (playlist set once, in config 'spotify'); Windows remote checks batched for cmd.exe with conditional echoes; every fix's kind and host validated; never_auto; wording and numbers from config (site block, timing, page_hold_ms, auto-fix keep_months); hyperdeck_probe removed; --check and --exit-timeout - make_page.py: numbers from config, markers on every key, nav templates per Companion version, --out; push_page.py: a page is overwritten only when every key carries a marker (foreign keys named by position), secret-free fetch, host from the site; nav_keys.py: host from the site; netgear.py: switches and the shared-port limit from config - install.sh: validates with the new code first, keeps prev/ for --rollback, --keep-config, derived ExitTimeOut, wider deploy list, waits for the start stamp - config.json: site, timing, spotify sections; Windows hosts marked; deployed to NA Ingest and both pages re-imported once (--force) so every key carries its marker Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
127 lines
4.8 KiB
Python
127 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Make the page nav keys on every page follow the status page's ALL button.
|
|
|
|
Which keys follow it is "nav_keys" in config.json (now just the page number).
|
|
The status page (page 19) carries coloured copies of Companion's own nav keys
|
|
(see make_page.py and nav_templates.json). This copies those over the built-in
|
|
keys on every other page, using the same "copy button" call as the Companion UI,
|
|
and puts back the built-in key for any kind no longer listed. Each key keeps
|
|
its normal press action.
|
|
|
|
python3 nav_keys.py --dry-run list what would change
|
|
python3 nav_keys.py apply (also refreshes keys already converted)
|
|
python3 nav_keys.py --revert put Companion's built-in nav keys back everywhere
|
|
|
|
Only nav keys are touched. Every location changed is recorded in nav_locations.json.
|
|
"""
|
|
import argparse
|
|
import gzip
|
|
import json
|
|
import os
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
from make_page import NAV_LOCATIONS, NAV_MARKER
|
|
from push_page import Trpc, WebSocket
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
RECORD = os.path.join(HERE, "nav_locations.json")
|
|
NAV_TYPES = ("pageup", "pagenum", "pagedown")
|
|
|
|
|
|
def page_export(host, port, page):
|
|
url = "http://%s:%d/int/export/page/%d?includeSecrets=false" % (host, port, page)
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=10) as r:
|
|
raw = r.read()
|
|
except urllib.error.HTTPError:
|
|
return None
|
|
try:
|
|
raw = gzip.decompress(raw)
|
|
except OSError:
|
|
pass
|
|
return json.loads(raw)
|
|
|
|
|
|
def nav_kind(control):
|
|
if control.get("type") in NAV_TYPES:
|
|
return control["type"]
|
|
notes = control.get("options", {}).get("notes", "") or ""
|
|
if control.get("type") == "button-layered" and notes.startswith(NAV_MARKER):
|
|
for kind in NAV_TYPES:
|
|
if "(%s)" % kind in notes:
|
|
return kind
|
|
return None
|
|
|
|
|
|
def is_ours(control):
|
|
return (control.get("options", {}).get("notes", "") or "").startswith(NAV_MARKER)
|
|
|
|
|
|
def find_nav_keys(host, port, status_page):
|
|
"""[(page, row, column, kind, ours)] for every nav key outside the status page."""
|
|
found, page = [], 1
|
|
while True:
|
|
export = page_export(host, port, page)
|
|
if export is None:
|
|
break
|
|
if page != status_page:
|
|
for r, cols in export["page"]["controls"].items():
|
|
for c, control in cols.items():
|
|
kind = nav_kind(control or {})
|
|
if kind:
|
|
found.append((page, int(r), int(c), kind, is_ours(control)))
|
|
page += 1
|
|
return found
|
|
|
|
|
|
def main():
|
|
cfg = json.load(open(os.path.join(HERE, "config.json")))
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
ap.add_argument("--host", default=cfg.get("site", {}).get("ingest_hostname") or cfg["companion"].get("host", "127.0.0.1"))
|
|
ap.add_argument("--port", type=int, default=cfg["companion"].get("port", 8000))
|
|
ap.add_argument("--dry-run", action="store_true")
|
|
ap.add_argument("--revert", action="store_true")
|
|
args = ap.parse_args()
|
|
status_page = cfg["page"]
|
|
|
|
coloured = cfg.get("nav_keys", list(NAV_LOCATIONS))
|
|
record = json.load(open(RECORD)) if os.path.exists(RECORD) else {}
|
|
found = find_nav_keys(args.host, args.port, status_page)
|
|
if args.revert:
|
|
work = [(p, r, c, k, "restore") for p, r, c, k, ours in found if ours]
|
|
else:
|
|
work = [(p, r, c, k, "colour") for p, r, c, k, ours in found if k in coloured]
|
|
work += [(p, r, c, k, "restore") for p, r, c, k, ours in found if ours and k not in coloured]
|
|
print("%d nav keys to colour, %d to restore, on %d pages" % (
|
|
sum(1 for w in work if w[4] == "colour"), sum(1 for w in work if w[4] == "restore"), len({w[0] for w in work})))
|
|
if args.dry_run:
|
|
for w in work:
|
|
print(" page %d row %d col %d: %s -> %s" % w)
|
|
return
|
|
|
|
ws = WebSocket(args.host, args.port, "/trpc")
|
|
done = 0
|
|
try:
|
|
api = Trpc(ws)
|
|
for page, row, col, kind, what in work:
|
|
location = {"pageNumber": page, "row": row, "column": col}
|
|
if what == "restore":
|
|
api.mutation("controls.resetControl", {"location": location, "newType": kind})
|
|
record.pop("%d/%d/%d" % (page, row, col), None)
|
|
else:
|
|
r, c = NAV_LOCATIONS[kind]
|
|
source = {"pageNumber": status_page, "row": int(r), "column": int(c)}
|
|
if not api.mutation("controls.copyControl", {"fromLocation": source, "toLocation": location}):
|
|
raise RuntimeError("copy to page %d row %d col %d failed" % (page, row, col))
|
|
record["%d/%d/%d" % (page, row, col)] = kind
|
|
done += 1
|
|
finally:
|
|
ws.close()
|
|
json.dump(record, open(RECORD, "w"), indent=1, sort_keys=True)
|
|
print("done: %d nav keys changed" % done)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|