From b54243a1ff56aedd5b0ca71ada47a822676d0b56 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 2 Jul 2026 21:51:51 -0400 Subject: [PATCH] fix(subscribestar): inject the 18+ age cookie on every SubscribeStar domain The cookie was pinned to .subscribestar.adult only; cookies are domain-scoped, so sources on subscribestar.art (Elasid, event #54116) never sent it and every poll 302d to /age_confirmation_warning. Emit one line per domain (.com/.adult/.art) with a per-domain presence check, and admit .art in the platform url_pattern. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM --- .../app/services/platforms/subscribestar.py | 36 +++++++++++++------ tests/test_credential_service.py | 28 ++++++++++----- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/backend/app/services/platforms/subscribestar.py b/backend/app/services/platforms/subscribestar.py index 554c26c..886272b 100644 --- a/backend/app/services/platforms/subscribestar.py +++ b/backend/app/services/platforms/subscribestar.py @@ -14,9 +14,11 @@ `_personalization_id` age-confirmation cookie that the user can't easily refresh — SubscribeStar's frontend JS uses localStorage to suppress the age popup once dismissed. gallery-dl's own login flow - sidesteps this by setting `18_plus_agreement_generic=true` on - `.subscribestar.adult`; we mirror that for cookies captured via the - extension. + sidesteps this by setting `18_plus_agreement_generic=true`; we mirror + that for cookies captured via the extension — on EVERY SubscribeStar + domain, because cookies are domain-scoped and the wall exists on all + of them: an .adult-only line never rides along to a .art creator page + (Elasid, event #54116 — every poll 302'd to /age_confirmation_warning). """ from .base import GD_DEFAULTS, PlatformInfo, str_id_value @@ -29,20 +31,31 @@ def derive_post_url(data: dict) -> str | None: return None +# All domains SubscribeStar serves creators from; the age wall gates each. +_SS_DOMAINS = (".subscribestar.com", ".subscribestar.adult", ".subscribestar.art") + + def augment_cookies(netscape: str) -> str: - if "18_plus_agreement_generic" in netscape: - return netscape # Far-future expiry — gallery-dl's own login flow sets this with no # explicit expiry; the server only checks presence/value. expiry = 4102444800 # 2100-01-01 UTC - line = "\t".join([ - ".subscribestar.adult", "TRUE", "/", "TRUE", - str(expiry), "18_plus_agreement_generic", "true", - ]) body = netscape.rstrip("\n") if not body: body = "# Netscape HTTP Cookie File" - return body + "\n" + line + "\n" + lines = netscape.splitlines() + for domain in _SS_DOMAINS: + # Per-domain presence check: captured cookies carrying the age + # cookie for one domain must not suppress injection on the others. + if any( + line.startswith(domain + "\t") and "18_plus_agreement_generic" in line + for line in lines + ): + continue + body += "\n" + "\t".join([ + domain, "TRUE", "/", "TRUE", + str(expiry), "18_plus_agreement_generic", "true", + ]) + return body + "\n" INFO = PlatformInfo( @@ -51,10 +64,11 @@ INFO = PlatformInfo( description="Download posts from SubscribeStar creators", auth_type="cookies", requires_auth=True, - url_pattern=r"^https?://(www\.)?subscribestar\.(com|adult)/", + url_pattern=r"^https?://(www\.)?subscribestar\.(com|adult|art)/", url_examples=[ "https://subscribestar.adult/example_artist", "https://www.subscribestar.com/example_artist", + "https://subscribestar.art/example_artist", ], default_config={**GD_DEFAULTS, "content_types": ["all"]}, derive_post_url=derive_post_url, diff --git a/tests/test_credential_service.py b/tests/test_credential_service.py index b9857ec..e317f52 100644 --- a/tests/test_credential_service.py +++ b/tests/test_credential_service.py @@ -116,8 +116,10 @@ async def test_get_cookies_path_subscribestar_injects_age_cookie(db, crypto, tmp cookie; the browser-stored cookie expires annually and can't be easily refreshed (the JS age popup is suppressed by localStorage). Mirror gallery-dl's own login-flow workaround by injecting - `18_plus_agreement_generic=true` on `.subscribestar.adult` whenever - cookies for subscribestar are materialized.""" + `18_plus_agreement_generic=true` whenever cookies for subscribestar + are materialized — on EVERY SubscribeStar domain, since cookies are + domain-scoped and creators live on .com/.adult/.art alike (a .adult-only + line left .art sources stuck on the age wall — Elasid, event #54116).""" netscape_in = ( "# Netscape HTTP Cookie File\n" ".subscribestar.adult\tTRUE\t/\tTRUE\t1700000000\tsession_id\txyz\n" @@ -126,16 +128,18 @@ async def test_get_cookies_path_subscribestar_injects_age_cookie(db, crypto, tmp await svc.upsert(platform="subscribestar", credential_type="cookies", data=netscape_in) path = await svc.get_cookies_path("subscribestar") contents = path.read_text() - assert "18_plus_agreement_generic\ttrue" in contents - assert ".subscribestar.adult" in contents + for domain in (".subscribestar.com", ".subscribestar.adult", ".subscribestar.art"): + assert f"{domain}\tTRUE\t/\tTRUE\t4102444800\t18_plus_agreement_generic\ttrue" in contents # Original session_id cookie preserved. assert "session_id\txyz" in contents @pytest.mark.asyncio -async def test_get_cookies_path_subscribestar_idempotent_when_present(db, crypto, tmp_path): - """If the operator's captured cookies ALREADY contain the age cookie - (e.g. a manual paste, or a re-login), don't double-inject.""" +async def test_get_cookies_path_subscribestar_idempotent_per_domain(db, crypto, tmp_path): + """If the operator's captured cookies ALREADY contain the age cookie for + a domain (e.g. a manual paste, or a re-login), don't double-inject on + THAT domain — but a .adult-only capture must still get the cookie added + for .com and .art (per-domain check, not presence-anywhere).""" netscape_in = ( "# Netscape HTTP Cookie File\n" ".subscribestar.adult\tTRUE\t/\tTRUE\t1700000000\t18_plus_agreement_generic\ttrue\n" @@ -145,7 +149,15 @@ async def test_get_cookies_path_subscribestar_idempotent_when_present(db, crypto await svc.upsert(platform="subscribestar", credential_type="cookies", data=netscape_in) path = await svc.get_cookies_path("subscribestar") contents = path.read_text() - assert contents.count("18_plus_agreement_generic") == 1 + assert contents.count("18_plus_agreement_generic") == 3 + adult_lines = [ + line for line in contents.splitlines() + if line.startswith(".subscribestar.adult\t") + and "18_plus_agreement_generic" in line + ] + assert len(adult_lines) == 1 + assert ".subscribestar.com\t" in contents + assert ".subscribestar.art\t" in contents @pytest.mark.asyncio