package vercomics import ( "context" "fmt" "io" "net/http" "strings" "github.com/PuerkitoBio/goquery" "goyomi/internal/httpclient" "goyomi/internal/source" ) type Config struct { Name string BaseURL string Lang string URLSuffix string GenreSuffix 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 false } func (s *Source) GetPopularManga(page int) (source.MangasPage, error) { url := s.cfg.BaseURL + "/" + s.cfg.URLSuffix + fmt.Sprintf("/page/%d", page) doc, err := s.fetchDoc(url) if err != nil { return source.MangasPage{}, err } mangas := s.mangaListFromDoc(doc) hasNext := doc.Find("div.wp-pagenavi > span.current + a").Length() > 0 return source.MangasPage{Mangas: mangas, HasNextPage: hasNext}, nil } func (s *Source) mangaListFromDoc(doc *goquery.Document) []source.SManga { mangas := make([]source.SManga, 0) doc.Find("header:has(h1) ~ * .entry").Each(func(_ int, sel *goquery.Selection) { link := sel.Find("a.popimg").First() if link.Length() == 0 { return } title := link.Find("img").AttrOr("alt", "") href := link.AttrOr("href", "") thumb := s.imgAttr(link.Find("img")) mangas = append(mangas, source.SManga{ URL: href, Title: strings.TrimSpace(title), ThumbnailURL: thumb, }) }) return mangas } func (s *Source) imgAttr(img *goquery.Selection) string { if img.Length() == 0 { return "" } if src, ok := img.Attr("data-src"); ok { if abs, ok := img.Attr("abs:data-src"); ok { return abs } return src } if src, ok := img.Attr("data-lazy-src"); ok { if abs, ok := img.Attr("abs:data-lazy-src"); ok { return abs } return src } if srcset, ok := img.Attr("srcset"); ok { parts := strings.Split(srcset, " ") for _, p := range parts { if strings.HasPrefix(p, "http") { return p } } } if src, ok := img.Attr("data-cfsrc"); ok { if abs, ok := img.Attr("abs:data-cfsrc"); ok { return abs } return src } return img.AttrOr("abs:src", "") } func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) { return source.MangasPage{}, fmt.Errorf("not supported") } func (s *Source) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) { if query != "" { url := s.cfg.BaseURL if s.cfg.URLSuffix != "" { url += "/" + s.cfg.URLSuffix } url += fmt.Sprintf("/page/%d?s=%s", page, query) doc, err := s.fetchDoc(url) if err != nil { return source.MangasPage{}, err } return source.MangasPage{Mangas: s.mangaListFromDoc(doc), HasNextPage: false}, nil } for _, f := range filters { if sel, ok := f.(*source.SelectFilter); ok && sel.Selected > 0 && len(sel.Values) > sel.Selected { uriPart := sel.Values[sel.Selected] if uriPart != "" { url := s.cfg.BaseURL + "/" + s.cfg.GenreSuffix + "/" + uriPart + fmt.Sprintf("/page/%d", page) doc, err := s.fetchDoc(url) if err != nil { return source.MangasPage{}, err } return source.MangasPage{Mangas: s.mangaListFromDoc(doc), HasNextPage: false}, nil } } } return s.GetPopularManga(page) } func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) { doc, err := s.fetchDoc(manga.URL) if err != nil { return manga, err } genreList := doc.Find("div.tax_box:has(div.title:contains(Etiquetas)) a[rel=tag]") genres := make([]string, 0) genreList.Each(func(_ int, sel *goquery.Selection) { text := sel.Text() if text != "" { first := strings.ToUpper(string(text[0])) genres = append(genres, first+text[1:]) } }) manga.Genre = strings.Join(genres, ", ") manga.Status = 2 manga.Initialized = true return manga, nil } func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) { return []source.SChapter{ { URL: manga.URL, Name: manga.Title, }, }, 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) doc.Find("div.wp-content p > img:not(noscript img), div.wp-content div#lector > img:not(noscript img), div.wp-content > figure img:not(noscript img), div.wp-content > img, div.wp-content > p img, div.post-imgs > img").Each(func(i int, sel *goquery.Selection) { imgURL := s.imgAttr(sel) 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 { genres := []string{"", "accion", "animacion", "artes-marciales", "aventures", "carreras", "ciencia-ficcion", "comedia", "demonios", "deportes", "drama", "ecchi", "escuela", "espacio", "fantasia", "gore", "historico", "horror", "juego", "magia", "mecha", "militar", "misterio", "musica", "ninja", "parodia", "policia", "psicologico", "romance", "samurai", "sci-fi", "seinen", "shoujo", "shoujo-ai", "shounen", "shounen-ai"} filters := []source.Filter{ &source.TextFilter{FilterName: "Los filtros serán ignorados si la búsqueda no está vacía."}, &source.SelectFilter{FilterName: "Filtrar por género", Values: genres, 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 } 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))) } var _ source.CatalogueSource = (*Source)(nil)