Files
goyomi/sources/base/foolslide/foolslide.go
T
2026-05-11 06:48:23 +00:00

200 lines
5.8 KiB
Go
Executable File

// 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 }