v0.8/Initial - Working Downloads and Novel Processing

This commit is contained in:
Neshura 2025-02-17 00:06:16 +01:00
parent ef1c533009
commit a0b4b4361f
Signed by: Neshura
GPG key ID: 4E2D47B1374C297D
9 changed files with 1063 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/kavita-helper-go.iml generated Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

12
.idea/material_theme_project_new.xml generated Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-349a7812:1900e527711:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/kavita-helper-go.iml" filepath="$PROJECT_DIR$/.idea/kavita-helper-go.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

5
go.mod Normal file
View file

@ -0,0 +1,5 @@
module kavita-helper-go
go 1.23
require github.com/beevik/etree v1.5.0

2
go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=

417
jnc/jnc.go Normal file
View 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
}

596
main.go Normal file
View file

@ -0,0 +1,596 @@
package main
import (
"archive/zip"
"bufio"
"container/list"
"fmt"
"github.com/beevik/etree"
"io"
"kavita-helper-go/jnc"
"os"
"os/exec"
"regexp"
"slices"
"strconv"
"strings"
)
type Chapter struct {
numberMain int8
numberSub int8 // uses 0 for regular main chapters
chDisplay string
title string
firstPage string
lastPage string
}
type FileWrapper struct {
fileName string
file zip.File
fileSet bool
}
func NewFileWrapper() FileWrapper {
newFileWrapper := FileWrapper{}
newFileWrapper.fileSet = false
newFileWrapper.fileName = ""
return newFileWrapper
}
// TODO's
// [ ] Create Chapter .cbz's based on given Information (no ComicInfo.xml yet)
// [ ] Sort Into Volume Folders (requires following step)
// [ ] Get ComicInfo.xml data from the J-Novel Club API
// [ ] Get the Manga from the J-Novel Club API
func GetUserInput(prompt string) string {
reader := bufio.NewScanner(os.Stdin)
fmt.Print(prompt)
reader.Scan()
err := reader.Err()
if err != nil {
panic(err)
}
return reader.Text()
}
func ClearScreen() {
cmd := exec.Command("clear")
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
fmt.Println("[ERROR] - ", err)
}
}
func main() {
downloadDir := "/home/neshura/Documents/Go Workspaces/Kavita Helper/"
// initialize the J-Novel Club Instance
jnovel := jnc.NewJNC()
var username string
if slices.Contains(os.Args, "-u") {
idx := slices.Index(os.Args, "-u")
username = os.Args[idx+1]
} else {
username = GetUserInput("Enter Username: ")
}
jnovel.SetUsername(username)
var password string
if slices.Contains(os.Args, "-p") {
idx := slices.Index(os.Args, "-p")
password = os.Args[idx+1]
} else {
password = GetUserInput("Enter Password: ")
}
jnovel.SetPassword(password)
err := jnovel.Login()
if err != nil {
panic(err)
}
defer func(jnovel *jnc.Api) {
err := jnovel.Logout()
if err != nil {
panic(err)
}
}(&jnovel)
err = jnovel.FetchLibrary()
if err != nil {
panic(err)
}
err = jnovel.FetchLibrarySeries()
if err != nil {
panic(err)
}
seriesList, err := jnovel.GetLibrarySeries()
if err != nil {
panic(err)
}
ClearScreen()
fmt.Println("[1] - Download Single Volume")
fmt.Println("[2] - Download Entire Series")
fmt.Println("[3] - Download Entire Library")
fmt.Println("[4] - Download Entire Library [MANGA only]")
fmt.Println("[5] - Download Entire Library [NOVEL only]")
mode := GetUserInput("Select Mode: ")
ClearScreen()
if mode == "3" || mode == "4" || mode == "5" {
updatedOnly := GetUserInput("Download Only Where Newer Files Are Available? [Y/N]: ")
for s := range seriesList {
serie := seriesList[s]
if mode == "3" || mode == "4" {
if serie.Info.Type == "MANGA" {
HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y")
}
}
if mode == "3" || mode == "5" {
if serie.Info.Type == "NOVEL" {
HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y")
}
}
}
fmt.Println("Done")
} else {
fmt.Println("\n###[Series Selection]###")
for s := range seriesList {
serie := seriesList[s].Info
fmt.Printf("[%s] - [%s] - %s\n", strconv.Itoa(s+1), serie.Type, serie.Title)
}
msg := "Enter Series Number: "
var seriesNumber int
for {
selection := GetUserInput(msg)
seriesNumber, err = strconv.Atoi(selection)
if err != nil {
msg = "\rInvalid. Enter VALID Series Number: "
}
break
}
serie := seriesList[seriesNumber-1]
if mode == "2" {
HandleSeries(jnovel, serie, downloadDir, false)
} else {
ClearScreen()
fmt.Println("\n###[Volume Selection]###")
for i := range serie.Volumes {
vol := serie.Volumes[i]
fmt.Printf("[%s] - %s\n", strconv.Itoa(i+1), vol.Info.Title)
}
msg = "Enter Volume Number: "
var volumeNumber int
for {
selection := GetUserInput(msg)
volumeNumber, err = strconv.Atoi(selection)
if err != nil {
msg = "\rInvalid. Enter VALID Volume Number: "
}
break
}
volume := serie.Volumes[volumeNumber-1]
HandleVolume(jnovel, serie, volume, downloadDir)
}
}
}
func HandleSeries(jnovel jnc.Api, serie jnc.SerieAugmented, downloadDir string, updatedOnly bool) {
for v := range serie.Volumes {
volume := serie.Volumes[v]
if len(volume.Downloads) != 0 && volume.UpdateAvailable() {
downloadDir = PrepareSerieDirectory(serie, volume, downloadDir)
HandleVolume(jnovel, serie, volume, downloadDir)
}
}
}
func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string) string {
splits := strings.Split(downloadDir, "/")
// last element of this split is always empty due to the trailing slash
if splits[len(splits)-2] != serie.Info.Title {
if serie.Info.Title == "Ascendance of a Bookworm" {
partSplits := strings.Split(volume.Info.ShortTitle, " ")
part := " " + partSplits[0] + " " + partSplits[1]
if strings.Split(splits[len(splits)-2], " ")[0] == "Ascendance" {
splits[len(splits)-2] = serie.Info.Title + part
} else {
splits[len(splits)-1] = serie.Info.Title + part
splits = append(splits, "")
}
downloadDir = strings.Join(splits, "/")
} else {
downloadDir += serie.Info.Title + "/"
}
_, err := os.Stat(downloadDir)
if err != nil {
err = os.Mkdir(downloadDir, 0755)
if err != nil {
panic(err)
}
}
}
return downloadDir
}
func HandleVolume(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string) {
var downloadLink string
if len(volume.Downloads) == 0 {
fmt.Printf("Volume %s currently has no downloads available. Skipping \n", volume.Info.Title)
return
} else if len(volume.Downloads) > 1 {
fmt.Println("TODO: implement download selection/default select highest quality")
downloadLink = volume.Downloads[len(volume.Downloads)-1].Link
} else {
downloadLink = volume.Downloads[0].Link
}
DownloadAndProcessEpub(jnovel, serie, volume, downloadLink, downloadDir)
}
func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadLink string, downloadDirectory string) {
FormatNavigationFile := map[string]string{
"manga": "item/nav.ncx",
"novel": "OEBPS/toc.ncx",
}
FormatMetadataName := map[string]string{
"item/comic.opf": "manga",
"OEBPS/content.opf": "novel",
}
MetaInf := "META-INF/container.xml"
file, err := jnovel.Download(downloadLink, downloadDirectory)
if err != nil {
panic(err)
}
epub, err := zip.OpenReader(file)
if err != nil {
fmt.Println(err)
}
metadata := NewFileWrapper()
navigation := NewFileWrapper()
for i := range epub.Reader.File {
file := epub.Reader.File[i]
if file.Name == MetaInf {
doc := etree.NewDocument()
reader, err := file.Open()
if err != nil {
fmt.Println(err)
}
if _, err := doc.ReadFrom(reader); err != nil {
_ = reader.Close()
panic(err)
}
metadata.fileName = doc.FindElement("//container/rootfiles/rootfile").SelectAttr("full-path").Value
navigation.fileName = FormatNavigationFile[FormatMetadataName[metadata.fileName]]
}
if len(metadata.fileName) != 0 && file.Name == metadata.fileName {
metadata.file = *file
metadata.fileSet = true
}
if len(navigation.fileName) != 0 && file.Name == navigation.fileName {
navigation.file = *file
navigation.fileSet = true
}
if metadata.fileSet && navigation.fileSet {
println("breaking")
break
}
}
// Switch based on metadata file
switch FormatMetadataName[metadata.fileName] {
case "manga":
{
chapterList := GenerateMangaChapterList(navigation)
for ch := chapterList.Front(); ch != nil; ch = ch.Next() {
chap := ch.Value.(Chapter)
fmt.Printf("[%s] %s: %s - %s\n", chap.chDisplay, chap.title, chap.firstPage, chap.lastPage)
}
}
case "novel":
{
CalibreSeriesAttr := "calibre:series"
CalibreSeriesIndexAttr := "calibre:series_index"
EpubTitleTag := "//package/metadata"
metafile, err := metadata.file.Open()
if err != nil {
fmt.Println(err)
}
doc := etree.NewDocument()
if _, err = doc.ReadFrom(metafile); err != nil {
fmt.Println(err)
}
idx := doc.FindElement(EpubTitleTag + "/dc:title").Index()
seriesTitle := etree.NewElement("meta")
seriesTitle.CreateAttr("name", CalibreSeriesAttr)
var title string
var number string
if serie.Info.Title == "Ascendance of a Bookworm" {
splits := strings.Split(volume.Info.ShortTitle, " ")
title = serie.Info.Title + " " + splits[0] + " " + splits[1]
number = splits[3]
} else {
title = serie.Info.Title
number = strconv.Itoa(volume.Info.Number)
}
seriesTitle.CreateAttr("content", title)
seriesNumber := etree.NewElement("meta")
seriesNumber.CreateAttr("name", CalibreSeriesIndexAttr)
seriesNumber.CreateAttr("content", number)
doc.FindElement(EpubTitleTag).InsertChildAt(idx+1, seriesTitle)
doc.FindElement(EpubTitleTag).InsertChildAt(idx+2, seriesNumber)
doc.Indent(2)
// Open the zip file for writing
zipfile, err := os.OpenFile(file+".new", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
panic(err)
}
defer func(zipfile *os.File) {
err := zipfile.Close()
if err != nil {
panic(err)
}
}(zipfile)
zipWriter := zip.NewWriter(zipfile)
str, err := doc.WriteToString()
if err != nil {
panic(err)
}
metawriter, err := zipWriter.Create(metadata.fileName)
if err != nil {
panic(err)
}
_, err = metawriter.Write([]byte(str))
if err != nil {
panic(err)
}
for _, f := range epub.File {
if f.Name != metadata.fileName {
writer, err := zipWriter.Create(f.Name)
if err != nil {
panic(err)
}
reader, err := f.Open()
if err != nil {
panic(err)
}
_, err = io.Copy(writer, reader)
if err != nil {
_ = reader.Close()
panic(err)
}
}
}
epub.Close()
zipWriter.Close()
source, _ := os.Open(file + ".new")
dest, _ := os.Create(file)
io.Copy(dest, source)
os.Remove(file + ".new")
}
}
}
func FinalizeChapters(chapters *list.List, pageList []*etree.Element) *list.List {
fmt.Println("Finalizing Chapters")
lastPage := pageList[len(pageList)-1].SelectAttr("src").Value
sortedChapters := list.New()
for ch := chapters.Back(); ch != nil; ch = ch.Prev() {
var newChapterData Chapter
currentChapter := ch.Value.(Chapter)
if currentChapter.numberMain != -1 {
newChapterData.title = currentChapter.title
newChapterData.firstPage = currentChapter.firstPage
} else {
mainChapter := GetLastValidChapterNumber(ch)
for sch := sortedChapters.Front(); sch != nil; sch = sch.Next() {
if sch.Value.(Chapter).title == mainChapter.title {
newChapterData = sch.Value.(Chapter)
newChapterData.firstPage = currentChapter.firstPage
sch.Value = newChapterData
break
}
}
continue
}
if currentChapter.numberSub != 0 {
numberMain, numberSub := AnalyzeMainChapter(ch)
newChapterData.numberMain = numberMain
newChapterData.numberSub = numberSub
} else {
newChapterData.numberMain = currentChapter.numberMain
newChapterData.numberSub = currentChapter.numberSub
}
if ch.Next() == nil {
newChapterData.lastPage = lastPage
} else {
var lastChapterPage string
for p := range pageList {
page := pageList[p]
if page.SelectAttr("src").Value == ch.Next().Value.(Chapter).firstPage {
lastChapterPage = pageList[p-1].SelectAttr("src").Value
}
}
newChapterData.lastPage = lastChapterPage
}
if newChapterData.numberSub != 0 {
newChapterData.chDisplay = strconv.FormatInt(int64(newChapterData.numberMain), 10) + "." + strconv.FormatInt(int64(newChapterData.numberSub), 10)
} else {
newChapterData.chDisplay = strconv.FormatInt(int64(newChapterData.numberMain), 10)
}
sortedChapters.PushFront(newChapterData)
}
return sortedChapters
}
func AnalyzeMainChapter(currentChapter *list.Element) (int8, int8) {
var currentChapterNumber int8
subChapterCount := 1
if currentChapter.Next() != nil {
ch := currentChapter.Next()
for {
if ch.Value.(Chapter).numberSub != 0 {
subChapterCount++
if ch.Next() != nil {
ch = ch.Next()
} else {
break
}
} else {
break
}
}
}
if currentChapter.Prev() != nil {
ch := currentChapter.Prev()
// Then Get the Current Main Chapter
for {
if ch.Value.(Chapter).numberSub != 0 {
subChapterCount++
if ch.Prev() != nil {
ch = ch.Prev()
} else {
break
}
} else {
subChapterCount++ // actual Chapter also counts in this case
currentChapterNumber = ch.Value.(Chapter).numberMain
break
}
}
}
// Calculate integer sub number based on total subChapterC
subChapterNumber := int8((float32(currentChapter.Value.(Chapter).numberSub) / float32(subChapterCount)) * 10)
// return main Chapter Number + CurrentChapter Sub Number
return currentChapterNumber, subChapterNumber
}
func GetLastValidChapterNumber(currentChapter *list.Element) Chapter {
for {
if currentChapter.Next() != nil {
currentChapter = currentChapter.Next()
chapterData := currentChapter.Value.(Chapter)
if chapterData.numberMain != -1 && chapterData.numberSub == 0 {
return chapterData
}
}
break
}
return currentChapter.Value.(Chapter)
}
func GenerateMangaChapterList(navigation FileWrapper) *list.List {
reader, err := navigation.file.Open()
if err != nil {
fmt.Println(err)
}
doc := etree.NewDocument()
if _, err = doc.ReadFrom(reader); err != nil {
fmt.Println(err)
}
navMap := doc.FindElement("//ncx/navMap")
navChapters := navMap.FindElements("//navPoint")
chapters := list.New()
pageList := doc.FindElements("//ncx/pageList/pageTarget/content")
lastMainChapter := int8(-1)
subChapter := int8(0)
for c := range len(navChapters) {
navChapter := navChapters[c]
label := navChapter.FindElement("./navLabel/text").Text()
page := navChapter.FindElement("./content")
regex := "Chapter ([0-9]*)"
match, _ := regexp.MatchString(regex, label)
if match {
r, _ := regexp.Compile(regex)
num := r.FindStringSubmatch(label)[1]
parse, _ := strconv.ParseInt(num, 10, 8)
lastMainChapter = int8(parse)
subChapter = int8(0)
} else {
if lastMainChapter == -1 {
subChapter -= 1
} else {
subChapter += 1
}
}
chapterData := Chapter{
numberMain: lastMainChapter,
numberSub: subChapter,
chDisplay: "",
title: label,
firstPage: page.SelectAttr("src").Value,
lastPage: "",
}
chapters.PushBack(chapterData)
}
return FinalizeChapters(chapters, pageList)
}