diff --git a/backend/app/services/download_service.py b/backend/app/services/download_service.py index 66e960b..0fc04e6 100644 --- a/backend/app/services/download_service.py +++ b/backend/app/services/download_service.py @@ -269,6 +269,11 @@ class DownloadService: source_row = self.sync_session.get(Source, ctx["source_id"]) import_summary = {"attached": 0, "skipped": 0, "errors": 0} + # Archives detected but captured WITHOUT extracting any image (probe + # rejected / corrupt / missing extractor backend). Surfaced on the event + # so a post showing "no images" beside a zip is diagnosable (plan + # follow-up 2026-06-06 — the recurring archive-association report). + unextracted_archives: list[dict] = [] bytes_downloaded = 0 loop = asyncio.get_running_loop() @@ -322,6 +327,17 @@ class DownloadService: # mirror duplicate_hash cleanup so we don't keep two copies. # Operator-flagged 2026-06-02 (Lustria OST zip). import_summary["attached"] += 1 + # An archive captured WITHOUT extracting any image carries a + # reason on result.error — record it so the event explains the + # "no images beside a zip" symptom instead of staying silent. + if result.error: + unextracted_archives.append( + {"file": path.name, "reason": result.error} + ) + log.warning( + "archive captured unextracted (%s): %s", + path.name, result.error, + ) try: bytes_downloaded += path.stat().st_size # noqa: ASYNC240 except OSError: @@ -403,6 +419,10 @@ class DownloadService: "duration_seconds": dl_result.duration_seconds, "quarantined_paths": dl_result.quarantined_paths or None, "import_summary": import_summary, + # Archives detected but captured without extracting an image — the + # recurring "post shows a zip but no images" report. Each entry is + # {file, reason}; None when every archive extracted cleanly. + "unextracted_archives": unextracted_archives or None, } await self._update_source_health( source_id=ctx["source_id"], status=status, error_message=ev.error, diff --git a/backend/app/services/importer.py b/backend/app/services/importer.py index e1acc6e..c954bfe 100644 --- a/backend/app/services/importer.py +++ b/backend/app/services/importer.py @@ -430,13 +430,17 @@ class Importer: self._capture_attachment( source, post=post, artist=artist_use, resolved=True, ) - return ImportResult(status="attached") + reason = f"archive probe rejected, captured unextracted: {probe.reason}" + log.warning("%s: %s", source.name, reason) + return ImportResult(status="attached", error=reason) artist_use = artist if artist is not None else self._resolve_artist(source) post = self._post_for_sidecar(source, artist_use) member_ids: list[int] = [] + member_total = 0 with extract_archive(source) as members: for _name, member_path in members: + member_total += 1 if not is_supported(member_path): continue # non-media preserved via the stored archive res = self._import_media( @@ -453,7 +457,19 @@ class Importer: status="imported", image_id=member_ids[0], member_image_ids=member_ids, ) - return ImportResult(status="attached") + # No images landed — surface WHY so a post showing "no images" beside an + # archive is diagnosable instead of silent. Zero members usually means + # the extractor backend is missing/failed (unar for rar, py7zr for 7z) + # or the file is corrupt; non-zero-but-no-images means it held only + # non-media files. + reason = ( + "archive extracted but held no supported image/video members" + if member_total + else "archive yielded no members (unsupported/corrupt, or the " + "extractor backend failed)" + ) + log.warning("%s: %s", source.name, reason) + return ImportResult(status="attached", error=reason) def _import_media( self, source: Path, attribution_path: Path,