Wiederholungsregel für die Grund-Playlist
Ein Playlist-Titel kommt erst wieder, wenn seit seinem letzten Einsatz mindestens X % der Playlist gelaufen sind (Admin → Einstellungen → Ablauf, Standard 50 %). Der Player merkt sich die gespielten Titel über Neustarts. Wünsche sind ausgenommen (dort gilt weiter die Sperrzeit in Minuten). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ Admin-Bereich, auch vom Handy.
|
|||||||
- **Gäste:** Spotify-Suche, Wünschen (optional mit Namen), „Läuft gerade“, Warteschlange – ohne Spotify-Konto
|
- **Gäste:** Spotify-Suche, Wünschen (optional mit Namen), „Läuft gerade“, Warteschlange – ohne Spotify-Konto
|
||||||
- **Schutz:** Zugang nur per Einladungslink/QR-Code, Limit pro Gast, keine Duplikate, Sperrzeit für Wiederholungen, Sperrliste, optional Moderation und Cloudflare Turnstile
|
- **Schutz:** Zugang nur per Einladungslink/QR-Code, Limit pro Gast, keine Duplikate, Sperrzeit für Wiederholungen, Sperrliste, optional Moderation und Cloudflare Turnstile
|
||||||
- **Admin:** Play/Pause/Weiter/Lautstärke, „Als Nächstes spielen“, Not-Aus, Wünsche freigeben/umsortieren/löschen, Gäste sperren, Playlist wechseln, Bluetooth-Lautsprecher suchen und wechseln, Verlauf
|
- **Admin:** Play/Pause/Weiter/Lautstärke, „Als Nächstes spielen“, Not-Aus, Wünsche freigeben/umsortieren/löschen, Gäste sperren, Playlist wechseln, Bluetooth-Lautsprecher suchen und wechseln, Verlauf
|
||||||
|
- **Abwechslung:** Ein Playlist-Titel kommt erst wieder, wenn seit seinem letzten Einsatz mindestens X % der Playlist gelaufen sind (einstellbar, auch über Neustarts); Wünsche haben eine eigene Sperrzeit in Minuten
|
||||||
- **Robust:** Ohne Server spielt der Player die Playlist weiter; Bedienung über die normale Spotify-App bleibt möglich; Pause des Gastgebers wird auch über Neustarts respektiert
|
- **Robust:** Ohne Server spielt der Player die Playlist weiter; Bedienung über die normale Spotify-App bleibt möglich; Pause des Gastgebers wird auch über Neustarts respektiert
|
||||||
|
|
||||||
## Aufbau
|
## Aufbau
|
||||||
@@ -155,7 +156,9 @@ eigenen Refresh-Token desselben Kontos.
|
|||||||
|
|
||||||
```
|
```
|
||||||
cd server && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest
|
cd server && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest
|
||||||
.venv/bin/python -m pytest -q
|
.venv/bin/python -m pytest -q # Server
|
||||||
|
cd ../player && python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest
|
||||||
|
.venv/bin/python -m pytest -q tests # Player (Wiederholungsregel)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|||||||
+69
-19
@@ -50,6 +50,7 @@ BT_NEU_S = 10 # bei getrenntem Lautsprecher so oft neu verbinden
|
|||||||
MAX_BACKOFF_S = 15 # nach Netzausfall schnell wieder dran sein
|
MAX_BACKOFF_S = 15 # nach Netzausfall schnell wieder dran sein
|
||||||
PAUSE_MIN_MS = 5_000 # pausierter Titel ab dieser Position gilt beim Start als Besitzer-Pause
|
PAUSE_MIN_MS = 5_000 # pausierter Titel ab dieser Position gilt beim Start als Besitzer-Pause
|
||||||
BT_MELDEN_S = 30 # Lautsprecher-Zustand so oft an den Server melden
|
BT_MELDEN_S = 30 # Lautsprecher-Zustand so oft an den Server melden
|
||||||
|
ZULETZT_MAX = 1000 # so viele zuletzt gespielte Playlist-Titel merken (für die Wiederholungsregel)
|
||||||
BT_BEREIT_S = 3 # nach dem Verbinden so lange warten, bis BlueALSA das Audiogerät anbietet
|
BT_BEREIT_S = 3 # nach dem Verbinden so lange warten, bis BlueALSA das Audiogerät anbietet
|
||||||
|
|
||||||
|
|
||||||
@@ -88,34 +89,72 @@ class Eintrag:
|
|||||||
|
|
||||||
|
|
||||||
class Playlist:
|
class Playlist:
|
||||||
"""Grund-Playlist gemischt; ohne Wiederholung, bis alle Titel durch sind."""
|
"""Grund-Playlist gemischt, ohne Wiederholung, bis alle Titel durch sind.
|
||||||
|
|
||||||
def __init__(self, sp: SpotifyClient, playlist_id: str):
|
Zusätzlich gilt die Wiederholungsregel: Ein Titel kommt erst wieder, wenn seit
|
||||||
|
seinem letzten Einsatz mindestens wiederholung_prozent % der Playlist (verschiedene
|
||||||
|
Titel) gelaufen sind – auch über Neustarts und neues Mischen hinweg (zuletzt wird
|
||||||
|
in der Zustandsdatei gespeichert). Wünsche zählen hier nicht mit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sp: SpotifyClient, playlist_id: str, wiederholung_prozent: int = 50):
|
||||||
self.sp = sp
|
self.sp = sp
|
||||||
self.id = playlist_id
|
self.id = playlist_id
|
||||||
|
self.wiederholung_prozent = wiederholung_prozent
|
||||||
|
self.zuletzt: deque[str] = deque(maxlen=ZULETZT_MAX) # gespielte Playlist-Titel, neueste hinten
|
||||||
|
self._alle: list[str] = []
|
||||||
self._stapel: list[str] = []
|
self._stapel: list[str] = []
|
||||||
self._letzter: str | None = None
|
|
||||||
|
|
||||||
def wechseln(self, playlist_id: str) -> None:
|
def wechseln(self, playlist_id: str) -> None:
|
||||||
self.id = playlist_id
|
self.id = playlist_id
|
||||||
self._stapel = []
|
self._alle, self._stapel = [], []
|
||||||
|
|
||||||
def neu_mischen(self) -> None:
|
def neu_mischen(self) -> None:
|
||||||
self._stapel = [] # beim nächsten Titel neu laden und mischen
|
self._alle, self._stapel = [], [] # beim nächsten Titel neu laden und mischen
|
||||||
|
|
||||||
|
def gespielt(self, uri: str) -> None:
|
||||||
|
"""Ein Titel aus der Playlist ist tatsächlich gelaufen."""
|
||||||
|
self.zuletzt.append(uri)
|
||||||
|
|
||||||
|
def gesperrt(self) -> set[str]:
|
||||||
|
"""Titel, die wegen der Wiederholungsregel gerade nicht dran sein dürfen."""
|
||||||
|
anzahl = len(self._alle)
|
||||||
|
# mindestens ein Titel muss immer frei bleiben
|
||||||
|
grenze = min(anzahl - 1, anzahl * max(0, min(100, self.wiederholung_prozent)) // 100)
|
||||||
|
sperre: set[str] = set()
|
||||||
|
if grenze <= 0:
|
||||||
|
return sperre
|
||||||
|
vorhanden = set(self._alle)
|
||||||
|
for uri in reversed(self.zuletzt):
|
||||||
|
if uri in vorhanden:
|
||||||
|
sperre.add(uri)
|
||||||
|
if len(sperre) >= grenze:
|
||||||
|
break
|
||||||
|
return sperre
|
||||||
|
|
||||||
def naechster(self) -> str:
|
def naechster(self) -> str:
|
||||||
if not self._stapel:
|
if not self._alle:
|
||||||
titel = self.sp.playlist_titel(self.id)
|
self._alle = self.sp.playlist_titel(self.id)
|
||||||
if not titel:
|
self._stapel = []
|
||||||
|
if not self._alle:
|
||||||
raise RuntimeError("Grund-Playlist ist leer oder nicht lesbar")
|
raise RuntimeError("Grund-Playlist ist leer oder nicht lesbar")
|
||||||
random.shuffle(titel)
|
sperre = self.gesperrt()
|
||||||
# Nicht denselben Titel direkt zweimal hintereinander (an der Mischgrenze)
|
for runde in range(2):
|
||||||
if len(titel) > 1 and titel[-1] == self._letzter:
|
while self._stapel:
|
||||||
titel[0], titel[-1] = titel[-1], titel[0]
|
uri = self._stapel.pop()
|
||||||
self._stapel = titel
|
if uri not in sperre:
|
||||||
log.info("Grund-Playlist geladen und gemischt: %d Titel", len(titel))
|
return uri
|
||||||
self._letzter = self._stapel.pop()
|
if runde == 0:
|
||||||
return self._letzter
|
# Durchlauf zu Ende → neu laden (Änderungen an der Playlist übernehmen) und mischen
|
||||||
|
self._alle = self.sp.playlist_titel(self.id) or self._alle
|
||||||
|
sperre = self.gesperrt()
|
||||||
|
self._stapel = [u for u in self._alle if u not in sperre]
|
||||||
|
random.shuffle(self._stapel)
|
||||||
|
log.info("Grund-Playlist gemischt: %d Titel, davon %d gerade gesperrt (Wiederholungsregel %d %%)",
|
||||||
|
len(self._alle), len(sperre), self.wiederholung_prozent)
|
||||||
|
# Sollte nicht vorkommen (es bleibt immer ein Titel frei) – zur Sicherheit der am längsten nicht gespielte
|
||||||
|
letzte = {u: i for i, u in enumerate(self.zuletzt)}
|
||||||
|
return min(self._alle, key=lambda u: letzte.get(u, -1))
|
||||||
|
|
||||||
|
|
||||||
class Controller:
|
class Controller:
|
||||||
@@ -177,17 +216,23 @@ class Controller:
|
|||||||
return
|
return
|
||||||
self.besitzer_pause = bool(daten.get("besitzer_pause"))
|
self.besitzer_pause = bool(daten.get("besitzer_pause"))
|
||||||
self.stopp = bool(daten.get("stopp"))
|
self.stopp = bool(daten.get("stopp"))
|
||||||
self._gespeichert = {"besitzer_pause": self.besitzer_pause, "stopp": self.stopp}
|
if self.playlist is not None:
|
||||||
|
self.playlist.zuletzt.extend(u for u in daten.get("zuletzt", []) if isinstance(u, str))
|
||||||
|
self._gespeichert = self._zustand()
|
||||||
if self.stopp:
|
if self.stopp:
|
||||||
log.warning("Vor dem Neustart war Not-Aus aktiv – bleibt aktiv, bis er im Admin-Bereich aufgehoben wird")
|
log.warning("Vor dem Neustart war Not-Aus aktiv – bleibt aktiv, bis er im Admin-Bereich aufgehoben wird")
|
||||||
elif self.besitzer_pause:
|
elif self.besitzer_pause:
|
||||||
log.info("Vor dem Neustart war pausiert – bleibt pausiert, bis Play gedrückt wird")
|
log.info("Vor dem Neustart war pausiert – bleibt pausiert, bis Play gedrückt wird")
|
||||||
|
|
||||||
|
def _zustand(self) -> dict:
|
||||||
|
return {"besitzer_pause": self.besitzer_pause, "stopp": self.stopp,
|
||||||
|
"zuletzt": list(self.playlist.zuletzt) if self.playlist is not None else []}
|
||||||
|
|
||||||
def zustand_sichern(self) -> None:
|
def zustand_sichern(self) -> None:
|
||||||
"""Schreibt nur bei Änderungen (schont die SD-Karte)."""
|
"""Schreibt nur bei Änderungen (schont die SD-Karte)."""
|
||||||
if not self.zustand_pfad:
|
if not self.zustand_pfad:
|
||||||
return
|
return
|
||||||
aktuell = {"besitzer_pause": self.besitzer_pause, "stopp": self.stopp}
|
aktuell = self._zustand()
|
||||||
if aktuell == self._gespeichert:
|
if aktuell == self._gespeichert:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -273,6 +318,10 @@ class Controller:
|
|||||||
if k.get("playlist_id") and k["playlist_id"] != self.playlist.id:
|
if k.get("playlist_id") and k["playlist_id"] != self.playlist.id:
|
||||||
log.info("Grund-Playlist gewechselt: %s", k["playlist_id"])
|
log.info("Grund-Playlist gewechselt: %s", k["playlist_id"])
|
||||||
self.playlist.wechseln(k["playlist_id"])
|
self.playlist.wechseln(k["playlist_id"])
|
||||||
|
prozent = k.get("playlist_repeat_percent")
|
||||||
|
if isinstance(prozent, int) and prozent != self.playlist.wiederholung_prozent:
|
||||||
|
log.info("Wiederholungsregel: Playlist-Titel erst wieder nach %d %% der Playlist", prozent)
|
||||||
|
self.playlist.wiederholung_prozent = prozent
|
||||||
token = k.get("reshuffle_token")
|
token = k.get("reshuffle_token")
|
||||||
if self._reshuffle is not None and token != self._reshuffle:
|
if self._reshuffle is not None and token != self._reshuffle:
|
||||||
log.info("Grund-Playlist wird neu gemischt")
|
log.info("Grund-Playlist wird neu gemischt")
|
||||||
@@ -575,6 +624,7 @@ class Controller:
|
|||||||
self.seit_wunsch = 0
|
self.seit_wunsch = 0
|
||||||
elif eintrag.quelle == "playlist":
|
elif eintrag.quelle == "playlist":
|
||||||
self.seit_wunsch += 1
|
self.seit_wunsch += 1
|
||||||
|
self.playlist.gespielt(eintrag.uri)
|
||||||
# "owner" (Als Nächstes des Besitzers) zählt nicht im Wunsch-Takt
|
# "owner" (Als Nächstes des Besitzers) zählt nicht im Wunsch-Takt
|
||||||
else:
|
else:
|
||||||
self.aktuell_quelle = "fremd"
|
self.aktuell_quelle = "fremd"
|
||||||
@@ -664,7 +714,7 @@ def main() -> None:
|
|||||||
log.warning("SERVER_URL/PLAYER_API_TOKEN fehlen – nur Grund-Playlist, keine Wünsche")
|
log.warning("SERVER_URL/PLAYER_API_TOKEN fehlen – nur Grund-Playlist, keine Wünsche")
|
||||||
controller = Controller(
|
controller = Controller(
|
||||||
sp,
|
sp,
|
||||||
Playlist(sp, env["SPOTIFY_PLAYLIST_ID"]),
|
Playlist(sp, env["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 "Audiofy",
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Tests für die Wiederholungsregel der Grund-Playlist (ohne Spotify)."""
|
||||||
|
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
from controller import Controller, Playlist # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSpotify:
|
||||||
|
def __init__(self, titel):
|
||||||
|
self.titel = list(titel)
|
||||||
|
|
||||||
|
def playlist_titel(self, playlist_id):
|
||||||
|
return list(self.titel)
|
||||||
|
|
||||||
|
|
||||||
|
def spielen(pl: Playlist, anzahl: int) -> list[str]:
|
||||||
|
folge = []
|
||||||
|
for _ in range(anzahl):
|
||||||
|
uri = pl.naechster()
|
||||||
|
pl.gespielt(uri)
|
||||||
|
folge.append(uri)
|
||||||
|
return folge
|
||||||
|
|
||||||
|
|
||||||
|
def kleinster_abstand(folge: list[str]) -> int:
|
||||||
|
"""Wie viele verschiedene andere Titel liefen mindestens zwischen zwei Einsätzen desselben Titels?"""
|
||||||
|
minimum = 10**9
|
||||||
|
letzte: dict[str, int] = {}
|
||||||
|
for i, uri in enumerate(folge):
|
||||||
|
if uri in letzte:
|
||||||
|
minimum = min(minimum, len(set(folge[letzte[uri] + 1:i])))
|
||||||
|
letzte[uri] = i
|
||||||
|
return minimum
|
||||||
|
|
||||||
|
|
||||||
|
TITEL = [f"spotify:track:{i:022d}" for i in range(10)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("prozent, mindestens", [(50, 5), (80, 8), (100, 9)])
|
||||||
|
def test_regel_haelt_abstand_ein(prozent, mindestens):
|
||||||
|
random.seed(prozent)
|
||||||
|
pl = Playlist(FakeSpotify(TITEL), "liste", prozent)
|
||||||
|
assert kleinster_abstand(spielen(pl, 300)) >= mindestens
|
||||||
|
|
||||||
|
|
||||||
|
def test_jeder_titel_kommt_dran():
|
||||||
|
pl = Playlist(FakeSpotify(TITEL), "liste", 100)
|
||||||
|
assert set(spielen(pl, 10)) == set(TITEL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_regel_gilt_ueber_neustart():
|
||||||
|
random.seed(1)
|
||||||
|
alt = Playlist(FakeSpotify(TITEL), "liste", 50)
|
||||||
|
vorher = spielen(alt, 7)
|
||||||
|
neu = Playlist(FakeSpotify(TITEL), "liste", 50) # "Neustart"
|
||||||
|
neu.zuletzt.extend(alt.zuletzt)
|
||||||
|
gesperrt = set(vorher[-5:])
|
||||||
|
assert not gesperrt & set(spielen(neu, 5))
|
||||||
|
|
||||||
|
|
||||||
|
def test_neu_mischen_bricht_regel_nicht():
|
||||||
|
random.seed(2)
|
||||||
|
pl = Playlist(FakeSpotify(TITEL), "liste", 80)
|
||||||
|
folge = spielen(pl, 4)
|
||||||
|
pl.neu_mischen()
|
||||||
|
folge += spielen(pl, 30)
|
||||||
|
assert kleinster_abstand(folge) >= 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_null_prozent_wie_bisher():
|
||||||
|
pl = Playlist(FakeSpotify(TITEL), "liste", 0)
|
||||||
|
assert pl.gesperrt() == set()
|
||||||
|
assert len(spielen(pl, 25)) == 25
|
||||||
|
|
||||||
|
|
||||||
|
def test_titel_anderer_playlist_zaehlen_nicht():
|
||||||
|
pl = Playlist(FakeSpotify(TITEL), "liste", 100)
|
||||||
|
pl.zuletzt.extend(f"spotify:track:x{i:021d}" for i in range(50)) # alte Playlist
|
||||||
|
pl.naechster()
|
||||||
|
assert pl.gesperrt() == set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_ein_titel_bleibt_immer_frei():
|
||||||
|
pl = Playlist(FakeSpotify(TITEL[:2]), "liste", 100)
|
||||||
|
assert len(spielen(pl, 10)) == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_zustand_speichert_zuletzt(tmp_path):
|
||||||
|
datei = tmp_path / "zustand.json"
|
||||||
|
neu = lambda: Controller(sp=None, playlist=Playlist(FakeSpotify(TITEL), "liste", 50), server=None, # noqa: E731
|
||||||
|
n=3, geraetename="Audiofy", bt_mac=None, zustand_pfad=datei)
|
||||||
|
c = neu()
|
||||||
|
c.playlist.gespielt(TITEL[3])
|
||||||
|
c.playlist.gespielt(TITEL[4])
|
||||||
|
c.zustand_sichern()
|
||||||
|
assert list(neu().playlist.zuletzt) == [TITEL[3], TITEL[4]]
|
||||||
@@ -49,6 +49,7 @@ AENDERBAR = {
|
|||||||
"wish_limit_count": (int, 1, 50),
|
"wish_limit_count": (int, 1, 50),
|
||||||
"wish_limit_minutes": (int, 1, 1440),
|
"wish_limit_minutes": (int, 1, 1440),
|
||||||
"repeat_block_minutes": (int, 0, 1440),
|
"repeat_block_minutes": (int, 0, 1440),
|
||||||
|
"playlist_repeat_percent": (int, 0, 100),
|
||||||
"moderation": (bool, None, None),
|
"moderation": (bool, None, None),
|
||||||
"wishes_paused": (bool, None, None),
|
"wishes_paused": (bool, None, None),
|
||||||
"block_explicit": (bool, None, None),
|
"block_explicit": (bool, None, None),
|
||||||
|
|||||||
+2
-1
@@ -105,7 +105,8 @@ STANDARD_EINSTELLUNGEN = {
|
|||||||
"wishes_paused": False, # Wunschannahme pausiert
|
"wishes_paused": False, # Wunschannahme pausiert
|
||||||
"wish_limit_count": 2, # max. Wünsche pro Gast …
|
"wish_limit_count": 2, # max. Wünsche pro Gast …
|
||||||
"wish_limit_minutes": 30, # … pro Zeitraum
|
"wish_limit_minutes": 30, # … pro Zeitraum
|
||||||
"repeat_block_minutes": 60, # Sperrzeit für Wiederholungen
|
"repeat_block_minutes": 60, # Sperrzeit für Wiederholungen (Wünsche)
|
||||||
|
"playlist_repeat_percent": 50, # Playlist-Titel erst wieder nach X % der Playlist
|
||||||
"block_explicit": False, # explizite Titel nicht erlauben
|
"block_explicit": False, # explizite Titel nicht erlauben
|
||||||
"access_enabled": False, # Gastzugang per Einladungslink/QR-Code (anfangs aus)
|
"access_enabled": False, # Gastzugang per Einladungslink/QR-Code (anfangs aus)
|
||||||
"access_token": "", # geheimer Teil des Einladungslinks /z/<token>
|
"access_token": "", # geheimer Teil des Einladungslinks /z/<token>
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ def pi_config(db: sqlite3.Connection = Depends(get_db)) -> dict:
|
|||||||
"wish_every_n": e["wish_every_n"],
|
"wish_every_n": e["wish_every_n"],
|
||||||
"playlist_id": e["playlist_id"],
|
"playlist_id": e["playlist_id"],
|
||||||
"reshuffle_token": e["reshuffle_token"],
|
"reshuffle_token": e["reshuffle_token"],
|
||||||
|
"playlist_repeat_percent": e["playlist_repeat_percent"],
|
||||||
"stopped": e["emergency_stop"],
|
"stopped": e["emergency_stop"],
|
||||||
"commands": offene_befehle(db),
|
"commands": offene_befehle(db),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,8 +138,10 @@
|
|||||||
<div class="karte">
|
<div class="karte">
|
||||||
<h2>Ablauf</h2>
|
<h2>Ablauf</h2>
|
||||||
<div class="feld-zeile"><span>Jeder wievielte Titel ist ein Wunsch (N)</span><input id="e-n" type="number" min="1" max="20"></div>
|
<div class="feld-zeile"><span>Jeder wievielte Titel ist ein Wunsch (N)</span><input id="e-n" type="number" min="1" max="20"></div>
|
||||||
<div class="feld-zeile"><span>Sperrzeit für Wiederholungen (Min.)</span><input id="e-sperrzeit" type="number" min="0" max="1440"></div>
|
<div class="feld-zeile"><span>Wunsch erst wieder erlaubt nach (Min.)</span><input id="e-sperrzeit" type="number" min="0" max="1440"></div>
|
||||||
<button class="gross akzent voll" type="button" data-speichern="e-n:wish_every_n,e-sperrzeit:repeat_block_minutes">Speichern</button>
|
<div class="feld-zeile"><span>Playlist-Titel erst wieder nach (% der Playlist)</span><input id="e-playlist-prozent" type="number" min="0" max="100"></div>
|
||||||
|
<p class="hinweis klein">Ein Titel der Grund-Playlist kommt erst wieder, wenn seit seinem letzten Einsatz mindestens so viel Prozent der Playlist gelaufen sind. 100 % = erst wenn alle anderen dran waren. Für Wünsche gilt nur die Minuten-Sperre.</p>
|
||||||
|
<button class="gross akzent voll" type="button" data-speichern="e-n:wish_every_n,e-sperrzeit:repeat_block_minutes,e-playlist-prozent:playlist_repeat_percent">Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="karte">
|
<div class="karte">
|
||||||
<h2>Lautsprecher</h2>
|
<h2>Lautsprecher</h2>
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ function zeigeEinstellungen(z) {
|
|||||||
const e = z.settings;
|
const e = z.settings;
|
||||||
setzeWert("e-n", e.wish_every_n);
|
setzeWert("e-n", e.wish_every_n);
|
||||||
setzeWert("e-sperrzeit", e.repeat_block_minutes);
|
setzeWert("e-sperrzeit", e.repeat_block_minutes);
|
||||||
|
setzeWert("e-playlist-prozent", e.playlist_repeat_percent);
|
||||||
setzeWert("e-limit-anzahl", e.wish_limit_count);
|
setzeWert("e-limit-anzahl", e.wish_limit_count);
|
||||||
setzeWert("e-limit-minuten", e.wish_limit_minutes);
|
setzeWert("e-limit-minuten", e.wish_limit_minutes);
|
||||||
setzeSchalter("s-explizit", e.block_explicit);
|
setzeSchalter("s-explizit", e.block_explicit);
|
||||||
|
|||||||
@@ -176,8 +176,10 @@ def test_einstellungen_validiert(client):
|
|||||||
h = anmelden(client)
|
h = anmelden(client)
|
||||||
assert client.post("/admin/api/settings", json={"wish_every_n": 4, "moderation": True}, headers=h).status_code == 200
|
assert client.post("/admin/api/settings", json={"wish_every_n": 4, "moderation": True}, headers=h).status_code == 200
|
||||||
assert (einstellung("wish_every_n"), einstellung("moderation")) == (4, True)
|
assert (einstellung("wish_every_n"), einstellung("moderation")) == (4, True)
|
||||||
|
assert client.post("/admin/api/settings", json={"playlist_repeat_percent": 80}, headers=h).status_code == 200
|
||||||
|
assert einstellung("playlist_repeat_percent") == 80
|
||||||
for falsch in ({"wish_every_n": 0}, {"wish_every_n": "4"}, {"moderation": 1}, {"access_token": "x"},
|
for falsch in ({"wish_every_n": 0}, {"wish_every_n": "4"}, {"moderation": 1}, {"access_token": "x"},
|
||||||
{"emergency_stop": True}):
|
{"emergency_stop": True}, {"playlist_repeat_percent": 101}, {"playlist_repeat_percent": -1}):
|
||||||
assert client.post("/admin/api/settings", json=falsch, headers=h).status_code == 422, falsch
|
assert client.post("/admin/api/settings", json=falsch, headers=h).status_code == 422, falsch
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ def test_token_pflicht(client):
|
|||||||
def test_config_standardwerte(client):
|
def test_config_standardwerte(client):
|
||||||
daten = client.get("/api/player/config", headers=AUTH).json()
|
daten = client.get("/api/player/config", headers=AUTH).json()
|
||||||
assert daten == {"wish_every_n": 3, "playlist_id": "startliste", "reshuffle_token": 0,
|
assert daten == {"wish_every_n": 3, "playlist_id": "startliste", "reshuffle_token": 0,
|
||||||
"stopped": False, "commands": []}
|
"playlist_repeat_percent": 50, "stopped": False, "commands": []}
|
||||||
|
|
||||||
|
|
||||||
def test_next_wish_leer_und_reihenfolge(client):
|
def test_next_wish_leer_und_reihenfolge(client):
|
||||||
|
|||||||
Reference in New Issue
Block a user