Add completed markers for all six sites (#168)

site gains a Completed predicate answering whether a fetched body
carries the Site's own completed value: asura escaped props status,
demonic info-block Status pair, comix scoped detail entry (shared
comixDetailQuery helper with the cover read), kagane
publication_status only, novelfull status link, lightnovelworld
JSON-LD creativeWorkStatus. siteCompletedFrom dispatches it; an
absent hint stays false. Fixtures trimmed from live pages fetched
2026-08-22; TestSiteCompletedFrom covers completed, ongoing,
selector-removed, and challenge bodies per site, the kagane
upload/publication divergence, and an unknown site.
This commit is contained in:
2026-08-22 18:23:42 +07:00
parent 8e4fa6448e
commit e69e513be4
2 changed files with 382 additions and 14 deletions
+269
View File
@@ -188,6 +188,100 @@ const lnwSeriesFixture = `
</div>
`
// Completed-marker fixtures, trimmed from the live pages fetched 2026-08-22
// for the verification step below. Selector-removed rows delete the marker
// from the consts and must answer false.
// Trimmed from https://asurascans.com/comics/solo-leveling (301 to
// /comics/solo-leveling-b60d532c) fetched 2026-08-22. The astro-island props
// are HTML-escaped in the served document, so the quotes arrive as &quot;.
const asuraCompletedFixture = `
&quot;bookmarkCount&quot;:[0,39128],&quot;status&quot;:[0,&quot;completed&quot;],&quot;type&quot;:[0,&quot;manhwa&quot;]
`
// Trimmed from https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af
// fetched 2026-08-22.
const asuraOngoingFixture = `
&quot;bookmarkCount&quot;:[0,54027],&quot;status&quot;:[0,&quot;ongoing&quot;],&quot;type&quot;:[0,&quot;manhwa&quot;]
`
// Trimmed from https://demonicscans.org/manga/Solo-Leveling fetched
// 2026-08-22. The info block is sloppy: bare <li>s inside a <div>, label and
// value as a sibling pair.
const demonicCompletedFixture = `
<div class="flex flex-row">
<li style="width:150px;color:#b2b2b2;">Status</li>
<li>Completed</li>
</div>
`
// Trimmed from https://demonicscans.org/manga/Catastrophic-Necromancer
// fetched 2026-08-22. Same block, ongoing value.
const demonicOngoingFixture = `
<div class="flex flex-row">
<li style="width:150px;color:#b2b2b2;">Status</li>
<li>Ongoing</li>
</div>
`
// Trimmed from https://comix.to/title/q77m-countach fetched 2026-08-22 over a
// cleared Chrome tab (the in-tab fetch of the Series URL, the same
// server-rendered payload the poll reads). The recommended strip on this very
// page carries a finished entry; only the ["manga","detail","q77m"] entry
// counts.
const comixCompletedFixture = `<script type="application/json" id="initial-data">{"queries":{"[\"manga\",\"recommended\",\"q77m\",1]":{"items":[{"hid":"pz65","title":"Wangan Midnight: C1 Runner","status":"finished"}]},"[\"manga\",\"detail\",\"q77m\"]":{"id":13211,"hid":"q77m","title":"Countach","status":"finished","originalLanguage":"ja"}}}</script>`
// Trimmed from https://comix.to/title/n8we-dungeons-and-crayons fetched
// 2026-08-22 the same way. The recommended strip includes a finished entry;
// the target detail is releasing, and only the detail read counts.
const comixOngoingFixture = `<script type="application/json" id="initial-data">{"queries":{"[\"manga\",\"recommended\",\"n8we\",1]":{"items":[{"hid":"g2rk","title":"On the Way to Meet Mom","status":"finished"}]},"[\"manga\",\"detail\",\"n8we\"]":{"id":1429,"hid":"n8we","title":"Dungeons and Crayons","status":"releasing","originalLanguage":"ko"}}}</script>`
// Trimmed from GET https://kagane.to/api/v2/series/019c29c3-5abd-70e6-9efc-1f1f22d839f6
// on 2026-08-22.
const kaganeCompletedFixture = `{"series_id":"019c29c3-5abd-70e6-9efc-1f1f22d839f6","title":"Real Account 1-17","publication_status":"Completed","upload_status":"Completed"}`
// Trimmed from GET https://kagane.to/api/v2/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b
// on 2026-08-22.
const kaganeOngoingFixture = `{"series_id":"019f84bc-9ba0-7ed9-86f5-8b905ec7c28b","title":"Infinite Decryption: The Strongest Level 0","publication_status":"Ongoing","upload_status":"Ongoing"}`
// Composed, not a single live trim: upload Completed alongside a
// non-Completed publication status is the research note's inferred inverse
// case and did not turn up in the ~1900-series live scan of 2026-08-22 (both
// field vocabularies are live-verified; the note's live divergence is 'Cause
// Calypso Can, publication Ongoing + upload Hiatus). The predicate must read
// publication_status only.
const kaganeDivergentFixture = `{"series_id":"019f84bc-9ba0-7ed9-86f5-8b905ec7c28b","title":"Infinite Decryption: The Strongest Level 0","publication_status":"Ongoing","upload_status":"Completed"}`
// Trimmed from https://novelfull.com/reverend-insanity.html fetched
// 2026-08-22 (the challenge served to plain TLS cleared in a real Chrome the
// same day).
const novelfullCompletedFixture = `<div><h3>Status:</h3><a href="/status/Completed">Completed</a></div>`
// Trimmed from https://novelfull.com/a-cunning-pervert-in-the-cultivation-world.html
// fetched 2026-08-22.
const novelfullOngoingFixture = `<div><h3>Status:</h3><a href="/status/Ongoing">Ongoing</a></div>`
// Trimmed from the JSON-LD block in the head of
// https://lightnovelworld.net/novel/a-will-eternal/ fetched 2026-08-22. The
// block sits ahead of the wpdiscuz thread, so the predicate reads the whole
// body and needs no comment marker, unlike lnwLatestChapter.
const lnwCompletedFixture = `<script type="application/ld+json">{
"@context": "https://schema.org",
"@type": "Book",
"name": "A Will Eternal",
"creativeWorkStatus": "https://schema.org/CompletedActionStatus"
}</script>`
// Trimmed from
// https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/
// fetched 2026-08-22. Same block, ongoing value.
const lnwOngoingFixture = `<script type="application/ld+json">{
"@context": "https://schema.org",
"@type": "Book",
"name": "All Jobs and Classes I Just Wanted One Skill Not Them All",
"creativeWorkStatus": "https://schema.org/ActiveActionStatus"
}</script>`
// Trimmed from https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af
// (redirected to ...-00dcbf97) on 2026-08-10.
const asuraCoverFixture = `<meta property="og:image" content="https://cdn.asurascans.com/asura-images/covers/chronicles-of-the-demon-faction.d4dcb8.webp">`
@@ -473,3 +567,178 @@ func TestLatestChapterFrom(t *testing.T) {
})
}
}
func TestSiteCompletedFrom(t *testing.T) {
const asuraURL = "https://asurascans.com/comics/solo-leveling-b60d532c"
const demonicURL = "https://demonicscans.org/manga/Solo-Leveling"
const comixURL = "https://comix.to/title/q77m-countach"
const kaganeURL = "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b"
const novelfullURL = "https://novelfull.com/reverend-insanity.html"
const lnwURL = "https://lightnovelworld.net/novel/a-will-eternal/"
tests := []struct {
name string
site string
seriesURL string
body string
want bool
}{
{
name: "asura completed via escaped props status",
site: "asura", seriesURL: asuraURL, body: asuraCompletedFixture,
want: true,
},
{
name: "asura ongoing value is not completed",
site: "asura",
seriesURL: "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af",
body: asuraOngoingFixture,
want: false,
},
{
name: "asura with the status key removed",
site: "asura", seriesURL: asuraURL,
body: strings.ReplaceAll(asuraCompletedFixture, `&quot;status&quot;:[0,&quot;completed&quot;]`, ""),
want: false,
},
{
name: "asura challenge page",
site: "asura", seriesURL: asuraURL, body: challengeFixture,
want: false,
},
{
name: "demonic completed info-block pair",
site: "demonic", seriesURL: demonicURL, body: demonicCompletedFixture,
want: true,
},
{
name: "demonic ongoing value is not completed",
site: "demonic",
seriesURL: "https://demonicscans.org/manga/Catastrophic-Necromancer",
body: demonicOngoingFixture,
want: false,
},
{
name: "demonic with the value li removed",
site: "demonic", seriesURL: demonicURL,
body: strings.ReplaceAll(demonicCompletedFixture, "<li>Completed</li>", ""),
want: false,
},
{
name: "comix recommended finished does not count",
site: "comix",
seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons",
body: comixOngoingFixture,
want: false,
},
{
name: "comix detail finished wins over a finished recommended strip",
site: "comix",
seriesURL: comixURL,
body: comixCompletedFixture,
want: true,
},
{
name: "comix with the detail status removed",
site: "comix", seriesURL: comixURL,
body: strings.ReplaceAll(comixCompletedFixture, `"status":"finished",`, ""),
want: false,
},
{
name: "comix challenge page",
site: "comix", seriesURL: comixURL, body: challengeFixture,
want: false,
},
{
name: "kagane publication Completed",
site: "kagane",
seriesURL: "https://kagane.to/series/019c29c3-5abd-70e6-9efc-1f1f22d839f6",
body: kaganeCompletedFixture,
want: true,
},
{
name: "kagane upload Completed does not count",
site: "kagane", seriesURL: kaganeURL, body: kaganeDivergentFixture,
want: false,
},
{
name: "kagane ongoing values are not completed",
site: "kagane", seriesURL: kaganeURL, body: kaganeOngoingFixture,
want: false,
},
{
name: "kagane with publication_status removed",
site: "kagane", seriesURL: kaganeURL,
body: strings.ReplaceAll(kaganeCompletedFixture, `"publication_status":"Completed",`, ""),
want: false,
},
{
name: "kagane challenge page",
site: "kagane", seriesURL: kaganeURL, body: challengeFixture,
want: false,
},
{
name: "novelfull status link Completed",
site: "novelfull",
seriesURL: novelfullURL,
body: novelfullCompletedFixture,
want: true,
},
{
name: "novelfull ongoing link is not completed",
site: "novelfull",
seriesURL: "https://novelfull.com/a-cunning-pervert-in-the-cultivation-world.html",
body: novelfullOngoingFixture,
want: false,
},
{
name: "novelfull with the status link removed",
site: "novelfull", seriesURL: novelfullURL,
body: strings.ReplaceAll(novelfullCompletedFixture, `<a href="/status/Completed">Completed</a>`, ""),
want: false,
},
{
name: "novelfull challenge page",
site: "novelfull", seriesURL: novelfullURL, body: challengeFixture,
want: false,
},
{
name: "lightnovelworld JSON-LD CompletedActionStatus",
site: "lightnovelworld",
seriesURL: lnwURL,
body: lnwCompletedFixture,
want: true,
},
{
name: "lightnovelworld ActiveActionStatus is not completed",
site: "lightnovelworld",
seriesURL: "https://lightnovelworld.net/novel/all-jobs-and-classes-i-just-wanted-one-skill-not-them-all/",
body: lnwOngoingFixture,
want: false,
},
{
name: "lightnovelworld with creativeWorkStatus removed",
site: "lightnovelworld", seriesURL: lnwURL,
body: strings.ReplaceAll(lnwCompletedFixture, `"creativeWorkStatus": "https://schema.org/CompletedActionStatus"`, ""),
want: false,
},
{
name: "lightnovelworld challenge page",
site: "lightnovelworld", seriesURL: lnwURL, body: challengeFixture,
want: false,
},
{
name: "unknown site",
site: "mangadex", seriesURL: "https://mangadex.org/title/x", body: asuraCompletedFixture,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := siteCompletedFrom(tt.site, tt.seriesURL, tt.body); got != tt.want {
t.Errorf("siteCompletedFrom(%q, %q, body) = %v, want %v", tt.site, tt.seriesURL, got, tt.want)
}
})
}
}