update
This commit is contained in:
@@ -11,7 +11,7 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
import httpx
|
||||
|
||||
from .config import CLIPS_DIR, MEDIAMTX_PLAYBACK_URL
|
||||
from .config import CLIPS_DIR, MEDIAMTX_API_URL, MEDIAMTX_PLAYBACK_URL, MTX_SENDEPASSWORT
|
||||
from .db import connect
|
||||
from .users import WEM, besitzer, gehoert_mir
|
||||
|
||||
@@ -252,6 +252,50 @@ async def aufnahmen_des_abends(watched_at):
|
||||
return sorted(stuecke, key=lambda s: s["start"])
|
||||
|
||||
|
||||
def _stueck(s):
|
||||
return {
|
||||
"start": _iso(datetime.fromisoformat(s["start"].replace("Z", "+00:00"))),
|
||||
"dauer": round(s["duration"], 1),
|
||||
}
|
||||
|
||||
|
||||
async def aufnahmen_alle():
|
||||
"""Alle Aufnahmen, neueste zuerst — der Host räumt damit auf."""
|
||||
segmente = await _segmente(datetime(2000, 1, 1, tzinfo=timezone.utc), _jetzt())
|
||||
return sorted((_stueck(s) for s in segmente), key=lambda s: s["start"], reverse=True)
|
||||
|
||||
|
||||
def _im_stueck(segmente, start, dauer):
|
||||
"""Die Segmente (aus der API), die zu einem Stück der Aufnahme gehören:
|
||||
ihr Beginn liegt zwischen dessen Start und Ende. Eine Sekunde Luft an
|
||||
beiden Enden — Playback rundet die Zeiten anders als die API."""
|
||||
von = datetime.fromisoformat(start) - timedelta(seconds=1)
|
||||
bis = datetime.fromisoformat(start) + timedelta(seconds=dauer + 1)
|
||||
aus = []
|
||||
for s in segmente:
|
||||
beginn = datetime.fromisoformat(s["start"].replace("Z", "+00:00"))
|
||||
if von <= beginn <= bis:
|
||||
aus.append(s["start"])
|
||||
return aus
|
||||
|
||||
|
||||
async def aufnahme_loeschen(start, dauer):
|
||||
"""Ein Stück der Aufnahme löschen — MediaMTX räumt die Segmente weg.
|
||||
Gibt zurück, wie viele es waren."""
|
||||
auth = ("sender", MTX_SENDEPASSWORT)
|
||||
async with httpx.AsyncClient(timeout=30.0, auth=auth) as http:
|
||||
resp = await http.get(f"{MEDIAMTX_API_URL}/v3/recordings/get/{STREAM_PFAD}")
|
||||
resp.raise_for_status()
|
||||
starts = _im_stueck(resp.json().get("segments") or [], start, dauer)
|
||||
for s in starts:
|
||||
resp = await http.delete(
|
||||
f"{MEDIAMTX_API_URL}/v3/recordings/deletesegment",
|
||||
params={"path": STREAM_PFAD, "start": s},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return len(starts)
|
||||
|
||||
|
||||
async def aufnahme(um, spanne=SPANNE):
|
||||
"""Welche Stücke der Aufnahme es um einen Zeitpunkt herum gibt — für
|
||||
den Zeitstrahl im Editor. Relativ zur Marke, in Sekunden."""
|
||||
|
||||
@@ -27,6 +27,10 @@ PROVIDER_NETFLIX = 8
|
||||
MEDIAMTX_URL = os.getenv("MEDIAMTX_URL", "http://127.0.0.1:8890")
|
||||
# Der Playback-Server von MediaMTX: liefert Ausschnitte der Aufnahme
|
||||
MEDIAMTX_PLAYBACK_URL = os.getenv("MEDIAMTX_PLAYBACK_URL", "http://127.0.0.1:9996")
|
||||
# Die Steuer-API von MediaMTX, zum Löschen von Aufnahmen. Angemeldet
|
||||
# wird sich als der Sender-Nutzer mit dem Sendepasswort aus der .env.
|
||||
MEDIAMTX_API_URL = os.getenv("MEDIAMTX_API_URL", "http://127.0.0.1:9997")
|
||||
MTX_SENDEPASSWORT = os.getenv("MTX_SENDEPASSWORT", "")
|
||||
# Wohin die geschnittenen Clips kommen
|
||||
CLIPS_DIR = Path(os.getenv("CLIPS_DIR", ROOT / "clips"))
|
||||
REGION = "DE"
|
||||
|
||||
@@ -332,6 +332,33 @@ async def clip_aufnahmen():
|
||||
return {"tage": []}
|
||||
|
||||
|
||||
@app.get("/api/aufnahmen")
|
||||
async def aufnahmen_liste(response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||
"""Alle Stücke der Aufnahme, neueste zuerst — für die Verwaltung."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not users.ist_host(sid):
|
||||
raise HTTPException(status_code=403, detail="Nur für den Host")
|
||||
try:
|
||||
return {"stuecke": await clips.aufnahmen_alle()}
|
||||
except httpx.HTTPError:
|
||||
raise HTTPException(status_code=502, detail="Die Aufnahmen sind gerade nicht erreichbar.")
|
||||
|
||||
|
||||
@app.delete("/api/aufnahmen")
|
||||
async def aufnahme_loeschen(
|
||||
response: Response, start: str, dauer: float, filmabend_sid: str | None = Cookie(None)
|
||||
):
|
||||
"""Ein Stück der Aufnahme löschen — nur der Host."""
|
||||
sid = sitzung(response, filmabend_sid)
|
||||
if not users.ist_host(sid):
|
||||
raise HTTPException(status_code=403, detail="Nur für den Host")
|
||||
try:
|
||||
anzahl = await clips.aufnahme_loeschen(start, dauer)
|
||||
except httpx.HTTPError:
|
||||
raise HTTPException(status_code=502, detail="Löschen hat nicht geklappt.")
|
||||
return {"segmente": anzahl}
|
||||
|
||||
|
||||
@app.get("/api/clips/von-film/{movie_id}")
|
||||
async def clips_stuecke(
|
||||
movie_id: int, response: Response, filmabend_sid: str | None = Cookie(None)
|
||||
|
||||
@@ -204,3 +204,42 @@ def test_loeschen_nimmt_die_datei_mit(client):
|
||||
|
||||
def test_zuschauer_darf_keine_marke_setzen(zuschauer):
|
||||
assert zuschauer.post("/api/clips").status_code == 403
|
||||
|
||||
|
||||
def test_segmente_eines_stuecks():
|
||||
segmente = [
|
||||
{"start": "2026-09-23T19:00:00.5Z"},
|
||||
{"start": "2026-09-23T19:00:30.5Z"},
|
||||
{"start": "2026-09-23T19:01:00.5Z"},
|
||||
{"start": "2026-09-23T21:00:00Z"},
|
||||
]
|
||||
# Das Stück von 19:00 mit 61 s Dauer: drei Segmente, das von 21:00 nicht
|
||||
assert clips._im_stueck(segmente, "2026-09-23T19:00:00+00:00", 61.0) == [
|
||||
"2026-09-23T19:00:00.5Z",
|
||||
"2026-09-23T19:00:30.5Z",
|
||||
"2026-09-23T19:01:00.5Z",
|
||||
]
|
||||
|
||||
|
||||
def test_aufnahmen_verwaltet_nur_der_host(client, zuschauer, hostmodus, monkeypatch):
|
||||
stuecke = [{"start": "2026-09-23T19:00:00+00:00", "dauer": 60.0}]
|
||||
|
||||
async def alle():
|
||||
return stuecke
|
||||
|
||||
geloescht = []
|
||||
|
||||
async def loeschen(start, dauer):
|
||||
geloescht.append((start, dauer))
|
||||
return 2
|
||||
|
||||
monkeypatch.setattr(clips, "aufnahmen_alle", alle)
|
||||
monkeypatch.setattr(clips, "aufnahme_loeschen", loeschen)
|
||||
assert client.get("/api/aufnahmen").status_code == 403
|
||||
assert client.delete("/api/aufnahmen", params={"start": stuecke[0]["start"], "dauer": 60}).status_code == 403
|
||||
|
||||
hostmodus(zuschauer)
|
||||
assert zuschauer.get("/api/aufnahmen").json()["stuecke"] == stuecke
|
||||
antwort = zuschauer.delete("/api/aufnahmen", params={"start": stuecke[0]["start"], "dauer": 60})
|
||||
assert antwort.json() == {"segmente": 2}
|
||||
assert geloescht == [(stuecke[0]["start"], 60.0)]
|
||||
|
||||
Reference in New Issue
Block a user