From 103292cd389f343ad8fb7bbdb849fbcef50e9d43 Mon Sep 17 00:00:00 2001 From: Marcell Mars Date: Sun, 30 Jul 2023 17:41:38 +0200 Subject: [PATCH] more of a xml/rdf/opf play.. --- cmd/build.go | 81 ++++++- go.mod | 1 + go.sum | 5 + pkg/calibre/calibre.go | 465 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 548 insertions(+), 4 deletions(-) create mode 100644 pkg/calibre/calibre.go diff --git a/cmd/build.go b/cmd/build.go index dc68913..0fbf9a5 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -1,12 +1,14 @@ package cmd import ( + "accorder/pkg/calibre" "encoding/xml" "fmt" "log" + "os" + "strconv" - "accorder/pkg/calibre" - + "github.com/araddon/dateparse" "github.com/beevik/etree" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -83,6 +85,8 @@ should be enough for the next successful build).`, root := doc.SelectElement("rdf:RDF") for _, attachmentNode := range root.FindElements("[name()='link:type']") { var zoteroItem ZoteroItem + var bookOpf calibre.BookOpfW + if attachmentNode.Text() != "application/pdf" { continue } @@ -91,17 +95,54 @@ should be enough for the next successful build).`, filePathElement := zoteroUnion.CreateElement("filePath") filePathQuery := attachmentNode.Parent().FindElement("[name()='rdf:resource']").SelectAttr("rdf:resource").Value filePathElement.CreateText(filePathQuery) + mimeTypeElement := zoteroUnion.CreateElement("mimeType") mimeType := attachmentNode.Text() mimeTypeElement.CreateText(mimeType) bibliographyNode := root.FindElement(fmt.Sprintf("[@rdf:resource='%s']", attachmentNode.Parent().SelectAttr("rdf:about").Value)).Parent().Copy() + // newDoc.AddChild(bibliographyNode) + // newDoc.WriteTo(os.Stdout) + + dateQuery := bibliographyNode.FindElement("[name()='dc:date']") + if dateQuery != nil { + dateElement := zoteroUnion.CreateElement("date") + date, err := dateparse.ParseAny(dateQuery.Text()) + if err == nil { + formattedDate := date.Format("2006-01-02") + dateElement.CreateText(formattedDate) + + bookOpf.Metadata.Published = formattedDate + } else { + newDateQuery := fmt.Sprintf("1 %s", dateQuery.Text()) + newDate, err := dateparse.ParseAny(newDateQuery) + if err == nil { + newFormattedDate := newDate.Format("2006-01-02") + dateElement.CreateText(newFormattedDate) + bookOpf.Metadata.Published = newFormattedDate + } else { + lastChanceDate := dateQuery.Text()[len(dateQuery.Text())-4:] + year, err := strconv.Atoi(lastChanceDate) + if err == nil { + justYear := fmt.Sprintf("%d-01-01", year) + dateElement.CreateText(justYear) + bookOpf.Metadata.Published = justYear + } else { + fmt.Println("ERROR parsing date...", err) + } + } + } + + } + publisherQuery := bibliographyNode.FindElement("[name()='foaf:name']") if publisherQuery != nil { publisherElement := zoteroUnion.CreateElement("publisher") publisher := publisherQuery.Text() publisherElement.CreateText(publisher) + + bookOpf.Metadata.Publisher = publisher } authorsQuery := bibliographyNode.FindElements("[name()='foaf:Person']") @@ -118,7 +159,13 @@ should be enough for the next successful build).`, if surNameNode != nil { surName = surNameNode.Text() } - author.CreateText(fmt.Sprintf("%s %s", firstName, surName)) + fullName := fmt.Sprintf("%s %s", firstName, surName) + author.CreateText(fullName) + + bookOpf.Metadata.Creators = append(bookOpf.Metadata.Creators, calibre.Creator{ + Role: "aut", + Name: fullName, + }) } titleQuery := bibliographyNode.FindElement("[name()='dc:title']") @@ -126,6 +173,8 @@ should be enough for the next successful build).`, titleNode := zoteroUnion.CreateElement("title") title := titleQuery.Text() titleNode.CreateText(title) + + bookOpf.Metadata.Title = title } descriptionQuery := bibliographyNode.FindElement("[name()='dcterms:abstract']") @@ -133,8 +182,13 @@ should be enough for the next successful build).`, descriptionNode := zoteroUnion.CreateElement("description") description := descriptionQuery.Text() descriptionNode.CreateText(description) + + bookOpf.Metadata.Description = description } + // newDoc.WriteTo(os.Stdout) + // fmt.Println("") + b, err := newDoc.WriteToBytes() if err != nil { log.Fatal(err) @@ -142,8 +196,27 @@ should be enough for the next successful build).`, if err := xml.Unmarshal(b, &zoteroItem); err != nil { log.Fatalln(err) } - fmt.Printf("\nZoteroItem: %#v\n", zoteroItem) + // fmt.Printf("\nZoteroItem: %#v\n", zoteroItem) + bookOpf.Version = "2.0" + bookOpf.Xmlns = "http://www.idpf.org/2007/opf" + bookOpf.UniqueIdentifier = "uuid_id" + bookOpf.Metadata.DC = "http://purl.org/dc/elements/1.1/" + bookOpf.Metadata.OPF = "http://www.idpf.org/2007/opf" + + bookOpf.Metadata.Identifiers = append(bookOpf.Metadata.Identifiers, calibre.Identifier{ + Scheme: "calibre", + Id: "calibre_id", + Value: "-1", + }) + + bookOpfOutput, err := xml.MarshalIndent(bookOpf, " ", " ") + if err != nil { + log.Fatalln(err) + } + // _ = bookOpfOutput + os.Stdout.Write(bookOpfOutput) + fmt.Println("") } calibre.RenderStandaloneApp(calibrePath, librarianName, libraryUUID, librarySecret, jsonPath) }, diff --git a/go.mod b/go.mod index 04609d9..8b9de67 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module accorder go 1.16 require ( + github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect github.com/beevik/etree v1.1.0 github.com/karrick/godirwalk v1.16.1 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f diff --git a/go.sum b/go.sum index 67bcdac..924c98b 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -343,6 +345,7 @@ github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -446,6 +449,7 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= @@ -460,6 +464,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= diff --git a/pkg/calibre/calibre.go b/pkg/calibre/calibre.go new file mode 100644 index 0000000..b646758 --- /dev/null +++ b/pkg/calibre/calibre.go @@ -0,0 +1,465 @@ +package calibre + +import ( + "crypto/hmac" + "crypto/md5" + "embed" + "encoding/hex" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "io/fs" + "log" + "math" + "os" + "path/filepath" + "sort" + "strings" + "sync" + + // "github.com/cespare/xxhash" + "github.com/karrick/godirwalk" + uuid "github.com/satori/go.uuid" +) + +var ( + //go:embed embResources + embResources embed.FS +) + +func c(err error, m string) { + if err != nil { + log.Fatal(err, m) + } +} + +type HugoCatalogItem struct { + Key string + Book *BookJSON +} + +type DataJs struct { + Portable bool `json:"portable"` + Total int `json:"total,omitempty"` + Books []*BookJSON `json:"books"` +} + +// type BooksJSON struct { +// Books []*BookJSON `json:"books"` +// } + +type IdentifierJSON struct { + Scheme string `json:"scheme"` + Code string `json:"code"` +} + +type FormatJSON struct { + Format string `json:"format"` + DirPath string `json:"dir_path"` + FileName string `json:"file_name"` + Size int64 `json:"size"` + XXHash uint64 `json:"xxhash,omitempty"` +} + +type BookJSON struct { + Id string `json:"_id"` + Librarian string `json:"librarian"` + LibraryUUID string `json:"library_uuid"` + Title string `json:"title"` + TitleSort string `json:"title_sort"` + Authors []string `json:"authors"` + Abstract string `json:"abstract"` + Tags []string `json:"tags"` + Publisher string `json:"publisher"` + Pubdate string `json:"pubdate"` + LastModified string `json:"last_modified"` + Languages []string `json:"languages"` + Identifiers []*IdentifierJSON `json:"identifiers"` + Formats []*FormatJSON `json:"formats"` + CoverUrl string `json:"cover_url"` +} + +type Creator struct { + Role string `xml:"opf:role,attr"` + Name string `xml:",chardata"` +} + +type Identifier struct { + Scheme string `xml:"opf:scheme,attr"` + Id string `xml:"id,attr"` // Calibre has two internal Ids: calibre_id and uuid_id + Value string `xml:",chardata"` +} + +type BookOpfW struct { + XMLName xml.Name `xml:"package"` + Version string `xml:"version,attr"` + Xmlns string `xml:"xmlns,attr"` + UniqueIdentifier string `xml:"unique-identifier,attr"` + Metadata struct { + DC string `xml:"xmlns:dc,attr"` + OPF string `xml:"xmlns:opf,attr"` + Identifiers []struct { + Scheme string `xml:"opf:scheme,attr"` + Id string `xml:"id,attr"` // Calibre has two internal Ids: calibre_id and uuid_id + Value string `xml:",chardata"` + } `xml:"dc:identifier"` + Title string `xml:"dc:title"` + Creators []struct { + Role string `xml:"opf:role,attr"` + Name string `xml:",chardata"` + } `xml:"dc:creator"` + Published string `xml:"dc:date"` + Description string `xml:"dc:description"` + Publisher string `xml:"dc:publisher"` + Languages []struct { + Language string `xml:",chardata"` + } `xml:"dc:language"` + Tags []struct { + Tag string `xml:",chardata"` + } `xml:"dc:subject"` + Meta []struct { + Content string `xml:"content,attr"` + Name string `xml:"name,attr"` + } `xml:"meta"` + } `xml:"metadata"` +} + +type BookOpf struct { + XMLName xml.Name `xml:"package"` + Version string `xml:"version,attr"` + Xmlns string `xml:"xmlns,attr"` + UniqueIdentifier string `xml:"unique-identifier,attr"` + Metadata struct { + DC string `xml:"dc,attr"` + OPF string `xml:"opf,attr"` + Identifiers []struct { + Scheme string `xml:"scheme,attr"` + Id string `xml:"id,attr"` // Calibre has two internal Ids: calibre_id and uuid_id + Value string `xml:",chardata"` + } `xml:"identifier"` + Title string `xml:"title"` + Creators []struct { + Role string `xml:"role,attr"` + Name string `xml:",chardata"` + } `xml:"creator"` + Published string `xml:"date"` + Description string `xml:"description"` + Publisher string `xml:"publisher"` + Languages []struct { + Language string `xml:",chardata"` + } `xml:"language"` + Tags []struct { + Tag string `xml:",chardata"` + } `xml:"subject"` + Meta []struct { + Content string `xml:"content,attr"` + Name string `xml:"name,attr"` + } `xml:"meta"` + } `xml:"metadata"` +} + +// TitleSort returns if Calibre processed Title for sorting order +func (book BookOpf) TitleSort() string { + for _, meta := range book.Metadata.Meta { + if meta.Name == "calibre:title_sort" { + return meta.Content + } + } + return "" +} + +// LastModified returns when Calibre last time touched the file +func (book BookOpf) LastModified() string { + lastModified := "" + for _, meta := range book.Metadata.Meta { + if meta.Name == "calibre:timestamp" { + lastModified = meta.Content + } + } + return lastModified +} + +// Authors parses creators and give back authors +func (book BookOpf) Authors() []string { + authors := []string{} + if len(book.Metadata.Creators) > 0 { + for _, author := range book.Metadata.Creators { + if author.Role == "aut" { + authors = append(authors, author.Name) + } + } + } + return authors +} + +// Tags gets a list of tags from Subject nodes in OPF +func (book BookOpf) Tags() []string { + tags := []string{} + for _, tag := range book.Metadata.Tags { + tags = append(tags, tag.Tag) + } + return tags +} + +// Languages gets a list of languages +func (book BookOpf) Languages() []string { + languages := []string{} + for _, l := range book.Metadata.Languages { + languages = append(languages, l.Language) + } + return languages +} + +// Identifiers returns list of identifiers like ISBN, DOI, Google, Amazon... +func (book BookOpf) Identifiers() []*IdentifierJSON { + identifiers := []*IdentifierJSON{} + for _, i := range book.Metadata.Identifiers { + if i.Id == "" { + identifiers = append(identifiers, &IdentifierJSON{ + Scheme: strings.ToLower(i.Scheme), + Code: i.Value, + }) + } + } + return identifiers +} + +// Uuid returns calibre's book uuid... +func (book BookOpf) Uuid(librarySecret string) string { + for _, i := range book.Metadata.Identifiers { + if i.Id == "uuid_id" { + h := hmac.New(md5.New, []byte(librarySecret)) + h.Write([]byte(i.Value)) + + sha := hex.EncodeToString(h.Sum(nil)) + u, err := uuid.FromString(sha) + if err != nil { + fmt.Println(err) + } + return u.String() + } + } + return "" +} + +// Formats return list of files and info on them +func Formats(calibrePath, relativeDirPath string) []*FormatJSON { + path := filepath.Join(calibrePath, relativeDirPath) + if strings.HasSuffix(path, "/") != true { + path = path + "/" + } + formats := []*FormatJSON{} + files, err := os.ReadDir(path) + c(err, "list of formats...") + + for _, f := range files { + if f.Name() != "metadata.opf" && f.Name() != "cover.jpg" { + fi, err := f.Info() + c(err, "file info in Formats") + x, err := os.Open(filepath.Join(path, f.Name())) + c(err, "open file in Formats") + defer x.Close() + // xxHash := xxhash.New() + // _, err = io.Copy(xxHash, x) + // c(err, "io.Copy xxHash in Formats") + formats = append(formats, &FormatJSON{ + Format: strings.ReplaceAll(filepath.Ext(f.Name()), ".", ""), + DirPath: relativeDirPath, + FileName: f.Name(), + Size: fi.Size(), + // XXHash: xxHash.Sum64(), + }) + } + } + return formats +} + +func lsEmbResources() { + _ = fs.WalkDir(embResources, ".", func(path string, d fs.DirEntry, err error) error { + c(err, "") + // fmt.Println(" ", path) + _ = path + return nil + }) +} + +func copyEmbResources(destPath string) { + _ = fs.WalkDir(embResources, ".", func(fsPath string, fsEntry fs.DirEntry, err error) error { + destFsPath := strings.ReplaceAll(fsPath, "embResources/", "") + if fsPath == "." || fsPath == "embResources" { + return nil + } else if fsEntry.IsDir() { + err := os.MkdirAll(filepath.Join(destPath, destFsPath), 0755) + c(err, "make new directory at the destination...") + } else { + newFile, err := os.Create(filepath.Join(destPath, destFsPath)) + c(err, "create new file at the destination..") + defer newFile.Close() + + embFile, err := embResources.Open(fsPath) + _, err = io.Copy(newFile, embFile) + c(err, "copy embResources/ file into the new file at the destination...") + } + return nil + }) +} + +func writeDataJs(calibrePath string, books []*BookJSON) { + // j, _ := json.MarshalIndent(&BooksJSON{books}) + dataJsFirst, err := os.Create(filepath.Join(calibrePath, "static", "data1.js")) + c(err, "create dataJsFirst file...") + defer dataJsFirst.Close() + endB := 24 + if endB >= len(books) { + endB = len(books) + } + j1, _ := json.Marshal(&DataJs{ + Portable: true, + Total: len(books), + Books: books[0:endB], + }) + calibreBooks1 := []byte("CALIBRE_BOOKS1=") + _, _ = dataJsFirst.Write(append(calibreBooks1, j1...)) + + // [24:120], [120:216], [216:3288], [3288:6360], [6360:9432], [9432:12504] + block := endB + counter := 2 + for i := range [2]int{} { + ii := make([]int, int(math.Pow(2, float64(i+1)))) + for j := range ii { + _ = j + + var jsn []byte + + endBlock := block + int(math.Pow(32, float64(i+1)))*3 + + if endBlock > len(books) { + endBlock = len(books) + } + + dataJs, err := os.Create(filepath.Join(calibrePath, "static", fmt.Sprintf("data%d.js", counter))) + c(err, "create dataJs file...") + defer dataJs.Close() + + if endBlock <= len(books) { + jsn, _ = json.Marshal(&DataJs{ + Portable: true, + Books: books[block:endBlock], + }) + } else { + jsn, _ = json.Marshal(&DataJs{ + Portable: true, + Books: []*BookJSON{}, + }) + } + calibreBooks := []byte(fmt.Sprintf("CALIBRE_BOOKS%d=", counter)) + _, _ = dataJs.Write(append(calibreBooks, jsn...)) + block = endBlock + counter++ + } + } + dataJsLast, err := os.Create(filepath.Join(calibrePath, "static", "data8.js")) + c(err, "create dataJsLast file...") + defer dataJsLast.Close() + j8, _ := json.Marshal(&DataJs{ + Portable: true, + Total: len(books), + Books: books[block:], + }) + calibreBooks8 := []byte("CALIBRE_BOOKS8=") + _, _ = dataJsLast.Write(append(calibreBooks8, j8...)) + +} + +func PrintExePath() { + fmt.Println(os.Executable()) +} + +func RenderStandaloneApp(calibrePath, librarianName, libraryUUID, librarySecret, jsonPath string) { + var outputs sync.Map + var wg sync.WaitGroup + count := 0 + err := godirwalk.Walk(calibrePath, &godirwalk.Options{ + Callback: func(path string, info *godirwalk.Dirent) error { + if info.Name() == "metadata.opf" { + bookOpf := &BookOpf{} + count++ + wg.Add(1) + go func(bookOpf *BookOpf) { + // randSuffix := uuid.New() + // randSuffix := uuid.NewV4() + relativeDirPath := strings.NewReplacer(calibrePath, "", "metadata.opf", "").Replace(path) + f, _ := os.ReadFile(path) + _ = xml.Unmarshal([]byte(f), &bookOpf) + + book := &BookJSON{ + // Id: randSuffix.String(), + Id: bookOpf.Uuid(librarySecret), + Title: bookOpf.Metadata.Title, + Librarian: librarianName, + // LibraryUUID: randSuffix.String(), + LibraryUUID: libraryUUID, + TitleSort: bookOpf.TitleSort(), + Authors: bookOpf.Authors(), + Pubdate: bookOpf.Metadata.Published, + LastModified: bookOpf.LastModified(), + Tags: bookOpf.Tags(), + Publisher: bookOpf.Metadata.Publisher, + Abstract: bookOpf.Metadata.Description, + Languages: bookOpf.Languages(), + Identifiers: bookOpf.Identifiers(), + Formats: Formats(calibrePath, relativeDirPath), + CoverUrl: filepath.Join(relativeDirPath, "cover.jpg"), + } + // outputs.Store(fmt.Sprintf("%s_%s", book.LastModified, randSuffix.String()), book) + outputs.Store(fmt.Sprintf("%s_%s", book.LastModified, uuid.NewV4().String()), book) + wg.Done() + }(bookOpf) + } + if count > 100 { + wg.Wait() + count = 0 + } + return nil + }, + ErrorCallback: func(path string, err error) godirwalk.ErrorAction { + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + return godirwalk.SkipNode + }, + Unsorted: true}) + wg.Wait() + + c(err, "") + + var keysByDate []string + outputs.Range(func(k, v interface{}) bool { + keysByDate = append(keysByDate, k.(string)) + return true + }) + sort.Sort(sort.Reverse(sort.StringSlice(keysByDate))) + + var books []*BookJSON + for _, keyByDate := range keysByDate { + b, _ := outputs.Load(keyByDate) + books = append(books, b.(*BookJSON)) + } + + lsEmbResources() + copyEmbResources(calibrePath) + writeDataJs(calibrePath, books) + + hugoCatalog := map[string]*BookJSON{} + + for _, book := range books { + hugoCatalog[book.Id] = book + } + + if jsonPath != "" { + jsonDump, _ := json.Marshal(hugoCatalog) + // _ = j + err = os.WriteFile(jsonPath, jsonDump, 0666) + c(err, "writing json file failed...") + } + fmt.Printf("Check out: %sBROWSE_LIBRARY.html", calibrePath) +}