Skip to content

fix(madara): keep locked Tangerine Archive chapters in the reader's path - #12

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

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

Conversation

@RibatTRW

Copy link
Copy Markdown
Owner

Stacked candidate fix for PR lnreader#2564 (base is that PR's branch fm/lnreader-plugins-2555-tangerine, not the default branch). Wave-2 replica of the tangerine-premium-nav-continuity lane; it is a fresh implementation on the same base, not a copy of PR 5.

Tangerine Archive's coin-locked (premium) chapters carry href="#", so the Madara template dropped them from novel.chapters and the reader's prev/next jumped straight past them. The bodies are server-gated behind login + coins and stay that way. This change keeps the navigation continuous and the outcome honest.

What changed

plugins/multisrc/madara/template.ts - additive, flag-gated; sources without the flag take the old code path:

  • Rebuild locked URLs from the chapter number in the row title: /series/<slug>/chapter-<n>/, with the non-ASCII slug token when it is part of the slug (Chapter 176 🔞 -> chapter-176-%F0%9F%94%9E) and without a " - title" suffix (Chapter 242 - 🔞 -> chapter-242). Rule validated against the real hrefs of 3289 free rows: 3271 exact, 17 differ only by percent-encoding case, 1 is a WordPress _1 duplicate that is never reconstructed.
  • Label locked rows 🔒 Chapter 212 (10 coins, TBA) / 🔒 Chapter 161 (10 coins, 1 day); releaseTime comes from the site's own countdown as YYYY-MM-DD, or null for TBA (never a backwards-derived past date, never the literal LL).
  • Resolve duplicate derived URLs against the site (the gap wave 1 left open): when two rows derive the same URL, fetch it once and keep only the row whose own data-chapter-<id> is the post that URL really serves - villain-let-me-touch-you Chapter 166 now lists one honest row (Chapter 166 - Secret, id 16700) instead of two rows sharing a lying path. If the lookup fails, the colliding rows are dropped rather than guessed. A rebuilt URL colliding with a free chapter's real href is dropped too (0 today).
  • parseChapter returns an honest notice instead of blank text for a gated page: coin price from the page's coin-N class, unlock countdown/TBA and the row's own title read from the series chapter list by the page's data-id; an explicit "could not be loaded" notice when a rebuilt URL renders the series page (soft 404); and a prose-masking guard - a page is only treated as locked when .reading-content carries the gate block and no prose of its own, so an entitled session's prose is never hidden behind the (hidden) gate block.
  • Read-time lock state: holy-night chapter-359 is listed free-chap with a real href but its page is gated; it is reported from the fetched page (costs 12 coins, and since its row carries title="1 hour ago" with no countdown, The site lists this chapter as released (1 hour ago) but still serves it locked.), not from the list class.

plugins/multisrc/madara/sources.json: tangerinearchive opts in with listLockedChapters: true, hasLocked: true (the existing "Hide locked chapters" switch) and versionIncrements: 1 (plugin 2.2.1). README.md documents both options.

What was probed (all anonymous, read-only - no login, cookies, nonce replay or purchase)

  • Full catalogue: all 20 series, plugin parseNovel vs an independent fetch of the same ajax endpoint - 4073 chapters = 3289 free + 784 locked, every series matching exactly (785 live premium rows minus the one duplicate whose URL is undiscoverable). 42/42 checks.
  • Free -> locked -> free boundary walks in both directions on 4 series (the requested value-add, beyond PR 5's garden-of-may): garden-of-may 160 -> 161 -> 162 -> 161 -> 160; holy-night 358 -> 359 (gated free-labelled) -> 360 -> 359 -> 358; in-the-doghouse 242 -> 243 -> 244 -> 243 -> 242; me-the-weakest 148 -> 149 -> 150 -> 149 -> 148. Every free step returns prose (4k-10k chars), every gated step the notice with price + state, and the boundary entries are adjacent in the chapter array so the app's prev/next steps through them.
  • Edge cases: emoji slug (villain 176 -> page id 16720), " - 🔞" slug (it-seems 242 -> plain chapter-242, page id 16942), duplicate ReaperScans not showing novels  lnreader/lnreader-plugins#166 (one row, page id 16700, notice names Chapter 166 - Secret), fabricated /chapter-9999/ -> "could not be loaded", hideLocked both ways (212/52 off, 160/0 on).
  • Two wave-1 claims independently re-checked: the chapter sitemaps (8 files, 3286 unique URLs, 0 of the 671 derived premium paths present, 2685/2687 free paths present - plus 601 stale free URLs) and the wp-json route index (222 routes, no chapter route, wp-manga/v1/cache/stats 401, wp/v2/wp-manga-chapter 404).
  • New retrieval angles, all dead ends: WP Abilities API (/wp-json/wp-abilities/v1/abilities -> 401), coin plugin file inventory (directory 403, readme.txt 404, only modal-only frontend.js public), route aliases (/manga/, /comics/, /novel/ -> 404), series-page server render (one chapter href, newest premium id with no href).
  • npm run check:plugin TangerineArchive PASS (173 chapters, 14497 chars). Shared-template guard: SleepyTranslations[madara] PASS (16/44/9872) and Lovelyblossoms[madara] PASS (16/5/15373) with the flag off - identical numbers to the base commit. eslint, prettier, build:compile, build:multisrc pass; no new tsc errors (23 pre-existing repo-wide, unchanged).

What was NOT tested

  • An entitled session. No owner test account or session cookie exists in this environment, so the "locked chapter's real prose in hand" bar is not met and this PR does not claim it. Every credential-free route tried here (and by the wider swarm) returns the gate or nothing. A prebuilt probe (.scratch/entitled-probe.mjs, not committed) runs one command when an owner-authorized session exists and specifically fails if the plugin returns the locked notice while the site serves prose - it is validated in refusal mode (no creds -> exit 2; dummy cookie -> exit 1 "still gated").
  • The reader app's rendering of the notices - only the plugin-level strings are verified.
  • A live scheduled unlock flip; the plugin prefers the row's real href the moment the site serves one, so a flipped chapter becomes a normal free chapter with no code change.
  • The pre-existing releaseTime: 'LL' bug for free rows (dayjs().format('LL') without localizedFormat) is untouched and out of scope.

This PR was authored by an AI agent (Firstmate crewmate); the probes above are reproducible from the commands in the commit message and the shared findings board.

Tangerine Archive's coin-locked chapters carry href="#" in the chapter
list, so the shared Madara template dropped them and the reader's
prev/next skipped straight over them. Their bodies are server-gated
behind login + coins and stay that way; this change only keeps the
navigation continuous and the outcome honest.

Flag-gated `listLockedChapters` (only tangerinearchive opts in, plus
`hasLocked` for the existing "Hide locked chapters" switch):

- rebuild each locked row's /series/<slug>/chapter-<n>/ URL from the
  chapter number in its title, with the non-ASCII slug token when it is
  part of the slug ("Chapter 176 🔞" -> chapter-176-%F0%9F%94%9E) and
  without a " - title" suffix ("Chapter 242 - 🔞" -> chapter-242);
  verified against the real hrefs of 3289 free rows (3271 exact, 17
  percent-encoding case only, 1 WordPress `_1` duplicate);
- label locked rows with price and unlock state ("10 coins, TBA" /
  "12 coins, 2 weeks") and set releaseTime from the site's countdown
  (null for TBA) instead of a backwards-derived past date;
- resolve duplicate derived URLs against the site: keep only the row
  whose own data-chapter id is the post that URL serves (villain lnreader#166),
  and drop a rebuilt URL that collides with a free chapter, so a listed
  locked row can never resolve to different text than its name claims;
- parseChapter returns an honest locked notice (price + countdown/TBA,
  named post) instead of blank text, refuses a rebuilt URL that renders
  the series page (soft 404), and only treats a page as locked when the
  reading body has no prose of its own - so an entitled session's prose
  is never masked by the (hidden) gate block;
- a free-labelled row whose page is still gated (holy-night lnreader#359) is
  reported from the fetched page, not from the list class.

Verified live, anonymous, read-only (no login, cookies, nonce replay or
purchase): all 20 series, 4073 chapters = 3289 free + 784 locked,
matching the ajax inventory exactly (785 premium rows minus the one
duplicate whose URL is undiscoverable); free -> locked -> free boundary
walks in both directions on garden-of-may, holy-night, in-the-doghouse
and me-the-weakest...; edge cases (emoji slug, " - 🔞" slug, duplicate
lnreader#166, soft 404, hideLocked both ways). check:plugin Tangerine PASS,
unrelated Madara sources (SleepyTranslations, Lovelyblossoms) PASS with
the flag off, eslint/prettier/build:compile/build:multisrc pass.

The locked prose itself was NOT obtained: every credential-free route
(re-checked sitemaps, wp-json index, WP Abilities API, coin plugin
assets, route aliases, series-page render) returns the gate or nothing.
An entitled-session probe is ready and runs one command when an
owner-authorized session exists.

Co-Authored-By: pi (AI agent) <noreply@pi.dev>
@RibatTRW RibatTRW closed this Sep 23, 2026
@RibatTRW
RibatTRW deleted the fm/tangerine-replica-nav-continuity 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