Initial version
This commit is contained in:
150
examples/stream.sh
Executable file
150
examples/stream.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/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
|
||||
# "<id>\t<name>" 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" <<JSON
|
||||
{
|
||||
"compiledAt": "$now_iso",
|
||||
"game": $(emit_str "$game"),
|
||||
"gameId": $(emit_str "$game_id"),
|
||||
"title": $(emit_str "$title"),
|
||||
"countdownMin": $countdown
|
||||
}
|
||||
JSON
|
||||
|
||||
if ! python3 -m json.tool "$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
|
||||
Reference in New Issue
Block a user