355 lines
8.8 KiB
Go
Executable File
355 lines
8.8 KiB
Go
Executable File
package uzaymanga
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/url"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/PuerkitoBio/goquery"
|
||
|
||
"goyomi/internal/httpclient"
|
||
"goyomi/internal/source"
|
||
)
|
||
|
||
type Config struct {
|
||
Name string
|
||
BaseURL string
|
||
Lang string
|
||
CdnURL string
|
||
}
|
||
|
||
type Source struct {
|
||
cfg Config
|
||
client *httpclient.Client
|
||
id int64
|
||
}
|
||
|
||
func New(cfg Config) *Source {
|
||
c := httpclient.NewClient(httpclient.WithRateLimit(3, 1))
|
||
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 SearchResult struct {
|
||
ID int `json:"id"`
|
||
Name string `json:"name"`
|
||
Image string `json:"image"`
|
||
}
|
||
|
||
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
||
doc, err := s.fetchDoc(fmt.Sprintf("%s/search?page=%d&search=&order=4", s.cfg.BaseURL, page))
|
||
if err != nil {
|
||
return source.MangasPage{}, err
|
||
}
|
||
|
||
mangas := make([]source.SManga, 0)
|
||
doc.Find("section[aria-label='series area'] .card").Each(func(_ int, sel *goquery.Selection) {
|
||
title := sel.Find("h2").Text()
|
||
img := sel.Find("img")
|
||
thumb := img.AttrOr("abs:src", "")
|
||
link := sel.Find("a").AttrOr("abs:href", "")
|
||
|
||
mangas = append(mangas, source.SManga{
|
||
URL: link,
|
||
Title: strings.TrimSpace(title),
|
||
ThumbnailURL: thumb,
|
||
})
|
||
})
|
||
|
||
return source.MangasPage{Mangas: mangas, HasNextPage: len(mangas) > 0}, nil
|
||
}
|
||
|
||
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
||
doc, err := s.fetchDoc(fmt.Sprintf("%s/?page=%d", s.cfg.BaseURL, page))
|
||
if err != nil {
|
||
return source.MangasPage{}, err
|
||
}
|
||
|
||
var grid *goquery.Selection
|
||
if header := doc.Find("div.header:has(h2:contains(En Son Yüklenen))"); header.Length() > 0 {
|
||
grid = header.Next()
|
||
} else if grid = doc.Find("div.grid.grid-cols-1"); grid.Length() == 0 {
|
||
grid = doc.Find("div.grid").First()
|
||
}
|
||
|
||
if grid == nil || grid.Length() == 0 {
|
||
return source.MangasPage{Mangas: []source.SManga{}, HasNextPage: false}, nil
|
||
}
|
||
|
||
mangas := make([]source.SManga, 0)
|
||
grid.Find("> div").Each(func(_ int, sel *goquery.Selection) {
|
||
link := sel.Find("h2").Parent()
|
||
if link.Length() == 0 {
|
||
link = sel.Find("a[href*='/manga/']")
|
||
}
|
||
if link.Length() == 0 {
|
||
return
|
||
}
|
||
|
||
title := sel.Find("h2").Text()
|
||
img := sel.Find(".card-image img")
|
||
if img.Length() == 0 {
|
||
img = sel.Find("img")
|
||
}
|
||
thumb := img.AttrOr("abs:src", "")
|
||
href := link.AttrOr("abs:href", "")
|
||
|
||
mangas = append(mangas, source.SManga{
|
||
URL: href,
|
||
Title: strings.TrimSpace(title),
|
||
ThumbnailURL: thumb,
|
||
})
|
||
})
|
||
|
||
return source.MangasPage{Mangas: mangas, HasNextPage: len(mangas) > 0}, nil
|
||
}
|
||
|
||
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
||
if strings.HasPrefix(query, "slug:") {
|
||
slug := strings.TrimPrefix(query, "slug:")
|
||
urlStr := fmt.Sprintf("%s/manga/%s", s.cfg.BaseURL, slug)
|
||
doc, err := s.fetchDoc(urlStr)
|
||
if err != nil {
|
||
return source.MangasPage{}, err
|
||
}
|
||
|
||
if s.isMangaPage(doc) {
|
||
manga, err := s.parseMangaDetails(doc, urlStr)
|
||
if err != nil {
|
||
return source.MangasPage{}, err
|
||
}
|
||
return source.MangasPage{Mangas: []source.SManga{manga}, HasNextPage: false}, nil
|
||
}
|
||
return source.MangasPage{Mangas: []source.SManga{}, HasNextPage: false}, nil
|
||
}
|
||
|
||
u, _ := url.Parse(s.cfg.BaseURL + "/api/series/search/navbar")
|
||
q := u.Query()
|
||
q.Set("search", query)
|
||
u.RawQuery = q.Encode()
|
||
|
||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), 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()
|
||
|
||
body, _ := io.ReadAll(resp.Body)
|
||
if strings.Contains(string(body), "[]") || strings.TrimSpace(string(body)) == "" {
|
||
return source.MangasPage{Mangas: []source.SManga{}, HasNextPage: false}, nil
|
||
}
|
||
|
||
var results []SearchResult
|
||
if err := json.Unmarshal(body, &results); err != nil {
|
||
return source.MangasPage{}, err
|
||
}
|
||
|
||
mangas := make([]source.SManga, len(results))
|
||
baseImage := s.cfg.CdnURL
|
||
if baseImage == "" {
|
||
baseImage = strings.TrimSuffix(s.cfg.BaseURL, "/")
|
||
}
|
||
|
||
for i, r := range results {
|
||
imgURL := r.Image
|
||
if !strings.HasPrefix(imgURL, "http") {
|
||
imgURL = baseImage + "/" + strings.TrimLeft(imgURL, "/")
|
||
}
|
||
|
||
slug := strings.ToLower(r.Name)
|
||
slug = strings.ReplaceAll(slug, "ı", "i")
|
||
slug = strings.ReplaceAll(slug, "ğ", "g")
|
||
slug = strings.ReplaceAll(slug, "ü", "u")
|
||
slug = strings.ReplaceAll(slug, "ş", "s")
|
||
slug = strings.ReplaceAll(slug, "ö", "o")
|
||
slug = strings.ReplaceAll(slug, "ç", "c")
|
||
re := regexp.MustCompile(`[^a-z0-9\s]`)
|
||
slug = re.ReplaceAllString(slug, "")
|
||
slug = strings.TrimSpace(slug)
|
||
re = regexp.MustCompile(`\s+`)
|
||
slug = re.ReplaceAllString(slug, "-")
|
||
|
||
mangas[i] = source.SManga{
|
||
URL: fmt.Sprintf("/manga/%d/%s", r.ID, slug),
|
||
Title: r.Name,
|
||
ThumbnailURL: imgURL,
|
||
}
|
||
}
|
||
|
||
return source.MangasPage{Mangas: mangas, HasNextPage: false}, nil
|
||
}
|
||
|
||
func (s *Source) isMangaPage(doc *goquery.Document) bool {
|
||
return doc.Find("div.grid h2 + p").Length() > 0
|
||
}
|
||
|
||
func (s *Source) parseMangaDetails(doc *goquery.Document, url string) (source.SManga, error) {
|
||
content := doc.Find("#content")
|
||
if content.Length() == 0 {
|
||
return source.SManga{}, fmt.Errorf("content not found")
|
||
}
|
||
|
||
title := content.Find("h1").Text()
|
||
img := content.Find("img")
|
||
thumb := img.AttrOr("abs:src", "")
|
||
|
||
genres := content.Find("a[href^='search?categories']").Map(func(_ int, sel *goquery.Selection) string {
|
||
return sel.Text()
|
||
})
|
||
|
||
desc := content.Find("div.grid h2 + p").Text()
|
||
|
||
pageStatus := content.Find("span:contains(Durum) + span").Text()
|
||
status := 0
|
||
switch {
|
||
case strings.Contains(pageStatus, "Devam Ediyor") || strings.Contains(pageStatus, "Birakildi"):
|
||
status = 1
|
||
case strings.Contains(pageStatus, "Tamamlandi"):
|
||
status = 2
|
||
case strings.Contains(pageStatus, "Ara Veridi"):
|
||
status = 5
|
||
}
|
||
|
||
return source.SManga{
|
||
URL: url,
|
||
Title: strings.TrimSpace(title),
|
||
ThumbnailURL: thumb,
|
||
Genre: joinStrings(genres, ", "),
|
||
Description: strings.TrimSpace(desc),
|
||
Status: status,
|
||
Initialized: true,
|
||
}, nil
|
||
}
|
||
|
||
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
||
doc, err := s.fetchDoc(manga.URL)
|
||
if err != nil {
|
||
return manga, err
|
||
}
|
||
|
||
return s.parseMangaDetails(doc, manga.URL)
|
||
}
|
||
|
||
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
||
doc, err := s.fetchDoc(manga.URL)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
chapters := make([]source.SChapter, 0)
|
||
dateFormat := "Jan 2 ,2006"
|
||
|
||
doc.Find("div.list-episode a").Each(func(_ int, sel *goquery.Selection) {
|
||
name := sel.Find("h3").Text()
|
||
dateText := sel.Find("span").Text()
|
||
href := sel.AttrOr("abs:href", "")
|
||
|
||
var dateUpload int64
|
||
if t, err := time.Parse(dateFormat, strings.TrimSpace(dateText)); err == nil {
|
||
dateUpload = t.UnixMilli()
|
||
}
|
||
|
||
chapters = append(chapters, source.SChapter{
|
||
URL: href,
|
||
Name: strings.TrimSpace(name),
|
||
DateUpload: dateUpload,
|
||
})
|
||
})
|
||
|
||
return chapters, nil
|
||
}
|
||
|
||
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)
|
||
pageRegex := regexp.MustCompile(`"path":"([^"]+)"`)
|
||
|
||
doc.Find("script").Each(func(_ int, sel *goquery.Selection) {
|
||
html, err := sel.Html()
|
||
if err != nil {
|
||
return
|
||
}
|
||
matches := pageRegex.FindAllStringSubmatch(html, -1)
|
||
for i, m := range matches {
|
||
if len(m) > 1 {
|
||
imgPath := m[1]
|
||
cdnURL := s.cfg.CdnURL
|
||
if cdnURL == "" {
|
||
cdnURL = s.cfg.BaseURL
|
||
}
|
||
pages = append(pages, source.Page{
|
||
Index: i,
|
||
URL: chapter.URL,
|
||
ImageURL: cdnURL + "/" + imgPath,
|
||
})
|
||
}
|
||
}
|
||
})
|
||
|
||
return pages, nil
|
||
}
|
||
|
||
func (s *Source) GetImageURL(page source.Page) (string, error) {
|
||
return page.ImageURL, nil
|
||
}
|
||
|
||
func (s *Source) GetFilterList() []source.Filter {
|
||
return nil
|
||
}
|
||
|
||
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
|
||
}
|
||
req.Header.Set("Referer", s.cfg.BaseURL+"/")
|
||
|
||
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) |