fix(migrators): tag_apply phase 4 now covers deviantart + pixiv (was silently dropping IR PostMetadata from those platforms)
_PLATFORM_PROFILE_URL had only patreon/subscribestar/hentaifoundry but FC's extension_service.py recognizes 5 platforms. Any IR PostMetadata with platform=deviantart or pixiv fell through _profile_url returning None and the entry was silently skipped — explaining operator's 2026-05-25 finding that IR-migrated images had tags but no provenance for the deviantart + pixiv subscriptions. Pixiv caveat noted in comment: real profile URL takes numeric user_id (https://www.pixiv.net/users/12345) but IR's PostMetadata.artist stores display name. We slug the name and use it as if it were the id so the artist->post->image linkage survives migration; the resulting Source.url won't resolve in a browser and operator can fix via Settings -> Subscriptions later if they want. To recover existing IR-migrated state: re-run /api/migrate/tag_apply on the existing manifest. Phase 4 is idempotent; new posts get inserted only for the previously-skipped platforms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,13 +37,25 @@ from ...utils.slug import slugify
|
||||
from .ir_ingest import manifest_path
|
||||
|
||||
# Per-platform artist-profile URL — used as Source.url when restoring
|
||||
# IR PostMetadata into FC. Keep this table in sync with
|
||||
# backend/app/services/extension_service.py:_PLATFORM_PATTERNS and
|
||||
# extension/lib/platforms.js.
|
||||
# IR PostMetadata into FC. Must cover every platform that
|
||||
# backend/app/services/extension_service.py:_PLATFORM_PATTERNS
|
||||
# recognizes; an entry missing here silently drops ALL PostMetadata for
|
||||
# that platform during phase 4 (operator hit this 2026-05-25:
|
||||
# DeviantArt + Pixiv posts in the IR migration produced empty
|
||||
# ImageProvenance because they fell through this table).
|
||||
#
|
||||
# Pixiv caveat: the real profile URL takes a numeric user_id
|
||||
# (https://www.pixiv.net/users/12345), but IR's PostMetadata.artist
|
||||
# stores the display name not the id. We use the slugified name here
|
||||
# so we preserve the artist→post→image linkage; the resulting Source.url
|
||||
# won't resolve in a browser and the operator may want to manually fix
|
||||
# it via Settings → Subscriptions once the migration lands.
|
||||
_PLATFORM_PROFILE_URL = {
|
||||
"patreon": "https://www.patreon.com/{slug}",
|
||||
"subscribestar": "https://www.subscribestar.com/{slug}",
|
||||
"hentaifoundry": "https://www.hentai-foundry.com/user/{slug}",
|
||||
"deviantart": "https://www.deviantart.com/{slug}",
|
||||
"pixiv": "https://www.pixiv.net/users/{slug}",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -281,6 +281,42 @@ async def test_image_posts_unknown_platform_skipped(db, tmp_path):
|
||||
assert src_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platform,expected_url", [
|
||||
("deviantart", "https://www.deviantart.com/maewix"),
|
||||
("pixiv", "https://www.pixiv.net/users/maewix"),
|
||||
])
|
||||
@pytest.mark.asyncio
|
||||
async def test_image_posts_extended_platforms_create_source(
|
||||
db, tmp_path, platform, expected_url,
|
||||
):
|
||||
"""Regression for 2026-05-25 operator-reported bug: phase 4's
|
||||
_PLATFORM_PROFILE_URL had only patreon/subscribestar/hentaifoundry,
|
||||
silently dropping deviantart + pixiv PostMetadata from the IR migration."""
|
||||
sha = f"{platform[0]}" * 64
|
||||
await _seed_image(db, sha, suffix=f"_{platform}")
|
||||
await db.commit()
|
||||
|
||||
_write_manifest(tmp_path, image_posts=[
|
||||
{**_POST_ENTRY, "platform": platform, "image_sha256s": [sha]},
|
||||
])
|
||||
|
||||
result = await tag_apply.apply_async(db, images_root=tmp_path, dry_run=False)
|
||||
assert result["counts"]["rows_inserted"] >= 1
|
||||
|
||||
src_url = (await db.execute(
|
||||
select(Source.url).where(Source.platform == platform)
|
||||
)).scalar_one()
|
||||
assert src_url == expected_url
|
||||
|
||||
# ImageProvenance row was created.
|
||||
prov_count = (await db.execute(
|
||||
select(func.count(ImageProvenance.id))
|
||||
.join(Source, Source.id == ImageProvenance.source_id)
|
||||
.where(Source.platform == platform)
|
||||
)).scalar_one()
|
||||
assert prov_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_image_posts_dry_run_makes_no_writes(db, tmp_path):
|
||||
sha = "2" * 64
|
||||
|
||||
Reference in New Issue
Block a user