fix(patreon): handle the /cw/ creator-URL prefix in vanity extraction
A source URL like https://www.patreon.com/cw/Atole resolved vanity='cw' — the vanity regex only skipped a /c/ prefix, so Patreon's current /cw/ ("creator workspace") form fell through to the bare-vanity branch and captured the prefix instead of the slug. Every /cw/ source then failed campaign-id resolution (API + page-scrape both looked up "cw"). Operator-confirmed 2026-06-07 via the new error text: source_url='.../cw/Atole'; vanity='cw'. Add cw/ to the optional prefix group (ordered before c/ so the longer prefix wins), and have the creator-page fallback try the /cw/ form too. Test covers bare / c/ / cw/ extraction and that id: URLs stay non-vanity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,9 +30,17 @@ _CAMPAIGNS_URL = "https://www.patreon.com/api/campaigns"
|
|||||||
# A source URL of the form `.../id:<digits>` already carries the campaign id
|
# A source URL of the form `.../id:<digits>` already carries the campaign id
|
||||||
# (no lookup needed). The vanity regex deliberately EXCLUDES the id: form so the
|
# (no lookup needed). The vanity regex deliberately EXCLUDES the id: form so the
|
||||||
# two paths don't overlap.
|
# two paths don't overlap.
|
||||||
|
#
|
||||||
|
# Patreon serves the creator vanity under several path prefixes — bare
|
||||||
|
# (`patreon.com/Atole`), `c/` (`patreon.com/c/Atole`), and `cw/`
|
||||||
|
# (`patreon.com/cw/Atole`, its current "creator workspace" URL). The optional
|
||||||
|
# prefix group must list `cw/` BEFORE `c/` so the longer prefix wins — otherwise
|
||||||
|
# `cw/Atole` matches the bare branch and yields vanity="cw" (operator-flagged
|
||||||
|
# 2026-06-07: every `/cw/` source failed resolution on vanity="cw").
|
||||||
_ID_URL_RE = re.compile(r"/id:(\d+)")
|
_ID_URL_RE = re.compile(r"/id:(\d+)")
|
||||||
|
_VANITY_PREFIXES = ("cw/", "c/")
|
||||||
_VANITY_RE = re.compile(
|
_VANITY_RE = re.compile(
|
||||||
r"^https?://(?:www\.)?patreon\.com/(?:c/)?(?!id:)([^/?#]+)",
|
r"^https?://(?:www\.)?patreon\.com/(?:cw/|c/)?(?!id:)([^/?#]+)",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
_USER_AGENT = (
|
_USER_AGENT = (
|
||||||
@@ -141,10 +149,12 @@ def _lookup_via_page(vanity: str, cookies_path: str | None) -> str | None:
|
|||||||
Patreon redirects between. Never raises."""
|
Patreon redirects between. Never raises."""
|
||||||
jar = _load_cookie_jar(cookies_path)
|
jar = _load_cookie_jar(cookies_path)
|
||||||
headers = {"User-Agent": _USER_AGENT, "Accept": "text/html"}
|
headers = {"User-Agent": _USER_AGENT, "Accept": "text/html"}
|
||||||
for page_url in (
|
# Try the bare vanity and every known creator-path prefix Patreon
|
||||||
f"https://www.patreon.com/{vanity}",
|
# redirects between (c/, cw/) — the one the source used isn't known here
|
||||||
f"https://www.patreon.com/c/{vanity}",
|
# (extract_vanity already stripped it).
|
||||||
):
|
page_urls = [f"https://www.patreon.com/{vanity}"]
|
||||||
|
page_urls += [f"https://www.patreon.com/{p}{vanity}" for p in _VANITY_PREFIXES]
|
||||||
|
for page_url in page_urls:
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
page_url,
|
page_url,
|
||||||
|
|||||||
@@ -169,3 +169,17 @@ async def test_for_source_unresolvable_returns_none():
|
|||||||
cid, resolved = await resolve_campaign_id_for_source("not-a-patreon-url", None, {})
|
cid, resolved = await resolve_campaign_id_for_source("not-a-patreon-url", None, {})
|
||||||
assert cid is None
|
assert cid is None
|
||||||
assert resolved is None
|
assert resolved is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_vanity_handles_all_path_prefixes():
|
||||||
|
"""Patreon serves the vanity bare and under c/ and cw/ prefixes; all must
|
||||||
|
yield the creator slug, not the prefix (operator-flagged 2026-06-07: a
|
||||||
|
/cw/Atole source resolved vanity='cw')."""
|
||||||
|
from backend.app.services.patreon_resolver import extract_vanity
|
||||||
|
|
||||||
|
assert extract_vanity("https://www.patreon.com/Atole") == "Atole"
|
||||||
|
assert extract_vanity("https://www.patreon.com/c/Atole") == "Atole"
|
||||||
|
assert extract_vanity("https://www.patreon.com/cw/Atole") == "Atole"
|
||||||
|
assert extract_vanity("https://patreon.com/cw/Atole") == "Atole"
|
||||||
|
# id: URLs are NOT vanities (handled by the id path).
|
||||||
|
assert extract_vanity("https://www.patreon.com/id:123") is None
|
||||||
|
|||||||
Reference in New Issue
Block a user