Skip to content

fix(madara): list premium chapters and fail loudly on locked bodies - #2

Closed
RibatTRW wants to merge 2 commits into
fm/lnreader-plugins-2555-tangerinefrom
fm/tangerine-premium-fail-loud
Closed

RibatTRW wants to merge 2 commits into
fm/lnreader-plugins-2555-tangerinefrom
fm/tangerine-premium-fail-loud

Conversation

@RibatTRW

@RibatTRW RibatTRW commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Stacked candidate fix for lnreader#2564

Base is deliberately fm/lnreader-plugins-2555-tangerine, the head branch of lnreader#2564 — not a default branch. Merge this only after (or together with) that PR.

This fixes exactly the gap PR 2564 documented: premium (coin-locked) chapters were dropped from the chapter list because their ajax rows carry href="#", and parseChapter on their real URLs returned an empty body.

What changed

  • plugins/multisrc/madara/template.ts — new opt-in premiumChapterUrls source option, off by default so every other Madara source keeps the old code path:
    • parseNovel: premium rows (li ... premium-block) with href="#" get their real series/<slug>/chapter-<n>/ path reconstructed from the row's chapter text instead of being dropped. If no number can be read, the row is still dropped (no invented URL). The existing 🔒 prefix and the existing hideLocked handling are untouched.
    • parseChapter: for sources with the option, a server-gated body (.reading-content .content-blocked / .reading-content .premium-block, or the "This chapter is locked!" text) now throws Premium chapter locked: costs <coin-N> coins and requires a site account to unlock. instead of silently returning an empty chapter. The coin price is read from the coin-N class, so it is not hardcoded to 10.
    • parseChapter also throws Chapter content unavailable: the page has no readable body when the fetched page has no .reading-content or no readable text — a few odd premium slugs (emoji suffixes, duplicate chapter numbers) resolve to the series page, and that must never come back blank either.
  • plugins/multisrc/madara/sources.json — tangerinearchive opts in with "premiumChapterUrls": true and "versionIncrements": 1 (generated plugin version 2.2.1).
  • plugins/multisrc/madara/README.md — documents the new option.

Anonymous read-only probes (no login, no cookies, no purchase, no buy-endpoint call)

  • POST /series/garden-of-may/ajax/chapters/ → 212 rows: 160 free-chap + 52 premium coin-10 data-chapter-<id> premium-block, rows descending 212..1. Every row's anchor text is exactly Chapter N, and for all 160 free rows the href number equals the text number (0 mismatches), which is what makes the reconstruction exact.
  • Premium page GET /series/garden-of-may/chapter-161/ (also 170, 212) → HTTP 200, but .reading-content holds only <div class="premium coin-10 data-chapter-16157 content-blocked premium-block">This chapter is locked! <a href="#">Buy it?</a></div>; the prose is absent from the DOM, not hidden. data-chapter-<id> matches the ajax row's class id.
  • Free-page false-positive check: chapter-160/ contains 4 premium-block occurrences, all div.nav-next ... outside .reading-content; 0 inside. The detector is scoped to .reading-content, so free chapters never throw.
  • Second series: popularNovels[0] ajax list has 173 rows (53 free + 120 premium); the plugin now returns all 173.
  • A wider anonymous sweep (AMP, print, feed/RSS, oEmbed, ?_format variants, wp-json/REST, JSON-LD, sitemaps, cache/CDN variants, reading-nav and manga ajax parameter matrix, Wayback CDX, hidden page data) found no route that serves a locked body — the lock is enforced server-side, consistent with the earlier research report. No session/nonce forging, no login and no buy-endpoint call was made.

Verification

  • npm run build:compile and npm run build:multisrc pass.
  • npm run check:plugin -- plugins/english/TangerineArchive[madara].ts → PASS (popularNovels 12 novels, searchNovels PASS, parseNovel 173 chapters, parseChapter 14497 chars).
  • Custom live probe: parseNovel('series/garden-of-may/') = 212 chapters (52 locked / 160 free) with premium paths matching the live ajax numbers exactly; parseChapter on locked 161/212 throws the 10-coin message; free 1/80/160 return 7114/8396/6532 chars; a soft-404 series-page URL (.../chapter-173/) throws the no-readable-body error instead of returning ''.
  • Shared-template guard (unrelated sources, same generated template): Lovelyblossoms[madara].ts PASS and SleepyTranslations[madara].ts PASS. (BoxNovel[madara].ts was INCONCLUSIVE — its site returns Cloudflare 403 — unrelated to this change.)

NOT tested

  • No entitled-session test. Nothing here verifies the post-unlock reading experience; no account, cookie, coin balance, purchase or unlock request was used, and the server-gated prose was never fetched.
  • No in-app rendering check of the thrown error message (verified at the plugin API level only).
  • releaseTime: 'LL' for Tangerine chapters is the pre-existing dayjs().format('LL') template issue and is intentionally out of scope.

Closes the premium-listing part of lnreader#2555.

Co-authored by an AI agent (firstmate crewmate, task tangerine-premium-fail-loud).

RibatTRW and others added 2 commits September 23, 2026 19:50
Tangerine Archive lists premium (coin-locked) chapters with href="#",
so the template dropped them and parseChapter returned an empty body
for their real URLs. Add an opt-in premiumChapterUrls source option
that reconstructs the chapter-<n> URL from the row text, keeps the
locked chapters in the list, and throws a descriptive error naming the
coin price when the server-gated lock notice is served.

Only tangerinearchive enables the option, so other Madara sources are
unaffected. Bumps the source to 2.2.1.

Co-Authored-By: firstmate-crewmate <crewmate@firstmate.local>
Reconstructed premium URLs are exact for ~98% of rows, but a few odd
slugs (emoji suffixes, duplicate chapter numbers) fall back to the
series page, which has no .reading-content and previously produced a
blank chapter. For premiumChapterUrls sources, throw a clear error
when the page has no reading area or no readable text, so a wrong
derived URL can never come back blank.

Co-Authored-By: firstmate-crewmate <crewmate@firstmate.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant