#!/usr/bin/env python3 """ Update Twitch channel info (game + title) for the broadcaster account. ./set-channel.py --game-id 517520 "Slayer is back — Nightmare run" ./set-channel.py "Doom Eternal" "..." # exact-name lookup, fails if no exact hit ./set-channel.py --dry-run --game-id 517520 "..." For free-form / fuzzy game queries, use search-game.py first to resolve the id (loading.sh does this automatically). The /helix/games?name= fallback in this script is exact-and-case-sensitive. Loads `.env.ophi118` — see _twitch.py for the env contract. The token MUST belong to the broadcaster (Twitch enforces broadcaster_id == token user_id on PATCH /helix/channels), so this script intentionally targets that file and not `.env` (which holds the chat-bot's account). """ import json import sys import urllib.parse import _twitch as tw TITLE_LIMIT = 140 def lookup_game_id(env: dict, name: str) -> str: qs = urllib.parse.urlencode({"name": name}) status, body = tw.auth_call("GET", f"{tw.HELIX}/games?{qs}", env) if status != 200: raise RuntimeError(f"games lookup failed (status={status}): {body}") data = (body or {}).get("data") or [] if not data: raise RuntimeError( f"no Twitch game matches {name!r} exactly. /helix/games?name= is " f"case-sensitive — use search-game.py for fuzzy lookup." ) return data[0]["id"] def patch_channel(env: dict, game_id: str, title: str) -> None: qs = urllib.parse.urlencode({"broadcaster_id": env["TWITCH_BROADCASTER_ID"]}) payload = json.dumps({"game_id": game_id, "title": title}) status, body = tw.auth_call("PATCH", f"{tw.HELIX}/channels?{qs}", env, body=payload) if status not in (200, 204): raise RuntimeError(f"channel update failed (status={status}): {body}") def usage_and_die(): print("usage: set-channel.py [--dry-run] (--game-id | ) ", file=sys.stderr) sys.exit(2) def main(): args = sys.argv[1:] dry = False if args and args[0] == "--dry-run": dry, args = True, args[1:] game_id = None if args and args[0] == "--game-id": if len(args) < 3: usage_and_die() game_id, args = args[1], args[2:] expected = 1 if game_id else 2 if len(args) != expected: usage_and_die() if game_id: title = args[0][:TITLE_LIMIT] else: game_name = args[0] title = args[1][:TITLE_LIMIT] env = tw.load_env() needed = ("TWITCH_ACCESS_TOKEN", "TWITCH_REFRESH_TOKEN", "TWITCH_CLIENT_ID", "TWITCH_BROADCASTER_ID") missing = [k for k in needed if not env.get(k)] if missing: print(f"[err] missing in {tw.ENV_FILE.name}: {', '.join(missing)}", file=sys.stderr) sys.exit(1) if game_id is None: game_id = lookup_game_id(env, game_name) if dry: print(f" ✓ dry-run: would set game_id={game_id}, title={title!r} (no PATCH issued)") return patch_channel(env, game_id, title) print(f" ✓ Twitch channel updated → game_id={game_id}, title={title!r}") if __name__ == "__main__": try: main() except Exception as e: print(f"[err] {type(e).__name__}: {e}", file=sys.stderr) sys.exit(1)