Audiofy – Musikwünsche für Partys

Server (Website, Admin-Bereich, Player-API) und Player (raspotify +
Controller) mit Install-Skripten für Debian-Systeme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 19:16:39 +01:00
co-authored by Claude Opus 5.5
commit 3610de7bae
45 changed files with 5666 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
"""Tests für die Player-API (Wegwerf-Datenbank pro Test)."""
import pytest
from fastapi.testclient import TestClient
from app import config, db
TOKEN = "test-token-0123456789"
AUTH = {"Authorization": f"Bearer {TOKEN}"}
URI_A = "spotify:track:" + "A" * 22
URI_B = "spotify:track:" + "B" * 22
URI_C = "spotify:track:" + "C" * 22
@pytest.fixture
def client(tmp_path, monkeypatch):
env = tmp_path / ".env"
env.write_text(f"PLAYER_API_TOKEN={TOKEN}\nDB_PATH={tmp_path / 'audiofy.db'}\nSPOTIFY_PLAYLIST_ID=startliste\n")
monkeypatch.setenv("AUDIOFY_ENV", str(env))
config.konfig.cache_clear()
from app.main import app
with TestClient(app) as c:
yield c
config.konfig.cache_clear()
def wunsch(uri, status="queued", position=1.0, priority=0, title="Titel"):
conn = db.verbinden()
with conn:
cur = conn.execute("INSERT INTO wishes (track_uri, title, artist, status, priority, position, created_at) "
"VALUES (?, ?, 'Interpret', ?, ?, ?, ?)", (uri, title, status, priority, position, db.jetzt()))
conn.close()
return cur.lastrowid
def test_token_pflicht(client):
assert client.get("/api/player/config").status_code == 401
assert client.get("/api/player/config", headers={"Authorization": "Bearer falsch"}).status_code == 401
assert client.get("/api/player/config", headers=AUTH).status_code == 200
def test_config_standardwerte(client):
daten = client.get("/api/player/config", headers=AUTH).json()
assert daten == {"wish_every_n": 3, "playlist_id": "startliste", "reshuffle_token": 0,
"stopped": False, "commands": []}
def test_next_wish_leer_und_reihenfolge(client):
assert client.get("/api/player/next-wish", headers=AUTH).json() == {"wish": None}
wunsch(URI_A, position=2)
b = wunsch(URI_B, position=1)
wunsch(URI_C, status="pending", position=0) # zur Freigabe – zählt nicht
assert client.get("/api/player/next-wish", headers=AUTH).json()["wish"]["id"] == b
besitzer = wunsch(URI_C, position=9, priority=1) # "Als Nächstes" schlägt alles
w = client.get("/api/player/next-wish", headers=AUTH).json()["wish"]
assert w["id"] == besitzer and w["priority"] is True
def test_wunsch_gespielt_idempotent(client):
a = wunsch(URI_A)
assert client.post(f"/api/player/wish/{a}/played", headers=AUTH).json() == {"ok": True, "status": "played"}
assert client.post(f"/api/player/wish/{a}/played", headers=AUTH).json() == {"ok": True, "status": "played"}
assert client.get("/api/player/next-wish", headers=AUTH).json() == {"wish": None}
assert client.post("/api/player/wish/9999/played", headers=AUTH).status_code == 404
def test_now_playing_schreibt_verlauf_nur_bei_wechsel(client):
a = wunsch(URI_A)
titel = {"track_uri": URI_A, "title": "Lied A", "artist": "Band"}
for _ in range(3): # wiederholte Meldungen desselben Titels → ein Verlaufseintrag
r = client.post("/api/player/now-playing", headers=AUTH,
json={"track": titel, "is_playing": True, "source": "wish", "wish_id": a})
assert r.status_code == 200
client.post("/api/player/now-playing", headers=AUTH, json={"track": titel, "is_playing": True, "new_track": True})
client.post("/api/player/now-playing", headers=AUTH,
json={"track": {"track_uri": URI_B, "title": "Lied B", "artist": "X"}, "is_playing": True})
conn = db.verbinden()
verlauf = conn.execute("SELECT track_uri, source, wish_id FROM history ORDER BY id").fetchall()
conn.close()
assert [(v["track_uri"], v["source"], v["wish_id"]) for v in verlauf] == [
(URI_A, "wish", a), (URI_A, None, None), (URI_B, None, None)]
def test_pausierter_titel_nicht_im_verlauf(client):
titel = {"track_uri": URI_A, "title": "Lied A", "artist": "Band"}
client.post("/api/player/now-playing", headers=AUTH, json={"track": titel, "is_playing": False})
conn = db.verbinden()
assert conn.execute("SELECT COUNT(*) FROM history").fetchone()[0] == 0
conn.close()
client.post("/api/player/now-playing", headers=AUTH, json={"track": titel, "is_playing": True})
conn = db.verbinden()
assert conn.execute("SELECT COUNT(*) FROM history").fetchone()[0] == 1
conn.close()
def test_now_playing_validiert(client):
r = client.post("/api/player/now-playing", headers=AUTH,
json={"track": {"track_uri": "kaputt", "title": "x", "artist": "y"}})
assert r.status_code == 422
def test_befehle_abholen_quittieren_verfallen(client):
conn = db.verbinden()
skip = db.befehl_anlegen(conn, "skip")
vol = db.befehl_anlegen(conn, "volume", {"percent": 40})
with conn: # ein uralter Befehl darf nicht mehr ausgeliefert werden
conn.execute("INSERT INTO commands (type, created_at) VALUES ('skip', '2020-01-01T00:00:00+00:00')")
conn.close()
befehle = client.get("/api/player/config", headers=AUTH).json()["commands"]
assert [(b["id"], b["type"], b["payload"]) for b in befehle] == [(skip, "skip", {}), (vol, "volume", {"percent": 40})]
assert client.post(f"/api/player/commands/{skip}/ack", headers=AUTH, json={"ok": True}).status_code == 200
assert [b["id"] for b in client.get("/api/player/config", headers=AUTH).json()["commands"]] == [vol]
assert client.post("/api/player/commands/9999/ack", headers=AUTH, json={}).status_code == 404
def test_not_aus(client):
wunsch(URI_A)
conn = db.verbinden()
db.einstellung_setzen(conn, "emergency_stop", True)
conn.close()
assert client.get("/api/player/config", headers=AUTH).json()["stopped"] is True
assert client.get("/api/player/next-wish", headers=AUTH).json() == {"wish": None}
def test_lebenszeichen(client):
client.get("/api/player/config", headers=AUTH)
conn = db.verbinden()
assert conn.execute("SELECT last_seen FROM pi_state").fetchone()["last_seen"] is not None
conn.close()
def test_bluetooth_melden(client):
daten = {"current": {"mac": "AA:11:22:33:44:55", "name": "Box", "paired": True, "connected": True},
"devices": [{"mac": "AA:BB:CC:DD:EE:FF", "name": "Neu"}], "scanning": False,
"scanned_at": "2026-10-17T18:00:00+00:00"}
assert client.post("/api/player/bluetooth", headers=AUTH, json=daten).status_code == 200
from app.player_api import player_zustand
conn = db.verbinden()
bt = player_zustand(conn)["bluetooth"]
conn.close()
assert bt["current"]["name"] == "Box" and bt["devices"][0]["mac"] == "AA:BB:CC:DD:EE:FF"
falsch = {"devices": [{"mac": "kein-mac"}]}
assert client.post("/api/player/bluetooth", headers=AUTH, json=falsch).status_code == 422
assert client.post("/api/player/bluetooth", json=daten).status_code == 401
def test_alte_datenbank_wird_nachgeruestet(tmp_path):
import sqlite3
pfad = tmp_path / "alt.db"
alt = sqlite3.connect(pfad)
alt.execute("CREATE TABLE pi_state (id INTEGER PRIMARY KEY CHECK (id = 1), track_uri TEXT, "
"payload TEXT NOT NULL DEFAULT '{}', updated_at TEXT, last_seen TEXT)")
alt.commit()
alt.close()
db.initialisieren(pfad)
conn = sqlite3.connect(pfad)
assert "bluetooth" in {r[1] for r in conn.execute("PRAGMA table_info(pi_state)")}
conn.close()
def test_alter_pfad_api_pi_bleibt_nutzbar(client):
"""Ältere Player sprechen noch /api/pi/… an."""
assert client.get("/api/pi/config", headers=AUTH).status_code == 200
assert client.get("/api/pi/config").status_code == 401