This commit is contained in:
team3
2026-09-09 17:58:52 +02:00
parent 2bd03000e8
commit 847b747870
8 changed files with 419 additions and 55 deletions

View File

@@ -7,6 +7,7 @@ Jeder darf aufschreiben, was er sich wünscht. Alle sehen alle Wünsche,
from datetime import datetime, timezone
from .db import connect
from .users import besitzer, gehoert_mir
SCHEMA = """
CREATE TABLE IF NOT EXISTS features (
@@ -20,9 +21,23 @@ CREATE TABLE IF NOT EXISTS features (
MAX_LAENGE = 500
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, "features")
def alle(session_id):
@@ -34,10 +49,15 @@ def alle(session_id):
"LEFT JOIN users u ON u.id = s.user_id "
"ORDER BY f.created_at DESC"
).fetchall()
ich = besitzer(session_id)
return [
{
**{k: v for k, v in dict(r).items() if k != "session_id"},
"mine": r["session_id"] == session_id,
**{
k: v
for k, v in dict(r).items()
if k not in ("session_id", "user_id")
},
"mine": gehoert_mir(r, session_id, ich),
}
for r in rows
]
@@ -50,9 +70,11 @@ def anlegen(session_id, text):
return None
with connect() as conn:
conn.execute(
"INSERT INTO features (session_id, text, created_at) VALUES (?, ?, ?)",
"INSERT INTO features (session_id, user_id, text, created_at) "
"VALUES (?, ?, ?, ?)",
(
session_id,
besitzer(session_id),
text,
datetime.now(timezone.utc).isoformat(timespec="seconds"),
),

View File

@@ -15,11 +15,12 @@ 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 UNIQUE,
session_id TEXT NOT NULL,
movie_id INTEGER NOT NULL,
title TEXT NOT NULL,
year INTEGER,
@@ -37,9 +38,67 @@ COOKIE = "filmabend_sid"
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():
@@ -49,15 +108,31 @@ def neue_session():
def alle():
with connect() as conn:
rows = conn.execute(
"SELECT * FROM suggestions ORDER BY created_at ASC"
# 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 [dict(r) for r in rows]
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(
"SELECT * FROM suggestions WHERE session_id = ?", (session_id,)
f"SELECT * FROM suggestions WHERE {WEM}", (ich, ich, session_id)
).fetchone()
return dict(row) if row else None
@@ -66,6 +141,7 @@ 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"),
@@ -77,9 +153,10 @@ def anlegen(session_id, movie, name=None):
with connect() as conn:
conn.execute(
"INSERT INTO suggestions "
"(session_id, movie_id, title, year, poster_path, name, created_at) "
"VALUES (:session_id, :movie_id, :title, :year, :poster_path, "
":name, :created_at)",
"(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:
@@ -89,8 +166,7 @@ def anlegen(session_id, movie, name=None):
def ersetzen(session_id, movie, name=None):
"""Wer schon vorgeschlagen hat, darf seinen Vorschlag ändern."""
with connect() as conn:
conn.execute("DELETE FROM suggestions WHERE session_id = ?", (session_id,))
loeschen(session_id)
return anlegen(session_id, movie, name)
@@ -122,9 +198,10 @@ def ziehen():
def loeschen(session_id):
ich = besitzer(session_id)
with connect() as conn:
return conn.execute(
"DELETE FROM suggestions WHERE session_id = ?", (session_id,)
f"DELETE FROM suggestions WHERE {WEM}", (ich, ich, session_id)
).rowcount

View File

@@ -106,6 +106,28 @@ def aktuell(session_id):
return dict(row) if row else None
def besitzer(session_id):
"""Die user_id dieser Sitzung, oder None ohne gewählten Namen.
Sammlungen, Vorschläge und Wertungen werden damit beschriftet. Ein
Namenswechsel im selben Browser lässt die alten Einträge dann in Ruhe —
sie gehören weiter dem Namen, unter dem sie entstanden sind.
"""
with connect() as conn:
row = conn.execute(
"SELECT user_id FROM user_sessions WHERE session_id = ?",
(session_id,),
).fetchone()
return row["user_id"] if row else None
def gehoert_mir(zeile, session_id, user_id):
"""Ein Eintrag gehört mir, wenn der Name passt — sonst die Sitzung."""
if zeile["user_id"] is not None or user_id is not None:
return zeile["user_id"] == user_id
return zeile["session_id"] == session_id
def name_von(session_id):
"""Nur der Name, für Vorschläge und Wertungen."""
benutzer = aktuell(session_id)

View File

@@ -11,6 +11,7 @@ auch wenn der Film später aus dem Katalog fällt.
from datetime import datetime, timezone
from .db import connect
from .users import besitzer, gehoert_mir
SCHEMA = """
CREATE TABLE IF NOT EXISTS watched (
@@ -37,9 +38,23 @@ CREATE TABLE IF NOT EXISTS ratings (
MAX_STERNE = 5
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, "ratings")
# 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:
@@ -59,10 +74,11 @@ def alle(session_id):
"SELECT * FROM watched ORDER BY watched_at DESC, id DESC"
).fetchall()
stimmen = conn.execute(
"SELECT id, watched_id, session_id, stars, name FROM ratings "
"SELECT id, watched_id, session_id, user_id, stars, name FROM ratings "
"ORDER BY created_at ASC"
).fetchall()
ich = besitzer(session_id)
je_film = {}
for s in stimmen:
je_film.setdefault(s["watched_id"], []).append(s)
@@ -81,7 +97,11 @@ def alle(session_id):
"average": round(sum(werte) / len(werte), 1) if werte else None,
"count": len(werte),
"mine": next(
(s["stars"] for s in eigene if s["session_id"] == session_id),
(
s["stars"]
for s in eigene
if gehoert_mir(s, session_id, ich)
),
None,
),
}
@@ -145,17 +165,25 @@ def bewerten(watched_id, session_id, stars, name=None):
).fetchone()
if not da:
return None
# Eine Stimme je Sitzung — eine neue Bewertung ersetzt die alte
# Eine Stimme je Person — die eigene alte fliegt vorher raus.
# Mit Namen zählt der Name, ohne Namen die Sitzung.
ich = besitzer(session_id)
conn.execute(
"DELETE FROM ratings WHERE watched_id = ? AND "
"(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))",
(watched_id, ich, ich, session_id),
)
conn.execute(
"INSERT INTO ratings "
"(watched_id, session_id, stars, name, created_at) "
"VALUES (?, ?, ?, ?, ?) "
"(watched_id, session_id, user_id, stars, name, created_at) "
"VALUES (?, ?, ?, ?, ?, ?) "
"ON CONFLICT (watched_id, session_id) DO UPDATE SET "
"stars = excluded.stars, name = excluded.name, "
"created_at = excluded.created_at",
"user_id = excluded.user_id, stars = excluded.stars, "
"name = excluded.name, created_at = excluded.created_at",
(
watched_id,
session_id,
ich,
stars,
(name or "").strip()[:40] or None,
_jetzt(),

View File

@@ -8,10 +8,12 @@ Titel und Poster kommen mit in die Zeile. Ein gemerkter Film soll lesbar
bleiben, auch wenn er nie im Katalog stand oder Netflix ihn abgibt.
"""
import json
import sqlite3
from datetime import datetime, timezone
from .db import connect
from .users import besitzer, gehoert_mir
SCHEMA = """
CREATE TABLE IF NOT EXISTS wishlist (
@@ -27,24 +29,47 @@ CREATE TABLE IF NOT EXISTS wishlist (
"""
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, "wishlist")
def alle(session_id):
"""Alle Sammlungen, jede mit dem Namen ihres Besitzers."""
with connect() as conn:
rows = conn.execute(
"SELECT w.*, u.name FROM wishlist w "
# Genres kommen aus dem Katalog. Ein Film, der nie darin stand,
# hat keine — das Rad behandelt ihn dann als "nicht Horror".
"SELECT w.*, u.name, m.genres FROM wishlist w "
"LEFT JOIN user_sessions s ON s.session_id = w.session_id "
"LEFT JOIN users u ON u.id = s.user_id "
"LEFT JOIN movies m ON m.id = w.movie_id "
"ORDER BY w.created_at DESC"
).fetchall()
ich = besitzer(session_id)
return [
{
**{k: v for k, v in dict(r).items() if k != "session_id"},
"mine": r["session_id"] == session_id,
**{
k: v
for k, v in dict(r).items()
if k not in ("session_id", "user_id", "genres")
},
"genres": json.loads(r["genres"]) if r["genres"] else [],
"mine": gehoert_mir(r, session_id, ich),
}
for r in rows
]
@@ -56,10 +81,11 @@ def merken(session_id, movie):
with connect() as conn:
conn.execute(
"INSERT INTO wishlist "
"(session_id, movie_id, title, year, poster_path, created_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
"(session_id, user_id, movie_id, title, year, poster_path, "
"created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
(
session_id,
besitzer(session_id),
movie["id"],
movie["title"],
movie.get("year"),
@@ -72,20 +98,27 @@ def merken(session_id, movie):
return alle(session_id)
# 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 vergessen(session_id, movie_id):
ich = besitzer(session_id)
with connect() as conn:
return conn.execute(
"DELETE FROM wishlist WHERE session_id = ? AND movie_id = ?",
(session_id, movie_id),
f"DELETE FROM wishlist WHERE {WEM} AND movie_id = ?",
(ich, ich, session_id, movie_id),
).rowcount
def ids(session_id):
"""movie_ids der gemerkten Filme, für die Detailansicht."""
ich = besitzer(session_id)
with connect() as conn:
return [
r["movie_id"]
for r in conn.execute(
"SELECT movie_id FROM wishlist WHERE session_id = ?", (session_id,)
f"SELECT movie_id FROM wishlist WHERE {WEM}",
(ich, ich, session_id),
)
]