From b5366783b1ca92fbf0c1de847e017c48e7862394 Mon Sep 17 00:00:00 2001 From: Housemann <40449280+Housemann@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:51:23 +0200 Subject: [PATCH] Cover-Bilder aus DB laden mit Fallback auf URL-Proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API: neuer Endpunkt GET /api/records/{id}/cover liefert BYTEA direkt aus der DB; cover_mime_type in COLUMNS aufgenommen - Frontend: CoverImage lädt primär aus DB (/api/records/{id}/cover), fällt bei 404 automatisch auf den URL-Proxy zurück - types.ts: cover_mime_type zum MediaRecord-Interface hinzugefügt - scripts/deploy.sh: einmaliges SSH-Passwort für alle scp-Transfers Co-Authored-By: Claude Sonnet 4.6 --- api/main.py | 26 ++++++++++++++++++++ frontend/src/components/CoverImage.vue | 32 +++++++++++++++++++++---- frontend/src/components/DataTable.vue | 2 ++ frontend/src/components/MobileCards.vue | 2 +- frontend/src/types.ts | 1 + frontend/src/utils/coverUrl.ts | 19 +++++++++++++-- scripts/deploy.sh | 30 +++++++++++++++++++++++ 7 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 scripts/deploy.sh diff --git a/api/main.py b/api/main.py index 3a2e28a..73b4be3 100644 --- a/api/main.py +++ b/api/main.py @@ -53,6 +53,7 @@ COLUMNS = [ "audio_bitrate", "created_at", "seen_at", + "cover_mime_type", ] @@ -452,6 +453,31 @@ async def proxy_cover(url: str = Query(..., min_length=8)): ) +@app.get("/api/records/{record_id}/cover") +async def get_record_cover(record_id: int): + """Cover-Bild aus DB liefern (BYTEA). 404 wenn kein Bild gespeichert.""" + if pool is None: + raise HTTPException(503, detail="Datenbank nicht verbunden") + + async with pool.acquire() as conn: + row = await conn.fetchrow( + f"SELECT cover_image, cover_mime_type FROM {qualified_table} WHERE id = $1", + record_id, + ) + + if row is None: + raise HTTPException(404, detail="Eintrag nicht gefunden") + if not row["cover_image"]: + raise HTTPException(404, detail="Kein Bild gespeichert") + + mime = row["cover_mime_type"] or "image/jpeg" + return Response( + content=bytes(row["cover_image"]), + media_type=mime, + headers={"Cache-Control": "public, max-age=604800"}, + ) + + @app.get("/api/records") async def get_records( search: str | None = Query(None, description="Suche in Dateinamen"), diff --git a/frontend/src/components/CoverImage.vue b/frontend/src/components/CoverImage.vue index ea54833..7ddd662 100644 --- a/frontend/src/components/CoverImage.vue +++ b/frontend/src/components/CoverImage.vue @@ -4,17 +4,39 @@ import { coverImageSrc } from '../utils/coverUrl' const props = defineProps<{ url: string | null | undefined + id?: number | null + hasCoverInDb?: boolean | null size?: 'table' | 'card' }>() -const failed = ref(false) +// Ob das DB-Bild bereits fehlgeschlagen ist → Fallback auf URL +const dbFailed = ref(false) +const urlFailed = ref(false) -const src = computed(() => coverImageSrc(props.url)) +const src = computed(() => { + if (dbFailed.value) { + // Fallback: URL-Proxy + return coverImageSrc(props.url, null, false) + } + return coverImageSrc(props.url, props.id, props.hasCoverInDb) +}) + +const failed = computed(() => dbFailed.value && urlFailed.value) + +function onError() { + if (!dbFailed.value && props.id && props.hasCoverInDb) { + // DB-Bild fehlgeschlagen → Fallback auf URL versuchen + dbFailed.value = true + } else { + urlFailed.value = true + } +} watch( - () => props.url, + () => [props.id, props.url, props.hasCoverInDb], () => { - failed.value = false + dbFailed.value = false + urlFailed.value = false } ) @@ -29,7 +51,7 @@ watch( loading="lazy" decoding="async" referrerpolicy="no-referrer" - @error="failed = true" + @error="onError" />