From 9425aaae0f48f7d8aabd57a1d7e4a21312155f80 Mon Sep 17 00:00:00 2001 From: Tordarus Date: Sun, 17 Aug 2025 11:45:23 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + api.go | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++++ curl.go | 70 +++++++++++++++++ cursor.go | 37 +++++++++ errors.go | 11 +++ genre.go | 195 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 17 ++++ go.sum | 41 ++++++++++ tag.go | 3 + types.go | 109 ++++++++++++++++++++++++++ 10 files changed, 707 insertions(+) create mode 100644 .gitignore create mode 100644 api.go create mode 100644 curl.go create mode 100644 cursor.go create mode 100644 errors.go create mode 100644 genre.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 tag.go create mode 100644 types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a48d947 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*_test.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..4d87421 --- /dev/null +++ b/api.go @@ -0,0 +1,223 @@ +package nuapi + +import ( + "context" + "fmt" + "os" + "path" + "strconv" + "strings" + "time" + + "git.tordarus.net/tordarus/adverr/v2" + "git.tordarus.net/tordarus/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:141.0) Gecko/20100101 Firefox/141.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), "
") + + 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(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 { + return 1, nil + } + + 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 +} + +func (api *Api) getChapterEntriesByPageIndex(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") + + fmt.Printf("%#v\n", adverr.Must(s.Find("td:nth-child(3)").Html())) + + 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/2006", 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 +} diff --git a/curl.go b/curl.go new file mode 100644 index 0000000..b9d1a47 --- /dev/null +++ b/curl.go @@ -0,0 +1,70 @@ +package nuapi + +import ( + "compress/gzip" + "context" + "fmt" + "os/exec" + + "github.com/PuerkitoBio/goquery" +) + +func (api *Api) GetWithCookie(ctx context.Context, url string) (*goquery.Document, error) { + if api.Cookie == "" { + return nil, ErrNoCookieSet.New() + } + + curl := exec.Command("curl", + "-s", url, + "-H", fmt.Sprintf("User-Agent: %s", api.UserAgent), + "-H", fmt.Sprintf("Cookie: %s", api.Cookie), + "-H", fmt.Sprintf("Accept-Encoding: %s", "gzip"), + ) + + stdout, err := curl.StdoutPipe() + if err != nil { + return nil, ErrCurlRequestFailed.Wrap(err, url) + } + + if err := curl.Start(); err != nil { + return nil, ErrCurlRequestFailed.Wrap(err, url) + } + defer curl.Wait() + + r, err := gzip.NewReader(stdout) + if err != nil { + return nil, ErrInvalidGzipData.New(err) + } + + return goquery.NewDocumentFromReader(r) +} + +func (api *Api) Get(url string) (*goquery.Document, error) { + if api.Cookie == "" { + return nil, ErrNoCookieSet.New() + } + + curl := exec.Command("curl", + "-s", url, + "-H", fmt.Sprintf("Cookie: %s", api.Cookie), + "-H", fmt.Sprintf("User-Agent: %s", api.UserAgent), + "-H", fmt.Sprintf("Accept-Encoding: %s", "gzip"), + ) + + stdout, err := curl.StdoutPipe() + if err != nil { + return nil, ErrCurlRequestFailed.Wrap(err, url) + } + + if err := curl.Start(); err != nil { + return nil, ErrCurlRequestFailed.Wrap(err, url) + } + defer curl.Wait() + + r, err := gzip.NewReader(stdout) + if err != nil { + return nil, ErrInvalidGzipData.Wrap(err) + } + + return goquery.NewDocumentFromReader(r) +} diff --git a/cursor.go b/cursor.go new file mode 100644 index 0000000..319a328 --- /dev/null +++ b/cursor.go @@ -0,0 +1,37 @@ +package nuapi + +import ( + "context" +) + +type Cursor[T any] struct { + ctx context.Context + cancelFunc context.CancelFunc + Chan <-chan *T +} + +func (c *Cursor[T]) First() *T { + defer c.cancelFunc() + return <-c.Chan +} + +func (c *Cursor[T]) Close() { + c.cancelFunc() +} + +func (c *Cursor[T]) Next() (*T, bool) { + if c.ctx.Err() != nil { + return nil, false + } + + value, ok := <-c.Chan + return value, ok +} + +func (c *Cursor[T]) Slice() []T { + s := make([]T, 0) + for value, ok := c.Next(); ok; value, ok = c.Next() { + s = append(s, *value) + } + return s +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..d37dada --- /dev/null +++ b/errors.go @@ -0,0 +1,11 @@ +package nuapi + +import "git.tordarus.net/tordarus/adverr/v2" + +var ( + ErrNoCookieSet = adverr.NewErrTmpl("ErrNoCookieSet", "no API cookie set") + ErrCurlRequestFailed = adverr.NewErrTmpl("ErrCurlRequestFailed", "curl request failed for url: '%s'") + ErrInvalidGzipData = adverr.NewErrTmpl("ErrInvalidGzipData", "gzip encoded data expected") + ErrApiRequestFailed = adverr.NewErrTmpl("ErrApiRequestFailed", "Data retrieval from NU failed") + ErrApiElementNotFound = adverr.NewErrTmpl("ErrApiElementNotFound", "element not found: '%s'") +) diff --git a/genre.go b/genre.go new file mode 100644 index 0000000..4b1eab8 --- /dev/null +++ b/genre.go @@ -0,0 +1,195 @@ +package nuapi + +type GenreID string + +const ( + GenreAction GenreID = "action" + GenreAdult GenreID = "adult" + GenreAdventure GenreID = "adventure" + GenreComedy GenreID = "comedy" + GenreDrama GenreID = "drama" + GenreEcchi GenreID = "ecchi" + GenreFantasy GenreID = "fantasy" + GenreGenderBender GenreID = "gender-bender" + GenreHarem GenreID = "harem" + GenreHistorical GenreID = "historical" + GenreHorror GenreID = "horror" + GenreJosei GenreID = "josei" + GenreMartialArts GenreID = "martial-arts" + GenreMature GenreID = "mature" + GenreMecha GenreID = "mecha" + GenreMystery GenreID = "mystery" + GenrePsychological GenreID = "psychological" + GenreRomance GenreID = "romance" + GenreSchoolLife GenreID = "school-life" + GenreSciFi GenreID = "sci-fi" + GenreSeinen GenreID = "seinen" + GenreShoujo GenreID = "shoujo" + GenreShoujoAi GenreID = "shoujo-ai" + GenreShounen GenreID = "shounen" + GenreShounenAi GenreID = "shounen-ai" + GenreSliceOfLife GenreID = "slice-of-life" + GenreSmut GenreID = "smut" + GenreSports GenreID = "sports" + GenreSupernatural GenreID = "supernatural" + GenreTragedy GenreID = "tragedy" + GenreWuxia GenreID = "wuxia" + GenreXianxia GenreID = "xianxia" + GenreXuanhuan GenreID = "xuanhuan" + GenreYaoi GenreID = "yaoi" + GenreYuri GenreID = "yuri" +) + +func (g GenreID) String() string { + switch g { + case GenreAction: + return "Action" + case GenreAdult: + return "Adult" + case GenreAdventure: + return "Adventure" + case GenreComedy: + return "Comedy" + case GenreDrama: + return "Drama" + case GenreEcchi: + return "Ecchi" + case GenreFantasy: + return "Fantasy" + case GenreGenderBender: + return "Gender Bender" + case GenreHarem: + return "Harem" + case GenreHistorical: + return "Historical" + case GenreHorror: + return "Horror" + case GenreJosei: + return "Josei" + case GenreMartialArts: + return "Martial Arts" + case GenreMature: + return "Mature" + case GenreMecha: + return "Mecha" + case GenreMystery: + return "Mystery" + case GenrePsychological: + return "Psychological" + case GenreRomance: + return "Romance" + case GenreSchoolLife: + return "School Life" + case GenreSciFi: + return "Sci-fi" + case GenreSeinen: + return "Seinen" + case GenreShoujo: + return "Shoujo" + case GenreShoujoAi: + return "Shoujo Ai" + case GenreShounen: + return "Shounen" + case GenreShounenAi: + return "Shounen Ai" + case GenreSliceOfLife: + return "Slice of Life" + case GenreSmut: + return "Smut" + case GenreSports: + return "Sports" + case GenreSupernatural: + return "Supernatural" + case GenreTragedy: + return "Tragedy" + case GenreWuxia: + return "Wuxia" + case GenreXianxia: + return "Xianxia" + case GenreXuanhuan: + return "Xuanhuan" + case GenreYaoi: + return "Yaoi" + case GenreYuri: + return "Yuri" + default: + panic("invalid genre: " + g) + } +} + +func ParseGenre(str string) GenreID { + switch str { + case "Action": + return GenreAction + case "Adult": + return GenreAdult + case "Adventure": + return GenreAdventure + case "Comedy": + return GenreComedy + case "Drama": + return GenreDrama + case "Ecchi": + return GenreEcchi + case "Fantasy": + return GenreFantasy + case "Gender Bender": + return GenreGenderBender + case "Harem": + return GenreHarem + case "Historical": + return GenreHistorical + case "Horror": + return GenreHorror + case "Josei": + return GenreJosei + case "Martial Arts": + return GenreMartialArts + case "Mature": + return GenreMature + case "Mecha": + return GenreMecha + case "Mystery": + return GenreMystery + case "Psychological": + return GenrePsychological + case "Romance": + return GenreRomance + case "School Life": + return GenreSchoolLife + case "Sci-fi": + return GenreSciFi + case "Seinen": + return GenreSeinen + case "Shoujo": + return GenreShoujo + case "Shoujo Ai": + return GenreShoujoAi + case "Shounen": + return GenreShounen + case "Shounen Ai": + return GenreShounenAi + case "Slice of Life": + return GenreSliceOfLife + case "Smut": + return GenreSmut + case "Sports": + return GenreSports + case "Supernatural": + return GenreSupernatural + case "Tragedy": + return GenreTragedy + case "Wuxia": + return GenreWuxia + case "Xianxia": + return GenreXianxia + case "Xuanhuan": + return GenreXuanhuan + case "Yaoi": + return GenreYaoi + case "Yuri": + return GenreYuri + default: + panic("invalid genre: " + str) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f5005b4 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module git.tordarus.net/tordarus/nu-api + +go 1.23 + +toolchain go1.24.6 + +require ( + git.tordarus.net/tordarus/adverr/v2 v2.0.2 + git.tordarus.net/tordarus/slices v0.0.15 + github.com/PuerkitoBio/goquery v1.8.1 +) + +require ( + git.tordarus.net/tordarus/gmath v0.0.7 // indirect + github.com/andybalholm/cascadia v1.3.1 // indirect + golang.org/x/net v0.7.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d6571dc --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +git.tordarus.net/tordarus/adverr/v2 v2.0.2 h1:7nvNjMMjtGPq0EY6duMiv+seJ7MacNvKSBmckHl6Erg= +git.tordarus.net/tordarus/adverr/v2 v2.0.2/go.mod h1:gCC46KsWosZJh7MVNDEU99hKQoxEWZgHITDHtmFwwiQ= +git.tordarus.net/tordarus/gmath v0.0.7 h1:tR48idt9AUL0r556ww3ZxByTKJEr6NWCTlhl2ihzYxQ= +git.tordarus.net/tordarus/gmath v0.0.7/go.mod h1:mO7aPlvNrGVE9UFXEuuACjZgMDsM63l3OcQy6xSQnoE= +git.tordarus.net/tordarus/slices v0.0.15 h1:qxKS+7BCZ/LzQbRCvdDFoLQXaJZ2C0GfVju/kPt1r3g= +git.tordarus.net/tordarus/slices v0.0.15/go.mod h1:eJBw6pSDNivPI0l4e0sGKUJzou/lbzHflXdAUzp1g4o= +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tag.go b/tag.go new file mode 100644 index 0000000..b7df9b7 --- /dev/null +++ b/tag.go @@ -0,0 +1,3 @@ +package nuapi + +type TagID string diff --git a/types.go b/types.go new file mode 100644 index 0000000..8edb2b0 --- /dev/null +++ b/types.go @@ -0,0 +1,109 @@ +package nuapi + +import ( + "encoding/json" + "time" +) + +type NovelID = string +type ChapterID = string +type ReadingListID = string +type GroupID = string + +type ReadingList struct { + ID ReadingListID `json:"id"` + Entries []ReadingListEntry `json:"entries"` +} + +func (l ReadingList) String() string { + data, _ := json.MarshalIndent(l, "", "\t") + return string(data) +} + +type ReadingListEntry struct { + Novel NovelEntry `json:"novel"` + CurrentChapter ReadingListChapterEntry `json:"current_chapter"` + LatestChapter ReadingListChapterEntry `json:"latest_chapter"` +} + +func (e ReadingListEntry) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +func (e ReadingListEntry) NewChapterAvailable() bool { + return e.CurrentChapter.ID != e.LatestChapter.ID +} + +type ReadingListChapterEntry struct { + NovelID NovelID `json:"novel_id"` + ID ChapterID `json:"id"` + Link string `json:"link"` +} + +func (e ReadingListChapterEntry) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type NovelEntry struct { + NovelID NovelID `json:"id"` + Name string `json:"name"` +} + +func (e NovelEntry) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type Novel struct { + ID NovelID `json:"id"` + Name string `json:"name"` + AssociatedNames []string `json:"associated_names"` + Description string `json:"description"` + Cover string `json:"cover"` + Type NovelType `json:"type"` + OriginalLanguage Language `json:"original_language"` + Genres []GenreID `json:"genres"` + Tags []TagID `json:"tags"` +} + +func (n Novel) String() string { + data, _ := json.MarshalIndent(n, "", "\t") + return string(data) +} + +type NovelType string + +const ( + TypeLightNovel NovelType = "Light Novel" + TypePublishedNovel NovelType = "Published Novel" + TypeWebNovel NovelType = "Web Novel" +) + +type Language string + +const ( + LanguageJapanese Language = "jp" + LanguageChinese Language = "cn" + LanguageMalaysian Language = "my" + LanguageFilipino Language = "fil" + LanguageKhmer Language = "khm" + LanguageThai Language = "th" + LanguageIndonesian Language = "id" + LanguageKorean Language = "kr" + LanguageVietnamese Language = "vn" +) + +type NovelChapterEntry struct { + ID ChapterID `json:"id"` + Link string `json:"link"` + NovelID NovelID `json:"novel_id"` + Date time.Time `json:"date"` + Group GroupID `json:"group"` +} + +func (nce NovelChapterEntry) String() string { + data, _ := json.MarshalIndent(nce, "", "\t") + return string(data) +}