445 lines
16 KiB
Python
445 lines
16 KiB
Python
"""Bereits geschaute Filme — das Buch der Abende.
|
|
|
|
Der Host trägt ein, was am Abend lief. Jeder Film bekommt eine Seite mit
|
|
zwei Teilen. Links die Momentaufnahme: eine Wertung je Person, nur Sterne,
|
|
so wie es direkt nach dem Abspann war. Rechts die Diskussion: Kommentare,
|
|
auf die man antworten kann, so tief man will.
|
|
|
|
Titel und Poster werden mitgespeichert, damit ein Eintrag lesbar bleibt,
|
|
auch wenn der Film später aus dem Katalog fällt. Szenenbild und Laufzeit
|
|
kommen aus dem Katalog.
|
|
"""
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from . import users
|
|
from .db import connect
|
|
from .users import WEM, besitzer, gehoert_mir
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS watched (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
movie_id INTEGER NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
year INTEGER,
|
|
poster_path TEXT,
|
|
watched_at TEXT NOT NULL,
|
|
hidden INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
"""
|
|
|
|
RATINGS = """
|
|
CREATE TABLE IF NOT EXISTS ratings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
watched_id INTEGER NOT NULL,
|
|
session_id TEXT NOT NULL,
|
|
user_id INTEGER,
|
|
stars INTEGER NOT NULL,
|
|
name TEXT,
|
|
text TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
# Die Diskussion: ein Kommentar zum Abend, oder mit parent_id eine
|
|
# Antwort auf einen anderen Kommentar — beliebig tief
|
|
NOTES = """
|
|
CREATE TABLE IF NOT EXISTS watched_notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
watched_id INTEGER NOT NULL,
|
|
parent_id INTEGER,
|
|
session_id TEXT NOT NULL,
|
|
user_id INTEGER,
|
|
text TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
# Ein Herz je Person und Kommentar: "sehe ich auch so". Alle sehen die
|
|
# Zahl, wer es gegeben hat, sieht nur der Host.
|
|
HEARTS = """
|
|
CREATE TABLE IF NOT EXISTS watched_hearts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
note_id INTEGER NOT NULL,
|
|
session_id TEXT NOT NULL,
|
|
user_id INTEGER,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
# Wer am Abend dabei war — kommt aus der Runde am Rad, sobald der Host
|
|
# den gezogenen Film einträgt. Daraus rechnen sich die Punkte.
|
|
DABEI = """
|
|
CREATE TABLE IF NOT EXISTS watched_dabei (
|
|
watched_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
PRIMARY KEY (watched_id, user_id)
|
|
);
|
|
"""
|
|
|
|
MAX_STERNE = 5
|
|
# Manche schreiben eine ganze Rezension — rund 600 Wörter passen hinein
|
|
MAX_KOMMENTAR = 4000
|
|
|
|
# Was das Gästebuch aus dem Katalog dazuholt
|
|
AUS_KATALOG = ("backdrop_path", "images", "runtime", "genres")
|
|
|
|
|
|
def init():
|
|
with connect() as conn:
|
|
conn.executescript(SCHEMA + RATINGS + NOTES + HEARTS + DABEI)
|
|
# Eine Stimme je Person und Film, ein Herz je Person und Kommentar
|
|
users.je_person(conn, "ratings", RATINGS, "watched_id")
|
|
users.je_person(conn, "watched_hearts", HEARTS, "note_id")
|
|
# Kurz hingen Antworten an der Wertung statt aneinander. Daten gab
|
|
# es dazu nie, die Spalte bekommt nur ihren neuen Namen.
|
|
spalten = {r["name"] for r in conn.execute("PRAGMA table_info(watched_notes)")}
|
|
if "rating_id" in spalten:
|
|
conn.execute("ALTER TABLE watched_notes RENAME COLUMN rating_id TO parent_id")
|
|
# Eine Weile stand ein Text an der Wertung. Seit die Sterne allein
|
|
# die Wertung sind, gehört er in die Diskussion — einmalig umziehen.
|
|
alte = conn.execute(
|
|
"SELECT * FROM ratings WHERE text IS NOT NULL AND text != ''"
|
|
).fetchall()
|
|
for r in alte:
|
|
conn.execute(
|
|
"INSERT INTO watched_notes "
|
|
"(watched_id, parent_id, session_id, user_id, text, created_at) "
|
|
"VALUES (?, NULL, ?, ?, ?, ?)",
|
|
(r["watched_id"], r["session_id"], r["user_id"], r["text"], r["created_at"]),
|
|
)
|
|
if alte:
|
|
conn.execute("UPDATE ratings SET text = NULL")
|
|
# Die Tabelle stand vor der Spalte schon in bestehenden Datenbanken
|
|
spalten = {r["name"] for r in conn.execute("PRAGMA table_info(watched)")}
|
|
if "hidden" not in spalten:
|
|
conn.execute(
|
|
"ALTER TABLE watched ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0"
|
|
)
|
|
# Ein Satz zur Wertung kam später dazu
|
|
spalten = {r["name"] for r in conn.execute("PRAGMA table_info(ratings)")}
|
|
if "text" not in spalten:
|
|
conn.execute("ALTER TABLE ratings ADD COLUMN text TEXT")
|
|
|
|
|
|
def _jetzt():
|
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
|
|
|
|
def alle(session_id, host=False):
|
|
"""Zuletzt geschaut zuerst, mit Schnitt, eigener Stimme, Kommentaren
|
|
und wer dabei war. Die laufende Nummer zählt vom ersten Abend.
|
|
|
|
Wer wie gewertet und wer ein Herz gegeben hat, sieht nur der Host —
|
|
für die Runde bleibt die Wertung anonym, das nimmt Hemmung."""
|
|
felder = ", ".join(f"m.{f}" for f in AUS_KATALOG)
|
|
with connect() as conn:
|
|
filme = conn.execute(
|
|
f"SELECT w.*, {felder} FROM watched w "
|
|
"LEFT JOIN movies m ON m.id = w.movie_id "
|
|
"ORDER BY w.watched_at DESC, w.id DESC"
|
|
).fetchall()
|
|
stimmen = conn.execute(
|
|
"SELECT id, watched_id, session_id, user_id, stars, name, created_at "
|
|
"FROM ratings ORDER BY created_at ASC"
|
|
).fetchall()
|
|
notizen = conn.execute(
|
|
"SELECT n.*, u.name FROM watched_notes n "
|
|
"LEFT JOIN users u ON u.id = n.user_id "
|
|
"ORDER BY n.created_at ASC"
|
|
).fetchall()
|
|
herzen = conn.execute(
|
|
"SELECT h.note_id, h.session_id, h.user_id, u.name FROM watched_hearts h "
|
|
"LEFT JOIN users u ON u.id = h.user_id ORDER BY h.created_at ASC"
|
|
).fetchall()
|
|
anwesend = conn.execute(
|
|
"SELECT d.watched_id, u.id, u.name FROM watched_dabei d "
|
|
"JOIN users u ON u.id = d.user_id ORDER BY u.name COLLATE NOCASE"
|
|
).fetchall()
|
|
|
|
ich = besitzer(session_id)
|
|
|
|
herz_je_notiz = {}
|
|
for h in herzen:
|
|
herz_je_notiz.setdefault(h["note_id"], []).append(h)
|
|
dabei_je_film = {}
|
|
for d in anwesend:
|
|
dabei_je_film.setdefault(d["watched_id"], []).append({"id": d["id"], "name": d["name"]})
|
|
|
|
def notiz(n):
|
|
meine_herzen = herz_je_notiz.get(n["id"], [])
|
|
return {
|
|
"id": n["id"],
|
|
"text": n["text"],
|
|
"name": n["name"],
|
|
"created_at": n["created_at"],
|
|
"mine": gehoert_mir(n, session_id, ich),
|
|
"replies": [],
|
|
"hearts": len(meine_herzen),
|
|
"hearted": any(gehoert_mir(h, session_id, ich) for h in meine_herzen),
|
|
"hearted_by": [h["name"] or "Anonym" for h in meine_herzen] if host else [],
|
|
}
|
|
|
|
# Die Kommentare eines Films als Baum: jede Antwort hängt sich an
|
|
# ihren Vater, was keinen hat, steht oben. Die Liste kommt nach
|
|
# Zeit sortiert, also ist der Vater immer schon da.
|
|
knoten = {n["id"]: notiz(n) for n in notizen}
|
|
kommentare = {}
|
|
for n in notizen:
|
|
k = knoten[n["id"]]
|
|
vater = knoten.get(n["parent_id"]) if n["parent_id"] is not None else None
|
|
if vater:
|
|
vater["replies"].append(k)
|
|
else:
|
|
kommentare.setdefault(n["watched_id"], []).append(k)
|
|
|
|
je_film = {}
|
|
for s in stimmen:
|
|
je_film.setdefault(s["watched_id"], []).append(s)
|
|
|
|
out = []
|
|
for nummer, film in zip(range(len(filme), 0, -1), filme):
|
|
eigene = je_film.get(film["id"], [])
|
|
werte = [s["stars"] for s in eigene]
|
|
meine = next((s for s in eigene if gehoert_mir(s, session_id, ich)), None)
|
|
daten = dict(film)
|
|
daten["images"] = json.loads(daten["images"]) if daten["images"] else []
|
|
daten["genres"] = json.loads(daten["genres"]) if daten["genres"] else []
|
|
out.append(
|
|
{
|
|
**daten,
|
|
"nummer": nummer,
|
|
"ratings": [
|
|
{
|
|
"id": s["id"],
|
|
"stars": s["stars"],
|
|
"name": s["name"],
|
|
"created_at": s["created_at"],
|
|
"mine": gehoert_mir(s, session_id, ich),
|
|
}
|
|
for s in eigene
|
|
]
|
|
if host
|
|
else [],
|
|
"dabei": dabei_je_film.get(film["id"], []),
|
|
"notes": kommentare.get(film["id"], []),
|
|
"average": round(sum(werte) / len(werte), 1) if werte else None,
|
|
"count": len(werte),
|
|
"mine": meine["stars"] if meine else None,
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def juengster():
|
|
"""Der zuletzt geschaute Abend — für Marken ohne Dreh."""
|
|
with connect() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM watched ORDER BY watched_at DESC, id DESC LIMIT 1"
|
|
).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def von_film(movie_id):
|
|
with connect() as conn:
|
|
row = conn.execute("SELECT * FROM watched WHERE movie_id = ?", (movie_id,)).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
|
|
def markieren(movie):
|
|
"""Film als geschaut eintragen. Ein zweiter Klick ändert nichts."""
|
|
with connect() as conn:
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO watched "
|
|
"(movie_id, title, year, poster_path, watched_at) "
|
|
"VALUES (?, ?, ?, ?, ?)",
|
|
(
|
|
movie["id"],
|
|
movie["title"],
|
|
movie.get("year"),
|
|
movie.get("poster_path"),
|
|
_jetzt(),
|
|
),
|
|
)
|
|
row = conn.execute(
|
|
"SELECT * FROM watched WHERE movie_id = ?", (movie["id"],)
|
|
).fetchone()
|
|
return dict(row)
|
|
|
|
|
|
def dabei_setzen(watched_id, user_ids):
|
|
"""Die Anwesenheit eines Abends — ersetzt die bisherige Liste."""
|
|
with connect() as conn:
|
|
conn.execute("DELETE FROM watched_dabei WHERE watched_id = ?", (watched_id,))
|
|
conn.executemany(
|
|
"INSERT OR IGNORE INTO watched_dabei (watched_id, user_id) VALUES (?, ?)",
|
|
[(watched_id, i) for i in user_ids],
|
|
)
|
|
|
|
|
|
def _herzen_weg(conn, note_ids):
|
|
conn.executemany(
|
|
"DELETE FROM watched_hearts WHERE note_id = ?", [(i,) for i in note_ids]
|
|
)
|
|
|
|
|
|
def entfernen(watched_id):
|
|
"""Samt Wertungen, Kommentaren, Herzen und Anwesenheit."""
|
|
with connect() as conn:
|
|
notizen = [
|
|
r["id"]
|
|
for r in conn.execute("SELECT id FROM watched_notes WHERE watched_id = ?", (watched_id,))
|
|
]
|
|
_herzen_weg(conn, notizen)
|
|
conn.execute("DELETE FROM watched_dabei WHERE watched_id = ?", (watched_id,))
|
|
conn.execute("DELETE FROM watched_notes WHERE watched_id = ?", (watched_id,))
|
|
conn.execute("DELETE FROM ratings WHERE watched_id = ?", (watched_id,))
|
|
return conn.execute(
|
|
"DELETE FROM watched WHERE id = ?", (watched_id,)
|
|
).rowcount
|
|
|
|
|
|
def datum_setzen(watched_id, tag):
|
|
"""tag im Format JJJJ-MM-TT. Mittags in UTC, damit die Anzeige in
|
|
keiner Zeitzone auf den Nachbartag rutscht."""
|
|
with connect() as conn:
|
|
return conn.execute(
|
|
"UPDATE watched SET watched_at = ? WHERE id = ?",
|
|
(f"{tag}T12:00:00+00:00", watched_id),
|
|
).rowcount
|
|
|
|
|
|
def sichtbarkeit(watched_id, versteckt):
|
|
"""Versteckte Filme sieht nur der Host."""
|
|
with connect() as conn:
|
|
return conn.execute(
|
|
"UPDATE watched SET hidden = ? WHERE id = ?",
|
|
(1 if versteckt else 0, watched_id),
|
|
).rowcount
|
|
|
|
|
|
def bewerten(watched_id, session_id, stars, name=None):
|
|
"""Sterne setzen. Gibt None zurück, wenn der Film nicht in der Liste
|
|
steht."""
|
|
with connect() as conn:
|
|
da = conn.execute(
|
|
"SELECT 1 FROM watched WHERE id = ?", (watched_id,)
|
|
).fetchone()
|
|
if not da:
|
|
return None
|
|
ich = besitzer(session_id)
|
|
# Eine Stimme je Person — die eigene alte fliegt vorher raus
|
|
conn.execute(
|
|
f"DELETE FROM ratings WHERE watched_id = ? AND {WEM}",
|
|
(watched_id, ich, ich, session_id),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO ratings "
|
|
"(watched_id, session_id, user_id, stars, name, created_at) "
|
|
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
(watched_id, session_id, ich, stars, (name or "").strip()[:40] or None, _jetzt()),
|
|
)
|
|
return {"watched_id": watched_id, "stars": stars}
|
|
|
|
|
|
def stimme_loeschen(rating_id):
|
|
"""Der Host räumt eine einzelne Wertung weg."""
|
|
with connect() as conn:
|
|
return conn.execute(
|
|
"DELETE FROM ratings WHERE id = ?", (rating_id,)
|
|
).rowcount
|
|
|
|
|
|
def anmerken(watched_id, session_id, text, parent_id=None):
|
|
"""Kommentar zum Abend, oder mit parent_id eine Antwort auf einen
|
|
anderen. Gibt None zurück, wenn Film oder Vater fehlen."""
|
|
text = (text or "").strip()[:MAX_KOMMENTAR]
|
|
if not text:
|
|
return None
|
|
with connect() as conn:
|
|
da = conn.execute(
|
|
"SELECT 1 FROM watched WHERE id = ?", (watched_id,)
|
|
).fetchone()
|
|
if not da:
|
|
return None
|
|
if parent_id is not None:
|
|
passt = conn.execute(
|
|
"SELECT 1 FROM watched_notes WHERE id = ? AND watched_id = ?",
|
|
(parent_id, watched_id),
|
|
).fetchone()
|
|
if not passt:
|
|
return None
|
|
cur = conn.execute(
|
|
"INSERT INTO watched_notes "
|
|
"(watched_id, parent_id, session_id, user_id, text, created_at) "
|
|
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
(watched_id, parent_id, session_id, besitzer(session_id), text, _jetzt()),
|
|
)
|
|
return cur.lastrowid
|
|
|
|
|
|
def _nachkommen(conn, note_id):
|
|
"""Alle Antworten unter einem Kommentar, beliebig tief."""
|
|
gefunden, offen = [], [note_id]
|
|
while offen:
|
|
kinder = [
|
|
r["id"]
|
|
for r in conn.execute(
|
|
f"SELECT id FROM watched_notes WHERE parent_id IN "
|
|
f"({','.join('?' * len(offen))})",
|
|
offen,
|
|
)
|
|
]
|
|
gefunden.extend(kinder)
|
|
offen = kinder
|
|
return gefunden
|
|
|
|
|
|
def anmerkung_loeschen(note_id, session_id, host):
|
|
"""Der Verfasser und der Host dürfen weg damit — samt allem, was
|
|
darunter hängt. Eine Antwort ohne ihren Anlass ergäbe keinen Sinn."""
|
|
ich = besitzer(session_id)
|
|
with connect() as conn:
|
|
zeile = conn.execute(
|
|
"SELECT * FROM watched_notes WHERE id = ?", (note_id,)
|
|
).fetchone()
|
|
if not zeile:
|
|
return 0
|
|
if not host and not gehoert_mir(zeile, session_id, ich):
|
|
return 0
|
|
alle_ids = [note_id, *_nachkommen(conn, note_id)]
|
|
_herzen_weg(conn, alle_ids)
|
|
conn.executemany(
|
|
"DELETE FROM watched_notes WHERE id = ?", [(i,) for i in alle_ids]
|
|
)
|
|
return len(alle_ids)
|
|
|
|
|
|
def herz(session_id, note_id):
|
|
"""Herz setzen oder zurücknehmen. None, wenn es den Kommentar nicht
|
|
gibt, sonst ob das Herz jetzt steht."""
|
|
ich = besitzer(session_id)
|
|
with connect() as conn:
|
|
weg = conn.execute(
|
|
f"DELETE FROM watched_hearts WHERE note_id = ? AND {WEM}",
|
|
(note_id, ich, ich, session_id),
|
|
).rowcount
|
|
if weg:
|
|
return False
|
|
da = conn.execute("SELECT 1 FROM watched_notes WHERE id = ?", (note_id,)).fetchone()
|
|
if not da:
|
|
return None
|
|
conn.execute(
|
|
"INSERT INTO watched_hearts (note_id, session_id, user_id, created_at) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
(note_id, session_id, ich, _jetzt()),
|
|
)
|
|
return True
|
|
|
|
|
|
def ids():
|
|
"""movie_ids der geschauten Filme, für die Detailansicht."""
|
|
with connect() as conn:
|
|
return [r["movie_id"] for r in conn.execute("SELECT movie_id FROM watched")]
|