Skip to content

fix(madara): list Tangerine Archive locked chapters with a lock notice - #3

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

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

Conversation

@RibatTRW

@RibatTRW RibatTRW commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Stacked on lnreader#2564 (fm/lnreader-plugins-2555-tangerine) — this closes the one gap that PR documents: premium (coin-locked) chapters were dropped from the chapter list because their rows link to href="#", and their URLs returned an empty chapter.

What changed

Two files, additive and flag-gated:

  • plugins/multisrc/madara/sources.json — the tangerinearchive entry gains lockedChapterPlaceholder: true, hasLocked: true (exposes the template's existing "Hide locked chapters" switch) and versionIncrements: 1 (plugin version 2.2.1).
  • plugins/multisrc/madara/template.ts — new opt-in lockedChapterPlaceholder option:
    • Listing: a locked row gets a real URL instead of #, built from the row title on the same URL base the list's free rows use. The title is slugged the way the site does: leading number, dots become dashes, and a non-ASCII token glued straight onto the number stays in the slug (Chapter 176 🔞 → chapter-176-%F0%9F%94%9E), while a - Subtitle suffix is dropped (Chapter 434 - (Special Side Story End) → chapter-434). releaseTime is null for locked rows — their date span holds the unlock state (Unlocked in 4 weeks / TBA), which the app renders as "Invalid Date".
    • Reading: parseChapter returns a labelled notice instead of blank text in the two cases where there is no body to show:
      • gated body (.reading-content .content-blocked / .premium-block) → chapter title, Premium chapter - locked behind <N> coins., and the unlock countdown or TBA from the series chapter list;
      • no chapter page at all (a derived URL that falls back to the series page) → an explicit "page could not be found" notice.
        The notice is only used when the readable body is empty, so an entitled session — or a chapter whose early-access window just ended — always keeps its real prose even though the coin block is still in the markup.
    • Sources without the flag keep the previous code path unchanged.

Why the URL is derived from the title, not the row position

The template's chapterNumber is totalChapters - index, which is wrong for at least one Tangerine series: i-thought-i-was-the-monster-dukes-fake-tranquilizer has 173 rows titled Chapter 0 - Prologue … Chapter 172, so every position-derived URL is off by one — /chapter-173/ is a soft 404 (HTTP 200, series page, no lock marker), while /chapter-172/ is the real page.

What was probed (anonymous, read-only, no login, no purchase)

  • POST /series/<slug>/ajax/chapters/: locked rows carry data-chapter-<id>, span.coin = 10 and span.chapter-release-date = TBA or Unlocked in N days/weeks; the locked chapter page is HTTP 200 with .reading-content > div.premium.coin-10.content-blocked.premium-block holding only This chapter is locked!, while free pages have .reading-content > .text-left with prose.
  • Slug rule measured against the site's own free-row hrefs: 3288/3289 exact across the whole catalogue (21 series, 4074 rows = 3289 free + 785 premium). The single miss is a WordPress duplicate-slug _1 that no rule can predict. The rule also resolves all 28 non-exact premium titles: - Subtitle suffixes are dropped and a glued non-ASCII token stays in the slug.
  • URL sweep: all 52 reconstructed locked URLs for Garden of May plus 12 sampled from each of 3 other series (88 total) → 88/88 HTTP 200 with the lock marker, 0 soft-404s.
  • npm run build:compile, npm run build:multisrc and npm run check:plugin on the generated plugin all pass (Tangerine: 173 chapters, parseChapter 14497 chars). eslint and prettier --check are clean on both changed files; the 3 lint errors and 10 format warnings elsewhere in the repo are pre-existing in untouched files.

Locked-row verification (live, plugin-level)

Counts match the live lists exactly and paths are unique except one known site-side collision:

series rows vs live locked vs live notes
garden-of-may 212/212 52/52 emoji-free; TBA + countdown rows
villain-let-me-touch-you 191/191 46/46 emoji slug chapter-21-%f0%9f%94%9e listed; one duplicate-number collision (below)
holy-night-my-husband-is-definitely-a-paladin 464/464 105/105 12 title-suffixed premium rows; chapter-359 list-marked free but served gated

parseChapter: garden locked 212 → notice with TBA; locked 169 → notice with Unlocked in 4 weeks; free 160 → 6538 chars of prose; villain free 144 → 7859 chars; villain emoji slug → lock notice; villain /chapter-176/ (soft-404 slug) → "page could not be found" notice instead of a blank chapter. Read-time detection also covers rows the list labels free but the site serves gated: holy-night-my-husband-is-definitely-a-paladin/chapter-359/ is list-marked free with a real href, yet returns the lock notice ("locked behind 12 coins") while chapter-358/ returns 6827 chars of prose.

Known site-side collision: villain-let-me-touch-you has two locked rows, Chapter 166 and Chapter 166 - Secret, which both slug to chapter-166; the site serves post id 16700 there. I checked chapter-166_1 and chapter-166-2 — neither exists, so the second post is not reachable by any predictable slug. Both rows stay listed and the notice names the site's own chapter at that address rather than guessing.

Regression guard for other Madara sources

CitrusAurora, DuskBlossoms, TranslatinOtaku and SonicMTL all still pass check:plugin with this template change. ZetroTranslation's popularNovels FAIL reproduces identically on the base commit f2f3e4a, so it is pre-existing.

What was NOT tested

  • No entitled-session test. No account, cookie jar, purchase or unlock request was used. Whether a reader who buys a chapter sees the real body inside LNReader is unverified: LNReader plugins cannot log in, so an entitled session can only be exercised manually with an owner-provided test account. A ready-to-run probe for that is prepared outside this PR (it logs in with the account's own cookies, never forges sessions/nonces and never unlocks unless explicitly told to spend that account's coins).
  • An anonymous sweep of 77 retrieval avenues (AMP/print/_format/output/raw/preview/style variants, RSS/Atom/RDF feeds, oEmbed, wp-json + wp-manga/v1 + rest_route, the sitemap family, header/cache variants, and the whole admin-ajax action family taken from the site's own JS bundles) found no route that returns a locked body — every >200-char "hit" was a soft-404 fallback to the series page or an unrelated site feed. So the lock really is entitlement-only; there is nothing to parse around.
  • No verification of the site's countdown accuracy (that Unlocked in 4 weeks flips on schedule). The template picks a chapter up as free as soon as the site serves it with a real href.
  • Only Tangerine Archive enables the new option; other Madara sources were exercised only through the regression guard above.

RibatTRW and others added 2 commits September 23, 2026 19:52
Tangerine Archive lists its coin-locked (premium) chapters with href="#"
and replaces the chapter body with a "This chapter is locked!" block, so
the source dropped them from the chapter list entirely and their URLs
returned an empty chapter.

Add an opt-in lockedChapterPlaceholder option to the Madara template and
enable it for tangerinearchive:

- locked rows whose title is exactly "Chapter <n>" get a URL rebuilt from
  the pattern the free rows of the same list use, instead of href="#";
- parseChapter recognises a gated body (content-blocked/premium-block in
  .reading-content) and returns a readable lock notice with the chapter
  title, the coin price and the unlock countdown/TBA from the chapter
  list, instead of blank text.

No story prose is ever synthesized and the coin-lock is never bypassed;
sources without the flag keep the previous behaviour.

Co-Authored-By: pi crewmate (AI agent) <crewmate@firstmate.local>
Follow-ups from a fleet-wide probe of Tangerine Archive's locked rows:

- Slug the row title the way the site does (leading number, dots become
  dashes, and a non-ASCII token glued straight onto the number stays in
  the slug, e.g. "Chapter 176 🔞" -> chapter-176-%F0%9F%94%9E), instead
  of only accepting titles that are exactly "Chapter <n>". Measured
  against the real hrefs of 1509 free rows: 1508 exact, the single miss
  being a WordPress duplicate-slug "_1" that no rule can predict. This
  lists the 12 premium rows with title suffixes/subtitles and the two
  emoji-suffixed rows that were previously skipped.
- Never return a blank chapter: when the fetched page has no chapter body
  at all (a derived URL that falls back to the series page) return an
  explicit "page could not be found" notice.
- releaseTime is null for locked rows: their date span holds the unlock
  state ("Unlocked in 4 weeks" / "TBA"), which the app renders as
  "Invalid Date" if passed through as a release date.
- Never mask real prose: the lock notice now also requires the readable
  body to be empty, so an entitled session (or a chapter whose window
  just ended) keeps its real text even though the coin block is still in
  the markup.

Co-Authored-By: pi crewmate (AI agent) <crewmate@firstmate.local>
@RibatTRW RibatTRW closed this Sep 23, 2026
@RibatTRW
RibatTRW deleted the fm/tangerine-premium-placeholder 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