update
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import secrets
|
||||
import time
|
||||
@@ -6,13 +7,14 @@ import time
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
import httpx
|
||||
from fastapi import Body, Cookie, FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import (
|
||||
backup,
|
||||
clips,
|
||||
punkte,
|
||||
db,
|
||||
events,
|
||||
@@ -93,6 +95,7 @@ async def startup():
|
||||
users.init()
|
||||
suggestions.init()
|
||||
watched.init()
|
||||
clips.init()
|
||||
wishlist.init()
|
||||
features.init()
|
||||
# Erst die Sicherung, dann der Sync — so liegt vor jedem Katalog-Update
|
||||
@@ -296,6 +299,160 @@ async def ki_suche(
|
||||
return await ki.aufloesen(vorschlaege, filter_, ausser=ausser)
|
||||
|
||||
|
||||
# ── Clips: Marken setzen, Fenster ziehen, schneiden, freigeben ──────────
|
||||
def clip_oder_404(clip):
|
||||
if clip is None:
|
||||
raise HTTPException(status_code=404, detail="Clip nicht gefunden oder nicht deiner")
|
||||
return clip
|
||||
|
||||
|
||||
@app.post("/api/clips")
|
||||
def marke_setzen(response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
"""Taste C: der Moment wird gemerkt, mehr nicht. Der Film ist der
|
||||
zuletzt gezogene, sonst der neueste Abend unter Gesehen."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
mitglied(sid)
|
||||
gezogen = letzter_dreh["ergebnis"]
|
||||
if gezogen:
|
||||
movie_id = gezogen["gewinner"]["movie_id"]
|
||||
else:
|
||||
abend = watched.juengster()
|
||||
movie_id = abend["movie_id"] if abend else None
|
||||
clip = clips.markieren(sid, movie_id)
|
||||
events.senden("clips")
|
||||
return clip
|
||||
|
||||
|
||||
@app.get("/api/clips/aufnahmen")
|
||||
async def clip_aufnahmen():
|
||||
"""Zu welchen Abenden es eine Aufnahme gibt."""
|
||||
try:
|
||||
return {"tage": await clips.tage_mit_aufnahme()}
|
||||
except httpx.HTTPError:
|
||||
return {"tage": []}
|
||||
|
||||
|
||||
@app.post("/api/clips/von-film")
|
||||
async def clip_von_film(
|
||||
response: Response,
|
||||
movie_id: int = Body(..., embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Ein Clip zu einem alten Abend: die Marke kommt an den Anfang der
|
||||
Aufnahme, das Fenster schiebt man dann im Editor hin."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
mitglied(sid)
|
||||
abend = watched.von_film(movie_id)
|
||||
if not abend:
|
||||
raise HTTPException(status_code=404, detail="Dieser Film steht nicht unter Gesehen.")
|
||||
try:
|
||||
beginn = await clips.aufnahme_des_abends(abend["watched_at"])
|
||||
except httpx.HTTPError:
|
||||
beginn = None
|
||||
if beginn is None:
|
||||
raise HTTPException(status_code=404, detail="Zu diesem Abend gibt es keine Aufnahme.")
|
||||
clip = clips.markieren(sid, movie_id, beginn + timedelta(seconds=clips.VORHER + 30))
|
||||
events.senden("clips")
|
||||
return clip
|
||||
|
||||
|
||||
@app.get("/api/clips")
|
||||
def clips_liste(response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
return clips.alle(sid, users.ist_host(sid))
|
||||
|
||||
|
||||
@app.patch("/api/clips/{clip_id}")
|
||||
def clip_aendern(
|
||||
clip_id: int,
|
||||
response: Response,
|
||||
titel: str | None = Body(None, embed=True),
|
||||
vorher: float | None = Body(None, embed=True),
|
||||
dauer: float | None = Body(None, embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
clip = clip_oder_404(clips.aendern(clip_id, sid, users.ist_host(sid), titel, vorher, dauer))
|
||||
events.senden("clips")
|
||||
return clip
|
||||
|
||||
|
||||
@app.delete("/api/clips/{clip_id}")
|
||||
def clip_loeschen(clip_id: int, response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not clips.loeschen(clip_id, sid, users.ist_host(sid)):
|
||||
raise HTTPException(status_code=404, detail="Clip nicht gefunden oder nicht deiner")
|
||||
events.senden("clips")
|
||||
return {"removed": 1}
|
||||
|
||||
|
||||
@app.post("/api/clips/{clip_id}/schneiden")
|
||||
async def clip_schneiden(clip_id: int, response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
"""Den Ausschnitt aus der Aufnahme als Datei ablegen."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
try:
|
||||
clip = clip_oder_404(await clips.schneiden(clip_id, sid, users.ist_host(sid)))
|
||||
except httpx.HTTPError:
|
||||
raise HTTPException(status_code=502, detail="Die Aufnahme liefert diesen Ausschnitt nicht.")
|
||||
events.senden("clips")
|
||||
return clip
|
||||
|
||||
|
||||
@app.get("/api/clips/{clip_id}/aufnahme")
|
||||
async def clip_aufnahme(clip_id: int, response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
"""Die Stücke der Aufnahme rund um die Marke, für den Zeitstrahl."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
row = clip_oder_404(clips.zeile(clip_id, sid, users.ist_host(sid)))
|
||||
try:
|
||||
return await clips.aufnahme(row["marke_at"])
|
||||
except httpx.HTTPError:
|
||||
return []
|
||||
|
||||
|
||||
@app.get("/api/clips/{clip_id}/vorschau")
|
||||
async def clip_vorschau(
|
||||
clip_id: int,
|
||||
response: Response,
|
||||
vorher: float | None = None,
|
||||
dauer: float | None = None,
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Ein Fenster um die Marke ansehen, ohne zu schneiden — beim Ziehen
|
||||
der Regler. Kommt direkt vom Playback-Server durch."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
row = clip_oder_404(clips.zeile(clip_id, sid, users.ist_host(sid)))
|
||||
url, params = clips.playback_url(row, vorher, dauer)
|
||||
http = httpx.AsyncClient(timeout=120.0)
|
||||
try:
|
||||
antwort = await http.send(http.build_request("GET", url, params=params), stream=True)
|
||||
except httpx.HTTPError:
|
||||
await http.aclose()
|
||||
raise HTTPException(status_code=502, detail="Die Aufnahme liefert diesen Ausschnitt nicht.")
|
||||
if antwort.status_code != 200:
|
||||
await antwort.aclose()
|
||||
await http.aclose()
|
||||
raise HTTPException(status_code=502, detail="Die Aufnahme liefert diesen Ausschnitt nicht.")
|
||||
|
||||
async def weiterreichen():
|
||||
try:
|
||||
async for stueck in antwort.aiter_bytes():
|
||||
yield stueck
|
||||
finally:
|
||||
await antwort.aclose()
|
||||
await http.aclose()
|
||||
|
||||
return StreamingResponse(weiterreichen(), media_type="video/mp4")
|
||||
|
||||
|
||||
@app.get("/api/clips/{clip_id}/video")
|
||||
def clip_video(clip_id: int, response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
row = clip_oder_404(clips.zeile(clip_id, sid, users.ist_host(sid)))
|
||||
if not row["datei"] or not (clips.CLIPS_DIR / row["datei"]).exists():
|
||||
raise HTTPException(status_code=404, detail="Noch nicht geschnitten")
|
||||
return FileResponse(clips.CLIPS_DIR / row["datei"], media_type="video/mp4")
|
||||
|
||||
|
||||
@app.get("/api/movies/{movie_id}/credits")
|
||||
async def film_credits(movie_id: int):
|
||||
"""Regie und Besetzung, live von TMDB."""
|
||||
@@ -699,13 +856,14 @@ def host_modus(
|
||||
response: Response,
|
||||
an: bool = Body(..., embed=True),
|
||||
schluessel: str | None = Body(None, embed=True),
|
||||
movie_id: int | 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.
|
||||
Freischalten geht mit dem Schlüssel aus der .env, einmal über
|
||||
/?host=<schlüssel>, oder mit dem Host-Film: die Tastenkombination
|
||||
fragt danach, wie beim Anmeldefilm eines Namens.
|
||||
|
||||
Die Freischaltung bleibt, bis jemand sie abgibt. Ob die Hostknöpfe
|
||||
gerade sichtbar sind, entscheidet der Browser für sich — dafür ist
|
||||
@@ -715,14 +873,51 @@ def host_modus(
|
||||
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)
|
||||
film = users.host_film()
|
||||
darf = (
|
||||
users.ist_host(sid)
|
||||
or (bool(HOST_SCHLUESSEL) and secrets.compare_digest(schluessel or "", HOST_SCHLUESSEL))
|
||||
or (film is not None and movie_id == film)
|
||||
)
|
||||
if not darf:
|
||||
raise HTTPException(status_code=403, detail="Kein Hostschlüssel")
|
||||
return {"host": users.host_setzen(sid, True)}
|
||||
|
||||
|
||||
@app.get("/api/host/film")
|
||||
async def host_film_zeigen(response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
"""Welcher Film den Hostmodus öffnet — nur der Host sieht ihn."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not users.ist_host(sid):
|
||||
raise HTTPException(status_code=403, detail="Nur für den Host")
|
||||
movie_id = users.host_film()
|
||||
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/host/film")
|
||||
def host_film_setzen(
|
||||
response: Response,
|
||||
movie_id: int | None = Body(None, embed=True),
|
||||
filmabend_sid: str | None = Cookie(None),
|
||||
):
|
||||
"""Den Host-Film setzen oder aufheben — nur als Host."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not users.ist_host(sid):
|
||||
raise HTTPException(status_code=403, detail="Nur für den Host")
|
||||
users.host_film_setzen(movie_id)
|
||||
return {"gesetzt": movie_id is not None}
|
||||
|
||||
|
||||
@app.post("/api/users/waehlen")
|
||||
def name_waehlen(
|
||||
response: Response,
|
||||
|
||||
Reference in New Issue
Block a user