Files
goyomi/sources/base/peachscan/peachscan.go
T

283 lines
8.4 KiB
Go

// Package peachscan implements the PeachScan manga base.
// Brazilian scan site; pages extracted from inline JS const urls = [...] array.
package peachscan
import (
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"goyomi/internal/httpclient"
"goyomi/internal/source"
"goyomi/sources/base/util"
)
var urlsRe = regexp.MustCompile(`(?s)const\s+urls\s*=\s*\[(.*?)\]\s*;`)
type Config struct {
Name string
BaseURL string
Lang string
}
type Source struct {
cfg Config
client *httpclient.Client
id int64
}
func New(cfg Config) *Source {
c := httpclient.NewClient(httpclient.WithRateLimit(1, 2))
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 }
func (s *Source) base() string { return strings.TrimRight(s.cfg.BaseURL, "/") }
func (s *Source) get(ctx context.Context, rawURL string) (*goquery.Document, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Referer", s.cfg.BaseURL+"/")
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("peachscan: HTTP %d", resp.StatusCode)
}
return goquery.NewDocumentFromReader(resp.Body)
}
func (s *Source) getRaw(ctx context.Context, rawURL string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Referer", s.cfg.BaseURL+"/")
resp, err := s.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
if page > 1 {
return source.MangasPage{}, nil
}
doc, err := s.get(context.Background(), s.base()+"/todas-as-obras/")
if err != nil {
return source.MangasPage{}, err
}
var mangas []source.SManga
doc.Find(".comics__all__box").Each(func(_ int, el *goquery.Selection) {
a := el.Find("a").First()
if a.Length() == 0 {
return
}
m := source.SManga{
URL: a.AttrOr("href", ""),
Title: strings.TrimSpace(el.Find(".comics__all__title, h3, h2").First().Text()),
}
if m.Title == "" {
m.Title = strings.TrimSpace(a.AttrOr("title", ""))
}
if img := el.Find("img").First(); img.Length() > 0 {
m.ThumbnailURL = util.AbsURL(s.cfg.BaseURL, img.AttrOr("src", img.AttrOr("data-src", "")))
}
if m.URL != "" && m.Title != "" {
mangas = append(mangas, m)
}
})
return source.MangasPage{Mangas: mangas, HasNextPage: false}, nil
}
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
var u string
if page == 1 {
u = s.base()
} else {
u = fmt.Sprintf("%s/page/%d/", s.base(), page)
}
doc, err := s.get(context.Background(), u)
if err != nil {
return source.MangasPage{}, err
}
var mangas []source.SManga
doc.Find("div.comic:not(:has(a.box-image > p))").Each(func(_ int, el *goquery.Selection) {
// Exclude novels: skip if any p under .box-image contains "Novel"
if el.Find("a.box-image > p").FilterFunction(func(_ int, p *goquery.Selection) bool {
return strings.Contains(p.Text(), "Novel")
}).Length() > 0 {
return
}
a := el.Find("a").First()
if a.Length() == 0 {
return
}
m := source.SManga{
URL: a.AttrOr("href", ""),
Title: strings.TrimSpace(el.Find(".comic-title, h3, h2").First().Text()),
}
if m.Title == "" {
m.Title = strings.TrimSpace(a.AttrOr("title", ""))
}
if img := el.Find("img").First(); img.Length() > 0 {
m.ThumbnailURL = util.AbsURL(s.cfg.BaseURL, img.AttrOr("src", img.AttrOr("data-src", "")))
}
if m.URL != "" && m.Title != "" {
mangas = append(mangas, m)
}
})
hasNext := doc.Find(".pagination .next, a[rel=next]").Length() > 0
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
}
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
u := fmt.Sprintf("%s/?s=%s&paged=%d", s.base(), query, page)
doc, err := s.get(context.Background(), u)
if err != nil {
return source.MangasPage{}, err
}
var mangas []source.SManga
doc.Find(".comics__all__box, div.comic").Each(func(_ int, el *goquery.Selection) {
a := el.Find("a").First()
if a.Length() == 0 {
return
}
m := source.SManga{
URL: a.AttrOr("href", ""),
Title: strings.TrimSpace(el.Find(".comics__all__title, .comic-title, h3, h2").First().Text()),
}
if m.Title == "" {
m.Title = strings.TrimSpace(a.AttrOr("title", ""))
}
if img := el.Find("img").First(); img.Length() > 0 {
m.ThumbnailURL = util.AbsURL(s.cfg.BaseURL, img.AttrOr("src", img.AttrOr("data-src", "")))
}
if m.URL != "" && m.Title != "" {
mangas = append(mangas, m)
}
})
hasNext := doc.Find(".pagination .next, a[rel=next]").Length() > 0
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
}
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
doc, err := s.get(context.Background(), util.AbsURL(s.cfg.BaseURL, manga.URL))
if err != nil {
return manga, err
}
result := source.SManga{URL: manga.URL}
result.Title = strings.TrimSpace(doc.Find("h1.manga-title, h1.comic-title, h1").First().Text())
if result.Title == "" {
result.Title = manga.Title
}
if img := doc.Find("div.manga-cover img, div.comic-cover img, img.cover").First(); img.Length() > 0 {
result.ThumbnailURL = util.AbsURL(s.cfg.BaseURL, img.AttrOr("src", ""))
}
result.Description = strings.TrimSpace(doc.Find(".manga-description, .comic-description, .sinopse").First().Text())
doc.Find(".manga-info p, .comic-info p, .meta p").Each(func(_ int, el *goquery.Selection) {
text := strings.TrimSpace(el.Text())
lower := strings.ToLower(text)
val := func() string {
parts := strings.SplitN(text, ":", 2)
if len(parts) < 2 {
return ""
}
return strings.TrimSpace(parts[1])
}
if strings.HasPrefix(lower, "autor") || strings.HasPrefix(lower, "author") {
result.Author = val()
} else if strings.HasPrefix(lower, "artista") || strings.HasPrefix(lower, "artist") {
result.Artist = val()
} else if strings.HasPrefix(lower, "gênero") || strings.HasPrefix(lower, "genre") {
result.Genre = val()
} else if strings.HasPrefix(lower, "status") {
result.Status = util.StatusFromString(val())
}
})
return result, nil
}
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
doc, err := s.get(context.Background(), util.AbsURL(s.cfg.BaseURL, manga.URL))
if err != nil {
return nil, err
}
var chapters []source.SChapter
doc.Find(".link__capitulos, ul.chapters li a, .chapter-list a").Each(func(_ int, el *goquery.Selection) {
href := el.AttrOr("href", "")
if href == "" {
return
}
name := strings.TrimSpace(el.Text())
if name == "" {
name = "Chapter"
}
chapters = append(chapters, source.SChapter{
URL: href,
Name: name,
})
})
return chapters, nil
}
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
html, err := s.getRaw(context.Background(), util.AbsURL(s.cfg.BaseURL, chapter.URL))
if err != nil {
return nil, err
}
m := urlsRe.FindStringSubmatch(html)
if m != nil {
inner := m[1]
var pages []source.Page
for i, part := range strings.Split(inner, ",") {
part = strings.TrimSpace(part)
part = strings.Trim(part, `"'`)
part = strings.TrimSpace(part)
if part == "" {
continue
}
imgURL := util.AbsURL(s.cfg.BaseURL, part) + "#page"
pages = append(pages, source.Page{Index: i, ImageURL: imgURL})
}
if len(pages) > 0 {
return pages, nil
}
}
// Fallback: #imageContainer img
doc, err := s.get(context.Background(), util.AbsURL(s.cfg.BaseURL, chapter.URL))
if err != nil {
return nil, err
}
var pages []source.Page
doc.Find("#imageContainer img").Each(func(i int, img *goquery.Selection) {
u := img.AttrOr("src", img.AttrOr("data-src", ""))
if u != "" {
pages = append(pages, source.Page{Index: i, ImageURL: util.AbsURL(s.cfg.BaseURL, u)})
}
})
return pages, nil
}
func (s *Source) GetImageURL(page source.Page) (string, error) { return page.ImageURL, nil }
func (s *Source) GetFilterList() []source.Filter { return nil }