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:
2026-10-17 19:16:39 +01:00
co-authored by Claude Opus 5.5
commit 3610de7bae
45 changed files with 5666 additions and 0 deletions
+195
View File
@@ -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
+168
View File
@@ -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
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
+19
View File
@@ -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
+30
View File
@@ -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
+8
View File
@@ -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
+8
View File
@@ -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}}