212 lines
6.5 KiB
Python
212 lines
6.5 KiB
Python
"""Filmvorschläge fürs Rad.
|
|
|
|
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 .db import connect
|
|
from .users import besitzer
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS suggestions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
session_id TEXT NOT NULL,
|
|
movie_id INTEGER NOT NULL,
|
|
title TEXT NOT NULL,
|
|
year INTEGER,
|
|
poster_path TEXT,
|
|
name TEXT,
|
|
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 = 4.2
|
|
|
|
|
|
# Ein Vorschlag je Person. Früher stand das UNIQUE auf der Sitzung — dann
|
|
# blockierte der Vorschlag des einen Namens den des nächsten, sobald beide
|
|
# denselben Browser benutzten.
|
|
INDIZES = """
|
|
CREATE UNIQUE INDEX IF NOT EXISTS suggestions_je_benutzer
|
|
ON suggestions (user_id) WHERE user_id IS NOT NULL;
|
|
CREATE UNIQUE INDEX IF NOT EXISTS suggestions_je_sitzung
|
|
ON suggestions (session_id) WHERE user_id IS NULL;
|
|
"""
|
|
|
|
|
|
def _unique_loesen(conn):
|
|
"""Das alte UNIQUE steckt in der Spaltendefinition — Tabelle neu bauen."""
|
|
sql = conn.execute(
|
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='suggestions'"
|
|
).fetchone()
|
|
if not sql or "UNIQUE" not in sql["sql"]:
|
|
return
|
|
conn.executescript(
|
|
"""
|
|
ALTER TABLE suggestions RENAME TO suggestions_alt;
|
|
CREATE TABLE 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
|
|
);
|
|
INSERT INTO suggestions
|
|
(id, session_id, user_id, movie_id, title, year, poster_path,
|
|
name, created_at)
|
|
SELECT id, session_id, user_id, movie_id, title, year, poster_path,
|
|
name, created_at FROM suggestions_alt;
|
|
DROP TABLE suggestions_alt;
|
|
"""
|
|
)
|
|
|
|
|
|
def _spalte_user_id(conn, tabelle):
|
|
"""Nachrüsten und aus der Sitzung füllen — einmalig je Datenbank."""
|
|
spalten = {r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")}
|
|
if "user_id" in spalten:
|
|
return
|
|
conn.execute(f"ALTER TABLE {tabelle} ADD COLUMN user_id INTEGER")
|
|
conn.execute(
|
|
f"UPDATE {tabelle} SET user_id = ("
|
|
" SELECT user_id FROM user_sessions s WHERE s.session_id = "
|
|
f" {tabelle}.session_id)"
|
|
)
|
|
|
|
|
|
def init():
|
|
with connect() as conn:
|
|
conn.executescript(SCHEMA)
|
|
_spalte_user_id(conn, "suggestions")
|
|
_unique_loesen(conn)
|
|
conn.executescript(INDIZES)
|
|
|
|
|
|
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
|
|
]
|
|
|
|
|
|
# Mit Namen zählt der Name, ohne Namen die Sitzung
|
|
WEM = "(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))"
|
|
|
|
|
|
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 ziehen():
|
|
"""Server lost aus, damit alle Seiten dasselbe Ergebnis sehen.
|
|
|
|
Der Zielwinkel gehört dazu: sonst dreht jede Seite woanders hin und
|
|
der Zeiger stünde am Ende auf einem anderen Segment.
|
|
"""
|
|
eintraege = alle()
|
|
if not eintraege:
|
|
return None
|
|
|
|
treffer = random.randrange(len(eintraege))
|
|
schritt = 360 / len(eintraege)
|
|
runden = 5 + random.randrange(3)
|
|
# Der Zeiger steht oben, also muss die Mitte des Zielsegments dorthin
|
|
winkel = runden * 360 + (360 - (treffer * schritt + schritt / 2))
|
|
|
|
return {
|
|
"index": treffer,
|
|
"winkel": winkel,
|
|
"dauer": DREH_DAUER,
|
|
"gewinner": {
|
|
k: v for k, v in eintraege[treffer].items() if k != "session_id"
|
|
},
|
|
"anzahl": len(eintraege),
|
|
}
|
|
|
|
|
|
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:
|
|
return conn.execute("DELETE FROM suggestions").rowcount
|