package main import ( "archive/zip" "bufio" "bytes" "container/list" "encoding/xml" "errors" "fmt" "github.com/beevik/etree" "io" "kavita-helper-go/jnc" "os" "os/exec" "path/filepath" "regexp" "slices" "strconv" "strings" "time" ) type Chapter struct { numberMain int8 numberSub int8 // uses 0 for regular main chapters chDisplay string title string firstPage string lastPage string pages []string } // 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 GetArg(argKey string) (string, error) { if slices.Contains(os.Args, argKey) { idx := slices.Index(os.Args, argKey) return os.Args[idx+1], nil } 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 } provider, _ := GetArg("-P") serLan, _ := GetArg("-L") if serLan != "" { serLan = " " + serLan } 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) } } } func RunJNCProvider(settings jnc.ProviderSettings) { // initialize the J-Novel Club Instance jnovel := jnc.NewJNC(settings.Domain) jnovel.SetUsername(settings.Username) jnovel.SetPassword(settings.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, settings.Directory, updatedOnly == "Y" || updatedOnly == "y", settings.Language) } } if mode == "3" || mode == "5" { if serie.Info.Type == "NOVEL" { HandleSeries(jnovel, serie, settings.Directory, updatedOnly == "Y" || updatedOnly == "y", settings.Language) } } } 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, settings.Directory, false, settings.Language) } 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, settings.Directory, settings.Language) } } } func HandleSeries(jnovel jnc.Api, serie jnc.SerieAugmented, downloadDir string, updatedOnly bool, titleSuffix string) { for v := range serie.Volumes { volume := serie.Volumes[v] if updatedOnly { if len(volume.Downloads) != 0 && volume.UpdateAvailable() { downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix) HandleVolume(jnovel, serie, volume, downloadDir, titleSuffix) } } else { downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix) HandleVolume(jnovel, serie, volume, downloadDir, titleSuffix) } } } func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string, titleSuffix 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+titleSuffix { 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 + titleSuffix } else { splits[len(splits)-1] = serie.Info.Title + part + titleSuffix splits = append(splits, "") } downloadDir = strings.Join(splits, "/") } else { downloadDir += serie.Info.Title + titleSuffix + "/" } _, 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, titleSuffix 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 } 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) } epubData.format = DetermineEpubFormat(epub) ProcessEpub(epub, epubData) } type EpubInfo struct { path string title string series string number string info ComicInfo format FormatInfo } type FormatInfo struct { fType string metadata zip.File navigation zip.File } 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" metaInfFileIdx := slices.IndexFunc(epub.Reader.File, func(f *zip.File) bool { if f.Name == MetaInf { return true } return false }) metaInfFile := epub.Reader.File[metaInfFileIdx] doc := etree.NewDocument() reader, err := metaInfFile.Open() if err != nil { fmt.Println(err) } if _, err := doc.ReadFrom(reader); err != nil { _ = reader.Close() panic(err) } 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(epubData.format.navigation) if chapterList.Len() == 0 { fmt.Println("No chapters found, chapter name likely not supported") } basePath := filepath.Dir(epubData.path) + "/" + epubData.title PrepareVolumeDirectory(basePath) doc := etree.NewDocument() reader, err := epubData.format.metadata.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) zipPath := basePath + "/" + epubData.title + " Chapter " + chap.chDisplay + ".cbz" if _, err = os.Stat(zipPath); err == nil { err := os.Remove(zipPath) if err != nil { return } } 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("%02d", chapterPageIndex+1) + "." + strings.Split(iParts[len(iParts)-1], ".")[1] err = addFileToZip(newZip, imageFile, fileName) if err != nil { fmt.Println(err) } } comicInfo, err := GenerateChapterMetadata(epubData, len(chap.pages), language, chap.chDisplay) if err != nil { fmt.Println(err) } err = addBytesToZip(newZip, comicInfo, comicInfoName) if err != nil { fmt.Println(err) } newZip.Close() newZipFile.Close() } epub.Close() os.Remove(epubData.path) } case "novel": { CalibreSeriesAttr := "calibre:series" CalibreSeriesIndexAttr := "calibre:series_index" EpubTitleTag := "//package/metadata" metafile, err := epubData.format.metadata.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) seriesTitle.CreateAttr("content", epubData.series) seriesNumber := etree.NewElement("meta") seriesNumber.CreateAttr("name", CalibreSeriesIndexAttr) 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(epubData.path+".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(epubData.format.metadata.Name) if err != nil { panic(err) } _, err = metawriter.Write([]byte(str)) if err != nil { panic(err) } for _, f := range epub.File { if f.Name != epubData.format.metadata.Name { 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(epubData.path + ".new") dest, _ := os.Create(epubData.path) io.Copy(dest, source) os.Remove(epubData.path + ".new") } } } 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", } comicInfo.Series = epubData.info.Series comicInfo.Title = epubData.title comicInfo.Number = chapterNumber comicInfo.Volume = epubData.info.Volume comicInfo.Count = -1 // TODO somehow fetch actual completion status comicInfo.Summary = epubData.info.Summary comicInfo.Year = epubData.info.Year comicInfo.Month = epubData.info.Month comicInfo.Day = epubData.info.Day if epubData.info.Writer != "" { comicInfo.Writer = epubData.info.Writer } if epubData.info.Letterer != "" { comicInfo.Letterer = epubData.info.Letterer } if epubData.info.CoverArtist != "" { comicInfo.CoverArtist = epubData.info.CoverArtist } if epubData.info.Translator != "" { comicInfo.Translator = epubData.info.Translator } if epubData.info.Editor != "" { comicInfo.Editor = epubData.info.Editor } comicInfo.Publisher = epubData.info.Publisher comicInfo.Tags = epubData.info.Tags comicInfo.PageCount = pageCount comicInfo.Language = language comicInfo.Format = "Comic" // JNC reports this as type in the epub file return xml.MarshalIndent(comicInfo, " ", " ") } type ComicInfo struct { XMLName string `xml:"ComicInfo"` XMLNS string `xml:"xmlns:xsi,attr"` XSI string `xml:"xsi:noNamespaceSchemaLocation,attr"` Series string `xml:"Series"` Title string `xml:"Title"` Volume int `xml:"Volume"` Number string `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") sortedChapters := list.New() var mainChapter Chapter for ch := chapters.Back(); ch != nil; ch = ch.Prev() { currentChapter := ch.Value.(Chapter) if currentChapter.numberMain != -1 { // 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) for sch := sortedChapters.Front(); sch != nil; sch = sch.Next() { if sch.Value.(Chapter).title == mainChapter.title { 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 } } } } 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 } } else { break } } return currentChapter.Value.(Chapter) } func GenerateMangaChapterList(navigation zip.File) *list.List { reader, err := navigation.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|Episode) ([0-9IVXLCDM]*)" match, _ := regexp.MatchString(regex, label) if match { r, _ := regexp.Compile(regex) num := r.FindStringSubmatch(label)[1] parse, _ := strconv.ParseInt(num, 10, 8) if int8(parse) == 0 { fmt.Printf("Unlikely Chapter Number Detected (0): '%s'\n", num) fmt.Println("Attempting Roman Numerals") lastMainChapter = int8(romanToInt(num)) } else { 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: "", pages: []string{}, } chapters.PushBack(chapterData) } return FinalizeChapters(chapters, pageList) } func romanToInt(s string) int { know := map[string]int{ "I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000, } lengthOfString := len(s) lastElement := s[len(s)-1 : lengthOfString] var result int result = know[lastElement] for i := len(s) - 1; i > 0; i-- { if know[s[i:i+1]] <= know[s[i-1:i]] { result += know[s[i-1:i]] } else { result -= know[s[i-1:i]] } } return result }