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:
+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
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -88,34 +89,72 @@ class Eintrag:
|
||||
|
||||
|
||||
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.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._letzter: str | None = None
|
||||
|
||||
def wechseln(self, playlist_id: str) -> None:
|
||||
self.id = playlist_id
|
||||
self._stapel = []
|
||||
self._alle, self._stapel = [], []
|
||||
|
||||
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:
|
||||
if not self._stapel:
|
||||
titel = self.sp.playlist_titel(self.id)
|
||||
if not titel:
|
||||
if not self._alle:
|
||||
self._alle = self.sp.playlist_titel(self.id)
|
||||
self._stapel = []
|
||||
if not self._alle:
|
||||
raise RuntimeError("Grund-Playlist ist leer oder nicht lesbar")
|
||||
random.shuffle(titel)
|
||||
# Nicht denselben Titel direkt zweimal hintereinander (an der Mischgrenze)
|
||||
if len(titel) > 1 and titel[-1] == self._letzter:
|
||||
titel[0], titel[-1] = titel[-1], titel[0]
|
||||
self._stapel = titel
|
||||
log.info("Grund-Playlist geladen und gemischt: %d Titel", len(titel))
|
||||
self._letzter = self._stapel.pop()
|
||||
return self._letzter
|
||||
sperre = self.gesperrt()
|
||||
for runde in range(2):
|
||||
while self._stapel:
|
||||
uri = self._stapel.pop()
|
||||
if uri not in sperre:
|
||||
return uri
|
||||
if runde == 0:
|
||||
# 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:
|
||||
@@ -177,17 +216,23 @@ class Controller:
|
||||
return
|
||||
self.besitzer_pause = bool(daten.get("besitzer_pause"))
|
||||
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:
|
||||
log.warning("Vor dem Neustart war Not-Aus aktiv – bleibt aktiv, bis er im Admin-Bereich aufgehoben wird")
|
||||
elif self.besitzer_pause:
|
||||
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:
|
||||
"""Schreibt nur bei Änderungen (schont die SD-Karte)."""
|
||||
if not self.zustand_pfad:
|
||||
return
|
||||
aktuell = {"besitzer_pause": self.besitzer_pause, "stopp": self.stopp}
|
||||
aktuell = self._zustand()
|
||||
if aktuell == self._gespeichert:
|
||||
return
|
||||
try:
|
||||
@@ -273,6 +318,10 @@ class Controller:
|
||||
if k.get("playlist_id") and k["playlist_id"] != self.playlist.id:
|
||||
log.info("Grund-Playlist gewechselt: %s", 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")
|
||||
if self._reshuffle is not None and token != self._reshuffle:
|
||||
log.info("Grund-Playlist wird neu gemischt")
|
||||
@@ -575,6 +624,7 @@ class Controller:
|
||||
self.seit_wunsch = 0
|
||||
elif eintrag.quelle == "playlist":
|
||||
self.seit_wunsch += 1
|
||||
self.playlist.gespielt(eintrag.uri)
|
||||
# "owner" (Als Nächstes des Besitzers) zählt nicht im Wunsch-Takt
|
||||
else:
|
||||
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")
|
||||
controller = Controller(
|
||||
sp,
|
||||
Playlist(sp, env["SPOTIFY_PLAYLIST_ID"]),
|
||||
Playlist(sp, env["SPOTIFY_PLAYLIST_ID"], int(env.get("PLAYLIST_REPEAT_PERCENT") or 50)),
|
||||
server,
|
||||
n=int(env.get("WISH_EVERY_N") or 3),
|
||||
geraetename=env.get("DEVICE_NAME") or "Audiofy",
|
||||
|
||||
Reference in New Issue
Block a user