initial commit...

This commit is contained in:
educative 2021-10-31 01:25:44 +02:00 committed by Marcell Mars
commit 968e86e8b1
30 changed files with 2160 additions and 0 deletions

0
LICENSE Normal file
View File

78
cmd/build.go Normal file
View File

@ -0,0 +1,78 @@
package cmd
import (
"fmt"
"accorder/pkg/calibre"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var buildCmd = &cobra.Command{
Use: "build",
Short: "Build standalone, portable webapp from Calibre library.",
Long: `Build searchable, standalone, portable webapp against the local Calibre
library including all the metadata needed. It creates BROWSE_LIBRARY.html in
root directory of the local Calibre library. For search (authors, titles,
tags...) it uses rendered metadata from static/data{1-8}.js files.
Every time the directory path and/or librarian is provided it is saved in
the configuration file for the future use (therefore: 'accorder build SESSION'
should be enough for the next successful build).`,
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
session := args[0]
for vipFlag, cliFlag := range map[string]string{
"librarian_name": "librarian",
"local_upload": "directory",
"library_uuid": "library-uuid",
"library_secret": "library-secret",
"server_upload": "server",
"bucket_upload": "bucket",
} {
viper.BindPFlag(fmt.Sprintf("%s.%s", session, vipFlag), cmd.Flags().Lookup(cliFlag))
}
},
Run: func(cmd *cobra.Command, args []string) {
session := args[0]
MissingRequiredFlags(
map[string]string{
"librarian_name": "librarian",
"local_upload": "directory",
},
session,
cmd,
)
libraryUUID := ViperValue(session, "library_uuid")
librarySecret := ViperValue(session, "library_secret")
calibrePath := ViperValue(session, "local_upload")
librarianName := ViperValue(session, "librarian_name")
jsonPath := CliFlagValue(cmd, "jsonpath")
calibre.RenderStandaloneApp(calibrePath, librarianName, libraryUUID, librarySecret, jsonPath)
},
}
func init() {
rootCmd.AddCommand(buildCmd)
buildCmd.Flags().StringP("directory", "d", "", "A local Calibre directory.")
buildCmd.Flags().StringP("librarian", "l", "", "Librarian's name.")
buildCmd.Flags().StringP("jsonpath", "j", "", "Path where to render all metadata into JSON.")
buildCmd.Flags().StringP("import-bibtex", "i", "", "Import books from BibTex file into Calibre.")
buildCmd.Flags().StringP("library-uuid", "u", "", "A library's UUID used if part of MotW.")
buildCmd.Flags().StringP("library-secret", "p", "", "A password used if part of MotW.")
buildCmd.Flags().StringP("server", "s", "minio.memoryoftheworld.org", "Server.")
buildCmd.Flags().StringP("bucket", "b", "", "Server.")
CustomHelpOutput(buildCmd)
buildCmd.Flags().MarkHidden("library-uuid")
buildCmd.Flags().MarkHidden("library-secret")
buildCmd.Flags().MarkHidden("server")
buildCmd.Flags().MarkHidden("bucket")
}

86
cmd/commoners.go Normal file
View File

