#!/usr/bin/env bash # stream.sh — interactive pre-stream helper. # # ./stream.sh # # Prompts for game + title + countdown, resolves the game to a Twitch # category id via ../search-game.py, updates the channel via ../set-channel.py, # and writes stream.json next to this script for any overlay / starting-soon # timer / status page that wants to read the manifest. # # Re-running uses your previous answers as defaults — press Enter to keep them. # # Output path can be overridden with STREAM_JSON=/path/to/file.json ./stream.sh set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BOT_DIR="$(cd "$DIR/.." && pwd)" JSON="${STREAM_JSON:-$DIR/stream.json}" SEARCH_GAME="$BOT_DIR/search-game.py" SET_CHANNEL="$BOT_DIR/set-channel.py" # Reads a top-level field from the previous stream.json. Always exits 0; # prints empty string if file/field is missing. prev() { [[ -f "$JSON" ]] || return 0 python3 - "$JSON" "$1" <<'PY' 2>/dev/null || true import json, sys try: d = json.load(open(sys.argv[1])) v = d.get(sys.argv[2]) print('' if v is None else v) except Exception: pass PY } ask() { # ask "question" "default" → echoes user input or default if blank local q="$1" def="${2:-}" local prompt=" $q" [[ -n "$def" ]] && prompt+=" [$def]" prompt+=": " local v read -rp "$prompt" v [[ -z "$v" && -n "$def" ]] && v="$def" printf '%s' "$v" } ask_int() { local v while :; do v=$(ask "$1" "${2:-}") [[ "$v" =~ ^[0-9]+$ ]] && { printf '%s' "$v"; return; } echo " ✗ must be a whole number" >&2 done } sanitize() { printf '%s' "${1:-}" \ | tr -d '"\\' \ | tr '\n\r\t' ' ' \ | sed 's/ */ /g; s/^ //; s/ $//' } emit_str() { [[ -z "${1:-}" ]] && printf 'null' || printf '"%s"' "$1"; } # ── Pull previous answers as defaults ─────────────── prev_game=$(prev game) prev_game_id=$(prev gameId) prev_title=$(prev title) prev_count=$(prev countdownMin); [[ -z "$prev_count" ]] && prev_count="5" echo "── stream manifest ──" echo " (press Enter to keep [defaults])" echo # Resolve Game via Twitch search → exact directory entry. The picker prints # "\t" to stdout on success, or non-zero on no-match / cancel. # Re-running and accepting the previous game unchanged reuses the cached id # so we don't burn an API call to re-resolve a known answer. have_twitch=0 [[ -x "$SEARCH_GAME" ]] && have_twitch=1 game=""; game_id="" while :; do query=$(ask "Game (search)" "$prev_game") if [[ -z "$query" ]]; then echo " ✗ Game is required" >&2 continue fi if (( have_twitch )); then # Reuse cached id only if it looks like a real Twitch numeric id. A past # bug (search-game.py prompt leaking into stdout) wrote "Pick: 506462" # here; the regex makes sure such corruption falls back to a fresh search # instead of getting passed straight to PATCH /helix/channels. if [[ -n "$prev_game_id" && "$query" == "$prev_game" && "$prev_game_id" =~ ^[0-9]+$ ]]; then game="$prev_game"; game_id="$prev_game_id" echo " ↻ reusing cached: $game (id=$game_id)" break fi if line=$("$SEARCH_GAME" "$query"); then IFS=$'\t' read -r game_id game <<<"$line" break fi # search-game.py already printed its own error; loop and re-ask else # no twitch helper available — accept the raw input, no id resolution game="$query"; game_id="" break fi done title=$(ask "Title (Twitch stream title)" "$prev_title") countdown=$(ask_int "Countdown (minutes)" "$prev_count") game=$(sanitize "$game") title=$(sanitize "$title") now_iso=$(date -u +%FT%TZ) tmp="$JSON.tmp.$$" cat > "$tmp" </dev/null 2>&1; then echo " ✗ produced invalid JSON, leaving $tmp for inspection" >&2 exit 1 fi mv -f "$tmp" "$JSON" echo echo " ✓ wrote $JSON" # ── Push to Twitch (game + title) via the broadcaster token ───────────── # Soft-fail: a Twitch hiccup must not block the local manifest from being # written — anything reading stream.json can still load offline. # Title falls back to the game name when the user left it blank. if (( have_twitch )) && [[ -x "$SET_CHANNEL" && -n "$game_id" ]]; then twitch_title="${title:-$game}" echo if ! "$SET_CHANNEL" --game-id "$game_id" "$twitch_title"; then echo " ! Twitch sync failed (manifest still saved) — fix and re-run if needed" >&2 fi fi