feat(artist): resolve patreon + subscribestar display names at add-time (#130 step 5)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m41s

Parity with pixiv (operator ask): the extension add now resolves the real
display name for our other native platforms too, not just the URL handle.
patreon_resolver.resolve_display_name reads the campaigns API's
attributes.name; SubscribeStarClient.resolve_display_name pulls the creator
name off the profile page (og:title, else the <title> stripped of the
SubscribeStar suffix). extension_service._resolve_artist_name dispatches per
platform (pixiv=token, patreon/subscribestar=cookies via get_cookies_path),
best-effort in an executor, falling back to the readable URL handle on any
failure. Still all curator core — the extension is unchanged (sends only the
URL). gallery-dl platforms keep the handle (readable, no native client).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-04 22:37:50 -04:00
parent 0c4b8aef8c
commit 0963bf0db3
6 changed files with 187 additions and 30 deletions
+30 -13
View File
@@ -85,25 +85,42 @@ async def test_quick_add_reuses_source_artist_after_rename(client, ext_key):
@pytest.mark.asyncio
async def test_resolve_artist_name_pixiv_uses_api(db, monkeypatch):
# #130: a pixiv add resolves the display name via the app API (with a token),
# not the numeric id; falls back to the id without crypto/token.
async def test_resolve_artist_name_dispatches_per_platform(db, monkeypatch):
# #130: each native platform resolves its real display name at add-time
# (pixiv=token API, patreon=campaigns API, subscribestar=profile page);
# gallery-dl platforms and any failure fall back to the URL handle.
from backend.app.services import patreon_resolver
from backend.app.services.credential_service import CredentialService
from backend.app.services.extension_service import ExtensionService
from backend.app.services.pixiv_client import PixivClient
from backend.app.services.subscribestar_client import SubscribeStarClient
async def _tok(self, platform):
return "tok" if platform == "pixiv" else None
monkeypatch.setattr(CredentialService, "get_token", _tok)
monkeypatch.setattr(
PixivClient, "resolve_display_name", lambda self, uid: "Kurotsuchi Machi"
)
return "tok"
svc = ExtensionService(db, crypto=object()) # crypto unused (token stubbed)
assert await svc._resolve_artist_name("pixiv", "555") == "Kurotsuchi Machi"
assert await svc._resolve_artist_name("patreon", "alice") == "alice" # passthrough
# No crypto → no resolution attempt → fall back to the raw handle.
assert await ExtensionService(db)._resolve_artist_name("pixiv", "555") == "555"
async def _cookies(self, platform):
return "/tmp/cookies.txt"
monkeypatch.setattr(CredentialService, "get_token", _tok)
monkeypatch.setattr(CredentialService, "get_cookies_path", _cookies)
monkeypatch.setattr(PixivClient, "resolve_display_name", lambda self, uid: "Pixiv Name")
monkeypatch.setattr(patreon_resolver, "resolve_display_name", lambda v, c: "Patreon Name")
monkeypatch.setattr(SubscribeStarClient, "resolve_display_name", lambda self, u: "SS Name")
svc = ExtensionService(db, crypto=object()) # crypto seam only (calls stubbed)
assert await svc._resolve_artist_name(
"pixiv", "555", "https://www.pixiv.net/users/555") == "Pixiv Name"
assert await svc._resolve_artist_name(
"patreon", "maewix", "https://patreon.com/maewix") == "Patreon Name"
assert await svc._resolve_artist_name(
"subscribestar", "sabu", "https://subscribestar.adult/sabu") == "SS Name"
# gallery-dl platform → readable handle passthrough (no resolver).
assert await svc._resolve_artist_name("hentaifoundry", "Foo", "u") == "Foo"
# No crypto → no resolution attempt → the raw handle.
assert await ExtensionService(db)._resolve_artist_name("pixiv", "555", "u") == "555"
# Resolver returns None → fall back to the handle.
monkeypatch.setattr(patreon_resolver, "resolve_display_name", lambda v, c: None)
assert await svc._resolve_artist_name("patreon", "maewix", "u") == "maewix"
@pytest.mark.parametrize("url,platform,slug", [