Cover-Bilder aus DB laden mit Fallback auf URL-Proxy

- 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 <noreply@anthropic.com>
This commit is contained in:
Housemann
2026-07-17 19:51:23 +02:00
co-authored by Claude Sonnet 4.6
parent 944ca9a308
commit b5366783b1
7 changed files with 104 additions and 8 deletions
+27 -5
View File
@@ -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
}
)
</script>
@@ -29,7 +51,7 @@ watch(
loading="lazy"
decoding="async"
referrerpolicy="no-referrer"
@error="failed = true"
@error="onError"
/>
<div v-else class="cover__placeholder" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+2
View File
@@ -49,6 +49,8 @@ const columns: ColumnDef<MediaRecord>[] = [
cell: ({ row }) =>
h(CoverImage, {
url: row.original.cover_url,
id: row.original.id,
hasCoverInDb: Boolean(row.original.cover_mime_type),
size: 'table',
}),
},
+1 -1
View File
@@ -74,7 +74,7 @@ const detailFields: { key: keyof MediaRecord; label: string }[] = [
@click="onCardClick(row, $event)"
>
<div class="card__hero">
<CoverImage :url="row.cover_url" size="card" />
<CoverImage :url="row.cover_url" :id="row.id" :has-cover-in-db="Boolean(row.cover_mime_type)" size="card" />
<div class="card__hero-text">
<header class="card__header">
<div class="card__meta">
+1
View File
@@ -4,6 +4,7 @@ export interface MediaRecord {
file_name_new: string | null
object_type: string | null
cover_url: string | null
cover_mime_type: string | null
movie_rating: string | number | null
movie_votes: number | null
video_compression_format: string | null
+17 -2
View File
@@ -1,5 +1,5 @@
/** Cover über API-Proxy laden (Hotlink/Referrer-Probleme). */
export function coverImageSrc(url: string | null | undefined): string | null {
/** Cover-URL aus DB-URL bereinigen (für Fallback-Proxy). */
function cleanCoverUrl(url: string | null | undefined): string | null {
if (!url) return null
const cleaned = url.trim().replace(/^["']+|["']+$/g, '')
if (!cleaned) return null
@@ -8,3 +8,18 @@ export function coverImageSrc(url: string | null | undefined): string | null {
}
return cleaned
}
/**
* Primäre Quelle: Bild aus DB (/api/records/{id}/cover).
* Fallback-URL für den Fall dass kein Bild in DB gespeichert ist.
*/
export function coverImageSrc(
url: string | null | undefined,
id?: number | null,
hasCoverInDb?: boolean | null,
): string | null {
if (id && hasCoverInDb) {
return `/api/records/${id}/cover`
}
return cleanCoverUrl(url)
}