This commit is contained in:
team3
2026-09-09 18:15:10 +02:00
parent 847b747870
commit 3dc091d04a
8 changed files with 103 additions and 155 deletions

View File

@@ -6,6 +6,7 @@ Jeder darf aufschreiben, was er sich wünscht. Alle sehen alle Wünsche,
from datetime import datetime, timezone
from . import users
from .db import connect
from .users import besitzer, gehoert_mir
@@ -13,6 +14,7 @@ SCHEMA = """
CREATE TABLE IF NOT EXISTS features (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
user_id INTEGER,
text TEXT NOT NULL,
created_at TEXT NOT NULL
);
@@ -21,23 +23,10 @@ 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")
users.user_id_nachruesten(conn, "features")
def alle(session_id):
@@ -45,8 +34,7 @@ def alle(session_id):
with connect() as conn:
rows = conn.execute(
"SELECT f.*, u.name FROM features f "
"LEFT JOIN user_sessions s ON s.session_id = f.session_id "
"LEFT JOIN users u ON u.id = s.user_id "
"LEFT JOIN users u ON u.id = f.user_id "
"ORDER BY f.created_at DESC"
).fetchall()
ich = besitzer(session_id)

View File

@@ -80,9 +80,9 @@ async def guarded_sync():
@app.on_event("startup")
async def startup():
db.init()
users.init()
suggestions.init()
watched.init()
users.init()
wishlist.init()
features.init()
scheduler.add_job(guarded_sync, "cron", hour=4, minute=30, id="daily-sync")
@@ -294,13 +294,15 @@ def letzter():
def liste_vorschlaege(response: Response, filmabend_sid: str | None = Cookie(None)):
sid = sitzung(response, filmabend_sid)
eintraege = suggestions.alle()
ich = users.besitzer(sid)
return {
"items": [
{k: v for k, v in e.items() if k != "session_id"} for e in eintraege
{k: v for k, v in e.items() if k not in ("session_id", "user_id")}
for e in eintraege
],
# Damit die Oberfläche den eigenen Vorschlag hervorheben kann
"mine": next(
(e["id"] for e in eintraege if e["session_id"] == sid),
(e["id"] for e in eintraege if users.gehoert_mir(e, sid, ich)),
None,
),
}
@@ -317,7 +319,9 @@ async def vorschlagen(
# Ein zweiter Vorschlag ersetzt den ersten, statt abgelehnt zu werden
eintrag = suggestions.ersetzen(sid, movie, users.name_von(sid))
events.senden("suggestions")
return {k: v for k, v in eintrag.items() if k != "session_id"}
return {
k: v for k, v in eintrag.items() if k not in ("session_id", "user_id")
}
@app.delete("/api/suggestions")

View File

@@ -14,13 +14,15 @@ import sqlite3
import uuid
from datetime import datetime, timezone
from . import users
from .db import connect
from .users import besitzer
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,
@@ -38,67 +40,11 @@ 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)
# Ein Vorschlag je Person
users.je_person(conn, "suggestions", SCHEMA)
def neue_session():
@@ -123,10 +69,6 @@ def alle():
]
# 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)
@@ -191,7 +133,9 @@ def ziehen():
"winkel": winkel,
"dauer": DREH_DAUER,
"gewinner": {
k: v for k, v in eintraege[treffer].items() if k != "session_id"
k: v
for k, v in eintraege[treffer].items()
if k not in ("session_id", "user_id")
},
"anzahl": len(eintraege),
}

View File

@@ -128,6 +128,63 @@ def gehoert_mir(zeile, session_id, user_id):
return zeile["session_id"] == session_id
# Dasselbe als WHERE-Klausel. Parameter: (user_id, user_id, session_id)
WEM = "(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))"
def user_id_nachruesten(conn, tabelle):
"""Spalte ergänzen 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 je_person(conn, tabelle, schema, *schluessel):
"""Eindeutig je Person statt je Sitzung.
Früher stand das UNIQUE auf der Sitzung — dann blockierte der Eintrag
des einen Namens den des nächsten, sobald beide denselben Browser
benutzten. Mit Namen zählt jetzt der Name, ohne Namen die Sitzung.
Das alte UNIQUE steckt in der Spaltendefinition und lässt sich nicht
ändern — die Tabelle wird dafür nach `schema` neu gebaut. Hat ein Name
aus mehreren Browsern doppelt eingetragen, bleibt der jüngste Eintrag.
"""
user_id_nachruesten(conn, tabelle)
alt = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (tabelle,)
).fetchone()
if alt and "UNIQUE" in alt["sql"]:
spalten = ", ".join(
r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")
)
conn.executescript(
f"ALTER TABLE {tabelle} RENAME TO {tabelle}_alt;"
f"{schema}"
f"INSERT INTO {tabelle} ({spalten}) "
f"SELECT {spalten} FROM {tabelle}_alt;"
f"DROP TABLE {tabelle}_alt;"
)
mit_name = ", ".join((*schluessel, "user_id"))
ohne_name = ", ".join((*schluessel, "session_id"))
conn.execute(
f"DELETE FROM {tabelle} WHERE user_id IS NOT NULL AND id NOT IN "
f"(SELECT MAX(id) FROM {tabelle} GROUP BY {mit_name})"
)
conn.executescript(
f"CREATE UNIQUE INDEX IF NOT EXISTS {tabelle}_je_benutzer "
f"ON {tabelle} ({mit_name}) WHERE user_id IS NOT NULL;"
f"CREATE UNIQUE INDEX IF NOT EXISTS {tabelle}_je_sitzung "
f"ON {tabelle} ({ohne_name}) WHERE user_id IS NULL;"
)
def name_von(session_id):
"""Nur der Name, für Vorschläge und Wertungen."""
benutzer = aktuell(session_id)

