diff --git a/sources/base/keyoapp/keyoapp.go b/sources/base/keyoapp/keyoapp.go index 817f02b..3864a36 100755 --- a/sources/base/keyoapp/keyoapp.go +++ b/sources/base/keyoapp/keyoapp.go @@ -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()) diff --git a/sources/base/madara/madara.go b/sources/base/madara/madara.go index 16b6267..080136e 100755 --- a/sources/base/madara/madara.go +++ b/sources/base/madara/madara.go @@ -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", diff --git a/sources/base/masonry/masonry.go b/sources/base/masonry/masonry.go index 0d48c52..9c62ea2 100755 --- a/sources/base/masonry/masonry.go +++ b/sources/base/masonry/masonry.go @@ -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 {