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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,4 +1,4 @@
|
||||
# Audiofy – Caddy-Konfiguration (erzeugt von install-server.sh)
|
||||
# Playwish – Caddy-Konfiguration (erzeugt von install-server.sh)
|
||||
{
|
||||
email {{ACME_EMAIL}}
|
||||
servers {
|
||||
|
||||
@@ -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}}
|
||||
+6
-6
@@ -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
|
||||
@@ -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
-1
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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 @@
|
||||
"""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
@@ -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"})
|
||||
|
||||
|
||||
@@ -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
@@ -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).
|
||||
"""
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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; }
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
/* Audiofy – Gastseite (mobil zuerst) */
|
||||
/* Playwish – Gastseite (mobil zuerst) */
|
||||
:root {
|
||||
--bg: #121212;
|
||||
--karte: #1e1e1e;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user