224 lines
5.7 KiB
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
|
|
}
|