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) }