fix(download): dispatch archive/non-media in attach_in_place; reshuffle showcase on mount
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 14s
CI / frontend-build (push) Successful in 26s
CI / intimp (push) Successful in 3m32s
CI / intapi (push) Successful in 7m43s
CI / intcore (push) Successful in 8m14s

attach_in_place mirrored only the media flow, so gallery-dl-downloaded
zips/PDFs/audio bounced back as `skipped+invalid_image`, which
download_service counted as an ingest error and flipped runs to
status="error" despite N successful image attaches. Lustria patreon
event #38998 (21 images + 1 OST zip) went red for exactly this reason.

Now attach_in_place dispatches the same way as import_one: archives →
_import_archive (extracts media members, captures archive as
PostAttachment), non-media → _capture_attachment. Download_service
accepts the new `attached` result and treats non-duplicate skips as
soft skips, not ingest errors.

Also: ShowcaseView always loadInitial() on mount, not just when the
store is empty — Pinia persists across navigations and operator wants
a fresh shuffle every time the showcase loads.
This commit is contained in:
2026-06-02 08:16:13 -04:00
parent 412edec028
commit 91be9df671
4 changed files with 74 additions and 3 deletions
+21
View File
@@ -279,6 +279,21 @@ class DownloadService:
for img_id in ids:
generate_thumbnail.delay(img_id)
tag_and_embed.delay(img_id)
elif result.status == "attached":
# Non-media or extracted archive captured as PostAttachment
# (FC-2d-iii). The canonical copy lives in the attachments
# store; the original download path is now redundant —
# mirror duplicate_hash cleanup so we don't keep two copies.
# Operator-flagged 2026-06-02 (Lustria OST zip).
import_summary["attached"] += 1
try:
bytes_downloaded += path.stat().st_size # noqa: ASYNC240
except OSError:
pass
try:
path.unlink(missing_ok=True) # noqa: ASYNC240
except OSError:
pass
elif result.status == "skipped" and result.skip_reason and result.skip_reason.value in (
"duplicate_hash", "duplicate_phash",
):
@@ -287,6 +302,12 @@ class DownloadService:
path.unlink(missing_ok=True) # noqa: ASYNC240
except OSError:
pass
elif result.status == "skipped":
# Soft skip (too_small, too_transparent, invalid_image) —
# the file just didn't qualify, not a download/ingest
# failure. Don't flag the run as error; the file stays
# on disk for operator inspection.
import_summary["skipped"] += 1
else:
import_summary["errors"] += 1
+20 -2
View File
@@ -650,16 +650,34 @@ class Importer:
them through. The sidecar JSON gallery-dl emits next to each
downloaded file is read by `_apply_sidecar` via `find_sidecar`.
File-type dispatch parity with `import_one` (FC-2d-iii): zips,
PDFs, audio etc. become PostAttachments; archives are extracted.
Without this dispatch, gallery-dl-downloaded non-media bounced
back as `skipped+invalid_image`, which DownloadService counted
as an ingest error and flipped otherwise-successful runs to
status="error". Operator-flagged 2026-06-02 after a Lustria
patreon run with a 94MB OST zip went red despite 21 successful
image attaches.
Caller's responsibilities after this returns:
- duplicate_hash / duplicate_phash skip → delete the on-disk file
- superseded → file stays where it is (now canonical)
- imported → file stays where it is
- attached → the file's been copied into the attachments store;
caller may delete the on-disk original (mirrors duplicate_hash)
- failed → file untouched; caller decides
"""
if not is_supported(path):
if path.suffix.lower() == ".json":
return ImportResult(
status="skipped", skip_reason=SkipReason.invalid_image,
error=f"unsupported extension {path.suffix}",
error="sidecar json is metadata, not content",
)
if is_archive(path):
return self._import_archive(path)
if not is_supported(path):
post = self._post_for_sidecar(path, artist) if artist else None
return self._capture_attachment(
path, post=post, artist=artist, resolved=True,
)
# Format / dimension / transparency filters (mirror _import_media).
+6 -1
View File
@@ -70,7 +70,12 @@ function onKeydown(e) {
}
onMounted(() => {
if (store.images.length === 0) store.loadInitial()
// Operator-stated 2026-06-02: every load of the showcase view should
// show new images. Pinia persists the store across navigations, so
// returning to /showcase used to render the same set as before. Now
// we always reshuffle on mount — loadInitial() resets `seen`,
// `images`, and `exhausted` and pipelines a fresh batch.
store.loadInitial()
window.addEventListener('keydown', onKeydown)
})
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
+27
View File
@@ -159,3 +159,30 @@ def test_attach_in_place_invalid_image_returns_skipped(importer):
result = importer.attach_in_place(bad)
assert result.status == "skipped"
assert result.skip_reason.value == "invalid_image"
def test_attach_in_place_non_media_routes_to_attachment(importer, db_sync):
"""FC-2d-iii dispatch parity for the download path. A non-media file
(.pdf, .txt etc.) downloaded by gallery-dl must become a PostAttachment,
not bounce back as `skipped+invalid_image` — the latter flipped
otherwise-successful runs to status="error" downstream. Operator-flagged
2026-06-02 (Lustria patreon OST zip)."""
from backend.app.models import PostAttachment
images_root = importer.images_root
artist = Artist(name="Gus", slug="gus")
db_sync.add(artist)
db_sync.flush()
txt = images_root / "gus" / "patreon" / "post" / "notes.txt"
txt.parent.mkdir(parents=True, exist_ok=True)
txt.write_bytes(b"some non-media payload that the importer should preserve")
result = importer.attach_in_place(txt, artist=artist)
assert result.status == "attached"
row = db_sync.execute(
select(PostAttachment).where(PostAttachment.original_filename == "notes.txt")
).scalar_one()
assert row.ext == ".txt"
assert row.artist_id == artist.id