fix(download): dispatch archive/non-media in attach_in_place; reshuffle showcase on mount
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:
@@ -279,6 +279,21 @@ class DownloadService:
|
|||||||
for img_id in ids:
|
for img_id in ids:
|
||||||
generate_thumbnail.delay(img_id)
|
generate_thumbnail.delay(img_id)
|
||||||
tag_and_embed.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 (
|
elif result.status == "skipped" and result.skip_reason and result.skip_reason.value in (
|
||||||
"duplicate_hash", "duplicate_phash",
|
"duplicate_hash", "duplicate_phash",
|
||||||
):
|
):
|
||||||
@@ -287,6 +302,12 @@ class DownloadService:
|
|||||||
path.unlink(missing_ok=True) # noqa: ASYNC240
|
path.unlink(missing_ok=True) # noqa: ASYNC240
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
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:
|
else:
|
||||||
import_summary["errors"] += 1
|
import_summary["errors"] += 1
|
||||||
|
|
||||||
|
|||||||
@@ -650,16 +650,34 @@ class Importer:
|
|||||||
them through. The sidecar JSON gallery-dl emits next to each
|
them through. The sidecar JSON gallery-dl emits next to each
|
||||||
downloaded file is read by `_apply_sidecar` via `find_sidecar`.
|
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:
|
Caller's responsibilities after this returns:
|
||||||
- duplicate_hash / duplicate_phash skip → delete the on-disk file
|
- duplicate_hash / duplicate_phash skip → delete the on-disk file
|
||||||
- superseded → file stays where it is (now canonical)
|
- superseded → file stays where it is (now canonical)
|
||||||
- imported → file stays where it is
|
- 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
|
- failed → file untouched; caller decides
|
||||||
"""
|
"""
|
||||||
if not is_supported(path):
|
if path.suffix.lower() == ".json":
|
||||||
return ImportResult(
|
return ImportResult(
|
||||||
status="skipped", skip_reason=SkipReason.invalid_image,
|
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).
|
# Format / dimension / transparency filters (mirror _import_media).
|
||||||
|
|||||||
@@ -70,7 +70,12 @@ function onKeydown(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
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)
|
window.addEventListener('keydown', onKeydown)
|
||||||
})
|
})
|
||||||
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
||||||
|
|||||||
@@ -159,3 +159,30 @@ def test_attach_in_place_invalid_image_returns_skipped(importer):
|
|||||||
result = importer.attach_in_place(bad)
|
result = importer.attach_in_place(bad)
|
||||||
assert result.status == "skipped"
|
assert result.status == "skipped"
|
||||||
assert result.skip_reason.value == "invalid_image"
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user