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

403 lines
9.9 KiB
Go
Executable File

package zeistmanga
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"goyomi/internal/httpclient"
"goyomi/internal/source"
)
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()
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 Feed struct {
Entries []Entry `json:"entry"`
}
type Entry struct {
ID string `json:"id"`
Title string `json:"title"`
Link []Link `json:"link"`
Content Content `json:"content"`
Category []Category `json:"category"`
Updated struct {
T string `json:"$t"`
} `json:"updated"`
}
type Link struct {
Href string `json:"href"`
Rel string `json:"rel"`
}
type Content struct {
T string `json:"$t"`
}
type Category struct {
Term string `json:"term"`
}
func (s *Source) apiURL(feed string) string {
return s.cfg.BaseURL + "/feeds/posts/default/-/" + feed + "?alt=json"
}
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
doc, err := s.fetchDoc(s.cfg.BaseURL)
if err != nil {
return source.MangasPage{}, err
}
mangas := make([]source.SManga, 0)
doc.Find("div.PopularPosts div.grid > figure").Each(func(_ int, sel *goquery.Selection) {
thumb := sel.Find("img").AttrOr("abs:src", "")
title := sel.Find("figcaption > a").Text()
link := sel.Find("figcaption > a").AttrOr("href", "")
mangas = append(mangas, source.SManga{
URL: link,
Title: strings.TrimSpace(title),
ThumbnailURL: thumb,
})
})
return source.MangasPage{Mangas: mangas, HasNextPage: false}, nil
}
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
startIndex := 20 * (page - 1)
u, _ := url.Parse(s.apiURL("Series"))
q := u.Query()
q.Set("orderby", "published")
q.Set("max-results", "21")
q.Set("start-index", fmt.Sprintf("%d", startIndex+1))
u.RawQuery = q.Encode()
return s.fetchFeed(u.String())
}
func (s *Source) fetchFeed(rawURL string) (source.MangasPage, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, rawURL, nil)
if err != nil {
return source.MangasPage{}, err
}
resp, err := s.client.Do(req)
if err != nil {
return source.MangasPage{}, err
}
defer resp.Body.Close()
var feed struct {
Feed Feed `json:"feed"`
}
if err := json.NewDecoder(resp.Body).Decode(&feed); err != nil {
return source.MangasPage{}, err
}
mangas := make([]source.SManga, 0)
for _, e := range feed.Feed.Entries {
hasSeriesCategory := false
for _, c := range e.Category {
if c.Term == "Series" {
hasSeriesCategory = true
break
}
}
if !hasSeriesCategory {
continue
}
thumb := ""
for _, l := range e.Link {
if l.Rel == "alternate" {
mangas = append(mangas, source.SManga{
URL: l.Href,
Title: strings.TrimSpace(e.Title),
ThumbnailURL: thumb,
})
break
}
}
}
hasNext := len(mangas) > 20
if hasNext {
mangas = mangas[:20]
}
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil
}
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
startIndex := 20 * (page - 1)
var feedName string = "Series"
for _, f := range filters {
if sel, ok := f.(*source.SelectFilter); ok && sel.Selected > 0 && len(sel.Values) > sel.Selected {
feedName = sel.Values[sel.Selected]
}
}
u, _ := url.Parse(s.apiURL(feedName))
q := u.Query()
q.Set("max-results", "21")
q.Set("start-index", fmt.Sprintf("%d", startIndex+1))
if query != "" {
searchURL := u.String() + "&q=label:" + feedName + "+" + url.QueryEscape(query)
return s.fetchFeed(searchURL)
}
u.RawQuery = q.Encode()
return s.fetchFeed(u.String())
}
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
doc, err := s.fetchDoc(manga.URL)
if err != nil {
return manga, err
}
profileManga := doc.Find(".grid.gtc-235fr")
if profileManga.Length() == 0 {
return manga, nil
}
thumb := profileManga.Find("img").AttrOr("abs:src", "")
desc := profileManga.Find("#synopsis").Text()
var altName string
if alt := profileManga.Find("header > p"); alt.Length() > 0 {
altName = alt.Text()
if altName != "" {
desc = "Alternative name(s): " + altName + "\n\n" + desc
}
}
genres := make([]string, 0)
profileManga.Find("div.mt-15 > a[rel=tag]").Each(func(_ int, sel *goquery.Selection) {
genres = append(genres, sel.Text())
})
author := profileManga.Find("span#author").Text()
artist := profileManga.Find("span#artist").Text()
statusText := profileManga.Find("span[data-status]").Text()
status := s.parseStatus(statusText)
manga.ThumbnailURL = thumb
manga.Description = strings.TrimSpace(desc)
manga.Genre = joinStrings(genres, ", ")
manga.Author = strings.TrimSpace(author)
manga.Artist = strings.TrimSpace(artist)
manga.Status = status
manga.Initialized = true
return manga, nil
}
func (s *Source) parseStatus(text string) int {
lower := strings.ToLower(strings.TrimSpace(text))
switch {
case strings.Contains(lower, "ongoing") || strings.Contains(lower, "em andamento") || strings.Contains(lower, "en curso") || strings.Contains(lower, "en emisión") || strings.Contains(lower, "aktif") || strings.Contains(lower, "lançando") || strings.Contains(lower, "مستمر"):
return 1
case strings.Contains(lower, "completed") || strings.Contains(lower, "completo") || strings.Contains(lower, "finalizado") || strings.Contains(lower, "مكتمل"):
return 2
case strings.Contains(lower, "hiatus") || strings.Contains(lower, "pausado"):
return 5
case strings.Contains(lower, "cancelled") || strings.Contains(lower, "dropped") || strings.Contains(lower, "cancelado") || strings.Contains(lower, "abandonado"):
return 6
}
return 0
}
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
doc, err := s.fetchDoc(manga.URL)
if err != nil {
return nil, err
}
feedURL := s.getChapterFeedURL(doc)
if feedURL == "" {
return nil, fmt.Errorf("chapter feed URL not found")
}
u, _ := url.Parse(feedURL)
q := u.Query()
q.Set("max-results", "999999")
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var feed struct {
Feed Feed `json:"feed"`
}
if err := json.NewDecoder(resp.Body).Decode(&feed); err != nil {
return nil, err
}
chapters := make([]source.SChapter, 0)
for _, e := range feed.Feed.Entries {
hasChapterCategory := false
for _, c := range e.Category {
if c.Term == "Chapter" {
hasChapterCategory = true
break
}
}
if !hasChapterCategory {
continue
}
link := ""
for _, l := range e.Link {
if l.Rel == "alternate" {
link = l.Href
break
}
}
chapters = append(chapters, source.SChapter{
URL: link,
Name: strings.TrimSpace(e.Title),
})
}
return chapters, nil
}
func (s *Source) getChapterFeedURL(doc *goquery.Document) string {
sel := doc.Find("#clwd > script")
if sel.Length() == 0 {
sel = doc.Find("#latest > script")
}
if sel.Length() == 0 {
return ""
}
html, _ := sel.Html()
re := regexp.MustCompile(`clwd\.run\(["'](.*?)["']\)`)
matches := re.FindStringSubmatch(html)
if len(matches) > 1 {
feed := matches[1]
return s.apiURL(feed) + "&max-results=999999"
}
re = regexp.MustCompile(`label\s*=\s*'([^']+)'`)
matches = re.FindStringSubmatch(html)
if len(matches) > 1 {
feed := matches[1]
return s.apiURL(feed) + "&max-results=999999"
}
return ""
}
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
doc, err := s.fetchDoc(chapter.URL)
if err != nil {
return nil, err
}
pages := make([]source.Page, 0)
doc.Find("div.check-box div.separator").Each(func(i int, sel *goquery.Selection) {
imgURL := sel.Find("img[src]").AttrOr("abs:src", "")
if imgURL != "" {
pages = append(pages, source.Page{Index: i, ImageURL: imgURL})
}
})
return pages, nil
}
func (s *Source) GetImageURL(page source.Page) (string, error) {
return page.ImageURL, nil
}
func (s *Source) GetFilterList() []source.Filter {
statusValues := []string{"", "Ongoing", "Completed", "Dropped", "Upcoming", "Hiatus", "Cancelled"}
typeValues := []string{"", "Manga", "Manhua", "Manhwa", "Novel", "Web Novel (JP)", "Web Novel (KR)", "Web Novel (CN)", "Doujinshi"}
genreValues := []string{"Action", "Adventure", "Comedy", "Drama", "Ecchi", "Fantasy", "Harem", "Historical", "Horror", "Josei", "Martial Arts", "Mecha", "Mystery", "Psychological", "Romance", "School", "Seinen", "Shoujo", "Shounen", "Slice of Life", "Sports", "Supernatural", "Thriller"}
filters := []source.Filter{
&source.TextFilter{FilterName: "Filters are ignored if search is not empty"},
&source.SelectFilter{FilterName: "Status", Values: statusValues, Selected: 0},
&source.SelectFilter{FilterName: "Type", Values: typeValues, Selected: 0},
&source.SelectFilter{FilterName: "Genre", Values: genreValues, Selected: 0},
}
return filters
}
func (s *Source) fetchDoc(rawURL string) (*goquery.Document, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, rawURL, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
html, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return goquery.NewDocumentFromReader(strings.NewReader(string(html)))
}
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
}
var _ source.CatalogueSource = (*Source)(nil)