198 lines
5.8 KiB
Go
Executable File
198 lines
5.8 KiB
Go
Executable File
// Package ezmanhwa implements the EZManhwa JSON REST base.
|
|
// GET {apiUrl}/series?page={n}&perPage=20&sort=popular
|
|
package ezmanhwa
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"goyomi/internal/httpclient"
|
|
"goyomi/internal/source"
|
|
"goyomi/sources/base/util"
|
|
)
|
|
|
|
type Config struct {
|
|
Name string
|
|
BaseURL string
|
|
APIURL string
|
|
Lang string
|
|
}
|
|
|
|
type Source struct {
|
|
cfg Config
|
|
client *httpclient.Client
|
|
id int64
|
|
}
|
|
|
|
func New(cfg Config) *Source {
|
|
if cfg.APIURL == "" {
|
|
cfg.APIURL = cfg.BaseURL
|
|
}
|
|
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) api() string { return strings.TrimRight(s.cfg.APIURL, "/") }
|
|
|
|
func (s *Source) getJSON(ctx context.Context, rawURL string, out any) error {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
|
req.Header.Set("Referer", s.cfg.BaseURL+"/")
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("ezmanhwa: HTTP %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return json.Unmarshal(body, out)
|
|
}
|
|
|
|
type seriesListDTO struct {
|
|
Data []seriesDTO `json:"data"`
|
|
TotalPages int `json:"totalPages"`
|
|
CurrentPage int `json:"current"`
|
|
}
|
|
|
|
type seriesDTO struct {
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Cover string `json:"cover"`
|
|
Synopsis string `json:"synopsis"`
|
|
Author string `json:"author"`
|
|
Status string `json:"status"`
|
|
Genres []string `json:"genres"`
|
|
}
|
|
|
|
type chapterListDTO struct {
|
|
Data []chapterDTO `json:"data"`
|
|
TotalPages int `json:"totalPages"`
|
|
CurrentPage int `json:"current"`
|
|
}
|
|
|
|
type chapterDTO struct {
|
|
Slug string `json:"slug"`
|
|
Number float64 `json:"number"`
|
|
Title string `json:"title"`
|
|
RequiresPurchase bool `json:"requiresPurchase"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type pageListDTO struct {
|
|
Images []struct{ URL string `json:"url"` } `json:"images"`
|
|
RequiresPurchase bool `json:"requiresPurchase"`
|
|
}
|
|
|
|
func (s *Source) toSManga(m seriesDTO) source.SManga {
|
|
return source.SManga{
|
|
URL: m.Slug,
|
|
Title: m.Title,
|
|
Author: m.Author,
|
|
Description: m.Synopsis,
|
|
Genre: strings.Join(m.Genres, ", "),
|
|
Status: util.StatusFromString(m.Status),
|
|
ThumbnailURL: m.Cover,
|
|
}
|
|
}
|
|
|
|
func (s *Source) fetchSeries(ctx context.Context, page int, sort, search string) (source.MangasPage, error) {
|
|
var u string
|
|
if search != "" {
|
|
u = fmt.Sprintf("%s/series/search?page=%d&perPage=20&search=%s", s.api(), page, search)
|
|
} else {
|
|
u = fmt.Sprintf("%s/series?page=%d&perPage=20&sort=%s", s.api(), page, sort)
|
|
}
|
|
var result seriesListDTO
|
|
if err := s.getJSON(ctx, u, &result); err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
mangas := make([]source.SManga, len(result.Data))
|
|
for i, m := range result.Data {
|
|
mangas[i] = s.toSManga(m)
|
|
}
|
|
hasNext := result.CurrentPage < result.TotalPages
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
|
|
}
|
|
|
|
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
|
return s.fetchSeries(context.Background(), page, "popular", "")
|
|
}
|
|
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
|
return s.fetchSeries(context.Background(), page, "latest", "")
|
|
}
|
|
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
|
return s.fetchSeries(context.Background(), page, "popular", query)
|
|
}
|
|
|
|
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
|
var result seriesDTO
|
|
if err := s.getJSON(context.Background(), fmt.Sprintf("%s/series/%s", s.api(), manga.URL), &result); err != nil {
|
|
return manga, err
|
|
}
|
|
out := s.toSManga(result)
|
|
out.URL = manga.URL
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
|
var result chapterListDTO
|
|
if err := s.getJSON(context.Background(),
|
|
fmt.Sprintf("%s/series/%s/chapters?page=1&perPage=100&sort=desc", s.api(), manga.URL),
|
|
&result); err != nil {
|
|
return nil, err
|
|
}
|
|
var chapters []source.SChapter
|
|
for _, ch := range result.Data {
|
|
if ch.RequiresPurchase {
|
|
continue
|
|
}
|
|
numStr := fmt.Sprintf("%.0f", ch.Number)
|
|
if ch.Number != float64(int(ch.Number)) {
|
|
numStr = fmt.Sprintf("%g", ch.Number)
|
|
}
|
|
name := "Chapter " + numStr
|
|
if ch.Title != "" && ch.Title != numStr {
|
|
name += " - " + ch.Title
|
|
}
|
|
chapters = append(chapters, source.SChapter{
|
|
// URL format: series/{seriesSlug}/chapters/{chSlug}
|
|
URL: fmt.Sprintf("series/%s/chapters/%s", manga.URL, ch.Slug),
|
|
Name: name,
|
|
DateUpload: util.ParseAbsoluteDate(ch.CreatedAt, "2006-01-02T15:04:05.000Z"),
|
|
})
|
|
}
|
|
return chapters, nil
|
|
}
|
|
|
|
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
|
|
var result pageListDTO
|
|
if err := s.getJSON(context.Background(), fmt.Sprintf("%s/%s", s.api(), chapter.URL), &result); err != nil {
|
|
return nil, err
|
|
}
|
|
if result.RequiresPurchase {
|
|
return nil, fmt.Errorf("ezmanhwa: chapter requires purchase")
|
|
}
|
|
pages := make([]source.Page, len(result.Images))
|
|
for i, img := range result.Images {
|
|
pages[i] = source.Page{Index: i, ImageURL: img.URL}
|
|
}
|
|
return pages, nil
|
|
}
|
|
|
|
func (s *Source) GetImageURL(page source.Page) (string, error) { return page.ImageURL, nil }
|
|
func (s *Source) GetFilterList() []source.Filter { return nil }
|