feat: the artist picker on Patreon and SubscribeStar too — and the Patreon name is canon (milestone 429)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
CI and images / extension-test (push) Successful in 19s
CI and images / frontend-build (push) Successful in 25s
CI and images / backend-lint-and-test (push) Successful in 33s
CI and images / integration (push) Successful in 2m22s
CI and images / build-agent (push) Successful in 6s
CI and images / sign-extension (push) Successful in 4m48s
CI and images / build-web (push) Failing after 6s
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped

Operator: "yes add the artist picker to patreon and subscribestar too. but
generally we treat the patreon name as the canon"

- Every add now goes through the panel. On Patreon/SubscribeStar it opens
  on the creator's display name (read only when the panel opens:
  probe?names=1), searches FC's artists with it, and auto-picks a match.
  An untouched URL handle sends no name, so the server still resolves it.
- Patreon is canon: joining a Patreon source to an artist known by another
  name offers "Rename “x” to the Patreon name “X”", ticked by default.
  quick-add's use_platform_name renames server-side from the name it reads
  itself; name only, the slug never moves (#130); never to a URL handle when
  the name can't be read; ignored on SubscribeStar and Discord.
- _platform_display_name returns None rather than the handle, bounded by the
  same 6s lookup budget as Discord's names.
- panelDefaults / addRequest / renameOffer replace the Discord-only helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-25 07:50:48 -04:00
co-authored by Claude Opus 5.5
parent eeb9263125
commit 423275a1e5
9 changed files with 386 additions and 86 deletions
+115
View File
@@ -526,6 +526,121 @@ async def test_probe_a_discord_dm_is_not_a_source(client, ext_key):
assert body["state"] == "unknown_platform"
# --- Patreon is canon: the Add panel's names and the rename (milestone 429) ---
@pytest.fixture
def platform_names(monkeypatch):
"""Stub both platforms' display-name lookups (no network in tests)."""
from backend.app.services import patreon_resolver
from backend.app.services.credential_service import CredentialService
from backend.app.services.subscribestar_client import SubscribeStarClient
async def _cookies(self, platform):
return "/tmp/cookies.txt"
names = {"patreon": "Tamada Heijun", "subscribestar": "SS Tamada"}
monkeypatch.setattr(CredentialService, "get_cookies_path", _cookies)
monkeypatch.setattr(
patreon_resolver, "resolve_display_name", lambda v, c: names["patreon"],
)
monkeypatch.setattr(
SubscribeStarClient, "resolve_display_name", lambda self, u: names["subscribestar"],
)
return names
@pytest.mark.asyncio
async def test_probe_with_names_reads_the_patreon_display_name(client, ext_key, platform_names):
resp = await client.get(
"/api/extension/probe",
query_string={"url": "https://www.patreon.com/tamadaheijun", "names": "1"},
headers={"X-Extension-Key": ext_key},
)
body = await resp.get_json()
assert body["state"] == "new"
assert body["display_name"] == "Tamada Heijun"
@pytest.mark.asyncio
async def test_a_plain_probe_does_not_look_the_name_up(client, ext_key, platform_names):
resp = await client.get(
"/api/extension/probe",
query_string={"url": "https://www.patreon.com/tamadaheijun"},
headers={"X-Extension-Key": ext_key},
)
assert "display_name" not in await resp.get_json()
async def _artist(db, name, slug):
artist = Artist(name=name, slug=slug, is_subscription=True)
db.add(artist)
await db.commit()
return artist
@pytest.mark.asyncio
async def test_a_patreon_source_renames_the_artist_it_joins_to_the_patreon_name(
client, ext_key, db, db_sync, platform_names,
):
artist = await _artist(db, "tamada", "tamada")
resp = await client.post(
"/api/extension/quick-add-source",
json={"url": "https://www.patreon.com/tamadaheijun",
"artist_id": artist.id, "use_platform_name": True},
headers={"X-Extension-Key": ext_key},
)
assert resp.status_code == 201
body = await resp.get_json()
assert body["artist"]["name"] == "Tamada Heijun"
assert body["renamed_from"] == "tamada"
# Name only: the slug, and every path keyed off it, stays.
row = db_sync.execute(select(Artist.name, Artist.slug).where(Artist.id == artist.id)).one()
assert tuple(row) == ("Tamada Heijun", "tamada")
@pytest.mark.asyncio
async def test_no_rename_without_the_flag(client, ext_key, db, platform_names):
artist = await _artist(db, "tamada", "tamada")
body = await (await client.post(
"/api/extension/quick-add-source",
json={"url": "https://www.patreon.com/tamadaheijun", "artist_id": artist.id},
headers={"X-Extension-Key": ext_key},
)).get_json()
assert body["artist"]["name"] == "tamada"
assert "renamed_from" not in body
@pytest.mark.asyncio
async def test_an_unreadable_patreon_name_never_renames_to_the_handle(
client, ext_key, db, platform_names,
):
platform_names["patreon"] = None
artist = await _artist(db, "Tamada", "tamada")
body = await (await client.post(
"/api/extension/quick-add-source",
json={"url": "https://www.patreon.com/tamadaheijun",
"artist_id": artist.id, "use_platform_name": True},
headers={"X-Extension-Key": ext_key},
)).get_json()
assert body["artist"]["name"] == "Tamada"
assert "renamed_from" not in body
@pytest.mark.asyncio
async def test_only_patreon_names_are_canon(client, ext_key, db, platform_names):
"""A SubscribeStar source joins the picked artist under the name it has."""
artist = await _artist(db, "Tamada Heijun", "tamada-heijun")
body = await (await client.post(
"/api/extension/quick-add-source",
json={"url": "https://subscribestar.adult/tamada",
"artist_id": artist.id, "use_platform_name": True},
headers={"X-Extension-Key": ext_key},
)).get_json()
assert body["artist"]["name"] == "Tamada Heijun"
assert "renamed_from" not in body
# --- /api/extension/manifest ---------------------------------------