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.
226 lines
6.6 KiB
Go
226 lines
6.6 KiB
Go
// Package gmanga implements the GManga Arabic manga base.
|
|
// GET {base}/api/releases?page={n}; POST search.
|
|
package gmanga
|
|
|
|
import (
|
|
"bytes"
|
|
"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
|
|
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, "/") }
|
|
|
|
type releaseDTO struct {
|
|
Manga struct {
|
|
ID int `json:"id"`
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
ArTitle string `json:"arabic_title"`
|
|
Thumbnail string `json:"thumbnail"`
|
|
} `json:"manga"`
|
|
}
|
|
|
|
type releasesDTO struct {
|
|
Data []releaseDTO `json:"data"`
|
|
Meta struct {
|
|
LastPage int `json:"last_page"`
|
|
CurrentPage int `json:"current_page"`
|
|
} `json:"meta"`
|
|
}
|
|
|
|
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")
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("gmanga: HTTP %d", resp.StatusCode)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return json.Unmarshal(body, out)
|
|
}
|
|
|
|
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
|
var result releasesDTO
|
|
if err := s.getJSON(context.Background(), fmt.Sprintf("%s/api/releases?page=%d", s.base(), page), &result); err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
seen := map[int]bool{}
|
|
var mangas []source.SManga
|
|
for _, r := range result.Data {
|
|
if seen[r.Manga.ID] {
|
|
continue
|
|
}
|
|
seen[r.Manga.ID] = true
|
|
title := r.Manga.Title
|
|
if title == "" {
|
|
title = r.Manga.ArTitle
|
|
}
|
|
mangas = append(mangas, source.SManga{
|
|
URL: fmt.Sprintf("/mangas/%s", r.Manga.Slug),
|
|
Title: title,
|
|
ThumbnailURL: util.AbsURL(s.cfg.BaseURL, r.Manga.Thumbnail),
|
|
})
|
|
}
|
|
hasNext := result.Meta.CurrentPage < result.Meta.LastPage
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
|
|
}
|
|
|
|
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
|
return s.GetPopularManga(page)
|
|
}
|
|
|
|
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
|
body, _ := json.Marshal(map[string]any{"q": query, "page": page})
|
|
req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost,
|
|
s.base()+"/api/mangas/search", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
var result struct {
|
|
Data []struct {
|
|
ID int `json:"id"`
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Thumbnail string `json:"thumbnail"`
|
|
} `json:"data"`
|
|
HasMore bool `json:"has_more"`
|
|
}
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
mangas := make([]source.SManga, len(result.Data))
|
|
for i, m := range result.Data {
|
|
mangas[i] = source.SManga{
|
|
URL: fmt.Sprintf("/mangas/%s", m.Slug),
|
|
Title: m.Title,
|
|
ThumbnailURL: util.AbsURL(s.cfg.BaseURL, m.Thumbnail),
|
|
}
|
|
}
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: result.HasMore}, nil
|
|
}
|
|
|
|
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
|
slug := util.SlugFromURL(manga.URL)
|
|
var result struct {
|
|
Manga struct {
|
|
Title string `json:"title"`
|
|
ArTitle string `json:"arabic_title"`
|
|
Thumbnail string `json:"thumbnail"`
|
|
Description string `json:"description"`
|
|
Authors []string `json:"authors"`
|
|
Status string `json:"status"`
|
|
Tags []struct{ Name string `json:"name"` } `json:"tags"`
|
|
} `json:"manga"`
|
|
}
|
|
if err := s.getJSON(context.Background(), fmt.Sprintf("%s/api/mangas/%s", s.base(), slug), &result); err != nil {
|
|
return manga, err
|
|
}
|
|
title := result.Manga.Title
|
|
if title == "" {
|
|
title = result.Manga.ArTitle
|
|
}
|
|
genres := make([]string, len(result.Manga.Tags))
|
|
for i, t := range result.Manga.Tags {
|
|
genres[i] = t.Name
|
|
}
|
|
return source.SManga{
|
|
URL: manga.URL,
|
|
Title: title,
|
|
Author: strings.Join(result.Manga.Authors, ", "),
|
|
Description: result.Manga.Description,
|
|
Genre: strings.Join(genres, ", "),
|
|
Status: util.StatusFromString(result.Manga.Status),
|
|
ThumbnailURL: util.AbsURL(s.cfg.BaseURL, result.Manga.Thumbnail),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
|
slug := util.SlugFromURL(manga.URL)
|
|
var result struct {
|
|
Chapters []struct {
|
|
ID int `json:"id"`
|
|
Number float32 `json:"number"`
|
|
Title string `json:"title"`
|
|
CreatedAt string `json:"created_at"`
|
|
} `json:"chapters"`
|
|
}
|
|
if err := s.getJSON(context.Background(), fmt.Sprintf("%s/api/mangas/%s/chapters", s.base(), slug), &result); err != nil {
|
|
return nil, err
|
|
}
|
|
chapters := make([]source.SChapter, len(result.Chapters))
|
|
for i, ch := range result.Chapters {
|
|
name := fmt.Sprintf("Chapter %.1f", ch.Number)
|
|
if ch.Title != "" {
|
|
name += " - " + ch.Title
|
|
}
|
|
chapters[i] = source.SChapter{
|
|
URL: fmt.Sprintf("/mangas/%s/%d", slug, ch.ID),
|
|
Name: name,
|
|
DateUpload: util.ParseAbsoluteDate(ch.CreatedAt, "2006-01-02T15:04:05Z"),
|
|
}
|
|
}
|
|
return chapters, nil
|
|
}
|
|
|
|
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
|
|
chID := util.SlugFromURL(chapter.URL)
|
|
var result struct {
|
|
Pages []struct {
|
|
URL string `json:"url"`
|
|
} `json:"chapter_data"`
|
|
}
|
|
if err := s.getJSON(context.Background(), fmt.Sprintf("%s/api/chapters/%s", s.base(), chID), &result); err != nil {
|
|
return nil, err
|
|
}
|
|
pages := make([]source.Page, len(result.Pages))
|
|
for i, p := range result.Pages {
|
|
pages[i] = source.Page{Index: i, ImageURL: util.AbsURL(s.cfg.BaseURL, p.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 }
|