From 2ed4231d8fb2c1b5ac6e6500adc0758d038483dc Mon Sep 17 00:00:00 2001 From: rbtr Date: Sun, 8 Mar 2020 11:15:02 -0500 Subject: [PATCH] trakt collector --- .gitignore | 1 + Makefile | 2 +- README.md | 1 + cmd/{config.go => genconf.go} | 34 +-- cmd/root.go | 22 +- cmd/sort.go | 14 +- cmd/trakt.go | 84 ++++++++ docs/examples/pachinko.yaml | 2 + docs/examples/trakt | 1 + docs/plugins/outputs/trakt.md | 32 +++ go.mod | 5 +- go.sum | 31 +-- internal/config/config.go | 214 ------------------- internal/config/genconf.go | 94 ++++++++ internal/config/root.go | 49 +++++ internal/config/sort.go | 110 ++++++++++ internal/config/trakt.go | 38 ++++ internal/plugin/processor/pre/categorizer.go | 3 +- internal/trakt/trakt.go | 143 +++++++++++++ plugin/input/path.go | 8 +- plugin/input/types.go | 4 +- plugin/output/logger.go | 4 +- plugin/output/path_mover.go | 3 +- plugin/output/trakt_collector.go | 117 ++++++++++ plugin/output/types.go | 4 +- plugin/processor/intra/tmdb.go | 9 +- plugin/processor/intra/tvdb.go | 5 +- plugin/processor/post/movie_path_solver.go | 3 +- plugin/processor/post/tv_path_solver.go | 3 +- plugin/processor/pre/movie.go | 3 +- plugin/processor/pre/movie_test.go | 5 +- plugin/processor/pre/tv.go | 3 +- plugin/processor/pre/tv_test.go | 5 +- plugin/processor/types.go | 6 +- types/media.go | 1 + 35 files changed, 777 insertions(+), 286 deletions(-) rename cmd/{config.go => genconf.go} (66%) create mode 100644 cmd/trakt.go create mode 100644 docs/examples/trakt create mode 100644 docs/plugins/outputs/trakt.md delete mode 100644 internal/config/config.go create mode 100644 internal/config/genconf.go create mode 100644 internal/config/root.go create mode 100644 internal/config/sort.go create mode 100644 internal/config/trakt.go create mode 100644 internal/trakt/trakt.go create mode 100644 plugin/output/trakt_collector.go diff --git a/.gitignore b/.gitignore index 5d22227..cac648a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ pachinko* .vscode .pachinko.yaml !docs/**/* +trakt diff --git a/Makefile b/Makefile index 2fb79f1..8fd7fae 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ build: #vendor ## build -o bin/$(MODULE) ./ clean: ## clean workspace - @rm -rf ./bin ./pachinko + @rm -rf ./bin ./$(MODULE) help: ## print this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md index b4b4d93..a0938db 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ other datastore types planned include : s3 (and whatever you would like to contr pachinko currently supports these outputs: - local filesystem (`path_mover`) - stdout (`logger`) +- [trakt collector (`trakt_collector`)](docs/plugins/outputs/trakt.md) #### processors pachinko has the following required processors: diff --git a/cmd/config.go b/cmd/genconf.go similarity index 66% rename from cmd/config.go rename to cmd/genconf.go index 32452f8..5ea1f4a 100644 --- a/cmd/config.go +++ b/cmd/genconf.go @@ -20,51 +20,51 @@ import ( "gopkg.in/yaml.v2" ) -// configCmd represents the config command -var configCmd = &cobra.Command{ - Use: "config", +// genconf represents the config command +var genconf = &cobra.Command{ + Use: "genconf", Short: "Generate pachinko configs", Long: ` Use this command to generate pachinko configs. With no arguments, the config generated will contain the default configs for every compiled plug-in. - $ pachinko config > config.yaml + $ pachinko genconf > config.yaml Note: the generated config is always in alphabetical order of keys. If order matters to pipeline plug-in execution, it will need to be reordered after generation. The config can be output as either yaml (default) or toml. - $ pachinko config -o toml > config.toml + $ pachinko genconf -o toml > config.toml To only generate stubs for a subset of plug-ins, pass the plug-in names as a comma separated list to the flag of their type. - $ pachinko config --inputs=path --processors=tvid,movid > config.yaml + $ pachinko genconf --inputs=path --processors=tvid,movid > config.yaml The common flags (dry-run, logging) will be automatically set in the output config when they are used on the config command. - $ pachinko config --log-level=debug > config.yaml + $ pachinko genconf --log-level=debug > config.yaml The config file can then be edited and to fully customize the plug-ins and the pachinko pipeline. `, Run: func(cmd *cobra.Command, args []string) { log.SetLevel(log.TraceLevel) - cfg, err := config.LoadCmdConfig() + cfg, err := config.LoadGenconf(rootCtx) if err != nil { log.Fatal(err) } if err := cfg.Validate(); err != nil { log.Fatal(err) } - pipelineCfg := config.NewCmdSort() - if err := cfg.DefaultConfig(pipelineCfg); err != nil { + sortCfg := config.NewSort(rootCtx) + if err := cfg.DefaultConfig(sortCfg); err != nil { log.Fatal(err) } var out map[string]interface{} - if err := mapstructure.Decode(pipelineCfg, &out); err != nil { + if err := mapstructure.Decode(sortCfg, &out); err != nil { log.Fatal(err) } @@ -84,12 +84,12 @@ and the pachinko pipeline. } func init() { - rootCmd.AddCommand(configCmd) - configCmd.Flags().StringP("format", "o", "yaml", "config output format") - configCmd.Flags().StringSlice("inputs", []string{}, "comma-separated list of input plugins") - configCmd.Flags().StringSlice("outputs", []string{}, "comma-separated list of output plugins") - configCmd.Flags().StringSlice("processors", []string{}, "comma-separated list of processor plugins") - if err := viper.BindPFlags(configCmd.Flags()); err != nil { + root.AddCommand(genconf) + genconf.Flags().StringP("format", "o", "yaml", "config output format") + genconf.Flags().StringSlice("inputs", []string{}, "comma-separated list of input plugins") + genconf.Flags().StringSlice("outputs", []string{}, "comma-separated list of output plugins") + genconf.Flags().StringSlice("processors", []string{}, "comma-separated list of processor plugins") + if err := viper.BindPFlags(genconf.Flags()); err != nil { log.Fatal(err) } } diff --git a/cmd/root.go b/cmd/root.go index b7b70c9..67839eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,8 +26,8 @@ import ( var cfgFile string var rootCtx context.Context -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +// root represents the base command when called without any subcommands +var root = &cobra.Command{ Use: "pachinko", Long: ` _ _ _ @@ -40,21 +40,21 @@ pluggable media sorter`, } func Execute() { - if err := rootCmd.Execute(); err != nil { + if err := root.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } func init() { - cobra.OnInitialize(initConfig) + cobra.OnInitialize(rootConfig) - // bind flags - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.pachinko.yaml)") - rootCmd.PersistentFlags().Bool("dry-run", false, "run pipeline as read only and do not make changes") - rootCmd.PersistentFlags().StringP("log-level", "v", "info", "log verbosity (trace,debug,info,warn,error)") - rootCmd.PersistentFlags().String("log-format", "text", "log format (text,json)") - if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { + // bind root flags + root.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.pachinko.yaml)") + root.PersistentFlags().Bool("dry-run", false, "run pipeline as read only and do not make changes") + root.PersistentFlags().StringP("log-level", "v", "info", "log verbosity (trace,debug,info,warn,error)") + root.PersistentFlags().String("log-format", "text", "log format (text,json)") + if err := viper.BindPFlags(root.PersistentFlags()); err != nil { log.Fatal(err) } @@ -72,7 +72,7 @@ func init() { }() } -func initConfig() { +func rootConfig() { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { diff --git a/cmd/sort.go b/cmd/sort.go index 0129686..67d4121 100644 --- a/cmd/sort.go +++ b/cmd/sort.go @@ -14,14 +14,14 @@ import ( "github.com/spf13/cobra" ) -// sortCmd represents the sort command -var sortCmd = &cobra.Command{ +// sort represents the sort command +var sort = &cobra.Command{ Use: "sort", Short: "Run the sorting pipeline.", Long: ` Use this command to execute the sorting pipeline. -With no arguments, sort will load the config from %HOME/.pachinko.yaml. +With no arguments, sort will load the config from $HOME/.pachinko.yaml. $ pachinko sort If no config is provided, no plugins will be loaded and the pipeline will @@ -29,16 +29,16 @@ not do anything useful. `, Run: func(cmd *cobra.Command, args []string) { log.SetLevel(log.TraceLevel) - cfg, err := config.LoadCmdSort() + sortConf, err := config.LoadSort(rootCtx) if err != nil { log.Fatal(err) } - if err := cfg.Validate(); err != nil { + if err := sortConf.Validate(); err != nil { log.Fatal(err) } p := pipeline.NewPipeline() - if err := cfg.ConfigurePipeline(p); err != nil { + if err := sortConf.ConfigurePipeline(p); err != nil { log.Fatal(err) } @@ -49,5 +49,5 @@ not do anything useful. } func init() { - rootCmd.AddCommand(sortCmd) + root.AddCommand(sort) } diff --git a/cmd/trakt.go b/cmd/trakt.go new file mode 100644 index 0000000..e9687ec --- /dev/null +++ b/cmd/trakt.go @@ -0,0 +1,84 @@ +/* +Copyright © 2020 The Pachinko Authors + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ +package cmd + +import ( + "github.com/rbtr/pachinko/internal/config" + internaltrakt "github.com/rbtr/pachinko/internal/trakt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var traktFile string + +// trakt represents the config command +var trakt = &cobra.Command{ + Use: "trakt", + Short: "Connect to Trakt", + Long: ` +Use this command to connect Pachinko to Trakt. + +Trakt requires that you connect and authorize Pachinko before it can make any +changes on your behalf. + +This command will print a URL and code. Open the URL in your browser, sign in +to Trakt (if you aren't signed in already), and enter the code. + +Pachinko will write the authorized credentials out to the file specified in +the "--authfile" flag. The credential is stored in a JSON notation: + { + "access-token": "[access-token]", + "client-id": "76a0c1e8d3331021f6e312115e27fe4c29f4ef23ef89a0a69143a62d136ab994", + "client-secret": "fe8d1f0921413028f92428d2922e13a728e27d2f35b26e315cf3dde31228568d", + "created": "[created-at]", + "expires": "7776000", + "refresh-token": "[refresh-token" + } + +This credential file is portable and can be moved around with your +Pachinko install. Pachinko will automatically refresh it every 80 days during +normal operations. + +The token expires after 90 days. If Pachinko can't refresh the token before +it expires, you will need to rerun this to generate a new authorization. +`, + Run: func(cmd *cobra.Command, args []string) { + log.SetLevel(log.TraceLevel) + cfg, err := config.LoadTrakt(rootCtx) + if err != nil { + log.Fatal(err) + } + if err := cfg.Validate(); err != nil { + log.Fatal(err) + } + auth, err := internaltrakt.ReadAuthFile(cfg.Authfile) + if err != nil { + log.Fatal(err) + } + client, err := internaltrakt.NewTrakt(auth) + if err != nil { + log.Fatal(err) + } + if auth, err = client.Authorize(rootCtx); err != nil { + log.Fatal(err) + } + if err := internaltrakt.WriteAuthFile(cfg.Authfile, auth); err != nil { + log.Fatal(err) + } + }, +} + +func init() { + root.AddCommand(trakt) + trakt.Flags().StringVar(&traktFile, "authfile", internaltrakt.DefaultAuthfile, "where to save the trakt authorization credential") + trakt.Flags().BoolP("overwrite", "f", false, "overwrite the authfile if it exists already") + if err := viper.BindPFlags(trakt.Flags()); err != nil { + log.Fatal(err) + } +} diff --git a/docs/examples/pachinko.yaml b/docs/examples/pachinko.yaml index 54f0337..e79644b 100644 --- a/docs/examples/pachinko.yaml +++ b/docs/examples/pachinko.yaml @@ -11,6 +11,8 @@ outputs: dry-run: false name: path-mover overwrite: false +- authfile: "/etc/pachinko/trakt" + name: trakt-collector pipeline: buffer: 10 processors: diff --git a/docs/examples/trakt b/docs/examples/trakt new file mode 100644 index 0000000..47fe9d0 --- /dev/null +++ b/docs/examples/trakt @@ -0,0 +1 @@ +{"access-token":"xyz987","client-id":"76a0c1e8d3331021f6e312115e27fe4c29f4ef23ef89a0a69143a62d136ab994","client-secret":"fe8d1f0921413028f92428d2922e13a728e27d2f35b26e315cf3dde31228568d","created-at":"2020-01-02T03:04:05-00:00","expires-after":7776000000000000,"refresh-token":"abc123"} diff --git a/docs/plugins/outputs/trakt.md b/docs/plugins/outputs/trakt.md new file mode 100644 index 0000000..b9810d7 --- /dev/null +++ b/docs/plugins/outputs/trakt.md @@ -0,0 +1,32 @@ +### Trakt and the Trakt Collector output +Pachinko can interact with Trakt. Currently, Pachinko can: +- add sorted items to your Trakt collection (`trakt_collector` output plugin) + +#### Trakt Authorization +To communicate with Trakt, it needs an access token. A helper command is included for authorizating: +```bash +$ pachinko trakt +Authenticating in Trakt! +Please open in your browser: https://trakt.tv/activate + and enter the code: 1234A1AA +``` + +Enter the provided code at the [link](https://trakt.tv/activate) and Pachinko will receive an access token. It will write the access credentials out to a file, by default `/etc/pachinko/trakt`. Specify a different file by using the `--authfile /path/to/file` flag on the `trakt` command. + +The [authfile](../../examples/trakt) is JSON and contains authorization credentials. + +#### Trakt Collector +To add items to your Trakt collection when Pachinko is done processing them, enable the Trakt Collector output plugin in your Pachinko config file. + +The only configurable option is the authfile location - point it at the authfile created by the authorization step as described [above](#trakt-authorization). + +```yaml +#... +outputs: +- name: trakt-collector + authfile: "/etc/pachinko/trakt" + +#... +``` + +Now when Pachinko identifies and processes TV or Movies they will be automatically collected in Trakt! diff --git a/go.mod b/go.mod index a654e27..b4b8166 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,15 @@ go 1.13 require ( github.com/BurntSushi/toml v0.3.1 - github.com/cyruzin/golang-tmdb v1.2.1 + github.com/cyruzin/golang-tmdb v1.3.1 github.com/lithammer/fuzzysearch v1.1.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.1.2 github.com/pkg/errors v0.9.1 + github.com/rbtr/go-trakt v0.0.0-20200310010953-144101cfef69 github.com/rbtr/go-tvdb v0.0.0-20200127015222-6fcb5ef30e70 github.com/sirupsen/logrus v1.4.2 - github.com/spf13/cobra v0.0.5 + github.com/spf13/cobra v0.0.6 github.com/spf13/viper v1.6.2 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 510172d..8a02781 100644 --- a/go.sum +++ b/go.sum @@ -19,13 +19,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cyruzin/golang-tmdb v1.2.1 h1:jrp2vjn12e+BgD9j0f7yHhXmvqSBEwcGQiexyfthGjw= -github.com/cyruzin/golang-tmdb v1.2.1/go.mod h1:ka1Gufj/wBUkU4phN6Ixx3a+a4UDwnZjK4eXLCjxkkI= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cyruzin/golang-tmdb v1.3.1 h1:zE7CEDVqpzEUhpnKQKp+kNs3KBvb8dqe/16ApzYmW+w= +github.com/cyruzin/golang-tmdb v1.3.1/go.mod h1:O+rbwyyMRUmPpBIHCyCueKsH0NklhJB3b1dHeXh/Bf0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -106,6 +105,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -120,6 +120,8 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -149,6 +151,10 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -168,10 +174,13 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rbtr/go-trakt v0.0.0-20200310010953-144101cfef69 h1:qYy5bg6U4JgWzXcl8FjzyzVaqIGpAfve2v5J+ZxJG/s= +github.com/rbtr/go-trakt v0.0.0-20200310010953-144101cfef69/go.mod h1:C8/th48mq7Wm+2OKaQWZ6o8JtU31PzIqd0Jnu+aZgyA= github.com/rbtr/go-tvdb v0.0.0-20200127015222-6fcb5ef30e70 h1:QErGMHDOJst097L83bKNCh8YbMGTnDNATLiGf9mnTAk= github.com/rbtr/go-tvdb v0.0.0-20200127015222-6fcb5ef30e70/go.mod h1:AcL7ia8jMFxrExAGzZ/qLtdyZQa3uDzVHYoi+ZK3PnA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -185,13 +194,13 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -199,15 +208,15 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -218,7 +227,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -245,7 +253,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index b72ca0b..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,214 +0,0 @@ -/* -Copyright © 2020 The Pachinko Authors - -This Source Code Form is subject to the terms of the Mozilla Public -License, v. 2.0. If a copy of the MPL was not distributed with this -file, You can obtain one at https://mozilla.org/MPL/2.0/. -*/ -package config - -import ( - "strings" - - "github.com/mitchellh/mapstructure" - "github.com/rbtr/pachinko/internal/pipeline" - internalpre "github.com/rbtr/pachinko/internal/plugin/processor/pre" - "github.com/rbtr/pachinko/plugin/input" - "github.com/rbtr/pachinko/plugin/output" - "github.com/rbtr/pachinko/plugin/processor" - log "github.com/sirupsen/logrus" - "github.com/spf13/viper" -) - -type RootConfig struct { - DryRun bool `mapstructure:"dry-run"` - LogLevel string `mapstructure:"log-level"` - LogFormat string `mapstructure:"log-format"` -} - -type CmdConfig struct { - RootConfig `mapstructure:",squash"` - Format string `mapstructure:"format"` - Inputs []string `mapstructure:"inputs"` - Outputs []string `mapstructure:"outputs"` - Processors map[processor.Type][]string `mapstructure:"processors"` -} - -type CmdSort struct { - RootConfig `mapstructure:",squash"` - Pipeline pipeline.Config `mapstructure:"pipeline"` - Inputs []map[string]interface{} `mapstructure:"inputs"` - Outputs []map[string]interface{} `mapstructure:"outputs"` - Processors map[processor.Type][]map[string]interface{} `mapstructure:"processors"` -} - -func (c *RootConfig) configLogger() { - log.SetLevel(log.InfoLevel) - switch c.LogFormat { - case "json": - log.SetFormatter(&log.JSONFormatter{}) - default: - log.SetFormatter(&log.TextFormatter{}) - } - if c.LogLevel != "" { - if lvl, err := log.ParseLevel(c.LogLevel); err != nil { - log.Fatal(err) - } else { - log.SetLevel(lvl) - } - } -} - -// Validate validate -func (c *RootConfig) Validate() error { - c.configLogger() - log.Debugf("loaded config: %+v", *c) - if c.DryRun { - log.Warn("DRY RUN: no changes will be made") - } - return nil -} - -func (c *CmdSort) ConfigurePipeline(pipe *pipeline.Pipeline) error { - if err := mapstructure.Decode(c.Pipeline, pipe); err != nil { - return err - } - for _, p := range c.Inputs { - if name, ok := p["name"]; ok { - if initializer, ok := input.Registry[name.(string)]; ok { - plugin := initializer() - if err := mapstructure.Decode(p, plugin); err != nil { - return err - } - if err := plugin.Init(); err != nil { - return err - } - pipe.WithInputs(plugin) - } - } - } - - for _, p := range c.Outputs { - if name, ok := p["name"]; ok { - if initializer, ok := output.Registry[name.(string)]; ok { - plugin := initializer() - if err := mapstructure.Decode(p, plugin); err != nil { - return err - } - if err := plugin.Init(); err != nil { - return err - } - pipe.WithOutputs(plugin) - } - } - } - - categorizer := internalpre.NewCategorizer() - if err := categorizer.Init(); err != nil { - return err - } - pipe.WithProcessors(categorizer) - - for _, t := range processor.Types { - for _, p := range c.Processors[t] { - if name, ok := p["name"]; ok { - if initializer, ok := processor.Registry[t][name.(string)]; ok { - plugin := initializer() - if err := mapstructure.Decode(p, plugin); err != nil { - return err - } - if err := plugin.Init(); err != nil { - return err - } - pipe.WithProcessors(plugin) - } - } - } - } - - return nil -} - -func NewCmdSort() *CmdSort { - return &CmdSort{ - Processors: map[processor.Type][]map[string]interface{}{ - processor.Pre: {}, - processor.Intra: {}, - processor.Post: {}, - }, - } -} - -// LoadConfig loadconfig -func LoadCmdSort() (*CmdSort, error) { - cfg := NewCmdSort() - viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) - viper.AutomaticEnv() - err := viper.Unmarshal(cfg) - return cfg, err -} - -func (c *CmdConfig) DefaultConfig(p *CmdSort) error { - if len(c.Inputs) == 0 && len(c.Outputs) == 0 && len(c.Processors) == 0 { - // no plugins specified, dump configs for them all - for k := range input.Registry { - c.Inputs = append(c.Inputs, k) - } - for k := range output.Registry { - c.Outputs = append(c.Outputs, k) - } - for _, t := range []processor.Type{processor.Pre, processor.Post, processor.Intra} { - for k := range processor.Registry[t] { - c.Processors[t] = append(c.Processors[t], k) - } - } - } - - for _, name := range c.Inputs { - log.Tracef("making default config for plugin %s", name) - if initializer, ok := input.Registry[name]; ok { - var out map[string]interface{} - if err := mapstructure.Decode(initializer(), &out); err != nil { - return err - } - out["name"] = name - p.Inputs = append(p.Inputs, out) - } - } - - for _, name := range c.Outputs { - log.Tracef("making default config for plugin %s", name) - if initializer, ok := output.Registry[name]; ok { - var out map[string]interface{} - if err := mapstructure.Decode(initializer(), &out); err != nil { - return err - } - out["name"] = name - p.Outputs = append(p.Outputs, out) - } - } - - for t, names := range c.Processors { - for _, name := range names { - log.Tracef("making default config for plugin %s", name) - if initializer, ok := processor.Registry[t][name]; ok { - var out map[string]interface{} - if err := mapstructure.Decode(initializer(), &out); err != nil { - return err - } - out["name"] = name - p.Processors[t] = append(p.Processors[t], out) - } - } - } - - return nil -} - -func LoadCmdConfig() (*CmdConfig, error) { - cfg := &CmdConfig{} - viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) - viper.AutomaticEnv() - err := viper.Unmarshal(cfg) - return cfg, err -} diff --git a/internal/config/genconf.go b/internal/config/genconf.go new file mode 100644 index 0000000..1c92ce7 --- /dev/null +++ b/internal/config/genconf.go @@ -0,0 +1,94 @@ +/* +Copyright © 2020 The Pachinko Authors + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ +package config + +import ( + "context" + "strings" + + "github.com/mitchellh/mapstructure" + "github.com/rbtr/pachinko/plugin/input" + "github.com/rbtr/pachinko/plugin/output" + "github.com/rbtr/pachinko/plugin/processor" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +type Genconf struct { + Root `mapstructure:",squash"` + Format string `mapstructure:"format"` + Inputs []string `mapstructure:"inputs"` + Outputs []string `mapstructure:"outputs"` + Processors map[processor.Type][]string `mapstructure:"processors"` +} + +func (c *Genconf) DefaultConfig(p *Sort) error { + if len(c.Inputs) == 0 && len(c.Outputs) == 0 && len(c.Processors) == 0 { + // no plugins specified, dump configs for them all + for k := range input.Registry { + c.Inputs = append(c.Inputs, k) + } + for k := range output.Registry { + c.Outputs = append(c.Outputs, k) + } + for _, t := range []processor.Type{processor.Pre, processor.Post, processor.Intra} { + for k := range processor.Registry[t] { + c.Processors[t] = append(c.Processors[t], k) + } + } + } + + for _, name := range c.Inputs { + log.Tracef("making default config for plugin %s", name) + if initializer, ok := input.Registry[name]; ok { + var out map[string]interface{} + if err := mapstructure.Decode(initializer(), &out); err != nil { + return err + } + out["name"] = name + p.Inputs = append(p.Inputs, out) + } + } + + for _, name := range c.Outputs { + log.Tracef("making default config for plugin %s", name) + if initializer, ok := output.Registry[name]; ok { + var out map[string]interface{} + if err := mapstructure.Decode(initializer(), &out); err != nil { + return err + } + out["name"] = name + p.Outputs = append(p.Outputs, out) + } + } + + for t, names := range c.Processors { + for _, name := range names { + log.Tracef("making default config for plugin %s", name) + if initializer, ok := processor.Registry[t][name]; ok { + var out map[string]interface{} + if err := mapstructure.Decode(initializer(), &out); err != nil { + return err + } + out["name"] = name + p.Processors[t] = append(p.Processors[t], out) + } + } + } + + return nil +} + +func LoadGenconf(ctx context.Context) (*Genconf, error) { + cfg := &Genconf{} + cfg.ctx = ctx + viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) + viper.AutomaticEnv() + err := viper.Unmarshal(cfg) + return cfg, err +} diff --git a/internal/config/root.go b/internal/config/root.go new file mode 100644 index 0000000..3febcb0 --- /dev/null +++ b/internal/config/root.go @@ -0,0 +1,49 @@ +/* +Copyright © 2020 The Pachinko Authors + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ +package config + +import ( + "context" + + log "github.com/sirupsen/logrus" +) + +type Root struct { + // nolint: structcheck + ctx context.Context + DryRun bool `mapstructure:"dry-run"` + LogLevel string `mapstructure:"log-level"` + LogFormat string `mapstructure:"log-format"` +} + +func (c *Root) configLogger() { + log.SetLevel(log.InfoLevel) + switch c.LogFormat { + case "json": + log.SetFormatter(&log.JSONFormatter{}) + default: + log.SetFormatter(&log.TextFormatter{}) + } + if c.LogLevel != "" { + if lvl, err := log.ParseLevel(c.LogLevel); err != nil { + log.Fatal(err) + } else { + log.SetLevel(lvl) + } + } +} + +// Validate validate +func (c *Root) Validate() error { + c.configLogger() + log.Debugf("loaded config: %+v", *c) + if c.DryRun { + log.Warn("DRY RUN: no changes will be made") + } + return nil +} diff --git a/internal/config/sort.go b/internal/config/sort.go new file mode 100644 index 0000000..f447d91 --- /dev/null +++ b/internal/config/sort.go @@ -0,0 +1,110 @@ +/* +Copyright © 2020 The Pachinko Authors + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ +package config + +import ( + "context" + "strings" + + "github.com/mitchellh/mapstructure" + "github.com/rbtr/pachinko/internal/pipeline" + internalpre "github.com/rbtr/pachinko/internal/plugin/processor/pre" + "github.com/rbtr/pachinko/plugin/input" + "github.com/rbtr/pachinko/plugin/output" + "github.com/rbtr/pachinko/plugin/processor" + "github.com/spf13/viper" +) + +type Sort struct { + Root `mapstructure:",squash"` + Pipeline pipeline.Config `mapstructure:"pipeline"` + Inputs []map[string]interface{} `mapstructure:"inputs"` + Outputs []map[string]interface{} `mapstructure:"outputs"` + Processors map[processor.Type][]map[string]interface{} `mapstructure:"processors"` +} + +func (c *Sort) ConfigurePipeline(pipe *pipeline.Pipeline) error { + if err := mapstructure.Decode(c.Pipeline, pipe); err != nil { + return err + } + for _, p := range c.Inputs { + if name, ok := p["name"]; ok { + if initializer, ok := input.Registry[name.(string)]; ok { + plugin := initializer() + if err := mapstructure.Decode(p, plugin); err != nil { + return err + } + if err := plugin.Init(c.ctx); err != nil { + return err + } + pipe.WithInputs(plugin) + } + } + } + + for _, p := range c.Outputs { + if name, ok := p["name"]; ok { + if initializer, ok := output.Registry[name.(string)]; ok { + plugin := initializer() + if err := mapstructure.Decode(p, plugin); err != nil { + return err + } + if err := plugin.Init(c.ctx); err != nil { + return err + } + pipe.WithOutputs(plugin) + } + } + } + + categorizer := internalpre.NewCategorizer() + if err := categorizer.Init(c.ctx); err != nil { + return err + } + pipe.WithProcessors(categorizer) + + for _, t := range processor.Types { + for _, p := range c.Processors[t] { + if name, ok := p["name"]; ok { + if initializer, ok := processor.Registry[t][name.(string)]; ok { + plugin := initializer() + if err := mapstructure.Decode(p, plugin); err != nil { + return err + } + if err := plugin.Init(c.ctx); err != nil { + return err + } + pipe.WithProcessors(plugin) + } + } + } + } + + return nil +} + +func NewSort(ctx context.Context) *Sort { + cfg := &Sort{ + Processors: map[processor.Type][]map[string]interface{}{ + processor.Pre: {}, + processor.Intra: {}, + processor.Post: {}, + }, + } + cfg.ctx = ctx + return cfg +} + +// LoadConfig loadconfig +func LoadSort(ctx context.Context) (*Sort, error) { + cfg := NewSort(ctx) + viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) + viper.AutomaticEnv() + err := viper.Unmarshal(cfg) + return cfg, err +} diff --git a/internal/config/trakt.go b/internal/config/trakt.go new file mode 100644 index 0000000..2648c54 --- /dev/null +++ b/internal/config/trakt.go @@ -0,0 +1,38 @@ +/* +Copyright © 2020 The Pachinko Authors + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ +package config + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +type Trakt struct { + Root `mapstructure:",squash"` + Authfile string `mapstructure:"authfile"` + Overwrite bool `mapstructure:"overwrite"` +} + +func LoadTrakt(ctx context.Context) (*Trakt, error) { + cfg := &Trakt{} + cfg.ctx = ctx + viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) + viper.AutomaticEnv() + err := viper.Unmarshal(cfg) + return cfg, err +} + +func (t *Trakt) Validate() error { + if t.Authfile == "" { + return errors.New("authfile must be set") + } + return nil +} diff --git a/internal/plugin/processor/pre/categorizer.go b/internal/plugin/processor/pre/categorizer.go index e858dd8..8a95d25 100644 --- a/internal/plugin/processor/pre/categorizer.go +++ b/internal/plugin/processor/pre/categorizer.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package pre import ( + "context" "path" "strings" @@ -61,7 +62,7 @@ type FileCategorizer struct { fileExtensionCategories map[string]types.Category } -func (cat *FileCategorizer) Init() error { +func (cat *FileCategorizer) Init(context.Context) error { log.Trace("categorizer initializing") cat.fileExtensionCategories = map[string]types.Category{} // transpose the category/extension map for immediate lookups diff --git a/internal/trakt/trakt.go b/internal/trakt/trakt.go new file mode 100644 index 0000000..dbc3dbf --- /dev/null +++ b/internal/trakt/trakt.go @@ -0,0 +1,143 @@ +/* +Copyright © 2020 The Pachinko Authors + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ +package trakt + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "time" + + "github.com/rbtr/go-trakt" + gotrakt "github.com/rbtr/go-trakt" +) + +const ( + DefaultAuthfile = "/etc/pachinko/trakt" + ClientID = "76a0c1e8d3331021f6e312115e27fe4c29f4ef23ef89a0a69143a62d136ab994" + // nolint: gosec + ClientSecret = "fe8d1f0921413028f92428d2922e13a728e27d2f35b26e315cf3dde31228568d" +) + +type Auth struct { + AccessToken string `json:"access-token,omitempty"` + ClientID string `json:"client-id,omitempty"` + ClientSecret string `json:"client-secret,omitempty"` + CreatedAt time.Time `json:"created-at,omitempty"` + ExpiresAfter time.Duration `json:"expires-after,omitempty"` + RefreshToken string `json:"refresh-token,omitempty"` +} + +func (auth *Auth) IsExpired() bool { + return time.Now().After(auth.CreatedAt.Add(auth.ExpiresAfter)) +} + +func (auth *Auth) ShouldRefresh(threshold time.Duration) bool { + return time.Now().After(auth.CreatedAt.Add(auth.ExpiresAfter).Add(-threshold)) +} + +func ReadAuthFile(path string) (*Auth, error) { + auth := &Auth{} + _, err := os.Stat(path) + if os.IsNotExist(err) { + return auth, nil + } + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + if err := json.Unmarshal(b, auth); err != nil { + return nil, err + } + return auth, nil +} + +func WriteAuthFile(path string, auth *Auth) error { + b, err := json.Marshal(auth) + if err != nil { + return err + } + return ioutil.WriteFile(path, b, 0600) +} + +type Trakt struct { + *gotrakt.Client + auth *Auth +} + +func NewTrakt(auth *Auth) (*Trakt, error) { + if auth.ClientID == "" { + auth.ClientID = ClientID + } + if auth.ClientSecret == "" { + auth.ClientSecret = ClientSecret + } + client, err := gotrakt.NewClient(nil, auth.ClientID, auth.ClientSecret) + if auth.AccessToken != "" { + client.SetAuthorization(auth.AccessToken) + } + return &Trakt{client, auth}, err +} + +// Authorize authorizes the client using 2-legged oauth. +// Authorized credentials are stored in the client and also returned. +func (t *Trakt) Authorize(ctx context.Context) (*Auth, error) { + res, err := t.DeviceCode(ctx) + if err != nil { + return nil, err + } + fmt.Printf( + "Authenticating in Trakt!\nPlease open in your browser:\t%s\n\t and enter the code:\t\t%s\n", + res.VerificationURL, + res.UserCode, + ) + + ctx, cancel := context.WithTimeout(ctx, time.Duration(res.ExpiresIn)*time.Second) + defer cancel() + + ticker := time.NewTicker(time.Duration(res.Interval) * time.Second) + go func(ctx context.Context, cancelFunc func()) { + <-ctx.Done() + cancelFunc() + }(ctx, ticker.Stop) + + var result *trakt.AuthResult + code := res.DeviceCode + for range ticker.C { + result, err = t.DeviceToken(ctx, code) + if err == nil { + break + } + } + if result == nil { + return nil, err + } + fmt.Printf("Success! Your Authorization token is:\n\t> %s <\n", result.AccessToken) + t.auth.AccessToken = result.AccessToken + t.auth.RefreshToken = result.RefreshToken + t.auth.ExpiresAfter = time.Duration(result.ExpiresIn) * time.Second + t.auth.CreatedAt = time.Unix(int64(result.CreatedAt), 0) + return t.auth, nil +} + +// Refresh reauthorizes the client using the refresh token. +// Authorized credentials are stored in the client and also returned. +func (t *Trakt) Refresh(ctx context.Context) (*Auth, error) { + res, err := t.RefreshToken(ctx, t.auth.RefreshToken) + if err != nil { + return nil, err + } + t.SetAuthorization(res.AccessToken) + t.auth.AccessToken = res.AccessToken + t.auth.RefreshToken = res.RefreshToken + t.auth.ExpiresAfter = time.Duration(res.ExpiresIn) * time.Second + t.auth.CreatedAt = time.Unix(int64(res.CreatedAt), 0) + return t.auth, nil +} diff --git a/plugin/input/path.go b/plugin/input/path.go index af7ab3c..17b013c 100644 --- a/plugin/input/path.go +++ b/plugin/input/path.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package input import ( + "context" "os" "path/filepath" @@ -23,7 +24,7 @@ type FilePathInput struct { } // Init noop -func (*FilePathInput) Init() error { +func (*FilePathInput) Init(context.Context) error { return nil } @@ -43,7 +44,10 @@ func (p *FilePathInput) Consume(sink chan<- types.Media) { return nil } log.Infof("path_input: found file: %s", path) - sink <- types.Media{SourcePath: path} + sink <- types.Media{ + Identifiers: make(map[string]string), + SourcePath: path, + } count++ return nil }); err != nil { diff --git a/plugin/input/types.go b/plugin/input/types.go index aedb469..351368c 100644 --- a/plugin/input/types.go +++ b/plugin/input/types.go @@ -14,6 +14,8 @@ pipeline datastream. package input import ( + "context" + "github.com/rbtr/pachinko/types" log "github.com/sirupsen/logrus" ) @@ -21,7 +23,7 @@ import ( // input input type Input interface { Consume(chan<- types.Media) - Init() error + Init(context.Context) error } var Registry map[string](func() Input) = map[string](func() Input){} diff --git a/plugin/output/logger.go b/plugin/output/logger.go index 208d64c..42613ca 100644 --- a/plugin/output/logger.go +++ b/plugin/output/logger.go @@ -8,6 +8,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package output import ( + "context" + "github.com/rbtr/pachinko/types" log "github.com/sirupsen/logrus" ) @@ -15,7 +17,7 @@ import ( // Logger is a noop logging output used for dry-runs and testing type Logger struct{} -func (*Logger) Init() error { +func (*Logger) Init(context.Context) error { return nil } diff --git a/plugin/output/path_mover.go b/plugin/output/path_mover.go index 51f3fe6..a8dc45b 100644 --- a/plugin/output/path_mover.go +++ b/plugin/output/path_mover.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package output import ( + "context" "io" "os" "path/filepath" @@ -25,7 +26,7 @@ type FilepathMover struct { Overwrite bool `mapstructure:"overwrite"` } -func (*FilepathMover) Init() error { +func (*FilepathMover) Init(context.Context) error { return nil } diff --git a/plugin/output/trakt_collector.go b/plugin/output/trakt_collector.go new file mode 100644 index 0000000..f10c47f --- /dev/null +++ b/plugin/output/trakt_collector.go @@ -0,0 +1,117 @@ +/* +Copyright © 2020 The Pachinko Authors + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ +package output + +import ( + "context" + "strconv" + + "github.com/rbtr/go-trakt" + internaltrakt "github.com/rbtr/pachinko/internal/trakt" + "github.com/rbtr/pachinko/types" + "github.com/rbtr/pachinko/types/metadata/movie" + "github.com/rbtr/pachinko/types/metadata/tv" + log "github.com/sirupsen/logrus" +) + +var _ Output = (*TraktCollector)(nil) + +type TraktCollector struct { + Authfile string `mapstructure:"authfile"` + + client *internaltrakt.Trakt +} + +// Init reads the authfile, creates a client, refreshes the +// credentials, and writes them back to the authfile. Any failures +// will return an error. +func (t *TraktCollector) Init(ctx context.Context) error { + auth, err := internaltrakt.ReadAuthFile(t.Authfile) + if err != nil { + return err + } + if t.client, err = internaltrakt.NewTrakt(auth); err != nil { + return err + } + auth, err = t.client.Refresh(ctx) + if err != nil { + return err + } + return internaltrakt.WriteAuthFile(t.Authfile, auth) +} + +func (t *TraktCollector) collectTV(m types.Media) error { + tvdbID, err := strconv.Atoi(m.Identifiers["tvdb"]) + log.Debugf("trakt_collector: collecting by tvdb id: %d", tvdbID) + if err != nil { + return err + } + resp, err := t.client.Collection(context.TODO(), &trakt.CollectionBody{ + Episodes: []trakt.Episode{ + { + IDs: trakt.IDs{ + TVDB: tvdbID, + }, + }, + }, + }) + if err != nil { + return err + } + log.Debugf("trakt_collector: added %d, updated %d, existing %d", resp.Added.Episodes, resp.Updated.Episodes, resp.Existing.Episodes) + return nil +} + +func (t *TraktCollector) collectMovie(m types.Media) error { + tmdbID, err := strconv.Atoi(m.Identifiers["tmdb"]) + log.Debugf("trakt_collector: collecting by tmdb id: %d", tmdbID) + if err != nil { + return err + } + resp, err := t.client.Collection(context.TODO(), &trakt.CollectionBody{ + Movies: []trakt.Movie{ + { + IDs: trakt.IDs{ + TMDb: tmdbID, + }, + }, + }, + }) + if err != nil { + return err + } + log.Debugf("trakt_collector: added %d, updated %d, existing %d", resp.Added.Movies, resp.Updated.Movies, resp.Existing.Movies) + return nil +} + +func (t *TraktCollector) Receive(in <-chan types.Media) { + log.Trace("started trakt_collector output") + for m := range in { + log.Tracef("trakt_collector: received_input %#v", m) + if m.Type == tv.TV { + log.Infof("trakt_collector: collecting tv") + if err := t.collectTV(m); err != nil { + log.Error(err) + } + } + if m.Type == movie.Movie { + log.Infof("trakt_collector: collecting movie") + if err := t.collectMovie(m); err != nil { + log.Error(err) + } + } + } +} + +func init() { + Register("trakt-collector", func() Output { + return &TraktCollector{ + Authfile: internaltrakt.DefaultAuthfile, + } + }) +} diff --git a/plugin/output/types.go b/plugin/output/types.go index 103a7e2..efd87fc 100644 --- a/plugin/output/types.go +++ b/plugin/output/types.go @@ -14,6 +14,8 @@ the end of processing. package output import ( + "context" + "github.com/rbtr/pachinko/types" log "github.com/sirupsen/logrus" ) @@ -21,7 +23,7 @@ import ( // Output is plugin interface to handle the result type Output interface { Receive(<-chan types.Media) - Init() error + Init(context.Context) error } var Registry map[string](func() Output) = map[string](func() Output){} diff --git a/plugin/processor/intra/tmdb.go b/plugin/processor/intra/tmdb.go index fe4cc09..0dc52b8 100644 --- a/plugin/processor/intra/tmdb.go +++ b/plugin/processor/intra/tmdb.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package intra import ( + "context" "strconv" "time" @@ -26,7 +27,7 @@ type TMDbClient struct { client *api.Client } -func (c *TMDbClient) Init() error { +func (c *TMDbClient) Init(context.Context) error { var err error if c.client, err = api.Init(c.APIKey); err != nil { return err @@ -64,14 +65,16 @@ func (c *TMDbClient) addTMDbMetadata(m types.Media) types.Media { log.Errorf("tmdb_decorator: error identifying movie: %s", err) return m } - - m.MovieMetadata.Title = movie.Title + log.Debugf("tmdb_decorator: got movie from tmdb: %v", movie) + m.Identifiers["tmdb"] = strconv.FormatInt(movie.ID, 10) + m.Identifiers["imdb"] = movie.IMDbID log.Debugf("tmdb_decorator: parsing release date: %s", movie.ReleaseDate) if p, err := time.Parse("2006-01-02", movie.ReleaseDate); err != nil { log.Error(err) } else { m.MovieMetadata.ReleaseYear = p.Year() } + m.MovieMetadata.Title = movie.Title log.Tracef("tmdb_decorator: populated %v from tmdb", m) return m } diff --git a/plugin/processor/intra/tvdb.go b/plugin/processor/intra/tvdb.go index 44e719f..e4692c8 100644 --- a/plugin/processor/intra/tvdb.go +++ b/plugin/processor/intra/tvdb.go @@ -28,7 +28,7 @@ import ( // WordMatcher regex var matcher *regexp.Regexp = regexp.MustCompile(`[^\w]`) -// Client TODO +// TVDbClient adds metadata from the TVDb type TVDbClient struct { APIKey string `mapstructure:"api-key"` RequestLimit int64 `mapstructure:"request-limit"` @@ -37,7 +37,7 @@ type TVDbClient struct { limiter *time.Ticker } -func (c *TVDbClient) Init() error { +func (c *TVDbClient) Init(context.Context) error { authn := &models.Auth{ Apikey: c.APIKey, } @@ -101,6 +101,7 @@ func (c *TVDbClient) addTVDBMetadata(m types.Media) types.Media { return m } log.Debugf("tvdb_decorator: got episode from tvdb: %v", ep) + m.Identifiers["tvdb"] = strconv.FormatInt(ep.ID, 10) m.TVMetadata.Name = series.SeriesName m.TVMetadata.AbsoluteNumber = int(ep.AbsoluteNumber) m.TVMetadata.Episode.Title = ep.EpisodeName diff --git a/plugin/processor/post/movie_path_solver.go b/plugin/processor/post/movie_path_solver.go index 04584d2..21cd598 100644 --- a/plugin/processor/post/movie_path_solver.go +++ b/plugin/processor/post/movie_path_solver.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package post import ( + "context" "fmt" "path" @@ -24,7 +25,7 @@ type MoviePathSolver struct { OutputFormat string `mapstructure:"format"` } -func (*MoviePathSolver) Init() error { +func (*MoviePathSolver) Init(context.Context) error { return nil } diff --git a/plugin/processor/post/tv_path_solver.go b/plugin/processor/post/tv_path_solver.go index 514f909..33679fd 100644 --- a/plugin/processor/post/tv_path_solver.go +++ b/plugin/processor/post/tv_path_solver.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package post import ( + "context" "fmt" "path" @@ -25,7 +26,7 @@ type TVPathSolver struct { OutputFormat string `mapstructure:"format"` } -func (*TVPathSolver) Init() error { +func (*TVPathSolver) Init(context.Context) error { return nil } diff --git a/plugin/processor/pre/movie.go b/plugin/processor/pre/movie.go index 1a350c6..8f73efa 100644 --- a/plugin/processor/pre/movie.go +++ b/plugin/processor/pre/movie.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package tvmeta import ( + "context" "regexp" "strconv" "strings" @@ -29,7 +30,7 @@ type MoviePreProcessor struct { matchers []*regexp.Regexp } -func (p *MoviePreProcessor) Init() error { +func (p *MoviePreProcessor) Init(context.Context) error { log.Trace("movie_path_metadata: initializing") for _, str := range p.MatcherStrings { r := regexp.MustCompile(str) diff --git a/plugin/processor/pre/movie_test.go b/plugin/processor/pre/movie_test.go index 0dec84b..547982b 100644 --- a/plugin/processor/pre/movie_test.go +++ b/plugin/processor/pre/movie_test.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package tvmeta import ( + "context" "testing" internaltesting "github.com/rbtr/pachinko/internal/testing" @@ -17,7 +18,7 @@ import ( func TestMoviePreProcessor_extractMetadata(t *testing.T) { tv := &MoviePreProcessor{MatcherStrings: defaultMovieMatchers, Sanitize: true} - tv.Init() + tv.Init(context.TODO()) for _, tt := range internaltesting.Movies { tt := tt for _, in := range tt.Inputs { @@ -45,7 +46,7 @@ func TestMoviePreProcessor_extractMetadata(t *testing.T) { func TestMoviePreProcessor_identify(t *testing.T) { p := &MoviePreProcessor{MatcherStrings: defaultMovieMatchers, Sanitize: true} - _ = p.Init() + _ = p.Init(context.TODO()) for _, tt := range internaltesting.Movies { tt := tt for _, in := range tt.Inputs { diff --git a/plugin/processor/pre/tv.go b/plugin/processor/pre/tv.go index 2373ac3..3a12a76 100644 --- a/plugin/processor/pre/tv.go +++ b/plugin/processor/pre/tv.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package tvmeta import ( + "context" "regexp" "strconv" "strings" @@ -33,7 +34,7 @@ type TVPreProcessor struct { matchers []*regexp.Regexp } -func (p *TVPreProcessor) Init() error { +func (p *TVPreProcessor) Init(context.Context) error { log.Trace("tv_path_metadata: initializing") for _, str := range p.MatcherStrings { r := regexp.MustCompile(str) diff --git a/plugin/processor/pre/tv_test.go b/plugin/processor/pre/tv_test.go index 7c53c92..5f0ead9 100644 --- a/plugin/processor/pre/tv_test.go +++ b/plugin/processor/pre/tv_test.go @@ -8,6 +8,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/. package tvmeta import ( + "context" "testing" internaltesting "github.com/rbtr/pachinko/internal/testing" @@ -17,7 +18,7 @@ import ( func TestTVPreProcessor_extractMetadata(t *testing.T) { tv := &TVPreProcessor{MatcherStrings: defaultTVMatchers, Sanitize: true} - tv.Init() + tv.Init(context.TODO()) for _, tt := range internaltesting.TV { tt := tt for _, in := range tt.Inputs { @@ -45,7 +46,7 @@ func TestTVPreProcessor_extractMetadata(t *testing.T) { func TestTVPreProcessor_identify(t *testing.T) { p := &TVPreProcessor{MatcherStrings: defaultTVMatchers, Sanitize: true} - _ = p.Init() + _ = p.Init(context.TODO()) for _, tt := range internaltesting.TV { tt := tt for _, in := range tt.Inputs { diff --git a/plugin/processor/types.go b/plugin/processor/types.go index b8eb5f8..2d3e992 100644 --- a/plugin/processor/types.go +++ b/plugin/processor/types.go @@ -1,6 +1,8 @@ package processor import ( + "context" + "github.com/rbtr/pachinko/types" log "github.com/sirupsen/logrus" ) @@ -18,13 +20,13 @@ const ( var Types []Type = []Type{Pre, Intra, Post} type Processor interface { - Init() error + Init(context.Context) error Process(<-chan types.Media, chan<- types.Media) } type Func func(<-chan types.Media, chan<- types.Media) -func (Func) Init() error { +func (Func) Init(context.Context) error { return nil } diff --git a/types/media.go b/types/media.go index eb4f0db..9245b3f 100644 --- a/types/media.go +++ b/types/media.go @@ -18,6 +18,7 @@ import ( // Media is the container struct for a file flowing through the entire pipeline type Media struct { + Identifiers map[string]string SourcePath string DestinationPath string Category Category