ca609ccae7
Ports bases from previous session: util (shared helpers), bakkin, fmreader, foolslide, gigaviewer, gmanga, grouple, guya, heancms, hentaihand, kemono, madara, madtheme, mangadventure, mangahub, mangathemesia, mangaworld, mmrcms, senkuro, wpcomics.
200 lines
5.8 KiB
Go
200 lines
5.8 KiB
Go
// Package foolslide implements the FoolSlide reader base.
|
|
// Popular: HTML GET {base}/directory/{n}/
|
|
// Chapters/Pages: JSON API GET {base}/api/reader/...
|
|
package foolslide
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
|
|
"goyomi/internal/httpclient"
|
|
"goyomi/internal/source"
|
|
"goyomi/sources/base/util"
|
|
)
|
|
|
|
type Config struct {
|
|
Name string
|
|
BaseURL string
|
|
Lang string
|
|
URLModifier string // appended to BaseURL before /directory/
|
|
}
|
|
|
|
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, "/") + s.cfg.URLModifier
|
|
}
|
|
|
|
func (s *Source) get(ctx context.Context, rawURL string) (*http.Response, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("foolslide: HTTP %d", resp.StatusCode)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (s *Source) getDoc(ctx context.Context, rawURL string) (*goquery.Document, error) {
|
|
resp, err := s.get(ctx, rawURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
return goquery.NewDocumentFromReader(resp.Body)
|
|
}
|
|
|
|
func (s *Source) getJSON(ctx context.Context, rawURL string, out any) error {
|
|
resp, err := s.get(ctx, rawURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(body, out)
|
|
}
|
|
|
|
func (s *Source) parseMangaList(doc *goquery.Document) source.MangasPage {
|
|
var mangas []source.SManga
|
|
doc.Find("div.group").Each(func(_ int, el *goquery.Selection) {
|
|
m := source.SManga{}
|
|
el.Find("a[title]").First().Each(func(_ int, a *goquery.Selection) {
|
|
if href, ok := a.Attr("href"); ok {
|
|
m.URL = href
|
|
}
|
|
m.Title = a.AttrOr("title", strings.TrimSpace(a.Text()))
|
|
})
|
|
el.Find("img").First().Each(func(_ int, img *goquery.Selection) {
|
|
m.ThumbnailURL = util.AbsURL(s.cfg.BaseURL, img.AttrOr("src", ""))
|
|
})
|
|
if m.URL != "" {
|
|
mangas = append(mangas, m)
|
|
}
|
|
})
|
|
hasNext := doc.Find("a.next, .next_page").Length() > 0
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}
|
|
}
|
|
|
|
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
|
doc, err := s.getDoc(context.Background(), fmt.Sprintf("%s/directory/%d/", s.base(), page))
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
return s.parseMangaList(doc), nil
|
|
}
|
|
|
|
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
|
doc, err := s.getDoc(context.Background(), fmt.Sprintf("%s/latest/%d/", s.base(), page))
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
return s.parseMangaList(doc), nil
|
|
}
|
|
|
|
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
|
doc, err := s.getDoc(context.Background(), fmt.Sprintf("%s/search/?q=%s&p=%d", s.base(), query, page))
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
return s.parseMangaList(doc), nil
|
|
}
|
|
|
|
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
|
doc, err := s.getDoc(context.Background(), manga.URL)
|
|
if err != nil {
|
|
return manga, err
|
|
}
|
|
result := source.SManga{URL: manga.URL}
|
|
doc.Find("h1.title").First().Each(func(_ int, el *goquery.Selection) { result.Title = strings.TrimSpace(el.Text()) })
|
|
doc.Find(".comic_cover img, img.cover").First().Each(func(_ int, img *goquery.Selection) {
|
|
result.ThumbnailURL = util.AbsURL(s.cfg.BaseURL, img.AttrOr("src", ""))
|
|
})
|
|
return result, nil
|
|
}
|
|
|
|
type foolSlideChapter struct {
|
|
Stub string `json:"stub"`
|
|
Chapter string `json:"chapter"`
|
|
Subchapter string `json:"subchapter"`
|
|
Name string `json:"name"`
|
|
Added string `json:"added"`
|
|
}
|
|
|
|
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
|
slug := util.SlugFromURL(strings.TrimRight(manga.URL, "/"))
|
|
apiURL := fmt.Sprintf("%s/api/reader/chapters?comic=%s", s.base(), slug)
|
|
var chapters []foolSlideChapter
|
|
if err := s.getJSON(context.Background(), apiURL, &chapters); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]source.SChapter, len(chapters))
|
|
for i, ch := range chapters {
|
|
chNum := ch.Chapter
|
|
if ch.Subchapter != "" && ch.Subchapter != "0" {
|
|
chNum += "." + ch.Subchapter
|
|
}
|
|
result[i] = source.SChapter{
|
|
URL: fmt.Sprintf("%s/reader/series/%s/%s/", s.base(), slug, chNum),
|
|
Name: ch.Name,
|
|
DateUpload: util.ParseAbsoluteDate(ch.Added, "2006-01-02 15:04:05"),
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
type foolSlidePage struct {
|
|
Filename string `json:"filename"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
|
|
// Extract chapter ID from URL or use the chapter URL directly for the API
|
|
slug := util.SlugFromURL(strings.TrimRight(chapter.URL, "/"))
|
|
apiURL := fmt.Sprintf("%s/api/reader/images?chapter=%s", s.base(), slug)
|
|
var pages []foolSlidePage
|
|
if err := s.getJSON(context.Background(), apiURL, &pages); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]source.Page, len(pages))
|
|
for i, p := range pages {
|
|
imgURL := p.URL
|
|
if imgURL == "" {
|
|
imgURL = p.Filename
|
|
}
|
|
result[i] = source.Page{Index: i, URL: chapter.URL, ImageURL: util.AbsURL(s.cfg.BaseURL, imgURL)}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Source) GetImageURL(page source.Page) (string, error) { return page.ImageURL, nil }
|
|
func (s *Source) GetFilterList() []source.Filter { return nil }
|