feat(provenance): importer sets image_record.artist_id, stops minting artist tags
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user