feat(latest): extract per-site covers (#58)

This commit is contained in:
2026-08-10 00:53:54 +07:00
parent e8d1cba6c5
commit ff84eec3a0
2 changed files with 178 additions and 0 deletions
+77
View File
@@ -1,6 +1,7 @@
package latest
import (
"html"
"net/url"
"regexp"
"strconv"
@@ -146,3 +147,79 @@ func latestChapterFrom(site, seriesURL, body string) (latestChapter, bool) {
}
return best, found
}
var metaTagRe = regexp.MustCompile(`(?is)<meta\b[^>]*>`)
var doubleQuotedMetaAttrRe = regexp.MustCompile(`(?is)([a-z][a-z0-9:_-]*)\s*=\s*"([^"]*)"`)
var singleQuotedMetaAttrRe = regexp.MustCompile(`(?is)([a-z][a-z0-9:_-]*)\s*=\s*'([^']*)'`)
// comix stores its cover in the target series object in the page state rather
// than an HTML metadata tag. The medium rendition is deliberately selected
// instead of deriving a URL for a thumbnail or retaining the larger rendition.
var comixHIDRe = regexp.MustCompile(`"hid"\s*:\s*"[^"]+"`)
var comixPosterMediumRe = regexp.MustCompile(`(?s)"poster"\s*:\s*\{.*?"medium"\s*:\s*"([^"]+)"`)
// kagane's browser-fetched series response carries this published cover URL.
// Match the cover field, not merely an image-shaped URL elsewhere in JSON.
var kaganeCoverFieldRe = regexp.MustCompile(`"cover"\s*:\s*"([^"]+)"`)
func comixCoverURL(seriesURL, body string) string {
m := comixSlugRe.FindStringSubmatch(seriesURL)
if m == nil {
return ""
}
id := m[1]
if i := strings.Index(id, "-"); i != -1 {
id = id[:i]
}
hidRe := regexp.MustCompile(`"hid"\s*:\s*"` + regexp.QuoteMeta(id) + `"`)
hid := hidRe.FindStringIndex(body)
if hid == nil {
return ""
}
end := len(body)
if next := comixHIDRe.FindStringIndex(body[hid[1]:]); next != nil {
end = hid[1] + next[0]
}
if m := comixPosterMediumRe.FindStringSubmatch(body[hid[0]:end]); m != nil {
return publishedCoverURL(m[1])
}
return ""
}
func coverFrom(site, seriesURL, body string) (string, bool) {
var cover string
switch site {
case "asura", "demonic", "lightnovelworld":
cover = metaContent(body, "property", "og:image")
case "novelfull":
cover = metaContent(body, "name", "image")
case "comix":
cover = comixCoverURL(seriesURL, body)
case "kagane":
if m := kaganeCoverFieldRe.FindStringSubmatch(body); m != nil {
cover = publishedCoverURL(m[1])
}
}
return cover, cover != ""
}
func metaContent(body, attrName, attrValue string) string {
for _, tag := range metaTagRe.FindAllString(body, -1) {
attrs := make(map[string]string)
for _, m := range doubleQuotedMetaAttrRe.FindAllStringSubmatch(tag, -1) {
attrs[strings.ToLower(m[1])] = m[2]
}
for _, m := range singleQuotedMetaAttrRe.FindAllStringSubmatch(tag, -1) {
attrs[strings.ToLower(m[1])] = m[2]
}
if strings.EqualFold(attrs[strings.ToLower(attrName)], attrValue) {
return publishedCoverURL(attrs["content"])
}
}
return ""
}
func publishedCoverURL(value string) string {
value = strings.TrimSpace(html.UnescapeString(value))
return strings.ReplaceAll(value, " ", "%20")
}