configure.py: three new host roles (files/sync/clips) in --full, --hosts, .env, Caddy generator; main host serves about/ at the root (/about* redirects); /time only where it is fetched same-origin. about/index.html links across hosts; addCommunityMediaServer.sh asks for the new names; Caddyfile.example regenerated; README/FRESH-INSTALL/HOSTNAME-CHANGE (procedure C)/STATUS updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
190 lines
13 KiB
Bash
Executable File
190 lines
13 KiB
Bash
Executable File
#!/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 "Every hostname below is a CNAME 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}")
|
|
LIVE_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("live",""))')
|
|
LIVE=$(ask "Live portal hostname" "${LIVE_CUR:-live.$BASE}")
|
|
FILES_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("files",""))')
|
|
FILES=$(ask "Files (copyparty) hostname" "${FILES_CUR:-files.$BASE}")
|
|
SYNC_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("sync",""))')
|
|
SYNC=$(ask "Syncthing UI hostname" "${SYNC_CUR:-sync.$BASE}")
|
|
CLIPS_CUR=$(python3 -c 'import configure as c; print(c.read_hosts().get("clips",""))')
|
|
CLIPS=$(ask "Clips self-service hostname" "${CLIPS_CUR:-clips.$BASE}")
|
|
|
|
# ---- DNS sanity ----------------------------------------------------------------------------------
|
|
say "DNS check"
|
|
MAIN_IP=$(ip_of "$MAIN"); ok=1
|
|
for h in "$AUTH" "$WATCH" "$GIT" "$BOT" "$LIVE" "$FILES" "$SYNC" "$CLIPS"; 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" "$LIVE" "$FILES" "$SYNC" "$CLIPS" | 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 <<MSG
|
|
|
|
$(printf '\033[1m== Phase 1 done. Next, by hand:\033[0m')
|
|
|
|
PHASE 2 — Authentik + Discord
|
|
1. https://$AUTH/if/flow/initial-setup/ -> 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=<the tag now running> (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/<slug>/
|
|
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/
|
|
|
|
AUTHENTIK FORWARD-AUTH (gates every Vanguard host: $MAIN, $FILES, $CLIPS, $SYNC, $LIVE, $BOT) — once:
|
|
1. Applications -> Outposts -> edit "authentik Embedded Outpost" -> config: authentik_host: https://$AUTH
|
|
2. Applications -> Create with provider -> Application "Vanguard sites"; Proxy Provider, authorization
|
|
flow implicit-consent, mode "Forward auth (domain level)", authentication URL https://$AUTH,
|
|
cookie domain ${BASE}, Invalidation flow default-provider-invalidation-flow (logout = full re-login)
|
|
-> then edit the Embedded Outpost -> Applications: add it -> Update
|
|
3. Per-host access is enforced by Caddy from X-Authentik-Groups: .env BOT_REQUIRE_GROUP=bot-users
|
|
(default), LIVE_REQUIRE_GROUP= (empty = any logged-in member). ./configure.py apply regenerates.
|
|
4. Discord roles -> Authentik groups: Directory -> Groups: create bot-users (keep streamers/editors/admins);
|
|
Discord source -> Scopes: "guilds guilds.members.read"; Customization -> Policies -> Expression Policy
|
|
"discord-roles-to-groups" from authentik/discord-roles-to-groups.py (server id + role-id->group table);
|
|
bind to default-source-enrollment AND default-source-authentication (engine mode all).
|
|
5. Test: private window -> https://$LIVE/ -> Discord -> portal; https://$BOT/ -> 403 unless you hold the
|
|
bot role; https://$LIVE/me shows your username and groups.
|
|
|
|
LIVE PORTAL (https://$LIVE/) — also hosts the OBS dock (https://$LIVE/dock/) and the timecode clock source.
|
|
Deploy the page from the dev box: ./deployLive.sh (builds live/ and copies dist/ to /srv/live).
|
|
WebRTC viewers and Go Live need UDP/TCP 8189 open (firewall rule included). Optional "is live" Discord
|
|
post: LIVE_WEBHOOK_URL in .env (asked by configure.py --full).
|
|
|
|
BOT WEB UI (https://$BOT/) — gated by the forward-auth above + group bot-users; the bot dev's brief is bot/AGENT.md.
|
|
|
|
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/<org>/<repo>.git or https://$GIT/<org>/<repo>.git
|
|
|
|
Then continue with Phase 4 in CommunityDiscordMedia.md (the Vanguard Metadata Jellyfin plugin).
|
|
MSG
|