View File

@@ -10,8 +10,9 @@ auch wenn der Film später aus dem Katalog fällt.
from datetime import datetime, timezone
from . import users
from .db import connect
from .users import besitzer, gehoert_mir
from .users import WEM, besitzer, gehoert_mir
SCHEMA = """
CREATE TABLE IF NOT EXISTS watched (
@@ -23,38 +24,28 @@ CREATE TABLE IF NOT EXISTS watched (
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,
created_at TEXT NOT NULL,
UNIQUE (watched_id, session_id)
created_at TEXT NOT NULL
);
"""
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")
conn.executescript(SCHEMA + RATINGS)
# Eine Stimme je Person und Film
users.je_person(conn, "ratings", RATINGS, "watched_id")
# 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:
@@ -165,21 +156,16 @@ def bewerten(watched_id, session_id, stars, name=None):
).fetchone()
if not da:
return None
# Eine Stimme je Person — die eigene alte fliegt vorher raus.
# Mit Namen zählt der Name, ohne Namen die Sitzung.
# Eine Stimme je Person — die eigene alte fliegt vorher raus
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 = ?))",
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 (?, ?, ?, ?, ?, ?) "
"ON CONFLICT (watched_id, session_id) DO UPDATE SET "
"user_id = excluded.user_id, stars = excluded.stars, "
"name = excluded.name, created_at = excluded.created_at",
"VALUES (?, ?, ?, ?, ?, ?)",
(
watched_id,
session_id,

View File

@@ -12,40 +12,29 @@ import json
import sqlite3
from datetime import datetime, timezone
from . import users
from .db import connect
from .users import besitzer, gehoert_mir
from .users import WEM, besitzer, gehoert_mir
SCHEMA = """
CREATE TABLE IF NOT EXISTS wishlist (
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,
created_at TEXT NOT NULL,
UNIQUE (session_id, movie_id)
created_at TEXT NOT NULL
);
"""
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")
# Jeder Film einmal je Person
users.je_person(conn, "wishlist", SCHEMA, "movie_id")
def alle(session_id):
@@ -55,8 +44,7 @@ def alle(session_id):
# 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 users u ON u.id = w.user_id "
"LEFT JOIN movies m ON m.id = w.movie_id "
"ORDER BY w.created_at DESC"
).fetchall()
@@ -98,10 +86,6 @@ 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:

View File

@@ -694,7 +694,8 @@ onUnmounted(() => {
v-else-if="activeTab === 'wheel'"
ref="rad"
:host="host"
:ich="ich"
:merkliste="merkliste"
:mine="meinVorschlag?.id ?? null"
@open="selected = $event"
@reset="meinVorschlag = null"
/>

View File

@@ -1,17 +1,17 @@
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { posterUrl } from '../images'
import { abonnieren } from '../live'
const props = defineProps({
host: Boolean,
// Wer gerade angemeldet ist. Beim Wechsel gehört eine andere Sammlung dazu.
ich: { type: Object, default: null },
// Beides hält die App schon aktuell — auch nach einem Namenswechsel
merkliste: { type: Array, default: () => [] },
mine: { type: Number, default: null },
})
const emit = defineEmits(['open', 'reset'])
const items = ref([])
const mine = ref(null)
const laden = ref(true)
const fehler = ref('')
@@ -91,7 +91,6 @@ async function laden_() {
if (!resp.ok) throw new Error('Vorschläge konnten nicht geladen werden')
const daten = await resp.json()
items.value = daten.items
mine.value = daten.mine
} catch (err) {
fehler.value = err.message
} finally {
@@ -99,14 +98,8 @@ async function laden_() {
}
}
watch(() => props.ich?.id, () => {
sammlungLaden()
laden_()
})
onMounted(async () => {
await laden_()
await sammlungLaden()
await laufendenDrehHolen()
verbinden()
})
@@ -197,18 +190,9 @@ async function zuruecksetzen() {
// Aus der eigenen Sammlung würfeln. Der Schalter trennt Horror von allem
// anderen — für Abende, an denen es keiner gruselig mag.
const nurHorror = ref(true)
const sammlung = ref([])
const sammlung = computed(() => props.merkliste.filter((e) => e.mine))
const wuerfelt = ref(false)
async function sammlungLaden() {
try {
const resp = await fetch('/api/wishlist')
if (resp.ok) sammlung.value = (await resp.json()).filter((e) => e.mine)
} catch {
sammlung.value = []
}
}
function nachGenre(horror) {
return sammlung.value.filter(
(e) => (e.genres || []).includes('Horror') === horror