640 lines
25 KiB
Bash
Executable File
640 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/deploy-rig.sh — bring up the ophi118 OBS rig from scratch.
|
|
# Idempotent: re-running is safe, every phase checks before acting.
|
|
#
|
|
# Usage:
|
|
# bash scripts/deploy-rig.sh # interactive, full install
|
|
# bash scripts/deploy-rig.sh --yes # non-interactive, accept all prompts
|
|
# bash scripts/deploy-rig.sh --check # validate state, write nothing
|
|
# bash scripts/deploy-rig.sh --rig-name X # override hostname-derived rig name
|
|
#
|
|
# Run on a fresh Midgolem (or re-run on Ignia to verify). After:
|
|
# git clone <repo> ~/.config/obs-studio
|
|
# cd ~/.config/obs-studio
|
|
# bash scripts/deploy-rig.sh
|
|
#
|
|
# What it covers:
|
|
# 1. Arch system packages (pacman + AUR for sansation-font)
|
|
# 2. Python user packages (python-mpd2, websockets)
|
|
# 3. Flatpak OBS Studio + filesystems=host override
|
|
# 4. Git submodules + asset directories under $HOME
|
|
# 5. PipeWire null sinks (mpd_stream + discord_stream) + loopbacks
|
|
# 6. MPD config (zeroconf_name = this rig) + system units
|
|
# 7. Custom systemd --user units (bridge, bot)
|
|
# 8. OBS first-launch to seed obs-websocket plugin config
|
|
# 9. Secrets stub + manual next-steps report
|
|
set -euo pipefail
|
|
|
|
# ── globals ────────────────────────────────────────────────────────────────
|
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
OBS_DIR="$(cd "$DIR/.." && pwd)"
|
|
RIG_NAME="${RIG_NAME:-$(hostname)}"
|
|
CHECK_ONLY=0
|
|
ASSUME_YES=0
|
|
|
|
# ── arg parsing ────────────────────────────────────────────────────────────
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--check) CHECK_ONLY=1; shift ;;
|
|
--yes|-y) ASSUME_YES=1; shift ;;
|
|
--rig-name) RIG_NAME="${2:?--rig-name needs an argument}"; shift 2 ;;
|
|
-h|--help) sed -n 's/^# \?//p' "$0" | head -28; exit 0 ;;
|
|
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
RIG_NAME_TC="${RIG_NAME^}" # title-case: ignia → Ignia
|
|
|
|
# ── output helpers ─────────────────────────────────────────────────────────
|
|
PHASE=0
|
|
TOTAL=9
|
|
step() { PHASE=$((PHASE+1)); printf '\n\033[1;36m[%d/%d] %s\033[0m\n' "$PHASE" "$TOTAL" "$*"; }
|
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
|
skip() { printf ' \033[90m·\033[0m %s\n' "$*"; }
|
|
warn() { printf ' \033[33m!\033[0m %s\n' "$*" >&2; }
|
|
err() { printf ' \033[31m✗\033[0m %s\n' "$*" >&2; }
|
|
die() { err "$*"; exit 1; }
|
|
|
|
confirm() {
|
|
(( ASSUME_YES )) && return 0
|
|
local q="$1"
|
|
read -rp " ? $q [y/N]: " a
|
|
[[ "${a,,}" =~ ^(y|yes)$ ]]
|
|
}
|
|
|
|
would() {
|
|
# In --check mode, prefix the message and skip the action.
|
|
(( CHECK_ONLY )) && { skip "would: $*"; return 1; }
|
|
return 0
|
|
}
|
|
|
|
# ── pre-flight ─────────────────────────────────────────────────────────────
|
|
[[ "$OBS_DIR" == "$HOME/.config/obs-studio" ]] \
|
|
|| die "Run this from \$HOME/.config/obs-studio (current OBS_DIR=$OBS_DIR)"
|
|
|
|
cat <<BANNER
|
|
╔═══════════════════════════════════════════════════════════════╗
|
|
║ ophi118 rig deployment ║
|
|
║ rig name : $RIG_NAME_TC
|
|
║ config : $OBS_DIR
|
|
║ mode : $( ((CHECK_ONLY)) && echo '--check (no writes)' || echo 'install' )
|
|
║ confirms : $( ((ASSUME_YES)) && echo 'auto-yes' || echo 'interactive' )
|
|
╚═══════════════════════════════════════════════════════════════╝
|
|
BANNER
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 1 — system packages
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "System packages (pacman)"
|
|
|
|
PACMAN_PKGS=(
|
|
flatpak
|
|
python python-pip
|
|
pipewire pipewire-pulse pipewire-alsa wireplumber
|
|
ffmpeg
|
|
mpd mpc
|
|
avahi nss-mdns
|
|
fontconfig
|
|
)
|
|
|
|
if command -v pacman >/dev/null; then
|
|
MISSING=()
|
|
for p in "${PACMAN_PKGS[@]}"; do
|
|
pacman -Qq "$p" >/dev/null 2>&1 || MISSING+=("$p")
|
|
done
|
|
if (( ${#MISSING[@]} == 0 )); then
|
|
ok "all base packages installed"
|
|
elif would "sudo pacman -S --needed ${MISSING[*]}"; then
|
|
sudo pacman -S --needed $( ((ASSUME_YES)) && echo --noconfirm ) "${MISSING[@]}"
|
|
ok "installed ${#MISSING[@]} package(s)"
|
|
fi
|
|
|
|
# NVIDIA branch
|
|
if lspci 2>/dev/null | grep -qi 'vga.*nvidia'; then
|
|
NV_MISSING=()
|
|
for p in nvidia nvidia-utils; do
|
|
pacman -Qq "$p" >/dev/null 2>&1 || NV_MISSING+=("$p")
|
|
done
|
|
if (( ${#NV_MISSING[@]} == 0 )); then
|
|
ok "NVIDIA driver present"
|
|
elif would "sudo pacman -S --needed ${NV_MISSING[*]}"; then
|
|
sudo pacman -S --needed $( ((ASSUME_YES)) && echo --noconfirm ) "${NV_MISSING[@]}"
|
|
ok "installed NVIDIA driver"
|
|
fi
|
|
else
|
|
skip "no NVIDIA GPU detected (skipping nvidia/nvidia-utils)"
|
|
fi
|
|
|
|
# AUR: sansation-font (used by the `info` text source on Game scene).
|
|
if pacman -Qq sansation-font >/dev/null 2>&1 \
|
|
|| fc-list 2>/dev/null | grep -qi 'sansation'; then
|
|
ok "Sansation font present"
|
|
else
|
|
AUR_HELPER=""
|
|
for h in paru yay; do command -v "$h" >/dev/null && { AUR_HELPER="$h"; break; }; done
|
|
if [[ -z "$AUR_HELPER" ]]; then
|
|
warn "Sansation font missing and no AUR helper (paru/yay) found"
|
|
warn " → install manually from AUR or accept the system-default fallback"
|
|
elif would "$AUR_HELPER -S sansation-font"; then
|
|
"$AUR_HELPER" -S $( ((ASSUME_YES)) && echo --noconfirm ) sansation-font || \
|
|
warn "AUR install failed — install sansation-font manually if you want the Game scene 'info' text to render exactly"
|
|
fi
|
|
fi
|
|
else
|
|
warn "pacman not found — this script targets Arch. Skipping system-package phase."
|
|
warn " Make sure these are installed by other means: ${PACMAN_PKGS[*]}"
|
|
fi
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 2 — Python user packages
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "Python user packages"
|
|
|
|
PIP_PKGS=(python-mpd2 websockets)
|
|
PY_MISSING=()
|
|
for p in "${PIP_PKGS[@]}"; do
|
|
python -c "import importlib, sys; importlib.import_module('${p//-/_}'.replace('python_mpd2','mpd'))" 2>/dev/null \
|
|
|| PY_MISSING+=("$p")
|
|
done
|
|
if (( ${#PY_MISSING[@]} == 0 )); then
|
|
ok "python-mpd2 and websockets importable"
|
|
elif would "pip install --user ${PY_MISSING[*]}"; then
|
|
python -m pip install --user --upgrade "${PY_MISSING[@]}"
|
|
ok "installed ${PY_MISSING[*]}"
|
|
fi
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 3 — OBS Flatpak
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "OBS Studio (Flatpak)"
|
|
|
|
if ! command -v flatpak >/dev/null; then
|
|
die "flatpak not installed — phase 1 should have done this"
|
|
fi
|
|
|
|
if ! flatpak remotes --user 2>/dev/null | grep -q '^flathub' \
|
|
&& ! flatpak remotes 2>/dev/null | grep -q '^flathub'; then
|
|
if would "flatpak remote-add flathub"; then
|
|
flatpak remote-add --if-not-exists --user flathub \
|
|
https://dl.flathub.org/repo/flathub.flatpakrepo
|
|
ok "added Flathub remote"
|
|
fi
|
|
else
|
|
ok "Flathub remote present"
|
|
fi
|
|
|
|
if flatpak info com.obsproject.Studio >/dev/null 2>&1; then
|
|
ok "OBS Studio flatpak installed"
|
|
else
|
|
if would "flatpak install flathub com.obsproject.Studio"; then
|
|
flatpak install $( ((ASSUME_YES)) && echo -y ) flathub com.obsproject.Studio
|
|
ok "installed OBS Studio flatpak"
|
|
fi
|
|
fi
|
|
|
|
if flatpak info --show-permissions com.obsproject.Studio 2>/dev/null \
|
|
| grep -q 'filesystems=.*host'; then
|
|
ok "filesystems=host override present (config dir reads from ~/.config/)"
|
|
else
|
|
if would "flatpak override --user com.obsproject.Studio --filesystems=host"; then
|
|
flatpak override --user com.obsproject.Studio --filesystems=host
|
|
ok "added filesystems=host override"
|
|
else
|
|
warn "without filesystems=host, OBS will read from ~/.var/app/... not from this dir!"
|
|
fi
|
|
fi
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 4 — submodules + asset dirs
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "Submodules + asset directories"
|
|
|
|
if [[ -f "$OBS_DIR/.gitmodules" ]]; then
|
|
if would "git submodule update --init --recursive"; then
|
|
( cd "$OBS_DIR" && git submodule update --init --recursive )
|
|
ok "submodules initialized"
|
|
fi
|
|
fi
|
|
|
|
ASSET_DIRS=(
|
|
"$HOME/Videos"
|
|
"$HOME/HDD/Music"
|
|
"$HOME/HDD/Images/Gifs"
|
|
"$HOME/cloud.jakubzych.com/_img/logos"
|
|
)
|
|
for d in "${ASSET_DIRS[@]}"; do
|
|
if [[ -d "$d" ]]; then
|
|
ok "exists: $d"
|
|
elif would "mkdir -p $d"; then
|
|
mkdir -p "$d"
|
|
ok "created: $d"
|
|
fi
|
|
done
|
|
|
|
ASSET_FILES=(
|
|
"$HOME/HDD/Images/Gifs/stamd.png:Game-scene 'Starting Soon' image"
|
|
"$HOME/cloud.jakubzych.com/_img/logos/dgw.png:Doomguard watermark"
|
|
)
|
|
MISSING_ASSETS=0
|
|
for entry in "${ASSET_FILES[@]}"; do
|
|
path="${entry%%:*}"; what="${entry##*:}"
|
|
if [[ -f "$path" ]]; then
|
|
ok "asset present: $path"
|
|
else
|
|
warn "missing asset: $path ($what)"
|
|
MISSING_ASSETS=1
|
|
fi
|
|
done
|
|
(( MISSING_ASSETS )) && warn " → bring these over manually from Ignia; OBS will surface 'missing source' errors otherwise"
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 5 — PipeWire null sinks (mpd_stream + discord_stream)
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Two isolated streams into OBS so Discord voice and MPD music don't mix
|
|
# with each other — or with the rest of the desktop. Each gets its own
|
|
# null sink + loopback (so you still hear them locally) + monitor source
|
|
# that OBS captures via pulse_output_capture.
|
|
step "PipeWire null sinks (mpd_stream + discord_stream)"
|
|
|
|
PW_CONF_DIR="$HOME/.config/pipewire/pipewire-pulse.conf.d"
|
|
mkdir -p "$PW_CONF_DIR"
|
|
|
|
PW_CONF_CHANGED=0
|
|
|
|
write_pw_conf() {
|
|
# write_pw_conf <path> <body>
|
|
local path="$1"; local body="$2"
|
|
if [[ -f "$path" ]]; then
|
|
ok "$(basename "$path") already present"
|
|
elif would "write $path"; then
|
|
printf '%s' "$body" > "$path"
|
|
ok "wrote $path"
|
|
PW_CONF_CHANGED=1
|
|
fi
|
|
}
|
|
|
|
write_pw_conf "$PW_CONF_DIR/mpd-stream.conf" '# Persistent virtual sink for streaming MPD into OBS without mixing in
|
|
# desktop/browser/Discord audio.
|
|
#
|
|
# MPD ──► null sink "mpd_stream"
|
|
# │
|
|
# ├──► loopback ──► default sink (so you still hear it on speakers)
|
|
# │
|
|
# └──► monitor ──► OBS "Audio Output Capture (PulseAudio) →
|
|
# Monitor of MPD-Stream"
|
|
#
|
|
# Wired on the MPD side via target "mpd_stream" in ~/.config/mpd/mpd.conf.
|
|
# Loaded automatically when pipewire-pulse starts.
|
|
|
|
pulse.cmd = [
|
|
{
|
|
cmd = "load-module"
|
|
args = "module-null-sink sink_name=mpd_stream sink_properties=device.description=MPD-Stream"
|
|
}
|
|
{
|
|
cmd = "load-module"
|
|
args = "module-loopback source=mpd_stream.monitor latency_msec=50"
|
|
}
|
|
]
|
|
'
|
|
|
|
write_pw_conf "$PW_CONF_DIR/discord-stream.conf" '# Persistent virtual sink for capturing Discord audio in OBS without mixing
|
|
# in the rest of the desktop (browser, MPD, system bleeps).
|
|
#
|
|
# Mirrors mpd-stream.conf — separate isolated channel for voice chat:
|
|
# Discord ──► null sink "discord_stream"
|
|
# │
|
|
# ├──► loopback ──► default sink (you still hear voice
|
|
# │ chat on speakers/headset)
|
|
# │
|
|
# └──► monitor ──► OBS "Audio Output Capture (PulseAudio)
|
|
# → Monitor of Discord-Stream"
|
|
#
|
|
# Discord side: set the app'"'"'s audio output to "Discord-Stream" once via
|
|
# pavucontrol (Playback tab). PipeWire remembers the routing across launches.
|
|
# Discord has no equivalent of MPD'"'"'s `target` setting, so this is a
|
|
# one-time per-rig manual step.
|
|
#
|
|
# Loaded automatically when pipewire-pulse starts.
|
|
|
|
pulse.cmd = [
|
|
{
|
|
cmd = "load-module"
|
|
args = "module-null-sink sink_name=discord_stream sink_properties=device.description=Discord-Stream"
|
|
}
|
|
{
|
|
cmd = "load-module"
|
|
args = "module-loopback source=discord_stream.monitor latency_msec=50"
|
|
}
|
|
]
|
|
'
|
|
|
|
# Restart pipewire-pulse only if we wrote a new conf AND OBS is not running
|
|
# (pipewire-pulse restart silently detaches OBS pulse_output_capture sources;
|
|
# fix needs OBS process restart, not just a settings refresh).
|
|
if (( PW_CONF_CHANGED )) && (( ! CHECK_ONLY )); then
|
|
if flatpak ps --columns=application 2>/dev/null | grep -qx com.obsproject.Studio \
|
|
|| pgrep -x obs >/dev/null 2>&1; then
|
|
warn "OBS is running — skipping pipewire-pulse restart (would detach OBS audio sources)"
|
|
warn " → close OBS, then: systemctl --user restart pipewire-pulse.service"
|
|
warn " → or just relog; conf loads on next pipewire-pulse start either way"
|
|
elif would "systemctl --user restart pipewire-pulse"; then
|
|
systemctl --user restart pipewire-pulse.service 2>/dev/null || \
|
|
warn "could not restart pipewire-pulse (will load on next login anyway)"
|
|
fi
|
|
fi
|
|
|
|
# Verify the sinks are actually loaded (only meaningful in install mode).
|
|
if (( ! CHECK_ONLY )); then
|
|
sleep 1
|
|
for sink in mpd_stream discord_stream; do
|
|
if pactl list short sinks 2>/dev/null | grep -q "^[0-9]*[[:space:]]\+${sink}\b"; then
|
|
ok "$sink sink loaded"
|
|
else
|
|
warn "$sink sink NOT visible to pactl — restart pipewire-pulse or relog and re-check"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 6 — MPD
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "MPD config + service"
|
|
|
|
MPD_DIR="$HOME/.config/mpd"
|
|
MPD_CONF="$MPD_DIR/mpd.conf"
|
|
|
|
write_mpd_conf() {
|
|
mkdir -p "$MPD_DIR/playlists"
|
|
cat > "$MPD_CONF" <<MPDCONF
|
|
# ~/.config/mpd/mpd.conf — generated by scripts/deploy-rig.sh.
|
|
# Re-run the deploy script to regenerate, or hand-edit for local tweaks.
|
|
|
|
music_directory "~/HDD/Music"
|
|
playlist_directory "~/.config/mpd/playlists"
|
|
db_file "~/.config/mpd/database"
|
|
state_file "~/.config/mpd/state"
|
|
sticker_file "~/.config/mpd/sticker.sql"
|
|
|
|
log_file "syslog"
|
|
auto_update "yes"
|
|
restore_paused "yes"
|
|
filesystem_charset "UTF-8"
|
|
|
|
zeroconf_enabled "yes"
|
|
zeroconf_name "MPD on $RIG_NAME_TC"
|
|
|
|
audio_output {
|
|
type "pipewire"
|
|
target "mpd_stream"
|
|
name "MPD"
|
|
}
|
|
MPDCONF
|
|
}
|
|
|
|
if [[ -f "$MPD_CONF" ]]; then
|
|
if grep -q "MPD on $RIG_NAME_TC" "$MPD_CONF"; then
|
|
ok "$MPD_CONF already configured for $RIG_NAME_TC"
|
|
else
|
|
warn "$MPD_CONF exists but zeroconf_name doesn't match $RIG_NAME_TC"
|
|
if confirm "overwrite $MPD_CONF?" && would "rewrite $MPD_CONF"; then
|
|
cp "$MPD_CONF" "$MPD_CONF.before-deploy.$$"
|
|
write_mpd_conf
|
|
ok "rewrote $MPD_CONF (backup: $MPD_CONF.before-deploy.$$)"
|
|
fi
|
|
fi
|
|
else
|
|
if would "write $MPD_CONF"; then
|
|
write_mpd_conf
|
|
ok "wrote $MPD_CONF"
|
|
fi
|
|
fi
|
|
|
|
if systemctl --user is-enabled mpd.socket >/dev/null 2>&1; then
|
|
ok "mpd.socket enabled"
|
|
else
|
|
if would "systemctl --user enable --now mpd.socket mpd.service"; then
|
|
systemctl --user enable --now mpd.socket mpd.service
|
|
ok "enabled mpd.socket + mpd.service"
|
|
fi
|
|
fi
|
|
|
|
if [[ -d "$HOME/HDD/Music" ]] && [[ -n "$(find "$HOME/HDD/Music" -maxdepth 3 -type f -print -quit 2>/dev/null)" ]]; then
|
|
if would "mpc update"; then
|
|
mpc update >/dev/null 2>&1 || warn "mpc update failed (mpd not yet running?)"
|
|
ok "MPD library scan kicked off"
|
|
fi
|
|
else
|
|
warn "~/HDD/Music is empty — MPD will have no tracks until you populate it"
|
|
fi
|
|
|
|
# Avahi for zeroconf advertisement (system service).
|
|
if systemctl is-enabled avahi-daemon.service >/dev/null 2>&1; then
|
|
ok "avahi-daemon enabled (system)"
|
|
else
|
|
warn "avahi-daemon not enabled — MPD zeroconf won't advertise. To fix:"
|
|
warn " sudo systemctl enable --now avahi-daemon.service"
|
|
fi
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 7 — custom systemd --user units
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "Systemd --user units (bridge + bot)"
|
|
|
|
UNIT_DIR="$HOME/.config/systemd/user"
|
|
mkdir -p "$UNIT_DIR"
|
|
|
|
write_unit() {
|
|
local name body path
|
|
name="$1"
|
|
body="$2"
|
|
path="$UNIT_DIR/$name"
|
|
if [[ -f "$path" ]] && diff -q <(printf '%s' "$body") "$path" >/dev/null 2>&1; then
|
|
ok "$name already up to date"
|
|
elif would "write $path"; then
|
|
printf '%s' "$body" > "$path"
|
|
ok "wrote $path"
|
|
fi
|
|
}
|
|
|
|
write_unit obs-mpd-bridge.service "[Unit]
|
|
Description=MPD → OBS WebSocket state bridge
|
|
Documentation=file:%h/.config/obs-studio/bridges/mpd-state.py
|
|
After=mpd.service mpd.socket pipewire.service
|
|
Wants=mpd.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python3 %h/.config/obs-studio/bridges/mpd-state.py
|
|
Restart=on-failure
|
|
RestartSec=2
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"
|
|
|
|
write_unit obs-twitch-bot.service "[Unit]
|
|
Description=Twitch chat → MPD control bot
|
|
Documentation=file:%h/.config/obs-studio/twitch-bot/bot.py
|
|
After=mpd.service mpd.socket network-online.target
|
|
Wants=mpd.service network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python3 %h/.config/obs-studio/twitch-bot/bot.py
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"
|
|
|
|
if would "systemctl --user daemon-reload"; then
|
|
systemctl --user daemon-reload
|
|
fi
|
|
|
|
# bridge can start now (no secrets needed)
|
|
if systemctl --user is-enabled obs-mpd-bridge.service >/dev/null 2>&1; then
|
|
ok "obs-mpd-bridge.service enabled"
|
|
else
|
|
if would "systemctl --user enable --now obs-mpd-bridge.service"; then
|
|
systemctl --user enable --now obs-mpd-bridge.service || \
|
|
warn "obs-mpd-bridge failed to start — needs vendor/obs-config.js (see phase 8)"
|
|
fi
|
|
fi
|
|
|
|
# bot must NOT auto-start until .env is populated; just enable it
|
|
if systemctl --user is-enabled obs-twitch-bot.service >/dev/null 2>&1; then
|
|
ok "obs-twitch-bot.service enabled (start manually after .env is filled)"
|
|
else
|
|
if would "systemctl --user enable obs-twitch-bot.service"; then
|
|
systemctl --user enable obs-twitch-bot.service
|
|
ok "enabled obs-twitch-bot.service (NOT started — needs .env)"
|
|
fi
|
|
fi
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 8 — OBS first-launch + WebSocket bootstrap
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "OBS first-launch + WebSocket bootstrap"
|
|
|
|
WS_CFG="$OBS_DIR/plugin_config/obs-websocket/config.json"
|
|
|
|
if [[ -f "$WS_CFG" ]]; then
|
|
ok "obs-websocket config exists ($WS_CFG)"
|
|
else
|
|
if would "launch OBS once to seed plugin_config"; then
|
|
echo " → launching OBS once to seed plugin configs."
|
|
echo " Close the OBS window when it appears (or wait — script auto-stops it)."
|
|
flatpak run com.obsproject.Studio >/dev/null 2>&1 &
|
|
OBS_PID=$!
|
|
for _ in $(seq 1 60); do
|
|
[[ -f "$WS_CFG" ]] && break
|
|
sleep 1
|
|
done
|
|
kill "$OBS_PID" 2>/dev/null || true
|
|
wait "$OBS_PID" 2>/dev/null || true
|
|
if [[ -f "$WS_CFG" ]]; then
|
|
ok "obs-websocket config seeded"
|
|
else
|
|
warn "config not seeded after 60s — start OBS manually once and re-run"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ -f "$WS_CFG" ]]; then
|
|
if python3 -c "import json, sys; sys.exit(0 if json.load(open('$WS_CFG')).get('server_enabled') else 1)" 2>/dev/null; then
|
|
ok "obs-websocket server_enabled = true"
|
|
else
|
|
if would "set server_enabled=true in $WS_CFG"; then
|
|
python3 - "$WS_CFG" <<'PY'
|
|
import json, sys
|
|
p = sys.argv[1]
|
|
d = json.load(open(p))
|
|
d['server_enabled'] = True
|
|
json.dump(d, open(p, 'w'), indent=4)
|
|
PY
|
|
ok "set server_enabled=true (takes effect on next OBS launch)"
|
|
fi
|
|
fi
|
|
|
|
if [[ -f "$OBS_DIR/vendor/obs-config.js" ]]; then
|
|
ok "vendor/obs-config.js present"
|
|
else
|
|
if would "bash scripts/setup.sh"; then
|
|
bash "$DIR/setup.sh" || warn "setup.sh failed — re-run after OBS WS port is reachable"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
# Phase 9 — secrets stub + final report
|
|
# ────────────────────────────────────────────────────────────────────────────
|
|
step "Secrets stub + final report"
|
|
|
|
stub_env() {
|
|
local target="$1"
|
|
if [[ -f "$target" ]]; then
|
|
ok "$target exists (leaving alone)"
|
|
return
|
|
fi
|
|
if [[ -f "$OBS_DIR/twitch-bot/.env.example" ]] && would "stub $target from .env.example"; then
|
|
cp "$OBS_DIR/twitch-bot/.env.example" "$target"
|
|
chmod 600 "$target"
|
|
ok "stubbed $target (chmod 600)"
|
|
fi
|
|
}
|
|
|
|
stub_env "$OBS_DIR/twitch-bot/.env"
|
|
stub_env "$OBS_DIR/twitch-bot/.env.ophi118"
|
|
|
|
# Refresh telemetry so the rig name is correct on this machine.
|
|
if (( ! CHECK_ONLY )); then
|
|
if would "scripts/telemetry.sh --collect --no-review (rig snapshot)"; then
|
|
bash "$DIR/telemetry.sh" --collect --no-review || \
|
|
warn "telemetry.sh --collect failed — re-run interactively to review"
|
|
ok "telemetry refreshed (rig=$RIG_NAME_TC)"
|
|
fi
|
|
fi
|
|
|
|
cat <<DONE
|
|
|
|
╔═══════════════════════════════════════════════════════════════╗
|
|
║ Deployment complete — manual steps remaining: ║
|
|
╚═══════════════════════════════════════════════════════════════╝
|
|
|
|
1. Twitch bot token (chat:read chat:edit)
|
|
https://twitchtokengenerator.com
|
|
→ fill twitch-bot/.env
|
|
|
|
2. Broadcaster token (channel:manage:broadcast)
|
|
https://twitchtokengenerator.com
|
|
→ fill twitch-bot/.env.ophi118
|
|
|
|
3. Twitch stream key
|
|
Twitch Dashboard → Settings → Stream → Copy stream key
|
|
→ paste into OBS Settings → Stream
|
|
(saved to basic/profiles/ophi118/service.json)
|
|
|
|
4. Once .env is populated:
|
|
systemctl --user restart obs-twitch-bot.service
|
|
|
|
5. Review telemetry interactively:
|
|
bash scripts/telemetry.sh --collect
|
|
|
|
6. Launch OBS:
|
|
flatpak run com.obsproject.Studio
|
|
|
|
7. Route Discord audio to the Discord-Stream sink (one-time):
|
|
Open pavucontrol → Playback tab → while Discord plays sound,
|
|
click its dropdown and pick "Discord-Stream". PipeWire remembers
|
|
this across launches; the OBS "Discord" source picks up audio.
|
|
|
|
Sanity checks:
|
|
pactl list short sinks | grep -E 'mpd_stream|discord_stream'
|
|
systemctl --user status mpd obs-mpd-bridge obs-twitch-bot
|
|
mpc status
|
|
ss -tlnp | grep 4455
|
|
|
|
DONE
|