From fd7893b5d23bf2201d8c5e22d209b5988f745822 Mon Sep 17 00:00:00 2001 From: team3 Date: Wed, 30 Sep 2026 14:31:50 +0200 Subject: [PATCH] update --- README.md | 19 ++-- backend/app/clips.py | 114 ++++++++++++++------ backend/app/main.py | 93 +++++++---------- backend/tests/test_clips.py | 119 +++++++++++---------- frontend/src/App.vue | 7 +- frontend/src/components/ClipsTab.vue | 120 +++++++--------------- frontend/src/components/VerwaltungTab.vue | 77 ++++++++++++-- 7 files changed, 302 insertions(+), 247 deletions(-) diff --git a/README.md b/README.md index c9e023a..b0eeede 100644 --- a/README.md +++ b/README.md @@ -262,15 +262,16 @@ Löschen kann der Host in der Verwaltung, Stück für Stück: die App schickt das an die MediaMTX-API (Port 9997, nur intern, angemeldet als `sender` mit dem Sendepasswort), die die Segmente wegräumt. -Daraus kommen die Clips. Im Stream-Reiter setzt die Taste C (oder der -Knopf „Clip") eine Marke: nur der Zeitpunkt, vier Sekunden vor jetzt, -weil der Player so weit hinter der Live-Kante hängt. Der Film ist der -zuletzt gezogene. Im Reiter „Clips" zieht man später das Fenster um die -Marke, sieht es sich an, schneidet und gibt frei. Geschnittene Clips -liegen als MP4 unter `clips/`, freigegebene erscheinen im Buch auf der -Seite des Abends. Bis zur Freigabe sieht sie nur, wer sie gesetzt hat — -und der Host, der auch aufräumen darf. Ein freigegebener Clip bringt -3 Punkte, einmal je Film. +Jede Aufnahme hat ihren Film: der Host weist ihn in der Verwaltung zu, +sonst gilt der Abend mit dem gleichen Datum, sonst läuft sie unter +„Sonstige". Daraus kommen die Clips. Im Stream-Reiter setzt die Taste C +(oder der Knopf „Clip") eine Marke: nur der Zeitpunkt, vier Sekunden vor +jetzt, weil der Player so weit hinter der Live-Kante hängt. Der Film ist +der der laufenden Aufnahme, sonst der zuletzt gezogene. „Clip erstellen" +im Reiter „Clips" setzt eine Marke an den Anfang einer beliebigen +Aufnahme. Dort zieht man das Fenster um die Marke, sieht es sich an und +schneidet; die MP4 liegt dann unter `clips/`. Jeder sieht nur seine +Clips, der Host alle — er räumt auch auf. Die Ausschnitte liefert der Playback-Server von MediaMTX (Port 9996, nur intern): `/get?path=filmabend&start=&duration=&format=mp4`. diff --git a/backend/app/clips.py b/backend/app/clips.py index d8e34bf..d676e50 100644 --- a/backend/app/clips.py +++ b/backend/app/clips.py @@ -28,6 +28,13 @@ CREATE TABLE IF NOT EXISTS clips ( datei TEXT, created_at TEXT NOT NULL ); + +-- Der Film einer Aufnahme, vom Host gesetzt. Ohne Eintrag gilt der Abend +-- mit dem gleichen Datum — oder keiner. +CREATE TABLE IF NOT EXISTS aufnahme_film ( + start TEXT PRIMARY KEY, + movie_id INTEGER NOT NULL +); """ STREAM_PFAD = "filmabend" @@ -223,33 +230,9 @@ async def _segmente(von, bis): return resp.json() -async def tage_mit_aufnahme(): - """An welchen Abenden es Aufnahmen gibt — als Datum, wobei der Abend - bis zum nächsten Mittag reicht. Für die Auswahl im Clips-Reiter.""" - segmente = await _segmente(datetime(2000, 1, 1, tzinfo=timezone.utc), _jetzt()) - tage = set() - for s in segmente: - start = datetime.fromisoformat(s["start"].replace("Z", "+00:00")) - tage.add((start - timedelta(hours=12)).strftime("%Y-%m-%d")) - return sorted(tage) - - -async def aufnahmen_des_abends(watched_at): - """Die Aufnahmen zu einem Abend: alles vom Mittag des Tages bis zum - nächsten Mittag — so gehört auch die Nacht nach Mitternacht dazu. - Wurde der Stream zwischendurch beendet, sind es mehrere Stücke, - nach Beginn geordnet.""" - tag = datetime.fromisoformat(watched_at[:10]).replace(tzinfo=timezone.utc) - mittag = tag + timedelta(hours=12) - segmente = await _segmente(mittag, mittag + timedelta(days=1)) - stuecke = [ - { - "start": _iso(datetime.fromisoformat(s["start"].replace("Z", "+00:00"))), - "dauer": round(s["duration"], 1), - } - for s in segmente - ] - return sorted(stuecke, key=lambda s: s["start"]) +# ── Aufnahmen: die Stücke aus MediaMTX, jedes mit seinem Film ────────── +# Eine Aufnahme hängt an keinem Abend. Der Host weist ihr einen Film zu; +# ohne Zuweisung gilt der Abend mit dem gleichen Datum, sonst keiner. def _stueck(s): @@ -259,10 +242,83 @@ def _stueck(s): } +def _abendtag(iso): + """Der Abend reicht bis zum nächsten Mittag — die Nacht gehört dazu.""" + zeit = datetime.fromisoformat(iso.replace("Z", "+00:00")) + return (zeit - timedelta(hours=12)).strftime("%Y-%m-%d") + + +def film_zuweisen(start, movie_id): + """Der Host hängt einen Film an eine Aufnahme — oder nimmt ihn weg.""" + with connect() as conn: + if movie_id is None: + conn.execute("DELETE FROM aufnahme_film WHERE start = ?", (start,)) + else: + conn.execute( + "INSERT OR REPLACE INTO aufnahme_film (start, movie_id) VALUES (?, ?)", + (start, movie_id), + ) + + +def _filme(conn): + """Zuweisungen je Start und Abende je Tag — um Aufnahmen ihren Film zu geben.""" + zugewiesen = { + r["start"]: {"id": r["movie_id"], "title": r["title"], "poster_path": r["poster_path"]} + for r in conn.execute( + "SELECT a.start, a.movie_id, m.title, m.poster_path FROM aufnahme_film a " + "LEFT JOIN movies m ON m.id = a.movie_id" + ) + } + abende = {} + for w in conn.execute( + "SELECT movie_id, title, poster_path, watched_at FROM watched ORDER BY id" + ): + abende.setdefault( + _abendtag(w["watched_at"]), + {"id": w["movie_id"], "title": w["title"], "poster_path": w["poster_path"]}, + ) + return zugewiesen, abende + + +def _mit_film(stueck, zugewiesen, abende): + film = zugewiesen.get(stueck["start"]) + return { + **stueck, + "film": film or abende.get(_abendtag(stueck["start"])), + "zugewiesen": film is not None, + } + + async def aufnahmen_alle(): - """Alle Aufnahmen, neueste zuerst — der Host räumt damit auf.""" + """Alle Aufnahmen, neueste zuerst, jede mit ihrem Film.""" 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) + with connect() as conn: + zugewiesen, abende = _filme(conn) + stuecke = [_mit_film(_stueck(s), zugewiesen, abende) for s in segmente] + return sorted(stuecke, key=lambda s: s["start"], reverse=True) + + +def film_der_aufnahme(start): + """Nur die id — für neue Marken.""" + with connect() as conn: + zugewiesen, abende = _filme(conn) + film = _mit_film({"start": start}, zugewiesen, abende)["film"] + return film["id"] if film else None + + +async def film_live(): + """Der Film der gerade laufenden Aufnahme — für die Taste C. None, + wenn nichts aufgenommen wird oder MediaMTX nicht antwortet.""" + try: + segmente = await _segmente(_jetzt() - timedelta(hours=6), _jetzt()) + except httpx.HTTPError: + return None + for s in segmente: + stueck = _stueck(s) + ende = datetime.fromisoformat(stueck["start"]) + timedelta(seconds=stueck["dauer"]) + if ende >= _jetzt() - timedelta(seconds=90): + return film_der_aufnahme(stueck["start"]) + return None def _im_stueck(segmente, start, dauer): diff --git a/backend/app/main.py b/backend/app/main.py index 6dd68bb..c0967cc 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -307,15 +307,17 @@ def clip_oder_404(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.""" +async def marke_setzen(response: Response, filmabend_sid: str | None = Cookie(None)): + """Taste C: der Moment wird gemerkt, mehr nicht. Der Film ist der der + laufenden Aufnahme, sonst der zuletzt gezogene, sonst der neueste + Abend unter Gesehen.""" sid = sitzung(response, filmabend_sid) mitglied(sid) + movie_id = await clips.film_live() gezogen = letzter_dreh["ergebnis"] - if gezogen: + if movie_id is None and gezogen: movie_id = gezogen["gewinner"]["movie_id"] - else: + if movie_id is None: abend = watched.juengster() movie_id = abend["movie_id"] if abend else None clip = clips.markieren(sid, movie_id) @@ -323,27 +325,34 @@ def marke_setzen(response: Response, filmabend_sid: str | None = Cookie(None)): 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.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") +async def aufnahmen_liste(): + """Alle Aufnahmen, neueste zuerst, jede mit ihrem Film — für + „Clip erstellen" und die Verwaltung.""" try: return {"stuecke": await clips.aufnahmen_alle()} except httpx.HTTPError: raise HTTPException(status_code=502, detail="Die Aufnahmen sind gerade nicht erreichbar.") +@app.put("/api/aufnahmen/film") +async def aufnahme_film_setzen( + response: Response, + start: str = Body(..., embed=True), + movie_id: int | None = Body(None, embed=True), + filmabend_sid: str | None = Cookie(None), +): + """Der Host sagt, welcher Film in einer Aufnahme lief — oder nimmt es zurück.""" + sid = sitzung(response, filmabend_sid) + if not users.ist_host(sid): + raise HTTPException(status_code=403, detail="Nur für den Host") + if movie_id is not None: + # Suchtreffer stehen nicht im Katalog — Titel und Poster müssen aber her + await film_holen(movie_id) + clips.film_zuweisen(start, movie_id) + return {"zugewiesen": movie_id is not None} + + @app.delete("/api/aufnahmen") async def aufnahme_loeschen( response: Response, start: str, dauer: float, filmabend_sid: str | None = Cookie(None) @@ -360,51 +369,25 @@ async def aufnahme_loeschen( ) except httpx.HTTPError: raise HTTPException(status_code=502, detail="Die MediaMTX-API ist nicht erreichbar.") + clips.film_zuweisen(start, None) 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) -): - """Die Aufnahmen zu einem Abend — mehrere, wenn der Stream neu - gestartet wurde. Daraus wählt man das Stück für den Clip.""" - 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: - return {"stuecke": await clips.aufnahmen_des_abends(abend["watched_at"])} - except httpx.HTTPError: - return {"stuecke": []} - - -@app.post("/api/clips/von-film") -async def clip_von_film( +@app.post("/api/clips/von-aufnahme") +def clip_von_aufnahme( response: Response, - movie_id: int = Body(..., embed=True), - start: str | None = Body(None, embed=True), + start: str = Body(..., embed=True), filmabend_sid: str | None = Cookie(None), ): - """Ein Clip zu einem alten Abend: die Marke kommt an den Anfang des - gewählten Stücks — ohne Wahl an den des ersten. Das Fenster schiebt - man dann im Editor hin.""" + """Ein Clip aus einer alten Aufnahme: die Marke kommt kurz nach deren + Anfang, das Fenster schiebt man dann im Editor hin. Den Film bringt + die Aufnahme mit — oder keinen.""" 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.") - if start is None: - try: - stuecke = await clips.aufnahmen_des_abends(abend["watched_at"]) - except httpx.HTTPError: - stuecke = [] - if not stuecke: - raise HTTPException(status_code=404, detail="Zu diesem Abend gibt es keine Aufnahme.") - start = stuecke[0]["start"] beginn = datetime.fromisoformat(start) - clip = clips.markieren(sid, movie_id, beginn + timedelta(seconds=clips.VORHER + 30)) + clip = clips.markieren( + sid, clips.film_der_aufnahme(start), beginn + timedelta(seconds=clips.VORHER + 30) + ) events.senden("clips") return clip diff --git a/backend/tests/test_clips.py b/backend/tests/test_clips.py index d8fc992..0c0626a 100644 --- a/backend/tests/test_clips.py +++ b/backend/tests/test_clips.py @@ -2,7 +2,7 @@ import httpx -from app import clips, main, punkte +from app import clips, db, main, punkte def marke(client): @@ -38,7 +38,7 @@ def test_ohne_dreh_zaehlt_der_neueste_abend(client, film): assert marke(client)["movie_id"] == film(2) -def test_tage_mit_aufnahme(client, monkeypatch): +def stuecke_mocken(monkeypatch): async def segmente(von, bis): # Ein Abend mit Stück vor und nach Mitternacht, ein weiterer am Tag danach return [ @@ -48,63 +48,72 @@ def test_tage_mit_aufnahme(client, monkeypatch): ] monkeypatch.setattr(clips, "_segmente", segmente) - assert client.get("/api/clips/aufnahmen").json()["tage"] == ["2026-09-23", "2026-09-25"] -def test_clip_zu_altem_abend(client, film, monkeypatch): - from datetime import datetime, timezone +def test_aufnahmen_bekommen_ihren_film(client, zuschauer, hostmodus, film, monkeypatch): + """Zuweisung schlägt den Abend am gleichen Datum; sonst kein Film.""" + stuecke_mocken(monkeypatch) + client.post("/api/watched", json={"movie_id": film(1)}) + with db.connect() as conn: + conn.execute("UPDATE watched SET watched_at = '2026-09-23T20:41:00+00:00'") - client.post("/api/watched", json={"movie_id": film(4)}) - # Kein Abend zu diesem Film - assert client.post("/api/clips/von-film", json={"movie_id": 999}).status_code == 404 - - # Keine Aufnahme an dem Tag - async def keine(watched_at): - return [] - - monkeypatch.setattr(clips, "aufnahmen_des_abends", keine) - assert client.get(f"/api/clips/von-film/{film(4)}").json()["stuecke"] == [] - antwort = client.post("/api/clips/von-film", json={"movie_id": film(4)}) - assert antwort.status_code == 404 and "Aufnahme" in antwort.json()["detail"] - - # Zwei Stücke, weil der Stream neu gestartet wurde - stuecke = [ - {"start": "2026-09-23T19:00:00+00:00", "dauer": 1800.0}, - {"start": "2026-09-23T20:00:00+00:00", "dauer": 600.0}, + liste = client.get("/api/aufnahmen").json()["stuecke"] + assert [s["start"] for s in liste] == [ + "2026-09-25T19:00:00+00:00", + "2026-09-24T00:30:00+00:00", + "2026-09-23T20:00:00+00:00", ] + # Der Abend vom 23. gilt auch für die Nacht danach + assert liste[2]["film"]["id"] == film(1) and liste[2]["zugewiesen"] is False + assert liste[1]["film"]["id"] == film(1) + assert liste[0]["film"] is None - async def da(watched_at): - return stuecke + # Zuweisen darf nur der Host + zuweisung = {"start": "2026-09-25T19:00:00+00:00", "movie_id": film(2)} + assert client.put("/api/aufnahmen/film", json=zuweisung).status_code == 403 + hostmodus(zuschauer) + assert zuschauer.put("/api/aufnahmen/film", json=zuweisung).json() == {"zugewiesen": True} + liste = client.get("/api/aufnahmen").json()["stuecke"] + assert liste[0]["film"]["title"] == "Film 2" and liste[0]["zugewiesen"] is True + # Die Zuweisung schlägt den Abend + zuschauer.put("/api/aufnahmen/film", json={"start": liste[2]["start"], "movie_id": film(2)}) + assert client.get("/api/aufnahmen").json()["stuecke"][2]["film"]["id"] == film(2) + # Aufheben + zuschauer.put("/api/aufnahmen/film", json={"start": zuweisung["start"], "movie_id": None}) + assert client.get("/api/aufnahmen").json()["stuecke"][0]["film"] is None - monkeypatch.setattr(clips, "aufnahmen_des_abends", da) - assert client.get(f"/api/clips/von-film/{film(4)}").json()["stuecke"] == stuecke - # Ohne Wahl: die Marke liegt kurz nach dem Beginn des ersten Stücks - m = client.post("/api/clips/von-film", json={"movie_id": film(4)}).json() - assert m["movie_id"] == film(4) +def test_clip_aus_aufnahme(client, zuschauer, hostmodus, film): + hostmodus(zuschauer) + start = "2026-09-23T19:00:00+00:00" + zuschauer.put("/api/aufnahmen/film", json={"start": start, "movie_id": film(4)}) + m = client.post("/api/clips/von-aufnahme", json={"start": start}).json() + assert m["movie_id"] == film(4) and m["title"] == "Film 4" + # Die Marke liegt kurz nach dem Anfang der Aufnahme assert m["marke_at"] == "2026-09-23T19:01:00+00:00" - - # Mit Wahl: nach dem Beginn des gewählten Stücks - m = client.post( - "/api/clips/von-film", json={"movie_id": film(4), "start": stuecke[1]["start"]} - ).json() - assert m["marke_at"] == "2026-09-23T20:01:00+00:00" + # Ohne Film gibt es die Marke trotzdem — unter Sonstige + m = client.post("/api/clips/von-aufnahme", json={"start": "2026-09-25T19:00:00+00:00"}).json() + assert m["movie_id"] is None -def test_stuecke_eines_abends_nach_beginn_geordnet(monkeypatch): - import asyncio +def test_taste_c_nimmt_den_film_der_laufenden_aufnahme(client, zuschauer, hostmodus, film, monkeypatch): + from datetime import timedelta + + start = clips._iso(clips._jetzt() - timedelta(seconds=100)) async def segmente(von, bis): - return [ - {"start": "2026-09-23T21:00:00Z", "duration": 600.0}, - {"start": "2026-09-23T19:00:00Z", "duration": 1800.04}, - ] + return [{"start": start, "duration": 60}] monkeypatch.setattr(clips, "_segmente", segmente) - assert asyncio.run(clips.aufnahmen_des_abends("2026-09-23T20:00:00+00:00")) == [ - {"start": "2026-09-23T19:00:00+00:00", "dauer": 1800.0}, - {"start": "2026-09-23T21:00:00+00:00", "dauer": 600.0}, - ] + # Es läuft eine Aufnahme, aber ohne Film: dann wie bisher der neueste Abend + client.post("/api/watched", json={"movie_id": film(1)}) + with db.connect() as conn: + conn.execute("UPDATE watched SET watched_at = '2020-01-01T20:00:00+00:00'") + assert marke(client)["movie_id"] == film(1) + # Zugewiesen: der Film der Aufnahme + hostmodus(zuschauer) + zuschauer.put("/api/aufnahmen/film", json={"start": start, "movie_id": film(3)}) + assert marke(client)["movie_id"] == film(3) def test_fremde_sehen_nichts_der_host_alles(client, client2, zuschauer, hostmodus): @@ -221,25 +230,21 @@ def test_segmente_eines_stuecks(): ] -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 - +def test_aufnahme_loescht_nur_der_host(client, zuschauer, hostmodus, film, monkeypatch): 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 + start = "2026-09-23T19:00:00+00:00" + assert client.delete("/api/aufnahmen", params={"start": 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}) + zuschauer.put("/api/aufnahmen/film", json={"start": start, "movie_id": film(1)}) + antwort = zuschauer.delete("/api/aufnahmen", params={"start": start, "dauer": 60}) assert antwort.json() == {"segmente": 2} - assert geloescht == [(stuecke[0]["start"], 60.0)] + assert geloescht == [(start, 60.0)] + # Mit der Aufnahme geht auch die Zuweisung + assert clips.film_der_aufnahme(start) is None diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 93bcd77..1e403ef 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -434,12 +434,7 @@ onMounted(load) - +
diff --git a/frontend/src/components/ClipsTab.vue b/frontend/src/components/ClipsTab.vue index 7a47cb3..c270c32 100644 --- a/frontend/src/components/ClipsTab.vue +++ b/frontend/src/components/ClipsTab.vue @@ -10,64 +10,37 @@ import PersonName from './PersonName.vue' const props = defineProps({ angemeldet: Boolean, host: Boolean, - // Die gesehenen Abende — für Clips aus alten Aufnahmen - abende: { type: Array, default: () => [] }, }) -// "Clip erstellen": erst den Abend wählen, dann öffnet der Editor. -// Zur Wahl stehen nur Abende, zu denen es eine Aufnahme gibt. Wurde der -// Stream an dem Abend neu gestartet, fragt ein zweiter Schritt nach dem -// Stück. +// "Clip erstellen": eine Aufnahme wählen, dann öffnet der Editor. Jede +// Aufnahme bringt ihren Film mit — oder keinen, dann läuft der Clip +// unter „Sonstige". const wahl = ref(false) const wahlFehler = ref('') -const tage = ref(null) -const gewaehlt = ref(null) -const stuecke = ref(null) +const aufnahmen = ref(null) async function wahlOeffnen() { wahl.value = true wahlFehler.value = '' - tage.value = null - gewaehlt.value = null - stuecke.value = null + aufnahmen.value = null try { - const resp = await fetch('/api/clips/aufnahmen') - tage.value = resp.ok ? (await resp.json()).tage : [] + const resp = await fetch('/api/aufnahmen') + aufnahmen.value = resp.ok ? (await resp.json()).stuecke : [] } catch { - tage.value = [] + aufnahmen.value = [] } } -const abendeMitAufnahme = computed(() => - tage.value ? props.abende.filter((a) => tage.value.includes(a.watched_at.slice(0, 10))) : [], -) - -async function abendWaehlen(abend) { - wahlFehler.value = '' - gewaehlt.value = abend - stuecke.value = null - let liste = [] - try { - const resp = await fetch(`/api/clips/von-film/${abend.movie_id}`) - liste = resp.ok ? (await resp.json()).stuecke : [] - } catch { - liste = [] - } - // Ein Stück: direkt los. Sonst wählt man, welches. - if (liste.length <= 1) return erstellen(abend, liste[0]?.start) - stuecke.value = liste -} - function minuten(sekunden) { return `${Math.max(1, Math.round(sekunden / 60))} Min` } -async function erstellen(abend, start = null) { +async function erstellen(aufnahme) { wahlFehler.value = '' - const resp = await fetch('/api/clips/von-film', { + const resp = await fetch('/api/clips/von-aufnahme', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ movie_id: abend.movie_id, start }), + body: JSON.stringify({ start: aufnahme.start }), }) if (!resp.ok) { const daten = await resp.json().catch(() => ({})) @@ -104,7 +77,7 @@ const gruppen = computed(() => { const nach = new Map() for (const c of clips.value) { const key = c.movie_id ?? 0 - if (!nach.has(key)) nach.set(key, { key, titel: c.title || 'Ohne Film', clips: [] }) + if (!nach.has(key)) nach.set(key, { key, titel: c.title || 'Sonstige', clips: [] }) nach.get(key).clips.push(c) } return [...nach.values()] @@ -140,46 +113,31 @@ onUnmounted(() => abbestellen?.())
- +
- {{ stuecke ? 'Welche Aufnahme?' : 'Zu welchem Abend?' }} + Welche Aufnahme?

{{ wahlFehler }}

- - - - -

Suche Aufnahmen …

-

- Zu keinem Abend gibt es eine Aufnahme. -

+

Suche Aufnahmen …

+

Es gibt keine Aufnahmen.

    -
  • -
  • @@ -290,6 +248,8 @@ h3 { position: relative; display: block; aspect-ratio: 16 / 9; + /* Sonst zieht ein hochkantiges Poster die Karte in die Länge */ + overflow: hidden; background: #000; } @@ -404,18 +364,6 @@ h3 { line-height: 1; } -.abendwahl .zurueck { - align-self: flex-start; - padding: 0.3rem 0.5rem; - color: var(--muted); - font-size: 0.85rem; - border-radius: 6px; -} - -.abendwahl .zurueck:hover { - color: var(--text); -} - .abendwahl-fehler { color: var(--accent); font-size: 0.85rem; @@ -453,6 +401,14 @@ h3 { border-radius: 4px; } +/* Ohne Film: ein leeres Cover an Stelle des Posters */ +.abendwahl li .ohne-cover { + flex: 0 0 40px; + height: 60px; + background: var(--line); + border-radius: 4px; +} + .abend-text { display: flex; flex-direction: column; diff --git a/frontend/src/components/VerwaltungTab.vue b/frontend/src/components/VerwaltungTab.vue index 60a4647..0fb63d6 100644 --- a/frontend/src/components/VerwaltungTab.vue +++ b/frontend/src/components/VerwaltungTab.vue @@ -106,6 +106,21 @@ async function aufnahmeLoeschen(s) { await aufnahmenLaden() } +// Welcher Film in einer Aufnahme lief, sagt der Host — per Filmsuche +const filmDialogFuer = ref(null) + +async function filmZuweisen(start, movieId) { + aufnahmenFehler.value = '' + const resp = await fetch('/api/aufnahmen/film', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ start, movie_id: movieId }), + }) + if (!resp.ok) aufnahmenFehler.value = 'Zuweisen hat nicht geklappt.' + filmDialogFuer.value = null + await aufnahmenLaden() +} + function aufnahmeText(s) { const d = new Date(s.start) const datum = d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) @@ -245,19 +260,52 @@ async function entfernen(user) {

    Keine Aufnahmen.

    • - {{ aufnahmeText(s) }} - + + {{ aufnahmeText(s) }} · + {{ s.film.title }} + kein Film + + + + + +
    + +
    + +
    +
    +