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