9a42dd2ab1
- Remove global ProxyEnabled() logic from httpclient - Each source now explicitly chooses client at import time: - flare client: for JS-rendering/cloudflare sources - normal httpclient: for REST API sources - Updated 29 base sources based on Kotlin reference (network.cloudflareClient)
340 lines
8.1 KiB
Go
Executable File
340 lines
8.1 KiB
Go
Executable File
package stalkercms
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
|
|
"goyomi/internal/httpclient/flare"
|
|
"goyomi/internal/source"
|
|
)
|
|
|
|
type Config struct {
|
|
Name string
|
|
BaseURL string
|
|
Lang string
|
|
PopularMangaPath string
|
|
LatestUpdatesLoadMorePath string
|
|
}
|
|
|
|
type Source struct {
|
|
cfg Config
|
|
client *flare.Client
|
|
id int64
|
|
}
|
|
|
|
func New(cfg Config) *Source {
|
|
if cfg.PopularMangaPath == "" {
|
|
cfg.PopularMangaPath = "/manga/todos/"
|
|
}
|
|
if cfg.Lang == "" {
|
|
cfg.Lang = "pt-BR"
|
|
}
|
|
c := flare.NewClient(flare.WithRateLimit(2, 1))
|
|
return &Source{
|
|
cfg: cfg,
|
|
client: c,
|
|
id: source.GenerateSourceID(cfg.Name, cfg.Lang),
|
|
}
|
|
}
|
|
|
|
func (s *Source) ID() int64 { return s.id }
|
|
func (s *Source) Name() string { return s.cfg.Name }
|
|
func (s *Source) Lang() string { return s.cfg.Lang }
|
|
|
|
func (s *Source) SupportsLatest() bool { return true }
|
|
|
|
type LoadMoreReleasesDTO struct {
|
|
HTML string `json:"html"`
|
|
HasNext bool `json:"hasNext"`
|
|
NextPage int `json:"nextPage"`
|
|
}
|
|
|
|
type SearchResult struct {
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Image string `json:"image"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type SearchDTO struct {
|
|
Results []SearchResult `json:"results"`
|
|
}
|
|
|
|
func (s *Source) parseStatus(status string) int {
|
|
switch strings.TrimSpace(strings.ToLower(status)) {
|
|
case "em andamento":
|
|
return 1
|
|
case "concluído":
|
|
return 2
|
|
case "hiato":
|
|
return 5
|
|
case "cancelado":
|
|
return 6
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
|
doc, err := s.fetchDoc(fmt.Sprintf("%s%s?page=%d", s.cfg.BaseURL, cfg.PopularMangaPath, page))
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
|
|
mangas := s.mangaListFromDoc(doc)
|
|
hasNext := doc.Find(".page-link[aria-label=Próxima]:not(disabled)").Length() > 0
|
|
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
|
|
}
|
|
|
|
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
|
if page > 1 && s.cfg.LatestUpdatesLoadMorePath != "" {
|
|
url := fmt.Sprintf("%s%s?page=%d", s.cfg.BaseURL, s.cfg.LatestUpdatesLoadMorePath, page)
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var dto LoadMoreReleasesDTO
|
|
if err := json.NewDecoder(resp.Body).Decode(&dto); err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
|
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(dto.HTML))
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
|
|
mangas := s.mangaListFromDoc(doc)
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: dto.HasNext}, nil
|
|
}
|
|
|
|
doc, err := s.fetchDoc(s.cfg.BaseURL)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
|
|
mangas := s.mangaListFromDoc(doc)
|
|
|
|
hasNext := true
|
|
if s.cfg.LatestUpdatesLoadMorePath != "" {
|
|
hasNext = doc.Find("#load-more-btn").Length() > 0
|
|
} else {
|
|
hasNext = doc.Find(".page-link[aria-label=Próxima]:not(disabled)").Length() > 0
|
|
}
|
|
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
|
|
}
|
|
|
|
func (s *Source) mangaListFromDoc(doc *goquery.Document) []source.SManga {
|
|
mangas := make([]source.SManga, 0)
|
|
doc.Find(".comics-grid a.comic-card-link, div.manga-card-simple").Each(func(_ int, sel *goquery.Selection) {
|
|
title := sel.Find("h3").Text()
|
|
img := sel.Find("img")
|
|
thumb := img.AttrOr("abs:src", "")
|
|
if thumb == "" {
|
|
href := sel.AttrOr("abs:href", "")
|
|
if a := sel.Find("a"); a.Length() > 0 {
|
|
href = a.AttrOr("abs:href", "")
|
|
}
|
|
mangas = append(mangas, source.SManga{
|
|
URL: href,
|
|
Title: strings.TrimSpace(title),
|
|
ThumbnailURL: thumb,
|
|
})
|
|
} else {
|
|
href := sel.AttrOr("abs:href", "")
|
|
if a := sel.Find("a"); a.Length() > 0 {
|
|
href = a.AttrOr("abs:href", "")
|
|
}
|
|
mangas = append(mangas, source.SManga{
|
|
URL: href,
|
|
Title: strings.TrimSpace(title),
|
|
ThumbnailURL: thumb,
|
|
})
|
|
}
|
|
})
|
|
return mangas
|
|
}
|
|
|
|
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
|
if query == "" {
|
|
return source.MangasPage{}, nil
|
|
}
|
|
|
|
u, _ := url.Parse(s.cfg.BaseURL + "/search/live-search/")
|
|
q := u.Query()
|
|
q.Set("q", query)
|
|
u.RawQuery = q.Encode()
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result SearchDTO
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
|
|
mangas := make([]source.SManga, len(result.Results))
|
|
for i, r := range result.Results {
|
|
mangas[i] = source.SManga{
|
|
URL: r.URL,
|
|
Title: r.Title,
|
|
ThumbnailURL: r.Image,
|
|
Description: r.Description,
|
|
}
|
|
}
|
|
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: false}, nil
|
|
}
|
|
|
|
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
|
doc, err := s.fetchDoc(manga.URL)
|
|
if err != nil {
|
|
return manga, err
|
|
}
|
|
|
|
title := doc.Find("h1").Text()
|
|
thumb := doc.Find(".sidebar-cover-image img").AttrOr("abs:src", "")
|
|
desc := doc.Find(".manga-description").Text()
|
|
genre := doc.Find("a.genre-tag").Map(func(_ int, sel *goquery.Selection) string {
|
|
return sel.Text()
|
|
})
|
|
statusText := doc.Find(".status-tag").Text()
|
|
status := s.parseStatus(statusText)
|
|
|
|
manga.Title = strings.TrimSpace(title)
|
|
manga.ThumbnailURL = thumb
|
|
manga.Description = strings.TrimSpace(desc)
|
|
manga.Genre = joinStrings(genre, ", ")
|
|
manga.Status = status
|
|
manga.Initialized = true
|
|
|
|
return manga, nil
|
|
}
|
|
|
|
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
|
chapters := make([]source.SChapter, 0)
|
|
page := 1
|
|
|
|
for {
|
|
mangaURL := manga.URL
|
|
if strings.Contains(mangaURL, "?") {
|
|
mangaURL += "&page=" + fmt.Sprintf("%d", page)
|
|
} else {
|
|
mangaURL += "?page=" + fmt.Sprintf("%d", page)
|
|
}
|
|
|
|
doc, err := s.fetchDoc(mangaURL)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
doc.Find(".chapter-item-list a.chapter-link").Each(func(_ int, sel *goquery.Selection) {
|
|
name := sel.Find(".chapter-number").Text()
|
|
dateText := sel.Find(".chapter-date").Text()
|
|
href := sel.AttrOr("abs:href", "")
|
|
|
|
var dateUpload int64
|
|
dateFormat := "02/01/2006"
|
|
if t, err := time.Parse(dateFormat, strings.TrimSpace(dateText)); err == nil {
|
|
dateUpload = t.UnixMilli()
|
|
}
|
|
|
|
chapters = append(chapters, source.SChapter{
|
|
URL: href,
|
|
Name: strings.TrimSpace(name),
|
|
DateUpload: dateUpload,
|
|
})
|
|
})
|
|
|
|
if doc.Find(".page-link[aria-label=Próxima]:not(disabled)").Length() == 0 {
|
|
break
|
|
}
|
|
page++
|
|
}
|
|
|
|
return chapters, nil
|
|
}
|
|
|
|
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
|
|
doc, err := s.fetchDoc(chapter.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pages := make([]source.Page, 0)
|
|
doc.Find(".chapter-image-canvas").Each(func(i int, sel *goquery.Selection) {
|
|
imgURL := sel.AttrOr("data-src-url", "")
|
|
pages = append(pages, source.Page{Index: i, ImageURL: imgURL})
|
|
})
|
|
|
|
return pages, nil
|
|
}
|
|
|
|
func (s *Source) GetImageURL(page source.Page) (string, error) {
|
|
return page.ImageURL, nil
|
|
}
|
|
|
|
func (s *Source) GetFilterList() []source.Filter {
|
|
return nil
|
|
}
|
|
|
|
func (s *Source) fetchDoc(rawURL string) (*goquery.Document, error) {
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, rawURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
html, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return goquery.NewDocumentFromReader(strings.NewReader(string(html)))
|
|
}
|
|
|
|
func joinStrings(ss []string, sep string) string {
|
|
if len(ss) == 0 {
|
|
return ""
|
|
}
|
|
result := ss[0]
|
|
for i := 1; i < len(ss); i++ {
|
|
result += sep + ss[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
var cfg = struct {
|
|
PopularMangaPath string
|
|
}{}
|
|
|
|
func init() {
|
|
cfg.PopularMangaPath = "/manga/todos/"
|
|
}
|
|
|
|
var _ source.CatalogueSource = (*Source)(nil) |