910 lines
22 KiB
Go
910 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"container/list"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/beevik/etree"
|
|
"io"
|
|
"kavita-helper-go/jnc"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Chapter struct {
|
|
numberMain int8
|
|
numberSub int8 // uses 0 for regular main chapters
|
|
chDisplay string
|
|
title string
|
|
firstPage string
|
|
lastPage string
|
|
pages []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 GetArg(argKey string) (string, error) {
|
|
if slices.Contains(os.Args, argKey) {
|
|
idx := slices.Index(os.Args, argKey)
|
|
return os.Args[idx+1], nil
|
|
}
|
|
return "", errors.New("arg " + argKey + " not found")
|
|
}
|
|
|
|
func main() {
|
|
interactive := false
|
|
if slices.Contains(os.Args, "-I") {
|
|
interactive = true
|
|
}
|
|
|
|
if !interactive {
|
|
panic("automatic mode is not implemented yet")
|
|
}
|
|
|
|
domain, err := GetArg("-D")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
serLan, err := GetArg("-L")
|
|
if serLan != "" {
|
|
serLan = " " + serLan
|
|
}
|
|
|
|
downloadDir, err := GetArg("-d")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 {
|
|
downloadDir = downloadDir + "/"
|
|
}
|
|
|
|
// initialize the J-Novel Club Instance
|
|
jnovel := jnc.NewJNC(domain)
|
|
|
|
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", serLan)
|
|
}
|
|
}
|
|
if mode == "3" || mode == "5" {
|
|
if serie.Info.Type == "NOVEL" {
|
|
HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y", serLan)
|
|
}
|
|
}
|
|
}
|
|
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, serLan)
|
|
} 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, serLan)
|
|
}
|
|
}
|
|
}
|
|
|
|
func HandleSeries(jnovel jnc.Api, serie jnc.SerieAugmented, downloadDir string, updatedOnly bool, titleSuffix string) {
|
|
for v := range serie.Volumes {
|
|
volume := serie.Volumes[v]
|
|
if updatedOnly {
|
|
if len(volume.Downloads) != 0 && volume.UpdateAvailable() {
|
|
downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix)
|
|
HandleVolume(jnovel, serie, volume, downloadDir, titleSuffix)
|
|
}
|
|
} else {
|
|
downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix)
|
|
HandleVolume(jnovel, serie, volume, downloadDir, titleSuffix)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string, titleSuffix 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+titleSuffix {
|
|
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 + titleSuffix
|
|
} else {
|
|
splits[len(splits)-1] = serie.Info.Title + part + titleSuffix
|
|
splits = append(splits, "")
|
|
}
|
|
downloadDir = strings.Join(splits, "/")
|
|
} else {
|
|
downloadDir += serie.Info.Title + titleSuffix + "/"
|
|
}
|
|
|
|
_, 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, titleSuffix 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, titleSuffix)
|
|
}
|
|
|
|
func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadLink string, downloadDirectory string, titleSuffix 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 {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Switch based on metadata file
|
|
|
|
switch FormatMetadataName[metadata.fileName] {
|
|
case "manga":
|
|
{
|
|
comicInfoName := "ComicInfo.xml"
|
|
chapterList := GenerateMangaChapterList(navigation)
|
|
|
|
if chapterList.Len() == 0 {
|
|
fmt.Println("No chapters found, chapter name likely not supported")
|
|
}
|
|
|
|
basePath := downloadDirectory + volume.Info.Title + titleSuffix + "/"
|
|
PrepareVolumeDirectory(basePath)
|
|
volume.Info, err = jnovel.FetchVolumeInfo(volume.Info)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
doc := etree.NewDocument()
|
|
reader, err := metadata.file.Open()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
if _, err := doc.ReadFrom(reader); err != nil {
|
|
_ = reader.Close()
|
|
panic(err)
|
|
}
|
|
|
|
language := doc.FindElement("package/metadata/dc:language").Text()
|
|
|
|
for ch := chapterList.Front(); ch != nil; ch = ch.Next() {
|
|
chap := ch.Value.(Chapter)
|
|
|
|
zipPath := basePath + volume.Info.Title + " Chapter " + chap.chDisplay + ".cbz"
|
|
|
|
if _, err = os.Stat(zipPath); err == nil {
|
|
err := os.Remove(zipPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
newZipFile, err := os.Create(zipPath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
newZip := zip.NewWriter(newZipFile)
|
|
|
|
for chapterPageIndex := range chap.pages {
|
|
chapterFile := chap.pages[chapterPageIndex]
|
|
|
|
epubFileIndex := slices.IndexFunc(epub.File, func(f *zip.File) bool {
|
|
fParts := strings.Split(f.Name, "/")
|
|
cParts := strings.Split(chapterFile, "/")
|
|
return fParts[len(fParts)-1] == cParts[len(cParts)-1]
|
|
})
|
|
|
|
epubFile := epub.File[epubFileIndex]
|
|
|
|
doc := etree.NewDocument()
|
|
reader, err := epubFile.Open()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
if _, err := doc.ReadFrom(reader); err != nil {
|
|
_ = reader.Close()
|
|
panic(err)
|
|
}
|
|
|
|
imageFilePath := doc.FindElement("//html/body/svg/image").SelectAttr("href").Value
|
|
_ = reader.Close()
|
|
|
|
if imageFilePath[2:] == ".." {
|
|
fParts := strings.Split(epubFile.Name, "/")
|
|
imageFilePath = fParts[len(fParts)-2] + imageFilePath[2:len(imageFilePath)-1]
|
|
}
|
|
|
|
iParts := strings.Split(imageFilePath, "/")
|
|
imageFileIndex := slices.IndexFunc(epub.File, func(f *zip.File) bool {
|
|
fParts := strings.Split(f.Name, "/")
|
|
return fParts[len(fParts)-1] == iParts[len(iParts)-1]
|
|
})
|
|
|
|
imageFile := epub.File[imageFileIndex]
|
|
fileName := fmt.Sprintf("%02d", chapterPageIndex+1) + "." + strings.Split(iParts[len(iParts)-1], ".")[1]
|
|
|
|
err = addFileToZip(newZip, imageFile, fileName)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
}
|
|
|
|
comicInfo, err := GenerateChapterMetadata(volume, serie, len(chap.pages), language, chap.chDisplay, titleSuffix)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
err = addBytesToZip(newZip, comicInfo, comicInfoName)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
newZip.Close()
|
|
newZipFile.Close()
|
|
}
|
|
|
|
epub.Close()
|
|
os.Remove(file)
|
|
}
|
|
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] + titleSuffix
|
|
number = splits[3]
|
|
} else {
|
|
title = serie.Info.Title + titleSuffix
|
|
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 GenerateChapterMetadata(volume jnc.VolumeAugmented, serie jnc.SerieAugmented, pageCount int, language string, chapterNumber string, titleSuffix string) ([]byte, error) {
|
|
comicInfo := ComicInfo{
|
|
XMLName: "ComicInfo",
|
|
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
|
|
XSI: "ComicInfo.xsd",
|
|
}
|
|
|
|
vInfo := volume.Info
|
|
sInfo := serie.Info
|
|
|
|
comicInfo.Series = sInfo.Title
|
|
comicInfo.Title = vInfo.Title + titleSuffix
|
|
comicInfo.Number = chapterNumber
|
|
comicInfo.Volume = vInfo.Number
|
|
|
|
comicInfo.Count = -1 // TODO somehow fetch actual completion status
|
|
|
|
comicInfo.Summary = vInfo.Description
|
|
|
|
publishingTime, err := time.Parse(time.RFC3339, vInfo.Publishing)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
comicInfo.Year = publishingTime.Year()
|
|
comicInfo.Month = int(publishingTime.Month())
|
|
comicInfo.Day = publishingTime.Day()
|
|
|
|
if vInfo.Creators.Contains("AUTHOR") {
|
|
comicInfo.Writer = vInfo.Creators.Get("AUTHOR").Name
|
|
}
|
|
|
|
if vInfo.Creators.Contains("LETTERER") {
|
|
comicInfo.Letterer = vInfo.Creators.Get("LETTERER").Name
|
|
} else if vInfo.Creators.Contains("ILLUSTRATOR") {
|
|
comicInfo.Letterer = vInfo.Creators.Get("ILLUSTRATOR").Name
|
|
}
|
|
|
|
if vInfo.Creators.Contains("ARTIST") {
|
|
comicInfo.CoverArtist = vInfo.Creators.Get("ARTIST").Name
|
|
} else if vInfo.Creators.Contains("ILLUSTRATOR") {
|
|
comicInfo.CoverArtist = vInfo.Creators.Get("ILLUSTRATOR").Name
|
|
}
|
|
|
|
if vInfo.Creators.Contains("TRANSLATOR") {
|
|
comicInfo.Translator = vInfo.Creators.Get("TRANSLATOR").Name
|
|
}
|
|
|
|
if vInfo.Creators.Contains("EDITOR") {
|
|
comicInfo.Editor = vInfo.Creators.Get("EDITOR").Name
|
|
}
|
|
|
|
comicInfo.Publisher = "J-Novel Club"
|
|
|
|
comicInfo.Tags = strings.Join(sInfo.Tags, ",")
|
|
comicInfo.PageCount = pageCount
|
|
comicInfo.Language = language
|
|
comicInfo.Format = "Comic" // JNC reports this as type in the epub file
|
|
|
|
return xml.MarshalIndent(comicInfo, " ", " ")
|
|
}
|
|
|
|
type ComicInfo struct {
|
|
XMLName string `xml:"ComicInfo"`
|
|
XMLNS string `xml:"xmlns:xsi,attr"`
|
|
XSI string `xml:"xsi:noNamespaceSchemaLocation,attr"`
|
|
Series string `xml:"Series"`
|
|
Title string `xml:"Title"`
|
|
Volume int `xml:"Volume"`
|
|
Number string `xml:"Number"`
|
|
Count int `xml:"Count"`
|
|
Summary string `xml:"Summary"`
|
|
Year int `xml:"Year"`
|
|
Month int `xml:"Month"`
|
|
Day int `xml:"Day"`
|
|
Writer string `xml:"Writer"`
|
|
Letterer string `xml:"Letterer"`
|
|
CoverArtist string `xml:"CoverArtist"`
|
|
Editor string `xml:"Editor"`
|
|
Translator string `xml:"Translator"`
|
|
Publisher string `xml:"Publisher"`
|
|
Tags string `xml:"Tags"`
|
|
PageCount int `xml:"PageCount"`
|
|
Language string `xml:"LanguageISO"`
|
|
Format string `xml:"Format"`
|
|
}
|
|
|
|
func addBytesToZip(zipWriter *zip.Writer, fileBytes []byte, filename string) error {
|
|
reader := bytes.NewReader(fileBytes)
|
|
|
|
w, err := zipWriter.Create(filename)
|
|
if err != nil {
|
|
return error(err)
|
|
}
|
|
|
|
if _, err := io.Copy(w, reader); err != nil {
|
|
return error(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func addFileToZip(zipWriter *zip.Writer, file *zip.File, filename string) error {
|
|
fileToZip, err := file.Open()
|
|
if err != nil {
|
|
return error(err)
|
|
}
|
|
defer fileToZip.Close()
|
|
|
|
w, err := zipWriter.Create(filename)
|
|
if err != nil {
|
|
return error(err)
|
|
}
|
|
|
|
if _, err := io.Copy(w, fileToZip); err != nil {
|
|
return error(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func PrepareVolumeDirectory(directory string) {
|
|
_, err := os.Stat(directory)
|
|
if err != nil {
|
|
err = os.Mkdir(directory, 0755)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func FinalizeChapters(chapters *list.List, pageList []*etree.Element) *list.List {
|
|
fmt.Println("Finalizing Chapters")
|
|
|
|
sortedChapters := list.New()
|
|
|
|
var mainChapter Chapter
|
|
|
|
for ch := chapters.Back(); ch != nil; ch = ch.Prev() {
|
|
currentChapter := ch.Value.(Chapter)
|
|
|
|
if currentChapter.numberMain != -1 {
|
|
// pages
|
|
if currentChapter.numberSub != 0 {
|
|
numberMain, numberSub := AnalyzeMainChapter(ch)
|
|
currentChapter.numberMain = numberMain
|
|
currentChapter.numberSub = numberSub
|
|
currentChapter.chDisplay = strconv.FormatInt(int64(currentChapter.numberMain), 10) + "." + strconv.FormatInt(int64(currentChapter.numberSub), 10)
|
|
} else {
|
|
currentChapter.chDisplay = strconv.FormatInt(int64(currentChapter.numberMain), 10)
|
|
}
|
|
|
|
currentChapter.lastPage = pageList[len(pageList)-1].SelectAttr("src").Value
|
|
collecting := false
|
|
for p := range pageList {
|
|
page := pageList[p].SelectAttr("src").Value
|
|
if page == currentChapter.firstPage {
|
|
collecting = true
|
|
currentChapter.pages = append(currentChapter.pages, page)
|
|
} else if ch.Next() != nil && page == ch.Next().Value.(Chapter).firstPage {
|
|
currentChapter.lastPage = pageList[p-1].SelectAttr("src").Value
|
|
collecting = false
|
|
} else if collecting {
|
|
currentChapter.pages = append(currentChapter.pages, page)
|
|
}
|
|
}
|
|
|
|
sortedChapters.PushFront(currentChapter)
|
|
} else {
|
|
mainChapter = GetLastValidChapterNumber(ch)
|
|
|
|
for sch := sortedChapters.Front(); sch != nil; sch = sch.Next() {
|
|
if sch.Value.(Chapter).title == mainChapter.title {
|
|
mainChapterData := sch.Value.(Chapter)
|
|
mainChapterData.firstPage = currentChapter.firstPage
|
|
|
|
collecting := false
|
|
pages := []string{}
|
|
for p := range pageList {
|
|
page := pageList[p].SelectAttr("src").Value
|
|
if page == currentChapter.firstPage {
|
|
collecting = true
|
|
pages = append(pages, page)
|
|
} else if ch.Next() != nil && page == ch.Next().Value.(Chapter).firstPage {
|
|
currentChapter.lastPage = pageList[p-1].SelectAttr("src").Value
|
|
collecting = false
|
|
} else if collecting {
|
|
pages = append(pages, page)
|
|
}
|
|
}
|
|
mainChapterData.pages = append(pages, mainChapterData.pages...)
|
|
sch.Value = mainChapterData
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
} else {
|
|
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|Episode) ([0-9IVXLCDM]*)"
|
|
match, _ := regexp.MatchString(regex, label)
|
|
|
|
if match {
|
|
r, _ := regexp.Compile(regex)
|
|
num := r.FindStringSubmatch(label)[1]
|
|
parse, _ := strconv.ParseInt(num, 10, 8)
|
|
if int8(parse) == 0 {
|
|
fmt.Printf("Unlikely Chapter Number Detected (0): '%s'\n", num)
|
|
fmt.Println("Attempting Roman Numerals")
|
|
lastMainChapter = int8(romanToInt(num))
|
|
} else {
|
|
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: "",
|
|
pages: []string{},
|
|
}
|
|
|
|
chapters.PushBack(chapterData)
|
|
}
|
|
|
|
return FinalizeChapters(chapters, pageList)
|
|
}
|
|
|
|
func romanToInt(s string) int {
|
|
|
|
know := map[string]int{
|
|
"I": 1,
|
|
"V": 5,
|
|
"X": 10,
|
|
"L": 50,
|
|
"C": 100,
|
|
"D": 500,
|
|
"M": 1000,
|
|
}
|
|
lengthOfString := len(s)
|
|
lastElement := s[len(s)-1 : lengthOfString]
|
|
var result int
|
|
result = know[lastElement]
|
|
for i := len(s) - 1; i > 0; i-- {
|
|
if know[s[i:i+1]] <= know[s[i-1:i]] {
|
|
result += know[s[i-1:i]]
|
|
} else {
|
|
result -= know[s[i-1:i]]
|
|
}
|
|
}
|
|
return result
|
|
}
|