Add Creator Info + Fetching to JNC Module. Implement .cbz chapter generation

This commit is contained in:
Neshura 2025-02-18 22:17:29 +01:00
parent 1661c7de42
commit b36bc9b0b2
Signed by: Neshura
GPG key ID: 4E2D47B1374C297D
2 changed files with 345 additions and 49 deletions

View file

@ -8,6 +8,7 @@ import (
"io"
"net/http"
"os"
"slices"
"sort"
"strconv"
"strings"
@ -90,7 +91,7 @@ type Volume struct {
Number int `json:"number"`
OriginalPublisher string `json:"originalPublisher"`
Label string `json:"label"`
Creators []string `json:"creators"` // TODO: check, might not be correct
Creators Creators `json:"creators"` // TODO: check, might not be correct
hidden bool
ForumTopicId int `json:"forumTopicId"`
Created string `json:"created"`
@ -105,6 +106,28 @@ type Volume struct {
OnSale bool `json:"onSale"`
}
type Creators []Creator
type Creator struct {
Id string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
OriginalName string `json:"originalName "`
}
func (creators *Creators) Contains(name string) bool {
return slices.ContainsFunc(*creators, func(c Creator) bool {
return c.Name == name
})
}
func (creators *Creators) Get(name string) *Creator {
idx := slices.IndexFunc(*creators, func(c Creator) bool {
return c.Name == name
})
return &(*creators)[idx]
}
type VolumeAugmented struct {
Info Volume
Downloads []Download
@ -116,7 +139,7 @@ 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)
@ -367,6 +390,30 @@ func (jncApi *Api) FetchLibrarySeries() error {
return nil
}
// FetchVolumeInfo retrieves additional Volume Info that was not returned when retrieving the entire Library
func (jncApi *Api) FetchVolumeInfo(volume Volume) (Volume, error) {
fmt.Println("Fetching Volume details...")
res, err := http.Get(ApiV2Url + "volumes/" + volume.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam())
if err != nil {
return volume, err
}
if res.StatusCode != 200 {
return volume, errors.New(res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return volume, err
}
err = json.Unmarshal(body, &volume)
if err != nil {
return volume, err
}
return volume, 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))

343
main.go
View file

@ -3,7 +3,9 @@ package main
import (
"archive/zip"
"bufio"
"bytes"
"container/list"
"encoding/xml"
"fmt"
"github.com/beevik/etree"
"io"
@ -14,6 +16,7 @@ import (
"slices"
"strconv"
"strings"
"time"
)
type Chapter struct {
@ -23,6 +26,7 @@ type Chapter struct {
title string
firstPage string
lastPage string
pages []string
}
type FileWrapper struct {
@ -64,7 +68,25 @@ func ClearScreen() {
}
func main() {
downloadDir := "/home/neshura/Documents/Go Workspaces/Kavita Helper/"
interactive := false
if slices.Contains(os.Args, "-I") {
interactive = true
}
if !interactive {
panic("automatic mode is not implemented yet")
}
var downloadDir string
if slices.Contains(os.Args, "-d") {
idx := slices.Index(os.Args, "-d")
downloadDir = os.Args[idx+1]
if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 {
downloadDir = downloadDir + "/"
}
} else {
panic("working directory not specified")
}
// initialize the J-Novel Club Instance
jnovel := jnc.NewJNC()
@ -192,10 +214,16 @@ func main() {
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() {
if updatedOnly {
if len(volume.Downloads) != 0 && volume.UpdateAvailable() {
downloadDir = PrepareSerieDirectory(serie, volume, downloadDir)
HandleVolume(jnovel, serie, volume, downloadDir)
}
} else {
downloadDir = PrepareSerieDirectory(serie, volume, downloadDir)
HandleVolume(jnovel, serie, volume, downloadDir)
}
}
}
@ -300,7 +328,6 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc
}
if metadata.fileSet && navigation.fileSet {
println("breaking")
break
}
}
@ -310,11 +337,104 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc
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 + "/"
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)
fmt.Printf("[%s] %s: %s - %s\n", chap.chDisplay, chap.title, chap.firstPage, chap.lastPage)
zipPath := basePath + "Chapter " + chap.chDisplay + ".cbz"
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("%020s", 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)
if err != nil {
fmt.Println(err)
}
err = addBytesToZip(newZip, comicInfo, comicInfoName)
if err != nil {
fmt.Println(err)
}
epub.Close()
newZip.Close()
newZipFile.Close()
os.Remove(file)
}
}
case "novel":
@ -416,65 +536,193 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc
}
}
func GenerateChapterMetadata(volume jnc.VolumeAugmented, serie jnc.SerieAugmented, pageCount int, language 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.Number = 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("ILLUSTRATOR") {
comicInfo.Letterer = vInfo.Creators.Get("ILLUSTRATOR").Name
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
}
if vInfo.Creators.Contains("PUBLISHER") {
comicInfo.Publisher = vInfo.Creators.Get("PUBLISHER").Name
}
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.Marshal(comicInfo)
}
type ComicInfo struct {
XMLName string `xml:"ComicInfo"`
XMLNS string `xml:"xmlns,attr"`
XSI string `xml:"xsi,attr"`
Series string `xml:"Series"`
Number int `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")
lastPage := pageList[len(pageList)-1].SelectAttr("src").Value
sortedChapters := list.New()
var mainChapter Chapter
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
// 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)
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
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
}
}
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
}
@ -563,7 +811,7 @@ func GenerateMangaChapterList(navigation FileWrapper) *list.List {
label := navChapter.FindElement("./navLabel/text").Text()
page := navChapter.FindElement("./content")
regex := "Chapter ([0-9]*)"
regex := "(?:Chapter|Episode) ([0-9]*)"
match, _ := regexp.MatchString(regex, label)
if match {
@ -587,6 +835,7 @@ func GenerateMangaChapterList(navigation FileWrapper) *list.List {
title: label,
firstPage: page.SelectAttr("src").Value,
lastPage: "",
pages: []string{},
}
chapters.PushBack(chapterData)