Research: completion markers on the six Sites' Series pages (#129)

This commit is contained in:
2026-08-19 19:19:51 +07:00
parent f5d3fe58ec
commit 3f680b5860
+375
View File
@@ -0,0 +1,375 @@
# Machine-readable completion markers on the six Sites' Series pages
Research note for Gitea issue #129 (part of #114, "dashboard suggests *potentially
finished* Series"): for each of the six Sites, does the Series page publish a
completion marker a Poll could read — an explicit status field, not a
heuristic — and how trustworthy is it? The dashboard side (storage,
presentation, containment) is issue #130's job, not this note's.
All facts fetched live on **2026-08-19**. Sources are the live pages (URLs
below, fetched 2026-08-19) and the in-tree adapter code
(`backend/internal/latest/…`, branch `main` @ `f5d3fe5`). Every claim carries a
URL or a `file:line`. Interpretation rather than observation is marked
`[INFERENCE]`. Raw fetches archived in `/tmp/cm/` (throwaway).
Fetch method per Site: plain TLS (`curl`) where the site serves real HTML;
real Chrome via the repo's CDP sidecar pattern (Playwright) where a Cloudflare
challenge blocks plain fetch — comix.to and kagane.to always, novelfull.com
time-varying (today it 403'd plain fetch and cleared instantly in Chrome).
---
## 1. Summary verdict table
| Site | Status signal | Fixed vocabulary | Hiatus/dropped ≠ completed | Where the marker lives | Adapter cost |
|---|---|---|---|---|---|
| asurascans.com | **Yes** — visible block + JSON in page state | `{ongoing, completed, hiatus, dropped}` (fixed, verified via browse filter) | **Yes** — four distinct values | Series page Status block; JSON in astro-island props; badge on listing cards; no chapter-list END row | **Low–Medium** |
| demonicscans.org | **Yes** — info-block label/value pair | exactly `{Ongoing, Completed}` | **No** — the site cannot express hiatus/dropped | Series page info block only; search filter; no badges on listings, no END rows | **Medium** |
| comix.to | **Yes** — `status` key in `initial-data` JSON (same blob the adapter parses) + header badge | `{releasing, finished, on_hiatus}` observed; 5 values in browse filter incl. `discontinued`, `not yet released` | **Yes** — `on_hiatus` distinct; `discontinued` a separate value | Series page header badge; `initial-data` JSON; browse-card chips | **Low** |
| kagane.to | **Yes** — **two** explicit fields in the API JSON the adapter already fetches | `{Ongoing, Completed, Hiatus}` observed on both fields (99-series sample); no Dropped seen | **Yes for hiatus** (`upload_status`), **dropped unobservable** | Series API JSON (`publication_status`, `upload_status`); two badges on the page; no status filter in search | **Low** |
| novelfull.com | **Yes** — info-panel status link into `/status/<value>` taxonomy | exactly `{Ongoing, Completed}` (`OnGoing`/`Ongoing` alias); `Hiatus`/`Dropped` 404 | **No** — not expressible | Series page info panel; `/status/<value>` taxonomy pages; no chapter-list END row | **Medium** |
| lightnovelworld.net | **Yes** — visible class-named span **and** JSON-LD `creativeWorkStatus` | `{Ongoing, Completed, Hiatus}` (search filter radios; no dropped) | **Yes** — `PotentialActionStatus` (hiatus) ≠ `CompletedActionStatus` | Series page `.sertostat` span; `<script type="application/ld+json">` in the head; no badges on listings, no END rows | **Low–Medium** |
Bottom line: **all six Sites publish an explicit, machine-readable completion
marker on the Series page.** None requires inferring completion from absence of
updates. Four of six (asura, comix, kagane, lnw) express hiatus as a distinct
value; demonic and novelfull have a binary ongoing/completed vocabulary, which
is *safe* for the Poll (a stalled-but-unlabelled series reads "ongoing") but
cannot surface dropped/hiatus. Only kagane separates *work* status from
*translation/upload* status — the exact distinction the false-positive
question cares about. Details and quotes per Site in §2; the false-positive
verdict in §3.
---
## 2. Per-Site findings
### 2.1 asurascans.com — four-value status, JSON in page state
Fetch: plain TLS, 200, no challenge (all pages below, 2026-08-19). Series URLs
are `/comics/<slug>` where `<slug>` ends in a build hash the site appends
(adapter strips it: `asuraBuildHash`, `sites.go:73`).
**Series page — visible Status block** (ongoing sample,
`https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af`):
```html
<div class="text-xs text-white/50 mb-1">Status</div>
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-[#A78BFA]"></span>
<span class="text-base font-bold text-[#A78BFA] capitalize"> ongoing </span>
</div>
```
The completed sample (`https://asurascans.com/comics/solo-leveling`) renders the
same block with the value `completed`. The label is a plain-text span — the
value must be extracted from the text node, not a class.
**Same value in machine-readable JSON.** The series page state (serialized into
the astro-island props, HTML-escaped in the served document) carries
`&quot;status&quot;:[0,&quot;completed&quot;]` (Solo Leveling) /
`[0,&quot;ongoing&quot;]` (ongoing samples) — one key inside the same props
object the adapter already scans for the chapter list.
**Vocabulary: fixed, four values.** Homepage cards and browse pages embed one
status per card; the browse filter enumerates the set:
`https://asurascans.com/browse?status=completed`, `…?status=hiatus`,
`…?status=dropped` each return 20-card lists whose blobs read
`&quot;status&quot;:[0,&quot;completed&quot;]`, `[0,&quot;hiatus&quot;]`,
`[0,&quot;dropped&quot;]`. Homepage: 72 `ongoing` + 4 `hiatus` card blobs.
**Semantics of `dropped`** — scanlation-editorial, not work-ended
`[INFERENCE, evidence below]`. `https://asurascans.com/comics/demon-king-b60d532c`
is labelled `dropped` and its chapter links stop at `…/chapter/14`; the work
continues elsewhere. So the Poll must treat **only `completed`** as
potentially-finished; `dropped`/`hiatus` mean the site stopped releasing.
**Other marker locations.** Listing cards carry a badge:
`<span class="text-xs font-medium px-2 py-1 rounded capitalize bg-[#913FE2]/20 text-[#A78BFA]"> ongoing </span>`
for ongoing, and `… bg-[#86EFAC]/20 text-[#86EFAC]"> completed </span>` for
completed — the *color* does not discriminate (completed, hiatus and dropped
all share the green `#86EFAC` badge; ongoing is purple `#913FE2`), so a badge
reader must read the text. **No structured chapter-list END row**: Solo
Leveling's final chapter is titled `Side Story 21 { THE END }` — the marker is
prose inside a chapter title, unusable as a signal.
**Adapter touchpoints.** `asuraLatestChapter` (`sites.go:158`) scopes chapter
anchors by `asuraSlugRe` (`sites.go:67`); cover via `ogImageCover`
(`sites.go:334`, wired `sites.go:450`). The `status` key lives in the same
escaped props blob the chapter-list scan already walks — a new regex on the
same body, or a parse of the visible Status block.
### 2.2 demonicscans.org — binary vocabulary, nothing else expressible
Fetch: plain TLS, 200 (2026-08-19). Series URLs `/manga/<title>`.
**Series page — info block** (ongoing sample,
`https://demonicscans.org/manga/Catastrophic-Necromancer`):
```html
<div class="flex flex-row">
<li style="width:150px;color:#b2b2b2;">Status</li>
<li>Ongoing</li>
</div>
```
Completed sample (`https://demonicscans.org/manga/Solo-Leveling`): same markup,
`<li>Completed</li>`. The row sits inside the same info-block `<div>` as
Author / Rating / Last Update — a sibling `<li>` pair per field.
**Vocabulary: exactly `{Ongoing, Completed}`.** The advanced-search filter
(`https://demonicscans.org/advanced.php`) proves the set — the status `<select>`
has two options:
```html
<select class="form-control" name="status">
<option value="all">All</option>
<option value="ongoing">Ongoing</option>
<option value="completed">Completed</option>
</select>
```
There is no hiatus/dropped value anywhere on the site (`[INFERENCE]`: no such
string in the filter or any series page sampled). A scanlation that stalls
silently keeps reading `Ongoing`; the site cannot label a series
dropped/on-hiatus.
**Other marker locations.** None: homepage and `/lastupdates.php` show no
status text or badges on their cards (2026-08-19). No chapter-list END rows
(the last-chapter anchor is a plain `chaptered.php` link).
**Adapter touchpoints.** `demonicLatestChapter` (`sites.go:177`) scans
`chaptered.php?manga=<id>&chapter=<n>` anchors via `demonicChapterRe`
(`sites.go:77`), unscoped; cover `ogImageCover` (`sites.go:334`, wired
`sites.go:457`). A status reader adds a selector over the info-block `<li>`
pair; the block is sloppy HTML (bare `<li>` inside `<div>`s) but stable across
the two sampled themes/versions.
### 2.3 comix.to — `status` key in the exact JSON the adapter already parses
Fetch: Cloudflare JS challenge; cleared through a real Chrome (~12 s), then the
adapter's own payload shape was fetched in-tab (2026-08-19). The payload is
server-rendered HTML carrying `<script id="initial-data">…</script>` JSON
(`comixInitialDataRe`, `sites.go:248`); the Series detail entry
`["manga","detail","<slug>"]` contains `"status"` and the chapter list.
Observed values (three sampled series, all 2026-08-19):
| Series | URL | `status` | Header badge |
|---|---|---|---|
| Dungeons & Crayons (ongoing) | `https://comix.to/title/n8we-dungeons-and-crayons` | `"releasing"` | `RELEASING` |
| Countach (completed) | `https://comix.to/title/q77m-countach` | `"finished"` | `FINISHED` |
| How to Survive as the Daughter… (hiatus) | `https://comix.to/title/d0n08-how-to-survive-as-the-daughter-of-the-emperor-who-killed-me` | `"on_hiatus"` | `ON_HIATUS` |
The visible header badge (hiatus sample):
```html
<span class="mpage__badge mpage__badge--status mpage__badge--status-on_hiatus">ON_HIATUS</span>
```
The class encodes the value (`--status-on_hiatus`), the text is the raw
uppercase snake_case label.
**Vocabulary: five fixed values** from the browse advanced filter ("RELEASE
STATUS"): `Releasing`, `Finished`, `On hiatus`, `Discontinued`,
`Not yet released`. The filter URL encodes snake_case:
`https://comix.to/browse?statuses=on_hiatus` (plural `statuses`; a plain
`?status=finished` probe did *not* filter the list — titles of other statuses
remained). `discontinued` and `not yet released` were not observed on series
pages but are site-selectable filter values `[INFERENCE: they map to
`discontinued`/`not_yet_released` keys]`.
**Hiatus/dropped ≠ completed: yes** — `on_hiatus` is a distinct JSON value and
`discontinued` is a separate filter option.
**Other marker locations.** Browse cards render status chips (FINISHED /
RELEASING seen); the header badge above; the browse filter itself. No
chapter-list END row (chapter entries are per-chapter rows).
**Adapter touchpoints.** `comixLatestChapter` (`sites.go:185`) already parses
this exact `["manga","detail","<slug>"]` entry for the chapter URL — `status`
is a sibling key of the entry, so the read cost is near-zero. Cover
`comixCoverEntry` (`sites.go:344`). Browser-backed read `comixRead`
(`browser.go:154`), `Done` excludes the interstitial (`sites.go:471`).
### 2.4 kagane.to — two explicit status fields in the API JSON the adapter already fetches
Fetch: Cloudflare JS challenge; cleared through a real Chrome (12–32 s, flaky;
the challenge occasionally re-fires). The adapter's payload shape was fetched
in-tab: the Series page itself requests
`https://kagane.to/api/v2/series/<uuid>` (`kaganeSeriesRe`, `browser.go:25`) and
the poller consumes that JSON (`kaganeRead`, `browser.go:128`). Series URLs are
`/series/<uuid>`, e.g. `https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b`
(Infinite Decryption: The Strongest Level 0).
The API JSON carries **two explicit status fields** plus year bounds:
```json
"publication_status": "Ongoing",
"upload_status": "Ongoing",
"start_year": 2025,
"end_year": null,
```
- `publication_status` — the work's status (Ongoing / Completed / Hiatus).
- `upload_status` — the site's own release status.
**They diverge — proven.** "'Cause Calypso Can"
(`https://kagane.to/series/019c2034-f431-73ac-9350-11bc498f8c40`):
`"publication_status": "Ongoing"` but `"upload_status": "Hiatus"` — the work is
ongoing, the site's release is paused. This is precisely the
translation-stopped-vs-work-ended distinction the dashboard's false-positive
question needs, and kagane is the **only** Site that separates the two.
**Vocabulary.** Across a 99-series sample (search page 1, in-tab re-POST of the
page's own `POST /api/v2/search/series?page=0&size=99` request with body
`{"content_rating":["Safe","Suggestive"]}` — the page's own request returned
38 results, a direct re-POST 403'd even with Referer), both fields take only
`{Ongoing, Completed, Hiatus}`. **No Dropped value observed** on either field
in the sample; a dropped release presumably stays `Hiatus` or `Ongoing`
`[INFERENCE]`. The search page exposes no status filter (filters: Format,
Genres, Tags, People, Sources, Content Rating, Language, Presets).
**Page renders two badges** (value in text only, class carries no value), the
hiatus one yellow:
```html
<span class="inline-flex items-center rounded-full border px-2.5 py-0.5 … bg-yellow-500/10 text-yellow-600 dark:text-yellow-400">Hiatus</span>
```
**Adapter touchpoints.** `kaganeLatestChapter` (`sites.go:197`) scans
`"chapter_no":"<n>"` in the API body (`kaganeChapterRe`, `sites.go:98`);
cover `kaganeCoverEntry` (`sites.go:349`). `publication_status`/`upload_status`
are top-level keys of the same body — read cost near-zero.
### 2.5 novelfull.com — binary status, taxonomy-linked
Fetch: time-varying challenge. Plain TLS today returned HTTP 403 with a
"Just a moment…" interstitial (~5.7 KB); the same URL cleared instantly through
a real Chrome, and the cleared DOM is the adapter's payload shape (`novelfullRead`,
`browser.go:141`; `Done` excludes the interstitial, `sites.go:501`).
**Series page — info panel** (completed sample,
`https://novelfull.com/reverend-insanity.html`):
```html
<div><h3>Status:</h3><a href="/status/Completed">Completed</a></div>
```
Ongoing sample (`https://novelfull.com/a-cunning-pervert-in-the-cultivation-world.html`):
`<a href="/status/Ongoing">Ongoing</a>` in the same block.
**Vocabulary: exactly `{Ongoing, Completed}`**, proven by the taxonomy pages:
`https://novelfull.com/status/Ongoing` and `https://novelfull.com/status/OnGoing`
both return 200 with identical listings (the two spellings alias; the page title
normalizes to "Status: Ongoing"), while `https://novelfull.com/status/Hiatus` and
`https://novelfull.com/status/Dropped` return 404. No hiatus/dropped state
exists on the site.
**Hiatus/dropped ≠ completed: no** — not expressible; a stalled novel keeps
reading `Ongoing` indefinitely. Safe direction for the Poll (nothing falsely
"completed"), but the site cannot surface stalled releases.
**Other marker locations.** The `/status/<value>` taxonomy listing pages; the
series-page info panel only. Homepage latest-updates cards carry no status
(2026-08-19). **No chapter-list END row** (chapter rows are plain anchors;
checked `reverend-insanity.html`'s cleared DOM).
**Adapter touchpoints.** `novelfullLatestChapter` (`sites.go:204`) scopes
chapter anchors by `novelfullSlugRe` (`sites.go:103`); cover `novelfullCoverEntry`
(`sites.go:339`). The status `<a href="/status/…">` in the info panel is a new,
independent selector on the same body.
### 2.6 lightnovelworld.net — class-named span + schema.org JSON-LD
Fetch: plain TLS, 200 (2026-08-19). Series URLs `/novel/<slug>/`.
**Visible marker — `.sertostat` span, class = value** (completed sample,
`https://lightnovelworld.net/novel/a-will-eternal/`):
```html
<div class="sertostat">
<span class="Completed">Completed</span>
</div>
```
Ongoing sample (`https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/`):
`<span class="Ongoing">Ongoing</span>`; hiatus sample
(`https://lightnovelworld.net/novel/rebuild-world-wn/`): `<span class="Hiatus">Hiatus</span>`.
**Machine-readable marker — JSON-LD in the head.** The same pages carry a
`<script type="application/ld+json">` block (line 469 of the a-will-eternal
fetch) with a schema.org `Book` entry whose `creativeWorkStatus` is the
**completion signal, mapped to the ActionStatus vocabulary**:
| Sample | `creativeWorkStatus` |
|---|---|
| A Will Eternal (completed) | `"https://schema.org/CompletedActionStatus"` |
| All Jobs and Classes… (ongoing) | `"https://schema.org/ActiveActionStatus"` |
| Rebuild World (hiatus) | `"https://schema.org/PotentialActionStatus"` |
**Vocabulary: `{Ongoing, Completed, Hiatus}`.** The homepage search filter
enumerates it: radio inputs `id="anime_status-ongoing"`,
`id="anime_status-completed"`, `id="anime_status-hiatus"` — no dropped option.
**Hiatus ≠ completed: yes** — `PotentialActionStatus` (hiatus) is a distinct
JSON-LD value from `CompletedActionStatus`; the Poll can treat only the latter
as potentially-finished.
**Other marker locations.** The `.sertostat` span only. Homepage/listing cards
carry **no** status badges (2026-08-19 — earlier word hits on the homepage were
the filter radios and a series title containing "Dropped"). **No chapter-list
END row**: the chapter list is `epl-num`/`epl-title` rows with no end marker on
the completed A Will Eternal.
**Adapter touchpoints.** `lnwLatestChapter` (`sites.go:222`) scopes chapter
anchors by `lnwChapterRe` (`sites.go:115`) and truncates at the
`lnwCommentMarker` ("wpd-threads", `sites.go:124`); cover `ogImageCover`
(`sites.go:334`, wired `sites.go:508`). The JSON-LD block is a separate script
in the head — one regex or JSON parse away; the `.sertostat` span is in the
article body.
---
## 3. The false-positive question — cross-site verdict
The dashboard wants to *suggest* "potentially finished" without false
positives (issue #129 context). Per Site, what would a Poll have to believe to
mark a Series finished, and can that belief be wrong?
| Site | Poll reads | False-positive exposure |
|---|---|---|
| asurascans.com | `status == completed` only | `dropped`/`hiatus` are distinct values — reading them as anything but completed is a false *negative*, not a false positive. `completed` is the site's own editorial call on a finished work (Solo Leveling). Safe. |
| demonicscans.org | `Status == Completed` | Binary vocabulary: `Completed` is the site's label and hiatus/dropped are unexpressible, so a label never lies *about completion* — but "Completed" is site-editorial and could mark a translation ended early. Poll a labelled `Completed` series as a *suggestion*, not a fact. |
| comix.to | `status == finished` | `on_hiatus` and `discontinued` are distinct values; only `finished` triggers. Same editorial caveat. Safe. |
| kagane.to | `publication_status == Completed` **or** `upload_status == Completed` | **The only Site where the two questions are answered separately.** Work-finished should read `publication_status`; `upload_status == Completed` is the site's release-complete flag and can coexist with `publication_status == Ongoing` (inverse of the Calypso Can case `[INFERENCE]`). The Poll should decide which field drives "potentially finished". |
| novelfull.com | `Status == Completed` | Binary vocabulary; hiatus/dropped unexpressible (taxonomy 404s). A labelled `Completed` (e.g. Reverend Insanity, The Legendary Mechanic) is the site's call. No hiatus false-positive path exists. |
| lightnovelworld.net | `creativeWorkStatus == CompletedActionStatus` | `PotentialActionStatus` (hiatus) is distinct; only `CompletedActionStatus` triggers. Safest single-value reading of the six. |
Cross-site verdict: **no Site's status field is owner-written
`series.finished_at` data** (that stays the owner's prerogative, #114/#124
context) — every value here is the site's editorial status. But **none of the
six forces the Poll to guess**: all publish explicit markers, and only asura
and kagane introduce *translation-state* vocabulary (`dropped` / `upload_status`)
that a naive "status == completed" reader could misinterpret — in both cases
the danger is a false *negative* (treating a dropped/hiatus series as still
ongoing), not a false positive, **provided the Poll reads only the
completed/finished value** and treats anything else as not-completed.
---
## 4. Adapter touchpoints (for #130's containment decision)
| Site | Marker source | Existing parse this piggybacks on | New parse needed |
|---|---|---|---|
| asura | escaped `&quot;status&quot;:[0,&quot;…&quot;]` in astro-island props, or visible Status block | `asuraLatestChapter` walks the same props blob (`sites.go:158`) | one regex on the same body |
| demonic | info-block `<li>` pair | — (`demonicLatestChapter` scans chapter anchors only, `sites.go:177`) | selector over the info block |
| comix | `["manga","detail",…]."status"` in `initial-data` | `comixLatestChapter` parses that exact entry (`sites.go:185`, `sites.go:248`) | sibling key read |
| kagane | `publication_status` / `upload_status` top-level keys | `kaganeLatestChapter` reads the same API body (`sites.go:197`) | sibling key read (×2) |
| novelfull | info-panel `<a href="/status/…">` | — (`novelfullLatestChapter` scopes chapter anchors, `sites.go:204`) | selector over the info panel |
| lightnovelworld | head JSON-LD `creativeWorkStatus`, or `.sertostat` span | — (`lnwLatestChapter` truncates at `wpd-threads`, `sites.go:222`) | one JSON-LD regex / parse |
Challenge status for a future Poll: asura, demonic, lnw are plain-fetch; comix
and kagane require the browser sidecar on every poll (Cloudflare challenge
present today, 2026-08-19); novelfull requires the sidecar *whenever its
challenge is up* (it was up today, 2026-08-19).