fix(subscribestar): route .art creators to .adult; clear source failure on disable
CI / lint (push) Successful in 6s
CI / frontend-build (push) Successful in 46s
CI / backend-lint-and-test (push) Successful in 1m10s
CI / integration (push) Successful in 7m34s

Two pre-merge fixes:

1. SubscribeStar .art age wall: the 18+ cookie doesn't clear the age gate on
   the .art domain (keeps 302'ing to /age_confirmation_warning even with the
   cookie — Elasid #54116), but the same creator is reachable on .adult where
   the cookie works. _normalize_ss_host rewrites subscribestar.art →
   subscribestar.adult at request time (stored Source.url untouched), logged so
   it's visible in walk logs. .com/.adult pass through.

2. Disabling a source now clears its failure state (last_error, error_type,
   consecutive_failures) so subs you pause (not paying for) stop lingering as
   'failing'. Only the explicit disable clears — an unrelated edit to an
   already-disabled source leaves state alone.

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-03 14:43:02 -04:00
parent 544e30bfb9
commit c9696a2faf
4 changed files with 104 additions and 2 deletions
+51
View File
@@ -135,6 +135,57 @@ async def test_update_changes_fields(db):
assert updated.config_overrides == {"videos": False}
@pytest.mark.asyncio
async def test_disable_clears_failure_state(db):
"""Disabling a source wipes its failure state so it stops showing as
'failing' (operator: disable subs you're not paying for)."""
artist = await _artist(db)
svc = SourceService(db)
rec = await svc.create(
artist_id=artist.id, platform="patreon", url="https://patreon.com/a",
)
source = (await db.execute(
select(Source).where(Source.id == rec.id)
)).scalar_one()
source.last_error = "auth failed"
source.error_type = "auth_error"
source.consecutive_failures = 5
await db.commit()
updated = await svc.update(rec.id, enabled=False)
assert updated.enabled is False
assert updated.last_error is None
assert updated.consecutive_failures == 0
refetched = (await db.execute(
select(Source).where(Source.id == rec.id)
)).scalar_one()
assert refetched.error_type is None
@pytest.mark.asyncio
async def test_update_while_enabled_keeps_failure_state(db):
"""A non-disable edit must NOT wipe failure state — only the explicit
disable clears it (else a config tweak would hide a real failure)."""
artist = await _artist(db)
svc = SourceService(db)
rec = await svc.create(
artist_id=artist.id, platform="patreon", url="https://patreon.com/a",
)
source = (await db.execute(
select(Source).where(Source.id == rec.id)
)).scalar_one()
source.last_error = "auth failed"
source.consecutive_failures = 3
await db.commit()
await svc.update(rec.id, config_overrides={"videos": False})
refetched = (await db.execute(
select(Source).where(Source.id == rec.id)
)).scalar_one()
assert refetched.last_error == "auth failed"
assert refetched.consecutive_failures == 3
@pytest.mark.asyncio
async def test_list_hides_sidecar_synthetic_anchors(db):
"""Filesystem-import synthetic Sources (url='sidecar:<platform>:<slug>',
+20
View File
@@ -22,6 +22,7 @@ from backend.app.services.subscribestar_client import (
SubscribeStarClient,
SubscribeStarDriftError,
_parse_ss_datetime,
_split_creator_url,
)
from backend.app.services.subscribestar_downloader import SubscribeStarDownloader
from backend.app.services.subscribestar_ingester import (
@@ -82,6 +83,25 @@ def test_parse_ss_datetime_variants():
assert _parse_ss_datetime("nonsense") is None
def test_split_creator_url_rewrites_art_to_adult():
# The .art age wall doesn't clear with the 18+ cookie; the same creator is
# reachable on .adult (Elasid, event #54116). Requests must route to .adult.
base, slug = _split_creator_url("https://subscribestar.art/elasid")
assert base == "https://subscribestar.adult"
assert slug == "elasid"
# www. prefix + trailing path still normalizes the host only.
base, slug = _split_creator_url("https://www.subscribestar.art/elasid/posts")
assert base == "https://www.subscribestar.adult"
assert slug == "elasid"
def test_split_creator_url_leaves_com_and_adult_untouched():
assert _split_creator_url("https://subscribestar.adult/sabu")[0] == \
"https://subscribestar.adult"
assert _split_creator_url("https://www.subscribestar.com/sabu")[0] == \
"https://www.subscribestar.com"
def test_date_parses_when_wrapped_in_permalink_anchor():
# Image posts wrap the date in an <a> permalink; a plain regex missed them
# → null dates (cheunart 2026-06-17). gallery-dl's method handles both.