316ae2f9db
Add 8 all/ sources (7 Masonry, 1 Madara) and 38 en/ sources spanning Madara, MangaThemesia, MadTheme, Keyoapp, and Guya bases, plus 8 earlier all/ standalone sources from the previous session (ahottie, akuma, allporncomicsco, asmhentai, baobua, beauty3600000, buondua, comicfury, comicgrowl, comicklive, comicsvalley, comikey, commitstrip, coomer). Also annotates phase4-standalone.md with base-class tags for 43 additional unimplemented en/ sources identified in a full scan.
239 lines
7.1 KiB
Go
239 lines
7.1 KiB
Go
// Package asmhentai implements the AsmHentai source (GalleryAdults-style).
|
|
// Multi-language: en/english, ja/japanese, zh/chinese, all (multi).
|
|
// Page list may require a POST to /inc/api.php for galleries with many pages.
|
|
package asmhentai
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
|
|
"goyomi/internal/httpclient/flare"
|
|
"goyomi/internal/registry"
|
|
"goyomi/internal/source"
|
|
)
|
|
|
|
const siteURL = "https://asmhentai.com"
|
|
|
|
type Source struct {
|
|
name string
|
|
lang string
|
|
mangaLang string // e.g. "english", "japanese", "" for all
|
|
client *flare.Client
|
|
id int64
|
|
}
|
|
|
|
func newSource(name, lang, mangaLang string) *Source {
|
|
return &Source{
|
|
name: name,
|
|
lang: lang,
|
|
mangaLang: mangaLang,
|
|
client: flare.NewClient(flare.WithRateLimit(1, 2)),
|
|
id: source.GenerateSourceID(name, lang),
|
|
}
|
|
}
|
|
|
|
func (s *Source) ID() int64 { return s.id }
|
|
func (s *Source) Name() string { return s.name }
|
|
func (s *Source) Lang() string { return s.lang }
|
|
func (s *Source) SupportsLatest() bool { return s.mangaLang != "" }
|
|
|
|
func (s *Source) langPath() string {
|
|
if s.mangaLang != "" {
|
|
return "language/" + s.mangaLang + "/"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Source) get(ctx context.Context, rawURL string) (*goquery.Document, error) {
|
|
resp, err := s.client.Get(ctx, rawURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("asmhentai: HTTP %d", resp.StatusCode)
|
|
}
|
|
return goquery.NewDocumentFromReader(resp.Body)
|
|
}
|
|
|
|
func imgAttr(img *goquery.Selection) string {
|
|
for _, attr := range []string{"data-lazy-src", "data-src", "src"} {
|
|
if v := img.AttrOr(attr, ""); v != "" && !strings.HasPrefix(v, "data:") {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func thumbnailToFull(u string) string {
|
|
ext := u[strings.LastIndex(u, "."):]
|
|
return strings.Replace(u, "t"+ext, ext, 1)
|
|
}
|
|
|
|
func (s *Source) parsePage(doc *goquery.Document) source.MangasPage {
|
|
var mangas []source.SManga
|
|
doc.Find(".preview_item").Each(func(_ int, el *goquery.Selection) {
|
|
href := el.Find(".image a").First().AttrOr("href", "")
|
|
if href == "" {
|
|
return
|
|
}
|
|
m := source.SManga{URL: href}
|
|
m.ThumbnailURL = imgAttr(el.Find(".image img").First())
|
|
m.Title = strings.TrimSpace(el.Find(".caption").Text())
|
|
if m.Title == "" {
|
|
m.Title = strings.TrimSpace(el.Find("h2, h3").First().Text())
|
|
}
|
|
if m.URL != "" && m.Title != "" {
|
|
mangas = append(mangas, m)
|
|
}
|
|
})
|
|
hasNext := doc.Find(".next.page-numbers, a[aria-label=Next]").Length() > 0
|
|
return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}
|
|
}
|
|
|
|
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
|
u := fmt.Sprintf("%s/%spopular/?page=%d", siteURL, s.langPath(), page)
|
|
doc, err := s.get(context.Background(), u)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
return s.parsePage(doc), nil
|
|
}
|
|
|
|
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
|
u := fmt.Sprintf("%s/%s?page=%d", siteURL, s.langPath(), page)
|
|
doc, err := s.get(context.Background(), u)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
return s.parsePage(doc), nil
|
|
}
|
|
|
|
func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
|
u := fmt.Sprintf("%s/search/?q=%s&page=%d", siteURL, url.QueryEscape(query), page)
|
|
doc, err := s.get(context.Background(), u)
|
|
if err != nil {
|
|
return source.MangasPage{}, err
|
|
}
|
|
return s.parsePage(doc), nil
|
|
}
|
|
|
|
func extractTags(info *goquery.Selection, tag string) string {
|
|
var items []string
|
|
info.Find(".tags").Each(func(_ int, tags *goquery.Selection) {
|
|
if !strings.Contains(tags.Text(), tag+":") {
|
|
return
|
|
}
|
|
tags.Find(".tag_list a").Each(func(_ int, a *goquery.Selection) {
|
|
t := strings.TrimSpace(a.Find(".tag").Text())
|
|
if t == "" {
|
|
t = strings.TrimSpace(a.Text())
|
|
}
|
|
if t != "" {
|
|
items = append(items, t)
|
|
}
|
|
})
|
|
})
|
|
return strings.Join(items, ", ")
|
|
}
|
|
|
|
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
|
rawURL := manga.URL
|
|
if !strings.HasPrefix(rawURL, "http") {
|
|
rawURL = siteURL + rawURL
|
|
}
|
|
doc, err := s.get(context.Background(), rawURL)
|
|
if err != nil {
|
|
return manga, err
|
|
}
|
|
result := source.SManga{URL: manga.URL, Status: source.StatusCompleted}
|
|
info := doc.Find(".book_page")
|
|
result.Title = strings.TrimSpace(info.Find("h1").Text())
|
|
if result.Title == "" {
|
|
result.Title = manga.Title
|
|
}
|
|
result.ThumbnailURL = imgAttr(info.Find(".cover img").First())
|
|
result.Genre = extractTags(info, "Tags")
|
|
result.Author = extractTags(info, "Artists")
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
|
return []source.SChapter{{URL: manga.URL, Name: "Chapter"}}, nil
|
|
}
|
|
|
|
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
|
|
rawURL := chapter.URL
|
|
if !strings.HasPrefix(rawURL, "http") {
|
|
rawURL = siteURL + rawURL
|
|
}
|
|
doc, err := s.get(context.Background(), rawURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pages []source.Page
|
|
doc.Find(".preview_thumb img").Each(func(_ int, img *goquery.Selection) {
|
|
if u := imgAttr(img); u != "" {
|
|
pages = append(pages, source.Page{Index: len(pages), ImageURL: thumbnailToFull(u)})
|
|
}
|
|
})
|
|
|
|
// POST for remaining pages when gallery has more than the initially loaded count.
|
|
tPagesStr := doc.Find("input#t_pages").AttrOr("value", "")
|
|
tPages, _ := strconv.Atoi(tPagesStr)
|
|
if tPages > len(pages) && tPages > 0 {
|
|
loadID := doc.Find("input#load_id").AttrOr("value", "")
|
|
loadDir := doc.Find("input#load_dir").AttrOr("value", "")
|
|
csrfToken := doc.Find("meta[name=csrf-token]").AttrOr("content", "")
|
|
form := url.Values{
|
|
"id": {loadID},
|
|
"dir": {loadDir},
|
|
"visible_pages": {strconv.Itoa(len(pages))},
|
|
"t_pages": {tPagesStr},
|
|
"type": {"2"},
|
|
}
|
|
if csrfToken != "" {
|
|
form.Set("_token", csrfToken)
|
|
}
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
|
|
siteURL+"/inc/api.php", strings.NewReader(form.Encode()))
|
|
if err == nil {
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
|
req.Header.Set("Referer", rawURL)
|
|
resp, err := s.client.Do(req)
|
|
if err == nil {
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode == http.StatusOK {
|
|
if extraDoc, err := goquery.NewDocumentFromReader(resp.Body); err == nil {
|
|
extraDoc.Find(".preview_thumb img").Each(func(_ int, img *goquery.Selection) {
|
|
if u := imgAttr(img); u != "" {
|
|
pages = append(pages, source.Page{Index: len(pages), ImageURL: thumbnailToFull(u)})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 init() {
|
|
registry.Register(newSource("AsmHentai", "all", ""))
|
|
registry.Register(newSource("AsmHentai (English)", "en", "english"))
|
|
registry.Register(newSource("AsmHentai (Japanese)", "ja", "japanese"))
|
|
registry.Register(newSource("AsmHentai (Chinese)", "zh", "chinese"))
|
|
}
|