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
|
from .ir_ingest import manifest_path
|
||||||
|
|
||||||
# Per-platform artist-profile URL — used as Source.url when restoring
|
# Per-platform artist-profile URL — used as Source.url when restoring
|
||||||
# IR PostMetadata into FC. Keep this table in sync with
|
# IR PostMetadata into FC. Must cover every platform that
|
||||||
# backend/app/services/extension_service.py:_PLATFORM_PATTERNS and
|
# backend/app/services/extension_service.py:_PLATFORM_PATTERNS
|
||||||
# extension/lib/platforms.js.
|
# 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 = {
|
_PLATFORM_PROFILE_URL = {
|
||||||
"patreon": "https://www.patreon.com/{slug}",
|
"patreon": "https://www.patreon.com/{slug}",
|
||||||
"subscribestar": "https://www.subscribestar.com/{slug}",
|
"subscribestar": "https://www.subscribestar.com/{slug}",
|
||||||
"hentaifoundry": "https://www.hentai-foundry.com/user/{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
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_image_posts_dry_run_makes_no_writes(db, tmp_path):
|
async def test_image_posts_dry_run_makes_no_writes(db, tmp_path):
|
||||||
sha = "2" * 64
|
sha = "2" * 64
|
||||||
|
|||||||
Reference in New Issue
Block a user