update
This commit is contained in:
@@ -105,7 +105,8 @@ def holen(clip_id, session_id):
|
|||||||
|
|
||||||
|
|
||||||
def alle(session_id, host=False):
|
def alle(session_id, host=False):
|
||||||
"""Die eigenen Clips; der Host sieht alle — er räumt auf."""
|
"""Alle Clips, für jeden. Ändern darf nur der Besitzer — und der Host,
|
||||||
|
der aufräumt; dafür steht an jedem Clip, ob er einem gehört."""
|
||||||
ich = besitzer(session_id)
|
ich = besitzer(session_id)
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
@@ -114,11 +115,7 @@ def alle(session_id, host=False):
|
|||||||
"LEFT JOIN movies m ON m.id = c.movie_id "
|
"LEFT JOIN movies m ON m.id = c.movie_id "
|
||||||
"ORDER BY c.marke_at DESC"
|
"ORDER BY c.marke_at DESC"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [
|
return [_oeffentlich(r, session_id, ich) for r in rows]
|
||||||
_oeffentlich(r, session_id, ich)
|
|
||||||
for r in rows
|
|
||||||
if host or gehoert_mir(r, session_id, ich)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _zeile(conn, clip_id):
|
def _zeile(conn, clip_id):
|
||||||
@@ -189,10 +186,10 @@ def playback_url(row, vorher=None, dauer=None):
|
|||||||
|
|
||||||
|
|
||||||
def zeile(clip_id, session_id, host):
|
def zeile(clip_id, session_id, host):
|
||||||
"""Die rohe Zeile, wenn man sie sehen darf: Besitzer oder Host."""
|
"""Die rohe Zeile zum Ansehen — Clips sieht jeder. Ändern und
|
||||||
|
Löschen prüfen gesondert, ob es der Besitzer oder der Host ist."""
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
row = _zeile(conn, clip_id)
|
return _zeile(conn, clip_id)
|
||||||
return row if _darf(row, session_id, host) else None
|
|
||||||
|
|
||||||
|
|
||||||
async def schneiden(clip_id, session_id, host):
|
async def schneiden(clip_id, session_id, host):
|
||||||
|
|||||||
@@ -116,11 +116,12 @@ def test_taste_c_nimmt_den_film_der_laufenden_aufnahme(client, zuschauer, hostmo
|
|||||||
assert marke(client)["movie_id"] == film(3)
|
assert marke(client)["movie_id"] == film(3)
|
||||||
|
|
||||||
|
|
||||||
def test_fremde_sehen_nichts_der_host_alles(client, client2, zuschauer, hostmodus):
|
def test_alle_sehen_clips_aendern_nur_besitzer_und_host(client, client2, zuschauer, hostmodus):
|
||||||
m = marke(client)
|
m = marke(client)
|
||||||
assert [c for c in client2.get("/api/clips").json() if c["id"] == m["id"]] == []
|
fremd = [c for c in client2.get("/api/clips").json() if c["id"] == m["id"]]
|
||||||
|
assert len(fremd) == 1 and fremd[0]["mine"] is False
|
||||||
assert client2.patch(f"/api/clips/{m['id']}", json={"titel": "x"}).status_code == 404
|
assert client2.patch(f"/api/clips/{m['id']}", json={"titel": "x"}).status_code == 404
|
||||||
assert client2.get(f"/api/clips/{m['id']}/video").status_code == 404
|
assert client2.delete(f"/api/clips/{m['id']}").status_code == 404
|
||||||
hostmodus(zuschauer)
|
hostmodus(zuschauer)
|
||||||
assert zuschauer.patch(f"/api/clips/{m['id']}", json={"titel": "Vom Host"}).json()["titel"] == "Vom Host"
|
assert zuschauer.patch(f"/api/clips/{m['id']}", json={"titel": "Vom Host"}).json()["titel"] == "Vom Host"
|
||||||
assert zuschauer.get("/api/clips").json()[0]["id"] == m["id"]
|
assert zuschauer.get("/api/clips").json()[0]["id"] == m["id"]
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ const tabs = computed(() => [
|
|||||||
{ id: 'merkliste', label: 'Sammlungen', count: null },
|
{ id: 'merkliste', label: 'Sammlungen', count: null },
|
||||||
{ id: 'wheel', label: 'Rad', count: null },
|
{ id: 'wheel', label: 'Rad', count: null },
|
||||||
{ id: 'stream', label: 'Stream', count: null },
|
{ id: 'stream', label: 'Stream', count: null },
|
||||||
...(angemeldet.value || host.value ? [{ id: 'clips', label: 'Clips', count: null }] : []),
|
{ id: 'clips', label: 'Clips', count: null },
|
||||||
{ id: 'watched', label: 'Gesehen', count: gesehenZahl.value },
|
{ id: 'watched', label: 'Gesehen', count: gesehenZahl.value },
|
||||||
{
|
{
|
||||||
id: 'wuensche',
|
id: 'wuensche',
|
||||||
|
|||||||
58
frontend/src/components/ClipBild.vue
Normal file
58
frontend/src/components/ClipBild.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
// Das Vorschaubild eines geschnittenen Clips: der erste Frame des Videos.
|
||||||
|
// Ein Video-Element im Hintergrund lädt den Anfang, der Frame wandert
|
||||||
|
// auf ein Canvas und von dort als Bild in die Karte. So gibt es keine
|
||||||
|
// Player-Steuerung, keine Bild-in-Bild-Hinweise und kein schwarzes Feld.
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: { type: String, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const bild = ref(null)
|
||||||
|
let video = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
video = document.createElement('video')
|
||||||
|
video.muted = true
|
||||||
|
video.playsInline = true
|
||||||
|
video.preload = 'auto'
|
||||||
|
video.addEventListener('loadeddata', () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = video.videoWidth
|
||||||
|
canvas.height = video.videoHeight
|
||||||
|
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||||
|
try {
|
||||||
|
bild.value = canvas.toDataURL('image/jpeg', 0.82)
|
||||||
|
} catch {
|
||||||
|
bild.value = null
|
||||||
|
}
|
||||||
|
// Das Video wird nicht mehr gebraucht
|
||||||
|
video.removeAttribute('src')
|
||||||
|
video.load()
|
||||||
|
})
|
||||||
|
video.src = props.src
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (video) {
|
||||||
|
video.removeAttribute('src')
|
||||||
|
video.load()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img v-if="bild" :src="bild" alt="" />
|
||||||
|
<span v-else class="leer"></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
img,
|
||||||
|
.leer {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,6 +6,9 @@ import ClipTrimmer from './ClipTrimmer.vue'
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
clip: { type: Object, required: true },
|
clip: { type: Object, required: true },
|
||||||
|
// Besitzer und Host dürfen schneiden, ändern und löschen — alle
|
||||||
|
// anderen schauen nur
|
||||||
|
darf: Boolean,
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['zu', 'geaendert'])
|
const emit = defineEmits(['zu', 'geaendert'])
|
||||||
|
|
||||||
@@ -72,6 +75,7 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
|
|||||||
<div class="editor">
|
<div class="editor">
|
||||||
<header>
|
<header>
|
||||||
<input
|
<input
|
||||||
|
v-if="darf"
|
||||||
v-model="titel"
|
v-model="titel"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
@@ -79,12 +83,20 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
|
|||||||
class="titel"
|
class="titel"
|
||||||
@change="patch({ titel })"
|
@change="patch({ titel })"
|
||||||
/>
|
/>
|
||||||
|
<h2 v-else class="titel">{{ clip.titel || 'Ohne Titel' }}</h2>
|
||||||
<button type="button" class="zu" title="Schließen" @click="emit('zu')">×</button>
|
<button type="button" class="zu" title="Schließen" @click="emit('zu')">×</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<video :src="vorschauUrl" controls playsinline class="vorschau"></video>
|
<!-- Wer nur schaut, bekommt bei geschnittenen Clips das fertige Video -->
|
||||||
|
<video
|
||||||
|
:src="!darf && clip.geschnitten ? `/api/clips/${clip.id}/video` : vorschauUrl"
|
||||||
|
controls
|
||||||
|
playsinline
|
||||||
|
class="vorschau"
|
||||||
|
></video>
|
||||||
|
|
||||||
<ClipTrimmer
|
<ClipTrimmer
|
||||||
|
v-if="darf"
|
||||||
v-model:vorher="vorher"
|
v-model:vorher="vorher"
|
||||||
v-model:dauer="dauer"
|
v-model:dauer="dauer"
|
||||||
:segmente="segmente"
|
:segmente="segmente"
|
||||||
@@ -95,14 +107,14 @@ onUnmounted(() => window.removeEventListener('keydown', escape))
|
|||||||
<p v-if="fehler" class="fehler">{{ fehler }}</p>
|
<p v-if="fehler" class="fehler">{{ fehler }}</p>
|
||||||
|
|
||||||
<div class="knoepfe">
|
<div class="knoepfe">
|
||||||
<button type="button" class="haupt" :disabled="schneidet" @click="schneiden">
|
<button v-if="darf" type="button" class="haupt" :disabled="schneidet" @click="schneiden">
|
||||||
{{ schneidet ? 'Schneidet …' : clip.geschnitten ? 'Speichern' : 'Schneiden' }}
|
{{ schneidet ? 'Schneidet …' : clip.geschnitten ? 'Speichern' : 'Schneiden' }}
|
||||||
</button>
|
</button>
|
||||||
<a v-if="clip.geschnitten" :href="`/api/clips/${clip.id}/video`" download class="link"
|
<a v-if="clip.geschnitten" :href="`/api/clips/${clip.id}/video`" download class="link"
|
||||||
>Herunterladen</a
|
>Herunterladen</a
|
||||||
>
|
>
|
||||||
<span class="rest"></span>
|
<span class="rest"></span>
|
||||||
<button type="button" class="weg" @click="loeschen">Löschen</button>
|
<button v-if="darf" type="button" class="weg" @click="loeschen">Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|||||||
import { abonnieren } from '../live'
|
import { abonnieren } from '../live'
|
||||||
import { backdropUrl, posterUrl } from '../images'
|
import { backdropUrl, posterUrl } from '../images'
|
||||||
import ClipEditor from './ClipEditor.vue'
|
import ClipEditor from './ClipEditor.vue'
|
||||||
|
import ClipBild from './ClipBild.vue'
|
||||||
import PersonName from './PersonName.vue'
|
import PersonName from './PersonName.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -149,7 +150,7 @@ onUnmounted(() => abbestellen?.())
|
|||||||
<p v-if="laden" class="note">Lade Clips…</p>
|
<p v-if="laden" class="note">Lade Clips…</p>
|
||||||
<p v-else-if="fehler" class="note error">{{ fehler }}</p>
|
<p v-else-if="fehler" class="note error">{{ fehler }}</p>
|
||||||
<p v-else-if="!clips.length" class="note">
|
<p v-else-if="!clips.length" class="note">
|
||||||
Noch keine Marken. Im Stream-Reiter setzt die Taste C eine.
|
Noch keine Clips. Im Stream-Reiter setzt die Taste C eine Marke.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section v-for="g in gruppen" :key="g.key">
|
<section v-for="g in gruppen" :key="g.key">
|
||||||
@@ -159,13 +160,7 @@ onUnmounted(() => abbestellen?.())
|
|||||||
<!-- Geschnitten: das Video selbst als Bild. Sonst das Filmbild
|
<!-- Geschnitten: das Video selbst als Bild. Sonst das Filmbild
|
||||||
mit dem Hinweis, dass hier erst eine Marke steht. -->
|
mit dem Hinweis, dass hier erst eine Marke steht. -->
|
||||||
<span class="bild">
|
<span class="bild">
|
||||||
<video
|
<ClipBild v-if="c.geschnitten" :src="`/api/clips/${c.id}/video`" />
|
||||||
v-if="c.geschnitten"
|
|
||||||
:src="`/api/clips/${c.id}/video#t=0.5`"
|
|
||||||
preload="metadata"
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
></video>
|
|
||||||
<img v-else-if="c.poster_path" :src="backdropUrl(c.poster_path, 'w342')" alt="" />
|
<img v-else-if="c.poster_path" :src="backdropUrl(c.poster_path, 'w342')" alt="" />
|
||||||
<span v-else class="leer"></span>
|
<span v-else class="leer"></span>
|
||||||
<span class="dauer">{{ Math.round(c.dauer) }} s</span>
|
<span class="dauer">{{ Math.round(c.dauer) }} s</span>
|
||||||
@@ -183,7 +178,13 @@ onUnmounted(() => abbestellen?.())
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<ClipEditor v-if="offen" :clip="offen" @zu="offen = null" @geaendert="laden_" />
|
<ClipEditor
|
||||||
|
v-if="offen"
|
||||||
|
:clip="offen"
|
||||||
|
:darf="offen.mine || host"
|
||||||
|
@zu="offen = null"
|
||||||
|
@geaendert="laden_"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user