feat(maintenance): reconcile duplicate posts (gallery-dl→native unify) #124

Merged
bvandeusen merged 1 commits from dev into main 2026-06-17 22:01:12 -04:00
Owner

What

Unifies duplicate post records created when an artist was first downloaded by gallery-dl (posts keyed by the per-attachment id) and later walked by the native ingester (posts keyed by the real post id). They never dedup (uq_post_source_external_id is on external_post_id) → duplicate rows (cheunart: 943→1109). Images are not touched (content-addressed/deduped already; operator-confirmed). Canonical key = the post id (the format the current native downloader produces), so future walks dedup and this can't recur.

How

  • Matching key is recoverable in-DB: Post.raw_metadata["post_id"] (both eras store the sidecar there). Confirmed on disk: legacy external_post_id = the sidecar's attachment id, ≠ post_id.
  • reconcile_duplicate_posts (cleanup_service): group by (source_id, canonical post_id); keep the post-id-keyed row; conflict-safe re-point of ImageRecord.primary_post_id / ImageProvenance / PostAttachment / ExternalLink (drop the loser's row where the keeper already has the equivalent, per each table's uniqueness); backfill the keeper's empty date/title/body/raw_metadata from a loser; set external_post_id=post id + derive post_url; delete losers.
  • Preview/apply share find_duplicate_post_groups (rule 93). API /api/admin/posts/reconcile-duplicates. UI: a second section on PostMaintenanceCard (preview groups+sample → confirm merge).

Tests (all PASSED in CI)

merge + metadata backfill + image move; no-op when unique; provenance-collision dedup. Lint/unit/frontend green; reconcile integration tests confirmed green in run 1258.

Design: milestone #73. Forensics: note #917. Out of scope (flagged): cheunart vs Cheunart case-variant artist dirs.

🤖 Generated with Claude Code

## What Unifies duplicate **post records** created when an artist was first downloaded by gallery-dl (posts keyed by the per-attachment id) and later walked by the native ingester (posts keyed by the real post id). They never dedup (`uq_post_source_external_id` is on `external_post_id`) → duplicate rows (cheunart: 943→1109). **Images are not touched** (content-addressed/deduped already; operator-confirmed). Canonical key = the post id (the format the current native downloader produces), so future walks dedup and this can't recur. ## How - Matching key is recoverable **in-DB**: `Post.raw_metadata["post_id"]` (both eras store the sidecar there). Confirmed on disk: legacy `external_post_id` = the sidecar's attachment `id`, ≠ `post_id`. - `reconcile_duplicate_posts` (cleanup_service): group by `(source_id, canonical post_id)`; keep the post-id-keyed row; conflict-safe re-point of `ImageRecord.primary_post_id` / `ImageProvenance` / `PostAttachment` / `ExternalLink` (drop the loser's row where the keeper already has the equivalent, per each table's uniqueness); backfill the keeper's empty date/title/body/raw_metadata from a loser; set `external_post_id`=post id + derive `post_url`; delete losers. - Preview/apply share `find_duplicate_post_groups` (rule 93). API `/api/admin/posts/reconcile-duplicates`. UI: a second section on `PostMaintenanceCard` (preview groups+sample → confirm merge). ## Tests (all PASSED in CI) merge + metadata backfill + image move; no-op when unique; provenance-collision dedup. Lint/unit/frontend green; reconcile integration tests confirmed green in run 1258. Design: milestone #73. Forensics: note #917. Out of scope (flagged): `cheunart` vs `Cheunart` case-variant artist dirs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 1 commit 2026-06-17 22:01:03 -04:00
feat(maintenance): reconcile duplicate posts (gallery-dl→native unify)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 38s
CI / integration (push) Successful in 3m17s
eff64275fc
An artist first downloaded by gallery-dl gets Post rows keyed by the per-
attachment id; a later native walk keys the SAME real post by the post id. They
never dedup (uq_post_source_external_id is on external_post_id) → duplicate post
rows (cheunart: 943→1109). The real post id is recoverable in-DB from
raw_metadata['post_id'] (both eras store the sidecar there).

reconcile_duplicate_posts (cleanup_service): group posts by (source_id, canonical
post_id = raw_metadata.post_id else external_post_id); for each group >1, keep the
row already keyed by the post id (the format the CURRENT native downloader
produces, so future walks dedup and this can't recur), re-point
ImageRecord.primary_post_id / ImageProvenance / PostAttachment / ExternalLink onto
it conflict-safe (drop the loser's row where the keeper already has the equivalent,
per each table's uniqueness), backfill the keeper's empty date/title/body/raw_meta
from a loser, set external_post_id=post_id + derive post_url, delete losers.
IMAGES ARE NOT TOUCHED (content-addressed/deduped already; operator-confirmed).

Preview/apply share find_duplicate_post_groups (rule 93). API
/api/admin/posts/reconcile-duplicates (dry_run→{groups,posts_to_merge,sample};
apply→{groups,merged,sample}; optional source_id). UI: a second section on
PostMaintenanceCard (preview groups+sample → confirm merge). Tests: merge +
metadata backfill + image move, no-op when unique, provenance-collision dedup.

Design: milestone #73. Forensics: note #917. Out of scope (flagged): cheunart vs
Cheunart case-variant artist dirs/rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bvandeusen merged commit 57fe15c267 into main 2026-06-17 22:01:12 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledCurator#124