update
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import secrets
|
||||
import time
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
@@ -22,7 +23,7 @@ from . import (
|
||||
watched,
|
||||
wishlist,
|
||||
)
|
||||
from .config import IMAGE_BASE, STATIC_DIR
|
||||
from .config import HOST_SCHLUESSEL, IMAGE_BASE, STATIC_DIR
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
# Ein einzelner Zuschauer erzeugt gut zwanzig Stream-Anfragen je Sekunde.
|
||||
@@ -93,12 +94,19 @@ async def startup():
|
||||
features.init()
|
||||
# Erst die Sicherung, dann der Sync — so liegt vor jedem Katalog-Update
|
||||
# ein sauberer Stand
|
||||
if not HOST_SCHLUESSEL:
|
||||
log.warning(
|
||||
"HOST_SCHLUESSEL fehlt in der .env — der Hostmodus bleibt zu."
|
||||
)
|
||||
scheduler.add_job(sicherung, "cron", hour=4, minute=0, id="daily-backup")
|
||||
scheduler.add_job(guarded_sync, "cron", hour=4, minute=30, id="daily-sync")
|
||||
scheduler.start()
|
||||
if not db.get_meta("last_sync"):
|
||||
log.info("Leere Datenbank, starte ersten Sync im Hintergrund")
|
||||
asyncio.create_task(guarded_sync())
|
||||
else:
|
||||
# Altbestand: gemerkte Filme, die noch keine Katalogdaten haben
|
||||
asyncio.create_task(sync.katalog_nachtragen())
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
@@ -364,14 +372,21 @@ def vorschlag_loeschen(response: Response, filmabend_sid: str | None = Cookie(No
|
||||
|
||||
|
||||
async def film_holen(movie_id: int):
|
||||
"""Erst der Katalog, dann TMDB — Suchtreffer stehen nicht in der DB."""
|
||||
"""Erst der Katalog, dann TMDB — Suchtreffer stehen nicht in der DB.
|
||||
|
||||
Was von TMDB kommt, landet gleich im Katalog. Sonst blieben die Karten
|
||||
in Sammlung, Rad und Gesehen leer: die lesen ihre Daten von dort.
|
||||
"""
|
||||
movie = db.get_movie(movie_id)
|
||||
if movie:
|
||||
return movie
|
||||
try:
|
||||
return await search.detail(movie_id)
|
||||
movie = await search.detail(movie_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail="Film nicht gefunden")
|
||||
with db.connect() as conn:
|
||||
db.upsert(conn, movie)
|
||||
return movie
|
||||
|
||||
|
||||
@app.get("/api/features")
|
||||
@@ -493,7 +508,11 @@ def vergessen(
|
||||
@app.get("/api/users")
|
||||
def runde(response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
return {"items": users.alle(), "mine": users.aktuell(sid)}
|
||||
return {
|
||||
"items": users.alle(),
|
||||
"mine": users.aktuell(sid),
|
||||
"host": users.ist_host(sid),
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/users")
|
||||
@@ -514,17 +533,116 @@ def name_entfernen(user_id: int):
|
||||
return {"removed": 1}
|
||||
|
||||
|
||||
@app.get("/api/users/{user_id}/schutz")
|
||||
async def schutz_zeigen(
|
||||
user_id: int,
|
||||
response: Response,
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Welcher Film hinterlegt ist — nur für den Host und den Namen selbst.
|
||||
|
||||
Wer ihn ändern darf, darf ihn auch sehen: sonst müsste man raten, was
|
||||
man gerade überschreibt.
|
||||
"""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not users.ist_host(sid) and users.besitzer(sid) != user_id:
|
||||
raise HTTPException(status_code=403, detail="Nicht dein Name")
|
||||
movie_id = users.schutz_von(user_id)
|
||||
if movie_id is None:
|
||||
return {"film": None}
|
||||
film = await film_holen(movie_id)
|
||||
return {
|
||||
"film": {
|
||||
"id": film["id"],
|
||||
"title": film["title"],
|
||||
"year": film.get("year"),
|
||||
"poster_path": film.get("poster_path"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/users/{user_id}/schutz")
|
||||
def schutz_setzen(
|
||||
user_id: int,
|
||||
response: Response,
|
||||
movie_id: int | None = Body(None, embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Anmeldefilm hinterlegen — oder mit movie_id null wieder freigeben.
|
||||
|
||||
Der Host pflegt die Runde, sonst darf jeder nur den eigenen Namen
|
||||
ändern. Wer angemeldet ist, hat den alten Film ohnehin gewusst.
|
||||
"""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not users.ist_host(sid) and users.besitzer(sid) != user_id:
|
||||
raise HTTPException(status_code=403, detail="Nicht dein Name")
|
||||
if not users.schutz_setzen(user_id, movie_id):
|
||||
raise HTTPException(status_code=404, detail="Unbekannter Name")
|
||||
events.senden("users")
|
||||
return {"geschuetzt": movie_id is not None}
|
||||
|
||||
|
||||
@app.get("/api/host")
|
||||
def host_stand(response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
return {"host": users.ist_host(sid)}
|
||||
|
||||
|
||||
@app.post("/api/host")
|
||||
def host_modus(
|
||||
response: Response,
|
||||
an: bool = Body(..., embed=True),
|
||||
schluessel: str | None = Body(None, embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Die Sitzung als Host freischalten — oder die Rechte abgeben.
|
||||
|
||||
Freischalten geht nur mit dem Schlüssel aus der .env, einmal über
|
||||
/?host=<schlüssel>. Ohne das käme jeder mit der Tastenkombination an
|
||||
den Anmeldefilmen vorbei.
|
||||
|
||||
Die Freischaltung bleibt, bis jemand sie abgibt. Ob die Hostknöpfe
|
||||
gerade sichtbar sind, entscheidet der Browser für sich — dafür ist
|
||||
diese Route nicht zuständig.
|
||||
"""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not an:
|
||||
return {"host": users.host_setzen(sid, False)}
|
||||
|
||||
darf = users.ist_host(sid) or (
|
||||
bool(HOST_SCHLUESSEL) and secrets.compare_digest(schluessel or "", HOST_SCHLUESSEL)
|
||||
)
|
||||
if not darf:
|
||||
raise HTTPException(status_code=403, detail="Kein Hostschlüssel")
|
||||
return {"host": users.host_setzen(sid, True)}
|
||||
|
||||
|
||||
@app.post("/api/users/waehlen")
|
||||
def name_waehlen(
|
||||
response: Response,
|
||||
user_id: int | None = Body(None, embed=True),
|
||||
movie_id: int | None = Body(None, embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Wer bin ich? Ohne user_id wieder anonym."""
|
||||
"""Wer bin ich? Ohne user_id wieder anonym.
|
||||
|
||||
Hat der Name einen Anmeldefilm, muss movie_id ihn treffen. Der Host
|
||||
und wer schon angemeldet ist, kommen ohne durch.
|
||||
"""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if user_id is None:
|
||||
users.abwaehlen(sid)
|
||||
return {"mine": None}
|
||||
|
||||
film = users.schutz_von(user_id)
|
||||
frei = (
|
||||
film is None
|
||||
or users.ist_host(sid)
|
||||
or users.besitzer(sid) == user_id
|
||||
)
|
||||
if not frei and movie_id != film:
|
||||
raise HTTPException(status_code=403, detail="Das ist nicht der Film.")
|
||||
|
||||
benutzer = users.waehlen(sid, user_id)
|
||||
if not benutzer:
|
||||
raise HTTPException(status_code=404, detail="Unbekannter Name")
|
||||
@@ -576,14 +694,13 @@ def bewerten(
|
||||
watched_id: int,
|
||||
response: Response,
|
||||
stars: int = Body(..., embed=True),
|
||||
text: str | None = Body(None, embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
mitglied(sid)
|
||||
stars = max(1, min(watched.MAX_STERNE, stars))
|
||||
# Der Name kommt aus der Runde, nicht aus der Anfrage
|
||||
eintrag = watched.bewerten(watched_id, sid, stars, users.name_von(sid), text)
|
||||
eintrag = watched.bewerten(watched_id, sid, stars, users.name_von(sid))
|
||||
if not eintrag:
|
||||
raise HTTPException(status_code=404, detail="Film steht nicht in der Liste")
|
||||
events.senden("watched")
|
||||
@@ -599,6 +716,40 @@ def wertung_loeschen(rating_id: int):
|
||||
return {"removed": 1}
|
||||
|
||||
|
||||
@app.post("/api/watched/{watched_id}/notes")
|
||||
def gaestebuch_schreiben(
|
||||
watched_id: int,
|
||||
response: Response,
|
||||
text: str = Body(..., embed=True),
|
||||
parent_id: int | None = Body(None, embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Kommentar zum Abend, oder mit parent_id eine Antwort auf einen anderen."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
mitglied(sid)
|
||||
note_id = watched.anmerken(watched_id, sid, text, parent_id)
|
||||
if note_id is None:
|
||||
raise HTTPException(status_code=400, detail="Kein Text oder Ziel unbekannt")
|
||||
events.senden("watched")
|
||||
return {"id": note_id}
|
||||
|
||||
|
||||
# Eigener Pfad, damit er /api/watched/{watched_id} nicht in die Quere kommt
|
||||
@app.delete("/api/watched-notes/{note_id}")
|
||||
def gaestebuch_loeschen(
|
||||
note_id: int,
|
||||
response: Response,
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Der Verfasser oder der Host — samt allen Antworten darunter."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
entfernt = watched.anmerkung_loeschen(note_id, sid, users.ist_host(sid))
|
||||
if not entfernt:
|
||||
raise HTTPException(status_code=404, detail="Nicht gefunden oder nicht deins")
|
||||
events.senden("watched")
|
||||
return {"removed": entfernt}
|
||||
|
||||
|
||||
@app.delete("/api/suggestions/alle")
|
||||
def runde_zuruecksetzen():
|
||||
"""Host leert die Runde: Vorschläge weg, Rad wieder leer."""
|
||||
|
||||
Reference in New Issue
Block a user