diff --git a/.gitignore b/.gitignore index a3e8ccb..f8dc3c5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ output *.zip *.txt *.xml +/target diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..77d1d83 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "fmt" + "os" + "runtime/debug" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.cpmachado.pt/gelo/internal/config" +) + +var ( + runVersionFlag bool + cfgFile string = config.DefaultConfigFile +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "gelo", + Short: "A tool to keep up with ratings", + RunE: func(cmd *cobra.Command, args []string) error { + if !runVersionFlag { + return cmd.Help() + } + progBuildInfo, ok := debug.ReadBuildInfo() + if ok { + fmt.Printf("%s-%s", cmd.Use, progBuildInfo.Main.Version) + } else { + fmt.Printf("%s-unknown", cmd.Use) + } + return nil + }, +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.Flags().BoolVarP(&runVersionFlag, "version", "v", false, "Display version") + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", config.DefaultConfigFile, "config file") +} + +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + + // Search config in home directory with name ".cobra" (without extension). + viper.AddConfigPath(config.DefaultConfigHome) + viper.SetConfigType("json") + viper.SetConfigName("config") + } + + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/doc/FPX.md b/doc/FPX.md new file mode 100644 index 0000000..7686163 --- /dev/null +++ b/doc/FPX.md @@ -0,0 +1,39 @@ +# FPX (Federação Portuguesa de Xadrez) + +Website: + +Portuguese federation data: + +- blitz: +- classic: +- rapid: + +It's a simple csv using ";" as separator and the fields. + +It has some issues with encoding and random Carriage return in some records. + +So far it appears, by order: +- FPX id +- Name +- Federation +- Sex +- Club ID +- Club Name +- Date of Birth: only year in format (2006-_-_) +- Number of games? (Still not sure) +- FIDE ID +- Rating +- Title +- Age Group + + U08 + + U10 + + U12 + + U14 + + U16 + + U18 + + U20 + + Sen + + S50 + + S65 +- Flags: Only inactive? +- K factor? (appears so, but some are 0 and empty, are these unrated?) diff --git a/go.mod b/go.mod index ff8e90c..181a273 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,25 @@ module go.cpmachado.pt/gelo go 1.25.1 + +require ( + github.com/adrg/xdg v0.5.3 + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.21.0 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect +) diff --git a/go.sum b/go.sum index e69de29..1b32ffc 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,60 @@ +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index f01229c..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,49 +0,0 @@ -package config - -import ( - "log/slog" - "os" -) - -// LogConfig stores Logger Configuration -type LogConfig struct { - Level slog.Level - Group string -} - -func (lc LogConfig) Apply() { - logger := slog.New( - slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: lc.Level, - }), - ).WithGroup(lc.Group) - - slog.SetDefault(logger) -} - -type Config struct { - Destination string - Log LogConfig -} - -func (c Config) Clone() Config { - return c -} - -func (c Config) Apply() { - c.Log.Apply() -} - -var config = &DefaultConfig - -func GetConfig() *Config { - return config -} - -var DefaultConfig = Config{ - Destination: "output", - Log: LogConfig{ - Level: slog.LevelInfo, - Group: "data", - }, -} diff --git a/internal/config/consts.go b/internal/config/consts.go new file mode 100644 index 0000000..dabc1ff --- /dev/null +++ b/internal/config/consts.go @@ -0,0 +1,12 @@ +package config + +import ( + "path" + + "github.com/adrg/xdg" +) + +var ( + DefaultConfigHome string = path.Join(xdg.ConfigHome, "gelo") + DefaultConfigFile string = path.Join(DefaultConfigHome, "config.json") +) diff --git a/main.go b/main.go index 3c0afa3..4acfdb7 100644 --- a/main.go +++ b/main.go @@ -1,187 +1,7 @@ package main -import ( - "archive/zip" - "encoding/csv" - "encoding/xml" - "flag" - "fmt" - "io" - "log/slog" - "net/http" - "net/url" - "os" - "path" - - "go.cpmachado.pt/gelo/fide" - "go.cpmachado.pt/gelo/internal/config" -) - -var Version string = "0.2.0" - -const ( - PlayersNumberCap = 100000000 - PlayersNumberLog = 500000 -) - -func init() { - cfg := config.GetConfig() - parseFlags(cfg) - cfg.Apply() - slog.Info("INIT", slog.Any("config", cfg)) -} +import "go.cpmachado.pt/gelo/cmd" func main() { - cfg := config.GetConfig() - - slog.Info("MAIN", slog.String("message", "Creating directory"), slog.String("destination", cfg.Destination)) - if err := os.MkdirAll(cfg.Destination, os.ModePerm); err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - xurl, err := url.Parse(fide.XmlURL) - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - _, filename := path.Split(xurl.Path) - file, err := os.OpenFile(path.Join(cfg.Destination, filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - slog.Info("MAIN", - slog.String("message", "Retrieving records"), - slog.String("origin", fide.XmlURL), - slog.String("filename", filename), - ) - resp, err := http.Get(fide.XmlURL) - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - _, err = io.Copy(file, resp.Body) - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - err = file.Close() - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - err = resp.Body.Close() - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - slog.Info("MAIN", slog.String("message", "Open zip")) - archive, err := zip.OpenReader(path.Join(cfg.Destination, filename)) - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - slog.Info("MAIN", slog.String("message", "Opening records"), slog.String("file", archive.File[0].Name)) - xmlFile, err := archive.File[0].Open() - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - decoder := xml.NewDecoder(xmlFile) - - file, err = os.OpenFile(path.Join(cfg.Destination, "players.csv"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) - if err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - w := csv.NewWriter(file) - - slog.Info("MAIN", slog.String("message", "Decoding XML, and encoding csv")) - - if err = w.Write(fide.PlayerCsvHeader); err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - var player fide.Player - - for i := 0; i < PlayersNumberCap; { - tok, err := decoder.Token() - if err != nil { - if err == io.EOF { - slog.Info("MAIN", - slog.String("message", "Number of parsed players"), - slog.Int("parsed", i)) - break - } - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - switch v := tok.(type) { - case xml.StartElement: - if v.Name.Local == "player" { - i++ - if err := decoder.DecodeElement(&player, &v); err != nil { - fmt.Println("Error decoding player element:", err) - return - } - player.CorrectRecord() - if err = w.Write(player.ToCsvRecord()); err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - // log each 100k - if i%PlayersNumberLog == 0 { - slog.Info("MAIN", - slog.String("message", "Number of parsed players"), - slog.Int("parsed", i)) - } - } - } - } - - if err = xmlFile.Close(); err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - if err = archive.Close(); err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - w.Flush() - if err = w.Error(); err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - - if err = file.Close(); err != nil { - slog.Error("MAIN", slog.Any("error", err)) - os.Exit(1) - } - slog.Info("MAIN", slog.String("message", "Operation Complete")) -} - -func parseFlags(cfg *config.Config) { - var version bool - flag.StringVar(&cfg.Destination, "d", cfg.Destination, "Destination directory for resources") - flag.BoolVar(&version, "v", false, "Display version") - flag.Parse() - - if version { - displayVersion() - os.Exit(0) - } -} - -func displayVersion() { - fmt.Printf("gelo-%s Copyright (c) 2025 cpmachado", Version) + cmd.Execute() }