Skip to content

fix(madara): list Tangerine Archive premium chapters and fail loudly on locked bodies (replica + hardening) - #11

Closed
RibatTRW wants to merge 1 commit into
fm/lnreader-plugins-2555-tangerinefrom
fm/tangerine-replica-fail-loud
Closed

RibatTRW wants to merge 1 commit into
fm/lnreader-plugins-2555-tangerinefrom
fm/tangerine-replica-fail-loud

Conversation

@RibatTRW

Copy link
Copy Markdown
Owner

Stacked candidate fix for lnreader#2564

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

This is the wave-2 replica of the fm/tangerine-premium-fail-loud lane (PR #2 in this fork): same fail-loud approach, rebuilt on PR 2564's head with the hardening that lane lacked. It 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,sources.json,README.md})

New premiumChapterUrls option, off by default and enabled only for tangerinearchive (versionIncrements: 1 → plugin 2.2.1). Sources without the flag take the previous code path unchanged.

  • Listing: a locked row (premium-block, href="#") gets its real chapter-<n> path reconstructed from the first number in the row's anchor text. The rule also covers the site's odd slugs: a non-ASCII token directly after the number is part of the slug (Chapter 176 🔞 → chapter-176-%f0%9f%94%9e) while a - title suffix is not (Chapter 242 - 🔞 → chapter-242), and 27.2 becomes 27-2. A row with no number is skipped rather than listed with a guessed URL.
  • Duplicate-number collisions: a derived URL can collide with another row (the site has two posts titled Chapter 166 on one slug, and in principle a free row could own the same slug). Only those ambiguous rows are resolved with one extra fetch, and a row is kept only when the page's #wp-manga-current-chap[data-id] is its own data-chapter-<id>; the other row is dropped. A derived URL can therefore never be listed as a different chapter.
  • parseChapter fail-loud (flag-gated): a fetched page with no chapter container (the series-page soft-404) throws Chapter content unavailable: … did not return a chapter page …; a gated page throws Premium chapter locked: costs N coins and requires a site account to unlock. (price read from the gate block, so it also covers rows listed free whose page is still gated); an empty chapter page throws a distinct "no readable text" error. Free chapters parse exactly as before.
  • releaseTime is null for reconstructed rows (the site's "Unlocked in 4 weeks"/TBA is not a publication date; the template's parseData would otherwise report a past date).
  • hasLocked: true exposes the existing "Hide locked chapters" switch (default off ⇒ locked rows are shown).

What was probed (all anonymous, read-only; no cookies, no login, buy endpoint never called)

  • URL reconstruction, full site: 19 series from wp-manga-sitemap.xml, POST /series/<slug>/ajax/chapters/ each → 3359 rows = 2687 free + 672 premium. The rule reproduces 2669/2687 free rows' real hrefs exactly, 17 differ only by percent-encoding case (same URL), 1 is the WP duplicate-slug chapter-55_1.
  • Every premium derived URL fetched (672 requests): 671/672 return HTTP 200 with the row's own data-id and the lock block; the 1 exception is the documented duplicate Chapter 166 (id 16701, resolves to sibling 16700), which this PR drops from the list. 0 soft-404s, 0 free-prose-under-premium.
  • Free-chapter regression sample: 156 free pages across all series (incl. chapter-0, emoji and _1 slugs) all carry #wp-manga-current-chap + .reading-content and parse prose (median 4465 chars).
  • Generated plugin probe (.scratch/probe-premium.mjs, 44/44): for 6 series the plugin's chapter count equals the live ajax list minus resolved collisions; every premium path equals the independently derived set; emoji rows 175/176 keep their emoji slug; villain Chapter 166 lists only the row that owns the slug; free garden-160/villain-144 parse 6538/7859 chars; locked garden-161/212 and the villain emoji slug throw the 10-coin message; the plain villain-176 URL (series-page soft-404) and a deliberately wrong /chapter-999/ throw the distinct "did not return a chapter page" error; hideLocked on/off gives 160/212 chapters.
  • Repo checks: npm run build:multisrc, npm run build:compile, npm run check:plugin on the generated Tangerine plugin (PASS: 12 novels, 173 chapters, 14497 chars), prettier/eslint clean on the three changed files. Repo-wide eslint is unchanged from the base commit (79 problems both sides); the 10 prettier warnings are pre-existing files untouched here.
  • Shared-template guard: unrelated Madara sources with the flag off — SleepyTranslations[madara] (44 chapters, 9872 chars) and Lovelyblossoms[madara] (5 chapters, 15373 chars) — both PASS.

What was NOT tested

  • No entitled session: LNReader plugins cannot log in, so no locked body was read and none is served. The lock is a real server-side gate; this PR only lists the chapters honestly and names the gate.
  • No purchase, no session/nonce forging, no third-party copy of the prose.
  • The reader app's rendering of the thrown error (only the plugin-level error message is verified).
  • The site's scheduled unlock (the one legitimate route to the prose) was not captured during this session: the soonest countdowns were 8–9 h out (it-seems-like-…/chapter-231, i-thought-…/chapter-53) and garden-of-may chapter-161 still reports "Unlocked in 1 day". A read-only flip watcher is armed to capture a flip if one lands.

Authored with AI assistance (firstmate crewmate) — reviewers should weight accordingly.

…on locked bodies

Premium (coin-locked) chapter rows in the ajax chapter list carry href="#",
so they were dropped from novel.chapters and their real (guessable) URLs
returned an empty body. With the new premiumChapterUrls option (enabled only
for tangerinearchive), locked rows are listed with their reconstructed
chapter-<n> path and parseChapter throws a descriptive error instead of a
blank chapter:

- reconstruct from the first number in the row's anchor text, keeping a
  non-ASCII token that is part of the site's slug (Chapter 176 -> chapter-176
  with the percent-encoded emoji) but not a " - title" suffix, and turning
  "27.2" into "27-2"; a row with no number is skipped;
- resolve duplicate-number collisions at list time: only rows whose derived
  URL resolves to their own post id are kept (villain-let-me-touch-you
  Chapter 166 has two posts on one slug), so a derived URL can never be
  listed as a different chapter;
- parseChapter: a page without a chapter container throws a distinct "did not
  return a chapter page" error (series-page soft-404), a gated page throws
  "Premium chapter locked: costs N coins ...", and an empty chapter page
  throws a "no readable text" error;
- releaseTime is null for reconstructed rows (the site's countdown/TBA is not
  a publication date);
- hasLocked gives users the existing "Hide locked chapters" switch.

Verified live, anonymous and read-only: 672/672 premium rows across 19 series
resolve to their own gated chapter page; 156/156 free pages still parse;
duplicate/emoji/title-suffix/soft-404 cases exercised. Sources without the
flag are unchanged (SleepyTranslations, Lovelyblossoms check:plugin PASS).

Co-authored-by: firstmate-crewmate <crewmate@firstmate.local>
@RibatTRW RibatTRW closed this Sep 23, 2026
@RibatTRW
RibatTRW deleted the fm/tangerine-replica-fail-loud branch September 24, 2026 01:06
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