Files
goyomi/sources/base/libgroup/libgroup.go
T
Achmad b199bad30d refactor: separate httpclient packages for regular and FlareSolverr sources
- Add internal/httpclient/flare package for Cloudflare-protected sites
- Update 7 bases (madara, zmanga, mangaworld, mangathemesia, mangareader,
  libgroup, liliana) to use flare client
- Remove unused internal/config/source.go
2026-05-11 10:48:05 +00:00

238 lines
7.0 KiB
Go
Executable File

// Package libgroup implements the LibGroup manga base (RusManga, MangaLib, etc.).
// Bearer token auth; GET {apiDomain}/api/manga; FlareSolverr required for initial token.
package libgroup
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"goyomi/internal/httpclient/flare"
"goyomi/internal/source"
"goyomi/sources/base/util"
)
const defaultAPIDomain = "https://api.lib.social"
type Config struct {
Name string
BaseURL string
Lang string
SiteID int
APIDomain string // defaults to "https://api.lib.social"
BearerToken string // optional; set after WebView acquisition
}
type Source struct {
cfg Config
client *flare.Client
id int64
}
func New(cfg Config) *Source {
if cfg.APIDomain == "" {
cfg.APIDomain = defaultAPIDomain
}
opts := []flare.Option{flare.WithRateLimit(1, 1)}
c := flare.NewClient(opts...)
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.APIDomain, "/") }
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")
req.Header.Set("Referer", s.cfg.BaseURL+"/")
req.Header.Set("Site-Id", fmt.Sprintf("%d", s.cfg.SiteID))
if s.cfg.BearerToken != "" {
req.Header.Set("Authorization", s.cfg.BearerToken)
}
resp, err := s.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("libgroup: HTTP %d", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
return json.Unmarshal(body, out)
}
type mangaDTO struct {
SlugURL string `json:"slug_url"`
Name string `json:"name"`
EngName string `json:"eng_name"`
Cover struct {
Default string `json:"default"`
Thumbnail string `json:"thumbnail"`
} `json:"cover"`
Summary string `json:"summary"`
Authors []struct{ Name string `json:"name"` } `json:"authors"`
Status struct{ Label string `json:"label"` } `json:"status"`
Genres []struct{ Name string `json:"name"` } `json:"genres"`
}
type metaDTO struct {
HasNextPage bool `json:"has_next_page"`
}
type mangaListDTO struct {
Data []mangaDTO `json:"data"`
Meta metaDTO `json:"meta"`
}
type dataDTO[T any] struct {
Data T `json:"data"`
}
func (s *Source) toSManga(m mangaDTO, useEng bool) source.SManga {
title := m.Name
if useEng && m.EngName != "" {
title = m.EngName
}
thumb := m.Cover.Default
if thumb == "" {
thumb = m.Cover.Thumbnail
}
var authors []string
for _, a := range m.Authors {
authors = append(authors, a.Name)
}
var genres []string
for _, g := range m.Genres {
genres = append(genres, g.Name)
}
slug := m.SlugURL
if !strings.HasPrefix(slug, "/") {
slug = "/" + slug
}
return source.SManga{
URL: slug,
Title: title,
Author: strings.Join(authors, ", "),
Description: m.Summary,
Genre: strings.Join(genres, ", "),
Status: util.StatusFromString(m.Status.Label),
ThumbnailURL: thumb,
}
}
func (s *Source) isEng() bool { return s.cfg.Lang == "en" }
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
u := fmt.Sprintf("%s/api/manga?site_id[]=%d&page=%d", s.api(), s.cfg.SiteID, page)
var result mangaListDTO
if err := s.getJSON(context.Background(), 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, s.isEng())
}
return source.MangasPage{Mangas: mangas, HasNextPage: result.Meta.HasNextPage}, nil
}
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
u := fmt.Sprintf("%s/api/latest-updates?site_id[]=%d&page=%d", s.api(), s.cfg.SiteID, page)
var result mangaListDTO
if err := s.getJSON(context.Background(), 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, s.isEng())
}
return source.MangasPage{Mangas: mangas, HasNextPage: result.Meta.HasNextPage}, nil
}
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
u := fmt.Sprintf("%s/api/manga?site_id[]=%d&page=%d&q=%s", s.api(), s.cfg.SiteID, page, query)
var result mangaListDTO
if err := s.getJSON(context.Background(), 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, s.isEng())
}
return source.MangasPage{Mangas: mangas, HasNextPage: result.Meta.HasNextPage}, nil
}
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
u := fmt.Sprintf("%s/api/manga%s?fields[]=eng_name", s.api(), manga.URL)
var result dataDTO[mangaDTO]
if err := s.getJSON(context.Background(), u, &result); err != nil {
return manga, err
}
out := s.toSManga(result.Data, s.isEng())
out.URL = manga.URL
return out, nil
}
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
u := fmt.Sprintf("%s/api/manga%s/chapters", s.api(), manga.URL)
var result dataDTO[[]struct {
ID int `json:"id"`
Volume string `json:"volume"`
Number float64 `json:"number"`
Name string `json:"name"`
Date string `json:"created_at"`
BranchID int `json:"branch_id"`
}]
if err := s.getJSON(context.Background(), u, &result); err != nil {
return nil, err
}
slugURL := strings.TrimPrefix(manga.URL, "/")
chapters := make([]source.SChapter, len(result.Data))
for i, ch := range result.Data {
name := fmt.Sprintf("%.1f", ch.Number)
if ch.Name != "" {
name += " - " + ch.Name
}
chURL := fmt.Sprintf("/%s/%d?volume=%s&number=%.1f", slugURL, ch.ID, ch.Volume, ch.Number)
if ch.BranchID > 0 {
chURL += fmt.Sprintf("&branch_id=%d", ch.BranchID)
}
chapters[i] = source.SChapter{
URL: chURL,
Name: "Chapter " + name,
DateUpload: util.ParseAbsoluteDate(ch.Date, "2006-01-02T15:04:05.000000Z"),
}
}
return chapters, nil
}
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
u := fmt.Sprintf("%s/api/manga%s", s.api(), chapter.URL)
var result dataDTO[struct {
Pages []struct {
ID int `json:"id"`
URL string `json:"url"`
} `json:"pages"`
}]
if err := s.getJSON(context.Background(), u, &result); err != nil {
return nil, err
}
pages := make([]source.Page, len(result.Data.Pages))
for i, p := range result.Data.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 }