package nuapi import ( "context" "fmt" "os" "path" "strconv" "strings" "time" "git.milar.in/milarin/adverr" "git.milar.in/milarin/slices" "github.com/PuerkitoBio/goquery" ) type Api struct { UserAgent string Cookie string } func NewApi(cookie string) *Api { return &Api{ Cookie: cookie, UserAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0", } } func (api *Api) GetReadingList(ctx context.Context, listIndex int) (*ReadingList, error) { doc, err := api.GetWithCookie(ctx, fmt.Sprintf("https://www.novelupdates.com/reading-list/?list=%d", listIndex)) if err != nil { return nil, ErrApiRequestFailed.Wrap(err) } listID := ReadingListID(doc.Find("#cssmenu ul li.active a").Text()) selection := doc.Find("table tbody tr") entries := make([]ReadingListEntry, 0, selection.Length()) selection.Each(func(i int, s *goquery.Selection) { link := s.Find("td:nth-child(2) a") href, ok := link.Attr("href") if !ok { return } novelID := NovelID(path.Base(href)) novel := NovelEntry{ NovelID: novelID, Name: link.Text(), } currentChapterLink := s.Find("td:nth-child(3) a") currentChapter := ReadingListChapterEntry{ NovelID: novelID, ID: ChapterID(currentChapterLink.Text()), Link: currentChapterLink.AttrOr("href", ""), } latestChapterLink := s.Find("td:nth-child(3) a") latestChapter := ReadingListChapterEntry{ NovelID: novelID, ID: ChapterID(latestChapterLink.Text()), Link: latestChapterLink.AttrOr("href", ""), } entries = append(entries, ReadingListEntry{ Novel: novel, CurrentChapter: currentChapter, LatestChapter: latestChapter, }) }) return &ReadingList{ ID: listID, Entries: entries, }, nil } func (api *Api) GetNovelByID(novelID NovelID) (*Novel, error) { doc, err := api.Get(context.Background(), fmt.Sprintf("https://www.novelupdates.com/series/%s/", novelID)) if err != nil { return nil, ErrApiRequestFailed.Wrap(err) } title := doc.Find(".seriestitlenu").Text() description := strings.TrimSpace(doc.Find("#editdescription").Text()) cover := doc.Find(".wpb_wrapper img").AttrOr("src", "") associatedNamesHtml, err := doc.Find("#editassociated").Html() if err != nil { return nil, ErrApiElementNotFound.Wrap(err, "#editassociated") } associatedNames := strings.Split(strings.TrimSpace(associatedNamesHtml), "
") novelType := NovelType(doc.Find("#showtype a.genre.type").Text()) originalLanguage := Language(strings.ToLower(strings.Trim(doc.Find("#showtype a.genre.type + span").Text(), "()"))) genreElems := doc.Find("#seriesgenre a.genre") genres := make([]GenreID, 0, genreElems.Length()) genreElems.Each(func(i int, s *goquery.Selection) { href, ok := s.Attr("href") if !ok { return } genres = append(genres, GenreID(path.Base(href))) }) tagElems := doc.Find("#showtags a.genre") tags := make([]TagID, 0, genreElems.Length()) tagElems.Each(func(i int, s *goquery.Selection) { href, ok := s.Attr("href") if !ok { return } tags = append(tags, TagID(path.Base(href))) }) return &Novel{ ID: novelID, Name: title, AssociatedNames: associatedNames, Description: description, Cover: cover, Type: novelType, OriginalLanguage: originalLanguage, Genres: genres, Tags: tags, }, nil } func (api *Api) GetChapterEntriesByNovelID(novelID NovelID) *Cursor[NovelChapterEntry] { ctx, cancelFunc := context.WithCancel(context.Background()) out := make(chan *NovelChapterEntry, 15) go func() { defer close(out) doc, err := api.Get(ctx, fmt.Sprintf("https://www.novelupdates.com/series/%s/?pg=%d", novelID, 1)) if err != nil { fmt.Fprintln(os.Stderr, err) return } pageCount, err := api.getPageCount(doc) if err != nil { fmt.Fprintln(os.Stderr, err) return } for pageIndex := pageCount; pageIndex > 0; pageIndex-- { if ctx.Err() != nil { break } entries, err := api.getChapterEntriesByPageIndex(ctx, novelID, pageIndex) if err != nil { fmt.Fprintln(os.Stderr, err) return } for _, entry := range entries { entry := entry out <- &entry } } }() return &Cursor[NovelChapterEntry]{ ctx: ctx, cancelFunc: cancelFunc, Chan: out, } } func (api *Api) getPageCount(doc *goquery.Document) (int, error) { pagination := doc.Find(".digg_pagination") if pagination.Length() > 0 { // more than 1 page pageCount, err := strconv.ParseInt(pagination.Find("a:nth-last-child(2)").Text(), 10, 64) if err != nil { adverr.Println(ErrApiElementNotFound.Wrap(err, ".digg_pagination a:nth-child(5)")) return 0, err } return int(pageCount), nil } else { return 1, nil } } func (api *Api) getChapterEntriesByPageIndex(ctx context.Context, novelID NovelID, pageIndex int) ([]NovelChapterEntry, error) { doc, err := api.Get(ctx, fmt.Sprintf("https://www.novelupdates.com/series/%s/?pg=%d", novelID, pageIndex)) if err != nil { return nil, ErrApiRequestFailed.Wrap(err) } entryElems := doc.Find("#myTable tbody tr") entries := make([]NovelChapterEntry, 0, entryElems.Length()) entryElems.Each(func(i int, s *goquery.Selection) { td3 := s.Find("td:nth-child(3) a.chp-release") chapterID := td3.Text() groupID := path.Base(s.Find("td:nth-child(2) a").AttrOr("href", "")) link := "https:" + td3.AttrOr("href", "") date, err := time.Parse("01/02/06", strings.TrimSpace(s.Find("td:first-child").Text())) if err != nil { adverr.Println(ErrApiElementNotFound.Wrap(err, "td:first-child")) return } entries = append(entries, NovelChapterEntry{ NovelID: novelID, ID: chapterID, Link: link, Date: date, Group: groupID, }) }) return slices.Reverse(entries), nil }