@ -0,0 +1,86 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/kirsle/configdir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"path/filepath"
)
func MissingRequiredFlags(flags map[string]string, session string, cmd *cobra.Command) {
exit := false
for vipFlag, cliFlag := range flags {
if ViperValue(session, vipFlag) == "" {
fmt.Printf("ERROR: A flag --%s is missing for SESSION `%s` to work.\n", cliFlag, session)
exit = true
}
}
if exit {
fmt.Println("~ ~ ~ ~")
cmd.Usage()
os.Exit(1)
}
if cmd.Flags().NFlag() > 0 {
viper.WriteConfig()
}
}
func CustomHelpOutput(cmd *cobra.Command) {
cmd.Flags().SortFlags = false
cmd.SetHelpTemplate(HelpTemplate)
cmd.SetUsageTemplate(UsageTemplate)
}
func ViperSettingsPrint() {
vipAll, err := json.MarshalIndent(viper.AllSettings(), "", " ")
if err != nil {
fmt.Println("error:", err)
}
fmt.Print(string(vipAll))
}
func CliFlagValue(cmd *cobra.Command, key string) (value string) {
value, _ = cmd.Flags().GetString(key)
return
}
func ViperValue(session, key string) string {
return viper.GetString(fmt.Sprintf("%s.%s", session, key))
}
func CheckProfile(cmd *cobra.Command, args []string) error {
hasProfile := false
// fmt.Println("VIPER:", viper.AllSettings())
for k, v := range viper.AllSettings() {
fmt.Println("VIPER KEY/VALUE:", k, v)
if k == args[0] {
hasProfile = true
}
}
if !hasProfile {
return fmt.Errorf("PROFILE %s should be added to the configuration file.\n", args[0])
}
return fmt.Errorf("MEH! %s should be added to the configuration file.\n", args[0])
}
func ConfigBaseDir() string {
return configdir.LocalConfig("akkorder")
}
func ConfigMinioDir() string {
return filepath.Join(ConfigBaseDir(), "minio")
}
func initConfigPaths() {
err := configdir.MakePath(ConfigBaseDir()) // Ensure it exists.
if err != nil {
panic(err)
}
os.MkdirAll(filepath.Join(ConfigBaseDir(), "minio"), 0777)
}

104
cmd/helptemplates.go Normal file
View File

