From 914cf14d9e3d212bab77729adb04914e947be4ac Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 18 May 2026 21:33:55 -0400 Subject: [PATCH] feat(provenance): importer sets image_record.artist_id, stops minting artist tags Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/services/importer.py | 40 +++++++---------------- tests/test_importer_artist_id.py | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 tests/test_importer_artist_id.py diff --git a/backend/app/services/importer.py b/backend/app/services/importer.py index 82296ed..589fa2a 100644 --- a/backend/app/services/importer.py +++ b/backend/app/services/importer.py @@ -27,10 +27,7 @@ from ..models import ( ImportSettings, Post, Source, - Tag, - TagKind, ) -from ..models.tag import image_tag from ..utils.paths import derive_subdir, derive_top_level_artist, hash_suffixed_name from ..utils.phash import compute_phash, find_similar from ..utils.sidecar import find_sidecar, parse_sidecar @@ -244,7 +241,7 @@ class Importer: artist = None artist_name = derive_top_level_artist(source, self.import_root) if artist_name: - artist = self._attach_artist_tag(record.id, artist_name) + artist = self._attach_artist(record, artist_name) # Sidecar provenance (best-effort; never fails the import). self._apply_sidecar(record, source, artist) @@ -266,33 +263,15 @@ class Importer: self.session.flush() return artist - def _attach_artist_tag(self, image_id: int, artist_name: str) -> Artist: - """Idempotently creates Artist + artist tag, attaches to image. - Returns the Artist (sidecar provenance attaches its Source to it).""" + 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) - - tag = self.session.execute( - select(Tag).where(Tag.name == artist_name).where(Tag.kind == TagKind.artist) - ).scalar_one_or_none() - if tag is None: - tag = Tag(name=artist_name, kind=TagKind.artist) - self.session.add(tag) + if record.artist_id is None: + record.artist_id = artist.id self.session.flush() - - # Idempotent association; using a raw insert + on-conflict-do-nothing - # is cleaner here than fetching first, but for a sync session we just - # check existence. - already = self.session.execute( - select(image_tag.c.tag_id) - .where(image_tag.c.image_record_id == image_id) - .where(image_tag.c.tag_id == tag.id) - ).scalar_one_or_none() - if already is None: - self.session.execute( - image_tag.insert().values( - image_record_id=image_id, tag_id=tag.id, source="auto", - ) - ) return artist @staticmethod @@ -339,6 +318,9 @@ class Importer: ) return + if record.artist_id is None: + record.artist_id = artist.id + platform = sd.platform or "unknown" url = sd.post_url or f"sidecar:{platform}" src = self.session.execute( diff --git a/tests/test_importer_artist_id.py b/tests/test_importer_artist_id.py new file mode 100644 index 0000000..3ce7aae --- /dev/null +++ b/tests/test_importer_artist_id.py @@ -0,0 +1,54 @@ +"""FC-2d-vii-c: importer sets artist_id, creates no artist tags.""" + +import pytest +from PIL import Image +from sqlalchemy import func, select + +from backend.app.models import Artist, ImageRecord, ImportSettings, Tag, TagKind +from backend.app.services.importer import Importer +from backend.app.services.thumbnailer import Thumbnailer + +pytestmark = pytest.mark.integration + + +@pytest.fixture +def import_layout(tmp_path): + import_root = tmp_path / "import" + images_root = tmp_path / "images" + import_root.mkdir() + images_root.mkdir() + return import_root, images_root + + +@pytest.fixture +def importer(db_sync, import_layout): + import_root, images_root = import_layout + settings = db_sync.execute( + select(ImportSettings).where(ImportSettings.id == 1) + ).scalar_one() + return Importer( + session=db_sync, images_root=images_root, import_root=import_root, + thumbnailer=Thumbnailer(images_root=images_root), settings=settings, + ) + + +def _img(path): + path.parent.mkdir(parents=True, exist_ok=True) + Image.new("RGB", (64, 64), (123, 200, 50)).save(path, "JPEG") + + +def test_folder_import_sets_artist_id_no_tag(importer, import_layout): + import_root, _ = import_layout + m = import_root / "Alice" / "pic.jpg" + _img(m) + r = importer.import_one(m) + assert r.status == "imported" + rec = importer.session.get(ImageRecord, r.image_id) + artist = importer.session.execute( + select(Artist).where(Artist.slug == "alice") + ).scalar_one() + assert rec.artist_id == artist.id + n_artist_tags = importer.session.execute( + select(func.count()).select_from(Tag).where(Tag.kind == TagKind.artist) + ).scalar_one() + assert n_artist_tags == 0