#!/bin/bash
# oshkosh-live.sh [start|stop] — built for AirVenture 2026; reusable each year.
# BEFORE AirVenture 2027 refresh: the CAMS YouTube ids, LIVEFEED_EAA videoId (EAA rotates
# it, sometimes mid-show), and lineup.json. Method + checklist:
#   /mnt/obsidian/Reports/oshkosh-2027-refresh-stream-ids-lineup-before-ne.md
# Live EAA AirVenture video tiles on the desktop (mpv desktop-layer windows,
# same technique as camera-wallpaper.sh live tiles).
#
# v2 (2026-07-15 PM): YouTube rate-limited the WAN IP when 14 tiles each ran
# their own yt-dlp resolve with 8s retry loops (HTTP 429 / bot-check). Now:
#   - each stream's HLS URL is resolved ONCE via yt-dlp, cached in /tmp,
#     shared by both monitors' tiles (7 resolves per ~5h, not 100+/min)
#   - resolves are serialized behind an flock + spaced 5s apart
#   - mpv plays the direct googlevideo URL (--no-ytdl) — no resolver in mpv
#   - failure backoff is 120s, not 8s
export DISPLAY=:0
export XAUTHORITY="${XAUTHORITY:-$HOME/.Xauthority}"

FLAG=/tmp/oshkosh-live.flag
PIDFILE=/tmp/oshkosh-live.pid
LOCK=/tmp/oshkosh-resolve.lock
LOG=/tmp/oshkosh-live.log

CAMS=(
  "Green Dot|CvBfCakKQOA"
  "Warbirds|8XtERF62tGA"
  "Boeing Plaza|rXH0zy6sLqo"
  "Vintage Tower|ZAsxWfxeaVY"
  "Ultralights|frrnSfxoFkM"
  "Seaplane Base|Kpr9ZJphux0"
  "Oshkosh LIVE|LIVEFEED"
)
# LIVEFEED tile (2026-07-20): EAA's produced broadcast from eaa.org/airventure/live DURING
# air-show windows from lineup.json (show time -30min → +4h; night shows → +3h), else the
# ADS-B radar. The stream alone can't be trusted: EAA keeps it live with an ambient plaza cam
# after the show, so the schedule gates it; the ffmpeg FRAME-TEST covers off-air resolution.
# LIVEFEED playback is always capped at 15 min so the tile re-evaluates the window promptly.
# FB_MARK present = currently on radar (informational).
LIVEFEED_EAA="https://players.brightcove.net/627008079/default_default/index.html?videoId=6401832923112"  # EAA "Featured Stream" = the page's primary airshow feed (2026-07-22: old id 6375380916112 was stale/rotated-away, showed wrong content)
LIVEFEED_FALLBACK="-jSEC_rpBUo"   # EAA Radio ADS-B radar (24/7)
FB_MARK=/tmp/oshkosh-hls-LIVEFEED.fallback

in_show_window() {  # rc 0 = now is inside a lineup.json air-show window (host clock is CT)
  python3 - <<'PY'
import json, os, re, sys, datetime
# lineup.json lookup (2026-07-31): this script is PUBLISHED for download, so it
# must not reference the author's vault. Search, in order: $OSHKOSH_LINEUP, the
# script's own directory, ~/.config/oshkosh/, then the published copy. Anyone who
# downloads the script now gets working show-window logic instead of a silent
# fall-through to the frame test.
def _lineup():
    here = os.path.dirname(os.path.abspath(os.environ.get("OSHKOSH_SELF", sys.argv[0] or ".")))
    for p in filter(None, [os.environ.get("OSHKOSH_LINEUP"),
                           os.path.join(here, "lineup.json"),
                           os.path.expanduser("~/.config/oshkosh/lineup.json")]):
        try:
            return json.load(open(p))["days"]
        except Exception:
            continue
    try:   # last resort: the published schedule (offline-safe, short timeout)
        import urllib.request
        # Explicit User-Agent: Cloudflare 403s the default python-urllib UA
        # (same trap as the Discord webhooks — verified here 2026-07-31).
        req = urllib.request.Request("https://dizydiz.com/oshkosh/lineup.json",
                                     headers={"User-Agent": "oshkosh-live/2027 (+https://dizydiz.com/oshkosh)"})
        with urllib.request.urlopen(req, timeout=5) as r:
            return json.load(r)["days"]
    except Exception:
        return None

days = _lineup()
if days is None:
    sys.exit(0)   # no schedule readable -> let the frame-test decide
now = datetime.datetime.now()
d = days.get(now.strftime("%Y-%m-%d"))
if not d: sys.exit(1)
def win(t, hours):
    m = re.search(r"(\d{1,2})(?::(\d{2}))?\s*(AM|PM)", t or "")
    if not m: return None
    hh = int(m.group(1)) % 12 + (12 if m.group(3) == "PM" else 0)
    s = now.replace(hour=hh, minute=int(m.group(2) or 0), second=0) - datetime.timedelta(minutes=30)
    return (s, s + datetime.timedelta(hours=hours))
ws = [w for w in (win(d.get("time"), 4), win(d.get("night"), 3)) if w]
sys.exit(0 if any(a <= now <= b for a, b in ws) else 1)
PY
}
MONS=( "3440 1440 2170 0" "2560 1440 0 1440" )   # M0 top-middle, M2 far-left
COLS=4; ROWS=2
URL_MAX_AGE=18000   # re-resolve HLS URLs older than 5h (they expire ~6h)

get_url() {  # $1=video id (or LIVEFEED) → echo cached-or-fresh HLS url ("" on failure)
  local ID="$1" F="/tmp/oshkosh-hls-${ID}.url" MAXAGE=$URL_MAX_AGE
  [ "$ID" = "LIVEFEED" ] && MAXAGE=900   # re-evaluate the show window / feed every 15 min
  if [ -f "$F" ] && [ $(( $(date +%s) - $(stat -c %Y "$F") )) -lt $MAXAGE ] && [ -s "$F" ]; then
    cat "$F"; return 0
  fi
  (
    flock -w 300 9 || exit 1
    # re-check under the lock (another tile may have just resolved it)
    if [ -f "$F" ] && [ $(( $(date +%s) - $(stat -c %Y "$F") )) -lt $MAXAGE ] && [ -s "$F" ]; then
      exit 0
    fi
    sleep 5   # spacing between consecutive resolves
    if [ "$ID" = "LIVEFEED" ]; then
      CANDS=("https://www.youtube.com/watch?v=${LIVEFEED_FALLBACK}")
      in_show_window && CANDS=("$LIVEFEED_EAA" "${CANDS[@]}")
      for CAND in "${CANDS[@]}"; do
        U=$(yt-dlp -g -f "bestvideo[height<=480]/best[height<=480]/best" "$CAND" 2>>"$LOG" | head -1)
        if [ -n "$U" ] && timeout 20 ffmpeg -loglevel quiet -i "$U" -frames:v 1 -f null - </dev/null >/dev/null 2>&1; then
          printf '%s\n' "$U" > "$F"
          if [ "$CAND" = "$LIVEFEED_EAA" ]; then rm -f "$FB_MARK"; else touch "$FB_MARK"; fi
          break
        fi
        sleep 5
      done
    else
      yt-dlp -g -f "bestvideo[height<=480]/best[height<=480]/best" "https://www.youtube.com/watch?v=${ID}" \
        2>>"$LOG" | head -1 > "$F.tmp" && [ -s "$F.tmp" ] && mv "$F.tmp" "$F"
    fi
  ) 9>"$LOCK"
  [ -s "$F" ] && cat "$F"
}

place_tile() {  # $1=title $2=X $3=Y $4=W $5=H — wait for the (re)spawned window, pin it into its grid slot
  local T="$1" PX="$2" PY="$3" PW="$4" PH="$5" WID HEX
  for _ in $(seq 1 24); do
    WID=$(xdotool search --name "^${T}$" 2>/dev/null | head -1)
    if [ -n "$WID" ]; then
      HEX=$(printf "0x%08x" "$WID")
      wmctrl -i -r "$HEX" -b remove,maximized_vert,maximized_horz
      wmctrl -i -r "$HEX" -e "0,${PX},${PY},${PW},${PH}"
      wmctrl -i -r "$HEX" -b add,below,sticky
      wmctrl -i -r "$HEX" -b add,skip_taskbar,skip_pager
      xprop -id "$WID" -f _NET_WM_WINDOW_TYPE 32a -set _NET_WM_WINDOW_TYPE _NET_WM_WINDOW_TYPE_DESKTOP
      return 0
    fi
    sleep 5
  done
}

stop_live() {
  rm -f "$FLAG"
  [ -f "$PIDFILE" ] && while read -r P; do kill "$P" 2>/dev/null; done < "$PIDFILE"
  pkill -f "oshkosh-til[e]" 2>/dev/null
  rm -f "$PIDFILE"
  echo "Oshkosh live tiles stopped"
}

case "${1:-start}" in
  stop) stop_live; exit 0 ;;
esac

stop_live >/dev/null 2>&1
touch "$FLAG"

MI=0
for MON in "${MONS[@]}"; do
  read -r MW MH MX MY <<< "$MON"
  TW=$((MW / COLS)); TH=$((MH / ROWS))
  CI=0
  for cam in "${CAMS[@]}"; do
    ID="${cam#*|}"
    TITLE="oshkosh-tile-${MI}-${CI}"
    COL=$((CI % COLS)); ROW=$((CI / COLS))
    TX=$((MX + COL * TW)); TY=$((MY + ROW * TH))
    (
      while [ -f "$FLAG" ]; do
        URL=$(get_url "$ID")
        if [ -z "$URL" ]; then
          echo "$(date '+%T') $TITLE: no URL (rate-limited?) — backing off 120s" >> "$LOG"
          sleep 120; continue
        fi
        CAP=""   # LIVEFEED always capped so the tile re-evaluates the show window
        [ "$ID" = "LIVEFEED" ] && CAP="timeout 900"
        # every (re)spawned mpv window must be re-pinned into its slot — the startup
        # placement pass is long gone by the first LIVEFEED 15-min recycle, which left
        # respawned tiles floating at the WM default spot (2026-07-21 fix)
        # kill any stale/duplicate mpv already on THIS slot before spawning, so the
        # LIVEFEED 900s recycle (and any messy double-start) can't stack overlapping
        # players on one tile — the "airshow tile glitching" cause (2026-07-22 fix)
        pkill -f -- "--title=$TITLE" 2>/dev/null; sleep 0.3
        place_tile "$TITLE" "$TX" "$TY" "$TW" "$TH" &
        # (mpv --length errors out on live HLS — cap externally instead)
        $CAP mpv --no-osc --no-osd-bar --no-input-default-bindings --no-audio \
            --stop-screensaver=no --title="$TITLE" \
            --geometry="${TW}x${TH}+${TX}+${TY}" --no-border --ontop=no --keepaspect=no \
            --hidpi-window-scale=no --no-ytdl \
            --cache=yes --cache-secs=30 --demuxer-max-bytes=64MiB \
            --really-quiet "$URL" >>"$LOG" 2>&1
        # played then exited (segment gap or URL expiry): invalidate if old, chill
        [ -f "$FLAG" ] || break
        F="/tmp/oshkosh-hls-${ID}.url"
        M=$(stat -c %Y "$F" 2>/dev/null); [ -n "$M" ] && [ $(( $(date +%s) - M )) -gt 600 ] && rm -f "$F"
        sleep 20
      done
    ) &
    echo "$!" >> "$PIDFILE"
    CI=$((CI + 1))
  done
  MI=$((MI + 1))
done
echo "launched $((${#CAMS[@]} * ${#MONS[@]})) tile wrappers; URLs resolve serially (~5s apart); positioning as windows appear..."

for TRY in $(seq 1 40); do
  PLACED=0
  MI=0
  for MON in "${MONS[@]}"; do
    read -r MW MH MX MY <<< "$MON"
    TW=$((MW / COLS)); TH=$((MH / ROWS))
    CI=0
    for cam in "${CAMS[@]}"; do
      COL=$((CI % COLS)); ROW=$((CI / COLS))
      TX=$((MX + COL * TW)); TY=$((MY + ROW * TH))
      TITLE="oshkosh-tile-${MI}-${CI}"
      WID=$(xdotool search --name "^${TITLE}$" 2>/dev/null | head -1)
      if [ -n "$WID" ]; then
        HEX=$(printf "0x%08x" "$WID")
        wmctrl -i -r "$HEX" -b remove,maximized_vert,maximized_horz
        wmctrl -i -r "$HEX" -e "0,${TX},${TY},${TW},${TH}"
        wmctrl -i -r "$HEX" -b add,below,sticky
        wmctrl -i -r "$HEX" -b add,skip_taskbar,skip_pager
        xprop -id "$WID" -f _NET_WM_WINDOW_TYPE 32a -set _NET_WM_WINDOW_TYPE _NET_WM_WINDOW_TYPE_DESKTOP
        PLACED=$((PLACED + 1))
      fi
      CI=$((CI + 1))
    done
    MI=$((MI + 1))
  done
  echo "  placed ${PLACED}/$((${#CAMS[@]} * ${#MONS[@]}))"
  [ "$PLACED" -ge $((${#CAMS[@]} * ${#MONS[@]})) ] && break
  sleep 10
done
echo "Oshkosh LIVE tiles running. Stop: ~/scripts/oshkosh-live.sh stop"
