Files
goyomi/sources/base/raijinscans/raijinscans.go
T
achmad 9a42dd2ab1 refactor: use per-source HTTP client instead of global proxy
- 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)
2026-05-13 09:01:51 +07:00

267 lines
8.4 KiB
Go
Executable File

// Package raijinscans implements the RaijinScans manga base.
// French scan site; CF-protected; chapter page URLs are Base64-encoded in data-src attributes.
package raijinscans
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
"goyomi/internal/httpclient/flare"
"goyomi/internal/source"
"goyomi/sources/base/util"
)
type Config struct {
Name string
BaseURL string
Lang string
}
type Source struct {
cfg Config
client *flare.Client
id int64
}
func New(cfg Config) *Source {
c := flare.NewClient(flare.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("raijinscans: HTTP %d", resp.StatusCode)
}
return goquery.NewDocumentFromReader(resp.Body)
}
func (s *Source) parseMangaFromUnit(el *goquery.Selection) source.SManga {
a := el.Find("a").First()
m := source.SManga{
URL: a.AttrOr("href", ""),
Title: strings.TrimSpace(el.Find(".title, h2, h3, span.name").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", "")))
}
return m
}
func (s *Source) GetPopularManga(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("section#most-viewed div.swiper-slide.unit").Each(func(_ int, el *goquery.Selection) {
m := s.parseMangaFromUnit(el)
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) 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.original.card-lg div.unit, div.latest div.unit").Each(func(_ int, el *goquery.Selection) {
m := s.parseMangaFromUnit(el)
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&post_type=wp-manga&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("div.original.card-lg div.unit, div.search-result div.unit").Each(func(_ int, el *goquery.Selection) {
m := s.parseMangaFromUnit(el)
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.entry-title, h1.manga-title, h1").First().Text())
if result.Title == "" {
result.Title = manga.Title
}
if img := doc.Find("img.cover, div.cover img, div.manga-cover img").First(); img.Length() > 0 {
result.ThumbnailURL = util.AbsURL(s.cfg.BaseURL, img.AttrOr("src", img.AttrOr("data-src", "")))
}
result.Description = strings.TrimSpace(doc.Find(".summary__content, .manga-summary, .description").First().Text())
var genres []string
doc.Find("div.genre-list div.genre-link, a.genre-tag").Each(func(_ int, a *goquery.Selection) {
if t := strings.TrimSpace(a.Text()); t != "" {
genres = append(genres, t)
}
})
result.Genre = strings.Join(genres, ", ")
// Status from stat-item containing "État du titre" or "Status"
doc.Find("div.stat-item").Each(func(_ int, el *goquery.Selection) {
label := strings.ToLower(el.Find("span").First().Text())
if strings.Contains(label, "état") || strings.Contains(label, "status") {
val := strings.TrimSpace(el.Find("span.manga, span.value, span:last-child").Last().Text())
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("ul.scroll-sm li.item, ul.chapter-list li, li.wp-manga-chapter").Each(func(_ int, el *goquery.Selection) {
a := el.Find("a").First()
href := a.AttrOr("href", "")
if href == "" {
return
}
name := strings.TrimSpace(a.Text())
if name == "" {
name = "Chapter"
}
var ts int64
if span := el.Find("span.date, span.chapter-release-date, time").First(); span.Length() > 0 {
ts = parseFrenchDate(strings.TrimSpace(span.Text()))
}
chapters = append(chapters, source.SChapter{
URL: href,
Name: name,
DateUpload: ts,
})
})
return chapters, nil
}
func parseFrenchDate(s string) int64 {
lower := strings.ToLower(strings.TrimSpace(s))
// "aujourd'hui" = today, "hier" = yesterday
if strings.HasPrefix(lower, "aujourd") {
return util.ParseRelativeDate("0 days ago")
}
if lower == "hier" {
return util.ParseRelativeDate("1 day ago")
}
// "{n} jour(s)" / "{n} heure(s)" / "{n} semaine(s)"
lower = strings.ReplaceAll(lower, "jour(s)", "days")
lower = strings.ReplaceAll(lower, "jours", "days")
lower = strings.ReplaceAll(lower, "jour", "day")
lower = strings.ReplaceAll(lower, "heure(s)", "hours")
lower = strings.ReplaceAll(lower, "heures", "hours")
lower = strings.ReplaceAll(lower, "heure", "hour")
lower = strings.ReplaceAll(lower, "semaine(s)", "weeks")
lower = strings.ReplaceAll(lower, "semaines", "weeks")
lower = strings.ReplaceAll(lower, "semaine", "week")
lower = strings.ReplaceAll(lower, "mois", "months")
lower = strings.ReplaceAll(lower, "an(s)", "years")
lower = strings.ReplaceAll(lower, "ans", "years")
lower = lower + " ago"
if ts := util.ParseRelativeDate(lower); ts != 0 {
return ts
}
return util.ParseAbsoluteDate(s, "2 January 2006")
}
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
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("div.protected-image-data").Each(func(i int, el *goquery.Selection) {
encoded := strings.TrimSpace(el.AttrOr("data-src", ""))
if encoded == "" {
return
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
// Try RawStdEncoding (no padding)
decoded, err = base64.RawStdEncoding.DecodeString(encoded)
if err != nil {
return
}
}
imgURL := strings.TrimSpace(string(decoded))
if imgURL != "" {
pages = append(pages, source.Page{Index: i, ImageURL: imgURL})
}
})
// Fallback: regular img tags
if len(pages) == 0 {
doc.Find("div.reading-content img, div.reader-area 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 }