package manga_providers import ( "errors" "fmt" "net/url" "regexp" hibikemanga "seanime/internal/extension/hibike/manga" "seanime/internal/util" "seanime/internal/util/comparison" "slices" "strconv" "strings" "time" "github.com/PuerkitoBio/goquery" "github.com/imroc/req/v3" "github.com/rs/zerolog" ) // WeebCentral implements the manga provider for WeebCentral // It uses goquery to scrape search results, chapter lists, and chapter pages. type WeebCentral struct { Url string UserAgent string Client *req.Client logger *zerolog.Logger } // NewWeebCentral initializes and returns a new WeebCentral provider instance. func NewWeebCentral(logger *zerolog.Logger) *WeebCentral { client := req.C(). SetUserAgent(util.GetRandomUserAgent()). SetTimeout(60 * time.Second). EnableInsecureSkipVerify(). ImpersonateChrome() return &WeebCentral{ Url: "https://weebcentral.com", UserAgent: util.GetRandomUserAgent(), Client: client, logger: logger, } } func (w *WeebCentral) GetSettings() hibikemanga.Settings { return hibikemanga.Settings{ SupportsMultiScanlator: false, SupportsMultiLanguage: false, } } func (w *WeebCentral) Search(opts hibikemanga.SearchOptions) ([]*hibikemanga.SearchResult, error) { w.logger.Debug().Str("query", opts.Query).Msg("weebcentral: Searching manga") searchUrl := fmt.Sprintf("%s/search/simple?location=main", w.Url) form := url.Values{} form.Set("text", opts.Query) resp, err := w.Client.R(). SetContentType("application/x-www-form-urlencoded"). SetHeader("HX-Request", "true"). SetHeader("HX-Trigger", "quick-search-input"). SetHeader("HX-Trigger-Name", "text"). SetHeader("HX-Target", "quick-search-result"). SetHeader("HX-Current-URL", w.Url+"/"). SetBody(form.Encode()). Post(searchUrl) if err != nil { w.logger.Error().Err(err).Msg("weebcentral: Failed to send search request") return nil, err } if !resp.IsSuccessState() { w.logger.Error().Str("status", resp.Status).Msg("weebcentral: Search request failed") return nil, fmt.Errorf("search request failed: status %s", resp.Status) } body := resp.String() doc, err := goquery.NewDocumentFromReader(strings.NewReader(body)) if err != nil { w.logger.Error().Err(err).Msg("weebcentral: Failed to parse search HTML") return nil, err } var searchResults []*hibikemanga.SearchResult doc.Find("#quick-search-result > div > a").Each(func(i int, s *goquery.Selection) { link, exists := s.Attr("href") if !exists { return } title := strings.TrimSpace(s.Find(".flex-1").Text()) var image string if s.Find("source").Length() > 0 { image, _ = s.Find("source").Attr("srcset") } else if s.Find("img").Length() > 0 { image, _ = s.Find("img").Attr("src") } // Extract manga id from link assuming the format contains '/series/{id}/' idPart := "" parts := strings.Split(link, "/series/") if len(parts) > 1 { subparts := strings.Split(parts[1], "/") idPart = subparts[0] } if idPart == "" { return } titleCopy := title titles := []*string{&titleCopy} compRes, ok := comparison.FindBestMatchWithSorensenDice(&opts.Query, titles) if !ok || compRes.Rating < 0.6 { return } searchResults = append(searchResults, &hibikemanga.SearchResult{ ID: idPart, Title: title, Synonyms: []string{}, Year: 0, Image: image, Provider: WeebCentralProvider, SearchRating: compRes.Rating, }) }) if len(searchResults) == 0 { w.logger.Error().Msg("weebcentral: No search results found") return nil, errors.New("no results found") } w.logger.Info().Int("count", len(searchResults)).Msg("weebcentral: Found search results") return searchResults, nil } func (w *WeebCentral) FindChapters(mangaId string) ([]*hibikemanga.ChapterDetails, error) { w.logger.Debug().Str("mangaId", mangaId).Msg("weebcentral: Fetching chapters") chapterUrl := fmt.Sprintf("%s/series/%s/full-chapter-list", w.Url, mangaId) resp, err := w.Client.R(). SetHeader("HX-Request", "true"). SetHeader("HX-Target", "chapter-list"). SetHeader("HX-Current-URL", fmt.Sprintf("%s/series/%s", w.Url, mangaId)). SetHeader("Referer", fmt.Sprintf("%s/series/%s", w.Url, mangaId)). Get(chapterUrl) if err != nil { w.logger.Error().Err(err).Msg("weebcentral: Failed to fetch chapter list") return nil, err } if !resp.IsSuccessState() { w.logger.Error().Str("status", resp.Status).Msg("weebcentral: Chapter list request failed") return nil, fmt.Errorf("chapter list request failed: status %s", resp.Status) } body := resp.String() doc, err := goquery.NewDocumentFromReader(strings.NewReader(body)) if err != nil { w.logger.Error().Err(err).Msg("weebcentral: Failed to parse chapter list HTML") return nil, err } var chapters []*hibikemanga.ChapterDetails volumeCounter := 1 lastChapterNumber := 9999.0 chapterRegex := regexp.MustCompile("(\\d+(?:\\.\\d+)?)") doc.Find("div.flex.items-center").Each(func(i int, s *goquery.Selection) { a := s.Find("a") chapterUrl, exists := a.Attr("href") if !exists { return } chapterTitle := strings.TrimSpace(a.Find("span.grow > span").First().Text()) var chapterNumber string var parsedChapterNumber float64 match := chapterRegex.FindStringSubmatch(chapterTitle) if len(match) > 1 { chapterNumber = w.cleanChapterNumber(match[1]) if num, err := strconv.ParseFloat(chapterNumber, 64); err == nil { parsedChapterNumber = num } } else { chapterNumber = "" } if parsedChapterNumber > lastChapterNumber { volumeCounter++ } if parsedChapterNumber != 0 { lastChapterNumber = parsedChapterNumber } // Extract chapter id from the URL assuming format contains '/chapters/{id}' chapterId := "" parts := strings.Split(chapterUrl, "/chapters/") if len(parts) > 1 { chapterId = parts[1] } chapters = append(chapters, &hibikemanga.ChapterDetails{ ID: chapterId, URL: chapterUrl, Title: chapterTitle, Chapter: chapterNumber, Index: uint(i), Provider: WeebCentralProvider, }) }) if len(chapters) == 0 { w.logger.Error().Msg("weebcentral: No chapters found") return nil, errors.New("no chapters found") } slices.Reverse(chapters) for i := range chapters { chapters[i].Index = uint(i) } w.logger.Info().Int("count", len(chapters)).Msg("weebcentral: Found chapters") return chapters, nil } func (w *WeebCentral) FindChapterPages(chapterId string) ([]*hibikemanga.ChapterPage, error) { url := fmt.Sprintf("%s/chapters/%s/images?is_prev=False&reading_style=long_strip", w.Url, chapterId) resp, err := w.Client.R(). SetHeader("HX-Request", "true"). SetHeader("HX-Current-URL", fmt.Sprintf("%s/chapters/%s", w.Url, chapterId)). SetHeader("Referer", fmt.Sprintf("%s/chapters/%s", w.Url, chapterId)). Get(url) if err != nil { w.logger.Error().Err(err).Msg("weebcentral: Failed to fetch chapter pages") return nil, err } if !resp.IsSuccessState() { w.logger.Error().Str("status", resp.Status).Msg("weebcentral: Chapter pages request failed") return nil, fmt.Errorf("chapter pages request failed: status %s", resp.Status) } body := resp.String() doc, err := goquery.NewDocumentFromReader(strings.NewReader(body)) if err != nil { w.logger.Error().Err(err).Msg("weebcentral: Failed to parse chapter pages HTML") return nil, err } var pages []*hibikemanga.ChapterPage totalImgs := doc.Find("img").Length() doc.Find("section.flex-1 img").Each(func(i int, s *goquery.Selection) { imageUrl, exists := s.Attr("src") if !exists || imageUrl == "" { return } pages = append(pages, &hibikemanga.ChapterPage{ URL: imageUrl, Index: i, Headers: map[string]string{"Referer": w.Url}, Provider: WeebCentralProvider, }) }) if len(pages) == 0 && totalImgs > 0 { doc.Find("img").Each(func(i int, s *goquery.Selection) { imageUrl, exists := s.Attr("src") if !exists || imageUrl == "" { return } pages = append(pages, &hibikemanga.ChapterPage{ URL: imageUrl, Index: i, Headers: map[string]string{"Referer": w.Url}, Provider: WeebCentralProvider, }) }) } if len(pages) == 0 { w.logger.Error().Msg("weebcentral: No pages found") return nil, errors.New("no pages found") } w.logger.Info().Int("count", len(pages)).Msg("weebcentral: Found chapter pages") return pages, nil } func (w *WeebCentral) cleanChapterNumber(chapterStr string) string { cleaned := strings.TrimLeft(chapterStr, "0") if cleaned == "" { return "0" } return cleaned }