From 589f5dc14164f01b6e01d4dd11c4621624dc30bb Mon Sep 17 00:00:00 2001 From: Neshura <neshura@neshweb.net> Date: Sun, 2 Mar 2025 00:24:36 +0100 Subject: [PATCH] Support for Local Novels. Adjusted Metadata File Finding to not rely on File order. --- jnc/jnc.go | 17 ++ main.go | 453 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 308 insertions(+), 162 deletions(-) diff --git a/jnc/jnc.go b/jnc/jnc.go index e89aa71..0306bfa 100644 --- a/jnc/jnc.go +++ b/jnc/jnc.go @@ -125,6 +125,14 @@ func (creators Creators) Get(role string) *Creator { return &(creators)[idx] } +func (creators Creators) TryGetName(role string) string { + if creators.Contains(role) { + return creators.Get(role).Name + } else { + return "" + } +} + type VolumeAugmented struct { Info Volume Downloads []Download @@ -199,6 +207,15 @@ type Pagination struct { LastPage bool `json:"lastPage"` } +type ProviderSettings struct { + Domain string + Series string + Directory string + Username string + Password string + Language string +} + func NewJNC(domain string) Api { baseUrl := fmt.Sprintf("https://%s/", domain) jncApi := Api{ diff --git a/main.go b/main.go index 5fc732a..8724fc9 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "kavita-helper-go/jnc" "os" "os/exec" + "path/filepath" "regexp" "slices" "strconv" @@ -30,19 +31,6 @@ type Chapter struct { 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) @@ -76,57 +64,150 @@ func GetArg(argKey string) (string, error) { return "", errors.New("arg " + argKey + " not found") } +// Arguments: +// -I: Interactive Mode, prompts user for required info when needed +// -D: Domain. Only required if -P jnc is set +// -L: Language Suffix for Series Title +// -S: Series Name. optional when using -I +// -P: Provider +// -d: download Directory or File Directory. Depends on set -P +// -u: username. Only required if -P jnc is set +// -p: password. Only required if -P jnc is set +// -f: file. Only required when no -P is set. Specifies the file to be used +// -n: number. Only required when no -P is set. Specifies the volume number func main() { interactive := false if slices.Contains(os.Args, "-I") { interactive = true } - if !interactive { - panic("automatic mode is not implemented yet") - } + provider, _ := GetArg("-P") - domain, err := GetArg("-D") - if err != nil { - panic(err) - } - - serLan, err := GetArg("-L") + serLan, _ := GetArg("-L") if serLan != "" { serLan = " " + serLan } - downloadDir, err := GetArg("-d") - if err != nil { - panic(err) + switch provider { + case "": + { + // figure out settings needed for series input + series, err := GetArg("-S") + if err != nil { + series = GetUserInput("Enter Series Name: ") + } + + epubData := EpubInfo{ + series: series, + } + + dir, err := GetArg("-d") + if err != nil { + epubData.path, err = GetArg("-f") + if err != nil { + epubData.path = GetUserInput("Enter File Path: ") + epubData.title = filepath.Base(epubData.path)[0 : len(filepath.Base(epubData.path))-len(filepath.Ext(epubData.path))] + + number, err := GetArg("-n") + if err != nil { + number = GetUserInput("Enter Volume Number: ") + } + epubData.number = number + + epub, err := zip.OpenReader(epubData.path) + if err != nil { + fmt.Println(err) + } + + epubData.format = DetermineEpubFormat(epub) + if epubData.format.fType == "manga" { + panic("Manga without provider not yet handled") + } + ProcessEpub(epub, epubData) + } + } else { + if strings.LastIndex(dir, "/") != len(dir)-1 { + dir = dir + "/" + } + files, err := os.ReadDir(dir) + if err != nil { + panic(err) + } + for fIdx := range files { + epubData.path = dir + files[fIdx].Name() + epubData.title = files[fIdx].Name() + + epubData.number = GetUserInput(fmt.Sprintf("Enter Volume Number for '%s':", files[fIdx].Name())) + + epub, err := zip.OpenReader(epubData.path) + if err != nil { + fmt.Println(err) + } + + epubData.format = DetermineEpubFormat(epub) + if epubData.format.fType == "manga" { + panic("Manga without provider not yet handled") + } + ProcessEpub(epub, epubData) + } + } + } + case "jnc": + { + if !interactive { + panic("automatic mode for JNC is not implemented yet") + } + settings := jnc.ProviderSettings{ + Language: serLan, + } + + domain, err := GetArg("-D") + if err != nil { + panic(err) + } + settings.Domain = domain + + downloadDir, err := GetArg("-d") + if err != nil { + panic(err) + } + if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 { + downloadDir = downloadDir + "/" + } + settings.Directory = downloadDir + + series, err := GetArg("-S") + if err == nil { + settings.Series = series + } + + if slices.Contains(os.Args, "-u") { + idx := slices.Index(os.Args, "-u") + settings.Username = os.Args[idx+1] + } else { + settings.Username = GetUserInput("Enter Username: ") + } + + if slices.Contains(os.Args, "-p") { + idx := slices.Index(os.Args, "-p") + settings.Password = os.Args[idx+1] + } else { + settings.Password = GetUserInput("Enter Password: ") + } + + RunJNCProvider(settings) + } } - if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 { - downloadDir = downloadDir + "/" - } +} +func RunJNCProvider(settings jnc.ProviderSettings) { // initialize the J-Novel Club Instance - jnovel := jnc.NewJNC(domain) + jnovel := jnc.NewJNC(settings.Domain) + jnovel.SetUsername(settings.Username) + jnovel.SetPassword(settings.Password) - 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() + err := jnovel.Login() if err != nil { panic(err) } @@ -170,12 +251,12 @@ func main() { serie := seriesList[s] if mode == "3" || mode == "4" { if serie.Info.Type == "MANGA" { - HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y", serLan) + HandleSeries(jnovel, serie, settings.Directory, updatedOnly == "Y" || updatedOnly == "y", settings.Language) } } if mode == "3" || mode == "5" { if serie.Info.Type == "NOVEL" { - HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y", serLan) + HandleSeries(jnovel, serie, settings.Directory, updatedOnly == "Y" || updatedOnly == "y", settings.Language) } } } @@ -202,7 +283,7 @@ func main() { serie := seriesList[seriesNumber-1] if mode == "2" { - HandleSeries(jnovel, serie, downloadDir, false, serLan) + HandleSeries(jnovel, serie, settings.Directory, false, settings.Language) } else { ClearScreen() fmt.Println("\n###[Volume Selection]###") @@ -223,7 +304,7 @@ func main() { } volume := serie.Volumes[volumeNumber-1] - HandleVolume(jnovel, serie, volume, downloadDir, serLan) + HandleVolume(jnovel, serie, volume, settings.Directory, settings.Language) } } } @@ -287,89 +368,159 @@ func HandleVolume(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAug 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) + file, err := jnovel.Download(downloadLink, downloadDir) if err != nil { panic(err) } + volume.Info, err = jnovel.FetchVolumeInfo(volume.Info) + if err != nil { + fmt.Printf("Error fetching volume info: %s\n", err) + } + + 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) + } + + epubData := EpubInfo{ + path: file, + title: volume.Info.Title, + series: title, + number: number, + info: ComicInfo{ + Publisher: "J-Novel Club", + Tags: strings.Join(serie.Info.Tags, ","), + Series: serie.Info.Title, + Volume: volume.Info.Number, + Summary: volume.Info.Description, + Writer: volume.Info.Creators.TryGetName("AUTHOR"), + Translator: volume.Info.Creators.TryGetName("TRANSLATOR"), + Editor: volume.Info.Creators.TryGetName("EDITOR"), + }, + } + + if volume.Info.Creators.Contains("ARTIST") { + epubData.info.CoverArtist = volume.Info.Creators.Get("ARTIST").Name + } else if volume.Info.Creators.Contains("ILLUSTRATOR") { + epubData.info.CoverArtist = volume.Info.Creators.Get("ILLUSTRATOR").Name + } + + if volume.Info.Creators.Contains("LETTERER") { + epubData.info.Letterer = volume.Info.Creators.Get("LETTERER").Name + } else if volume.Info.Creators.Contains("ILLUSTRATOR") { + epubData.info.Letterer = volume.Info.Creators.Get("ILLUSTRATOR").Name + } + + publishingTime, err := time.Parse(time.RFC3339, volume.Info.Publishing) + if err != nil { + panic(err) + } + epubData.info.Year = publishingTime.Year() + epubData.info.Month = int(publishingTime.Month()) + epubData.info.Day = publishingTime.Day() epub, err := zip.OpenReader(file) if err != nil { fmt.Println(err) } - metadata := NewFileWrapper() - navigation := NewFileWrapper() + epubData.format = DetermineEpubFormat(epub) + ProcessEpub(epub, epubData) +} - for i := range epub.Reader.File { - file := epub.Reader.File[i] +type EpubInfo struct { + path string + title string + series string + number string + info ComicInfo + format FormatInfo +} - if file.Name == MetaInf { - doc := etree.NewDocument() - reader, err := file.Open() - if err != nil { - fmt.Println(err) - } +type FormatInfo struct { + fType string + metadata zip.File + navigation zip.File +} - if _, err := doc.ReadFrom(reader); err != nil { - _ = reader.Close() - panic(err) - } +func DetermineEpubFormat(epub *zip.ReadCloser) FormatInfo { + 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" - metadata.fileName = doc.FindElement("//container/rootfiles/rootfile").SelectAttr("full-path").Value - navigation.fileName = FormatNavigationFile[FormatMetadataName[metadata.fileName]] + metaInfFileIdx := slices.IndexFunc(epub.Reader.File, func(f *zip.File) bool { + if f.Name == MetaInf { + return true } + return false + }) - 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 - } + metaInfFile := epub.Reader.File[metaInfFileIdx] + doc := etree.NewDocument() + reader, err := metaInfFile.Open() + if err != nil { + fmt.Println(err) } - // Switch based on metadata file + if _, err := doc.ReadFrom(reader); err != nil { + _ = reader.Close() + panic(err) + } - switch FormatMetadataName[metadata.fileName] { + metadataFileName := doc.FindElement("//container/rootfiles/rootfile").SelectAttr("full-path").Value + metaFileIdx := slices.IndexFunc(epub.Reader.File, func(f *zip.File) bool { + if f.Name == metadataFileName { + return true + } + return false + }) + metadata := *epub.Reader.File[metaFileIdx] + + navigationFileName := FormatNavigationFile[FormatMetadataName[metadataFileName]] + navigationFileIdx := slices.IndexFunc(epub.Reader.File, func(f *zip.File) bool { + if f.Name == navigationFileName { + return true + } + return false + }) + navigation := *epub.Reader.File[navigationFileIdx] + + return FormatInfo{ + fType: FormatMetadataName[metadataFileName], + metadata: metadata, + navigation: navigation, + } +} + +func ProcessEpub(epub *zip.ReadCloser, epubData EpubInfo) { + // Switch based on metadata file + switch epubData.format.fType { case "manga": { comicInfoName := "ComicInfo.xml" - chapterList := GenerateMangaChapterList(navigation) + chapterList := GenerateMangaChapterList(epubData.format.navigation) if chapterList.Len() == 0 { fmt.Println("No chapters found, chapter name likely not supported") } - basePath := downloadDirectory + volume.Info.Title + titleSuffix + "/" + basePath := filepath.Dir(epubData.path) + "/" + epubData.title PrepareVolumeDirectory(basePath) - volume.Info, err = jnovel.FetchVolumeInfo(volume.Info) - if err != nil { - fmt.Println(err) - } doc := etree.NewDocument() - reader, err := metadata.file.Open() + reader, err := epubData.format.metadata.Open() if err != nil { fmt.Println(err) } @@ -384,7 +535,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc for ch := chapterList.Front(); ch != nil; ch = ch.Next() { chap := ch.Value.(Chapter) - zipPath := basePath + volume.Info.Title + " Chapter " + chap.chDisplay + ".cbz" + zipPath := basePath + "/" + epubData.title + " Chapter " + chap.chDisplay + ".cbz" if _, err = os.Stat(zipPath); err == nil { err := os.Remove(zipPath) @@ -444,7 +595,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc } } - comicInfo, err := GenerateChapterMetadata(volume, serie, len(chap.pages), language, chap.chDisplay, titleSuffix) + comicInfo, err := GenerateChapterMetadata(epubData, len(chap.pages), language, chap.chDisplay) if err != nil { fmt.Println(err) } @@ -459,7 +610,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc } epub.Close() - os.Remove(file) + os.Remove(epubData.path) } case "novel": { @@ -467,7 +618,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc CalibreSeriesIndexAttr := "calibre:series_index" EpubTitleTag := "//package/metadata" - metafile, err := metadata.file.Open() + metafile, err := epubData.format.metadata.Open() if err != nil { fmt.Println(err) } @@ -481,29 +632,18 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc 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) + seriesTitle.CreateAttr("content", epubData.series) seriesNumber := etree.NewElement("meta") seriesNumber.CreateAttr("name", CalibreSeriesIndexAttr) - seriesNumber.CreateAttr("content", number) + seriesNumber.CreateAttr("content", epubData.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) + zipfile, err := os.OpenFile(epubData.path+".new", os.O_RDWR|os.O_CREATE, 0644) if err != nil { panic(err) } @@ -521,7 +661,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc panic(err) } - metawriter, err := zipWriter.Create(metadata.fileName) + metawriter, err := zipWriter.Create(epubData.format.metadata.Name) if err != nil { panic(err) } @@ -532,7 +672,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc } for _, f := range epub.File { - if f.Name != metadata.fileName { + if f.Name != epubData.format.metadata.Name { writer, err := zipWriter.Create(f.Name) if err != nil { panic(err) @@ -552,68 +692,57 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc epub.Close() zipWriter.Close() - source, _ := os.Open(file + ".new") - dest, _ := os.Create(file) + source, _ := os.Open(epubData.path + ".new") + dest, _ := os.Create(epubData.path) io.Copy(dest, source) - os.Remove(file + ".new") + os.Remove(epubData.path + ".new") } } } -func GenerateChapterMetadata(volume jnc.VolumeAugmented, serie jnc.SerieAugmented, pageCount int, language string, chapterNumber string, titleSuffix string) ([]byte, error) { +func GenerateChapterMetadata(epubData EpubInfo, pageCount int, language string, chapterNumber 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.Series = epubData.info.Series + comicInfo.Title = epubData.title comicInfo.Number = chapterNumber - comicInfo.Volume = vInfo.Number + comicInfo.Volume = epubData.info.Volume comicInfo.Count = -1 // TODO somehow fetch actual completion status - comicInfo.Summary = vInfo.Description + comicInfo.Summary = epubData.info.Summary - 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() + comicInfo.Year = epubData.info.Year + comicInfo.Month = epubData.info.Month + comicInfo.Day = epubData.info.Day - if vInfo.Creators.Contains("AUTHOR") { - comicInfo.Writer = vInfo.Creators.Get("AUTHOR").Name + if epubData.info.Writer != "" { + comicInfo.Writer = epubData.info.Writer } - 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 epubData.info.Letterer != "" { + comicInfo.Letterer = epubData.info.Letterer } - 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 epubData.info.CoverArtist != "" { + comicInfo.CoverArtist = epubData.info.CoverArtist } - if vInfo.Creators.Contains("TRANSLATOR") { - comicInfo.Translator = vInfo.Creators.Get("TRANSLATOR").Name + if epubData.info.Translator != "" { + comicInfo.Translator = epubData.info.Translator } - if vInfo.Creators.Contains("EDITOR") { - comicInfo.Editor = vInfo.Creators.Get("EDITOR").Name + if epubData.info.Editor != "" { + comicInfo.Editor = epubData.info.Editor } - comicInfo.Publisher = "J-Novel Club" + comicInfo.Publisher = epubData.info.Publisher - comicInfo.Tags = strings.Join(sInfo.Tags, ",") + comicInfo.Tags = epubData.info.Tags comicInfo.PageCount = pageCount comicInfo.Language = language comicInfo.Format = "Comic" // JNC reports this as type in the epub file @@ -820,8 +949,8 @@ func GetLastValidChapterNumber(currentChapter *list.Element) Chapter { return currentChapter.Value.(Chapter) } -func GenerateMangaChapterList(navigation FileWrapper) *list.List { - reader, err := navigation.file.Open() +func GenerateMangaChapterList(navigation zip.File) *list.List { + reader, err := navigation.Open() if err != nil { fmt.Println(err) }