feat(artist): pixiv display-name at add-time + identity-by-source (#130 steps 2+3)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 29s
CI / integration (push) Successful in 3m32s

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:
2026-07-04 22:24:25 -04:00
parent a69bd1baa8
commit 0c4b8aef8c
5 changed files with 123 additions and 3 deletions
+18
View File
@@ -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():