initial commit...
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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}}
|
||||
|
||||
`
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "accorder/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
CALIBRE_BOOKS1={"portable":false}
|
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 978 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 6.4 KiB |
|
@ -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 |