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

View File

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

View File

@@ -14,13 +14,15 @@ import sqlite3
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from . import users
from .db import connect from .db import connect
from .users import besitzer from .users import WEM, besitzer
SCHEMA = """ SCHEMA = """
CREATE TABLE IF NOT EXISTS suggestions ( CREATE TABLE IF NOT EXISTS suggestions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL, session_id TEXT NOT NULL,
user_id INTEGER,
movie_id INTEGER NOT NULL, movie_id INTEGER NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
year INTEGER, year INTEGER,
@@ -38,67 +40,11 @@ COOKIE = "filmabend_sid"
DREH_DAUER = 4.2 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(): def init():
with connect() as conn: with connect() as conn:
conn.executescript(SCHEMA) conn.executescript(SCHEMA)
_spalte_user_id(conn, "suggestions") # Ein Vorschlag je Person
_unique_loesen(conn) users.je_person(conn, "suggestions", SCHEMA)
conn.executescript(INDIZES)
def neue_session(): 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): def von_session(session_id):
"""Der eigene Vorschlag — auch aus einem anderen Browser.""" """Der eigene Vorschlag — auch aus einem anderen Browser."""
ich = besitzer(session_id) ich = besitzer(session_id)
@@ -191,7 +133,9 @@ def ziehen():
"winkel": winkel, "winkel": winkel,
"dauer": DREH_DAUER, "dauer": DREH_DAUER,
"gewinner": { "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), "anzahl": len(eintraege),
} }

View File

@@ -128,6 +128,63 @@ def gehoert_mir(zeile, session_id, user_id):
return zeile["session_id"] == session_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): def name_von(session_id):
"""Nur der Name, für Vorschläge und Wertungen.""" """Nur der Name, für Vorschläge und Wertungen."""
benutzer = aktuell(session_id) 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 datetime import datetime, timezone
from . import users
from .db import connect from .db import connect
from .users import besitzer, gehoert_mir from .users import WEM, besitzer, gehoert_mir
SCHEMA = """ SCHEMA = """
CREATE TABLE IF NOT EXISTS watched ( CREATE TABLE IF NOT EXISTS watched (
@@ -23,38 +24,28 @@ CREATE TABLE IF NOT EXISTS watched (
watched_at TEXT NOT NULL, watched_at TEXT NOT NULL,
hidden INTEGER NOT NULL DEFAULT 0 hidden INTEGER NOT NULL DEFAULT 0
); );
"""
RATINGS = """
CREATE TABLE IF NOT EXISTS ratings ( CREATE TABLE IF NOT EXISTS ratings (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
watched_id INTEGER NOT NULL, watched_id INTEGER NOT NULL,
session_id TEXT NOT NULL, session_id TEXT NOT NULL,
user_id INTEGER,
stars INTEGER NOT NULL, stars INTEGER NOT NULL,
name TEXT, name TEXT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL
UNIQUE (watched_id, session_id)
); );
""" """
MAX_STERNE = 5 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(): def init():
with connect() as conn: with connect() as conn:
conn.executescript(SCHEMA) conn.executescript(SCHEMA + RATINGS)
_spalte_user_id(conn, "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 # Die Tabelle stand vor der Spalte schon in bestehenden Datenbanken
spalten = {r["name"] for r in conn.execute("PRAGMA table_info(watched)")} spalten = {r["name"] for r in conn.execute("PRAGMA table_info(watched)")}
if "hidden" not in spalten: if "hidden" not in spalten:
@@ -165,21 +156,16 @@ def bewerten(watched_id, session_id, stars, name=None):
).fetchone() ).fetchone()
if not da: if not da:
return None return None
# Eine Stimme je Person — die eigene alte fliegt vorher raus. # Eine Stimme je Person — die eigene alte fliegt vorher raus
# Mit Namen zählt der Name, ohne Namen die Sitzung.
ich = besitzer(session_id) ich = besitzer(session_id)
conn.execute( conn.execute(
"DELETE FROM ratings WHERE watched_id = ? AND " f"DELETE FROM ratings WHERE watched_id = ? AND {WEM}",
"(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))",
(watched_id, ich, ich, session_id), (watched_id, ich, ich, session_id),
) )
conn.execute( conn.execute(
"INSERT INTO ratings " "INSERT INTO ratings "
"(watched_id, session_id, user_id, stars, name, created_at) " "(watched_id, session_id, user_id, stars, name, created_at) "
"VALUES (?, ?, ?, ?, ?, ?) " "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",
( (
watched_id, watched_id,
session_id, session_id,

View File

@@ -12,40 +12,29 @@ import json
import sqlite3 import sqlite3
from datetime import datetime, timezone from datetime import datetime, timezone
from . import users
from .db import connect from .db import connect
from .users import besitzer, gehoert_mir from .users import WEM, besitzer, gehoert_mir
SCHEMA = """ SCHEMA = """
CREATE TABLE IF NOT EXISTS wishlist ( CREATE TABLE IF NOT EXISTS wishlist (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL, session_id TEXT NOT NULL,
user_id INTEGER,
movie_id INTEGER NOT NULL, movie_id INTEGER NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
year INTEGER, year INTEGER,
poster_path TEXT, poster_path TEXT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL
UNIQUE (session_id, movie_id)
); );
""" """
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(): def init():
with connect() as conn: with connect() as conn:
conn.executescript(SCHEMA) 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): def alle(session_id):
@@ -55,8 +44,7 @@ def alle(session_id):
# Genres kommen aus dem Katalog. Ein Film, der nie darin stand, # Genres kommen aus dem Katalog. Ein Film, der nie darin stand,
# hat keine — das Rad behandelt ihn dann als "nicht Horror". # hat keine — das Rad behandelt ihn dann als "nicht Horror".
"SELECT w.*, u.name, m.genres FROM wishlist w " "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 = w.user_id "
"LEFT JOIN users u ON u.id = s.user_id "
"LEFT JOIN movies m ON m.id = w.movie_id " "LEFT JOIN movies m ON m.id = w.movie_id "
"ORDER BY w.created_at DESC" "ORDER BY w.created_at DESC"
).fetchall() ).fetchall()
@@ -98,10 +86,6 @@ def merken(session_id, movie):
return alle(session_id) 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): def vergessen(session_id, movie_id):
ich = besitzer(session_id) ich = besitzer(session_id)
with connect() as conn: with connect() as conn:

View File

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

View File

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