402 lines
9.3 KiB
Go
Executable File
402 lines
9.3 KiB
Go
Executable File
package spicytheme
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"goyomi/internal/httpclient"
|
|
"goyomi/internal/source"
|
|
)
|
|
|
|
type Config struct {
|
|
Name string
|
|
BaseURL string
|
|
APIBaseURL string
|
|
Lang string
|
|
}
|
|
|
|
type Source struct {
|
|
cfg Config
|
|
client *httpclient.Client
|
|
id int64
|
|
}
|
|
|
|
func New(cfg Config) *Source {
|
|
c := httpclient.NewClient()
|
|
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 }
|
|
|
|
type MangaDTO struct {
|
|
ID string `json:"id"`
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
Image string `json:"image"`
|
|
Description string `json:"description"`
|
|
Status string `json:"status"`
|
|
Genres []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"genders"`
|
|
}
|
|
|
|
type FilterResponseDTO struct {
|
|
Data []MangaDTO `json:"data"`
|
|
Pagination struct {
|
|
CurrentPage int `json:"currentPage"`
|
|
LastPage int `json:"lastPage"`
|
|
Total int `json:"total"`
|
|
} `json:"pagination"`
|
|
}
|
|
|
|
type SeriesResponseDTO struct {
|
|
Series struct {
|
|
ID string `json:"id"`
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
Image string `json:"image"`
|
|
Description string `json:"description"`
|
|
Status string `json:"status"`
|
|
Origin string `json:"origin"`
|
|
Genres []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
} `json:"genders"`
|
|
Chapters []ChapterDTO `json:"chapters"`
|
|
} `json:"series"`
|
|
}
|
|
|
|
type ChapterDTO struct {
|
|
ID string `json:"id"`
|
|
Number string `json:"number"`
|
|
Name string `json:"name"`
|
|
CreatedAt string `json:"createdAt"`
|
|
}
|
|
|
|
type PagesResponseDTO struct {
|
|
Pages struct {
|
|
RawImages json.RawMessage `json:"rawImages"`
|
|
} `json:"pages"`
|
|
}
|
|
|
|
func (s *Source) filterURL(page int, orderBy string) string {
|
|
u, _ := url.Parse(s.cfg.APIBaseURL + "/filtrar")
|
|
q := u.Query()
|
|
q.Set("page", fmt.Sprintf("%d", page))
|
|
q.Set("limit", "12")
|
|
q.Set("orderBy", orderBy)
|
|
q.Set("sort", "desc")
|
|
q.Set("gendersId", "")
|
|
q.Set("origin", "")
|
|
q.Set("state", "")
|
|
q.Set("loading", "true")
|
|
u.RawQuery = q.Encode()
|
|
return u.String()
|
|
}
|
|
|
|
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
|
return s.fetchList(page, "id_popular")
|
|
}
|
|
|
|
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
|
return s.fetchList(page, "id_latest")
|
|
}
|
|
|
|
func (s *Source) fetchList(page int, orderBy string) (source.MangasPage, error) {
|
|
url := s.filterURL(page, orderBy)
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
s.setHeaders(req)
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result FilterResponseDTO
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
|
|
mangas := make([]source.SManga, len(result.Data))
|
|
for i, d := range result.Data {
|
|
mangas[i] = s.mangaFromDTO(d)
|
|
}
|
|
|
|
hasNext := result.Pagination.CurrentPage < result.Pagination.LastPage
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
|
|
}
|
|
|
|
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
|
if query != "" {
|
|
if len(query) < 2 {
|
|
return source.MangasPage{}, fmt.Errorf("escribe al menos 2 caracteres para buscar")
|
|
}
|
|
|
|
u, _ := url.Parse(s.cfg.APIBaseURL + "/home/buscar")
|
|
q := u.Query()
|
|
q.Set("query", query)
|
|
u.RawQuery = q.Encode()
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
s.setHeaders(req)
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result []MangaDTO
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
|
|
mangas := make([]source.SManga, len(result))
|
|
for i, d := range result {
|
|
mangas[i] = s.mangaFromDTO(d)
|
|
}
|
|
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: false}, nil
|
|
}
|
|
|
|
orderBy := "id_latest"
|
|
for _, f := range filters {
|
|
if sel, ok := f.(*source.SelectFilter); ok && len(sel.Values) > sel.Selected {
|
|
orderBy = sel.Values[sel.Selected]
|
|
}
|
|
}
|
|
|
|
return s.fetchList(page, orderBy)
|
|
}
|
|
|
|
func (s *Source) mangaFromDTO(d MangaDTO) source.SManga {
|
|
genres := make([]string, len(d.Genres))
|
|
for i, g := range d.Genres {
|
|
genres[i] = g.Name
|
|
}
|
|
|
|
status := 0
|
|
switch d.Status {
|
|
case "ongoing":
|
|
status = 1
|
|
case "completed":
|
|
status = 2
|
|
case "hiatus":
|
|
status = 5
|
|
case "cancelled":
|
|
status = 6
|
|
}
|
|
|
|
return source.SManga{
|
|
URL: d.Slug,
|
|
Title: d.Name,
|
|
ThumbnailURL: d.Image,
|
|
Description: d.Description,
|
|
Genre: joinStrings(genres, ", "),
|
|
Status: status,
|
|
}
|
|
}
|
|
|
|
func joinStrings(ss []string, sep string) string {
|
|
if len(ss) == 0 {
|
|
return ""
|
|
}
|
|
result := ss[0]
|
|
for i := 1; i < len(ss); i++ {
|
|
result += sep + ss[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
|
url := s.cfg.APIBaseURL + "/serie/" + manga.URL
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return manga, err
|
|
}
|
|
s.setHeaders(req)
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return manga, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result SeriesResponseDTO
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return manga, err
|
|
}
|
|
|
|
m := s.mangaFromDTO(MangaDTO{
|
|
ID: result.Series.ID,
|
|
Slug: result.Series.Slug,
|
|
Name: result.Series.Name,
|
|
Image: result.Series.Image,
|
|
Description: result.Series.Description,
|
|
Status: result.Series.Status,
|
|
Genres: result.Series.Genres,
|
|
})
|
|
m.Initialized = true
|
|
m.URL = manga.URL
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
|
url := s.cfg.APIBaseURL + "/serie/" + manga.URL
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.setHeaders(req)
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result SeriesResponseDTO
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chapters := make([]source.SChapter, len(result.Series.Chapters))
|
|
dateLayout := "2006-01-02T15:04:05.000Z"
|
|
|
|
for i, ch := range result.Series.Chapters {
|
|
dateUpload := int64(0)
|
|
if t, err := time.Parse(dateLayout, ch.CreatedAt); err == nil {
|
|
dateUpload = t.UnixMilli()
|
|
}
|
|
|
|
chapters[i] = source.SChapter{
|
|
URL: result.Series.Slug + "/" + ch.Number,
|
|
Name: "Chapter " + ch.Number + " - " + ch.Name,
|
|
DateUpload: dateUpload,
|
|
}
|
|
}
|
|
|
|
return chapters, nil
|
|
}
|
|
|
|
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
|
|
url := s.cfg.APIBaseURL + "/serie/" + chapter.URL + "/"
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.setHeaders(req)
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result PagesResponseDTO
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var images []string
|
|
if err := json.Unmarshal(result.Pages.RawImages, &images); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pages := make([]source.Page, len(images))
|
|
for i, img := range images {
|
|
pages[i] = source.Page{Index: i, ImageURL: img}
|
|
}
|
|
|
|
return pages, nil
|
|
}
|
|
|
|
func (s *Source) GetImageURL(page source.Page) (string, error) {
|
|
return page.ImageURL, nil
|
|
}
|
|
|
|
func (s *Source) GetFilterList() []source.Filter {
|
|
filters := make([]source.Filter, 0)
|
|
|
|
filters = append(filters, &source.TextFilter{FilterName: "Los filtros no se aplican a la búsqueda por texto"})
|
|
filters = append(filters, &source.SelectFilter{
|
|
FilterName: "Ordenar por",
|
|
Values: []string{"id_latest", "id_popular", "views", "release"},
|
|
Selected: 0,
|
|
})
|
|
filters = append(filters, &source.SelectFilter{
|
|
FilterName: "Origen",
|
|
Values: []string{"", "jp", "kr", "cn", "other"},
|
|
Selected: 0,
|
|
})
|
|
filters = append(filters, &source.SelectFilter{
|
|
FilterName: "Género",
|
|
Values: []string{"", "1", "2", "3", "4", "5", "6", "7", "8"},
|
|
Selected: 0,
|
|
})
|
|
filters = append(filters, &source.SelectFilter{
|
|
FilterName: "Estado",
|
|
Values: []string{"", "ongoing", "completed", "hiatus", "cancelled"},
|
|
Selected: 0,
|
|
})
|
|
|
|
return filters
|
|
}
|
|
|
|
func (s *Source) setHeaders(req *http.Request) {
|
|
req.Header.Set("Referer", s.cfg.BaseURL+"/")
|
|
req.Header.Set("Origin", s.cfg.BaseURL)
|
|
req.Header.Set("Accept", "application/json")
|
|
}
|
|
|
|
func (s *Source) fetchJSON(url string, out any) error {
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.setHeaders(req)
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return json.Unmarshal(body, out)
|
|
}
|
|
|
|
var _ source.CatalogueSource = (*Source)(nil) |