fix(base): add override hooks for masonry, madara, keyoapp

Madara:
- Add PopularURL/LatestURL Config hooks for custom URL building
  (needed by hentai4free which uses search-based popular/latest URLs)

Masonry:
- Replace CSS :not(:has(a[href*=/video/])) with programmatic filtering.
  goquery/cascadia doesn't support :has() + attribute selectors
  (Jsoup does, hence Kotlin works but Go didn't)

Keyoapp:
- Add overridable selector fields (PopularSelector, DescriptionSelector,
  StatusSelector, AuthorSelector, ArtistSelector) to Config
This commit is contained in:
achmad
2026-05-14 22:31:11 +07:00
parent 8c642905b7
commit 00e61480c3
3 changed files with 88 additions and 21 deletions
+41 -10
View File
@@ -21,6 +21,17 @@ type Config struct {
Name string
BaseURL string
Lang string
// Override popular manga selector. Empty means use default.
PopularSelector string
// Override description selector. Empty means use default.
DescriptionSelector string
// Override status selector. Empty means use default.
StatusSelector string
// Override author selector. Empty means use default.
AuthorSelector string
// Override artist selector. Empty means use default.
ArtistSelector string
}
type Source struct {
@@ -116,12 +127,13 @@ func (s *Source) mangaFromElement(el *goquery.Selection) source.SManga {
return m
}
var popularSelectors = []string{"Popular", "Popularie", "Trending"}
func popularSelector() string {
func (s *Source) popularSelector() string {
if s.cfg.PopularSelector != "" {
return s.cfg.PopularSelector
}
var parts []string
for _, s := range popularSelectors {
parts = append(parts, fmt.Sprintf("div:contains(%s) + div .group.overflow-hidden.grid", s))
for _, label := range []string{"Popular", "Popularie", "Trending"} {
parts = append(parts, fmt.Sprintf("div:contains(%s) + div .group.overflow-hidden.grid", label))
}
return strings.Join(parts, ", ")
}
@@ -132,7 +144,7 @@ func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
return source.MangasPage{}, err
}
var mangas []source.SManga
doc.Find(popularSelector()).Each(func(_ int, el *goquery.Selection) {
doc.Find(s.popularSelector()).Each(func(_ int, el *goquery.Selection) {
m := s.mangaFromElement(el)
if m.URL != "" && m.Title != "" {
mangas = append(mangas, m)
@@ -213,10 +225,29 @@ func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
// Thumbnail from div[class*=photoURL] background-image style
result.ThumbnailURL = getImageURL(doc.Find("div[class*=photoURL]").First(), s.cfg.BaseURL)
result.Description = strings.TrimSpace(doc.Find("div:containsOwn(Synopsis) ~ div").First().Text())
result.Status = parseStatus(doc.Find("div:has(span:containsOwn(Status)) ~ div").First())
result.Author = strings.TrimSpace(doc.Find("div:has(span:containsOwn(Author)) ~ div").First().Text())
result.Artist = strings.TrimSpace(doc.Find("div:has(span:containsOwn(Artist)) ~ div").First().Text())
descSel := "div:containsOwn(Synopsis) ~ div"
if s.cfg.DescriptionSelector != "" {
descSel = s.cfg.DescriptionSelector
}
result.Description = strings.TrimSpace(doc.Find(descSel).First().Text())
statusSel := "div:has(span:containsOwn(Status)) ~ div"
if s.cfg.StatusSelector != "" {
statusSel = s.cfg.StatusSelector
}
result.Status = parseStatus(doc.Find(statusSel).First())
authorSel := "div:has(span:containsOwn(Author)) ~ div"
if s.cfg.AuthorSelector != "" {
authorSel = s.cfg.AuthorSelector
}
result.Author = strings.TrimSpace(doc.Find(authorSel).First().Text())
artistSel := "div:has(span:containsOwn(Artist)) ~ div"
if s.cfg.ArtistSelector != "" {
artistSel = s.cfg.ArtistSelector
}
result.Artist = strings.TrimSpace(doc.Find(artistSel).First().Text())
// Title from h1 inside the series header
result.Title = strings.TrimSpace(doc.Find("h1").First().Text())
+27 -8
View File
@@ -30,6 +30,13 @@ type Config struct {
// UseNewChapterEndpoint: use /ajax/chapters instead of admin-ajax.php.
UseNewChapterEndpoint bool
// PopularURL overrides the URL for GetPopularManga. If nil, default is used.
// Takes page number (1-indexed), returns full URL string.
PopularURL func(page int) string
// LatestURL overrides the URL for GetLatestUpdates. If nil, default is used.
LatestURL func(page int) string
// Overridable selectors — leave empty to use defaults.
PopularMangaSelector string
PopularMangaURLSelector string
@@ -196,10 +203,7 @@ func (s *Source) parseSearchMangaFromElement(el *goquery.Selection) source.SMang
}
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
pageStr := s.searchPage(page)
rawURL := fmt.Sprintf("%s/%s/%s?m_orderby=views",
strings.TrimRight(s.cfg.BaseURL, "/"), s.cfg.MangaSubString, pageStr)
rawURL := s.popularURL(page)
doc, err := s.get(context.Background(), rawURL)
if err != nil {
return source.MangasPage{}, err
@@ -208,10 +212,7 @@ func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
}
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
pageStr := s.searchPage(page)
rawURL := fmt.Sprintf("%s/%s/%s?m_orderby=latest",
strings.TrimRight(s.cfg.BaseURL, "/"), s.cfg.MangaSubString, pageStr)
rawURL := s.latestURL(page)
doc, err := s.get(context.Background(), rawURL)
if err != nil {
return source.MangasPage{}, err
@@ -219,6 +220,24 @@ func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
return s.parseMangaList(doc, s.cfg.PopularMangaSelector, true), nil
}
func (s *Source) popularURL(page int) string {
if s.cfg.PopularURL != nil {
return s.cfg.PopularURL(page)
}
pageStr := s.searchPage(page)
return fmt.Sprintf("%s/%s/%s?m_orderby=views",
strings.TrimRight(s.cfg.BaseURL, "/"), s.cfg.MangaSubString, pageStr)
}
func (s *Source) latestURL(page int) string {
if s.cfg.LatestURL != nil {
return s.cfg.LatestURL(page)
}
pageStr := s.searchPage(page)
return fmt.Sprintf("%s/%s/%s?m_orderby=latest",
strings.TrimRight(s.cfg.BaseURL, "/"), s.cfg.MangaSubString, pageStr)
}
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
base := strings.TrimRight(s.cfg.BaseURL, "/")
searchURL := fmt.Sprintf("%s/?s=%s&post_type=wp-manga&paged=%d",
+20 -3
View File
@@ -69,10 +69,14 @@ func imgAttr(img *goquery.Selection) string {
}
func (s *Source) parseMangaList(doc *goquery.Document) source.MangasPage {
// Exclude static galleries and broken entries
const sel = ".list-gallery:not(.static) figure:not(:has(a[href*=cdn.]))"
var mangas []source.SManga
doc.Find(sel).Each(func(_ int, el *goquery.Selection) {
// goquery's :has() doesn't support attribute selectors like [href*=/video/],
// so we filter programmatically instead of relying on CSS :not(:has(...)).
doc.Find(".list-gallery:not(.static) figure").Each(func(_ int, el *goquery.Selection) {
// Skip video entries (matching Kotlin's :not(:has(a[href*=/video/])))
if hasAttr(el, "a", "href", "/video/") {
return
}
a := el.Find("a").First()
if a.Length() == 0 {
return
@@ -94,6 +98,19 @@ func (s *Source) parseMangaList(doc *goquery.Document) source.MangasPage {
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}
}
// hasAttr checks if any descendant matching tag has an href containing substr.
func hasAttr(el *goquery.Selection, tag, attr, substr string) bool {
found := false
el.Find(tag).EachWithBreak(func(_ int, a *goquery.Selection) bool {
if v, ok := a.Attr(attr); ok && strings.Contains(v, substr) {
found = true
return false
}
return true
})
return found
}
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
var u string
switch page {