248 lines
8.1 KiB
Python
248 lines
8.1 KiB
Python
"""Filmvorschläge für die Ziehung.
|
|
|
|
Jede Sitzung darf einen Film vorschlagen. Die Sitzung erkennt ein Cookie —
|
|
ohne Anmeldung geht es nicht genauer, und für einen Filmabend im Freundeskreis
|
|
reicht es.
|
|
|
|
Titel, Jahr und Poster werden mitgespeichert. So bleibt ein Vorschlag
|
|
lesbar, auch wenn der Film nie im Katalog war oder Netflix ihn abgibt.
|
|
"""
|
|
|
|
import json
|
|
import random
|
|
import sqlite3
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
from . import users
|
|
from .db import connect
|
|
from .users import WEM, besitzer
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS suggestions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT NOT NULL,
|
|
user_id INTEGER,
|
|
movie_id INTEGER NOT NULL,
|
|
title TEXT NOT NULL,
|
|
year INTEGER,
|
|
poster_path TEXT,
|
|
name TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
# Wer am Abend da ist, aber nichts vorschlagen will. Zusammen mit den
|
|
# Vorschlagenden ergibt das die Anwesenheit — die zählt beim Dreh.
|
|
DABEI = """
|
|
CREATE TABLE IF NOT EXISTS dabei (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT NOT NULL,
|
|
user_id INTEGER,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
COOKIE = "filmabend_sid"
|
|
|
|
# So lange läuft die Animation im Browser. Der Server merkt sich das
|
|
# Ergebnis mit, damit eine Seite, die mitten im Dreh geladen wird, es
|
|
# trotzdem bekommt.
|
|
DREH_DAUER = 11.2
|
|
|
|
# Die Ziehung läuft wie das Öffnen einer Lootbox: ein Streifen aus
|
|
# Postern rast vorbei und bremst auf dem Gewinner aus. Der Streifen ist
|
|
# so lang, und an dieser Stelle steht der Treffer.
|
|
STREIFEN_LAENGE = 64
|
|
STREIFEN_ZIEL = 56
|
|
|
|
# Manchmal bricht nach dem Halt die Halterung: die Bahn rutscht ein paar
|
|
# Poster zurück, und ein anderer gewinnt. So lange dauert das im Browser
|
|
# zusätzlich (Ruhe plus Rückfahrt — die Werte stehen auch in WheelTab.vue).
|
|
BRUCH_DAUER = 3.7
|
|
BRUCH_ANTEIL = 0.5
|
|
|
|
|
|
def init():
|
|
with connect() as conn:
|
|
conn.executescript(SCHEMA + DABEI)
|
|
# Ein Vorschlag je Person
|
|
users.je_person(conn, "suggestions", SCHEMA)
|
|
users.je_person(conn, "dabei", DABEI)
|
|
|
|
|
|
def neue_session():
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
def alle():
|
|
with connect() as conn:
|
|
rows = conn.execute(
|
|
# Laufzeit und Genres stehen im Katalog. Ein Vorschlag von
|
|
# außerhalb hat sie nicht — die Liste zeigt dann nur den Titel.
|
|
"SELECT s.*, m.runtime, m.genres FROM suggestions s "
|
|
"LEFT JOIN movies m ON m.id = s.movie_id "
|
|
"ORDER BY s.created_at ASC"
|
|
).fetchall()
|
|
return [
|
|
{
|
|
**{k: v for k, v in dict(r).items() if k != "genres"},
|
|
"genres": json.loads(r["genres"]) if r["genres"] else [],
|
|
}
|
|
for r in rows
|
|
]
|
|
|
|
|
|
def von_session(session_id):
|
|
"""Der eigene Vorschlag — auch aus einem anderen Browser."""
|
|
ich = besitzer(session_id)
|
|
with connect() as conn:
|
|
row = conn.execute(
|
|
f"SELECT * FROM suggestions WHERE {WEM}", (ich, ich, session_id)
|
|
).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def anlegen(session_id, movie, name=None):
|
|
"""Gibt den Vorschlag zurück, oder None wenn die Sitzung schon einen hat."""
|
|
eintrag = {
|
|
"session_id": session_id,
|
|
"user_id": besitzer(session_id),
|
|
"movie_id": movie["id"],
|
|
"title": movie["title"],
|
|
"year": movie.get("year"),
|
|
"poster_path": movie.get("poster_path"),
|
|
"name": (name or "").strip()[:40] or None,
|
|
"created_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
}
|
|
try:
|
|
with connect() as conn:
|
|
conn.execute(
|
|
"INSERT INTO suggestions "
|
|
"(session_id, user_id, movie_id, title, year, poster_path, "
|
|
"name, created_at) "
|
|
"VALUES (:session_id, :user_id, :movie_id, :title, :year, "
|
|
":poster_path, :name, :created_at)",
|
|
eintrag,
|
|
)
|
|
except sqlite3.IntegrityError:
|
|
return None
|
|
return von_session(session_id)
|
|
|
|
|
|
def ersetzen(session_id, movie, name=None):
|
|
"""Wer schon vorgeschlagen hat, darf seinen Vorschlag ändern."""
|
|
loeschen(session_id)
|
|
return anlegen(session_id, movie, name)
|
|
|
|
|
|
def _versatz():
|
|
"""Wo genau im Poster die Marke stehen bleibt — nie exakt mittig.
|
|
Jedes zweite Mal ganz knapp an der Kante: fast der andere."""
|
|
if random.random() < 0.5:
|
|
return round(random.choice((-1, 1)) * random.uniform(0.34, 0.46), 3)
|
|
return round(random.uniform(-0.3, 0.3), 3)
|
|
|
|
|
|
def ziehen():
|
|
"""Server lost aus, damit alle Seiten dasselbe Ergebnis sehen.
|
|
|
|
Der Streifen gehört dazu: sonst zeigt jede Seite andere Poster
|
|
vorbeirauschen, und der Treffer stünde am Ende woanders.
|
|
"""
|
|
eintraege = alle()
|
|
if not eintraege:
|
|
return None
|
|
|
|
# Schon gelaufene Filme überspringen. Sind alle Vorschläge gesehen,
|
|
# bleibt es bei der vollen Auswahl — sonst gäbe es keinen Gewinner.
|
|
with connect() as conn:
|
|
gesehen = {
|
|
r["movie_id"] for r in conn.execute("SELECT movie_id FROM watched")
|
|
}
|
|
frisch = [i for i, e in enumerate(eintraege) if e["movie_id"] not in gesehen]
|
|
treffer = random.choice(frisch) if frisch else random.randrange(len(eintraege))
|
|
|
|
# Blockweise gemischt: jeder Vorschlag kommt gleich oft vorbei
|
|
streifen = []
|
|
while len(streifen) < STREIFEN_LAENGE:
|
|
block = list(range(len(eintraege)))
|
|
random.shuffle(block)
|
|
streifen.extend(block)
|
|
streifen = streifen[:STREIFEN_LAENGE]
|
|
|
|
# Wo die Bahn hält — beim Bruch zweimal, und erst der zweite Halt zählt
|
|
stopps = [{"position": STREIFEN_ZIEL, "versatz": _versatz()}]
|
|
ziel = STREIFEN_ZIEL
|
|
if random.random() < BRUCH_ANTEIL:
|
|
ziel = STREIFEN_ZIEL - random.randint(1, 4)
|
|
stopps.append({"position": ziel, "versatz": _versatz()})
|
|
# Der erste Halt zeigt einen anderen — sonst wäre der Bruch egal
|
|
andere = [i for i in range(len(eintraege)) if i != treffer]
|
|
streifen[STREIFEN_ZIEL] = random.choice(andere) if andere else treffer
|
|
streifen[ziel] = treffer
|
|
|
|
def karte(e):
|
|
return {k: v for k, v in e.items() if k not in ("session_id", "user_id")}
|
|
|
|
return {
|
|
"index": treffer,
|
|
"streifen": streifen,
|
|
"stopps": stopps,
|
|
"dauer": DREH_DAUER + (BRUCH_DAUER if len(stopps) > 1 else 0),
|
|
# Der Streifen zeigt diesen Stand, auch wenn danach jemand einen
|
|
# Vorschlag dazutut
|
|
"karten": [karte(e) for e in eintraege],
|
|
"gewinner": karte(eintraege[treffer]),
|
|
}
|
|
|
|
|
|
def loeschen(session_id):
|
|
ich = besitzer(session_id)
|
|
with connect() as conn:
|
|
return conn.execute(
|
|
f"DELETE FROM suggestions WHERE {WEM}", (ich, ich, session_id)
|
|
).rowcount
|
|
|
|
|
|
def alles_loeschen():
|
|
"""Host setzt die Runde zurück."""
|
|
with connect() as conn:
|
|
conn.execute("DELETE FROM dabei")
|
|
return conn.execute("DELETE FROM suggestions").rowcount
|
|
|
|
|
|
def dabei_umschalten(session_id):
|
|
"""Dabei ohne Vorschlag — der zweite Klick nimmt es zurück.
|
|
Gibt zurück, ob es jetzt gesetzt ist."""
|
|
ich = besitzer(session_id)
|
|
with connect() as conn:
|
|
weg = conn.execute(
|
|
f"DELETE FROM dabei WHERE {WEM}", (ich, ich, session_id)
|
|
).rowcount
|
|
if weg:
|
|
return False
|
|
conn.execute(
|
|
"INSERT INTO dabei (session_id, user_id, created_at) VALUES (?, ?, ?)",
|
|
(session_id, ich, datetime.now(timezone.utc).isoformat(timespec="seconds")),
|
|
)
|
|
return True
|
|
|
|
|
|
def dabei_alle():
|
|
"""Wer sich ohne Vorschlag eingetragen hat — Namen und Kennungen."""
|
|
with connect() as conn:
|
|
rows = conn.execute(
|
|
"SELECT d.*, u.name FROM dabei d LEFT JOIN users u ON u.id = d.user_id "
|
|
"ORDER BY d.created_at ASC"
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
def teilnehmer():
|
|
"""user_ids aller Anwesenden: Vorschlagende und Eingetragene."""
|
|
with connect() as conn:
|
|
ids = {r["user_id"] for r in conn.execute("SELECT user_id FROM suggestions")}
|
|
ids |= {r["user_id"] for r in conn.execute("SELECT user_id FROM dabei")}
|
|
return sorted(i for i in ids if i is not None)
|