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
596
main.go
Normal file
596
main.go
Normal 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue