#!/bin/bash # Phase 1 of CommunityDiscordMedia.md: add Authentik (Discord login) + Jellyfin to a running # vanguard-stream box. Asks for the two hostnames, checks DNS, writes configs/.env via configure.py, # starts the containers, waits for them, then prints the Phase 2/3 setup steps with real hostnames. # Run as the debian user from ~/vanguard-stream. Re-runnable. set -euo pipefail cd "$(dirname "$0")" DRY="${VG_DRY:-0}" say() { printf '\n\033[1m== %s\033[0m\n' "$*"; } ask() { local q="$1" d="${2:-}" a; read -r -p "$q${d:+ [$d]}: " a; echo "${a:-$d}"; } yes() { local a; read -r -p "$1 [Y/n]: " a; [[ -z $a || $a =~ ^[Yy] ]]; } ip_of(){ getent ahostsv4 "$1" 2>/dev/null | awk 'NR==1{print $1}' || true; } # ---- preconditions ------------------------------------------------------------------------------ [[ -f configure.py && -f docker-compose.yml ]] || { echo "run from the vanguard-stream folder"; exit 1; } [[ $EUID -ne 0 ]] || { echo "run as the debian user, not root (sudo is used where needed)"; exit 1; } MAIN=$(python3 -c 'import configure as c; print(c.read_domain())') [[ -n $MAIN ]] || { echo "main hostname not set — run ./configure.py --full first"; exit 1; } if [[ $DRY != 1 ]]; then docker info >/dev/null 2>&1 || { echo "docker not usable as this user — log out/in (docker group) or run install.sh"; exit 1; } fi read -r AUTH_CUR WATCH_CUR < <(python3 -c 'import configure as c; h=c.read_hosts(); print(h.get("auth",""), h.get("watch",""))') BASE="${MAIN#*.}"; [[ $MAIN == *.*.* ]] || BASE="$MAIN" say "Community media server — Phase 1 (Authentik + Jellyfin) on $MAIN" echo "Needs two DNS names as CNAMEs to $MAIN. Create them now if you haven't." AUTH=$(ask "Authentik hostname" "${AUTH_CUR:-auth.$BASE}") WATCH=$(ask "Jellyfin hostname" "${WATCH_CUR:-watch.$BASE}") GIT_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("git",""))') GIT=$(ask "Gitea hostname" "${GIT_CUR:-git.$BASE}") BOT_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("bot",""))') BOT=$(ask "Bot web UI hostname" "${BOT_CUR:-bot.$BASE}") # ---- DNS sanity ---------------------------------------------------------------------------------- say "DNS check" MAIN_IP=$(ip_of "$MAIN"); ok=1 for h in "$AUTH" "$WATCH" "$GIT" "$BOT"; do ip=$(ip_of "$h") if [[ -z $ip ]]; then echo " $h: does not resolve yet"; ok=0 elif [[ $ip != "$MAIN_IP" ]]; then echo " $h -> $ip (main is $MAIN_IP) — points somewhere else"; ok=0 else echo " $h -> $ip ok"; fi done if [[ $ok == 0 ]]; then echo " Let's Encrypt will fail for names that don't point here. Caddy retries on its own once DNS is right." yes "continue anyway?" || exit 1 fi # ---- write configs, .env, start containers ----------------------------------------------------- say "Applying (configure.py --hosts)" VG_DRY=$DRY python3 configure.py --hosts "$AUTH" "$WATCH" "$GIT" "$BOT" | sed -n '/== writing/,$p' | grep -vE '^ (techy|diceofire|[a-z0-9_-]+) password' || true [[ $DRY == 1 ]] && { echo "(dry run: skipping container wait)"; exit 0; } # ---- wait --------------------------------------------------------------------------------------- say "Waiting for containers" for c in authentik-db authentik-redis authentik-server authentik-worker jellyfin mariadb discord-bot gitea caddy; do for _ in $(seq 1 60); do st=$(docker inspect -f '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' "$c" 2>/dev/null || echo missing) case "$st" in running\ healthy|running\ ) echo " $c: $st"; break;; esac sleep 3 done [[ $st == running* ]] || echo " $c: $st <- check: docker logs $c" done say "Waiting for HTTPS (certificates + Authentik migrations; up to ~3 min)" for h in "$AUTH" "$WATCH" "$GIT"; do for _ in $(seq 1 60); do code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' "https://$h/" || true) [[ $code =~ ^(200|302|301)$ ]] && { echo " https://$h/ $code"; break; } sleep 3 done [[ $code =~ ^(200|302|301)$ ]] || echo " https://$h/ not answering yet (got '${code:-none}') — docker logs caddy / authentik-server" done echo; free -h | awk 'NR<=2{print " "$0}' # ---- Vanguard Metadata plugin (optional: only if a built zip was uploaded with the bundle) ------------ if ls jellyfin-plugin-vanguard/dist/VanguardMetadata_*.zip >/dev/null 2>&1; then say "Installing the Vanguard Metadata Jellyfin plugin" bash installJellyfinPlugin.sh || echo " plugin install failed — see above; Jellyfin works without it" fi # ---- instructions ------------------------------------------------------------------------------- cat < set the akadmin password (keep it somewhere safe) 2. Discord developer portal -> New Application -> OAuth2 -> Redirects: add https://$AUTH/source/oauth/callback/discord/ (the last path segment is the source SLUG you choose in step 3 — keep them identical) copy Client ID and Client Secret 3. Authentik -> Directory -> Federation & Social login -> Create -> Discord OAuth Source Name: Discord Slug: discord (must match the redirect above) User matching: "Link users on unique identifier" (default; creates the user on first login) Consumer key = Client ID, Consumer secret = Client Secret; Scopes: leave empty Flow settings (if shown): authentication default-source-authentication, enrollment default-source-enrollment 4. Put the source on the login page (a new source is NOT shown until you do this): Flows & Stages -> Stages -> edit "default-authentication-identification" -> Sources: select the Discord source; tick "Show sources' labels" -> Update 5. Make Discord users "Internal" (External users can't open the portal or admin UI): Flows & Stages -> Stages -> edit "default-source-enrollment-write" -> User type: Internal -> Update 6. Directory -> Groups -> create: streamers, editors, admins 7. Open a private window -> https://$AUTH/ -> click "Discord" -> log in; confirm the user appears under Directory -> Users (if it says External, edit it -> User type Internal); add yourself to admins 8. (optional) only members of YOUR Discord server may log in: a. Discord -> User Settings -> Advanced -> Developer Mode; right-click your server -> Copy Server ID b. edit the Discord source -> Protocol settings -> Scopes: guilds -> Update c. Customization -> Policies -> Create -> Expression Policy, name discord-guild-member, paste authentik/discord-guild-policy.py (set ACCEPTED_GUILD_ID and GUILD_NAME) -> Create d. Flows & Stages -> Flows -> default-source-enrollment -> "Policy / Group / User Bindings" -> Bind existing policy -> discord-guild-member. Repeat for default-source-authentication. Then edit each of those two flows -> Policy engine mode: all (default "any" would let the existing if-sso binding alone pass the login; keep that binding, just switch the mode). e. test: private window, Discord login with a member (works) and a non-member (denied with message) Users who logged in before (b) get a one-time Discord re-consent for the new scope. 9. pin the version: edit .env -> AUTHENTIK_TAG= (docker inspect authentik-server) PHASE 3 — Jellyfin (direct play only on this CPU) 1. https://$WATCH/ -> wizard: admin user; libraries: /media/streams and /media/clips -> type "Shows": metadata downloaders = ONLY "Vanguard Metadata" (Series, Season, Episode); image fetchers = Vanguard Metadata (Series) + Screen Grabber (Episode); real-time monitoring on /media/finals -> type "Home Videos", all metadata OFF (the Vanguard plugin is installed by this script when its zip is present; it needs no config to do seasons — Discord/Authentik fields are optional and add names, avatars, event titles, the feed) then Dashboard -> Scheduled Tasks -> run "Vanguard: roll seasons" once after the first scan 2. Dashboard -> Playback -> Transcoding: hardware acceleration = none Users -> each user -> Playback: untick "Allow video playback that requires transcoding" 3. Dashboard -> Plugins -> Repositories -> "+" -> name SSO-Auth, URL: https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/manifest-release/manifest.json -> Save; Catalog tab -> "SSO Authentication" -> Install -> docker compose restart jellyfin 4. Authentik -> Applications -> Create with provider -> OAuth2/OpenID name: jellyfin; redirect URI: https://$WATCH/sso/OID/redirect/authentik note the client ID / secret and the provider's slug 5. Jellyfin -> Plugins -> SSO-Auth -> OID providers -> add "authentik": OID endpoint: https://$AUTH/application/o// client ID / secret; Enabled; Roles claim: groups; Admin roles: admins; Enabled folders: the three libraries; default "disable transcoding" on Scheme Override: https Port Override: EMPTY (a value adds ":443" to the redirect -> mismatch) -> Save at the bottom of the page (unsaved providers give "Provider does not exist") 6. test the plugin: open https://$WATCH/sso/OID/start/authentik -> Authentik -> back in, logged in. Always start from the /start/ URL; opening the /redirect/ URL by hand gives "missing state". 7. login page = Discord button first, local form behind an "Admin login" link: Dashboard -> General -> Branding Login disclaimer: paste jellyfin/branding-disclaimer.html (replace watch.HOST with $WATCH) Custom CSS: paste jellyfin/branding.css -> Save; reload https://$WATCH/ BOT WEB UI (https://$BOT/) — Authentik forward-auth gate, create once: Authentik -> Applications -> Create with provider -> Proxy Provider mode "Forward auth (single application)", External host https://$BOT -> Applications -> Outposts -> edit "authentik Embedded Outpost" -> add the new application -> Update -> policy on the application: groups admins / editors / streamers as you see fit Until this exists the site returns an error (never open). The bot receives X-Authentik-Username/-Groups/-Email. GITEA (git.$GIT) 1. https://$GIT/ -> the first visit shows the install page: database is pre-filled from env; set the admin account at the bottom (registration is closed afterwards) -> Install Gitea 2. ./registerGiteaRunner.sh -> registers "vanguard-runner"; Site Administration -> Actions -> Runners 3. users: Site Administration -> Identity & Access -> User Accounts -> Create (or Authentik OIDC later) 4. git remotes: ssh://git@$GIT:2222//.git or https://$GIT//.git Then continue with Phase 4 in CommunityDiscordMedia.md (the Vanguard Metadata Jellyfin plugin). MSG