9cbdb70e13
Two coupled problems, operator-flagged 2026-06-01: "missing thumbnails
but triggering backfill found nothing."
1. **Backfill UI was a black box.** `POST /api/thumbnails/backfill`
returned just `{celery_task_id}` and the admin card said
"Enqueued." with no counts. There was no way to tell whether the
scan found 0 candidates, 5000 candidates, or whether the worker
even picked up the task. "Found nothing" was indistinguishable
from a broken queue.
Fix: refactor the scan into a sync helper (`_run_backfill_scan`)
shared by the Celery task and the API endpoint. The API now runs
the scan in an executor and returns `{scanned, enqueued, ok,
regenerated}`. The actual thumbnail generation work still goes
to the thumbnail Celery queue per row via
`generate_thumbnail.delay()` — the scan itself is fast
(SELECT id+thumbnail_path + a file.stat() per row).
2. **`_thumb_is_valid` accepted header-only corrupt files.** The
magic-byte check passed for any 8-byte file starting with a JPEG
or PNG header, including empty/truncated/zero-pad files that
browsers render as broken. Backfill counted these as `ok` and
never regenerated.
Fix: also require file size ≥ MIN_THUMB_BYTES (256). Real
thumbnails are minimum ~2KB even on solid-color sources; header-
only corrupt files top out around 12 bytes. 256 is well above
the corrupt floor and well below any legitimate thumbnail.
Plus the admin card now shows the per-run counts instead of
"Enqueued.":
Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok
42 lines
1.4 KiB
Vue
42 lines
1.4 KiB
Vue
<template>
|
|
<v-card>
|
|
<v-card-title>Thumbnail backfill</v-card-title>
|
|
<v-card-text>
|
|
<p class="text-body-2 mb-3">
|
|
Scan the library for images with no thumbnail, or whose thumbnail file
|
|
is missing or corrupt on disk. Repair candidates are re-enqueued for
|
|
thumbnail generation. Safe to re-run.
|
|
</p>
|
|
<v-btn color="primary" rounded="pill" :loading="busy" @click="run">
|
|
<v-icon start>mdi-image-refresh</v-icon> Run backfill now
|
|
</v-btn>
|
|
<span v-if="result" class="ml-3 text-caption">
|
|
Scanned <strong>{{ result.scanned }}</strong> · enqueued
|
|
<strong>{{ result.enqueued }}</strong>
|
|
<span v-if="result.regenerated > 0">
|
|
({{ result.regenerated }} regenerated)
|
|
</span>
|
|
· {{ result.ok }} ok
|
|
</span>
|
|
<QueueStatusBar queue="thumbnail" queue-label="Thumbnail" />
|
|
</v-card-text>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { toast } from '../../utils/toast.js'
|
|
import { ref } from 'vue'
|
|
import { useThumbnailsStore } from '../../stores/thumbnails.js'
|
|
import QueueStatusBar from './QueueStatusBar.vue'
|
|
const store = useThumbnailsStore()
|
|
const busy = ref(false)
|
|
const result = ref(null)
|
|
async function run () {
|
|
busy.value = true
|
|
result.value = null
|
|
try { result.value = await store.triggerBackfill() }
|
|
catch (e) { toast({ text: e.message, type: 'error' }) }
|
|
finally { busy.value = false }
|
|
}
|
|
</script>
|