fix(recapture): link on-disk images to their post (#1288)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 3m32s

Recapture disk-skips already-downloaded media, and upsert_post_record only
writes Post fields — so a pre-existing image (e.g. one pulled under the old
gallery-dl path, imported bare with no post) stays orphaned even after its post
record is (re)written. Confirmed on the operator's instance: 329 pixiv images
with primary_post_id NULL, 694 pixiv posts with content but no linked images, 0
duplicate posts.

Fix: the recapture relink channel now carries the media's post_id (2- → 3-tuple
path/url/post_id), and phase 3 calls importer.link_existing_image_to_post — match
the on-disk image by path, find its Post by (source, external_post_id), upsert
image_provenance + primary_post_id. Factored the provenance-linking out of
_apply_sidecar into a shared _attach_provenance so the fresh-import and
recapture-backlink paths can't diverge. Idempotent; generic across native
platforms (no-op for already-linked Patreon/SubscribeStar). Re-running recapture
now repairs orphaned images; future walks never orphan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-04 20:13:53 -04:00
parent 437bf4d37a
commit 8838b325fb
6 changed files with 205 additions and 76 deletions
+20 -5
View File
@@ -418,7 +418,8 @@ class DownloadService:
# the duplicate file). Empty outside recapture mode.
relink_pairs = getattr(dl_result, "relink_source_paths", None) or []
relinked = 0
for rel_str, rel_url in relink_pairs:
post_linked = 0
for rel_str, rel_url, rel_post_id in relink_pairs:
rel_path = Path(rel_str)
if not rel_path.exists(): # noqa: ASYNC240
continue
@@ -428,13 +429,27 @@ class DownloadService:
if await loop.run_in_executor(None, _relink):
relinked += 1
# #1288: link the on-disk image to its Post. Recapture disk-skips the
# media (never re-imported), so a pre-existing image (e.g. one pulled
# under the old gallery-dl path) otherwise stays orphaned even after
# the post record is written. Idempotent for already-linked images.
def _link(p=rel_path, pid=rel_post_id):
return self.importer.link_existing_image_to_post(
p, pid, source=source_row, artist=artist,
)
if await loop.run_in_executor(None, _link):
post_linked += 1
if relink_pairs:
# recapture diagnostic: how many on-disk images got their
# source_filehash backfilled (inline-image localization). < total is
# normal — files already carrying a filehash are skipped (NULL-only).
# source_filehash backfilled (inline-image localization) and how many
# got (re)linked to their Post. < total for source_filehash is normal
# (files already carrying a filehash are skipped, NULL-only).
log.info(
"recap: relinked source_filehash on %d/%d on-disk image(s)",
relinked, len(relink_pairs),
"recap: relinked source_filehash on %d and linked %d/%d on-disk "
"image(s) to their post",
relinked, post_linked, len(relink_pairs),
)
# Kick the off-platform file-host downloader for any links this run