Add Creator Info + Fetching to JNC Module. Implement .cbz chapter generation
This commit is contained in:
parent
1661c7de42
commit
b36bc9b0b2
2 changed files with 345 additions and 49 deletions
51
jnc/jnc.go
51
jnc/jnc.go
|
@ -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
343
main.go
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue