fix(import): resolve artist from path for enrich-on-duplicate (#718)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 25s
CI / integration (push) Successful in 3m19s

The dedup branches of _import_media linked the existing image to the new post via
_apply_sidecar(artist=None), relying on the SIDECAR to carry the artist. But an
archive member's artist comes from its path, and under post-first the per-media
sidecar is minimal (no artist) — so a re-packed / cross-posted archive image
deduped and was left UNLINKED from the new post, i.e. the post showed "no images".

Resolve the path-anchored artist (derive_top_level_artist) up-front in
_import_media and pass it to both enrich-on-duplicate branches (sha256 + phash
larger_exists) and the new-record path. Drop the now-dead _attach_artist helper
(its logic is inlined at the single new-record call site).

Surfaced by the new test_archive_all_deduped_is_benign_not_flagged (was asserting
2==4: the second post got no provenance links).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 21:25:55 -04:00
parent 8dee2f9628
commit b48ba60830
+18 -22
View File
@@ -682,6 +682,15 @@ class Importer:
error=f"{pct:.2%} transparent",
)
# Artist anchored to the attribution path (folder→artist), resolved
# UP-FRONT so the enrich-on-duplicate branches link provenance with the
# right artist even when the sidecar carries none — which is now the norm
# for archive members under post-first (the per-media sidecar is minimal).
# Without this a cross-posted / re-packed archive image deduped and was
# left UNLINKED from the new post = a post showing "no images" (#718).
_artist_name = derive_top_level_artist(attribution_path, self.import_root)
path_artist = self._upsert_artist(_artist_name) if _artist_name else None
# Hash dedup (exact).
sha = _sha256_of(source)
existing_stmt = select(ImageRecord).where(ImageRecord.sha256 == sha)
@@ -691,9 +700,9 @@ class Importer:
# Enrich-on-duplicate (parity with attach_in_place): a re-scanned
# file that is a byte-dup of an existing image still links its
# post via _apply_sidecar, so a cross-posted image shows on every
# post. Artist is derived from the sidecar here (not yet resolved).
# post.
self._apply_sidecar(
existing, attribution_path, None,
existing, attribution_path, path_artist,
explicit_source=explicit_source,
)
self.session.commit()
@@ -727,7 +736,7 @@ class Importer:
larger = self.session.get(ImageRecord, match_id)
if larger is not None:
self._apply_sidecar(
larger, attribution_path, None,
larger, attribution_path, path_artist,
explicit_source=explicit_source,
)
self.session.commit()
@@ -761,13 +770,11 @@ class Importer:
self.session.flush()
self._phash_cache_append(phash, width, height, record.id)
# Folder→artist (anchored to attribution_path).
artist = None
artist_name = derive_top_level_artist(
attribution_path, self.import_root
)
if artist_name:
artist = self._attach_artist(record, artist_name)
# Folder→artist (path_artist resolved up-front above); bind it to the
# new record so it carries the canonical Artist.
if path_artist is not None and record.artist_id is None:
record.artist_id = path_artist.id
self.session.flush()
# Sidecar provenance (best-effort; never fails the import).
# explicit_source lets the FC-3c download path bind the new
@@ -777,7 +784,7 @@ class Importer:
# subscription-downloaded zip previously lost subscription
# linkage if the on-disk layout didn't match assumptions.
self._apply_sidecar(
record, attribution_path, artist, explicit_source=explicit_source,
record, attribution_path, path_artist, explicit_source=explicit_source,
)
# Thumbnail is queued separately by the calling task; the importer
@@ -1076,17 +1083,6 @@ class Importer:
self.session.flush()
return artist
def _attach_artist(self, record: ImageRecord, artist_name: str) -> Artist:
"""Upsert the Artist and set it as the image's canonical artist.
Artist-kind tags were retired in FC-2d-vii-c — the Artist row is
the single source of truth. Returns the Artist (sidecar
provenance attaches its Source to it)."""
artist = self._upsert_artist(artist_name)
if record.artist_id is None:
record.artist_id = artist.id
self.session.flush()
return artist
@staticmethod
def _sidecar_artist_name(data: dict) -> str | None:
for k in ("artist", "author_name"):