Umbenennung: Audiofy heißt jetzt Playwish

Name, Seitentitel, Dienste (playwish-web, playwish-controller), Dienst-Benutzer
(playwish), Pfade (/opt/playwish, /usr/local/src/playwish), Vorlagen und
Repository-Adresse. „Audiofy“ war bereits von mehreren Produkten belegt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 20:27:59 +01:00
co-authored by Claude Opus 5.5
parent ca8db176ac
commit fc15f2b933
44 changed files with 218 additions and 218 deletions
+31 -31
View File
@@ -1,4 +1,4 @@
# Audiofy – Musikwünsche für Partys # Playwish – Musikwünsche für Partys
Gäste scannen einen QR-Code und wünschen sich Songs. Ein **Player** spielt über Spotify Gäste scannen einen QR-Code und wünschen sich Songs. Ein **Player** spielt über Spotify
zufällige Titel aus einer Grund-Playlist – jeder N-te Titel ist ein Wunsch. Gibt es keine zufällige Titel aus einer Grund-Playlist – jeder N-te Titel ist ein Wunsch. Gibt es keine
@@ -14,10 +14,10 @@ Admin-Bereich, auch vom Handy.
## Aufbau ## Aufbau
``` ```
Gast-Handy ──HTTPS──▶ Server: Caddy → audiofy-web (FastAPI + SQLite) Gast-Handy ──HTTPS──▶ Server: Caddy → playwish-web (FastAPI + SQLite)
▲ Player-API (Bearer-Token) ▲ Player-API (Bearer-Token)
│ nur ausgehend vom Player │ nur ausgehend vom Player
Player: raspotify (Spotify Connect) + audiofy-controller Player: raspotify (Spotify Connect) + playwish-controller
└─▶ Bluetooth-Lautsprecher (BlueALSA) oder Kabel/HDMI/USB (ALSA) └─▶ Bluetooth-Lautsprecher (BlueALSA) oder Kabel/HDMI/USB (ALSA)
``` ```
@@ -39,7 +39,7 @@ Portfreigabe am Router.
## Spotify-App anlegen ## Spotify-App anlegen
Audiofy steuert Spotify über die offizielle Web API. Dafür braucht jede Installation eine Playwish steuert Spotify über die offizielle Web API. Dafür braucht jede Installation eine
eigene, kostenlose „App“ im Spotify-Developer-Bereich. Gäste brauchen nichts davon. eigene, kostenlose „App“ im Spotify-Developer-Bereich. Gäste brauchen nichts davon.
**Was du brauchst:** das Spotify-Konto, das später abspielt – mit **Premium**. **Was du brauchst:** das Spotify-Konto, das später abspielt – mit **Premium**.
@@ -51,7 +51,7 @@ Die Bezeichnungen im Dashboard können sich leicht ändern; sinngemäß:
| Feld | Eintrag | | Feld | Eintrag |
|---|---| |---|---|
| App name | z. B. `Audiofy` | | App name | z. B. `Playwish` |
| App description | z. B. `Musikwünsche für meine Party` | | App description | z. B. `Musikwünsche für meine Party` |
| Website | leer lassen | | Website | leer lassen |
| **Redirect URI** | **`http://127.0.0.1:8888/callback`** – genau so, dann „Add“ | | **Redirect URI** | **`http://127.0.0.1:8888/callback`** – genau so, dann „Add“ |
@@ -75,7 +75,7 @@ Die Bezeichnungen im Dashboard können sich leicht ändern; sinngemäß:
**Was der Development Mode bedeutet:** Neue Apps laufen im „Development Mode“. Dort gilt **Was der Development Mode bedeutet:** Neue Apps laufen im „Development Mode“. Dort gilt
(Stand 2026): Der App-Besitzer braucht Premium, nur eingetragene Konten dürfen die App (Stand 2026): Der App-Besitzer braucht Premium, nur eingetragene Konten dürfen die App
nutzen, die Suche liefert höchstens 10 Treffer, nur **eigene** Playlists sind lesbar, und nutzen, die Suche liefert höchstens 10 Treffer, nur **eigene** Playlists sind lesbar, und
die Nutzung muss **nicht kommerziell** sein. Für Audiofy reicht das völlig – abgespielt die Nutzung muss **nicht kommerziell** sein. Für Playwish reicht das völlig – abgespielt
wird nur über dein eigenes Konto. wird nur über dein eigenes Konto.
**Typische Fehler beim Login** **Typische Fehler beim Login**
@@ -97,23 +97,23 @@ wird nur über dein eigenes Konto.
**Per Einzeiler** – als Benutzer mit sudo-Rechten (oder als root) in einem Terminal: **Per Einzeiler** – als Benutzer mit sudo-Rechten (oder als root) in einem Terminal:
``` ```
curl -fsSL https://git.wind8000.de/0/audiofy/raw/branch/main/install/get.sh | bash -s -- server curl -fsSL https://git.wind8000.de/0/playwish/raw/branch/main/install/get.sh | bash -s -- server
curl -fsSL https://git.wind8000.de/0/audiofy/raw/branch/main/install/get.sh | bash -s -- player curl -fsSL https://git.wind8000.de/0/playwish/raw/branch/main/install/get.sh | bash -s -- player
``` ```
Das Startskript installiert bei Bedarf `git`, holt Audiofy nach `/usr/local/src/audiofy` und Das Startskript installiert bei Bedarf `git`, holt Playwish nach `/usr/local/src/playwish` und
startet das passende Install-Skript. Später aktualisieren: denselben Befehl mit `--update` startet das passende Install-Skript. Später aktualisieren: denselben Befehl mit `--update`
am Ende (`… | bash -s -- server --update`). am Ende (`… | bash -s -- server --update`).
Vorsichtiger – erst ansehen, dann ausführen: Vorsichtiger – erst ansehen, dann ausführen:
``` ```
curl -fsSLO https://git.wind8000.de/0/audiofy/raw/branch/main/install/get.sh curl -fsSLO https://git.wind8000.de/0/playwish/raw/branch/main/install/get.sh
less get.sh less get.sh
bash get.sh server # bzw. player bash get.sh server # bzw. player
``` ```
**Oder von Hand:** Repository klonen (`git clone https://git.wind8000.de/0/audiofy.git`) und **Oder von Hand:** Repository klonen (`git clone https://git.wind8000.de/0/playwish.git`) und
die Install-Skripte direkt starten: die Install-Skripte direkt starten:
**1. Server** **1. Server**
@@ -140,7 +140,7 @@ Spotify-Zugang und Playlist ab, installiert raspotify, koppelt auf Wunsch gleich
Bluetooth-Lautsprecher und richtet den Controller als Dienst ein. Danach folgt der Bluetooth-Lautsprecher und richtet den Controller als Dienst ein. Danach folgt der
Spotify-Login für die Wiedergabe-Steuerung. Spotify-Login für die Wiedergabe-Steuerung.
**3. Einmalig in der Spotify-App** das neue Gerät (Standard: „Audiofy“) auswählen – im selben **3. Einmalig in der Spotify-App** das neue Gerät (Standard: „Playwish“) auswählen – im selben
Netz wie der Player. Damit speichert raspotify seine Anmeldung und ist nach jedem Neustart Netz wie der Player. Damit speichert raspotify seine Anmeldung und ist nach jedem Neustart
von selbst wieder da. von selbst wieder da.
@@ -151,9 +151,9 @@ einschalten und den QR-Code zeigen oder ausdrucken.
| Aufgabe | Befehl | | Aufgabe | Befehl |
|---|---| |---|---|
| Status | `systemctl status audiofy-web` (Server) · `systemctl status audiofy-controller raspotify` (Player) | | Status | `systemctl status playwish-web` (Server) · `systemctl status playwish-controller raspotify` (Player) |
| Logs live | `journalctl -u audiofy-web -f` · `journalctl -u audiofy-controller -f` | | Logs live | `journalctl -u playwish-web -f` · `journalctl -u playwish-controller -f` |
| Neustart | `sudo systemctl restart audiofy-web` bzw. `audiofy-controller` | | Neustart | `sudo systemctl restart playwish-web` bzw. `playwish-controller` |
| Update | Einzeiler mit `--update` – oder im Repository `git pull && install/install-server.sh --update` bzw. `install/install-player.sh --update` | | Update | Einzeiler mit `--update` – oder im Repository `git pull && install/install-server.sh --update` bzw. `install/install-player.sh --update` |
`--update` ersetzt nur Code, Python-Umgebung und Dienstdatei; Konfiguration, Datenbank, `--update` ersetzt nur Code, Python-Umgebung und Dienstdatei; Konfiguration, Datenbank,
@@ -163,23 +163,23 @@ Caddy, Firewall und raspotify bleiben unverändert.
| Pfad | Inhalt | | Pfad | Inhalt |
|---|---| |---|---|
| `/opt/audiofy/app` | Code (gehört root, der Dienst darf nur lesen) | | `/opt/playwish/app` | Code (gehört root, der Dienst darf nur lesen) |
| `/opt/audiofy/.env` (Server) | Geheimnisse: Spotify, Admin-Passwort-Hash, Turnstile, `PLAYER_API_TOKEN` | | `/opt/playwish/.env` (Server) | Geheimnisse: Spotify, Admin-Passwort-Hash, Turnstile, `PLAYER_API_TOKEN` |
| `/opt/audiofy/data/audiofy.db` (Server) | Wünsche, Verlauf, Einstellungen | | `/opt/playwish/data/playwish.db` (Server) | Wünsche, Verlauf, Einstellungen |
| `/opt/audiofy/config/.env` (Player) | Geheimnisse: Spotify, `SERVER_URL`, `PLAYER_API_TOKEN`, Lautsprecher | | `/opt/playwish/config/.env` (Player) | Geheimnisse: Spotify, `SERVER_URL`, `PLAYER_API_TOKEN`, Lautsprecher |
Die `.env`-Dateien gehören dem Dienst-Benutzer `audiofy` (Rechte 600). **Sichere sie an einem Die `.env`-Dateien gehören dem Dienst-Benutzer `playwish` (Rechte 600). **Sichere sie an einem
sicheren Ort** (z. B. Passwort-Manager) – sie stehen bewusst nicht im Repository. sicheren Ort** (z. B. Passwort-Manager) – sie stehen bewusst nicht im Repository.
**Verwaltung** (interaktiv, im eigenen Terminal): **Verwaltung** (interaktiv, im eigenen Terminal):
``` ```
# Server: Admin-Passwort bzw. Turnstile-Schlüssel ändern # Server: Admin-Passwort bzw. Turnstile-Schlüssel ändern
sudo -u audiofy /opt/audiofy/.venv/bin/python /opt/audiofy/app/verwaltung.py passwort --env /opt/audiofy/.env sudo -u playwish /opt/playwish/.venv/bin/python /opt/playwish/app/verwaltung.py passwort --env /opt/playwish/.env
sudo -u audiofy /opt/audiofy/.venv/bin/python /opt/audiofy/app/verwaltung.py turnstile --env /opt/audiofy/.env sudo -u playwish /opt/playwish/.venv/bin/python /opt/playwish/app/verwaltung.py turnstile --env /opt/playwish/.env
# Spotify-Login erneuern – Server bzw. Player # Spotify-Login erneuern – Server bzw. Player
sudo -u audiofy /opt/audiofy/.venv/bin/python /opt/audiofy/app/auth.py --profil server --env /opt/audiofy/.env sudo -u playwish /opt/playwish/.venv/bin/python /opt/playwish/app/auth.py --profil server --env /opt/playwish/.env
sudo -u audiofy /opt/audiofy/.venv/bin/python /opt/audiofy/app/auth.py --profil player --env /opt/audiofy/config/.env sudo -u playwish /opt/playwish/.venv/bin/python /opt/playwish/app/auth.py --profil player --env /opt/playwish/config/.env
``` ```
Danach den jeweiligen Dienst neu starten. Danach den jeweiligen Dienst neu starten.
@@ -187,18 +187,18 @@ Danach den jeweiligen Dienst neu starten.
**Datenbank sichern** (Server): **Datenbank sichern** (Server):
``` ```
sudo -u audiofy sqlite3 /opt/audiofy/data/audiofy.db ".backup /opt/audiofy/data/sicherung.db" sudo -u playwish sqlite3 /opt/playwish/data/playwish.db ".backup /opt/playwish/data/sicherung.db"
``` ```
## Server härten (empfohlen, von Hand) ## Server härten (empfohlen, von Hand)
Das Install-Skript ändert die SSH-Konfiguration bewusst **nicht** – damit sperrt man sich Das Install-Skript ändert die SSH-Konfiguration bewusst **nicht** – damit sperrt man sich
leicht aus. Vorlage: `install/vorlagen/sshd-10-audiofy.conf` (anderer Port, nur Schlüssel, leicht aus. Vorlage: `install/vorlagen/sshd-10-playwish.conf` (anderer Port, nur Schlüssel,
kein root, nur ein Admin-Benutzer). Vorgehen: kein root, nur ein Admin-Benutzer). Vorgehen:
1. Eigenen Admin-Benutzer mit sudo anlegen, SSH-Schlüssel hinterlegen, Login testen. 1. Eigenen Admin-Benutzer mit sudo anlegen, SSH-Schlüssel hinterlegen, Login testen.
2. Neuen Port in ufw **und** einer eventuellen Cloud-Firewall öffnen. 2. Neuen Port in ufw **und** einer eventuellen Cloud-Firewall öffnen.
3. Vorlage ausgefüllt nach `/etc/ssh/sshd_config.d/10-audiofy.conf` legen, `sudo sshd -t`, `sudo systemctl restart ssh`. 3. Vorlage ausgefüllt nach `/etc/ssh/sshd_config.d/10-playwish.conf` legen, `sudo sshd -t`, `sudo systemctl restart ssh`.
4. **In einer zweiten Sitzung** testen – die alte bleibt offen, bis es klappt. 4. **In einer zweiten Sitzung** testen – die alte bleibt offen, bis es klappt.
5. Erst dann den alten Port in ufw/Cloud-Firewall schließen und fail2ban auf den neuen Port umstellen. 5. Erst dann den alten Port in ufw/Cloud-Firewall schließen und fail2ban auf den neuen Port umstellen.
@@ -209,7 +209,7 @@ kein root, nur ein Admin-Benutzer). Vorgehen:
| Keine Musik, Log: „Lautsprecher nicht verbunden“ | Lautsprecher einschalten – der Player verbindet ihn neu und startet die Musik von selbst. | | Keine Musik, Log: „Lautsprecher nicht verbunden“ | Lautsprecher einschalten – der Player verbindet ihn neu und startet die Musik von selbst. |
| Lautsprecher verbindet sich nicht mehr („Permission denied“) | Kopplung verloren. Im Admin unter Einstellungen → Lautsprecher neu suchen und verbinden (Lautsprecher im Kopplungsmodus). | | Lautsprecher verbindet sich nicht mehr („Permission denied“) | Kopplung verloren. Im Admin unter Einstellungen → Lautsprecher neu suchen und verbinden (Lautsprecher im Kopplungsmodus). |
| „Gerät nicht gefunden“ | `systemctl status raspotify`. Nach einer Neuinstallation einmal in der Spotify-App das Gerät auswählen. | | „Gerät nicht gefunden“ | `systemctl status raspotify`. Nach einer Neuinstallation einmal in der Spotify-App das Gerät auswählen. |
| Admin: „Player offline“ | `systemctl status audiofy-controller`, Internet des Players prüfen. Die Musik läuft trotzdem mit der Playlist weiter. | | Admin: „Player offline“ | `systemctl status playwish-controller`, Internet des Players prüfen. Die Musik läuft trotzdem mit der Playlist weiter. |
| Admin: „Grund-Playlist nicht lesbar“ | Nur eigene Playlists funktionieren (Spotify Development Mode). | | Admin: „Grund-Playlist nicht lesbar“ | Nur eigene Playlists funktionieren (Spotify Development Mode). |
| Befehle „No active device“ | Spotify hatte kein aktives Gerät; „Play“ startet dann die Playlist auf dem Player. | | Befehle „No active device“ | Spotify hatte kein aktives Gerät; „Play“ startet dann die Playlist auf dem Player. |
| Nach Neustart bleibt es still | Gewollt, wenn vorher pausiert oder Not-Aus aktiv war – im Admin „Play“ bzw. „Freigeben“. | | Nach Neustart bleibt es still | Gewollt, wenn vorher pausiert oder Not-Aus aktiv war – im Admin „Play“ bzw. „Freigeben“. |
@@ -231,14 +231,14 @@ cd ../player && python3 -m venv .venv && .venv/bin/pip install -r requirements.t
Copyright © 2026 Dominic Alexander Windolph Copyright © 2026 Dominic Alexander Windolph
Audiofy ist freie Software: Du kannst es unter den Bedingungen der **GNU General Public Playwish ist freie Software: Du kannst es unter den Bedingungen der **GNU General Public
License**, wie von der Free Software Foundation veröffentlicht, weitergeben und/oder License**, wie von der Free Software Foundation veröffentlicht, weitergeben und/oder
verändern – entweder gemäß Version 3 der Lizenz oder (nach deiner Wahl) jeder späteren verändern – entweder gemäß Version 3 der Lizenz oder (nach deiner Wahl) jeder späteren
Version. Der vollständige Lizenztext steht in [`LICENSE`](LICENSE). Version. Der vollständige Lizenztext steht in [`LICENSE`](LICENSE).
Audiofy wird in der Hoffnung verbreitet, dass es nützlich ist, aber **ohne jede Playwish wird in der Hoffnung verbreitet, dass es nützlich ist, aber **ohne jede
Gewährleistung** – sogar ohne die implizite Gewährleistung der Marktreife oder der Gewährleistung** – sogar ohne die implizite Gewährleistung der Marktreife oder der
Eignung für einen bestimmten Zweck. Details stehen in der GNU General Public License. Eignung für einen bestimmten Zweck. Details stehen in der GNU General Public License.
Audiofy ist kein Produkt von Spotify und steht in keiner Verbindung zu Spotify AB. Playwish ist kein Produkt von Spotify und steht in keiner Verbindung zu Spotify AB.
Spotify ist eine Marke der Spotify AB. Spotify ist eine Marke der Spotify AB.
+9 -9
View File
@@ -1,17 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Audiofy – Installation per Einzeiler (holt das Repository und startet das passende Install-Skript): # Playwish – Installation per Einzeiler (holt das Repository und startet das passende Install-Skript):
# #
# curl -fsSL https://git.wind8000.de/0/audiofy/raw/branch/main/install/get.sh | bash -s -- server # curl -fsSL https://git.wind8000.de/0/playwish/raw/branch/main/install/get.sh | bash -s -- server
# curl -fsSL https://git.wind8000.de/0/audiofy/raw/branch/main/install/get.sh | bash -s -- player # curl -fsSL https://git.wind8000.de/0/playwish/raw/branch/main/install/get.sh | bash -s -- player
# … | bash -s -- server --update später aktualisieren (holt den neuesten Stand) # … | bash -s -- server --update später aktualisieren (holt den neuesten Stand)
# #
# Die Quellen liegen danach in /usr/local/src/audiofy. Andere Quelle/Zweig: # Die Quellen liegen danach in /usr/local/src/playwish. Andere Quelle/Zweig:
# AUDIOFY_REPO=https://…/audiofy.git AUDIOFY_BRANCH=main # PLAYWISH_REPO=https://…/playwish.git PLAYWISH_BRANCH=main
set -euo pipefail set -euo pipefail
REPO="${AUDIOFY_REPO:-https://git.wind8000.de/0/audiofy.git}" REPO="${PLAYWISH_REPO:-https://git.wind8000.de/0/playwish.git}"
ZWEIG="${AUDIOFY_BRANCH:-main}" ZWEIG="${PLAYWISH_BRANCH:-main}"
QUELLE="${AUDIOFY_SRC:-/usr/local/src/audiofy}" QUELLE="${PLAYWISH_SRC:-/usr/local/src/playwish}"
ROLLE="${1:-}" ROLLE="${1:-}"
[ $# -gt 0 ] && shift [ $# -gt 0 ] && shift
@@ -25,7 +25,7 @@ if [ "$(id -u)" -eq 0 ]; then SUDO=""; else
SUDO="sudo" SUDO="sudo"
fi fi
echo "==> Audiofy-Quellen holen ($REPO, Zweig $ZWEIG)" echo "==> Playwish-Quellen holen ($REPO, Zweig $ZWEIG)"
if ! command -v git >/dev/null; then if ! command -v git >/dev/null; then
$SUDO apt-get update -qq $SUDO apt-get update -qq
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git >/dev/null $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git >/dev/null
+22 -22
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Audiofy – Installation der Rolle "Player": Spotify-Connect-Gerät (raspotify) und Controller. # Playwish – 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 Neuinstallation (fragt alles Nötige ab)
# install/install-player.sh --update nur Code, Python-Umgebung und Dienst aktualisieren # install/install-player.sh --update nur Code, Python-Umgebung und Dienst aktualisieren
@@ -20,23 +20,23 @@ for arg in "$@"; do
esac esac
done done
KONFIG="$AUDIOFY_DIR/config" KONFIG="$PLAYWISH_DIR/config"
ENV_DATEI="$KONFIG/.env" ENV_DATEI="$KONFIG/.env"
RASPOTIFY_CONF=/etc/raspotify/conf RASPOTIFY_CONF=/etc/raspotify/conf
export AUDIOFY_DIR AUDIOFY_USER export PLAYWISH_DIR PLAYWISH_USER
schritt "Audiofy-Player: $([ $UPDATE = 1 ] && echo Update || echo Neuinstallation)" schritt "Playwish-Player: $([ $UPDATE = 1 ] && echo Update || echo Neuinstallation)"
pruefe_system pruefe_system
ARCH="$(dpkg --print-architecture)" ARCH="$(dpkg --print-architecture)"
case "$ARCH" in arm64|armhf|amd64) ;; *) abbruch "raspotify gibt es nicht für $ARCH (nur arm64, armhf, amd64)." ;; esac 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 if [ $UPDATE = 1 ] && ! $SUDO test -f "$ENV_DATEI"; then
abbruch "Keine bestehende Installation in $AUDIOFY_DIR gefunden – ohne --update ausführen." abbruch "Keine bestehende Installation in $PLAYWISH_DIR gefunden – ohne --update ausführen."
fi fi
# ------------------------------------------------------------------ Abfragen # ------------------------------------------------------------------ Abfragen
if [ $UPDATE = 0 ]; then if [ $UPDATE = 0 ]; then
schritt "Angaben" schritt "Angaben"
frage GERAETENAME "Name des Spotify-Connect-Geräts" "Audiofy" frage GERAETENAME "Name des Spotify-Connect-Geräts" "Playwish"
echo " Audio-Ausgabe: 1) Bluetooth-Lautsprecher 2) Kabel/HDMI/USB (ALSA)" echo " Audio-Ausgabe: 1) Bluetooth-Lautsprecher 2) Kabel/HDMI/USB (ALSA)"
frage AUSGABE "Auswahl" "1" frage AUSGABE "Auswahl" "1"
if [ "$AUSGABE" = 2 ]; then if [ "$AUSGABE" = 2 ]; then
@@ -50,7 +50,7 @@ if [ $UPDATE = 0 ]; then
BLUETOOTH=1 BLUETOOTH=1
ALSA_GERAET="bluealsa:DEV=00:00:00:00:00:00,PROFILE=a2dp" # = zuletzt verbundener Lautsprecher ALSA_GERAET="bluealsa:DEV=00:00:00:00:00:00,PROFILE=a2dp" # = zuletzt verbundener Lautsprecher
fi fi
frage SERVER_URL "Adresse des Audiofy-Servers (leer = ohne Wünsche, nur Playlist)" "" frage SERVER_URL "Adresse des Playwish-Servers (leer = ohne Wünsche, nur Playlist)" ""
SERVER_URL="${SERVER_URL%/}" SERVER_URL="${SERVER_URL%/}"
PLAYER_TOKEN="" PLAYER_TOKEN=""
[ -n "$SERVER_URL" ] && frage_geheim PLAYER_TOKEN "PLAYER_API_TOKEN vom Server (verdeckt)" [ -n "$SERVER_URL" ] && frage_geheim PLAYER_TOKEN "PLAYER_API_TOKEN vom Server (verdeckt)"
@@ -102,7 +102,7 @@ if [ $UPDATE = 0 ]; then
info "Name „$GERAETENAME“, Ausgabe $ALSA_GERAET" info "Name „$GERAETENAME“, Ausgabe $ALSA_GERAET"
fi fi
$SUDO install -d /etc/systemd/system/raspotify.service.d $SUDO install -d /etc/systemd/system/raspotify.service.d
vorlage raspotify-10-audiofy.conf /etc/systemd/system/raspotify.service.d/10-audiofy.conf vorlage raspotify-10-playwish.conf /etc/systemd/system/raspotify.service.d/10-playwish.conf
$SUDO systemctl daemon-reload $SUDO systemctl daemon-reload
$SUDO systemctl enable raspotify >/dev/null 2>&1 $SUDO systemctl enable raspotify >/dev/null 2>&1
[ $UPDATE = 0 ] && $SUDO systemctl restart raspotify [ $UPDATE = 0 ] && $SUDO systemctl restart raspotify
@@ -144,16 +144,16 @@ fi
# ------------------------------------------------------------------ Benutzer, Ordner, Code # ------------------------------------------------------------------ Benutzer, Ordner, Code
schritt "Dienst-Benutzer, Ordner und Code" schritt "Dienst-Benutzer, Ordner und Code"
dienst_benutzer dienst_benutzer
$SUDO install -d -o root -g "$AUDIOFY_USER" -m 750 "$AUDIOFY_DIR" $SUDO install -d -o root -g "$PLAYWISH_USER" -m 750 "$PLAYWISH_DIR"
$SUDO install -d -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 700 "$KONFIG" $SUDO install -d -o "$PLAYWISH_USER" -g "$PLAYWISH_USER" -m 700 "$KONFIG"
code_kopieren "$REPO/player" "$AUDIOFY_DIR/app" code_kopieren "$REPO/player" "$PLAYWISH_DIR/app"
venv_einrichten "$AUDIOFY_DIR/app/requirements.txt" venv_einrichten "$PLAYWISH_DIR/app/requirements.txt"
info "Code in $AUDIOFY_DIR/app, Konfiguration in $KONFIG" info "Code in $PLAYWISH_DIR/app, Konfiguration in $KONFIG"
# ------------------------------------------------------------------ Konfiguration # ------------------------------------------------------------------ Konfiguration
schritt "Konfiguration ($ENV_DATEI)" schritt "Konfiguration ($ENV_DATEI)"
if [ $UPDATE = 0 ]; then if [ $UPDATE = 0 ]; then
$SUDO test -f "$ENV_DATEI" || $SUDO install -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 600 /dev/null "$ENV_DATEI" $SUDO test -f "$ENV_DATEI" || $SUDO install -o "$PLAYWISH_USER" -g "$PLAYWISH_USER" -m 600 /dev/null "$ENV_DATEI"
env_setzen "$ENV_DATEI" SPOTIFY_CLIENT_ID "$SPOTIFY_CLIENT_ID" 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_CLIENT_SECRET "$SPOTIFY_CLIENT_SECRET"
env_setzen "$ENV_DATEI" SPOTIFY_REDIRECT_URI "http://127.0.0.1:8888/callback" env_setzen "$ENV_DATEI" SPOTIFY_REDIRECT_URI "http://127.0.0.1:8888/callback"
@@ -170,7 +170,7 @@ if [ $UPDATE = 0 ]; then
schritt "Spotify-Login für die Wiedergabe-Steuerung" 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 "Ö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." 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" \ "${ALS_DIENST[@]}" "$PLAYWISH_DIR/.venv/bin/python" "$PLAYWISH_DIR/app/auth.py" --profil player --env "$ENV_DATEI" \
|| warnung "Login fehlgeschlagen – später nachholen (siehe README)." || warnung "Login fehlgeschlagen – später nachholen (siehe README)."
else else
# alte Schlüsselnamen übernehmen # alte Schlüsselnamen übernehmen
@@ -179,18 +179,18 @@ else
fi fi
# ------------------------------------------------------------------ Dienst # ------------------------------------------------------------------ Dienst
schritt "Dienst audiofy-controller" schritt "Dienst playwish-controller"
vorlage audiofy-controller.service /etc/systemd/system/audiofy-controller.service vorlage playwish-controller.service /etc/systemd/system/playwish-controller.service
$SUDO systemctl daemon-reload $SUDO systemctl daemon-reload
$SUDO systemctl enable audiofy-controller >/dev/null 2>&1 $SUDO systemctl enable playwish-controller >/dev/null 2>&1
$SUDO systemctl restart audiofy-controller $SUDO systemctl restart playwish-controller
sleep 5 sleep 5
systemctl is-active --quiet audiofy-controller || abbruch "audiofy-controller läuft nicht – siehe: journalctl -u audiofy-controller" systemctl is-active --quiet playwish-controller || abbruch "playwish-controller läuft nicht – siehe: journalctl -u playwish-controller"
info "audiofy-controller läuft" info "playwish-controller läuft"
# ------------------------------------------------------------------ Zusammenfassung # ------------------------------------------------------------------ Zusammenfassung
schritt "Fertig" schritt "Fertig"
info "Logs: journalctl -u audiofy-controller -f · journalctl -u raspotify -f" info "Logs: journalctl -u playwish-controller -f · journalctl -u raspotify -f"
if [ $UPDATE = 0 ]; then if [ $UPDATE = 0 ]; then
info "" info ""
info "Wichtig: Wähle das Gerät „$GERAETENAME“ EINMAL in der Spotify-App aus (im selben Netz)." info "Wichtig: Wähle das Gerät „$GERAETENAME“ EINMAL in der Spotify-App aus (im selben Netz)."
+26 -26
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Audiofy – Installation der Rolle "Server": Website für Gäste, Admin-Bereich, Player-API. # Playwish – 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 Neuinstallation (fragt alles Nötige ab)
# install/install-server.sh --update nur Code, Python-Umgebung und Dienst aktualisieren # install/install-server.sh --update nur Code, Python-Umgebung und Dienst aktualisieren
@@ -18,14 +18,14 @@ for arg in "$@"; do
esac esac
done done
ENV_DATEI="$AUDIOFY_DIR/.env" ENV_DATEI="$PLAYWISH_DIR/.env"
export AUDIOFY_DIR AUDIOFY_USER export PLAYWISH_DIR PLAYWISH_USER
export APP_PORT="${APP_PORT:-8000}" export APP_PORT="${APP_PORT:-8000}"
schritt "Audiofy-Server: $([ $UPDATE = 1 ] && echo Update || echo Neuinstallation)" schritt "Playwish-Server: $([ $UPDATE = 1 ] && echo Update || echo Neuinstallation)"
pruefe_system pruefe_system
if [ $UPDATE = 1 ] && ! $SUDO test -f "$ENV_DATEI"; then if [ $UPDATE = 1 ] && ! $SUDO test -f "$ENV_DATEI"; then
abbruch "Keine bestehende Installation in $AUDIOFY_DIR gefunden – ohne --update ausführen." abbruch "Keine bestehende Installation in $PLAYWISH_DIR gefunden – ohne --update ausführen."
fi fi
# ------------------------------------------------------------------ Abfragen # ------------------------------------------------------------------ Abfragen
@@ -67,20 +67,20 @@ pakete "${PAKETE[@]}"
# ------------------------------------------------------------------ Benutzer, Ordner, Code # ------------------------------------------------------------------ Benutzer, Ordner, Code
schritt "Dienst-Benutzer, Ordner und Code" schritt "Dienst-Benutzer, Ordner und Code"
dienst_benutzer dienst_benutzer
$SUDO install -d -o root -g "$AUDIOFY_USER" -m 750 "$AUDIOFY_DIR" $SUDO install -d -o root -g "$PLAYWISH_USER" -m 750 "$PLAYWISH_DIR"
$SUDO install -d -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 700 "$AUDIOFY_DIR/data" $SUDO install -d -o "$PLAYWISH_USER" -g "$PLAYWISH_USER" -m 700 "$PLAYWISH_DIR/data"
code_kopieren "$REPO/server" "$AUDIOFY_DIR/app" code_kopieren "$REPO/server" "$PLAYWISH_DIR/app"
$SUDO install -o root -g "$AUDIOFY_USER" -m 640 "$REPO/player/auth.py" "$AUDIOFY_DIR/app/auth.py" $SUDO install -o root -g "$PLAYWISH_USER" -m 640 "$REPO/player/auth.py" "$PLAYWISH_DIR/app/auth.py"
venv_einrichten "$AUDIOFY_DIR/app/requirements.txt" venv_einrichten "$PLAYWISH_DIR/app/requirements.txt"
info "Code in $AUDIOFY_DIR/app, Python-Umgebung in $AUDIOFY_DIR/.venv" info "Code in $PLAYWISH_DIR/app, Python-Umgebung in $PLAYWISH_DIR/.venv"
# ------------------------------------------------------------------ Konfiguration # ------------------------------------------------------------------ Konfiguration
schritt "Konfiguration ($ENV_DATEI)" schritt "Konfiguration ($ENV_DATEI)"
$SUDO test -f "$ENV_DATEI" || $SUDO install -o "$AUDIOFY_USER" -g "$AUDIOFY_USER" -m 600 /dev/null "$ENV_DATEI" $SUDO test -f "$ENV_DATEI" || $SUDO install -o "$PLAYWISH_USER" -g "$PLAYWISH_USER" -m 600 /dev/null "$ENV_DATEI"
VERWALTUNG=("${ALS_DIENST[@]}" "$AUDIOFY_DIR/.venv/bin/python" "$AUDIOFY_DIR/app/verwaltung.py") VERWALTUNG=("${ALS_DIENST[@]}" "$PLAYWISH_DIR/.venv/bin/python" "$PLAYWISH_DIR/app/verwaltung.py")
"${VERWALTUNG[@]}" geheimnisse --env "$ENV_DATEI" | sed 's/^/ /' "${VERWALTUNG[@]}" geheimnisse --env "$ENV_DATEI" | sed 's/^/ /'
if [ $UPDATE = 0 ]; then if [ $UPDATE = 0 ]; then
env_setzen "$ENV_DATEI" DB_PATH "$AUDIOFY_DIR/data/audiofy.db" env_setzen "$ENV_DATEI" DB_PATH "$PLAYWISH_DIR/data/playwish.db"
env_setzen "$ENV_DATEI" PUBLIC_URL "https://$DOMAIN" env_setzen "$ENV_DATEI" PUBLIC_URL "https://$DOMAIN"
env_setzen "$ENV_DATEI" SPOTIFY_CLIENT_ID "$SPOTIFY_CLIENT_ID" 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_CLIENT_SECRET "$SPOTIFY_CLIENT_SECRET"
@@ -91,7 +91,7 @@ if [ $UPDATE = 0 ]; then
info "Öffne gleich die angezeigte Adresse im Browser, melde dich an und füge danach die" 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." info "Adresse aus der Adresszeile (http://127.0.0.1:8888/callback?...) hier ein."
if ja_nein "Jetzt anmelden?" j; then if ja_nein "Jetzt anmelden?" j; then
"${ALS_DIENST[@]}" "$AUDIOFY_DIR/.venv/bin/python" "$AUDIOFY_DIR/app/auth.py" --profil server --env "$ENV_DATEI" \ "${ALS_DIENST[@]}" "$PLAYWISH_DIR/.venv/bin/python" "$PLAYWISH_DIR/app/auth.py" --profil server --env "$ENV_DATEI" \
|| warnung "Login fehlgeschlagen – später nachholen (siehe README)." || warnung "Login fehlgeschlagen – später nachholen (siehe README)."
fi fi
@@ -103,19 +103,19 @@ if [ $UPDATE = 0 ]; then
fi fi
# ------------------------------------------------------------------ Dienst # ------------------------------------------------------------------ Dienst
schritt "Dienst audiofy-web" schritt "Dienst playwish-web"
vorlage audiofy-web.service /etc/systemd/system/audiofy-web.service vorlage playwish-web.service /etc/systemd/system/playwish-web.service
$SUDO systemctl daemon-reload $SUDO systemctl daemon-reload
$SUDO systemctl enable audiofy-web >/dev/null 2>&1 $SUDO systemctl enable playwish-web >/dev/null 2>&1
$SUDO systemctl restart audiofy-web $SUDO systemctl restart playwish-web
sleep 3 sleep 3
systemctl is-active --quiet audiofy-web || abbruch "audiofy-web läuft nicht – siehe: journalctl -u audiofy-web" systemctl is-active --quiet playwish-web || abbruch "playwish-web läuft nicht – siehe: journalctl -u playwish-web"
curl -fsS "http://127.0.0.1:$APP_PORT/healthz" >/dev/null && info "audiofy-web läuft" curl -fsS "http://127.0.0.1:$APP_PORT/healthz" >/dev/null && info "playwish-web läuft"
# ------------------------------------------------------------------ Caddy & Schutz (nur Neuinstallation) # ------------------------------------------------------------------ Caddy & Schutz (nur Neuinstallation)
if [ $UPDATE = 0 ]; then if [ $UPDATE = 0 ]; then
schritt "Caddy (HTTPS)" 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 if $SUDO grep -q "Playwish" /etc/caddy/Caddyfile 2>/dev/null || $SUDO grep -q "/usr/share/caddy" /etc/caddy/Caddyfile 2>/dev/null; then
vorlage Caddyfile /etc/caddy/Caddyfile vorlage Caddyfile /etc/caddy/Caddyfile
$SUDO caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1 || abbruch "Caddyfile ungültig" $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 enable caddy >/dev/null 2>&1
@@ -138,7 +138,7 @@ if [ $UPDATE = 0 ]; then
fi fi
if [ $FAIL2BAN = 1 ]; then if [ $FAIL2BAN = 1 ]; then
schritt "fail2ban" schritt "fail2ban"
vorlage fail2ban-audiofy.conf /etc/fail2ban/jail.d/audiofy.conf vorlage fail2ban-playwish.conf /etc/fail2ban/jail.d/playwish.conf
$SUDO systemctl enable fail2ban >/dev/null 2>&1 $SUDO systemctl enable fail2ban >/dev/null 2>&1
$SUDO systemctl restart fail2ban $SUDO systemctl restart fail2ban
info "SSH-Schutz auf Port $SSH_PORT aktiv" info "SSH-Schutz auf Port $SSH_PORT aktiv"
@@ -150,7 +150,7 @@ if [ $UPDATE = 0 ]; then
fi fi
schritt "Logs begrenzen" schritt "Logs begrenzen"
$SUDO install -d /etc/systemd/journald.conf.d $SUDO install -d /etc/systemd/journald.conf.d
vorlage journald-10-audiofy.conf /etc/systemd/journald.conf.d/10-audiofy.conf vorlage journald-10-playwish.conf /etc/systemd/journald.conf.d/10-playwish.conf
$SUDO systemctl restart systemd-journald $SUDO systemctl restart systemd-journald
info "journald max. 200 MB" info "journald max. 200 MB"
fi fi
@@ -160,11 +160,11 @@ schritt "Fertig"
URL="$(env_wert "$ENV_DATEI" PUBLIC_URL)" URL="$(env_wert "$ENV_DATEI" PUBLIC_URL)"
info "Website: $URL (Gäste nur mit Einladungslink/QR-Code)" info "Website: $URL (Gäste nur mit Einladungslink/QR-Code)"
info "Admin: $URL/admin" info "Admin: $URL/admin"
info "Logs: journalctl -u audiofy-web -f" info "Logs: journalctl -u playwish-web -f"
if [ $UPDATE = 0 ]; then if [ $UPDATE = 0 ]; then
info "" info ""
info "Für die Player-Installation brauchst du:" info "Für die Player-Installation brauchst du:"
info " Server-Adresse: $URL" info " Server-Adresse: $URL"
info " Player-Token: sudo -u $AUDIOFY_USER grep PLAYER_API_TOKEN $ENV_DATEI" info " Player-Token: sudo -u $PLAYWISH_USER grep PLAYER_API_TOKEN $ENV_DATEI"
info "Gastzugang ist anfangs aus – im Admin unter „Gäste“ einschalten." info "Gastzugang ist anfangs aus – im Admin unter „Gäste“ einschalten."
fi fi
+12 -12
View File
@@ -1,17 +1,17 @@
# Audiofy – gemeinsame Hilfsfunktionen für die Install-Skripte (wird per "source" geladen) # Playwish – gemeinsame Hilfsfunktionen für die Install-Skripte (wird per "source" geladen)
# shellcheck shell=bash # shellcheck shell=bash
AUDIOFY_DIR="${AUDIOFY_DIR:-/opt/audiofy}" PLAYWISH_DIR="${PLAYWISH_DIR:-/opt/playwish}"
AUDIOFY_USER="${AUDIOFY_USER:-audiofy}" PLAYWISH_USER="${PLAYWISH_USER:-playwish}"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VORLAGEN="$REPO/install/vorlagen" VORLAGEN="$REPO/install/vorlagen"
if [ "$(id -u)" -eq 0 ]; then if [ "$(id -u)" -eq 0 ]; then
SUDO="" SUDO=""
ALS_DIENST=(runuser -u "$AUDIOFY_USER" --) ALS_DIENST=(runuser -u "$PLAYWISH_USER" --)
else else
SUDO="sudo" SUDO="sudo"
ALS_DIENST=(sudo -u "$AUDIOFY_USER") ALS_DIENST=(sudo -u "$PLAYWISH_USER")
fi fi
# --- Ausgabe ------------------------------------------------------------------ # --- Ausgabe ------------------------------------------------------------------
@@ -67,31 +67,31 @@ pakete() {
} }
dienst_benutzer() { dienst_benutzer() {
if ! id "$AUDIOFY_USER" >/dev/null 2>&1; then if ! id "$PLAYWISH_USER" >/dev/null 2>&1; then
$SUDO useradd --system --home-dir "$AUDIOFY_DIR" --no-create-home --shell /usr/sbin/nologin "$AUDIOFY_USER" $SUDO useradd --system --home-dir "$PLAYWISH_DIR" --no-create-home --shell /usr/sbin/nologin "$PLAYWISH_USER"
info "Dienst-Benutzer '$AUDIOFY_USER' angelegt (ohne Login, ohne sudo)" info "Dienst-Benutzer '$PLAYWISH_USER' angelegt (ohne Login, ohne sudo)"
fi fi
} }
# code_kopieren QUELLE ZIEL – ersetzt den Inhalt von ZIEL (gehört root, Dienst darf nur lesen) # code_kopieren QUELLE ZIEL – ersetzt den Inhalt von ZIEL (gehört root, Dienst darf nur lesen)
code_kopieren() { code_kopieren() {
local quelle="$1" ziel="$2" local quelle="$1" ziel="$2"
$SUDO install -d -o root -g "$AUDIOFY_USER" -m 750 "$ziel" $SUDO install -d -o root -g "$PLAYWISH_USER" -m 750 "$ziel"
$SUDO find "$ziel" -mindepth 1 -delete $SUDO find "$ziel" -mindepth 1 -delete
tar -C "$quelle" --exclude='__pycache__' --exclude='.venv' --exclude='.pytest_cache' --exclude='tests' -cf - . \ tar -C "$quelle" --exclude='__pycache__' --exclude='.venv' --exclude='.pytest_cache' --exclude='tests' -cf - . \
| $SUDO tar -C "$ziel" --no-same-owner -xf - | $SUDO tar -C "$ziel" --no-same-owner -xf -
$SUDO chown -R root:"$AUDIOFY_USER" "$ziel" $SUDO chown -R root:"$PLAYWISH_USER" "$ziel"
$SUDO find "$ziel" -type d -exec chmod 750 {} + $SUDO find "$ziel" -type d -exec chmod 750 {} +
$SUDO find "$ziel" -type f -exec chmod 640 {} + $SUDO find "$ziel" -type f -exec chmod 640 {} +
} }
# venv_einrichten REQUIREMENTS # venv_einrichten REQUIREMENTS
venv_einrichten() { venv_einrichten() {
local venv="$AUDIOFY_DIR/.venv" local venv="$PLAYWISH_DIR/.venv"
[ -x "$venv/bin/python" ] || $SUDO python3 -m venv "$venv" [ -x "$venv/bin/python" ] || $SUDO python3 -m venv "$venv"
$SUDO "$venv/bin/pip" install -q --upgrade pip $SUDO "$venv/bin/pip" install -q --upgrade pip
$SUDO "$venv/bin/pip" install -q -r "$1" $SUDO "$venv/bin/pip" install -q -r "$1"
$SUDO chown -R root:"$AUDIOFY_USER" "$venv" $SUDO chown -R root:"$PLAYWISH_USER" "$venv"
$SUDO chmod -R g+rX,o-rwx "$venv" $SUDO chmod -R g+rX,o-rwx "$venv"
} }
+1 -1
View File
@@ -1,4 +1,4 @@
# Audiofy – Caddy-Konfiguration (erzeugt von install-server.sh) # Playwish – Caddy-Konfiguration (erzeugt von install-server.sh)
{ {
email {{ACME_EMAIL}} email {{ACME_EMAIL}}
servers { servers {
-30
View File
@@ -1,30 +0,0 @@
[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
@@ -1,4 +1,4 @@
# Audiofy – SSH gegen Passwort-Raten schützen # Playwish – SSH gegen Passwort-Raten schützen
[sshd] [sshd]
enabled = true enabled = true
port = {{SSH_PORT}} port = {{SSH_PORT}}
@@ -1,15 +1,15 @@
[Unit] [Unit]
Description=Audiofy – Controller (Spotify-Steuerung, Wünsche vom Server) Description=Playwish – Controller (Spotify-Steuerung, Wünsche vom Server)
After=network-online.target bluetooth.target raspotify.service After=network-online.target bluetooth.target raspotify.service
Wants=network-online.target raspotify.service Wants=network-online.target raspotify.service
# nie aufgeben, immer wieder neu starten # nie aufgeben, immer wieder neu starten
StartLimitIntervalSec=0 StartLimitIntervalSec=0
[Service] [Service]
User={{AUDIOFY_USER}} User={{PLAYWISH_USER}}
Group={{AUDIOFY_USER}} Group={{PLAYWISH_USER}}
WorkingDirectory={{AUDIOFY_DIR}}/app WorkingDirectory={{PLAYWISH_DIR}}/app
ExecStart={{AUDIOFY_DIR}}/.venv/bin/python -u {{AUDIOFY_DIR}}/app/controller.py --env {{AUDIOFY_DIR}}/config/.env ExecStart={{PLAYWISH_DIR}}/.venv/bin/python -u {{PLAYWISH_DIR}}/app/controller.py --env {{PLAYWISH_DIR}}/config/.env
Restart=always Restart=always
RestartSec=5 RestartSec=5
UMask=0077 UMask=0077
@@ -17,7 +17,7 @@ UMask=0077
NoNewPrivileges=yes NoNewPrivileges=yes
ProtectSystem=strict ProtectSystem=strict
ProtectHome=yes ProtectHome=yes
ReadWritePaths={{AUDIOFY_DIR}}/config ReadWritePaths={{PLAYWISH_DIR}}/config
PrivateTmp=yes PrivateTmp=yes
PrivateDevices=yes PrivateDevices=yes
ProtectKernelTunables=yes ProtectKernelTunables=yes
+30
View File
@@ -0,0 +1,30 @@
[Unit]
Description=Playwish – Website, Admin-Bereich und Player-API
After=network-online.target
Wants=network-online.target
[Service]
User={{PLAYWISH_USER}}
Group={{PLAYWISH_USER}}
WorkingDirectory={{PLAYWISH_DIR}}/app
Environment=PLAYWISH_ENV={{PLAYWISH_DIR}}/.env
ExecStart={{PLAYWISH_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={{PLAYWISH_DIR}}/data {{PLAYWISH_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
@@ -1,4 +1,4 @@
# Audiofy: raspotify nie endgültig aufgeben (z. B. wenn der Lautsprecher kurz fehlt # Playwish: raspotify nie endgültig aufgeben (z. B. wenn der Lautsprecher kurz fehlt
# oder das Netz beim Start noch nicht da ist) # oder das Netz beim Start noch nicht da ist)
[Unit] [Unit]
StartLimitIntervalSec=0 StartLimitIntervalSec=0
@@ -1,4 +1,4 @@
# Audiofy – SSH-Härtung (NICHT automatisch installiert, siehe README „Server härten“) # Playwish – 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! # Erst einsetzen, wenn der Login per Schlüssel für {{ADMIN_USER}} nachweislich klappt!
Port {{SSH_PORT}} Port {{SSH_PORT}}
PermitRootLogin no PermitRootLogin no
+5 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Audiofy – einmaliger Spotify-Login (Authorization Code Flow). """Playwish – einmaliger Spotify-Login (Authorization Code Flow).
Holt einen Refresh-Token für das eigene Spotify-Konto und schreibt ihn in die Holt einen Refresh-Token für das eigene Spotify-Konto und schreibt ihn in die
.env-Datei. Funktioniert ohne Browser auf dem Gerät selbst: .env-Datei. Funktioniert ohne Browser auf dem Gerät selbst:
@@ -11,8 +11,8 @@ Holt einen Refresh-Token für das eigene Spotify-Konto und schreibt ihn in die
und hier einfügen. und hier einfügen.
Aufruf: Aufruf:
python3 auth.py --env /opt/audiofy/config/.env # Player (Wiedergabe steuern) python3 auth.py --env /opt/playwish/config/.env # Player (Wiedergabe steuern)
python3 auth.py --profil server --env /opt/audiofy/.env # Server (nur Suche) python3 auth.py --profil server --env /opt/playwish/.env # Server (nur Suche)
Benötigte Einträge in der .env: SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, Benötigte Einträge in der .env: SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET,
optional SPOTIFY_REDIRECT_URI. Nur Python-Standardbibliothek, kein venv nötig. optional SPOTIFY_REDIRECT_URI. Nur Python-Standardbibliothek, kein venv nötig.
@@ -75,7 +75,7 @@ def env_setzen(pfad: Path, key: str, wert: str) -> None:
inhalt = "\n".join(neu) + "\n" inhalt = "\n".join(neu) + "\n"
if not os.access(pfad.parent, os.W_OK): if not os.access(pfad.parent, os.W_OK):
# z. B. Server: /opt/audiofy gehört root, nur die .env selbst dem Dienst-Benutzer # z. B. Server: /opt/playwish gehört root, nur die .env selbst dem Dienst-Benutzer
fd = os.open(pfad, os.O_WRONLY | os.O_TRUNC) fd = os.open(pfad, os.O_WRONLY | os.O_TRUNC)
with os.fdopen(fd, "w") as f: with os.fdopen(fd, "w") as f:
f.write(inhalt) f.write(inhalt)
@@ -112,7 +112,7 @@ def me_abfragen(access_token: str) -> dict:
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Audiofy – Spotify-Login, schreibt den Refresh-Token in die .env") parser = argparse.ArgumentParser(description="Playwish – Spotify-Login, schreibt den Refresh-Token in die .env")
parser.add_argument("--profil", choices=SCOPES, default="player", help="Scope-Satz (Standard: player)") parser.add_argument("--profil", choices=SCOPES, default="player", help="Scope-Satz (Standard: player)")
parser.add_argument("--env", type=Path, default=Path(__file__).resolve().parent / ".env", parser.add_argument("--env", type=Path, default=Path(__file__).resolve().parent / ".env",
help="Pfad zur .env (Standard: neben dem Skript)") help="Pfad zur .env (Standard: neben dem Skript)")
+11 -11
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Audiofy – Controller (Player). """Playwish – Controller (Player).
Spielt zufällige Titel aus der Grund-Playlist auf dem Spotify-Connect-Gerät Spielt zufällige Titel aus der Grund-Playlist auf dem Spotify-Connect-Gerät
"Audiofy" und schiebt nach jeweils N-1 Playlist-Titeln einen Wunsch ein. "Playwish" und schiebt nach jeweils N-1 Playlist-Titeln einen Wunsch ein.
Spotify bekommt immer nur den jeweils nächsten Titel (POST /me/player/queue, Spotify bekommt immer nur den jeweils nächsten Titel (POST /me/player/queue,
ca. 15 s vor Titelende); die Reihenfolge verwaltet ausschließlich der Controller. ca. 15 s vor Titelende); die Reihenfolge verwaltet ausschließlich der Controller.
@@ -14,7 +14,7 @@ Ist der Server nicht erreichbar, läuft einfach die Grund-Playlist weiter.
Bedienung über die Spotify-App bleibt möglich: Fremde Titel werden erkannt und Bedienung über die Spotify-App bleibt möglich: Fremde Titel werden erkannt und
geduldet, eine Pause durch den Besitzer (App oder Admin-Bereich) wird respektiert. geduldet, eine Pause durch den Besitzer (App oder Admin-Bereich) wird respektiert.
Aufruf: .venv/bin/python controller.py --env /opt/audiofy/config/.env Aufruf: .venv/bin/python controller.py --env /opt/playwish/config/.env
""" """
import argparse import argparse
@@ -38,7 +38,7 @@ from auth import env_lesen, env_setzen
from spotify_client import SpotifyClient, SpotifyFehler from spotify_client import SpotifyClient, SpotifyFehler
from server_client import ServerVerbindung, Wunsch from server_client import ServerVerbindung, Wunsch
log = logging.getLogger("audiofy.controller") log = logging.getLogger("playwish.controller")
ORDNER = Path(__file__).resolve().parent ORDNER = Path(__file__).resolve().parent
POLL_S = 3 # Abfrageintervall /me/player und Server POLL_S = 3 # Abfrageintervall /me/player und Server
@@ -289,7 +289,7 @@ class Controller:
return None return None
def starten(self) -> None: def starten(self) -> None:
"""Wiedergabe auf dem Gerät "Audiofy" (neu) starten.""" """Wiedergabe auf dem Gerät "Playwish" (neu) starten."""
gid = self.geraet_id() gid = self.geraet_id()
if not gid: if not gid:
# Warnung beim ersten Mal, danach höchstens einmal pro Minute # Warnung beim ersten Mal, danach höchstens einmal pro Minute
@@ -547,7 +547,7 @@ class Controller:
log.info("Als Nächstes eingereiht: %s [%s]", eintrag.uri, eintrag.quelle) log.info("Als Nächstes eingereiht: %s [%s]", eintrag.uri, eintrag.quelle)
return return
# --- es läuft nichts (auf Audiofy) --- # --- es läuft nichts (auf Playwish) ---
if self.bt_pause: if self.bt_pause:
# librespot pausiert bei Audiofehlern selbst – nach Neuverbindung fortsetzen # librespot pausiert bei Audiofehlern selbst – nach Neuverbindung fortsetzen
if self.lautsprecher_bereit(): if self.lautsprecher_bereit():
@@ -558,13 +558,13 @@ class Controller:
log.info("Lautsprecher wieder da – Wiedergabe fortgesetzt") log.info("Lautsprecher wieder da – Wiedergabe fortgesetzt")
return return
if vorher is None: if vorher is None:
# Controller-Start: pausierter Titel auf Audiofy = Besitzer-Pause, sonst starten # Controller-Start: pausierter Titel auf Playwish = Besitzer-Pause, sonst starten
if st and unser and item and fortschritt >= PAUSE_MIN_MS: if st and unser and item and fortschritt >= PAUSE_MIN_MS:
if not self._bt_pruefen_jetzt(): if not self._bt_pruefen_jetzt():
log.warning("Audiofy pausiert und Lautsprecher getrennt – geht nach Neuverbindung weiter") log.warning("Playwish pausiert und Lautsprecher getrennt – geht nach Neuverbindung weiter")
self.bt_pause = True self.bt_pause = True
else: else:
log.info("Audiofy ist pausiert – warte, bis der Besitzer fortsetzt") log.info("Playwish ist pausiert – warte, bis der Besitzer fortsetzt")
self.besitzer_pause = True self.besitzer_pause = True
else: else:
self.neustart = True self.neustart = True
@@ -708,7 +708,7 @@ class Controller:
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Audiofy-Controller") parser = argparse.ArgumentParser(description="Playwish-Controller")
parser.add_argument("--env", type=Path, default=ORDNER / ".env") parser.add_argument("--env", type=Path, default=ORDNER / ".env")
args = parser.parse_args() args = parser.parse_args()
@@ -741,7 +741,7 @@ def main() -> None:
Playlist(sp, env.get("SPOTIFY_PLAYLIST_ID", ""), int(env.get("PLAYLIST_REPEAT_PERCENT") or 50)), Playlist(sp, env.get("SPOTIFY_PLAYLIST_ID", ""), int(env.get("PLAYLIST_REPEAT_PERCENT") or 50)),
server, server,
n=int(env.get("WISH_EVERY_N") or 3), n=int(env.get("WISH_EVERY_N") or 3),
geraetename=env.get("DEVICE_NAME") or "Audiofy", geraetename=env.get("DEVICE_NAME") or "Playwish",
bt_mac=env.get("BT_SPEAKER_MAC") or None, bt_mac=env.get("BT_SPEAKER_MAC") or None,
zustand_pfad=args.env.parent / "zustand.json", zustand_pfad=args.env.parent / "zustand.json",
env_pfad=args.env, env_pfad=args.env,
+2 -2
View File
@@ -1,4 +1,4 @@
"""Audiofy – Bluetooth-Lautsprecher suchen, koppeln und verbinden (bluetoothctl, ohne sudo). """Playwish – Bluetooth-Lautsprecher suchen, koppeln und verbinden (bluetoothctl, ohne sudo).
Gekoppelt wird immer MIT Agent in derselben bluetoothctl-Sitzung – sonst speichert Gekoppelt wird immer MIT Agent in derselben bluetoothctl-Sitzung – sonst speichert
BlueZ den Schlüssel nicht dauerhaft ("Bonded: no"), und der Lautsprecher lässt sich BlueZ den Schlüssel nicht dauerhaft ("Bonded: no"), und der Lautsprecher lässt sich
@@ -11,7 +11,7 @@ import shutil
import subprocess import subprocess
import time import time
log = logging.getLogger("audiofy.lautsprecher") log = logging.getLogger("playwish.lautsprecher")
MAC_RE = re.compile(r"^([0-9A-F]{2}:){5}[0-9A-F]{2}$") MAC_RE = re.compile(r"^([0-9A-F]{2}:){5}[0-9A-F]{2}$")
SUCHDAUER_S = 15 SUCHDAUER_S = 15
+2 -2
View File
@@ -1,4 +1,4 @@
"""Audiofy – Verbindung Player → Server (Player-API mit Bearer-Token, nur ausgehend). """Playwish – Verbindung Player → Server (Player-API mit Bearer-Token, nur ausgehend).
Alle Aufrufe fangen Fehler selbst ab: Ist der Server nicht erreichbar, liefern sie Alle Aufrufe fangen Fehler selbst ab: Ist der Server nicht erreichbar, liefern sie
None/False, und der Controller spielt einfach die Grund-Playlist weiter. Im None/False, und der Controller spielt einfach die Grund-Playlist weiter. Im
@@ -11,7 +11,7 @@ from dataclasses import dataclass
import httpx import httpx
log = logging.getLogger("audiofy.server") log = logging.getLogger("playwish.server")
OFFLINE_RETRY_S = 15 OFFLINE_RETRY_S = 15
TIMEOUT_S = 5 TIMEOUT_S = 5
+2 -2
View File
@@ -1,4 +1,4 @@
"""Audiofy – schlanker Spotify-Web-API-Client für den Controller (httpx). """Playwish – schlanker Spotify-Web-API-Client für den Controller (httpx).
Erneuert den Access-Token selbst per Refresh-Token. Fehler werden als Erneuert den Access-Token selbst per Refresh-Token. Fehler werden als
SpotifyFehler geworfen (bei 429 mit retry_after); Netzwerkfehler kommen als SpotifyFehler geworfen (bei 429 mit retry_after); Netzwerkfehler kommen als
@@ -12,7 +12,7 @@ from collections.abc import Callable
import httpx import httpx
log = logging.getLogger("audiofy.spotify") log = logging.getLogger("playwish.spotify")
API = "https://api.spotify.com/v1" API = "https://api.spotify.com/v1"
TOKEN_URL = "https://accounts.spotify.com/api/token" TOKEN_URL = "https://accounts.spotify.com/api/token"
+1 -1
View File
@@ -93,7 +93,7 @@ def test_ein_titel_bleibt_immer_frei():
def test_zustand_speichert_zuletzt(tmp_path): def test_zustand_speichert_zuletzt(tmp_path):
datei = tmp_path / "zustand.json" datei = tmp_path / "zustand.json"
neu = lambda: Controller(sp=None, playlist=Playlist(FakeSpotify(TITEL), "liste", 50), server=None, # noqa: E731 neu = lambda: Controller(sp=None, playlist=Playlist(FakeSpotify(TITEL), "liste", 50), server=None, # noqa: E731
n=3, geraetename="Audiofy", bt_mac=None, zustand_pfad=datei) n=3, geraetename="Playwish", bt_mac=None, zustand_pfad=datei)
c = neu() c = neu()
c.playlist.gespielt(TITEL[3]) c.playlist.gespielt(TITEL[3])
c.playlist.gespielt(TITEL[4]) c.playlist.gespielt(TITEL[4])
+1 -1
View File
@@ -42,7 +42,7 @@ class FakeServer:
def controller(sp, server): def controller(sp, server):
c = Controller(sp=sp, playlist=Playlist(sp, "liste"), server=server, n=1, geraetename="Audiofy", bt_mac=None) c = Controller(sp=sp, playlist=Playlist(sp, "liste"), server=server, n=1, geraetename="Playwish", bt_mac=None)
return c return c
+1 -1
View File
@@ -1 +1 @@
"""Audiofy-Web – Website, Admin-Bereich und Player-API (läuft auf dem Server).""" """Playwish-Web – Website, Admin-Bereich und Player-API (läuft auf dem Server)."""
+3 -3
View File
@@ -1,4 +1,4 @@
"""Audiofy-Web – Admin-Bereich (/admin): der Besitzer steuert alles, auch vom Handy. """Playwish-Web – Admin-Bereich (/admin): der Besitzer steuert alles, auch vom Handy.
Zugang: Zugang:
- Passwort als argon2-Hash in der .env (ADMIN_PASSWORD_HASH, gesetzt per verwaltung.py) - Passwort als argon2-Hash in der .env (ADMIN_PASSWORD_HASH, gesetzt per verwaltung.py)
@@ -36,7 +36,7 @@ from .spotify import Spotify, SpotifyFehler, spotify
router = APIRouter() router = APIRouter()
ADMIN_COOKIE = "audiofy_admin" ADMIN_COOKIE = "playwish_admin"
SITZUNG_TAGE = 14 SITZUNG_TAGE = 14
login_drossel = Drossel(5, 15 * 60) login_drossel = Drossel(5, 15 * 60)
_hasher = PasswordHasher() _hasher = PasswordHasher()
@@ -390,7 +390,7 @@ def zugang_qr(request: Request, format: Literal["svg", "png"] = "svg",
if format == "png": if format == "png":
qr.save(puffer, kind="png", scale=16, border=3) qr.save(puffer, kind="png", scale=16, border=3)
return Response(puffer.getvalue(), media_type="image/png", headers={ return Response(puffer.getvalue(), media_type="image/png", headers={
"Content-Disposition": 'attachment; filename="audiofy-einladung.png"', "Cache-Control": "no-store"}) "Content-Disposition": 'attachment; filename="playwish-einladung.png"', "Cache-Control": "no-store"})
qr.save(puffer, kind="svg", scale=8, border=2, xmldecl=False, svgns=True) qr.save(puffer, kind="svg", scale=8, border=2, xmldecl=False, svgns=True)
return Response(puffer.getvalue(), media_type="image/svg+xml", headers={"Cache-Control": "no-store"}) return Response(puffer.getvalue(), media_type="image/svg+xml", headers={"Cache-Control": "no-store"})
+4 -4
View File
@@ -1,6 +1,6 @@
"""Audiofy-Web – Konfiguration aus der .env. """Playwish-Web – Konfiguration aus der .env.
Pfad über die Umgebungsvariable AUDIOFY_ENV, Standard /opt/audiofy/.env. Pfad über die Umgebungsvariable PLAYWISH_ENV, Standard /opt/playwish/.env.
Geheimnisse stehen nur dort (Rechte 600), nie im Code oder in Logs. Geheimnisse stehen nur dort (Rechte 600), nie im Code oder in Logs.
""" """
@@ -51,11 +51,11 @@ def env_wert_setzen(pfad: Path, key: str, wert: str) -> None:
@lru_cache @lru_cache
def konfig() -> Konfig: def konfig() -> Konfig:
pfad = Path(os.environ.get("AUDIOFY_ENV", "/opt/audiofy/.env")) pfad = Path(os.environ.get("PLAYWISH_ENV", "/opt/playwish/.env"))
werte = env_lesen(pfad) if pfad.is_file() else {} werte = env_lesen(pfad) if pfad.is_file() else {}
return Konfig( return Konfig(
env_pfad=pfad, env_pfad=pfad,
db_pfad=Path(werte.get("DB_PATH") or "/opt/audiofy/data/audiofy.db"), db_pfad=Path(werte.get("DB_PATH") or "/opt/playwish/data/playwish.db"),
player_api_token=werte.get("PLAYER_API_TOKEN") or werte.get("PI_API_TOKEN", ""), # alter Name player_api_token=werte.get("PLAYER_API_TOKEN") or werte.get("PI_API_TOKEN", ""), # alter Name
spotify_client_id=werte.get("SPOTIFY_CLIENT_ID", ""), spotify_client_id=werte.get("SPOTIFY_CLIENT_ID", ""),
spotify_client_secret=werte.get("SPOTIFY_CLIENT_SECRET", ""), spotify_client_secret=werte.get("SPOTIFY_CLIENT_SECRET", ""),
+1 -1
View File
@@ -1,4 +1,4 @@
"""Audiofy-Web – SQLite-Datenbank (audiofy.db): Schema, Einstellungen, Hilfsfunktionen. """Playwish-Web – SQLite-Datenbank (playwish.db): Schema, Einstellungen, Hilfsfunktionen.
Zeitstempel sind UTC im ISO-Format (lexikografisch vergleichbar). Zeitstempel sind UTC im ISO-Format (lexikografisch vergleichbar).
""" """
+4 -4
View File
@@ -1,4 +1,4 @@
"""Audiofy-Web – Gastseite: Suche, Wünschen, "Läuft gerade" und Warteschlange. """Playwish-Web – Gastseite: Suche, Wünschen, "Läuft gerade" und Warteschlange.
Zugang nur mit Einladung: Der Link /z/<token> (auch als QR-Code im Admin-Bereich) Zugang nur mit Einladung: Der Link /z/<token> (auch als QR-Code im Admin-Bereich)
setzt ein Zugangs-Cookie. Ohne gültigen Zugang gibt es nur eine Hinweisseite, die setzt ein Zugangs-Cookie. Ohne gültigen Zugang gibt es nur eine Hinweisseite, die
@@ -42,8 +42,8 @@ from .spotify import Spotify, SpotifyFehler, spotify
router = APIRouter() router = APIRouter()
STATIC = Path(__file__).parent / "static" STATIC = Path(__file__).parent / "static"
GAST_COOKIE = "audiofy_gast" GAST_COOKIE = "playwish_gast"
ZUGANG_COOKIE = "audiofy_zugang" ZUGANG_COOKIE = "playwish_zugang"
ZUGANG_TAGE = 30 ZUGANG_TAGE = 30
GAST_RE = re.compile(r"^[A-Za-z0-9_-]{22}$") GAST_RE = re.compile(r"^[A-Za-z0-9_-]{22}$")
PLAYER_OFFLINE_S = 30 # ohne Lebenszeichen gilt der Player als offline PLAYER_OFFLINE_S = 30 # ohne Lebenszeichen gilt der Player als offline
@@ -92,7 +92,7 @@ def gast_cookie_setzen(response: Response, gid: str) -> None:
def _geheimnis() -> bytes: def _geheimnis() -> bytes:
return (konfig().session_secret or "audiofy").encode() return (konfig().session_secret or "playwish").encode()
def zugangs_wert(token: str) -> str: def zugangs_wert(token: str) -> str:
+2 -2
View File
@@ -1,4 +1,4 @@
"""Audiofy-Web – FastAPI-Anwendung. """Playwish-Web – FastAPI-Anwendung.
Start (Server): uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 1 --no-access-log Start (Server): uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 1 --no-access-log
Caddy terminiert HTTPS und leitet an 127.0.0.1:8000 weiter. Caddy terminiert HTTPS und leitet an 127.0.0.1:8000 weiter.
@@ -26,7 +26,7 @@ async def lifespan(app: FastAPI):
# Keine automatische API-Doku öffentlich ausliefern # Keine automatische API-Doku öffentlich ausliefern
app = FastAPI(title="Audiofy – Musikwünsche", lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None) app = FastAPI(title="Playwish – Musikwünsche", lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None)
@app.middleware("http") @app.middleware("http")
+2 -2
View File
@@ -1,4 +1,4 @@
"""Audiofy-Web – Player-API (/api/player/…), geschützt per Bearer-Token (PLAYER_API_TOKEN). """Playwish-Web – Player-API (/api/player/…), geschützt per Bearer-Token (PLAYER_API_TOKEN).
Der Player baut nur ausgehende Verbindungen auf und fragt hier ca. alle 3 s nach: Der Player baut nur ausgehende Verbindungen auf und fragt hier ca. alle 3 s nach:
GET /api/player/config Einstellungen + offene Befehle (Ziel: Reaktion < 3 s) GET /api/player/config Einstellungen + offene Befehle (Ziel: Reaktion < 3 s)
@@ -53,7 +53,7 @@ class JetztLaeuft(BaseModel):
source: Literal["playlist", "wish", "owner", "fremd"] | None = None source: Literal["playlist", "wish", "owner", "fremd"] | None = None
wish_id: int | None = None wish_id: int | None = None
next: Titel | None = None # bereits eingereihter nächster Titel next: Titel | None = None # bereits eingereihter nächster Titel
device_ok: bool = True # Gerät "Audiofy" erreichbar device_ok: bool = True # Gerät "Playwish" erreichbar
speaker_ok: bool = True # Bluetooth-Lautsprecher verbunden speaker_ok: bool = True # Bluetooth-Lautsprecher verbunden
stopped: bool = False # Controller im Not-Aus stopped: bool = False # Controller im Not-Aus
volume_percent: int | None = Field(default=None, ge=0, le=100) volume_percent: int | None = Field(default=None, ge=0, le=100)
+2 -2
View File
@@ -1,4 +1,4 @@
"""Audiofy-Web – Spotify-Zugriff für Suche und Titelinfos. """Playwish-Web – Spotify-Zugriff für Suche und Titelinfos.
Eigener Refresh-Token des Servers (auth.py --profil server, keine Scopes nötig). Eigener Refresh-Token des Servers (auth.py --profil server, keine Scopes nötig).
Im Development Mode liefert die Suche höchstens 10 Treffer. Im Development Mode liefert die Suche höchstens 10 Treffer.
@@ -14,7 +14,7 @@ import httpx
from .config import Konfig, env_wert_setzen, konfig from .config import Konfig, env_wert_setzen, konfig
log = logging.getLogger("audiofy.spotify") log = logging.getLogger("playwish.spotify")
API = "https://api.spotify.com/v1" API = "https://api.spotify.com/v1"
TOKEN_URL = "https://accounts.spotify.com/api/token" TOKEN_URL = "https://accounts.spotify.com/api/token"
+1 -1
View File
@@ -1,4 +1,4 @@
/* Audiofy – Admin-Bereich (ergänzt app.css) */ /* Playwish – Admin-Bereich (ergänzt app.css) */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 4px 0 12px; } .kopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 4px 0 12px; }
.kopf h1 { font-size: 1.4rem; } .kopf h1 { font-size: 1.4rem; }
+2 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#121212"> <meta name="theme-color" content="#121212">
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<title>Audiofy – Admin</title> <title>Playwish – Admin</title>
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css">
<link rel="stylesheet" href="/static/admin.css"> <link rel="stylesheet" href="/static/admin.css">
<script src="/static/admin.js" defer></script> <script src="/static/admin.js" defer></script>
@@ -13,7 +13,7 @@
<body> <body>
<main> <main>
<div class="kopf"> <div class="kopf">
<h1>Audiofy Admin</h1> <h1>Playwish Admin</h1>
<button id="abmelden" class="klein" type="button">Abmelden</button> <button id="abmelden" class="klein" type="button">Abmelden</button>
</div> </div>
<div class="pillen" id="pillen"></div> <div class="pillen" id="pillen"></div>
+1 -1
View File
@@ -1,4 +1,4 @@
// Audiofy – Admin-Bereich. Daten nur per textContent einsetzen (kein innerHTML mit fremden Inhalten). // Playwish – Admin-Bereich. Daten nur per textContent einsetzen (kein innerHTML mit fremden Inhalten).
"use strict"; "use strict";
const ABFRAGE_MS = 3000; const ABFRAGE_MS = 3000;
+1 -1
View File
@@ -1,4 +1,4 @@
/* Audiofy – Gastseite (mobil zuerst) */ /* Playwish – Gastseite (mobil zuerst) */
:root { :root {
--bg: #121212; --bg: #121212;
--karte: #1e1e1e; --karte: #1e1e1e;
+4 -4
View File
@@ -1,4 +1,4 @@
// Audiofy – Gastseite. Daten nur per textContent einsetzen (kein innerHTML mit fremden Inhalten). // Playwish – Gastseite. Daten nur per textContent einsetzen (kein innerHTML mit fremden Inhalten).
"use strict"; "use strict";
const STATUS_ALLE_MS = 5000; const STATUS_ALLE_MS = 5000;
@@ -193,7 +193,7 @@ async function suchen(q) {
async function wuenschen(titel, knopf) { async function wuenschen(titel, knopf) {
knopf.disabled = true; knopf.disabled = true;
const name = $("name").value.trim(); const name = $("name").value.trim();
try { localStorage.setItem("audiofy-name", name); } catch (_) { /* privat */ } try { localStorage.setItem("playwish-name", name); } catch (_) { /* privat */ }
try { try {
if (!schutz.bereit()) knopf.textContent = "Prüfung …"; if (!schutz.bereit()) knopf.textContent = "Prüfung …";
const turnstile = await schutz.token(); const turnstile = await schutz.token();
@@ -214,7 +214,7 @@ async function wuenschen(titel, knopf) {
} }
function start() { function start() {
try { $("name").value = localStorage.getItem("audiofy-name") || ""; } catch (_) { /* privat */ } try { $("name").value = localStorage.getItem("playwish-name") || ""; } catch (_) { /* privat */ }
$("suchfeld").addEventListener("input", (ereignis) => { $("suchfeld").addEventListener("input", (ereignis) => {
clearTimeout(sucheTimer); clearTimeout(sucheTimer);
@@ -226,7 +226,7 @@ function start() {
if (ereignis.key === "Enter") { ereignis.target.blur(); } // Tastatur auf dem Handy schließen if (ereignis.key === "Enter") { ereignis.target.blur(); } // Tastatur auf dem Handy schließen
}); });
window.AudiofyTurnstile.einrichten($("turnstile"), "wish").then((s) => { schutz = s; }); window.PlaywishTurnstile.einrichten($("turnstile"), "wish").then((s) => { schutz = s; });
ladeStatus(); ladeStatus();
setInterval(() => { if (!document.hidden) ladeStatus(); }, STATUS_ALLE_MS); setInterval(() => { if (!document.hidden) ladeStatus(); }, STATUS_ALLE_MS);
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#121212"> <meta name="theme-color" content="#121212">
<title>Audiofy – Musikwünsche</title> <title>Playwish – Musikwünsche</title>
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css">
<link rel="preconnect" href="https://challenges.cloudflare.com"> <link rel="preconnect" href="https://challenges.cloudflare.com">
<script src="/static/turnstile.js" defer></script> <script src="/static/turnstile.js" defer></script>
@@ -13,7 +13,7 @@
<body> <body>
<main> <main>
<header> <header>
<h1>Audiofy</h1> <h1>Playwish</h1>
<p class="untertitel">Musikwünsche</p> <p class="untertitel">Musikwünsche</p>
</header> </header>
+2 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#121212"> <meta name="theme-color" content="#121212">
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<title>Audiofy – Admin</title> <title>Playwish – Admin</title>
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css">
<link rel="stylesheet" href="/static/admin.css"> <link rel="stylesheet" href="/static/admin.css">
<link rel="preconnect" href="https://challenges.cloudflare.com"> <link rel="preconnect" href="https://challenges.cloudflare.com">
@@ -15,7 +15,7 @@
<body> <body>
<main> <main>
<header> <header>
<h1>Audiofy</h1> <h1>Playwish</h1>
<p class="untertitel">Admin-Bereich</p> <p class="untertitel">Admin-Bereich</p>
</header> </header>
<form id="login" class="karte"> <form id="login" class="karte">
+2 -2
View File
@@ -1,8 +1,8 @@
// Audiofy – Admin-Login // Playwish – Admin-Login
"use strict"; "use strict";
let schutz = { token: async () => null, reset() {}, bereit: () => true }; // Turnstile (falls aktiv) let schutz = { token: async () => null, reset() {}, bereit: () => true }; // Turnstile (falls aktiv)
window.AudiofyTurnstile.einrichten(document.getElementById("turnstile"), "login").then((s) => { schutz = s; }); window.PlaywishTurnstile.einrichten(document.getElementById("turnstile"), "login").then((s) => { schutz = s; });
document.getElementById("login").addEventListener("submit", async (ereignis) => { document.getElementById("login").addEventListener("submit", async (ereignis) => {
ereignis.preventDefault(); ereignis.preventDefault();
+2 -2
View File
@@ -1,7 +1,7 @@
// Audiofy – Cloudflare Turnstile (Bot-Schutz). Lädt das Widget nur, wenn der Server einen Site-Key liefert. // Playwish – Cloudflare Turnstile (Bot-Schutz). Lädt das Widget nur, wenn der Server einen Site-Key liefert.
"use strict"; "use strict";
window.AudiofyTurnstile = (() => { window.PlaywishTurnstile = (() => {
let ladeVersprechen = null; let ladeVersprechen = null;
function skriptLaden() { function skriptLaden() {
+2 -2
View File
@@ -5,13 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#121212"> <meta name="theme-color" content="#121212">
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<title>Audiofy – Musikwünsche</title> <title>Playwish – Musikwünsche</title>
<link rel="stylesheet" href="/static/app.css"> <link rel="stylesheet" href="/static/app.css">
</head> </head>
<body> <body>
<main> <main>
<header> <header>
<h1>Audiofy</h1> <h1>Playwish</h1>
<p class="untertitel">Musikwünsche</p> <p class="untertitel">Musikwünsche</p>
</header> </header>
<section class="karte"> <section class="karte">
+2 -2
View File
@@ -1,4 +1,4 @@
"""Audiofy-Web – Cloudflare Turnstile: Bot-Schutz für Admin-Login und Wünsche. """Playwish-Web – Cloudflare Turnstile: Bot-Schutz für Admin-Login und Wünsche.
Kein Domain-Umzug nötig: Der Browser holt sich bei Cloudflare ein Token, der Kein Domain-Umzug nötig: Der Browser holt sich bei Cloudflare ein Token, der
Server prüft es per siteverify. Ohne TURNSTILE_SITE_KEY/TURNSTILE_SECRET_KEY in Server prüft es per siteverify. Ohne TURNSTILE_SITE_KEY/TURNSTILE_SECRET_KEY in
@@ -16,7 +16,7 @@ from fastapi import HTTPException, Request
from .config import konfig from .config import konfig
log = logging.getLogger("audiofy.turnstile") log = logging.getLogger("playwish.turnstile")
VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
+2 -2
View File
@@ -14,9 +14,9 @@ PASSWORT = "richtig-langes-passwort"
@pytest.fixture @pytest.fixture
def client(tmp_path, monkeypatch): def client(tmp_path, monkeypatch):
env = tmp_path / ".env" env = tmp_path / ".env"
env.write_text(f"DB_PATH={tmp_path / 'audiofy.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n" env.write_text(f"DB_PATH={tmp_path / 'playwish.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n"
f"PUBLIC_URL=https://musik.example.de\nADMIN_PASSWORD_HASH={PasswordHasher().hash(PASSWORT)}\n") f"PUBLIC_URL=https://musik.example.de\nADMIN_PASSWORD_HASH={PasswordHasher().hash(PASSWORT)}\n")
monkeypatch.setenv("AUDIOFY_ENV", str(env)) monkeypatch.setenv("PLAYWISH_ENV", str(env))
config.konfig.cache_clear() config.konfig.cache_clear()
for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"): for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"):
monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60)) monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60))
+2 -2
View File
@@ -35,8 +35,8 @@ class FakeSpotify:
@pytest.fixture @pytest.fixture
def client(tmp_path, monkeypatch): def client(tmp_path, monkeypatch):
env = tmp_path / ".env" env = tmp_path / ".env"
env.write_text(f"DB_PATH={tmp_path / 'audiofy.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n") env.write_text(f"DB_PATH={tmp_path / 'playwish.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n")
monkeypatch.setenv("AUDIOFY_ENV", str(env)) monkeypatch.setenv("PLAYWISH_ENV", str(env))
config.konfig.cache_clear() config.konfig.cache_clear()
for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"): for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"):
monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60)) monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60))
+2 -2
View File
@@ -15,8 +15,8 @@ URI_C = "spotify:track:" + "C" * 22
@pytest.fixture @pytest.fixture
def client(tmp_path, monkeypatch): def client(tmp_path, monkeypatch):
env = tmp_path / ".env" env = tmp_path / ".env"
env.write_text(f"PLAYER_API_TOKEN={TOKEN}\nDB_PATH={tmp_path / 'audiofy.db'}\nSPOTIFY_PLAYLIST_ID=startliste\n") env.write_text(f"PLAYER_API_TOKEN={TOKEN}\nDB_PATH={tmp_path / 'playwish.db'}\nSPOTIFY_PLAYLIST_ID=startliste\n")
monkeypatch.setenv("AUDIOFY_ENV", str(env)) monkeypatch.setenv("PLAYWISH_ENV", str(env))
config.konfig.cache_clear() config.konfig.cache_clear()
from app.main import app from app.main import app
with TestClient(app) as c: with TestClient(app) as c:
+2 -2
View File
@@ -30,10 +30,10 @@ def antworten(monkeypatch):
@pytest.fixture @pytest.fixture
def client(tmp_path, monkeypatch, antworten): def client(tmp_path, monkeypatch, antworten):
env = tmp_path / ".env" env = tmp_path / ".env"
env.write_text(f"DB_PATH={tmp_path / 'audiofy.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n" env.write_text(f"DB_PATH={tmp_path / 'playwish.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n"
f"PUBLIC_URL=https://musik.example.de\nADMIN_PASSWORD_HASH={PasswordHasher().hash(PASSWORT)}\n" f"PUBLIC_URL=https://musik.example.de\nADMIN_PASSWORD_HASH={PasswordHasher().hash(PASSWORT)}\n"
"TURNSTILE_SITE_KEY=site-oeffentlich\nTURNSTILE_SECRET_KEY=secret-geheim\n") "TURNSTILE_SITE_KEY=site-oeffentlich\nTURNSTILE_SECRET_KEY=secret-geheim\n")
monkeypatch.setenv("AUDIOFY_ENV", str(env)) monkeypatch.setenv("PLAYWISH_ENV", str(env))
config.konfig.cache_clear() config.konfig.cache_clear()
for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"): for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"):
monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60)) monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60))
+6 -6
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Audiofy-Web – Verwaltung auf der Kommandozeile. """Playwish-Web – Verwaltung auf der Kommandozeile.
geheimnisse SESSION_SECRET und PLAYER_API_TOKEN erzeugen (nur falls noch nicht vorhanden) geheimnisse SESSION_SECRET und PLAYER_API_TOKEN erzeugen (nur falls noch nicht vorhanden)
passwort Admin-Passwort setzen (verdeckte Eingabe, gespeichert wird nur der argon2-Hash) passwort Admin-Passwort setzen (verdeckte Eingabe, gespeichert wird nur der argon2-Hash)
turnstile Cloudflare-Turnstile-Schlüssel setzen (Secret-Key verdeckt) turnstile Cloudflare-Turnstile-Schlüssel setzen (Secret-Key verdeckt)
Auf dem Server als Dienst-Benutzer ausführen, damit die .env ihm gehört: Auf dem Server als Dienst-Benutzer ausführen, damit die .env ihm gehört:
sudo -u audiofy /opt/audiofy/.venv/bin/python /opt/audiofy/app/verwaltung.py passwort sudo -u playwish /opt/playwish/.venv/bin/python /opt/playwish/app/verwaltung.py passwort
""" """
import argparse import argparse
@@ -46,7 +46,7 @@ def passwort(env: Path) -> None:
if getpass.getpass("Wiederholen: ") != erstes: if getpass.getpass("Wiederholen: ") != erstes:
sys.exit("Die Eingaben stimmen nicht überein.") sys.exit("Die Eingaben stimmen nicht überein.")
env_wert_setzen(env, "ADMIN_PASSWORD_HASH", PasswordHasher().hash(erstes)) env_wert_setzen(env, "ADMIN_PASSWORD_HASH", PasswordHasher().hash(erstes))
print(f"Passwort-Hash in {env} gespeichert. Bitte den Dienst neu starten (audiofy-web.service).") print(f"Passwort-Hash in {env} gespeichert. Bitte den Dienst neu starten (playwish-web.service).")
def turnstile(env: Path) -> None: def turnstile(env: Path) -> None:
@@ -56,13 +56,13 @@ def turnstile(env: Path) -> None:
sys.exit("Beide Schlüssel sind nötig.") sys.exit("Beide Schlüssel sind nötig.")
env_wert_setzen(env, "TURNSTILE_SITE_KEY", site_key) env_wert_setzen(env, "TURNSTILE_SITE_KEY", site_key)
env_wert_setzen(env, "TURNSTILE_SECRET_KEY", secret) env_wert_setzen(env, "TURNSTILE_SECRET_KEY", secret)
print(f"Turnstile-Schlüssel in {env} gespeichert. Bitte den Dienst neu starten (audiofy-web.service).") print(f"Turnstile-Schlüssel in {env} gespeichert. Bitte den Dienst neu starten (playwish-web.service).")
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Audiofy-Web – Verwaltung") parser = argparse.ArgumentParser(description="Playwish-Web – Verwaltung")
parser.add_argument("aktion", choices=("geheimnisse", "passwort", "turnstile")) parser.add_argument("aktion", choices=("geheimnisse", "passwort", "turnstile"))
parser.add_argument("--env", type=Path, default=Path(os.environ.get("AUDIOFY_ENV", "/opt/audiofy/.env"))) parser.add_argument("--env", type=Path, default=Path(os.environ.get("PLAYWISH_ENV", "/opt/playwish/.env")))
args = parser.parse_args() args = parser.parse_args()
{"geheimnisse": geheimnisse, "passwort": passwort, "turnstile": turnstile}[args.aktion](args.env) {"geheimnisse": geheimnisse, "passwort": passwort, "turnstile": turnstile}[args.aktion](args.env)