v0.8/Initial - Working Downloads and Novel Processing
This commit is contained in:
parent
ef1c533009
commit
a0b4b4361f
9 changed files with 1063 additions and 0 deletions
jnc
417
jnc/jnc.go
Normal file
417
jnc/jnc.go
Normal file
|
@ -0,0 +1,417 @@
|
|||
package jnc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const BaseUrl = "https://labs.j-novel.club/"
|
||||
const ApiV1Url = BaseUrl + "app/v1/"
|
||||
const ApiV2Url = BaseUrl + "app/v2/"
|
||||
const FeedUrl = BaseUrl + "feed/"
|
||||
|
||||
type ApiFormat int
|
||||
|
||||
const (
|
||||
JSON = iota
|
||||
TEXT
|
||||
PROTOBUF
|
||||
)
|
||||
|
||||
var ApiFormatParam = map[ApiFormat]string{
|
||||
JSON: "format=json",
|
||||
TEXT: "format=text",
|
||||
PROTOBUF: "",
|
||||
}
|
||||
|
||||
type BookStatus string
|
||||
type PublishingStatus string
|
||||
type SeriesFormat string
|
||||
type DownloadType string
|
||||
|
||||
type AuthBody struct {
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
Slim bool `json:"slim"`
|
||||
}
|
||||
|
||||
type JWT struct {
|
||||
Token string `json:"id"`
|
||||
TTL string `json:"ttl"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
type Api struct {
|
||||
_auth Auth
|
||||
_format ApiFormat
|
||||
_library Library
|
||||
_series map[string]SerieAugmented
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
_username string
|
||||
_password string
|
||||
_jwt JWT
|
||||
}
|
||||
|
||||
type Library struct {
|
||||
Books []Book `json:"books"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type Book struct {
|
||||
Id string `json:"id"`
|
||||
LegacyId string `json:"legacyId"`
|
||||
Volume Volume `json:"volume"`
|
||||
Serie Serie `json:"serie"`
|
||||
Purchased string `json:"purchased"`
|
||||
LastDownload string `json:"lastDownload"`
|
||||
LastUpdated string `json:"lastUpdated"`
|
||||
Downloads []Download `json:"downloads"`
|
||||
Status BookStatus `json:"status"`
|
||||
}
|
||||
|
||||
type Volume struct {
|
||||
Id string `json:"id"`
|
||||
LegacyId string `json:"legacyId"`
|
||||
Title string `json:"title"`
|
||||
ShortTitle string `json:"shortTitle"`
|
||||
OriginalTitle string `json:"originalTitle"`
|
||||
Slug string `json:"slug"`
|
||||
Number int `json:"number"`
|
||||
OriginalPublisher string `json:"originalPublisher"`
|
||||
Label string `json:"label"`
|
||||
Creators []string `json:"creators"` // TODO: check, might not be correct
|
||||
hidden bool
|
||||
ForumTopicId int `json:"forumTopicId"`
|
||||
Created string `json:"created"`
|
||||
Publishing string `json:"publishing"`
|
||||
Description string `json:"description"`
|
||||
ShortDescription string `json:"shortDescription"`
|
||||
Cover Cover `json:"cover"`
|
||||
Owned bool `json:"owned"`
|
||||
PremiumExtras string `json:"premiumExtras"`
|
||||
NoEbook bool `json:"noEbook"`
|
||||
TotalParts int `json:"totalParts"`
|
||||
OnSale bool `json:"onSale"`
|
||||
}
|
||||
|
||||
type VolumeAugmented struct {
|
||||
Info Volume
|
||||
Downloads []Download
|
||||
updated string
|
||||
downloaded string
|
||||
}
|
||||
|
||||
func (volume *VolumeAugmented) UpdateAvailable() bool {
|
||||
if volume.downloaded == "" && len(volume.Downloads) != 0 {
|
||||
return true // The file has never been downloaded but at least one is available
|
||||
}
|
||||
|
||||
downloadedTime, err := time.Parse(time.RFC3339, volume.downloaded)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
updatedTime, err := time.Parse(time.RFC3339, volume.updated)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if downloadedTime.Before(updatedTime) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type Cover struct {
|
||||
OriginalUrl string `json:"originalUrl"`
|
||||
CoverUrl string `json:"coverUrl"`
|
||||
ThumbnailUrl string `json:"thumbnail"`
|
||||
}
|
||||
|
||||
type Serie struct {
|
||||
Id string `json:"id"`
|
||||
LegacyId string `json:"legacyId"`
|
||||
Type SeriesFormat `json:"type"`
|
||||
Status PublishingStatus `json:"status"`
|
||||
Title string `json:"title"`
|
||||
ShortTitle string `json:"shortTitle"`
|
||||
OriginalTitle string `json:"originalTitle"`
|
||||
Slug string `json:"slug"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Created string `json:"created"`
|
||||
Description string `json:"description"`
|
||||
ShortDescription string `json:"shortDescription"`
|
||||
Tags []string `json:"tags"`
|
||||
Cover Cover `json:"cover"`
|
||||
Following bool `json:"following"`
|
||||
Catchup bool `json:"catchup"`
|
||||
Rentals bool `json:"rentals"`
|
||||
TopicId int `json:"topicId"`
|
||||
AgeRating int `json:"ageRating"`
|
||||
}
|
||||
|
||||
type SerieAugmented struct {
|
||||
Info Serie `json:"series"`
|
||||
Volumes []VolumeAugmented `json:"volumes"`
|
||||
}
|
||||
|
||||
type Download struct {
|
||||
Link string `json:"link"`
|
||||
Type DownloadType `json:"type"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Limit int `json:"limit"`
|
||||
Skip int `json:"skip"`
|
||||
LastPage bool `json:"lastPage"`
|
||||
}
|
||||
|
||||
func NewJNC() Api {
|
||||
jncApi := Api{
|
||||
_auth: Auth{
|
||||
_username: "",
|
||||
_password: "",
|
||||
_jwt: JWT{},
|
||||
},
|
||||
_library: Library{},
|
||||
_series: map[string]SerieAugmented{},
|
||||
}
|
||||
return jncApi
|
||||
}
|
||||
|
||||
func (jncApi *Api) ReturnFormat() string {
|
||||
return ApiFormatParam[jncApi._format]
|
||||
}
|
||||
|
||||
// Login attempts a login using the username and password set with SetUsername and SetPassword
|
||||
func (jncApi *Api) Login() error {
|
||||
fmt.Println("Logging in...")
|
||||
if jncApi._auth._username == "" {
|
||||
return errors.New("username not specified")
|
||||
}
|
||||
if jncApi._auth._password == "" {
|
||||
return errors.New("password not specified")
|
||||
}
|
||||
|
||||
authBody := AuthBody{
|
||||
Login: jncApi._auth._username,
|
||||
Password: jncApi._auth._password,
|
||||
Slim: true,
|
||||
}
|
||||
|
||||
jsonAuthBody, err := json.Marshal(authBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := http.Post(ApiV2Url+"auth/login?"+jncApi.ReturnFormat(), "application/json", bytes.NewBuffer(jsonAuthBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(body, &jncApi._auth._jwt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoginOTP starts an OTP based login using the username provided with SetUsername. NOTE: Currently not implemented
|
||||
func (jncApi *Api) LoginOTP() error {
|
||||
if jncApi._auth._username == "" {
|
||||
return errors.New("username not specified")
|
||||
}
|
||||
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (jncApi *Api) AuthParam() string {
|
||||
return "access_token=" + jncApi._auth._jwt.Token
|
||||
}
|
||||
|
||||
// Logout invalidates the auth token and resets the jnc instance to a blank slate. No information remains after calling.
|
||||
func (jncApi *Api) Logout() error {
|
||||
fmt.Println("Logging out...")
|
||||
res, err := http.Post(ApiV2Url+"auth/logout?"+jncApi.ReturnFormat()+"&"+jncApi.AuthParam(), "application/json", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 204 {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
*jncApi = Api{
|
||||
_auth: Auth{},
|
||||
_format: JSON,
|
||||
_library: Library{},
|
||||
_series: map[string]SerieAugmented{},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (jncApi *Api) SetPassword(password string) {
|
||||
jncApi._auth._password = password
|
||||
}
|
||||
|
||||
func (jncApi *Api) SetUsername(username string) {
|
||||
jncApi._auth._username = username
|
||||
}
|
||||
|
||||
// FetchLibrary retrieves the list of Book's in the logged-in User's J-Novel Club library
|
||||
func (jncApi *Api) FetchLibrary() error {
|
||||
fmt.Println("Fetching library contents...")
|
||||
res, err := http.Get(ApiV2Url + "me/library?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(body, &jncApi._library)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range jncApi._library.Books {
|
||||
book := &jncApi._library.Books[i]
|
||||
|
||||
data, ok := jncApi._series[book.Serie.Id]
|
||||
if ok {
|
||||
data.Volumes = append(data.Volumes, VolumeAugmented{
|
||||
Info: book.Volume,
|
||||
Downloads: book.Downloads,
|
||||
updated: book.LastUpdated,
|
||||
downloaded: book.LastDownload,
|
||||
})
|
||||
sort.Slice(data.Volumes, func(i, j int) bool {
|
||||
return data.Volumes[i].Info.Number < data.Volumes[j].Info.Number
|
||||
})
|
||||
jncApi._series[book.Serie.Id] = data
|
||||
continue
|
||||
}
|
||||
jncApi._series[book.Serie.Id] = SerieAugmented{
|
||||
Info: book.Serie,
|
||||
Volumes: []VolumeAugmented{
|
||||
{
|
||||
Info: book.Volume,
|
||||
Downloads: book.Downloads,
|
||||
updated: book.LastUpdated,
|
||||
downloaded: book.LastDownload,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchLibrarySeries is only needed because for whatever reason the Endpoint used in FetchLibrary does not return the Series Tags
|
||||
func (jncApi *Api) FetchLibrarySeries() error {
|
||||
progress := 1
|
||||
fmt.Printf("Fetching Series Info: 0/%s - 0%", strconv.Itoa(len(jncApi._series)))
|
||||
for i := range jncApi._series {
|
||||
fmt.Printf("\rFetching Series Info: %s/%s - %s%%", strconv.Itoa(progress), strconv.Itoa(len(jncApi._series)), strconv.FormatFloat(float64(progress)/float64(len(jncApi._series))*float64(100), 'f', 2, 32))
|
||||
serie := jncApi._series[i]
|
||||
|
||||
res, err := http.Get(ApiV2Url + "series/" + serie.Info.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &serie)
|
||||
if err != nil {
|
||||
jncApi._series[i] = serie
|
||||
}
|
||||
|
||||
progress++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLibrarySeries returns a list of unique series found in the User's library. Tags are only included if FetchLibrarySeries was previously called.
|
||||
func (jncApi *Api) GetLibrarySeries() (seriesList []SerieAugmented, err error) {
|
||||
arr := make([]SerieAugmented, 0, len(jncApi._series))
|
||||
for s := range jncApi._series {
|
||||
arr = append(arr, jncApi._series[s])
|
||||
}
|
||||
|
||||
sort.Slice(arr, func(i, j int) bool {
|
||||
return arr[i].Info.Title < arr[j].Info.Title
|
||||
})
|
||||
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
func (jncApi *Api) Download(link string, targetDir string) (name string, err error) {
|
||||
fmt.Printf("Downloading %s...", link)
|
||||
res, err := http.Get(link)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(res.Body)
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", errors.New(res.Status)
|
||||
}
|
||||
|
||||
filePath := targetDir + strings.Trim(strings.Split(res.Header["Content-Disposition"][0], "=")[1], "\"")
|
||||
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}(file)
|
||||
|
||||
_, err = io.Copy(file, res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue