nu-api/api.go

224 lines
5.7 KiB
Go

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:first-child")
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(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), "<br/>")
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(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(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 := strings.TrimSpace(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
}