feat(artist): pixiv display-name at add-time + identity-by-source (#130 steps 2+3)
Final piece of the artist decoupling. (1) Identity-by-source: quick_add_source resolves the artist by an existing (platform, url) Source first, so a re-add reuses the artist even after it was renamed (its frozen slug no longer matches the name) — a slug-based lookup would have duplicated it. (2) Pixiv naming: a new pixiv source resolves the real display name via the app API (PixivClient .resolve_display_name → /v1/user/detail) using the stored token, so the artist is 'Kurotsuchi Machi' not '12345678' — and its name-derived slug matches what a native download produces, unifying them. Falls back to the numeric id when no token/crypto. ExtensionService gains the crypto seam; the endpoint passes it. 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:
@@ -65,6 +65,47 @@ async def test_quick_add_source_idempotent(client, ext_key):
|
||||
assert body2["created_artist"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quick_add_reuses_source_artist_after_rename(client, ext_key):
|
||||
# #130 identity-by-source: after renaming the artist (slug frozen ≠ new
|
||||
# name-slug), re-adding the SAME source must reuse it — a slug-based lookup
|
||||
# would miss and duplicate the artist.
|
||||
url = "https://www.subscribestar.com/renamed-creator"
|
||||
h = {"X-Extension-Key": ext_key}
|
||||
a1 = (await (await client.post(
|
||||
"/api/extension/quick-add-source", json={"url": url}, headers=h
|
||||
)).get_json())["artist"]
|
||||
await client.patch(f"/api/artists/{a1['id']}", json={"name": "Totally Different"})
|
||||
b2 = await (await client.post(
|
||||
"/api/extension/quick-add-source", json={"url": url}, headers=h
|
||||
)).get_json()
|
||||
assert b2["created_artist"] is False
|
||||
assert b2["artist"]["id"] == a1["id"]
|
||||
assert b2["artist"]["name"] == "Totally Different"
|
||||
|
||||
|
||||
@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.
|
||||
from backend.app.services.credential_service import CredentialService
|
||||
from backend.app.services.extension_service import ExtensionService
|
||||
from backend.app.services.pixiv_client import PixivClient
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url,platform,slug", [
|
||||
("https://www.patreon.com/maewix", "patreon", "maewix"),
|
||||
("https://patreon.com/maewix", "patreon", "maewix"),
|
||||
|
||||
@@ -390,6 +390,24 @@ def test_verify_auth_bad_token():
|
||||
assert "rotate" in message.lower() or "rejected" in message.lower()
|
||||
|
||||
|
||||
def test_resolve_display_name(client, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
client, "_call",
|
||||
lambda e, p: {"user": {"name": "Kurotsuchi Machi", "id": p["user_id"]}},
|
||||
)
|
||||
assert client.resolve_display_name("99") == "Kurotsuchi Machi"
|
||||
|
||||
|
||||
def test_resolve_display_name_none_on_failure(client, monkeypatch):
|
||||
def boom(endpoint, params):
|
||||
raise PixivAPIError("nope", status_code=404)
|
||||
monkeypatch.setattr(client, "_call", boom)
|
||||
assert client.resolve_display_name("99") is None
|
||||
# Empty/whitespace name → None (caller falls back to the id).
|
||||
monkeypatch.setattr(client, "_call", lambda e, p: {"user": {"name": " "}})
|
||||
assert client.resolve_display_name("99") is None
|
||||
|
||||
|
||||
# -- rating ------------------------------------------------------------------------
|
||||
|
||||
def test_rating_label():
|
||||
|
||||
Reference in New Issue
Block a user