Audiofy – Musikwünsche für Partys
Server (Website, Admin-Bereich, Player-API) und Player (raspotify + Controller) mit Install-Skripten für Debian-Systeme. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
# Audiofy – Installation der Rolle "Player": Spotify-Connect-Gerät (raspotify) und Controller.
|
||||
#
|
||||
# install/install-player.sh Neuinstallation (fragt alles Nötige ab)
|
||||
# install/install-player.sh --update nur Code, Python-Umgebung und Dienst aktualisieren
|
||||
#
|
||||
# Läuft auf Raspberry Pi, Mini-PC oder VM mit Debian 12+ (arm64, armhf oder amd64).
|
||||
# Ausgabe wahlweise über einen Bluetooth-Lautsprecher (BlueALSA) oder einen ALSA-Ausgang
|
||||
# (Klinke, HDMI, USB-DAC). Der Player baut nur ausgehende Verbindungen auf.
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
UPDATE=0
|
||||
BLUETOOTH=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--update) UPDATE=1 ;;
|
||||
-h|--help) sed -n '2,10p' "$0"; exit 0 ;;
|
||||
*) abbruch "Unbekannte Option: $arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
KONFIG="$AUDIOFY_DIR/config"
|
||||
ENV_DATEI="$KONFIG/.env"
|
||||
RASPOTIFY_CONF=/etc/raspotify/conf
|
||||
export AUDIOFY_DIR AUDIOFY_USER
|
||||
|
||||
schritt "Audiofy-Player: $([ $UPDATE = 1 ] && echo Update || echo Neuinstallation)"
|
||||
pruefe_system
|
||||
ARCH="$(dpkg --print-architecture)"
|
||||
case "$ARCH" in arm64|armhf|amd64) ;; *) abbruch "raspotify gibt es nicht für $ARCH (nur arm64, armhf, amd64)." ;; esac
|
||||
if [ $UPDATE = 1 ] && ! $SUDO test -f "$ENV_DATEI"; then
|
||||
abbruch "Keine bestehende Installation in $AUDIOFY_DIR gefunden – ohne --update ausführen."
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Abfragen
|
||||
if [ $UPDATE = 0 ]; then
|
||||
schritt "Angaben"
|
||||
frage GERAETENAME "Name des Spotify-Connect-Geräts" "Audiofy"
|
||||
echo " Audio-Ausgabe: 1) Bluetooth-Lautsprecher 2) Kabel/HDMI/USB (ALSA)"
|
||||
frage AUSGABE "Auswahl" "1"
|
||||
if [ "$AUSGABE" = 2 ]; then
|
||||
BLUETOOTH=0
|
||||
if command -v aplay >/dev/null; then
|
||||
info "Verfügbare ALSA-Ausgänge:"
|
||||
aplay -L 2>/dev/null | grep -E '^(default|sysdefault|plughw|hdmi)' | sed 's/^/ /' | head -20 || true
|
||||
fi
|
||||
frage ALSA_GERAET "ALSA-Gerät" "default"
|
||||
else
|
||||
BLUETOOTH=1
|
||||
ALSA_GERAET="bluealsa:DEV=00:00:00:00:00:00,PROFILE=a2dp" # = zuletzt verbundener Lautsprecher
|
||||
fi
|
||||
frage SERVER_URL "Adresse des Audiofy-Servers (leer = ohne Wünsche, nur Playlist)" ""
|
||||
SERVER_URL="${SERVER_URL%/}"
|
||||
PLAYER_TOKEN=""
|
||||
[ -n "$SERVER_URL" ] && frage_geheim PLAYER_TOKEN "PLAYER_API_TOKEN vom Server (verdeckt)"
|
||||
info "Spotify-App (developer.spotify.com): Redirect-URI http://127.0.0.1:8888/callback eintragen."
|
||||
frage SPOTIFY_CLIENT_ID "Spotify Client ID"
|
||||
frage_geheim SPOTIFY_CLIENT_SECRET "Spotify Client Secret (verdeckt)"
|
||||
frage PLAYLIST "Grund-Playlist (Link oder ID; mit Server wird sie dort eingestellt)"
|
||||
PLAYLIST_ID="$(sed -E 's#.*playlist[/:]([A-Za-z0-9]{22}).*#\1#' <<<"$PLAYLIST")"
|
||||
[[ "$PLAYLIST_ID" =~ ^[A-Za-z0-9]{22}$ ]] || { warnung "Keine gültige Playlist – bitte später eintragen."; PLAYLIST_ID=""; }
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Pakete
|
||||
schritt "Pakete"
|
||||
$SUDO apt-get update -qq
|
||||
PAKETE=(python3-venv curl ca-certificates alsa-utils)
|
||||
if [ $UPDATE = 0 ] && [ "$BLUETOOTH" = 1 ]; then
|
||||
PAKETE+=(bluez bluez-alsa-utils libasound2-plugin-bluez)
|
||||
fi
|
||||
pakete "${PAKETE[@]}"
|
||||
|
||||
if ! dpkg -s raspotify >/dev/null 2>&1; then
|
||||
schritt "raspotify (Spotify Connect)"
|
||||
$SUDO curl -sSfL https://dtcooper.github.io/raspotify/key.asc -o /usr/share/keyrings/raspotify_key.asc
|
||||
$SUDO chmod 644 /usr/share/keyrings/raspotify_key.asc
|
||||
echo "deb [signed-by=/usr/share/keyrings/raspotify_key.asc] https://dtcooper.github.io/raspotify raspotify main" \
|
||||
| $SUDO tee /etc/apt/sources.list.d/raspotify.list >/dev/null
|
||||
$SUDO apt-get update -qq
|
||||
pakete raspotify
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ raspotify einstellen (nur Neuinstallation)
|
||||
if [ $UPDATE = 0 ]; then
|
||||
schritt "raspotify einstellen"
|
||||
$SUDO test -f "$RASPOTIFY_CONF.orig" || $SUDO cp "$RASPOTIFY_CONF" "$RASPOTIFY_CONF.orig"
|
||||
setze_raspotify() { # KEY WERT – vorhandene (auch auskommentierte) Zeile ersetzen, sonst anhängen
|
||||
if $SUDO grep -qE "^#?\s*$1=" "$RASPOTIFY_CONF"; then
|
||||
$SUDO sed -i -E "0,/^#?\s*$1=.*/s||$1=\"$2\"|" "$RASPOTIFY_CONF"
|
||||
else
|
||||
echo "$1=\"$2\"" | $SUDO tee -a "$RASPOTIFY_CONF" >/dev/null
|
||||
fi
|
||||
}
|
||||
setze_raspotify LIBRESPOT_NAME "$GERAETENAME"
|
||||
setze_raspotify LIBRESPOT_DEVICE_TYPE "speaker"
|
||||
setze_raspotify LIBRESPOT_BACKEND "alsa"
|
||||
setze_raspotify LIBRESPOT_DEVICE "$ALSA_GERAET"
|
||||
setze_raspotify LIBRESPOT_BITRATE "320"
|
||||
# Anmeldedaten speichern (sonst nach jedem Neustart unsichtbar) und normal protokollieren
|
||||
$SUDO sed -i -E 's/^(LIBRESPOT_DISABLE_CREDENTIAL_CACHE=|LIBRESPOT_QUIET=)/#\1/' "$RASPOTIFY_CONF"
|
||||
info "Name „$GERAETENAME“, Ausgabe $ALSA_GERAET"
|
||||
fi
|
||||
$SUDO install -d /etc/systemd/system/raspotify.service.d
|
||||
vorlage raspotify-10-audiofy.conf /etc/systemd/system/raspotify.service.d/10-audiofy.conf
|
||||
$SUDO systemctl daemon-reload
|
||||
$SUDO systemctl enable raspotify >/dev/null 2>&1
|
||||
[ $UPDATE = 0 ] && $SUDO systemctl restart raspotify
|
||||
|
||||
# ------------------------------------------------------------------ Bluetooth (nur Neuinstallation)
|
||||
BT_MAC=""
|
||||
if [ $UPDATE = 0 ] && [ "$BLUETOOTH" = 1 ]; then
|
||||
schritt "Bluetooth-Lautsprecher"
|
||||
# Der Player soll nicht selbst als Bluetooth-Empfänger für fremde Handys dienen
|
||||
$SUDO systemctl disable --now bluealsa-aplay >/dev/null 2>&1 || true
|
||||
info "Lautsprecher jetzt in den Kopplungsmodus versetzen (meist Bluetooth-Taste halten, bis die LED blinkt)."
|
||||
if ja_nein "Jetzt suchen und koppeln?" j; then
|
||||
BT_MAC="$(cd "$REPO/player" && python3 - <<'PY'
|
||||
import sys, lautsprecher as l
|
||||
print(" Suche 15 s …", file=sys.stderr)
|
||||
geraete = l.suchen()
|
||||
if not geraete:
|
||||
print(" Kein Audiogerät gefunden.", file=sys.stderr); sys.exit(0)
|
||||
for i, g in enumerate(geraete, 1):
|
||||
print(f" {i}) {g['name'] or 'Unbekannt'} ({g['mac']})", file=sys.stderr)
|
||||
print(" Nummer: ", end="", file=sys.stderr, flush=True)
|
||||
wahl = open("/dev/tty").readline().strip()
|
||||
if not wahl.isdigit() or not 1 <= int(wahl) <= len(geraete):
|
||||
sys.exit(0)
|
||||
mac = geraete[int(wahl) - 1]["mac"]
|
||||
ok, meldung = l.koppeln(mac)
|
||||
print(f" {meldung}", file=sys.stderr)
|
||||
if ok:
|
||||
print(mac)
|
||||
PY
|
||||
)" || true
|
||||
[ -n "$BT_MAC" ] || warnung "Kein Lautsprecher gekoppelt – später im Admin-Bereich unter Einstellungen → Lautsprecher."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Benutzer, Ordner, Code
|
||||
schritt "Dienst-Benutzer, Ordner und Code"
|
||||
dienst_benutzer
|
||||
$SUDO install -d -o root -g "$AUDIOFY_USER" -m 750 "$AUDIOFY_DIR"
|
||||
$SUDO install -d -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 700 "$KONFIG"
|
||||
code_kopieren "$REPO/player" "$AUDIOFY_DIR/app"
|
||||
venv_einrichten "$AUDIOFY_DIR/app/requirements.txt"
|
||||
info "Code in $AUDIOFY_DIR/app, Konfiguration in $KONFIG"
|
||||
|
||||
# ------------------------------------------------------------------ Konfiguration
|
||||
schritt "Konfiguration ($ENV_DATEI)"
|
||||
if [ $UPDATE = 0 ]; then
|
||||
$SUDO test -f "$ENV_DATEI" || $SUDO install -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 600 /dev/null "$ENV_DATEI"
|
||||
env_setzen "$ENV_DATEI" SPOTIFY_CLIENT_ID "$SPOTIFY_CLIENT_ID"
|
||||
env_setzen "$ENV_DATEI" SPOTIFY_CLIENT_SECRET "$SPOTIFY_CLIENT_SECRET"
|
||||
env_setzen "$ENV_DATEI" SPOTIFY_REDIRECT_URI "http://127.0.0.1:8888/callback"
|
||||
env_setzen "$ENV_DATEI" DEVICE_NAME "$GERAETENAME"
|
||||
env_setzen "$ENV_DATEI" WISH_EVERY_N "3"
|
||||
[ -n "$PLAYLIST_ID" ] && env_setzen "$ENV_DATEI" SPOTIFY_PLAYLIST_ID "$PLAYLIST_ID"
|
||||
[ -n "$BT_MAC" ] && env_setzen "$ENV_DATEI" BT_SPEAKER_MAC "$BT_MAC"
|
||||
if [ -n "$SERVER_URL" ]; then
|
||||
env_setzen "$ENV_DATEI" SERVER_URL "$SERVER_URL"
|
||||
env_setzen "$ENV_DATEI" PLAYER_API_TOKEN "$PLAYER_TOKEN"
|
||||
fi
|
||||
unset SPOTIFY_CLIENT_SECRET PLAYER_TOKEN
|
||||
|
||||
schritt "Spotify-Login für die Wiedergabe-Steuerung"
|
||||
info "Öffne gleich die angezeigte Adresse im Browser, melde dich an und füge danach die"
|
||||
info "Adresse aus der Adresszeile (http://127.0.0.1:8888/callback?...) hier ein."
|
||||
"${ALS_DIENST[@]}" "$AUDIOFY_DIR/.venv/bin/python" "$AUDIOFY_DIR/app/auth.py" --profil player --env "$ENV_DATEI" \
|
||||
|| warnung "Login fehlgeschlagen – später nachholen (siehe README)."
|
||||
else
|
||||
# alte Schlüsselnamen übernehmen
|
||||
env_umbenennen "$ENV_DATEI" VPS_URL SERVER_URL
|
||||
env_umbenennen "$ENV_DATEI" PI_API_TOKEN PLAYER_API_TOKEN
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Dienst
|
||||
schritt "Dienst audiofy-controller"
|
||||
vorlage audiofy-controller.service /etc/systemd/system/audiofy-controller.service
|
||||
$SUDO systemctl daemon-reload
|
||||
$SUDO systemctl enable audiofy-controller >/dev/null 2>&1
|
||||
$SUDO systemctl restart audiofy-controller
|
||||
sleep 5
|
||||
systemctl is-active --quiet audiofy-controller || abbruch "audiofy-controller läuft nicht – siehe: journalctl -u audiofy-controller"
|
||||
info "audiofy-controller läuft"
|
||||
|
||||
# ------------------------------------------------------------------ Zusammenfassung
|
||||
schritt "Fertig"
|
||||
info "Logs: journalctl -u audiofy-controller -f · journalctl -u raspotify -f"
|
||||
if [ $UPDATE = 0 ]; then
|
||||
info ""
|
||||
info "Wichtig: Wähle das Gerät „$GERAETENAME“ EINMAL in der Spotify-App aus (im selben Netz)."
|
||||
info "Danach sind die Anmeldedaten gespeichert, und der Player startet nach jedem Neustart selbst."
|
||||
fi
|
||||
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
# Audiofy – Installation der Rolle "Server": Website für Gäste, Admin-Bereich, Player-API.
|
||||
#
|
||||
# install/install-server.sh Neuinstallation (fragt alles Nötige ab)
|
||||
# install/install-server.sh --update nur Code, Python-Umgebung und Dienst aktualisieren
|
||||
#
|
||||
# Voraussetzungen: Debian 12+ (oder abgeleitet), eine (Sub-)Domain, deren A/AAAA-Record
|
||||
# auf diesen Rechner zeigt, und offene Ports 80/443 (auch in einer Cloud-Firewall).
|
||||
set -euo pipefail
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
UPDATE=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--update) UPDATE=1 ;;
|
||||
-h|--help) sed -n '2,9p' "$0"; exit 0 ;;
|
||||
*) abbruch "Unbekannte Option: $arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ENV_DATEI="$AUDIOFY_DIR/.env"
|
||||
export AUDIOFY_DIR AUDIOFY_USER
|
||||
export APP_PORT="${APP_PORT:-8000}"
|
||||
|
||||
schritt "Audiofy-Server: $([ $UPDATE = 1 ] && echo Update || echo Neuinstallation)"
|
||||
pruefe_system
|
||||
if [ $UPDATE = 1 ] && ! $SUDO test -f "$ENV_DATEI"; then
|
||||
abbruch "Keine bestehende Installation in $AUDIOFY_DIR gefunden – ohne --update ausführen."
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Abfragen
|
||||
if [ $UPDATE = 0 ]; then
|
||||
schritt "Angaben"
|
||||
frage DOMAIN "Domain der Website (z. B. musik.example.org)"
|
||||
[ -n "$DOMAIN" ] || abbruch "Ohne Domain geht es nicht (HTTPS über Let's Encrypt)."
|
||||
frage ACME_EMAIL "E-Mail für Let's Encrypt (Ablaufwarnungen)"
|
||||
info "Spotify-App (developer.spotify.com): Redirect-URI http://127.0.0.1:8888/callback eintragen."
|
||||
frage SPOTIFY_CLIENT_ID "Spotify Client ID"
|
||||
frage_geheim SPOTIFY_CLIENT_SECRET "Spotify Client Secret (verdeckt)"
|
||||
frage PLAYLIST "Grund-Playlist (Link oder ID, später im Admin änderbar)"
|
||||
PLAYLIST_ID="$(sed -E 's#.*playlist[/:]([A-Za-z0-9]{22}).*#\1#' <<<"$PLAYLIST")"
|
||||
if [ -n "$PLAYLIST_ID" ] && ! [[ "$PLAYLIST_ID" =~ ^[A-Za-z0-9]{22}$ ]]; then
|
||||
warnung "Das sieht nicht nach einer Playlist aus – bitte später im Admin-Bereich eintragen."
|
||||
PLAYLIST_ID=""
|
||||
fi
|
||||
SSH_PORT="$(${SUDO} sshd -T 2>/dev/null | awk '/^port /{print $2; exit}')"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
ja_nein "Firewall (ufw) einrichten? Erlaubt SSH (Port $SSH_PORT), 80 und 443" j && FIREWALL=1 || FIREWALL=0
|
||||
ja_nein "fail2ban für SSH einrichten?" j && FAIL2BAN=1 || FAIL2BAN=0
|
||||
ja_nein "Automatische Sicherheitsupdates aktivieren?" j && AUTOUPDATE=1 || AUTOUPDATE=0
|
||||
export DOMAIN ACME_EMAIL SSH_PORT
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Pakete
|
||||
schritt "Pakete"
|
||||
$SUDO apt-get update -qq
|
||||
PAKETE=(python3-venv sqlite3 caddy)
|
||||
if [ $UPDATE = 0 ]; then
|
||||
[ $FIREWALL = 1 ] && PAKETE+=(ufw)
|
||||
[ $FAIL2BAN = 1 ] && PAKETE+=(fail2ban)
|
||||
[ $AUTOUPDATE = 1 ] && PAKETE+=(unattended-upgrades)
|
||||
fi
|
||||
pakete "${PAKETE[@]}"
|
||||
|
||||
# ------------------------------------------------------------------ Benutzer, Ordner, Code
|
||||
schritt "Dienst-Benutzer, Ordner und Code"
|
||||
dienst_benutzer
|
||||
$SUDO install -d -o root -g "$AUDIOFY_USER" -m 750 "$AUDIOFY_DIR"
|
||||
$SUDO install -d -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 700 "$AUDIOFY_DIR/data"
|
||||
code_kopieren "$REPO/server" "$AUDIOFY_DIR/app"
|
||||
$SUDO install -o root -g "$AUDIOFY_USER" -m 640 "$REPO/player/auth.py" "$AUDIOFY_DIR/app/auth.py"
|
||||
venv_einrichten "$AUDIOFY_DIR/app/requirements.txt"
|
||||
info "Code in $AUDIOFY_DIR/app, Python-Umgebung in $AUDIOFY_DIR/.venv"
|
||||
|
||||
# ------------------------------------------------------------------ Konfiguration
|
||||
schritt "Konfiguration ($ENV_DATEI)"
|
||||
$SUDO test -f "$ENV_DATEI" || $SUDO install -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 600 /dev/null "$ENV_DATEI"
|
||||
VERWALTUNG=("${ALS_DIENST[@]}" "$AUDIOFY_DIR/.venv/bin/python" "$AUDIOFY_DIR/app/verwaltung.py")
|
||||
"${VERWALTUNG[@]}" geheimnisse --env "$ENV_DATEI" | sed 's/^/ /'
|
||||
if [ $UPDATE = 0 ]; then
|
||||
env_setzen "$ENV_DATEI" DB_PATH "$AUDIOFY_DIR/data/audiofy.db"
|
||||
env_setzen "$ENV_DATEI" PUBLIC_URL "https://$DOMAIN"
|
||||
env_setzen "$ENV_DATEI" SPOTIFY_CLIENT_ID "$SPOTIFY_CLIENT_ID"
|
||||
env_setzen "$ENV_DATEI" SPOTIFY_CLIENT_SECRET "$SPOTIFY_CLIENT_SECRET"
|
||||
[ -n "$PLAYLIST_ID" ] && env_setzen "$ENV_DATEI" SPOTIFY_PLAYLIST_ID "$PLAYLIST_ID"
|
||||
unset SPOTIFY_CLIENT_SECRET
|
||||
|
||||
schritt "Spotify-Login für die Suche"
|
||||
info "Öffne gleich die angezeigte Adresse im Browser, melde dich an und füge danach die"
|
||||
info "Adresse aus der Adresszeile (http://127.0.0.1:8888/callback?...) hier ein."
|
||||
if ja_nein "Jetzt anmelden?" j; then
|
||||
"${ALS_DIENST[@]}" "$AUDIOFY_DIR/.venv/bin/python" "$AUDIOFY_DIR/app/auth.py" --profil server --env "$ENV_DATEI" \
|
||||
|| warnung "Login fehlgeschlagen – später nachholen (siehe README)."
|
||||
fi
|
||||
|
||||
schritt "Admin-Passwort"
|
||||
"${VERWALTUNG[@]}" passwort --env "$ENV_DATEI" || warnung "Kein Passwort gesetzt – später nachholen (siehe README)."
|
||||
if ja_nein "Cloudflare Turnstile (Bot-Schutz für Login und Wünsche) einrichten?" n; then
|
||||
"${VERWALTUNG[@]}" turnstile --env "$ENV_DATEI" || warnung "Turnstile nicht eingerichtet."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Dienst
|
||||
schritt "Dienst audiofy-web"
|
||||
vorlage audiofy-web.service /etc/systemd/system/audiofy-web.service
|
||||
$SUDO systemctl daemon-reload
|
||||
$SUDO systemctl enable audiofy-web >/dev/null 2>&1
|
||||
$SUDO systemctl restart audiofy-web
|
||||
sleep 3
|
||||
systemctl is-active --quiet audiofy-web || abbruch "audiofy-web läuft nicht – siehe: journalctl -u audiofy-web"
|
||||
curl -fsS "http://127.0.0.1:$APP_PORT/healthz" >/dev/null && info "audiofy-web läuft"
|
||||
|
||||
# ------------------------------------------------------------------ Caddy & Schutz (nur Neuinstallation)
|
||||
if [ $UPDATE = 0 ]; then
|
||||
schritt "Caddy (HTTPS)"
|
||||
if $SUDO grep -q "Audiofy" /etc/caddy/Caddyfile 2>/dev/null || $SUDO grep -q "/usr/share/caddy" /etc/caddy/Caddyfile 2>/dev/null; then
|
||||
vorlage Caddyfile /etc/caddy/Caddyfile
|
||||
$SUDO caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1 || abbruch "Caddyfile ungültig"
|
||||
$SUDO systemctl enable caddy >/dev/null 2>&1
|
||||
$SUDO systemctl reload-or-restart caddy
|
||||
info "Caddy holt das Zertifikat für $DOMAIN (dauert ein paar Sekunden)."
|
||||
else
|
||||
warnung "/etc/caddy/Caddyfile enthält schon eine eigene Konfiguration – nicht verändert."
|
||||
warnung "Bitte einen Block für $DOMAIN mit 'reverse_proxy 127.0.0.1:$APP_PORT' ergänzen (Vorlage: install/vorlagen/Caddyfile)."
|
||||
fi
|
||||
|
||||
if [ $FIREWALL = 1 ]; then
|
||||
schritt "Firewall (ufw)"
|
||||
$SUDO ufw default deny incoming >/dev/null
|
||||
$SUDO ufw default allow outgoing >/dev/null
|
||||
$SUDO ufw allow "$SSH_PORT/tcp" comment "SSH" >/dev/null
|
||||
$SUDO ufw allow 80/tcp comment "HTTP (Caddy/ACME)" >/dev/null
|
||||
$SUDO ufw allow 443/tcp comment "HTTPS (Caddy)" >/dev/null
|
||||
$SUDO ufw --force enable >/dev/null
|
||||
info "ufw aktiv: $SSH_PORT, 80, 443"
|
||||
fi
|
||||
if [ $FAIL2BAN = 1 ]; then
|
||||
schritt "fail2ban"
|
||||
vorlage fail2ban-audiofy.conf /etc/fail2ban/jail.d/audiofy.conf
|
||||
$SUDO systemctl enable fail2ban >/dev/null 2>&1
|
||||
$SUDO systemctl restart fail2ban
|
||||
info "SSH-Schutz auf Port $SSH_PORT aktiv"
|
||||
fi
|
||||
if [ $AUTOUPDATE = 1 ]; then
|
||||
schritt "Automatische Sicherheitsupdates"
|
||||
vorlage 20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
info "aktiviert"
|
||||
fi
|
||||
schritt "Logs begrenzen"
|
||||
$SUDO install -d /etc/systemd/journald.conf.d
|
||||
vorlage journald-10-audiofy.conf /etc/systemd/journald.conf.d/10-audiofy.conf
|
||||
$SUDO systemctl restart systemd-journald
|
||||
info "journald max. 200 MB"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Zusammenfassung
|
||||
schritt "Fertig"
|
||||
URL="$(env_wert "$ENV_DATEI" PUBLIC_URL)"
|
||||
info "Website: $URL (Gäste nur mit Einladungslink/QR-Code)"
|
||||
info "Admin: $URL/admin"
|
||||
info "Logs: journalctl -u audiofy-web -f"
|
||||
if [ $UPDATE = 0 ]; then
|
||||
info ""
|
||||
info "Für die Player-Installation brauchst du:"
|
||||
info " Server-Adresse: $URL"
|
||||
info " Player-Token: sudo -u $AUDIOFY_USER grep PLAYER_API_TOKEN $ENV_DATEI"
|
||||
info "Gastzugang ist anfangs aus – im Admin unter „Gäste“ einschalten."
|
||||
fi
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
# Audiofy – gemeinsame Hilfsfunktionen für die Install-Skripte (wird per "source" geladen)
|
||||
# shellcheck shell=bash
|
||||
|
||||
AUDIOFY_DIR="${AUDIOFY_DIR:-/opt/audiofy}"
|
||||
AUDIOFY_USER="${AUDIOFY_USER:-audiofy}"
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
VORLAGEN="$REPO/install/vorlagen"
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
SUDO=""
|
||||
ALS_DIENST=(runuser -u "$AUDIOFY_USER" --)
|
||||
else
|
||||
SUDO="sudo"
|
||||
ALS_DIENST=(sudo -u "$AUDIOFY_USER")
|
||||
fi
|
||||
|
||||
# --- Ausgabe ------------------------------------------------------------------
|
||||
|
||||
schritt() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
warnung() { printf '\033[33m ! %s\033[0m\n' "$*"; }
|
||||
abbruch() { printf '\033[31m ✗ %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
# --- Abfragen -----------------------------------------------------------------
|
||||
|
||||
# frage VAR "Text" [Standard]
|
||||
frage() {
|
||||
local antwort
|
||||
read -r -p " $2${3:+ [$3]}: " antwort
|
||||
printf -v "$1" '%s' "${antwort:-${3:-}}"
|
||||
}
|
||||
|
||||
# frage_geheim VAR "Text" – Eingabe wird nicht angezeigt
|
||||
frage_geheim() {
|
||||
local antwort
|
||||
read -r -s -p " $2: " antwort
|
||||
echo
|
||||
printf -v "$1" '%s' "$antwort"
|
||||
}
|
||||
|
||||
# ja_nein "Frage" [j|n] – Rückgabewert 0 = ja
|
||||
ja_nein() {
|
||||
local standard="${2:-j}" antwort
|
||||
read -r -p " $1 [$([ "$standard" = j ] && echo J/n || echo j/N)]: " antwort
|
||||
antwort="${antwort:-$standard}"
|
||||
[[ "$antwort" =~ ^[jJyY] ]]
|
||||
}
|
||||
|
||||
# --- System -------------------------------------------------------------------
|
||||
|
||||
pruefe_system() {
|
||||
[ -f /etc/debian_version ] || abbruch "Nur für Debian und Debian-basierte Systeme (z. B. Raspberry Pi OS)."
|
||||
command -v systemctl >/dev/null || abbruch "systemd wird benötigt."
|
||||
command -v apt-get >/dev/null || abbruch "apt wird benötigt."
|
||||
if [ -n "$SUDO" ]; then
|
||||
command -v sudo >/dev/null || abbruch "Bitte als root ausführen oder sudo installieren."
|
||||
info "Für die Einrichtung sind root-Rechte nötig – sudo fragt ggf. nach deinem Passwort."
|
||||
sudo true || abbruch "sudo nicht möglich."
|
||||
fi
|
||||
python3 -c 'import sys; sys.exit(sys.version_info < (3, 11))' 2>/dev/null \
|
||||
|| abbruch "Python 3.11 oder neuer wird benötigt (Debian 12 oder neuer)."
|
||||
}
|
||||
|
||||
pakete() {
|
||||
info "Installiere: $*"
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" >/dev/null
|
||||
}
|
||||
|
||||
dienst_benutzer() {
|
||||
if ! id "$AUDIOFY_USER" >/dev/null 2>&1; then
|
||||
$SUDO useradd --system --home-dir "$AUDIOFY_DIR" --no-create-home --shell /usr/sbin/nologin "$AUDIOFY_USER"
|
||||
info "Dienst-Benutzer '$AUDIOFY_USER' angelegt (ohne Login, ohne sudo)"
|
||||
fi
|
||||
}
|
||||
|
||||
# code_kopieren QUELLE ZIEL – ersetzt den Inhalt von ZIEL (gehört root, Dienst darf nur lesen)
|
||||
code_kopieren() {
|
||||
local quelle="$1" ziel="$2"
|
||||
$SUDO install -d -o root -g "$AUDIOFY_USER" -m 750 "$ziel"
|
||||
$SUDO find "$ziel" -mindepth 1 -delete
|
||||
tar -C "$quelle" --exclude='__pycache__' --exclude='.venv' --exclude='.pytest_cache' --exclude='tests' -cf - . \
|
||||
| $SUDO tar -C "$ziel" --no-same-owner -xf -
|
||||
$SUDO chown -R root:"$AUDIOFY_USER" "$ziel"
|
||||
$SUDO find "$ziel" -type d -exec chmod 750 {} +
|
||||
$SUDO find "$ziel" -type f -exec chmod 640 {} +
|
||||
}
|
||||
|
||||
# venv_einrichten REQUIREMENTS
|
||||
venv_einrichten() {
|
||||
local venv="$AUDIOFY_DIR/.venv"
|
||||
[ -x "$venv/bin/python" ] || $SUDO python3 -m venv "$venv"
|
||||
$SUDO "$venv/bin/pip" install -q --upgrade pip
|
||||
$SUDO "$venv/bin/pip" install -q -r "$1"
|
||||
$SUDO chown -R root:"$AUDIOFY_USER" "$venv"
|
||||
$SUDO chmod -R g+rX,o-rwx "$venv"
|
||||
}
|
||||
|
||||
# vorlage DATEI ZIEL – ersetzt {{NAME}} durch den Wert der Shell-Variablen NAME
|
||||
vorlage() {
|
||||
local inhalt
|
||||
inhalt="$(python3 - "$VORLAGEN/$1" <<'PY'
|
||||
import os, re, sys
|
||||
text = open(sys.argv[1]).read()
|
||||
def ersetzen(m):
|
||||
wert = os.environ.get(m.group(1))
|
||||
if wert is None:
|
||||
sys.exit(f"Vorlage {sys.argv[1]}: {m.group(1)} ist nicht gesetzt")
|
||||
return wert
|
||||
print(re.sub(r"\{\{([A-Z_]+)\}\}", ersetzen, text), end="")
|
||||
PY
|
||||
)" || abbruch "Vorlage $1 konnte nicht ausgefüllt werden"
|
||||
printf '%s\n' "$inhalt" | $SUDO tee "$2" >/dev/null
|
||||
}
|
||||
|
||||
# --- .env (gehört dem Dienst-Benutzer, Rechte 600) ------------------------------
|
||||
|
||||
# env_setzen DATEI KEY WERT – der Wert läuft über stdin (nicht in der Prozessliste sichtbar)
|
||||
env_setzen() {
|
||||
printf '%s' "$3" | "${ALS_DIENST[@]}" python3 -c '
|
||||
import os, sys
|
||||
pfad, key = sys.argv[1], sys.argv[2]
|
||||
wert = sys.stdin.read()
|
||||
zeilen = open(pfad).read().splitlines() if os.path.exists(pfad) else []
|
||||
zeilen = [z for z in zeilen if not z.strip().startswith(key + "=")] + [f"{key}={wert}"]
|
||||
fd = os.open(pfad, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write("\n".join(zeilen) + "\n")
|
||||
' "$1" "$2"
|
||||
}
|
||||
|
||||
# env_wert DATEI KEY – gibt den Wert aus (leer, wenn nicht vorhanden)
|
||||
env_wert() {
|
||||
"${ALS_DIENST[@]}" sh -c 'grep -m1 "^$2=" "$1" 2>/dev/null | cut -d= -f2-' _ "$1" "$2" || true
|
||||
}
|
||||
|
||||
# env_umbenennen DATEI ALT NEU – übernimmt alte Schlüsselnamen (für Updates)
|
||||
env_umbenennen() {
|
||||
local wert
|
||||
if [ -z "$(env_wert "$1" "$3")" ]; then
|
||||
wert="$(env_wert "$1" "$2")"
|
||||
if [ -n "$wert" ]; then
|
||||
env_setzen "$1" "$3" "$wert"
|
||||
info "$2 → $3 übernommen"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
@@ -0,0 +1,19 @@
|
||||
# Audiofy – Caddy-Konfiguration (erzeugt von install-server.sh)
|
||||
{
|
||||
email {{ACME_EMAIL}}
|
||||
servers {
|
||||
protocols h1 h2
|
||||
}
|
||||
}
|
||||
|
||||
{{DOMAIN}} {
|
||||
encode zstd gzip
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000"
|
||||
X-Content-Type-Options nosniff
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy strict-origin-when-cross-origin
|
||||
-Server
|
||||
}
|
||||
reverse_proxy 127.0.0.1:{{APP_PORT}}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=Audiofy – Controller (Spotify-Steuerung, Wünsche vom Server)
|
||||
After=network-online.target bluetooth.target raspotify.service
|
||||
Wants=network-online.target raspotify.service
|
||||
# nie aufgeben, immer wieder neu starten
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
User={{AUDIOFY_USER}}
|
||||
Group={{AUDIOFY_USER}}
|
||||
WorkingDirectory={{AUDIOFY_DIR}}/app
|
||||
ExecStart={{AUDIOFY_DIR}}/.venv/bin/python -u {{AUDIOFY_DIR}}/app/controller.py --env {{AUDIOFY_DIR}}/config/.env
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
UMask=0077
|
||||
# Härtung
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths={{AUDIOFY_DIR}}/config
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_BLUETOOTH
|
||||
LockPersonality=yes
|
||||
MemoryMax=200M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
Description=Audiofy – Website, Admin-Bereich und Player-API
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User={{AUDIOFY_USER}}
|
||||
Group={{AUDIOFY_USER}}
|
||||
WorkingDirectory={{AUDIOFY_DIR}}/app
|
||||
Environment=AUDIOFY_ENV={{AUDIOFY_DIR}}/.env
|
||||
ExecStart={{AUDIOFY_DIR}}/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port {{APP_PORT}} --workers 1 --no-access-log --proxy-headers --forwarded-allow-ips 127.0.0.1
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
UMask=0077
|
||||
# Härtung
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths={{AUDIOFY_DIR}}/data {{AUDIOFY_DIR}}/.env
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
LockPersonality=yes
|
||||
MemoryMax=300M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,8 @@
|
||||
# Audiofy – SSH gegen Passwort-Raten schützen
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = {{SSH_PORT}}
|
||||
backend = systemd
|
||||
maxretry = 5
|
||||
findtime = 10m
|
||||
bantime = 1h
|
||||
@@ -0,0 +1,2 @@
|
||||
[Journal]
|
||||
SystemMaxUse=200M
|
||||
@@ -0,0 +1,8 @@
|
||||
# Audiofy: raspotify nie endgültig aufgeben (z. B. wenn der Lautsprecher kurz fehlt
|
||||
# oder das Netz beim Start noch nicht da ist)
|
||||
[Unit]
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
@@ -0,0 +1,8 @@
|
||||
# Audiofy – SSH-Härtung (NICHT automatisch installiert, siehe README „Server härten“)
|
||||
# Erst einsetzen, wenn der Login per Schlüssel für {{ADMIN_USER}} nachweislich klappt!
|
||||
Port {{SSH_PORT}}
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
AllowUsers {{ADMIN_USER}}
|
||||
Reference in New Issue
Block a user