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 +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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user