feat(latest): parse newest chapter for novelfull and lightnovelworld
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package latest
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -34,6 +35,17 @@ var comixSlugRe = regexp.MustCompile(`/title/([^/?#]+)`)
|
||||
// there are no anchors to scan.
|
||||
var kaganeChapterRe = regexp.MustCompile(`"chapter_no":"([0-9.]+)"`)
|
||||
|
||||
// novelfullSlugRe pulls the series slug out of a stored series_url. novelfull
|
||||
// series pages are "/<slug>.html"; their chapter anchors are
|
||||
// "/<slug>/chapter-<n>[-<title-slug>].html". Verified live 2026-08-05.
|
||||
var novelfullSlugRe = regexp.MustCompile(`^/([^/?#]+)\.html$`)
|
||||
|
||||
// lnwSlugRe does the same for lightnovelworld, whose series pages live under
|
||||
// /novel/<slug>/ while its chapter URLs are flat at the site root:
|
||||
// "/<slug>-chapter-<n>/", absolute in the page's own anchors. Verified live
|
||||
// 2026-08-05.
|
||||
var lnwSlugRe = regexp.MustCompile(`^/novel/([^/?#]+)/?$`)
|
||||
|
||||
// latestChapterFrom returns the highest chapter number body advertises for this
|
||||
// series. ok is false when the body yields nothing usable — an unknown site, an
|
||||
// empty body, a Cloudflare challenge page, and a site redesign all land here,
|
||||
@@ -86,6 +98,29 @@ func latestChapterFrom(site, seriesURL, body string) (latestChapter, bool) {
|
||||
re = regexp.MustCompile(`"latestChapterUrl":"/title/` + regexp.QuoteMeta(id) + `-[^"]*-chapter-([0-9.]+)"`)
|
||||
case "kagane":
|
||||
re = kaganeChapterRe
|
||||
case "novelfull":
|
||||
u, err := url.Parse(seriesURL)
|
||||
if err != nil {
|
||||
return latestChapter{}, false
|
||||
}
|
||||
m := novelfullSlugRe.FindStringSubmatch(u.Path)
|
||||
if m == nil {
|
||||
return latestChapter{}, false
|
||||
}
|
||||
// Scoped to this series' slug for the same reason asura is: page 1
|
||||
// carries a "latest chapters" widget and a "you may also like" strip,
|
||||
// and neither may contribute to the maximum.
|
||||
re = regexp.MustCompile(`/` + regexp.QuoteMeta(m[1]) + `/chapter-([0-9.]+)`)
|
||||
case "lightnovelworld":
|
||||
u, err := url.Parse(seriesURL)
|
||||
if err != nil {
|
||||
return latestChapter{}, false
|
||||
}
|
||||
m := lnwSlugRe.FindStringSubmatch(u.Path)
|
||||
if m == nil {
|
||||
return latestChapter{}, false
|
||||
}
|
||||
re = regexp.MustCompile(`lightnovelworld\.net/` + regexp.QuoteMeta(m[1]) + `-chapter-([0-9.]+)/`)
|
||||
default:
|
||||
return latestChapter{}, false
|
||||
}
|
||||
|
||||
@@ -52,6 +52,33 @@ const kaganeAPIFixture = `
|
||||
{"book_id":"c","title":"Episode 40.5","chapter_no":"40.5","sort_no":40}]}
|
||||
`
|
||||
|
||||
// Trimmed from https://novelfull.com/reverend-insanity.html fetched 2026-08-05.
|
||||
// The page carries a newest-first "latest chapters" widget above an
|
||||
// oldest-first paginated list, so the newest anchor is deliberately NOT last —
|
||||
// only a maximum finds it. The final anchor belongs to another series and must
|
||||
// be excluded by slug scoping.
|
||||
const novelfullSeriesFixture = `
|
||||
<div class="l-chapters">
|
||||
<a href="/reverend-insanity/chapter-2334-fang-yuan-and-giant-sun.html">Chapter 2334</a>
|
||||
<a href="/reverend-insanity/chapter-2333-three-venerables.html">Chapter 2333</a>
|
||||
</div>
|
||||
<ul class="list-chapter">
|
||||
<li><a href="/reverend-insanity/chapter-1.html">Chapter 1</a></li>
|
||||
<li><a href="/reverend-insanity/chapter-2.html">Chapter 2</a></li>
|
||||
</ul>
|
||||
<a href="/release-that-witch/chapter-9999.html">Chapter 9999</a>
|
||||
`
|
||||
|
||||
// Trimmed from https://lightnovelworld.net/novel/a-will-eternal/ fetched
|
||||
// 2026-08-05. Its chapter anchors are absolute and flat — /<slug>-chapter-<n>/
|
||||
// at the site root, not under /novel/. The last anchor is another series'.
|
||||
const lnwSeriesFixture = `
|
||||
<a href="https://lightnovelworld.net/a-will-eternal-chapter-1/">Chapter 1</a>
|
||||
<a href="https://lightnovelworld.net/a-will-eternal-chapter-1317/">Chapter 1317</a>
|
||||
<a href="https://lightnovelworld.net/a-will-eternal-chapter-1298/">Chapter 1298</a>
|
||||
<a href="https://lightnovelworld.net/overgeared-chapter-9999/">Chapter 9999</a>
|
||||
`
|
||||
|
||||
func TestLatestChapterFrom(t *testing.T) {
|
||||
const asuraURL = "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af"
|
||||
const demonicURL = "https://demonicscans.org/manga/Catastrophic-Necromancer"
|
||||
@@ -146,6 +173,48 @@ func TestLatestChapterFrom(t *testing.T) {
|
||||
body: challengeFixture,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "novelfull takes the max and ignores another series",
|
||||
site: "novelfull",
|
||||
seriesURL: "https://novelfull.com/reverend-insanity.html",
|
||||
body: novelfullSeriesFixture,
|
||||
wantOK: true, wantNum: 2334, wantLabel: "Chapter 2334",
|
||||
},
|
||||
{
|
||||
name: "novelfull yields nothing on a challenge page",
|
||||
site: "novelfull",
|
||||
seriesURL: "https://novelfull.com/reverend-insanity.html",
|
||||
body: challengeFixture,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "novelfull with an unparseable series url",
|
||||
site: "novelfull",
|
||||
seriesURL: "https://novelfull.com/genre/Fantasy",
|
||||
body: novelfullSeriesFixture,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "lightnovelworld takes the max and ignores another series",
|
||||
site: "lightnovelworld",
|
||||
seriesURL: "https://lightnovelworld.net/novel/a-will-eternal/",
|
||||
body: lnwSeriesFixture,
|
||||
wantOK: true, wantNum: 1317, wantLabel: "Chapter 1317",
|
||||
},
|
||||
{
|
||||
name: "lightnovelworld tolerates a series url with no trailing slash",
|
||||
site: "lightnovelworld",
|
||||
seriesURL: "https://lightnovelworld.net/novel/a-will-eternal",
|
||||
body: lnwSeriesFixture,
|
||||
wantOK: true, wantNum: 1317, wantLabel: "Chapter 1317",
|
||||
},
|
||||
{
|
||||
name: "lightnovelworld yields nothing on a challenge page",
|
||||
site: "lightnovelworld",
|
||||
seriesURL: "https://lightnovelworld.net/novel/a-will-eternal/",
|
||||
body: challengeFixture,
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user