// 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")) }