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
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
```
Gast-Handy ──HTTPS──▶ Server: Caddy → audiofy-web (FastAPI + SQLite)
Gast-Handy ──HTTPS──▶ Server: Caddy → playwish-web (FastAPI + SQLite)
▲ Player-API (Bearer-Token)
│ nur ausgehend vom Player
Player: raspotify (Spotify Connect) + audiofy-controller
Player: raspotify (Spotify Connect) + playwish-controller
└─▶ Bluetooth-Lautsprecher (BlueALSA) oder Kabel/HDMI/USB (ALSA)
```
@@ -39,7 +39,7 @@ Portfreigabe am Router.
## 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.
**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 |
|---|---|
| App name | z. B. `Audiofy` |
| App name | z. B. `Playwish` |
| App description | z. B. `Musikwünsche für meine Party` |
| Website | leer lassen |
| **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
(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
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.
**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:
```
curl -fsSL https://git.wind8000.de/0/audiofy/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 -- server
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`
am Ende (`… | bash -s -- server --update`).
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
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:
**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
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
von selbst wieder da.
@@ -151,9 +151,9 @@ einschalten und den QR-Code zeigen oder ausdrucken.
| Aufgabe | Befehl |
|---|---|
| Status | `systemctl status audiofy-web` (Server) · `systemctl status audiofy-controller raspotify` (Player) |
| Logs live | `journalctl -u audiofy-web -f` · `journalctl -u audiofy-controller -f` |
| Neustart | `sudo systemctl restart audiofy-web` bzw. `audiofy-controller` |
| Status | `systemctl status playwish-web` (Server) · `systemctl status playwish-controller raspotify` (Player) |
| Logs live | `journalctl -u playwish-web -f` · `journalctl -u playwish-controller -f` |
| 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` ersetzt nur Code, Python-Umgebung und Dienstdatei; Konfiguration, Datenbank,
@@ -163,23 +163,23 @@ Caddy, Firewall und raspotify bleiben unverändert.
| Pfad | Inhalt |
|---|---|
| `/opt/audiofy/app` | Code (gehört root, der Dienst darf nur lesen) |
| `/opt/audiofy/.env` (Server) | Geheimnisse: Spotify, Admin-Passwort-Hash, Turnstile, `PLAYER_API_TOKEN` |
| `/opt/audiofy/data/audiofy.db` (Server) | Wünsche, Verlauf, Einstellungen |
| `/opt/audiofy/config/.env` (Player) | Geheimnisse: Spotify, `SERVER_URL`, `PLAYER_API_TOKEN`, Lautsprecher |
| `/opt/playwish/app` | Code (gehört root, der Dienst darf nur lesen) |
| `/opt/playwish/.env` (Server) | Geheimnisse: Spotify, Admin-Passwort-Hash, Turnstile, `PLAYER_API_TOKEN` |
| `/opt/playwish/data/playwish.db` (Server) | Wünsche, Verlauf, Einstellungen |
| `/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.
**Verwaltung** (interaktiv, im eigenen Terminal):
```
# 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 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 passwort --env /opt/playwish/.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
sudo -u audiofy /opt/audiofy/.venv/bin/python /opt/audiofy/app/auth.py --profil server --env /opt/audiofy/.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 server --env /opt/playwish/.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.
@@ -187,18 +187,18 @@ Danach den jeweiligen Dienst neu starten.
**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)
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:
1. Eigenen Admin-Benutzer mit sudo anlegen, SSH-Schlüssel hinterlegen, Login testen.
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.
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. |
| 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. |
| 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). |
| 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“. |
@@ -231,14 +231,14 @@ cd ../player && python3 -m venv .venv && .venv/bin/pip install -r requirements.t
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
verändern – entweder gemäß Version 3 der Lizenz oder (nach deiner Wahl) jeder späteren
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
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.
+9 -9
View File
@@ -1,17 +1,17 @@
#!/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/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 -- server
# 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)
#
# Die Quellen liegen danach in /usr/local/src/audiofy. Andere Quelle/Zweig:
# AUDIOFY_REPO=https://…/audiofy.git AUDIOFY_BRANCH=main
# Die Quellen liegen danach in /usr/local/src/playwish. Andere Quelle/Zweig:
# PLAYWISH_REPO=https://…/playwish.git PLAYWISH_BRANCH=main
set -euo pipefail
REPO="${AUDIOFY_REPO:-https://git.wind8000.de/0/audiofy.git}"
ZWEIG="${AUDIOFY_BRANCH:-main}"
QUELLE="${AUDIOFY_SRC:-/usr/local/src/audiofy}"
REPO="${PLAYWISH_REPO:-https://git.wind8000.de/0/playwish.git}"
ZWEIG="${PLAYWISH_BRANCH:-main}"
QUELLE="${PLAYWISH_SRC:-/usr/local/src/playwish}"
ROLLE="${1:-}"
[ $# -gt 0 ] && shift
@@ -25,7 +25,7 @@ if [ "$(id -u)" -eq 0 ]; then SUDO=""; else
SUDO="sudo"
fi
echo "==> Audiofy-Quellen holen ($REPO, Zweig $ZWEIG)"
echo "==> Playwish-Quellen holen ($REPO, Zweig $ZWEIG)"
if ! command -v git >/dev/null; then
$SUDO apt-get update -qq
$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
# 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 --update nur Code, Python-Umgebung und Dienst aktualisieren
@@ -20,23 +20,23 @@ for arg in "$@"; do
esac
done
KONFIG="$AUDIOFY_DIR/config"
KONFIG="$PLAYWISH_DIR/config"
ENV_DATEI="$KONFIG/.env"
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
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."
abbruch "Keine bestehende Installation in $PLAYWISH_DIR gefunden – ohne --update ausführen."
fi
# ------------------------------------------------------------------ Abfragen
if [ $UPDATE = 0 ]; then
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)"
frage AUSGABE "Auswahl" "1"
if [ "$AUSGABE" = 2 ]; then
@@ -50,7 +50,7 @@ if [ $UPDATE = 0 ]; then
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)" ""
frage SERVER_URL "Adresse des Playwish-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)"
@@ -102,7 +102,7 @@ if [ $UPDATE = 0 ]; then
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
vorlage raspotify-10-playwish.conf /etc/systemd/system/raspotify.service.d/10-playwish.conf
$SUDO systemctl daemon-reload
$SUDO systemctl enable raspotify >/dev/null 2>&1
[ $UPDATE = 0 ] && $SUDO systemctl restart raspotify
@@ -144,16 +144,16 @@ 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"
$SUDO install -d -o root -g "$PLAYWISH_USER" -m 750 "$PLAYWISH_DIR"
$SUDO install -d -o "$PLAYWISH_USER" -g "$PLAYWISH_USER" -m 700 "$KONFIG"
code_kopieren "$REPO/player" "$PLAYWISH_DIR/app"
venv_einrichten "$PLAYWISH_DIR/app/requirements.txt"
info "Code in $PLAYWISH_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"
$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_SECRET "$SPOTIFY_CLIENT_SECRET"
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"
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" \
"${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)."
else
# alte Schlüsselnamen übernehmen
@@ -179,18 +179,18 @@ else
fi
# ------------------------------------------------------------------ Dienst
schritt "Dienst audiofy-controller"
vorlage audiofy-controller.service /etc/systemd/system/audiofy-controller.service
schritt "Dienst playwish-controller"
vorlage playwish-controller.service /etc/systemd/system/playwish-controller.service
$SUDO systemctl daemon-reload
$SUDO systemctl enable audiofy-controller >/dev/null 2>&1
$SUDO systemctl restart audiofy-controller
$SUDO systemctl enable playwish-controller >/dev/null 2>&1
$SUDO systemctl restart playwish-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"
systemctl is-active --quiet playwish-controller || abbruch "playwish-controller läuft nicht – siehe: journalctl -u playwish-controller"
info "playwish-controller läuft"
# ------------------------------------------------------------------ Zusammenfassung
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
info ""
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
# 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 --update nur Code, Python-Umgebung und Dienst aktualisieren
@@ -18,14 +18,14 @@ for arg in "$@"; do
esac
done
ENV_DATEI="$AUDIOFY_DIR/.env"
export AUDIOFY_DIR AUDIOFY_USER
ENV_DATEI="$PLAYWISH_DIR/.env"
export PLAYWISH_DIR PLAYWISH_USER
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
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
# ------------------------------------------------------------------ Abfragen
@@ -67,20 +67,20 @@ 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"
$SUDO install -d -o root -g "$PLAYWISH_USER" -m 750 "$PLAYWISH_DIR"
$SUDO install -d -o "$PLAYWISH_USER" -g "$PLAYWISH_USER" -m 700 "$PLAYWISH_DIR/data"
code_kopieren "$REPO/server" "$PLAYWISH_DIR/app"
$SUDO install -o root -g "$PLAYWISH_USER" -m 640 "$REPO/player/auth.py" "$PLAYWISH_DIR/app/auth.py"
venv_einrichten "$PLAYWISH_DIR/app/requirements.txt"
info "Code in $PLAYWISH_DIR/app, Python-Umgebung in $PLAYWISH_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")
$SUDO test -f "$ENV_DATEI" || $SUDO install -o "$PLAYWISH_USER" -g "$PLAYWISH_USER" -m 600 /dev/null "$ENV_DATEI"
VERWALTUNG=("${ALS_DIENST[@]}" "$PLAYWISH_DIR/.venv/bin/python" "$PLAYWISH_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" DB_PATH "$PLAYWISH_DIR/data/playwish.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"
@@ -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 "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" \
"${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)."
fi
@@ -103,19 +103,19 @@ if [ $UPDATE = 0 ]; then
fi
# ------------------------------------------------------------------ Dienst
schritt "Dienst audiofy-web"
vorlage audiofy-web.service /etc/systemd/system/audiofy-web.service
schritt "Dienst playwish-web"
vorlage playwish-web.service /etc/systemd/system/playwish-web.service
$SUDO systemctl daemon-reload
$SUDO systemctl enable audiofy-web >/dev/null 2>&1
$SUDO systemctl restart audiofy-web
$SUDO systemctl enable playwish-web >/dev/null 2>&1
$SUDO systemctl restart playwish-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"
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 "playwish-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
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
$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
@@ -138,7 +138,7 @@ if [ $UPDATE = 0 ]; then
fi
if [ $FAIL2BAN = 1 ]; then
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 restart fail2ban
info "SSH-Schutz auf Port $SSH_PORT aktiv"
@@ -150,7 +150,7 @@ if [ $UPDATE = 0 ]; then
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
vorlage journald-10-playwish.conf /etc/systemd/journald.conf.d/10-playwish.conf
$SUDO systemctl restart systemd-journald
info "journald max. 200 MB"
fi
@@ -160,11 +160,11 @@ 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"
info "Logs: journalctl -u playwish-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 " Player-Token: sudo -u $PLAYWISH_USER grep PLAYER_API_TOKEN $ENV_DATEI"
info "Gastzugang ist anfangs aus – im Admin unter „Gäste“ einschalten."
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
AUDIOFY_DIR="${AUDIOFY_DIR:-/opt/audiofy}"
AUDIOFY_USER="${AUDIOFY_USER:-audiofy}"
PLAYWISH_DIR="${PLAYWISH_DIR:-/opt/playwish}"
PLAYWISH_USER="${PLAYWISH_USER:-playwish}"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VORLAGEN="$REPO/install/vorlagen"
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
ALS_DIENST=(runuser -u "$AUDIOFY_USER" --)
ALS_DIENST=(runuser -u "$PLAYWISH_USER" --)
else
SUDO="sudo"
ALS_DIENST=(sudo -u "$AUDIOFY_USER")
ALS_DIENST=(sudo -u "$PLAYWISH_USER")
fi
# --- Ausgabe ------------------------------------------------------------------
@@ -67,31 +67,31 @@ pakete() {
}
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)"
if ! id "$PLAYWISH_USER" >/dev/null 2>&1; then
$SUDO useradd --system --home-dir "$PLAYWISH_DIR" --no-create-home --shell /usr/sbin/nologin "$PLAYWISH_USER"
info "Dienst-Benutzer '$PLAYWISH_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 install -d -o root -g "$PLAYWISH_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 chown -R root:"$PLAYWISH_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"
local venv="$PLAYWISH_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 chown -R root:"$PLAYWISH_USER" "$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}}
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]
enabled = true
port = {{SSH_PORT}}
@@ -1,15 +1,15 @@
[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
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
User={{PLAYWISH_USER}}
Group={{PLAYWISH_USER}}
WorkingDirectory={{PLAYWISH_DIR}}/app
ExecStart={{PLAYWISH_DIR}}/.venv/bin/python -u {{PLAYWISH_DIR}}/app/controller.py --env {{PLAYWISH_DIR}}/config/.env
Restart=always
RestartSec=5
UMask=0077
@@ -17,7 +17,7 @@ UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths={{AUDIOFY_DIR}}/config
ReadWritePaths={{PLAYWISH_DIR}}/config
PrivateTmp=yes
PrivateDevices=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)
[Unit]
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!
Port {{SSH_PORT}}
PermitRootLogin no
+5 -5
View File
@@ -1,5 +1,5 @@
#!/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
.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.
Aufruf:
python3 auth.py --env /opt/audiofy/config/.env # Player (Wiedergabe steuern)
python3 auth.py --profil server --env /opt/audiofy/.env # Server (nur Suche)
python3 auth.py --env /opt/playwish/config/.env # Player (Wiedergabe steuern)
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,
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"
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)
with os.fdopen(fd, "w") as f:
f.write(inhalt)
@@ -112,7 +112,7 @@ def me_abfragen(access_token: str) -> dict:
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("--env", type=Path, default=Path(__file__).resolve().parent / ".env",
help="Pfad zur .env (Standard: neben dem Skript)")
+11 -11
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""Audiofy – Controller (Player).
"""Playwish – Controller (Player).
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,
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
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
@@ -38,7 +38,7 @@ from auth import env_lesen, env_setzen
from spotify_client import SpotifyClient, SpotifyFehler
from server_client import ServerVerbindung, Wunsch
log = logging.getLogger("audiofy.controller")
log = logging.getLogger("playwish.controller")
ORDNER = Path(__file__).resolve().parent
POLL_S = 3 # Abfrageintervall /me/player und Server
@@ -289,7 +289,7 @@ class Controller:
return 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()
if not gid:
# 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)
return
# --- es läuft nichts (auf Audiofy) ---
# --- es läuft nichts (auf Playwish) ---
if self.bt_pause:
# librespot pausiert bei Audiofehlern selbst – nach Neuverbindung fortsetzen
if self.lautsprecher_bereit():
@@ -558,13 +558,13 @@ class Controller:
log.info("Lautsprecher wieder da – Wiedergabe fortgesetzt")
return
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 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
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
else:
self.neustart = True
@@ -708,7 +708,7 @@ class Controller:
def main() -> None:
parser = argparse.ArgumentParser(description="Audiofy-Controller")
parser = argparse.ArgumentParser(description="Playwish-Controller")
parser.add_argument("--env", type=Path, default=ORDNER / ".env")
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)),
server,
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,
zustand_pfad=args.env.parent / "zustand.json",
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
BlueZ den Schlüssel nicht dauerhaft ("Bonded: no"), und der Lautsprecher lässt sich
@@ -11,7 +11,7 @@ import shutil
import subprocess
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}$")
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
None/False, und der Controller spielt einfach die Grund-Playlist weiter. Im
@@ -11,7 +11,7 @@ from dataclasses import dataclass
import httpx
log = logging.getLogger("audiofy.server")
log = logging.getLogger("playwish.server")
OFFLINE_RETRY_S = 15
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
SpotifyFehler geworfen (bei 429 mit retry_after); Netzwerkfehler kommen als
@@ -12,7 +12,7 @@ from collections.abc import Callable
import httpx
log = logging.getLogger("audiofy.spotify")
log = logging.getLogger("playwish.spotify")
API = "https://api.spotify.com/v1"
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):
datei = tmp_path / "zustand.json"
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.playlist.gespielt(TITEL[3])
c.playlist.gespielt(TITEL[4])
+1 -1
View File
@@ -42,7 +42,7 @@ class FakeServer:
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
+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:
- 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()
ADMIN_COOKIE = "audiofy_admin"
ADMIN_COOKIE = "playwish_admin"
SITZUNG_TAGE = 14
login_drossel = Drossel(5, 15 * 60)
_hasher = PasswordHasher()
@@ -390,7 +390,7 @@ def zugang_qr(request: Request, format: Literal["svg", "png"] = "svg",
if format == "png":
qr.save(puffer, kind="png", scale=16, border=3)
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)
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.
"""
@@ -51,11 +51,11 @@ def env_wert_setzen(pfad: Path, key: str, wert: str) -> None:
@lru_cache
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 {}
return Konfig(
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
spotify_client_id=werte.get("SPOTIFY_CLIENT_ID", ""),
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).
"""
+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)
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()
STATIC = Path(__file__).parent / "static"
GAST_COOKIE = "audiofy_gast"
ZUGANG_COOKIE = "audiofy_zugang"
GAST_COOKIE = "playwish_gast"
ZUGANG_COOKIE = "playwish_zugang"
ZUGANG_TAGE = 30
GAST_RE = re.compile(r"^[A-Za-z0-9_-]{22}$")
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:
return (konfig().session_secret or "audiofy").encode()
return (konfig().session_secret or "playwish").encode()
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
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
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")
+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:
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
wish_id: int | None = None
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
stopped: bool = False # Controller im Not-Aus
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).
Im Development Mode liefert die Suche höchstens 10 Treffer.
@@ -14,7 +14,7 @@ import httpx
from .config import Konfig, env_wert_setzen, konfig
log = logging.getLogger("audiofy.spotify")
log = logging.getLogger("playwish.spotify")
API = "https://api.spotify.com/v1"
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 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="theme-color" content="#121212">
<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/admin.css">
<script src="/static/admin.js" defer></script>
@@ -13,7 +13,7 @@
<body>
<main>
<div class="kopf">
<h1>Audiofy Admin</h1>
<h1>Playwish Admin</h1>
<button id="abmelden" class="klein" type="button">Abmelden</button>
</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";
const ABFRAGE_MS = 3000;
+1 -1
View File
@@ -1,4 +1,4 @@
/* Audiofy – Gastseite (mobil zuerst) */
/* Playwish – Gastseite (mobil zuerst) */
:root {
--bg: #121212;
--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";
const STATUS_ALLE_MS = 5000;
@@ -193,7 +193,7 @@ async function suchen(q) {
async function wuenschen(titel, knopf) {
knopf.disabled = true;
const name = $("name").value.trim();
try { localStorage.setItem("audiofy-name", name); } catch (_) { /* privat */ }
try { localStorage.setItem("playwish-name", name); } catch (_) { /* privat */ }
try {
if (!schutz.bereit()) knopf.textContent = "Prüfung …";
const turnstile = await schutz.token();
@@ -214,7 +214,7 @@ async function wuenschen(titel, knopf) {
}
function start() {
try { $("name").value = localStorage.getItem("audiofy-name") || ""; } catch (_) { /* privat */ }
try { $("name").value = localStorage.getItem("playwish-name") || ""; } catch (_) { /* privat */ }
$("suchfeld").addEventListener("input", (ereignis) => {
clearTimeout(sucheTimer);
@@ -226,7 +226,7 @@ function start() {
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();
setInterval(() => { if (!document.hidden) ladeStatus(); }, STATUS_ALLE_MS);
+2 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<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="preconnect" href="https://challenges.cloudflare.com">
<script src="/static/turnstile.js" defer></script>
@@ -13,7 +13,7 @@
<body>
<main>
<header>
<h1>Audiofy</h1>
<h1>Playwish</h1>
<p class="untertitel">Musikwünsche</p>
</header>
+2 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#121212">
<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/admin.css">
<link rel="preconnect" href="https://challenges.cloudflare.com">
@@ -15,7 +15,7 @@
<body>
<main>
<header>
<h1>Audiofy</h1>
<h1>Playwish</h1>
<p class="untertitel">Admin-Bereich</p>
</header>
<form id="login" class="karte">
+2 -2
View File
@@ -1,8 +1,8 @@
// Audiofy – Admin-Login
// Playwish – Admin-Login
"use strict";
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) => {
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";
window.AudiofyTurnstile = (() => {
window.PlaywishTurnstile = (() => {
let ladeVersprechen = null;
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="theme-color" content="#121212">
<meta name="robots" content="noindex">
<title>Audiofy – Musikwünsche</title>
<title>Playwish – Musikwünsche</title>
<link rel="stylesheet" href="/static/app.css">
</head>
<body>
<main>
<header>
<h1>Audiofy</h1>
<h1>Playwish</h1>
<p class="untertitel">Musikwünsche</p>
</header>
<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
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
log = logging.getLogger("audiofy.turnstile")
log = logging.getLogger("playwish.turnstile")
VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
+2 -2
View File
@@ -14,9 +14,9 @@ PASSWORT = "richtig-langes-passwort"
@pytest.fixture
def client(tmp_path, monkeypatch):
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")
monkeypatch.setenv("AUDIOFY_ENV", str(env))
monkeypatch.setenv("PLAYWISH_ENV", str(env))
config.konfig.cache_clear()
for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"):
monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60))
+2 -2
View File
@@ -35,8 +35,8 @@ class FakeSpotify:
@pytest.fixture
def client(tmp_path, monkeypatch):
env = tmp_path / ".env"
env.write_text(f"DB_PATH={tmp_path / 'audiofy.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n")
monkeypatch.setenv("AUDIOFY_ENV", str(env))
env.write_text(f"DB_PATH={tmp_path / 'playwish.db'}\nSESSION_SECRET=geheim\nCOOKIE_SECURE=0\n")
monkeypatch.setenv("PLAYWISH_ENV", str(env))
config.konfig.cache_clear()
for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"):
monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60))
+2 -2
View File
@@ -15,8 +15,8 @@ URI_C = "spotify:track:" + "C" * 22
@pytest.fixture
def client(tmp_path, monkeypatch):
env = tmp_path / ".env"
env.write_text(f"PLAYER_API_TOKEN={TOKEN}\nDB_PATH={tmp_path / 'audiofy.db'}\nSPOTIFY_PLAYLIST_ID=startliste\n")
monkeypatch.setenv("AUDIOFY_ENV", str(env))
env.write_text(f"PLAYER_API_TOKEN={TOKEN}\nDB_PATH={tmp_path / 'playwish.db'}\nSPOTIFY_PLAYLIST_ID=startliste\n")
monkeypatch.setenv("PLAYWISH_ENV", str(env))
config.konfig.cache_clear()
from app.main import app
with TestClient(app) as c:
+2 -2
View File
@@ -30,10 +30,10 @@ def antworten(monkeypatch):
@pytest.fixture
def client(tmp_path, monkeypatch, antworten):
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"
"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()
for name in ("suche_drossel", "wunsch_drossel", "einladung_drossel"):
monkeypatch.setattr(gaeste, name, gaeste.Drossel(1000, 60))
+6 -6
View File
@@ -1,12 +1,12 @@
#!/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)
passwort Admin-Passwort setzen (verdeckte Eingabe, gespeichert wird nur der argon2-Hash)
turnstile Cloudflare-Turnstile-Schlüssel setzen (Secret-Key verdeckt)
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
@@ -46,7 +46,7 @@ def passwort(env: Path) -> None:
if getpass.getpass("Wiederholen: ") != erstes:
sys.exit("Die Eingaben stimmen nicht überein.")
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:
@@ -56,13 +56,13 @@ def turnstile(env: Path) -> None:
sys.exit("Beide Schlüssel sind nötig.")
env_wert_setzen(env, "TURNSTILE_SITE_KEY", site_key)
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:
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("--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()
{"geheimnisse": geheimnisse, "passwort": passwort, "turnstile": turnstile}[args.aktion](args.env)