@ -0,0 +1,104 @@
package cmd
const HelpTemplate = `Command usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}{{end}} PROFILE {{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available commands:{{range .Commands}}{{if and (ne .Name "completion") .IsAvailableCommand}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} 'command' --help" for more information about a command.{{end}}
~~~ About command '{{.Name}}' ~~~
{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}{{end}}
`
const RootHelpTemplate = `Usage:
{{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} PROFILE {{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available commands:{{range .Commands}}{{if and (ne .Name "completion") .IsAvailableCommand}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} 'command' --help" for more information about a command.{{end}}
~~~ About {{.Name}} ~~~{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}{{end}}
`
const RootUsageTemplate = `Usage:
{{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} PROFILE {{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available commands:{{range .Commands}}{{if and (ne .Name "completion") .IsAvailableCommand}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`
const UsageTemplate = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}{{end}} PROFILE {{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
Available commands:{{range .Commands}}{{if and (ne .Name "completion") .IsAvailableCommand}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`

30
cmd/mc.go Normal file
View File

@ -0,0 +1,30 @@
/*
Copyright © 2021
*/
package cmd
import (
miniocmd "github.com/minio/mc/cmd"
"github.com/spf13/cobra"
"os"
)
// mcCmd represents the mc command
var mcCmd = &cobra.Command{
Use: "mc",
Short: "A proxy for `mc` minio client responsible for upload & download`.",
Run: func(cmd *cobra.Command, args []string) {
// rgs := []string{"", "-C", ConfigMinioDir()}
// rgs := []string{"", "-C", ConfigMinioDir()}
os.Setenv("MC_HOST_minio.memoryoftheworld.org", "https://klemo:U9@?x$)Kdoq15)J~@minio.memoryoftheworld.org")
rgs := []string{"", "-C", ConfigMinioDir(), "ls", "minio.memoryoftheworld.org/klemo"}
rgs = append(rgs, args...)
miniocmd.Main(rgs)
},
}
func init() {
mcCmd.DisableFlagParsing = true
rootCmd.AddCommand(mcCmd)
}

36
cmd/release.go Normal file
View File

@ -0,0 +1,36 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// releaseCmd represents the release command
var releaseCmd = &cobra.Command{
Use: "release",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("release called")
},
}
func init() {
rootCmd.AddCommand(releaseCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// releaseCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// releaseCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

80
cmd/root.go Normal file
View File

@ -0,0 +1,80 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "accorder command",
Short: "Accorder helps amateur librarians stay in accordance with MotW",
Long: `
Accorder takes care of various tasks which Memory of the World amateur
librarians do in order to maintain their shared catalogs online.
It builds searchable, standalone, portable webapp which one could then
just copy to USB disk and open BROWSE_LIBRARY.html in her web browser.
It uploads all of the books and metadata from local Calibre's library
(together with portable webapp) to the server.
It helps a librarian to maintain and share her catalog at
https://library.memoryoftheworld.org
together with other amateur librarians.
It does all of above in one go by typing: accorder release SESSION.
The configuration file will keep information about one or more SESSION.
Good name for SESSION is the one which reminds you quickly on what
SESSION would do.
Under every SESSION's configuration section there will be information
about the directory path of local Calibre's library, librarian's name,
credentials needed to upload/download the files to the destination
server etc.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := initConfig(args); err != nil {
fmt.Println("ERROR:", err)
os.Exit(1)
}
return nil
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
// cobra.OnInitialize(initConfig)
rootCmd.SetHelpTemplate(RootHelpTemplate)
rootCmd.SetUsageTemplate(RootUsageTemplate)
}
// initConfig reads in config file and ENV variables if set.
func initConfig(args []string) error {
initConfigPaths()
viper.AddConfigPath(ConfigBaseDir())
viper.SetConfigName("config")
// viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("ERROR:%s with config:%s", err, viper.ConfigFileUsed())
}
return nil
}

26
cmd/submit.go Normal file
View File

@ -0,0 +1,26 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// submitCmd represents the submit command
var submitCmd = &cobra.Command{
Use: "submit",
Short: "Submit metadata to the aggregated MotW Library.",
Long: `Submit all the library's metadata to the aggregated Memory of the World
Library (https://library.memoryoftheworld.org).`,
// Args: OnlyProfileArgument,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(args)
},
}
func init() {
rootCmd.AddCommand(submitCmd)
submitCmd.PersistentFlags().StringP("librarian", "l", "", "Librarian's name.")
submitCmd.PersistentFlags().BoolP("remove-library", "", false, "Remove PROFILE's library from the MotW Library.")
CustomHelpOutput(submitCmd)
}

101
cmd/upload.go Normal file
View File

@ -0,0 +1,101 @@
package cmd
import (
"fmt"
"os"
miniocmd "github.com/minio/mc/cmd"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// uploadCmd represents the upload command
var uploadCmd = &cobra.Command{
Use: "upload",
Short: "Upload local Calibre library to the MotW server.",
Long: `Upload local Calibre library to the Memory of the World server.
It will take care of the differences so files already at the server
will not be uploaded again.
Every time the directory path and/or librarian is provided it is saved in
configuration file for the future use (therefore: 'accorder upload SESSION'
should be enough for the next successful upload).`,
Args: cobra.ExactArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
session := args[0]
for vipFlag, cliFlag := range map[string]string{
"server_upload": "server",
"library_uuid": "username",
"library_secret": "password",
"bucket_upload": "bucket",
"local_upload": "directory",
"librarian_name": "librarian",
} {
viper.BindPFlag(fmt.Sprintf("%s.%s", session, vipFlag), cmd.Flags().Lookup(cliFlag))
}
},
Run: func(cmd *cobra.Command, args []string) {
session := args[0]
MissingRequiredFlags(
map[string]string{
"server_upload": "server",
"library_uuid": "username",
"library_secret": "password",
"bucket_upload": "bucket",
"local_upload": "directory",
},
session,
cmd,
)
server := ViperValue(session, "server_upload")
// username in upload context comes from library_uuid
username := ViperValue(session, "library_uuid")
// password in upload context comes from library_secret
password := ViperValue(session, "library_secret")
bucket := ViperValue(session, "bucket_upload")
localDirectory := ViperValue(session, "local_upload")
deleteResidue, _ := cmd.PersistentFlags().GetBool("delete-residue")
verbose, _ := cmd.PersistentFlags().GetBool("verbose")
os.Setenv(
fmt.Sprintf("MC_HOST_%s", server),
fmt.Sprintf("https://%s:%s@%s", username, password, server),
)
rgs := []string{"",
"-C", ConfigMinioDir(),
"mirror", "--overwrite",
}
if deleteResidue {
rgs = append(rgs, "--remove")
}
if verbose {
rgs = append(rgs, "--json")
}
rgs = append(rgs, localDirectory, fmt.Sprintf("%s/%s", server, bucket))
miniocmd.Main(rgs)
},
}
func init() {
rootCmd.AddCommand(uploadCmd)
uploadCmd.Flags().StringP("directory", "d", "", "A local directory to be uploaded.")
uploadCmd.Flags().StringP("username", "u", "", "Username.")
uploadCmd.Flags().StringP("password", "p", "", "Password.")
uploadCmd.Flags().StringP("bucket", "b", "", "A remote directory/bucket where to upload.")
uploadCmd.Flags().StringP("server", "s", "minio.memoryoftheworld.org", "Server.")
uploadCmd.Flags().BoolP("delete-residue", "", false, "Delete any remote files not present locally anymore.")
uploadCmd.Flags().BoolP("verbose", "v", false, "Verbose log.")
uploadCmd.Flags().StringP("librarian", "l", "", "Librarian's name.")
CustomHelpOutput(uploadCmd)
uploadCmd.Flags().MarkHidden("librarian")
}

12
go.mod Normal file
View File

@ -0,0 +1,12 @@
module accorder
go 1.16
require (
github.com/karrick/godirwalk v1.16.1
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/minio/mc v0.0.0-20211116163708-d0c62eb584e5
github.com/satori/go.uuid v1.2.0
github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.9.0
)

1007
go.sum Normal file

File diff suppressed because it is too large Load Diff

7
main.go Normal file
View File

@ -0,0 +1,7 @@
package main
import "accorder/cmd"
func main() {
cmd.Execute()
}

426
pkg/calibre/calibre.go Normal file
View File

@ -0,0 +1,426 @@
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 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 {
XMLName xml.Name `xml:"metadata"`
DC string `xml:"dc,attr"`
OPF string `xml:"opf,attr"`
Identifiers []struct {
XMLName xml.Name `xml:"identifier"`
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 {
XMLName xml.Name `xml:"creator"`
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 {
XMLName xml.Name `xml:"language"`
Language string `xml:",chardata"`
} `xml:"language"`
Tags []struct {
XMLName xml.Name `xml:"subject"`
Tag string `xml:",chardata"`
} `xml:"subject"`
Meta []struct {
XMLName xml.Name `xml:"meta"`
Content string `xml:"content,attr"`
Name string `xml:"name,attr"`
} `xml:"meta"`
}
}
// 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)
}

View File

@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<title>Memory of the World Library</title>
<link rel="apple-touch-icon" sizes="180x180" href="static/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/favicons/favicon-16x16.png">
<link rel="mask-icon" href="static/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel='stylesheet' href='static/css/bundle.css'>
<script rel="prefetch" src="static/data1.js"></script>
<script defer src='static/js/bundle.js'></script>
</head>
<body class="pl-2 pr-2 monocle:p-0 phone:p-0">
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
CALIBRE_BOOKS1={"portable":false}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,106 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1374.000000pt" height="1374.000000pt" viewBox="0 0 1374.000000 1374.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,1374.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M6869 11514 c-24 -18 -49 -33 -56 -33 -6 -1 -13 -4 -15 -8 -1 -5 -48
-37 -103 -73 -55 -36 -102 -68 -105 -71 -3 -3 -9 -8 -15 -10 -20 -8 -280 -184
-283 -191 -2 -5 -8 -8 -13 -8 -6 0 -15 -5 -22 -10 -14 -12 -155 -105 -203
-135 -19 -11 -42 -28 -53 -37 -11 -9 -22 -14 -25 -11 -3 3 -8 -2 -12 -11 -3
-9 -10 -16 -16 -16 -5 0 -35 -17 -66 -39 -31 -21 -61 -39 -66 -39 -5 0 -12 -4
-15 -9 -10 -14 -79 -63 -89 -63 -5 0 -17 -9 -27 -20 -10 -11 -22 -20 -26 -20
-10 0 -133 -83 -137 -92 -2 -5 -7 -8 -12 -8 -10 0 -134 -82 -138 -92 -2 -4 -8
-8 -13 -8 -11 0 -133 -81 -137 -92 -2 -5 -7 -8 -12 -8 -8 0 -374 -239 -397
-259 -7 -6 -16 -11 -20 -11 -5 0 -20 -11 -35 -25 -15 -14 -31 -25 -36 -25 -4
0 -17 -7 -27 -15 -11 -9 -101 -69 -200 -135 -99 -65 -184 -122 -190 -127 -5
-4 -22 -14 -37 -22 -16 -8 -28 -18 -28 -23 0 -4 -5 -8 -10 -8 -6 0 -33 -16
-60 -35 -27 -19 -54 -35 -59 -35 -5 0 -11 -3 -13 -7 -3 -8 -174 -124 -193
-132 -5 -2 -33 -21 -62 -43 -29 -21 -55 -38 -58 -38 -3 0 -27 -16 -54 -35 -26
-19 -53 -35 -59 -35 -6 0 -12 -4 -14 -8 -4 -11 -155 -112 -167 -112 -5 0 -11
-3 -13 -7 -5 -12 -95 -73 -107 -73 -6 0 -11 -4 -11 -8 0 -5 -12 -15 -27 -23
-16 -8 -30 -16 -33 -20 -6 -6 -389 -261 -405 -269 -5 -3 -51 -33 -102 -67 -51
-35 -97 -63 -103 -63 -5 0 -10 -4 -10 -8 0 -4 -22 -21 -50 -37 -27 -17 -50
-33 -50 -37 0 -5 -7 -8 -15 -8 -8 0 -15 -4 -15 -10 0 -5 -4 -10 -10 -10 -5 0
-32 -16 -59 -35 -26 -19 -53 -35 -59 -35 -6 0 -12 -3 -14 -8 -1 -4 -50 -38
-108 -77 -58 -38 -108 -74 -112 -80 -5 -6 -8 -6 -8 0 0 6 -6 3 -13 -6 -7 -9
-22 -19 -35 -23 -12 -4 -22 -11 -22 -15 0 -5 -13 -14 -30 -21 -16 -7 -30 -16
-30 -20 0 -4 -16 -15 -35 -24 -19 -9 -35 -21 -35 -27 0 -6 -5 -7 -10 -4 -6 3
-10 1 -10 -4 0 -6 -4 -11 -9 -11 -6 0 -31 -15 -56 -32 -26 -18 -54 -38 -63
-43 -37 -22 -77 -50 -82 -56 -3 -3 -16 -12 -30 -19 -14 -7 -42 -26 -62 -42
-21 -16 -38 -27 -38 -23 0 3 -7 0 -15 -7 -33 -30 -45 -38 -54 -38 -5 0 -16 -6
-23 -13 -17 -17 -488 -330 -510 -339 -9 -4 -37 -26 -61 -50 -74 -74 -100 -190
-98 -438 1 -118 4 -169 16 -285 4 -27 8 -69 10 -93 3 -24 7 -56 11 -70 5 -27
11 -64 18 -112 2 -14 7 -41 11 -60 4 -19 9 -45 11 -57 1 -11 5 -30 8 -41 4
-11 8 -29 10 -39 10 -48 39 -164 56 -223 11 -36 22 -76 24 -90 3 -14 5 -25 6
-25 1 0 3 -7 5 -15 4 -17 62 -187 72 -210 3 -8 7 -17 7 -20 1 -3 7 -15 14 -27
7 -13 10 -23 7 -23 -3 0 6 -20 19 -45 14 -25 23 -45 20 -45 -7 0 64 -136 98
-187 15 -23 28 -45 28 -49 0 -4 7 -14 15 -23 8 -9 29 -32 47 -52 18 -20 51
-49 73 -65 22 -16 49 -37 59 -46 11 -10 26 -18 33 -18 7 0 13 -4 13 -10 0 -5
7 -10 15 -10 8 0 15 -4 15 -9 0 -5 20 -21 44 -35 25 -14 51 -32 58 -39 7 -7
47 -36 88 -65 41 -28 98 -67 125 -87 28 -20 68 -48 90 -63 22 -15 41 -30 43
-34 2 -5 10 -8 18 -8 8 0 14 -3 14 -8 0 -4 18 -18 40 -32 22 -14 40 -28 40
-32 0 -5 6 -8 13 -8 8 0 22 -9 32 -20 10 -11 22 -20 27 -20 5 0 14 -5 21 -10
7 -6 48 -36 92 -67 44 -32 82 -60 85 -63 3 -3 12 -9 21 -15 10 -5 37 -24 60
-41 24 -16 76 -52 114 -79 39 -27 76 -55 83 -61 6 -7 21 -16 32 -19 11 -3 20
-10 20 -14 0 -4 18 -17 40 -30 22 -12 40 -26 40 -32 0 -5 5 -9 11 -9 6 0 28
-14 49 -30 21 -17 42 -30 48 -30 6 0 12 -3 14 -7 4 -9 174 -133 182 -133 3 0
14 -7 24 -16 9 -8 60 -45 112 -82 52 -37 97 -70 98 -74 2 -5 9 -8 15 -8 7 0
24 -11 39 -25 15 -14 31 -25 35 -25 4 0 19 -9 33 -20 63 -51 120 -90 129 -90
5 0 11 -3 13 -7 5 -11 147 -113 157 -113 5 0 11 -4 13 -8 1 -5 26 -23 53 -42
28 -18 52 -36 55 -39 12 -14 70 -51 80 -51 5 0 10 -3 10 -8 0 -4 18 -18 40
-32 22 -14 40 -28 40 -32 0 -5 7 -8 15 -8 9 0 18 -7 21 -15 4 -8 10 -15 15
-15 8 0 113 -71 144 -98 6 -5 24 -17 40 -27 17 -11 56 -39 87 -62 31 -24 59
-43 62 -43 3 0 14 -6 23 -14 39 -31 113 -86 118 -86 4 0 132 -90 180 -127 6
-4 33 -24 60 -43 28 -19 52 -38 53 -42 2 -4 8 -8 13 -8 9 0 67 -38 79 -51 3
-3 28 -21 55 -39 28 -19 52 -37 53 -42 2 -4 10 -8 18 -8 8 0 14 -4 14 -10 0
-5 6 -10 14 -10 8 0 16 -4 18 -8 2 -5 26 -23 53 -42 28 -18 52 -36 55 -39 12
-14 70 -51 80 -51 5 0 10 -3 10 -7 0 -5 25 -24 55 -43 30 -19 54 -38 55 -42 0
-5 5 -8 11 -8 11 0 92 -60 97 -72 2 -5 10 -8 18 -8 8 0 14 -4 14 -10 0 -5 7
-10 15 -10 8 0 15 -4 15 -10 0 -5 7 -10 15 -10 8 0 15 -4 15 -10 0 -5 6 -10
14 -10 8 0 16 -3 18 -8 4 -9 202 -150 223 -158 15 -6 52 13 84 44 9 9 31 28
49 42 18 14 45 36 61 50 57 52 91 80 96 80 3 0 16 10 28 23 12 12 47 43 77 67
67 55 95 79 124 105 12 11 41 35 64 53 22 19 50 41 60 50 11 10 34 28 52 42
17 13 47 38 65 55 18 16 50 44 70 60 21 17 47 39 59 50 12 11 45 38 72 60 27
22 65 54 84 72 19 18 41 33 48 33 6 0 12 7 12 15 0 8 4 15 8 15 5 0 18 8 30
17 41 35 108 91 187 158 26 22 58 50 71 63 13 12 25 22 28 22 6 0 44 33 91 78
13 12 27 22 30 22 3 0 13 8 23 17 10 10 40 36 67 58 26 22 59 49 71 60 12 11
39 34 60 50 20 17 44 37 53 46 9 8 39 33 66 55 28 21 52 42 55 45 3 3 34 30
70 60 36 29 70 58 76 64 6 5 30 26 54 45 25 19 62 51 84 70 23 19 45 38 51 43
5 4 21 17 35 30 14 12 45 38 69 57 24 19 56 46 71 60 15 14 43 37 62 52 19 15
45 37 59 50 14 13 45 39 69 58 24 19 61 51 83 70 22 19 62 52 89 72 28 21 46
38 42 38 -5 1 1 6 12 13 21 12 69 52 162 134 26 24 53 43 60 43 6 0 12 7 12
15 0 8 4 15 10 15 10 0 27 14 98 78 21 18 58 49 85 69 26 21 47 41 47 45 0 4
5 8 10 8 6 0 18 8 28 17 31 30 85 77 117 101 17 13 50 41 75 62 40 35 154 131
219 185 13 11 58 49 101 85 84 71 84 70 215 179 50 41 97 81 106 90 9 9 25 23
36 31 26 21 137 113 143 120 5 5 118 99 135 113 6 4 21 17 35 30 14 12 45 38
69 57 24 19 48 40 55 45 6 6 38 33 71 60 33 28 67 58 76 67 9 10 19 18 23 18
4 0 21 13 39 29 34 32 160 139 242 206 70 57 104 87 131 114 13 13 24 21 24
18 0 -4 15 8 33 26 19 18 49 43 67 57 18 14 43 34 55 45 12 11 41 36 66 55 24
19 53 44 64 55 11 11 36 32 55 47 19 15 47 37 61 50 15 13 46 39 69 58 132
108 133 110 128 165 -5 64 -34 105 -96 136 -26 13 -49 24 -52 24 -3 0 -12 5
-20 10 -8 5 -60 32 -115 60 -102 52 -117 60 -170 88 -29 15 -51 57 -61 117 -4
29 -8 44 -19 85 -5 16 -9 39 -10 50 -1 11 -5 36 -8 55 -4 19 -10 49 -13 65 -7
41 -11 68 -18 150 -4 39 -9 90 -12 115 -6 50 -6 368 0 415 2 17 7 62 11 100 8
80 9 87 20 138 4 20 10 37 14 37 4 0 20 12 38 28 40 35 59 51 108 87 22 16 47
36 57 45 9 8 31 26 49 40 18 14 58 45 89 70 31 25 69 54 85 65 69 49 85 72 85
120 0 65 -34 115 -102 148 -29 14 -93 44 -143 67 -49 23 -112 52 -140 65 -54
25 -137 64 -308 144 -59 28 -109 51 -112 51 -2 0 -50 22 -105 49 -55 28 -103
50 -105 51 -5 1 -84 37 -230 105 -171 80 -233 109 -295 138 -36 17 -67 31 -70
32 -44 15 -70 29 -70 38 0 6 -3 8 -6 4 -4 -3 -27 5 -53 18 -76 39 -146 69
-153 67 -5 -1 -8 3 -8 10 0 6 -3 9 -6 6 -3 -4 -31 6 -62 22 -70 34 -202 97
-302 142 -41 19 -102 48 -135 64 -33 16 -62 29 -65 29 -3 0 -40 18 -83 38
-106 51 -191 91 -197 92 -3 1 -43 19 -90 42 -47 22 -119 57 -161 76 -115 53
-173 80 -291 136 -59 28 -109 51 -111 51 -3 0 -65 29 -138 63 -155 74 -171 81
-181 79 -5 -1 -8 3 -8 8 0 6 -4 10 -9 10 -5 0 -51 20 -102 44 -157 73 -209 96
-219 96 -6 0 -10 5 -10 10 0 6 -5 10 -12 10 -6 0 -49 18 -94 40 -46 22 -88 40
-93 40 -6 0 -11 5 -11 12 0 6 -3 9 -6 6 -3 -4 -41 11 -85 33 -43 21 -81 39
-83 39 -3 0 -49 20 -103 45 -132 62 -147 69 -153 76 -9 10 -94 30 -121 27 -14
-1 -45 -17 -70 -34z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long