├── .drone.yml ├── .github └── FUNDING.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── genconf.go ├── root.go ├── sort.go ├── trakt.go └── version.go ├── docs ├── examples │ ├── pachinko.yaml │ ├── systemd │ │ ├── pachinko@.path │ │ └── pachinko@.service │ └── trakt └── plugins │ ├── outputs │ └── trakt.md │ └── processor │ └── deleter.md ├── go.mod ├── go.sum ├── gopher.png ├── internal ├── config │ ├── genconf.go │ ├── root.go │ ├── sort.go │ └── trakt.go ├── pipeline │ └── controller.go ├── plugin │ ├── output │ │ └── deleter.go │ └── processor │ │ └── pre │ │ ├── categorizer.go │ │ └── types.go ├── testing │ └── testing.go └── trakt │ └── trakt.go ├── main.go ├── plugin ├── include.go ├── input │ ├── path.go │ ├── path_test.go │ ├── testdata │ │ ├── a │ │ │ └── a.txt │ │ └── b │ │ │ ├── b.txt │ │ │ ├── b │ │ │ └── b.txt │ │ │ └── c │ │ │ ├── c.txt │ │ │ └── d │ │ │ └── d.txt │ └── types.go ├── output │ ├── logger.go │ ├── path_mover.go │ ├── trakt_collector.go │ └── types.go └── processor │ ├── intra │ ├── tmdb.go │ ├── tvdb.go │ └── tvdb_test.go │ ├── post │ ├── deleter.go │ ├── movie_path_solver.go │ └── tv_path_solver.go │ ├── pre │ ├── movie.go │ ├── movie_test.go │ ├── tv.go │ └── tv_test.go │ └── types.go └── types ├── category.go ├── item.go ├── matcher.go ├── matcher_test.go └── metadata ├── movie └── movie.go ├── tv └── tv.go ├── types.go └── video └── video.go /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: docker 3 | name: default 4 | 5 | steps: 6 | - name: fetch 7 | image: alpine/git 8 | commands: 9 | - git fetch --tags 10 | 11 | - name: testing 12 | image: golang:latest 13 | commands: 14 | - make test 15 | 16 | - name: lint 17 | image: golangci/golangci-lint 18 | commands: 19 | - make lint 20 | 21 | - name: release 22 | image: golang:latest 23 | when: 24 | branch: 25 | - master 26 | event: 27 | - tag 28 | environment: 29 | GITHUB_TOKEN: 30 | from_secret: GITHUB_TOKEN 31 | commands: 32 | - curl -sL https://git.io/goreleaser | bash 33 | 34 | trigger: 35 | branch: 36 | exclude: 37 | - github-pages 38 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rbtr -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | bin/ 17 | pachinko* 18 | .vscode 19 | .pachinko.yaml 20 | !docs/**/* 21 | trakt 22 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | 4 | linters: 5 | enable-all: true 6 | disable: 7 | - funlen 8 | - gochecknoglobals 9 | - gochecknoinits 10 | - gocognit 11 | - godox 12 | - goerr113 13 | - gomnd 14 | - lll 15 | - maligned 16 | - nestif 17 | - wsl 18 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | nfpms: 2 | - name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 3 | bindir: "/usr/local/bin" 4 | replacements: 5 | amd64: x86_64 6 | formats: 7 | - deb 8 | - rpm 9 | empty_folders: 10 | - /etc/pachinko 11 | changelog: 12 | sort: asc 13 | filters: 14 | exclude: 15 | - '^docs:' 16 | - '^test:' 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY bin/pachinko /pachinko 3 | ENTRYPOINT ["/pachinko"] 4 | CMD ["sort"] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | ORG = rbtr 3 | MODULE = pachinko 4 | 5 | VERSION = $(shell if [[ -z $$(git status --porcelain) ]] && [[ -n $$(git tag -l --points-at HEAD) ]]; then echo $$(git tag -l --points-at HEAD); else echo $$(git rev-parse --short HEAD); fi) 6 | 7 | LDFLAGS = -ldflags "-s -w -X github.com/$(ORG)/$(MODULER)/cmd.Version=$(VERSION)" 8 | GCFLAGS = -gcflags "all=-trimpath=$(PWD)" -asmflags "all=-trimpath=$(PWD)" 9 | GO_BUILD_ENV_VARS := CGO_ENABLED=0 GOOS=linux GOARCH=amd64 10 | 11 | version: ## version 12 | @echo $(VERSION) 13 | 14 | lint: ## lint 15 | @golangci-lint run -v 16 | 17 | test: ## run tests 18 | @go test ./... 19 | 20 | build: ## build 21 | $(GO_BUILD_ENV_VARS) \ 22 | go build \ 23 | -tags selinux \ 24 | $(GCFLAGS) \ 25 | $(LDFLAGS) \ 26 | -o bin/$(MODULE) ./ 27 | 28 | container: clean build ## container 29 | @buildah bud -t $(ORG)/$(MODULE):latest . 30 | @podman tag $(ORG)/$(MODULE):latest $(ORG)/$(MODULE):$(VERSION) 31 | @podman push $(ORG)/$(MODULE):latest 32 | @podman push $(ORG)/$(MODULE):$(VERSION) 33 | 34 | clean: ## clean workspace 35 | @rm -rf ./bin ./$(MODULE) 36 | 37 | help: ## print this help 38 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```text 4 | _ _ _ 5 | ___ ___ ___| |_|_|___| |_ ___ 6 | | . | .'| _| | | | '_| . | 7 | | _|__,|___|_|_|_|_|_|_,_|___| 8 | |_| 9 | 10 | modular, plugin based media sorter 11 | created by @rbtr 12 | ``` 13 | --- 14 | 15 | [![Build Status](https://cloud.drone.io/api/badges/rbtr/pachinko/status.svg)](https://cloud.drone.io/rbtr/pachinko) 16 | [![Go Report Card](https://goreportcard.com/badge/github.com/rbtr/pachinko)](https://goreportcard.com/report/github.com/rbtr/pachinko) 17 | [![Release](https://img.shields.io/github/release/rbtr/pachinko.svg)](https://github.com/rbtr/pachinko/releases/latest) 18 | [![Docker](https://img.shields.io/docker/pulls/rbtr/pachinko)](https://hub.docker.com/r/rbtr/pachinko) 19 | [![License](https://img.shields.io/github/license/rbtr/pachinko)](/LICENSE) 20 | 21 | > #### Updates! 22 | > pachinko is now part of the GitHub Sponsors program! If you enjoy it and want to support my work, please [check out my sponsor page](https://github.com/sponsors/rbtr) or click on the "💖 Sponsor" button up above! 23 | 24 | ### what it is 25 | pachinko is a media sorter. it integrates with the tvdb and the moviedb to, given a directory of reasonably named mix media, organize that media into a clean hierarchal directory structure ideal for use in media servers like plex, kodi/xbmc, etc. 26 | 27 | unlike some of the prior implementations of this idea, pachinko was designed from inception to be automation and container-friendly. 28 | it has no heavy gui - configure it through the config file or via flags, then execute it and walk away. 29 | 30 | it is written in go so that it is approachable for anyone interested in contributing without sacrificing performance. 31 | the plugin-style architecture keeps the core codebase clear and efficient. 32 | 33 | ### design 34 | 35 | #### plugins 36 | pachinko has a plugin based pipeline design. the base plugin types are: 37 | - input - add data from a datasource to the stream 38 | - processor - modify the datastream in-flight 39 | - output - deal with the processed data by e.g. moving files from their original dir to their sorted path. 40 | 41 | these base plugin types make pachinko flexible. composing a pipeline of many combination of plugins is possible. 42 | 43 | additionally there are subtypes of `processor` plugins: 44 | - preprocessor - parse data already present in the datastream to classify, clean, or add information to the data before main processor plugins run. the preprocessors make modifications to the datastream based only on the data already present in the objects in the datastream. 45 | - (intra)processor - the main working processors, where external datasources may be queried to enrich the datastream and significant modifications made. 46 | - postprocessor - last chance to modify the datastream before it is sent to outputs, but after the rich data has been added. the postprocessors make final modifications that shouldn't be the responsibility of the intraprocessors but may depend on the data enrichments that those have added. 47 | 48 | the subtypes exist mainly to allow ordering of plugin flow. 49 | 50 | #### datatypes 51 | pachinko currently supports these data types: 52 | - tv and 53 | - movie video files 54 | 55 | other datatypes planned include: images (and whatever you would like to contribute!) 56 | 57 | #### inputs 58 | pachinko currently supports these inputs: 59 | - local filesystem (`path`). 60 | 61 | other datastore types planned include : s3 (and whatever you would like to contribute!) 62 | 63 | #### outputs 64 | pachinko currently supports these outputs: 65 | - local filesystem (`path_mover`) 66 | - stdout (`logger`) 67 | - [trakt collector (`trakt_collector`)](docs/plugins/outputs/trakt.md) 68 | 69 | #### processors 70 | pachinko has the following optional processors: 71 | - tv identifier (pre-tv) 72 | - movie identifier (pre-movie) 73 | - tvdb (intra-tvdb) 74 | - tmdb (intra-tmdb) 75 | - tv path solver (post-tv_path_solver) 76 | - movie path solver (post-movie_path_solver) 77 | - file deleter (deleter) 78 | 79 | ### how to run it 80 | pachinko is distributed as a container and as a cross-platform binary. 81 | 82 | the container is recommended: 83 | ```bash 84 | $ docker run -v /path/to/source:/src:z -v /path/to/dest:/dest:z -v /path/to/cfg:/cfg rbtr/pachinko:latest --config /cfg 85 | ``` 86 | 87 | to run the binary: 88 | ```bash 89 | $ ./pachinko sort --config /path/to/config 90 | ``` 91 | 92 | ### options 93 | pachinko is configurable via file (yaml, toml), cli flags, or env vars. 94 | 95 | the config file is recommended: 96 | ```yaml 97 | dry-run: true 98 | log-level: debug 99 | inputs: [] 100 | outputs: [] 101 | processors: {} 102 | ``` 103 | 104 | the full, current list of options is available by running `./pachinko genconf` on the commandline. 105 | the core pachinko options are: 106 | 107 | | option | inputs | usage | 108 | | - | - | - | 109 | | conf | string | full path to config file - ignored in the config file | 110 | | dry-run | bool | dry-runs print only, pachkino will not make changes | 111 | | log-level | string | one of (trace,debug,info,warn,error) for logging verbosity | 112 | | log-format | string | one of (json,text) | 113 | 114 | 115 | inputs, outputs, and processors are lists of plugins objects and look generally like: 116 | 117 | ```yaml 118 | inputs: 119 | - name: path 120 | src-dir: /path/to/source 121 | outputs: 122 | - name: stdout 123 | ``` 124 | 125 | note that each plugin may have its own independent config options; refer to that plugin's docs for details on configuring that specific plugin. here, the `path` input plugin has a `src-dir` parameter that we configure in the plugin list item. 126 | 127 | the plugin list is processed in the written order and repeats are allowed. all loaded plugins are guaranteed to see each of the items in the datastream at least once. if the order that your datastream is processed by each plugin matters, make sure to load your plugins in the correct order! 128 | 129 | 130 | ### testimonials 131 | 132 | here's what users had to say when asked what they thought about `pachinko`: 133 | 134 | > Ew. _Pachinko_? Why would you name it _pachinko_? _Pachinko_ just makes me think of flashing lights and cigarette smoke. - a Japanese user 135 | 136 | ### license 137 | 138 | `pachinko` is licensed under MPL-2 which generally means you can use it however you like with few exceptions. 139 | 140 | read the full license terms [here](https://www.mozilla.org/en-US/MPL/2.0/FAQ/). 141 | 142 | --- 143 | 144 | created by @rbtr 145 | inspired by the functionality and frustrating user experience of: [sorttv by cliffe](https://sourceforge.net/projects/sorttv/), filebot, tinymediamanager, and others 146 | and the excellent architecture patterns of telegraf, caddy, coredns, and others 147 | -------------------------------------------------------------------------------- /cmd/genconf.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package cmd 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | 14 | "github.com/BurntSushi/toml" 15 | "github.com/mitchellh/mapstructure" 16 | "github.com/rbtr/pachinko/internal/config" 17 | log "github.com/sirupsen/logrus" 18 | "github.com/spf13/cobra" 19 | "github.com/spf13/viper" 20 | "gopkg.in/yaml.v2" 21 | ) 22 | 23 | // genconf represents the config command. 24 | var genconf = &cobra.Command{ 25 | Use: "genconf", 26 | Short: "Generate pachinko configs", 27 | Long: ` 28 | Use this command to generate pachinko configs. 29 | 30 | With no arguments, the config generated will contain the default 31 | configs for every compiled plug-in. 32 | $ pachinko genconf > config.yaml 33 | 34 | Note: the generated config is always in alphabetical order of keys. 35 | If order matters to pipeline plug-in execution, it will need 36 | to be reordered after generation. 37 | 38 | The config can be output as either yaml (default) or toml. 39 | $ pachinko genconf -o toml > config.toml 40 | 41 | To only generate stubs for a subset of plug-ins, pass the plug-in 42 | names as a comma separated list to the flag of their type. 43 | $ pachinko genconf --inputs=path --processors=tvid,movid > config.yaml 44 | 45 | The common flags (dry-run, logging) will be automatically set in the 46 | output config when they are used on the config command. 47 | $ pachinko genconf --log-level=debug > config.yaml 48 | 49 | The config file can then be edited and to fully customize the plug-ins 50 | and the pachinko pipeline. 51 | `, 52 | Run: func(cmd *cobra.Command, args []string) { 53 | log.SetLevel(log.TraceLevel) 54 | cfg, err := config.LoadGenconf(rootCtx) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | if err := cfg.Validate(); err != nil { 59 | log.Fatal(err) 60 | } 61 | sortCfg := config.NewSort(rootCtx) 62 | if err := cfg.DefaultConfig(sortCfg); err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | var out map[string]interface{} 67 | if err := mapstructure.Decode(sortCfg, &out); err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | buf := new(bytes.Buffer) 72 | switch cfg.Format { 73 | case "toml": 74 | if err := toml.NewEncoder(buf).Encode(out); err != nil { 75 | log.Fatal(err) 76 | } 77 | default: 78 | if err := yaml.NewEncoder(buf).Encode(out); err != nil { 79 | log.Fatal(err) 80 | } 81 | } 82 | fmt.Println(buf) 83 | }, 84 | } 85 | 86 | func init() { 87 | root.AddCommand(genconf) 88 | genconf.Flags().StringP("format", "o", "yaml", "config output format") 89 | genconf.Flags().StringSlice("inputs", []string{}, "comma-separated list of input plugins") 90 | genconf.Flags().StringSlice("outputs", []string{}, "comma-separated list of output plugins") 91 | genconf.Flags().StringSlice("processors", []string{}, "comma-separated list of processor plugins") 92 | if err := viper.BindPFlags(genconf.Flags()); err != nil { 93 | log.Fatal(err) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package cmd 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "os" 14 | "os/signal" 15 | "syscall" 16 | 17 | homedir "github.com/mitchellh/go-homedir" 18 | log "github.com/sirupsen/logrus" 19 | "github.com/spf13/cobra" 20 | "github.com/spf13/viper" 21 | 22 | // include plugins 23 | _ "github.com/rbtr/pachinko/plugin" 24 | ) 25 | 26 | var cfgFile string 27 | var rootCtx context.Context 28 | 29 | // root represents the base command when called without any subcommands. 30 | var root = &cobra.Command{ 31 | Use: "pachinko", 32 | Long: ` 33 | _ _ _ 34 | ___ ___ ___| |_|_|___| |_ ___ 35 | | . | .'| _| | | | '_| . | 36 | | _|__,|___|_|_|_|_|_|_,_|___| 37 | |_| 38 | 39 | pluggable media sorter`, 40 | } 41 | 42 | func Execute() { 43 | if err := root.Execute(); err != nil { 44 | fmt.Println(err) 45 | os.Exit(1) 46 | } 47 | } 48 | 49 | func init() { 50 | cobra.OnInitialize(rootConfig) 51 | 52 | // bind root flags 53 | root.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.pachinko.yaml)") 54 | root.PersistentFlags().Bool("dry-run", false, "run pipeline as read only and do not make changes") 55 | root.PersistentFlags().StringP("log-level", "v", "info", "log verbosity (trace,debug,info,warn,error)") 56 | root.PersistentFlags().String("log-format", "text", "log format (text,json)") 57 | if err := viper.BindPFlags(root.PersistentFlags()); err != nil { 58 | log.Fatal(err) 59 | } 60 | 61 | // init signal channels 62 | var cancel context.CancelFunc 63 | rootCtx, cancel = context.WithCancel(context.Background()) 64 | 65 | sig := make(chan os.Signal, 1) 66 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 67 | go func() { 68 | <-sig 69 | log.Debug("caught exit sigal, exiting") 70 | cancel() 71 | os.Exit(0) 72 | }() 73 | } 74 | 75 | func rootConfig() { 76 | if cfgFile != "" { 77 | viper.SetConfigFile(cfgFile) 78 | } else { 79 | home, err := homedir.Dir() 80 | if err != nil { 81 | fmt.Println(err) 82 | os.Exit(1) 83 | } 84 | viper.AddConfigPath(home) 85 | viper.SetConfigName(".pachinko") 86 | } 87 | 88 | viper.AutomaticEnv() 89 | if err := viper.ReadInConfig(); err == nil { 90 | log.Debugf("Using config file: %s", viper.ConfigFileUsed()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cmd/sort.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package cmd 9 | 10 | import ( 11 | "github.com/rbtr/pachinko/internal/config" 12 | "github.com/rbtr/pachinko/internal/pipeline" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // sort represents the sort command. 18 | var sort = &cobra.Command{ 19 | Use: "sort", 20 | Short: "Run the sorting pipeline.", 21 | Long: ` 22 | Use this command to execute the sorting pipeline. 23 | 24 | With no arguments, sort will load the config from $HOME/.pachinko.yaml. 25 | $ pachinko sort 26 | 27 | If no config is provided, no plugins will be loaded and the pipeline will 28 | not do anything useful. 29 | `, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | log.SetLevel(log.TraceLevel) 32 | sortConf, err := config.LoadSort(rootCtx) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | if err := sortConf.Validate(); err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | p := pipeline.NewPipeline() 41 | if err := sortConf.ConfigurePipeline(p); err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | if err := p.Run(rootCtx); err != nil { 46 | log.Fatal(err) 47 | } 48 | }, 49 | } 50 | 51 | func init() { 52 | root.AddCommand(sort) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/trakt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package cmd 9 | 10 | import ( 11 | "github.com/rbtr/pachinko/internal/config" 12 | internaltrakt "github.com/rbtr/pachinko/internal/trakt" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | var traktFile string 19 | 20 | // trakt represents the trakt connect command. 21 | var trakt = &cobra.Command{ 22 | Use: "trakt", 23 | Short: "Connect to Trakt", 24 | Long: ` 25 | Use this command to connect Pachinko to Trakt. 26 | 27 | Trakt requires that you connect and authorize Pachinko before it can make any 28 | changes on your behalf. 29 | 30 | This command will print a URL and code. Open the URL in your browser, sign in 31 | to Trakt (if you aren't signed in already), and enter the code. 32 | 33 | Pachinko will write the authorized credentials out to the file specified in 34 | the "--authfile" flag. The credential is stored in a JSON notation: 35 | { 36 | "access-token": "[access-token]", 37 | "client-id": "76a0c1e8d3331021f6e312115e27fe4c29f4ef23ef89a0a69143a62d136ab994", 38 | "client-secret": "fe8d1f0921413028f92428d2922e13a728e27d2f35b26e315cf3dde31228568d", 39 | "created": "[created-at]", 40 | "expires": "7776000", 41 | "refresh-token": "[refresh-token" 42 | } 43 | 44 | This credential file is portable and can be moved around with your 45 | Pachinko install. Pachinko will automatically refresh it every 80 days during 46 | normal operations. 47 | 48 | The token expires after 90 days. If Pachinko can't refresh the token before 49 | it expires, you will need to rerun this to generate a new authorization. 50 | `, 51 | Run: func(cmd *cobra.Command, args []string) { 52 | log.SetLevel(log.TraceLevel) 53 | cfg, err := config.LoadTrakt(rootCtx) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | if err := cfg.Validate(); err != nil { 58 | log.Fatal(err) 59 | } 60 | auth, err := internaltrakt.ReadAuthFile(cfg.Authfile) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | client, err := internaltrakt.NewTrakt(auth) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | if auth, err = client.Authorize(rootCtx); err != nil { 69 | log.Fatal(err) 70 | } 71 | if err := internaltrakt.WriteAuthFile(cfg.Authfile, auth); err != nil { 72 | log.Fatal(err) 73 | } 74 | }, 75 | } 76 | 77 | func init() { 78 | root.AddCommand(trakt) 79 | trakt.Flags().StringVar(&traktFile, "authfile", internaltrakt.DefaultAuthfile, "where to save the trakt authorization credential") 80 | trakt.Flags().BoolP("overwrite", "f", false, "overwrite the authfile if it exists already") 81 | if err := viper.BindPFlags(trakt.Flags()); err != nil { 82 | log.Fatal(err) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package cmd 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var Version string 17 | 18 | // version command. 19 | var version = &cobra.Command{ 20 | Use: "version", 21 | Short: "the pachinko version", 22 | Long: ` 23 | Outputs the version of Pachinko. 24 | `, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | fmt.Println(Version) 27 | }, 28 | } 29 | 30 | func init() { 31 | root.AddCommand(version) 32 | } 33 | -------------------------------------------------------------------------------- /docs/examples/pachinko.yaml: -------------------------------------------------------------------------------- 1 | # /etc/pachinko/pachinko.yaml 2 | dry-run: false 3 | inputs: 4 | - name: filepath 5 | src-dir: /src 6 | log-format: "text" 7 | log-level: "info" 8 | outputs: 9 | - name: stdout 10 | - create-dirs: true 11 | name: path-mover 12 | overwrite: false 13 | - authfile: "/etc/pachinko/trakt" 14 | name: trakt-collector 15 | pipeline: 16 | buffer: 10 17 | processors: 18 | intra: 19 | - api-key: "2ba61c9f36d53da5ff58042ec71edeee" 20 | name: tmdb 21 | - api-key: "1ffca36f894fd585649d26b1fdc48d8c" 22 | name: tvdb 23 | request-limit: 10 24 | post: 25 | - dest-dir: /media 26 | movie-dirs: true 27 | movie-prefix: movies 28 | name: movie-path-solver 29 | - dest-dir: /media 30 | episode-names: false 31 | name: tv-path-solver 32 | season-dirs: true 33 | tv-prefix: tv 34 | - name: deleter 35 | pre: 36 | - name: movie 37 | sanitize-name: true 38 | - name: tv 39 | sanitize-name: true 40 | -------------------------------------------------------------------------------- /docs/examples/systemd/pachinko@.path: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/pachinko@.path 2 | # enable with `systemctl enable pachinko@.path` 3 | # where "path-to-src" is the output of `systemd-escape /path/to/src` 4 | [Unit] 5 | Description=Run pachinko automatically when files are added to %I 6 | 7 | [Path] 8 | DirectoryNotEmpty=%I 9 | 10 | [Install] 11 | WantedBy=default.target 12 | -------------------------------------------------------------------------------- /docs/examples/systemd/pachinko@.service: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/pachinko@.service 2 | [Unit] 3 | Description=Run pachinko from %I 4 | 5 | [Service] 6 | Type=simple 7 | Restart=on-failure 8 | ExecStart=/usr/local/bin/pachinko sort --config /etc/pachinko/pachinko.yaml 9 | -------------------------------------------------------------------------------- /docs/examples/trakt: -------------------------------------------------------------------------------- 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"} 2 | -------------------------------------------------------------------------------- /docs/plugins/outputs/trakt.md: -------------------------------------------------------------------------------- 1 | ### Trakt and the Trakt Collector output 2 | Pachinko can interact with Trakt. Currently, Pachinko can: 3 | - add sorted items to your Trakt collection (`trakt_collector` output plugin) 4 | 5 | #### Trakt Authorization 6 | To communicate with Trakt, it needs an access token. A helper command is included for authorizating: 7 | ```bash 8 | $ pachinko trakt 9 | Authenticating in Trakt! 10 | Please open in your browser: https://trakt.tv/activate 11 | and enter the code: 1234A1AA 12 | ``` 13 | 14 | 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. 15 | 16 | The [authfile](../../examples/trakt) is JSON and contains authorization credentials. 17 | 18 | #### Trakt Collector 19 | To add items to your Trakt collection when Pachinko is done processing them, enable the Trakt Collector output plugin in your Pachinko config file. 20 | 21 | The only configurable option is the authfile location - point it at the authfile created by the authorization step as described [above](#trakt-authorization). 22 | 23 | ```yaml 24 | #... 25 | outputs: 26 | - name: trakt-collector 27 | authfile: "/etc/pachinko/trakt" 28 | 29 | #... 30 | ``` 31 | 32 | Now when Pachinko identifies and processes TV or Movies they will be automatically collected in Trakt! 33 | -------------------------------------------------------------------------------- /docs/plugins/processor/deleter.md: -------------------------------------------------------------------------------- 1 | ### File deleter processor 2 | The deleter processor marks files for deletion by the internal deletion output. This allows files with certain extensions, empty directories, or that match specified regexps to be deleted after Pachinko has finished sorting. 3 | 4 | #### Configuration 5 | The default deleter plugin configuration is: 6 | ```yaml 7 | - categories: [] 8 | directories: true 9 | extensions: 10 | - 7z 11 | - gz 12 | - gzip 13 | - rar 14 | - tar 15 | - zip 16 | - bmp 17 | - gif 18 | - heic 19 | - jpeg 20 | - jpg 21 | - png 22 | - tiff 23 | - info 24 | - nfo 25 | - txt 26 | - website 27 | matchers: [] 28 | name: deleter 29 | ``` 30 | 31 | |||| 32 | |-|-|-| 33 | |`categories`|`[]string`|[unimplemented] list of categories of file such as text or archive to remove.| 34 | |`directories`|`bool`|whether to remove directories. Even if true, only *empty* dirs will be removed.| 35 | |`extensions`|`[]string` | list of file extensions to remove.| 36 | |`matchers`|`[]string` | regexps to match files to remove.| 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rbtr/pachinko 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/cyruzin/golang-tmdb v1.3.1 8 | github.com/kr/text v0.2.0 // indirect 9 | github.com/lithammer/fuzzysearch v1.1.0 10 | github.com/mitchellh/go-homedir v1.1.0 11 | github.com/mitchellh/mapstructure v1.1.2 12 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 13 | github.com/pkg/errors v0.9.1 14 | github.com/rbtr/go-trakt v0.0.0-20200310010953-144101cfef69 15 | github.com/rbtr/go-tvdb v0.0.0-20200127015222-6fcb5ef30e70 16 | github.com/sirupsen/logrus v1.4.2 17 | github.com/spf13/cobra v0.0.6 18 | github.com/spf13/pflag v1.0.5 // indirect 19 | github.com/spf13/viper v1.6.2 20 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect 21 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e // indirect 22 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 23 | gopkg.in/yaml.v2 v2.2.8 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 6 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 7 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 8 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 9 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 12 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 13 | github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 14 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= 15 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 16 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 17 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 18 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 19 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 20 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 21 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 22 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 23 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 24 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 25 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 26 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 27 | github.com/cyruzin/golang-tmdb v1.3.1 h1:zE7CEDVqpzEUhpnKQKp+kNs3KBvb8dqe/16ApzYmW+w= 28 | github.com/cyruzin/golang-tmdb v1.3.1/go.mod h1:O+rbwyyMRUmPpBIHCyCueKsH0NklhJB3b1dHeXh/Bf0= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 33 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 34 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 35 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 36 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 37 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 38 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 39 | github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 40 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 41 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 42 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 43 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 44 | github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= 45 | github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= 46 | github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= 47 | github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= 48 | github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= 49 | github.com/go-openapi/analysis v0.19.5 h1:8b2ZgKfKIUTVQpTb77MoRDIMEIwvDVw40o3aOXdfYzI= 50 | github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= 51 | github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= 52 | github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= 53 | github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= 54 | github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= 55 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 56 | github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 57 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 58 | github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= 59 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 60 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 61 | github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 62 | github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= 63 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 64 | github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= 65 | github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= 66 | github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= 67 | github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= 68 | github.com/go-openapi/loads v0.19.3 h1:jwIoahqCmaA5OBoc/B+1+Mu2L0Gr8xYQnbeyQEo/7b0= 69 | github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= 70 | github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= 71 | github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= 72 | github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= 73 | github.com/go-openapi/runtime v0.19.5 h1:h4Zk7oTfB3ZYM2oMNliQvL+3BrDstTIX8lqP7yaYCuI= 74 | github.com/go-openapi/runtime v0.19.5/go.mod h1:WIH6IYPXOrtgTClTV8xzdrD20jBlrK25D0aQbdSlqp8= 75 | github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 76 | github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 77 | github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= 78 | github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= 79 | github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= 80 | github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= 81 | github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= 82 | github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= 83 | github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= 84 | github.com/go-openapi/strfmt v0.19.3 h1:eRfyY5SkaNJCAwmmMcADjY31ow9+N7MCLW7oRkbsINA= 85 | github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= 86 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 87 | github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 88 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 89 | github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= 90 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 91 | github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= 92 | github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= 93 | github.com/go-openapi/validate v0.19.3 h1:PAH/2DylwWcIU1s0Y7k3yNmeAgWOcKrNE2Q7Ww/kCg4= 94 | github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= 95 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 96 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 97 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 98 | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= 99 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 100 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 101 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 102 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 103 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 104 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 105 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 106 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 107 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 108 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 109 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 110 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 111 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 112 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 113 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 114 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 115 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 116 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 117 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 118 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 119 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 120 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 121 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 122 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 123 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 124 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 125 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 126 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 127 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 128 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 129 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 130 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 131 | github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= 132 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 133 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 134 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 135 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 136 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 137 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 138 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 139 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 140 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 141 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 142 | github.com/lithammer/fuzzysearch v1.1.0 h1:go9v8tLCrNTTlH42OAaq4eHFe81TDHEnlrMEb6R4f+A= 143 | github.com/lithammer/fuzzysearch v1.1.0/go.mod h1:Bqx4wo8lTOFcJr3ckpY6HA9lEIOO0H5HrkJ5CsN56HQ= 144 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 145 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 146 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 147 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 148 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 149 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 150 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= 151 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 152 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 153 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 154 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 155 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 156 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 157 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 158 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 159 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 160 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 161 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 162 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 163 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 164 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 165 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 166 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 167 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 168 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 169 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 170 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 171 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 172 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 173 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 174 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 175 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 176 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 177 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 178 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 179 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 180 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 181 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 182 | github.com/rbtr/go-trakt v0.0.0-20200310010953-144101cfef69 h1:qYy5bg6U4JgWzXcl8FjzyzVaqIGpAfve2v5J+ZxJG/s= 183 | github.com/rbtr/go-trakt v0.0.0-20200310010953-144101cfef69/go.mod h1:C8/th48mq7Wm+2OKaQWZ6o8JtU31PzIqd0Jnu+aZgyA= 184 | github.com/rbtr/go-tvdb v0.0.0-20200127015222-6fcb5ef30e70 h1:QErGMHDOJst097L83bKNCh8YbMGTnDNATLiGf9mnTAk= 185 | github.com/rbtr/go-tvdb v0.0.0-20200127015222-6fcb5ef30e70/go.mod h1:AcL7ia8jMFxrExAGzZ/qLtdyZQa3uDzVHYoi+ZK3PnA= 186 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 187 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 188 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 189 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 190 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 191 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 192 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 193 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 194 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 195 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 196 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 197 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 198 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 199 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 200 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 201 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 202 | github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= 203 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 204 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 205 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 206 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 207 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 208 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 209 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 210 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 211 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 212 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 213 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 214 | github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 215 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 216 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 217 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 218 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 219 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 220 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 221 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 222 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 223 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 224 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 225 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 226 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 227 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 228 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 229 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 230 | go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= 231 | go.mongodb.org/mongo-driver v1.1.1 h1:Sq1fR+0c58RME5EoqKdjkiQAmPjmfHlZOoRI6fTUOcs= 232 | go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= 233 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 234 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 235 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 236 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 237 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 238 | golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 239 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 240 | golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 241 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 242 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 243 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 244 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 245 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 246 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 247 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 248 | golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 249 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 250 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 251 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 252 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 253 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= 254 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 255 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 256 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 257 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 258 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 259 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 260 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 261 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 262 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 263 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 264 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 265 | golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 266 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 267 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 268 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 269 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= 270 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 271 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 272 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 273 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 274 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 275 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 276 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 277 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 278 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 279 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 280 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 281 | golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 282 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 283 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 284 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 285 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 286 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 287 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 288 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 289 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 290 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 291 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 292 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 293 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 294 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 295 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 296 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 297 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 298 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 299 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 300 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 301 | -------------------------------------------------------------------------------- /gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbtr/pachinko/d9e3ef83607e5a12a3743dbadb754a25bed64170/gopher.png -------------------------------------------------------------------------------- /internal/config/genconf.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package config 9 | 10 | import ( 11 | "context" 12 | "strings" 13 | 14 | "github.com/mitchellh/mapstructure" 15 | "github.com/rbtr/pachinko/plugin/input" 16 | "github.com/rbtr/pachinko/plugin/output" 17 | "github.com/rbtr/pachinko/plugin/processor" 18 | log "github.com/sirupsen/logrus" 19 | "github.com/spf13/viper" 20 | ) 21 | 22 | type Genconf struct { 23 | Root `mapstructure:",squash"` 24 | Format string `mapstructure:"format"` 25 | Inputs []string `mapstructure:"inputs"` 26 | Outputs []string `mapstructure:"outputs"` 27 | Processors map[processor.Type][]string `mapstructure:"processors"` 28 | } 29 | 30 | func (c *Genconf) DefaultConfig(p *Sort) error { 31 | if len(c.Inputs) == 0 && len(c.Outputs) == 0 && len(c.Processors) == 0 { 32 | // no plugins specified, dump configs for them all 33 | for k := range input.Registry { 34 | c.Inputs = append(c.Inputs, k) 35 | } 36 | for k := range output.Registry { 37 | c.Outputs = append(c.Outputs, k) 38 | } 39 | for _, t := range []processor.Type{processor.Pre, processor.Post, processor.Intra} { 40 | for k := range processor.Registry[t] { 41 | c.Processors[t] = append(c.Processors[t], k) 42 | } 43 | } 44 | } 45 | 46 | for _, name := range c.Inputs { 47 | log.Tracef("making default config for plugin %s", name) 48 | if initializer, ok := input.Registry[name]; ok { 49 | var out map[string]interface{} 50 | if err := mapstructure.Decode(initializer(), &out); err != nil { 51 | return err 52 | } 53 | out["name"] = name 54 | p.Inputs = append(p.Inputs, out) 55 | } 56 | } 57 | 58 | for _, name := range c.Outputs { 59 | log.Tracef("making default config for plugin %s", name) 60 | if initializer, ok := output.Registry[name]; ok { 61 | var out map[string]interface{} 62 | if err := mapstructure.Decode(initializer(), &out); err != nil { 63 | return err 64 | } 65 | out["name"] = name 66 | p.Outputs = append(p.Outputs, out) 67 | } 68 | } 69 | 70 | for t, names := range c.Processors { 71 | for _, name := range names { 72 | log.Tracef("making default config for plugin %s", name) 73 | if initializer, ok := processor.Registry[t][name]; ok { 74 | var out map[string]interface{} 75 | if err := mapstructure.Decode(initializer(), &out); err != nil { 76 | return err 77 | } 78 | out["name"] = name 79 | p.Processors[t] = append(p.Processors[t], out) 80 | } 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func LoadGenconf(ctx context.Context) (*Genconf, error) { 88 | cfg := &Genconf{} 89 | cfg.ctx = ctx 90 | viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) 91 | viper.AutomaticEnv() 92 | err := viper.Unmarshal(cfg) 93 | return cfg, err 94 | } 95 | -------------------------------------------------------------------------------- /internal/config/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package config 9 | 10 | import ( 11 | "context" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Root struct { 17 | // nolint: structcheck 18 | ctx context.Context 19 | DryRun bool `mapstructure:"dry-run"` 20 | LogLevel string `mapstructure:"log-level"` 21 | LogFormat string `mapstructure:"log-format"` 22 | } 23 | 24 | func (c *Root) configLogger() { 25 | log.SetLevel(log.InfoLevel) 26 | switch c.LogFormat { 27 | case "json": 28 | log.SetFormatter(&log.JSONFormatter{}) 29 | default: 30 | log.SetFormatter(&log.TextFormatter{}) 31 | } 32 | if c.LogLevel != "" { 33 | if lvl, err := log.ParseLevel(c.LogLevel); err != nil { 34 | log.Fatal(err) 35 | } else { 36 | log.SetLevel(lvl) 37 | } 38 | } 39 | } 40 | 41 | // Validate validate. 42 | func (c *Root) Validate() error { 43 | c.configLogger() 44 | log.Debugf("loaded config: %+v", *c) 45 | if c.DryRun { 46 | log.Warn("DRY RUN: no changes will be made") 47 | } 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/config/sort.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package config 9 | 10 | import ( 11 | "context" 12 | "strings" 13 | 14 | "github.com/mitchellh/mapstructure" 15 | "github.com/rbtr/pachinko/internal/pipeline" 16 | internalout "github.com/rbtr/pachinko/internal/plugin/output" 17 | internalpre "github.com/rbtr/pachinko/internal/plugin/processor/pre" 18 | "github.com/rbtr/pachinko/plugin/input" 19 | "github.com/rbtr/pachinko/plugin/output" 20 | "github.com/rbtr/pachinko/plugin/processor" 21 | "github.com/spf13/viper" 22 | ) 23 | 24 | type Sort struct { 25 | Root `mapstructure:",squash"` 26 | Pipeline pipeline.Config `mapstructure:"pipeline"` 27 | Inputs []map[string]interface{} `mapstructure:"inputs"` 28 | Outputs []map[string]interface{} `mapstructure:"outputs"` 29 | Processors map[processor.Type][]map[string]interface{} `mapstructure:"processors"` 30 | } 31 | 32 | func (c *Sort) ConfigurePipeline(pipe *pipeline.Pipeline) error { 33 | if err := mapstructure.Decode(c.Pipeline, pipe); err != nil { 34 | return err 35 | } 36 | for _, p := range c.Inputs { 37 | if name, ok := p["name"]; ok { 38 | if initializer, ok := input.Registry[name.(string)]; ok { 39 | plugin := initializer() 40 | if err := mapstructure.Decode(p, plugin); err != nil { 41 | return err 42 | } 43 | if err := plugin.Init(c.ctx); err != nil { 44 | return err 45 | } 46 | pipe.WithInputs(plugin) 47 | } 48 | } 49 | } 50 | 51 | ocfg := output.Config{ 52 | DryRun: c.DryRun, 53 | } 54 | 55 | for _, p := range c.Outputs { 56 | if name, ok := p["name"]; ok { 57 | if initializer, ok := output.Registry[name.(string)]; ok { 58 | plugin := initializer() 59 | if err := mapstructure.Decode(p, plugin); err != nil { 60 | return err 61 | } 62 | if err := plugin.Init(c.ctx, ocfg); err != nil { 63 | return err 64 | } 65 | pipe.WithOutputs(plugin) 66 | } 67 | } 68 | } 69 | 70 | deleter := &internalout.Deleter{} 71 | if err := deleter.Init(c.ctx, ocfg); err != nil { 72 | return err 73 | } 74 | pipe.WithOutputs(deleter) 75 | 76 | categorizer := internalpre.NewCategorizer() 77 | if err := categorizer.Init(c.ctx); err != nil { 78 | return err 79 | } 80 | pipe.WithProcessors(categorizer) 81 | 82 | for _, t := range processor.Types { 83 | for _, p := range c.Processors[t] { 84 | if name, ok := p["name"]; ok { 85 | if initializer, ok := processor.Registry[t][name.(string)]; ok { 86 | plugin := initializer() 87 | if err := mapstructure.Decode(p, plugin); err != nil { 88 | return err 89 | } 90 | if err := plugin.Init(c.ctx); err != nil { 91 | return err 92 | } 93 | pipe.WithProcessors(plugin) 94 | } 95 | } 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | func NewSort(ctx context.Context) *Sort { 103 | cfg := &Sort{ 104 | Processors: map[processor.Type][]map[string]interface{}{ 105 | processor.Pre: {}, 106 | processor.Intra: {}, 107 | processor.Post: {}, 108 | }, 109 | } 110 | cfg.ctx = ctx 111 | return cfg 112 | } 113 | 114 | // LoadConfig loadconfig. 115 | func LoadSort(ctx context.Context) (*Sort, error) { 116 | cfg := NewSort(ctx) 117 | viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) 118 | viper.AutomaticEnv() 119 | err := viper.Unmarshal(cfg) 120 | return cfg, err 121 | } 122 | -------------------------------------------------------------------------------- /internal/config/trakt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package config 9 | 10 | import ( 11 | "context" 12 | "strings" 13 | 14 | "github.com/pkg/errors" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | type Trakt struct { 19 | Root `mapstructure:",squash"` 20 | Authfile string `mapstructure:"authfile"` 21 | Overwrite bool `mapstructure:"overwrite"` 22 | } 23 | 24 | func LoadTrakt(ctx context.Context) (*Trakt, error) { 25 | cfg := &Trakt{} 26 | cfg.ctx = ctx 27 | viper.SetEnvKeyReplacer(strings.NewReplacer("_", "-")) 28 | viper.AutomaticEnv() 29 | err := viper.Unmarshal(cfg) 30 | return cfg, err 31 | } 32 | 33 | func (t *Trakt) Validate() error { 34 | if t.Authfile == "" { 35 | return errors.New("authfile must be set") 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/pipeline/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package pipeline 9 | 10 | import ( 11 | "context" 12 | "sync" 13 | 14 | "github.com/rbtr/pachinko/plugin/input" 15 | "github.com/rbtr/pachinko/plugin/output" 16 | "github.com/rbtr/pachinko/plugin/processor" 17 | "github.com/rbtr/pachinko/types" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | type Config struct { 22 | Buffer int `mapstructure:"buffer"` 23 | } 24 | 25 | // Pipeline pipeline. 26 | type Pipeline struct { 27 | Config 28 | inputs []input.Input 29 | processors []processor.Processor 30 | outputs []output.Output 31 | } 32 | 33 | func NewPipeline() *Pipeline { 34 | return &Pipeline{ 35 | Config: Config{Buffer: 1}, 36 | } 37 | } 38 | 39 | func (p *Pipeline) runInputs(ctx context.Context, sink chan<- types.Item) { 40 | var wg sync.WaitGroup 41 | for _, input := range p.inputs { 42 | wg.Add(1) 43 | go func(_ context.Context, f func(chan<- types.Item), source chan<- types.Item) { 44 | defer wg.Done() 45 | f(source) 46 | }(ctx, input.Consume, sink) 47 | } 48 | wg.Wait() 49 | log.Debug("pipeline: inputs finished") 50 | } 51 | 52 | func (p *Pipeline) runProcessors(ctx context.Context, source, sink chan types.Item) { 53 | // this noop post-processor attaches the final internal input stream to the 54 | // external sink 55 | p.processors = processor.AppendFunc(p.processors, func(in <-chan types.Item, _ chan<- types.Item) { 56 | for m := range in { 57 | sink <- m 58 | } 59 | }) 60 | var wg sync.WaitGroup 61 | in := source 62 | out := make(chan types.Item) 63 | for _, processor := range p.processors { 64 | wg.Add(1) 65 | go func(_ context.Context, f func(<-chan types.Item, chan<- types.Item), in <-chan types.Item, out chan<- types.Item) { 66 | defer wg.Done() 67 | f(in, out) 68 | close(out) 69 | }(ctx, processor.Process, in, out) 70 | in = out 71 | out = make(chan types.Item) 72 | } 73 | wg.Wait() 74 | log.Debug("pipeline: processors finished") 75 | } 76 | 77 | func (p *Pipeline) runOutputs(ctx context.Context, source chan types.Item) { 78 | sinks := []chan<- types.Item{} 79 | var wg sync.WaitGroup 80 | for _, output := range p.outputs { 81 | wg.Add(1) 82 | out := make(chan types.Item) 83 | go func(_ context.Context, f func(<-chan types.Item), in <-chan types.Item) { 84 | defer wg.Done() 85 | f(in) 86 | }(ctx, output.Receive, out) 87 | sinks = append(sinks, out) 88 | } 89 | 90 | wg.Add(1) 91 | go func(ctx context.Context, in <-chan types.Item, outs []chan<- types.Item) { 92 | var wgOut sync.WaitGroup 93 | defer wg.Done() 94 | for m := range in { 95 | for _, out := range outs { 96 | wgOut.Add(1) 97 | go func(_ context.Context, i types.Item, o chan<- types.Item) { 98 | defer wgOut.Done() 99 | o <- i 100 | }(ctx, m, out) 101 | } 102 | } 103 | wgOut.Wait() 104 | for _, o := range outs { 105 | close(o) 106 | } 107 | }(ctx, source, sinks) 108 | wg.Wait() 109 | log.Debug("pipeline: outputs finished") 110 | } 111 | 112 | func (p *Pipeline) WithInputs(inputs ...input.Input) { 113 | p.inputs = append(p.inputs, inputs...) 114 | } 115 | 116 | func (p *Pipeline) WithProcessors(processors ...processor.Processor) { 117 | p.processors = append(p.processors, processors...) 118 | } 119 | 120 | func (p *Pipeline) WithOutputs(outputs ...output.Output) { 121 | p.outputs = append(p.outputs, outputs...) 122 | } 123 | 124 | func (p *Pipeline) Run(ctx context.Context) error { 125 | log.Debug("running pipeline") 126 | 127 | var wg sync.WaitGroup 128 | in := make(chan types.Item, p.Buffer) 129 | out := make(chan types.Item, p.Buffer) 130 | 131 | wg.Add(1) 132 | go func(ctx context.Context, sink chan types.Item) { 133 | log.Trace("pipeline: executing input thread") 134 | defer wg.Done() 135 | p.runInputs(ctx, sink) 136 | log.Trace("pipeline: closing input chan") 137 | close(sink) 138 | }(ctx, in) 139 | 140 | wg.Add(1) 141 | go func(ctx context.Context, source, sink chan types.Item) { 142 | log.Trace("pipeline: executing processor thread") 143 | defer wg.Done() 144 | p.runProcessors(ctx, source, sink) 145 | log.Trace("pipeline: closing processor chan") 146 | close(sink) 147 | }(ctx, in, out) 148 | 149 | wg.Add(1) 150 | go func(ctx context.Context, source chan types.Item) { 151 | log.Trace("pipeline: executing output thread") 152 | defer wg.Done() 153 | p.runOutputs(ctx, source) 154 | }(ctx, out) 155 | 156 | log.Debug("pipeline: waiting for threads to finish") 157 | wg.Wait() 158 | log.Debug("pipeline: threads finished") 159 | 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /internal/plugin/output/deleter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package output 9 | 10 | import ( 11 | "container/heap" 12 | "context" 13 | "os" 14 | 15 | "github.com/rbtr/pachinko/plugin/output" 16 | "github.com/rbtr/pachinko/types" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | // Deleter is a deleter output used to clean up chaff. 21 | type Deleter struct { 22 | dryRun bool 23 | } 24 | 25 | // Init init. 26 | func (d *Deleter) Init(ctx context.Context, cfg output.Config) error { 27 | d.dryRun = cfg.DryRun 28 | return nil 29 | } 30 | 31 | // Receive implements the Plugin interface on the Deleter. 32 | func (d *Deleter) Receive(c <-chan types.Item) { 33 | log.Trace("started deleter output") 34 | h := &stringHeap{} 35 | for m := range c { 36 | log.Debugf("deleter_output: received_input %#v", m) 37 | if m.Delete { 38 | log.Infof("deleter_output: queueing %s", m.SourcePath) 39 | heap.Push(h, m.SourcePath) 40 | } 41 | } 42 | for h.Len() > 0 { 43 | path := heap.Pop(h).(string) 44 | log.Infof("deleter_output: deleting %s", path) 45 | if d.dryRun { 46 | continue 47 | } 48 | if err := os.Remove(path); err != nil { 49 | log.Error(err) 50 | } 51 | } 52 | } 53 | 54 | type stringHeap []string 55 | 56 | func (h stringHeap) Len() int { return len(h) } 57 | func (h stringHeap) Less(i, j int) bool { return len(h[i]) > len(h[j]) } 58 | func (h stringHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 59 | 60 | func (h *stringHeap) Push(x interface{}) { 61 | *h = append(*h, x.(string)) 62 | } 63 | 64 | func (h *stringHeap) Pop() interface{} { 65 | old := *h 66 | n := len(old) 67 | x := old[n-1] 68 | *h = old[0 : n-1] 69 | return x 70 | } 71 | -------------------------------------------------------------------------------- /internal/plugin/processor/pre/categorizer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package pre 9 | 10 | import ( 11 | "context" 12 | "path" 13 | "strings" 14 | 15 | "github.com/pkg/errors" 16 | "github.com/rbtr/pachinko/plugin/processor" 17 | "github.com/rbtr/pachinko/types" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var defaultCategoryFileExtensions = map[types.Category][]string{ 22 | types.Archive: types.ArchiveExtensions, 23 | types.Image: types.ImageExtensions, 24 | types.Subtitle: types.SubtitleExtensions, 25 | types.Text: types.TextExtensions, 26 | types.Video: types.VideoExtensions, 27 | } 28 | 29 | type FileCategorizer struct { 30 | CategoryFileExtensions map[types.Category][]string `mapstructure:"file-extensions"` 31 | fileExtensionCategories map[string]types.Category 32 | } 33 | 34 | func (cat *FileCategorizer) Init(context.Context) error { 35 | log.Trace("categorizer initializing") 36 | cat.fileExtensionCategories = map[string]types.Category{} 37 | // transpose the category/extension map for immediate lookups 38 | for k, v := range cat.CategoryFileExtensions { 39 | for _, vv := range v { 40 | if kk, ok := cat.fileExtensionCategories[vv]; ok { 41 | return errors.Errorf("duplicate filetype::category mapping: %s::%s already exists as %s::%s, ", vv, k, vv, kk) 42 | } 43 | cat.fileExtensionCategories[vv] = k 44 | } 45 | } 46 | log.Trace("categorizer initialized") 47 | return nil 48 | } 49 | 50 | func (cat *FileCategorizer) identify(m types.Item) types.Item { 51 | // don't attempt to categorize directories 52 | if m.FileType == types.Directory { 53 | return m 54 | } 55 | 56 | ext := path.Ext(m.SourcePath) 57 | 58 | category := types.Unknown 59 | if ext == "" { 60 | log.Debug("categorizer: no extension, unknown category") 61 | } 62 | 63 | trimmed := strings.Trim(ext, ".") 64 | log.Tracef("categorizer: lookup extension '%s'", trimmed) 65 | if c, ok := cat.fileExtensionCategories[trimmed]; ok { 66 | log.Debugf("categorizer: identified %s as %s", ext, c) 67 | category = c 68 | } 69 | m.Category = category 70 | return m 71 | } 72 | 73 | func (cat *FileCategorizer) Process(in <-chan types.Item, out chan<- types.Item) { 74 | log.Trace("started categorizer") 75 | for m := range in { 76 | log.Debugf("categorizer: received input: %v", m) 77 | out <- cat.identify(m) 78 | } 79 | } 80 | 81 | func (*FileCategorizer) Type() processor.Type { 82 | return processor.Pre 83 | } 84 | 85 | func NewCategorizer() *FileCategorizer { 86 | return &FileCategorizer{ 87 | CategoryFileExtensions: defaultCategoryFileExtensions, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/plugin/processor/pre/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | /* 10 | Internal package processor (pre/post/intra) provides implementations of 11 | unconfigurable, non-optional processor plugins that are tied to the run 12 | of certain public plugins but are not specific enough to be integrated in to 13 | those plugins. 14 | */ 15 | package pre 16 | -------------------------------------------------------------------------------- /internal/testing/testing.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "github.com/rbtr/pachinko/types" 5 | "github.com/rbtr/pachinko/types/metadata/movie" 6 | "github.com/rbtr/pachinko/types/metadata/tv" 7 | ) 8 | 9 | type Test struct { 10 | Name string 11 | Inputs []string 12 | Want types.Item 13 | } 14 | 15 | var TV []*Test = []*Test{ 16 | { 17 | "SssEee", 18 | []string{ 19 | "Mr-Robot-2x5", 20 | "Mr.Robot.2x5", 21 | "Mr Robot 2x5", 22 | "Mr-Robot-2-5", 23 | "Mr.Robot.2-5", 24 | "Mr Robot 2-5", 25 | "Mr Robot S02E05", 26 | "Mr-Robot/Season-2/5", 27 | "Mr.Robot/Season.2/5", 28 | "Mr Robot/Season 2/5", 29 | "Mr-Robot-Season-02-Episode-05", 30 | "Mr.Robot.Season.02.Episode.05", 31 | "Mr Robot Season 02 Episode 05", 32 | "Mr Robot/Season 02/Episode 05", 33 | }, 34 | types.Item{ 35 | Category: types.Video, 36 | MediaType: tv.TV, 37 | TVMetadata: tv.Metadata{ 38 | Name: "Mr Robot", 39 | Episode: tv.Episode{ 40 | Number: 5, 41 | Season: tv.Season{ 42 | Number: 2, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | { 49 | "dir multimatch SSxEE", 50 | []string{ 51 | "/src/Broad City (2014) S01-03 Season 01-03 (1080p HEVC AAC 2.0)/Broad City 03x09 Getting There.mkv", 52 | }, 53 | types.Item{ 54 | Category: types.Video, 55 | MediaType: tv.TV, 56 | TVMetadata: tv.Metadata{ 57 | Name: "Broad City", 58 | Episode: tv.Episode{ 59 | Number: 9, 60 | Season: tv.Season{ 61 | Number: 3, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | { 68 | "dotted year", 69 | []string{ 70 | "/src/Doctor Who 2005 S12E03 1080p HEVC x265/Doctor.Who.2005.S12E03.1080p.HEVC.x265.mkv", 71 | }, 72 | types.Item{ 73 | Category: types.Video, 74 | MediaType: tv.TV, 75 | TVMetadata: tv.Metadata{ 76 | Name: "Doctor Who", 77 | ReleaseYear: 2005, 78 | Episode: tv.Episode{ 79 | Number: 3, 80 | Season: tv.Season{ 81 | Number: 12, 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | { 88 | "special characters in title", 89 | []string{ 90 | "/src/Tom Clancy's Jack Ryan S01E01.mkv", 91 | }, 92 | types.Item{ 93 | Category: types.Video, 94 | MediaType: tv.TV, 95 | TVMetadata: tv.Metadata{ 96 | Name: "Tom Clancy's Jack Ryan", 97 | Episode: tv.Episode{ 98 | Number: 1, 99 | Season: tv.Season{ 100 | Number: 1, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | { 107 | "special characters in title", 108 | []string{ 109 | "/src/Handmaid's Tale S03E01.mkv", 110 | }, 111 | types.Item{ 112 | Category: types.Video, 113 | MediaType: tv.TV, 114 | TVMetadata: tv.Metadata{ 115 | Name: "Handmaid's Tale", 116 | Episode: tv.Episode{ 117 | Number: 1, 118 | Season: tv.Season{ 119 | Number: 3, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | { 126 | "special characters in title", 127 | []string{ 128 | "src/Marvel's Runaways S01E01.mkv", 129 | }, 130 | types.Item{ 131 | Category: types.Video, 132 | MediaType: tv.TV, 133 | TVMetadata: tv.Metadata{ 134 | Name: "Marvel's Runaways", 135 | Episode: tv.Episode{ 136 | Number: 1, 137 | Season: tv.Season{ 138 | Number: 1, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }, 144 | } 145 | 146 | var Movies []*Test = []*Test{ 147 | { 148 | "year in title", 149 | []string{ 150 | "/src/Blade Runner 2049 (2017)/Blade Runner 2049.mkv", 151 | "/src/Blade Runner 2049 (2017).mkv", 152 | }, 153 | types.Item{ 154 | Category: types.Video, 155 | MediaType: movie.Movie, 156 | MovieMetadata: movie.Metadata{ 157 | Title: "Blade Runner 2049", 158 | ReleaseYear: 2017, 159 | }, 160 | }, 161 | }, 162 | { 163 | "year as title", 164 | []string{ 165 | "/movies/1917 (2020).mkv", 166 | }, 167 | types.Item{ 168 | Category: types.Video, 169 | MediaType: movie.Movie, 170 | MovieMetadata: movie.Metadata{ 171 | Title: "1917", 172 | ReleaseYear: 2020, 173 | }, 174 | }, 175 | }, 176 | { 177 | "typical", 178 | []string{ 179 | "/src/Finding Nemo (2003).mkv", 180 | }, 181 | types.Item{ 182 | Category: types.Video, 183 | MediaType: movie.Movie, 184 | MovieMetadata: movie.Metadata{ 185 | Title: "Finding Nemo", 186 | ReleaseYear: 2003, 187 | }, 188 | }, 189 | }, 190 | { 191 | "metadata", 192 | []string{ 193 | "/src/Frozen 2 (2019) [1080p x265 10bit FS93].mkv", 194 | }, 195 | types.Item{ 196 | Category: types.Video, 197 | MediaType: movie.Movie, 198 | MovieMetadata: movie.Metadata{ 199 | Title: "Frozen 2", 200 | ReleaseYear: 2019, 201 | }, 202 | }, 203 | }, 204 | { 205 | "subtitled", 206 | []string{ 207 | "TRON - Legacy (2010) (1080p BluRay).mkv", 208 | }, 209 | types.Item{ 210 | Category: types.Video, 211 | MediaType: movie.Movie, 212 | MovieMetadata: movie.Metadata{ 213 | Title: "TRON Legacy", 214 | ReleaseYear: 2010, 215 | }, 216 | }, 217 | }, 218 | { 219 | "special character :", 220 | []string{ 221 | "3:10 To Yuma (2007).mkv", 222 | }, 223 | types.Item{ 224 | Category: types.Video, 225 | MediaType: movie.Movie, 226 | MovieMetadata: movie.Metadata{ 227 | Title: "3:10 To Yuma", 228 | ReleaseYear: 2007, 229 | }, 230 | }, 231 | }, 232 | } 233 | 234 | var NotTV []*Test = []*Test{} 235 | var NotMovies []*Test = []*Test{} 236 | 237 | func init() { 238 | NotTV = append(NotTV, Movies...) 239 | NotMovies = append(NotMovies, TV...) 240 | } 241 | -------------------------------------------------------------------------------- /internal/trakt/trakt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package trakt 9 | 10 | import ( 11 | "context" 12 | "encoding/json" 13 | "fmt" 14 | "io/ioutil" 15 | "os" 16 | "time" 17 | 18 | "github.com/rbtr/go-trakt" 19 | ) 20 | 21 | const ( 22 | DefaultAuthfile = "/etc/pachinko/trakt" 23 | ClientID = "76a0c1e8d3331021f6e312115e27fe4c29f4ef23ef89a0a69143a62d136ab994" 24 | // nolint: gosec 25 | ClientSecret = "fe8d1f0921413028f92428d2922e13a728e27d2f35b26e315cf3dde31228568d" 26 | ) 27 | 28 | type Auth struct { 29 | AccessToken string `json:"access-token,omitempty"` 30 | ClientID string `json:"client-id,omitempty"` 31 | ClientSecret string `json:"client-secret,omitempty"` 32 | CreatedAt time.Time `json:"created-at,omitempty"` 33 | ExpiresAfter time.Duration `json:"expires-after,omitempty"` 34 | RefreshToken string `json:"refresh-token,omitempty"` 35 | } 36 | 37 | func (auth *Auth) IsExpired() bool { 38 | return time.Now().After(auth.CreatedAt.Add(auth.ExpiresAfter)) 39 | } 40 | 41 | func (auth *Auth) ShouldRefresh(threshold time.Duration) bool { 42 | return time.Now().After(auth.CreatedAt.Add(auth.ExpiresAfter).Add(-threshold)) 43 | } 44 | 45 | func ReadAuthFile(path string) (*Auth, error) { 46 | auth := &Auth{} 47 | _, err := os.Stat(path) 48 | if os.IsNotExist(err) { 49 | return auth, nil 50 | } 51 | b, err := ioutil.ReadFile(path) 52 | if err != nil { 53 | return nil, err 54 | } 55 | if err := json.Unmarshal(b, auth); err != nil { 56 | return nil, err 57 | } 58 | return auth, nil 59 | } 60 | 61 | func WriteAuthFile(path string, auth *Auth) error { 62 | b, err := json.Marshal(auth) 63 | if err != nil { 64 | return err 65 | } 66 | return ioutil.WriteFile(path, b, 0600) 67 | } 68 | 69 | type Trakt struct { 70 | *trakt.Client 71 | auth *Auth 72 | } 73 | 74 | func NewTrakt(auth *Auth) (*Trakt, error) { 75 | if auth.ClientID == "" { 76 | auth.ClientID = ClientID 77 | } 78 | if auth.ClientSecret == "" { 79 | auth.ClientSecret = ClientSecret 80 | } 81 | client, err := trakt.NewClient(nil, auth.ClientID, auth.ClientSecret) 82 | if auth.AccessToken != "" { 83 | client.SetAuthorization(auth.AccessToken) 84 | } 85 | return &Trakt{client, auth}, err 86 | } 87 | 88 | // Authorize authorizes the client using 2-legged oauth. 89 | // Authorized credentials are stored in the client and also returned. 90 | func (t *Trakt) Authorize(ctx context.Context) (*Auth, error) { 91 | res, err := t.DeviceCode(ctx) 92 | if err != nil { 93 | return nil, err 94 | } 95 | fmt.Printf( 96 | "Authenticating in Trakt!\nPlease open in your browser:\t%s\n\t and enter the code:\t\t%s\n", 97 | res.VerificationURL, 98 | res.UserCode, 99 | ) 100 | 101 | ctx, cancel := context.WithTimeout(ctx, time.Duration(res.ExpiresIn)*time.Second) 102 | defer cancel() 103 | 104 | ticker := time.NewTicker(time.Duration(res.Interval) * time.Second) 105 | go func(ctx context.Context, cancelFunc func()) { 106 | <-ctx.Done() 107 | cancelFunc() 108 | }(ctx, ticker.Stop) 109 | 110 | var result *trakt.AuthResult 111 | code := res.DeviceCode 112 | for range ticker.C { 113 | result, err = t.DeviceToken(ctx, code) 114 | if err == nil { 115 | break 116 | } 117 | } 118 | if result == nil { 119 | return nil, err 120 | } 121 | fmt.Printf("Success! Your Authorization token is:\n\t> %s <\n", result.AccessToken) 122 | t.auth.AccessToken = result.AccessToken 123 | t.auth.RefreshToken = result.RefreshToken 124 | t.auth.ExpiresAfter = time.Duration(result.ExpiresIn) * time.Second 125 | t.auth.CreatedAt = time.Unix(int64(result.CreatedAt), 0) 126 | return t.auth, nil 127 | } 128 | 129 | // Refresh reauthorizes the client using the refresh token. 130 | // Authorized credentials are stored in the client and also returned. 131 | func (t *Trakt) Refresh(ctx context.Context) (*Auth, error) { 132 | res, err := t.RefreshToken(ctx, t.auth.RefreshToken) 133 | if err != nil { 134 | return nil, err 135 | } 136 | t.SetAuthorization(res.AccessToken) 137 | t.auth.AccessToken = res.AccessToken 138 | t.auth.RefreshToken = res.RefreshToken 139 | t.auth.ExpiresAfter = time.Duration(res.ExpiresIn) * time.Second 140 | t.auth.CreatedAt = time.Unix(int64(res.CreatedAt), 0) 141 | return t.auth, nil 142 | } 143 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | /* 10 | pachinko: modular media sorting 11 | */ 12 | package main 13 | 14 | import "github.com/rbtr/pachinko/cmd" 15 | 16 | func main() { 17 | cmd.Execute() 18 | } 19 | -------------------------------------------------------------------------------- /plugin/include.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package plugin 9 | 10 | import ( 11 | // blank includes 12 | _ "github.com/rbtr/pachinko/plugin/input" 13 | _ "github.com/rbtr/pachinko/plugin/output" 14 | _ "github.com/rbtr/pachinko/plugin/processor/intra" 15 | _ "github.com/rbtr/pachinko/plugin/processor/post" 16 | _ "github.com/rbtr/pachinko/plugin/processor/pre" 17 | ) 18 | -------------------------------------------------------------------------------- /plugin/input/path.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package input 9 | 10 | import ( 11 | "context" 12 | "os" 13 | "path/filepath" 14 | 15 | "github.com/rbtr/pachinko/types" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // FilePathInput walks a directory [src-dir], pushing everything in 20 | // in that directory tree in to the pipeline. 21 | type FilePathInput struct { 22 | // SrcDir the directory to ingest 23 | SrcDir string `mapstructure:"src-dir"` 24 | } 25 | 26 | // Init noop. 27 | func (*FilePathInput) Init(context.Context) error { 28 | return nil 29 | } 30 | 31 | // Consume runs the directory ingestion and pushes the contents of the 32 | // directory tree in to the pipeline. 33 | func (p *FilePathInput) Consume(sink chan<- types.Item) { 34 | log.Tracef("started path_input at %s", p.SrcDir) 35 | count := 0 36 | if err := filepath.Walk(p.SrcDir, func(path string, info os.FileInfo, err error) error { 37 | // skip root 38 | if path == p.SrcDir { 39 | return nil 40 | } 41 | log.Debugf("path_input: encountered %s", path) 42 | if err != nil { 43 | log.Error(err) 44 | return err 45 | } 46 | log.Infof("path_input: found file: %s", path) 47 | i := types.Item{ 48 | Identifiers: make(map[string]string), 49 | SourcePath: path, 50 | FileType: types.File, 51 | } 52 | if info.IsDir() { 53 | i.FileType = types.Directory 54 | } 55 | sink <- i 56 | count++ 57 | return nil 58 | }); err != nil { 59 | log.Errorf("path_input: %s", err) 60 | } 61 | log.Debugf("path_input: ingested %d files", count) 62 | } 63 | 64 | func init() { 65 | Register("filepath", func() Input { 66 | return &FilePathInput{ 67 | SrcDir: "/src", 68 | } 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /plugin/input/path_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package input 9 | 10 | // import ( 11 | // "fmt" 12 | // "path/filepath" 13 | // "testing" 14 | 15 | // "github.com/rbtr/pachinko/internal/config" 16 | // ) 17 | 18 | // func Test_walkDir(t *testing.T) { 19 | // f, _ := filepath.Abs("testdata") 20 | // input, _ := NewPathInput(config.Config{ 21 | // SrcDir: f, 22 | // }) 23 | // files := []string{} 24 | // for out := range input.Consume() { 25 | // files = append(files, out.SourcePath) 26 | // } 27 | // if len(files) != 5 { 28 | // t.Errorf("expected %d files, got %d", 5, len(files)) 29 | // } 30 | // for _, t := range files { 31 | // fmt.Println(t) 32 | // } 33 | // } 34 | -------------------------------------------------------------------------------- /plugin/input/testdata/a/a.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbtr/pachinko/d9e3ef83607e5a12a3743dbadb754a25bed64170/plugin/input/testdata/a/a.txt -------------------------------------------------------------------------------- /plugin/input/testdata/b/b.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbtr/pachinko/d9e3ef83607e5a12a3743dbadb754a25bed64170/plugin/input/testdata/b/b.txt -------------------------------------------------------------------------------- /plugin/input/testdata/b/b/b.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbtr/pachinko/d9e3ef83607e5a12a3743dbadb754a25bed64170/plugin/input/testdata/b/b/b.txt -------------------------------------------------------------------------------- /plugin/input/testdata/b/c/c.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbtr/pachinko/d9e3ef83607e5a12a3743dbadb754a25bed64170/plugin/input/testdata/b/c/c.txt -------------------------------------------------------------------------------- /plugin/input/testdata/b/c/d/d.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbtr/pachinko/d9e3ef83607e5a12a3743dbadb754a25bed64170/plugin/input/testdata/b/c/d/d.txt -------------------------------------------------------------------------------- /plugin/input/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | /* 10 | Package input provides an interface for, and implementations of, 11 | input plugins which feed data from various sources in to the 12 | pipeline datastream. 13 | */ 14 | package input 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/rbtr/pachinko/types" 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | // Input defines the contract for Input pipeline plugins. 24 | type Input interface { 25 | Consume(chan<- types.Item) 26 | Init(context.Context) error 27 | } 28 | 29 | var Registry map[string](func() Input) = map[string](func() Input){} 30 | 31 | func Register(name string, initializer func() Input) { 32 | if _, ok := Registry[name]; ok { 33 | log.Fatalf("input registry already contains plugin named %s", name) 34 | } 35 | Registry[name] = initializer 36 | } 37 | -------------------------------------------------------------------------------- /plugin/output/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package output 9 | 10 | import ( 11 | "context" 12 | 13 | "github.com/rbtr/pachinko/types" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // Logger is a noop logging output used for dry-runs and testing. 18 | type Logger struct{} 19 | 20 | func (*Logger) Init(context.Context, Config) error { 21 | return nil 22 | } 23 | 24 | // Receive implements the Plugin interface on the Logger. 25 | func (stdr *Logger) Receive(c <-chan types.Item) { 26 | log.Trace("started stdout output") 27 | for m := range c { 28 | log.Tracef("stdout_output: received_input %#v", m) 29 | if m.SourcePath != "" && m.DestinationPath != "" { 30 | log.Infof("stdout_output: %s -> %s", m.SourcePath, m.DestinationPath) 31 | } 32 | } 33 | } 34 | 35 | func init() { 36 | Register("stdout", func() Output { 37 | return &Logger{} 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /plugin/output/path_mover.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package output 9 | 10 | import ( 11 | "context" 12 | "io" 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/pkg/errors" 17 | "github.com/rbtr/pachinko/types" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | // FilepathMover is a file mover, it will move files from src to dest with 22 | // some options like creating dirs or overwriting existing dests. 23 | type FilepathMover struct { 24 | CreateDirs bool `mapstructure:"create-dirs"` 25 | Overwrite bool `mapstructure:"overwrite"` 26 | 27 | dryRun bool 28 | } 29 | 30 | func (mv *FilepathMover) Init(ctx context.Context, cfg Config) error { 31 | mv.dryRun = cfg.DryRun 32 | return nil 33 | } 34 | 35 | func (mv *FilepathMover) mkdir(dir string) error { 36 | if mv.dryRun { 37 | log.Infof("move_output: (DRY_RUN) mkdir %s", dir) 38 | return nil 39 | } 40 | return os.MkdirAll(dir, os.ModePerm) 41 | } 42 | 43 | // rename attempts to rename the file: 44 | // if the source and dest are on the same volume, this is preferred - it's fast 45 | // and atomic and handled by the filesystem 46 | // if src and dest are on different volumes, it will error with a cross-device 47 | // link message. 48 | func (mv *FilepathMover) rename(src, dest string) error { 49 | if mv.dryRun { 50 | log.Infof("move_output: (DRY_RUN) rename %s -> %s", src, dest) 51 | return nil 52 | } 53 | return os.Rename(src, dest) 54 | } 55 | 56 | // move copies the file from src to dest: 57 | // this is slow as it actually copies the bits over from src to dest 58 | // should only be used to move data between volumes since rename is always 59 | // faster within the filesystem boundary. 60 | func (mv *FilepathMover) move(src, dest string) error { 61 | if mv.dryRun { 62 | log.Infof("move_output: (DRY_RUN) copy %s -> %s", src, dest) 63 | return nil 64 | } 65 | 66 | in, err := os.Open(src) 67 | if err != nil { 68 | return errors.Errorf("error opening src: %s", err) 69 | } 70 | defer in.Close() 71 | 72 | out, err := os.Create(dest) 73 | if err != nil { 74 | return errors.Errorf("error opening dest: %s", err) 75 | } 76 | defer out.Close() 77 | 78 | if _, err = io.Copy(out, in); err != nil { 79 | return errors.Errorf("error writing dest: %s", err) 80 | } 81 | 82 | err = os.Remove(src) 83 | if err != nil { 84 | return errors.Errorf("error removing src: %s", err) 85 | } 86 | return nil 87 | } 88 | 89 | func (mv *FilepathMover) moveMedia(m types.Item) error { 90 | if m.DestinationPath == "" { 91 | return errors.New("move_output: no dest path") 92 | } 93 | dir, _ := filepath.Split(m.DestinationPath) 94 | // check for dest directory, create if doesn't exist and allowed 95 | if _, err := os.Stat(dir); os.IsNotExist(err) { 96 | if !mv.CreateDirs { 97 | return errors.Errorf("move_output: dest (%s) does not exist and will not be created", dir) 98 | } 99 | if err := mv.mkdir(dir); err != nil { 100 | return err 101 | } 102 | } 103 | // check for dest file 104 | if _, err := os.Stat(m.DestinationPath); !os.IsNotExist(err) { 105 | if !mv.Overwrite { 106 | return errors.Errorf("move_output: file (%s) already exists and will not be overwritten", m.DestinationPath) 107 | } 108 | } 109 | // move src to dest 110 | if err := mv.rename(m.SourcePath, m.DestinationPath); err != nil { 111 | // failed to rename - probably cross-device link so try to move 112 | return mv.move(m.SourcePath, m.DestinationPath) 113 | } 114 | return nil 115 | } 116 | 117 | // Receive implements the Plugin interface on the FilepathMover. 118 | func (mv *FilepathMover) Receive(c <-chan types.Item) { 119 | log.Trace("started mover output") 120 | for m := range c { 121 | log.Tracef("mover_output: received_input %#v", m) 122 | if err := mv.moveMedia(m); err != nil { 123 | log.Errorf("mover_output: %s", err) 124 | } else { 125 | log.Infof("move_output: moved %s -> %s", m.SourcePath, m.DestinationPath) 126 | } 127 | } 128 | } 129 | 130 | func init() { 131 | Register("path-mover", func() Output { 132 | return &FilepathMover{ 133 | CreateDirs: true, 134 | Overwrite: false, 135 | dryRun: true, 136 | } 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /plugin/output/trakt_collector.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package output 9 | 10 | import ( 11 | "context" 12 | "strconv" 13 | 14 | "github.com/rbtr/go-trakt" 15 | internaltrakt "github.com/rbtr/pachinko/internal/trakt" 16 | "github.com/rbtr/pachinko/types" 17 | "github.com/rbtr/pachinko/types/metadata/movie" 18 | "github.com/rbtr/pachinko/types/metadata/tv" 19 | log "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var _ Output = (*TraktCollector)(nil) 23 | 24 | type TraktCollector struct { 25 | Authfile string `mapstructure:"authfile"` 26 | 27 | client *internaltrakt.Trakt 28 | } 29 | 30 | // Init reads the authfile, creates a client, refreshes the 31 | // credentials, and writes them back to the authfile. Any failures 32 | // will return an error. 33 | func (t *TraktCollector) Init(ctx context.Context, cfg Config) error { 34 | auth, err := internaltrakt.ReadAuthFile(t.Authfile) 35 | if err != nil { 36 | return err 37 | } 38 | if t.client, err = internaltrakt.NewTrakt(auth); err != nil { 39 | return err 40 | } 41 | auth, err = t.client.Refresh(ctx) 42 | if err != nil { 43 | return err 44 | } 45 | return internaltrakt.WriteAuthFile(t.Authfile, auth) 46 | } 47 | 48 | func (t *TraktCollector) collectTV(m types.Item) error { 49 | tvdbID, err := strconv.Atoi(m.Identifiers["tvdb"]) 50 | log.Debugf("trakt_collector: collecting by tvdb id: %d", tvdbID) 51 | if err != nil { 52 | return err 53 | } 54 | resp, err := t.client.Collection(context.TODO(), &trakt.CollectionBody{ 55 | Episodes: []trakt.Episode{ 56 | { 57 | IDs: trakt.IDs{ 58 | TVDB: tvdbID, 59 | }, 60 | }, 61 | }, 62 | }) 63 | if err != nil { 64 | return err 65 | } 66 | log.Debugf("trakt_collector: added %d, updated %d, existing %d", resp.Added.Episodes, resp.Updated.Episodes, resp.Existing.Episodes) 67 | return nil 68 | } 69 | 70 | func (t *TraktCollector) collectMovie(m types.Item) error { 71 | tmdbID, err := strconv.Atoi(m.Identifiers["tmdb"]) 72 | log.Debugf("trakt_collector: collecting by tmdb id: %d", tmdbID) 73 | if err != nil { 74 | return err 75 | } 76 | resp, err := t.client.Collection(context.TODO(), &trakt.CollectionBody{ 77 | Movies: []trakt.Movie{ 78 | { 79 | IDs: trakt.IDs{ 80 | TMDb: tmdbID, 81 | }, 82 | }, 83 | }, 84 | }) 85 | if err != nil { 86 | return err 87 | } 88 | log.Debugf("trakt_collector: added %d, updated %d, existing %d", resp.Added.Movies, resp.Updated.Movies, resp.Existing.Movies) 89 | return nil 90 | } 91 | 92 | func (t *TraktCollector) Receive(in <-chan types.Item) { 93 | log.Trace("started trakt_collector output") 94 | for m := range in { 95 | log.Tracef("trakt_collector: received_input %#v", m) 96 | if m.MediaType == tv.TV { 97 | log.Infof("trakt_collector: collecting tv") 98 | if err := t.collectTV(m); err != nil { 99 | log.Error(err) 100 | } 101 | } 102 | if m.MediaType == movie.Movie { 103 | log.Infof("trakt_collector: collecting movie") 104 | if err := t.collectMovie(m); err != nil { 105 | log.Error(err) 106 | } 107 | } 108 | } 109 | } 110 | 111 | func init() { 112 | Register("trakt-collector", func() Output { 113 | return &TraktCollector{ 114 | Authfile: internaltrakt.DefaultAuthfile, 115 | } 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /plugin/output/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | 9 | /* 10 | Package output provides an interface for, and implementations of, 11 | output plugins which consume data from the pipeline datastream at 12 | the end of processing. 13 | */ 14 | package output 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/rbtr/pachinko/types" 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | // Config is common/general output tunables. 24 | type Config struct { 25 | DryRun bool 26 | } 27 | 28 | // Output is plugin interface to handle the result. 29 | type Output interface { 30 | Receive(<-chan types.Item) 31 | Init(context.Context, Config) error 32 | } 33 | 34 | var Registry map[string](func() Output) = map[string](func() Output){} 35 | 36 | func Register(name string, initializer func() Output) { 37 | if _, ok := Registry[name]; ok { 38 | log.Fatalf("output registry already contains plugin named %s", name) 39 | } 40 | Registry[name] = initializer 41 | } 42 | -------------------------------------------------------------------------------- /plugin/processor/intra/tmdb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package intra 9 | 10 | import ( 11 | "context" 12 | "strconv" 13 | "time" 14 | 15 | api "github.com/cyruzin/golang-tmdb" 16 | "github.com/pkg/errors" 17 | "github.com/rbtr/pachinko/plugin/processor" 18 | "github.com/rbtr/pachinko/types" 19 | "github.com/rbtr/pachinko/types/metadata/movie" 20 | log "github.com/sirupsen/logrus" 21 | ) 22 | 23 | // Client TODO. 24 | type TMDbClient struct { 25 | APIKey string `mapstructure:"api-key"` 26 | 27 | client *api.Client 28 | } 29 | 30 | func (c *TMDbClient) Init(context.Context) error { 31 | var err error 32 | if c.client, err = api.Init(c.APIKey); err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | // identify returns the ID of best match movie search result, or an error. 39 | func (c *TMDbClient) identify(m types.Item) (api.MovieDetails, error) { 40 | opts := map[string]string{} 41 | if m.MovieMetadata.ReleaseYear > 0 { 42 | opts["year"] = strconv.FormatInt(int64(m.MovieMetadata.ReleaseYear), 10) 43 | } 44 | res, err := c.client.GetSearchMovies(m.MovieMetadata.Title, opts) 45 | if err != nil || res == nil { 46 | return api.MovieDetails{}, err 47 | } 48 | if res.TotalResults == 0 { 49 | return api.MovieDetails{}, errors.Errorf("tmdb_decorator: no results for tmdb search for %s", m.MovieMetadata.Title) 50 | } 51 | // TODO: ugh, why are the inputs and outputs of your library different types for the same field 52 | details, err := c.client.GetMovieDetails(int(res.Results[0].ID), nil) 53 | if err != nil { 54 | return api.MovieDetails{}, err 55 | } 56 | if details == nil { 57 | return api.MovieDetails{}, errors.Errorf("tmdb_decorator: movie details nil for id %d", res.Results[0].ID) 58 | } 59 | return *details, nil 60 | } 61 | 62 | func (c *TMDbClient) addTMDbMetadata(m types.Item) types.Item { 63 | movie, err := c.identify(m) 64 | if err != nil { 65 | log.Errorf("tmdb_decorator: error identifying movie: %s", err) 66 | return m 67 | } 68 | log.Debugf("tmdb_decorator: got movie from tmdb: %v", movie) 69 | m.Identifiers["tmdb"] = strconv.FormatInt(movie.ID, 10) 70 | m.Identifiers["imdb"] = movie.IMDbID 71 | log.Debugf("tmdb_decorator: parsing release date: %s", movie.ReleaseDate) 72 | if p, err := time.Parse("2006-01-02", movie.ReleaseDate); err != nil { 73 | log.Error(err) 74 | } else { 75 | m.MovieMetadata.ReleaseYear = p.Year() 76 | } 77 | m.MovieMetadata.Title = movie.Title 78 | log.Tracef("tmdb_decorator: populated %v from tmdb", m) 79 | return m 80 | } 81 | 82 | func (c *TMDbClient) Process(in <-chan types.Item, out chan<- types.Item) { 83 | log.Trace("started tmdb_decorator processor") 84 | for m := range in { 85 | log.Tracef("tmdb_decorator: received input: %#v", m) 86 | if m.MediaType == movie.Movie { 87 | log.Infof("tmdb_decorator: looking up %s in tmdb", m.SourcePath) 88 | m = c.addTMDbMetadata(m) 89 | } else { 90 | log.Debugf("tmdb_decorator: %s type [%s] != Movie, skipping", m.SourcePath, m.MediaType) 91 | } 92 | out <- m 93 | } 94 | } 95 | 96 | func init() { 97 | processor.Register(processor.Intra, "tmdb", func() processor.Processor { 98 | return &TMDbClient{} 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /plugin/processor/intra/tvdb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package intra 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "regexp" 14 | "sort" 15 | "strconv" 16 | "time" 17 | 18 | "github.com/lithammer/fuzzysearch/fuzzy" 19 | "github.com/pkg/errors" 20 | api "github.com/rbtr/go-tvdb" 21 | "github.com/rbtr/go-tvdb/generated/models" 22 | "github.com/rbtr/pachinko/plugin/processor" 23 | "github.com/rbtr/pachinko/types" 24 | "github.com/rbtr/pachinko/types/metadata/tv" 25 | log "github.com/sirupsen/logrus" 26 | ) 27 | 28 | // WordMatcher regex. 29 | var matcher *regexp.Regexp = regexp.MustCompile(`[^'\w]`) 30 | 31 | // TVDbClient adds metadata from the TVDb. 32 | type TVDbClient struct { 33 | APIKey string `mapstructure:"api-key"` 34 | RequestLimit int64 `mapstructure:"request-limit"` 35 | 36 | client *api.Client 37 | limiter *time.Ticker 38 | } 39 | 40 | func (c *TVDbClient) Init(context.Context) error { 41 | authn := &models.Auth{ 42 | Apikey: c.APIKey, 43 | } 44 | c.client = api.DefaultClient(authn) 45 | c.limiter = time.NewTicker((time.Second / time.Duration(c.RequestLimit))) 46 | return nil 47 | } 48 | 49 | func (c *TVDbClient) identify(m types.Item) (*models.Episode, *models.SeriesSearchResult, error) { 50 | cleanName := matcher.ReplaceAllLiteralString(m.TVMetadata.Name, " ") 51 | log.Debugf("tvdb_decorator: identifying %s", cleanName) 52 | 53 | // note: when TV has a (YEAR), it is because there's multiple series with the same 54 | // name (i.e. it's been rebooted) and the year is part of the name that is used to 55 | // disambiguate (at least that's how thetvdb does it) 56 | param := map[string]string{"name": cleanName} 57 | if m.TVMetadata.ReleaseYear > 0 { 58 | log.Tracef("tvdb_decorator: show has year %d", m.TVMetadata.ReleaseYear) 59 | param["name"] = fmt.Sprintf("%s (%d)", param["name"], m.TVMetadata.ReleaseYear) 60 | } 61 | 62 | res, err := c.client.SearchSeries(context.TODO(), param) 63 | if err != nil { 64 | return nil, nil, err 65 | } 66 | 67 | resMap := map[string]*models.SeriesSearchResult{} 68 | log.Tracef("tvdb_decorator: search series found: %#v", resMap) 69 | resKeys := []string{} 70 | for _, res := range res { 71 | resMap[res.SeriesName] = res 72 | resKeys = append(resKeys, res.SeriesName) 73 | } 74 | matches := fuzzy.RankFindFold(cleanName, resKeys) 75 | sort.Sort(matches) 76 | if len(matches) == 0 { 77 | return nil, nil, errors.Errorf("tvdb_decorator: no matches for %s", param["name"]) 78 | } 79 | name := matches[0].Target 80 | series := resMap[name] 81 | log.Debugf("tvdb_decorator: search for %s found %s", param["name"], name) 82 | 83 | eps, _, jsonErr, err := c.client.GetSeriesEpisode(context.TODO(), series.ID, 0, map[string]string{"airedSeason": strconv.Itoa(m.TVMetadata.Season.Number), "airedEpisode": strconv.Itoa(m.TVMetadata.Episode.Number)}) 84 | if err != nil { 85 | return nil, nil, err 86 | } 87 | if jsonErr != nil { 88 | return nil, nil, err 89 | } 90 | 91 | if len(eps) == 0 { 92 | return nil, nil, errors.New("no matching episode found") 93 | } 94 | return eps[0], series, nil 95 | } 96 | 97 | func (c *TVDbClient) addTVDBMetadata(m types.Item) types.Item { 98 | ep, series, err := c.identify(m) 99 | if err != nil || ep == nil || series == nil { 100 | log.Errorf("tvdb_decorator: error identifying episode: %s", err) 101 | return m 102 | } 103 | log.Debugf("tvdb_decorator: got episode from tvdb: %v", ep) 104 | m.Identifiers["tvdb"] = strconv.FormatInt(ep.ID, 10) 105 | m.TVMetadata.Name = series.SeriesName 106 | m.TVMetadata.AbsoluteNumber = int(ep.AbsoluteNumber) 107 | m.TVMetadata.Episode.Title = ep.EpisodeName 108 | log.Tracef("tvdb_decorator: populated %v from tvdb", m) 109 | return m 110 | } 111 | 112 | func (c *TVDbClient) Process(in <-chan types.Item, out chan<- types.Item) { 113 | log.Trace("started tvdb_decorator processor") 114 | for m := range in { 115 | log.Tracef("tvdb_decorator: received input: %#v", m) 116 | if m.MediaType == tv.TV { 117 | log.Infof("tvdb_decorator: looking up %s in tvdb", m.SourcePath) 118 | <-c.limiter.C // rate limiting on tvdb api calls 119 | m = c.addTVDBMetadata(m) 120 | } else { 121 | log.Debugf("tvdb_decorator: %s type [%s] != TV, skipping", m.SourcePath, m.MediaType) 122 | } 123 | out <- m 124 | } 125 | } 126 | 127 | func init() { 128 | processor.Register(processor.Intra, "tvdb", func() processor.Processor { 129 | return &TVDbClient{ 130 | RequestLimit: 10, 131 | } 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /plugin/processor/intra/tvdb_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package intra 9 | 10 | // package tvdb 11 | 12 | // import ( 13 | // "context" 14 | // "sort" 15 | // "testing" 16 | 17 | // "github.com/lithammer/fuzzysearch/fuzzy" 18 | // ) 19 | 20 | // func TestClient_FetchMetadata(t *testing.T) { 21 | // c, _ := NewTVDBDecorator() 22 | // name := matcher.ReplaceAllLiteralString("Mr-Robot -", "") 23 | // res, err := c.SearchSeries(context.TODO(), map[string]string{"name": name}) 24 | // if err != nil { 25 | // t.Error(err) 26 | // } 27 | // t.Logf("res %+v\n", res) 28 | 29 | // names := []string{} 30 | // for _, res := range res { 31 | // names = append(names, res.SeriesName) 32 | // } 33 | // t.Logf("names %v\n", names) 34 | // matches := fuzzy.RankFindFold(name, names) 35 | // t.Logf("matches %v\n", matches) 36 | // sort.Sort(matches) 37 | // t.Logf("sorted %v", matches) 38 | // if matches[0].Target != "Mr. Robot" { 39 | // t.Errorf("wanted 'Mr. Robot', got %s", matches[0].Target) 40 | // } 41 | // } 42 | 43 | // func TestFuzzy(t *testing.T) { 44 | // name := matcher.ReplaceAllLiteralString("Mr-Robot", "") 45 | // matches := fuzzy.RankFindNormalizedFold(name, []string{"Mr. Robot"}) 46 | // t.Logf("matches %v\n", matches) 47 | // } 48 | -------------------------------------------------------------------------------- /plugin/processor/post/deleter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package post 9 | 10 | import ( 11 | "context" 12 | "path" 13 | "regexp" 14 | "strings" 15 | 16 | "github.com/rbtr/pachinko/plugin/processor" 17 | "github.com/rbtr/pachinko/types" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | type Deleter struct { 22 | Categories []string `mapstructure:"categories"` 23 | Extensions []string `mapstructure:"extensions"` 24 | Directories bool `mapstructure:"directories"` 25 | MatcherStrings []string `mapstructure:"matchers"` 26 | 27 | matchers []*regexp.Regexp 28 | } 29 | 30 | func (p *Deleter) Init(context.Context) error { 31 | log.Trace("deleter: initializing") 32 | for _, str := range p.MatcherStrings { 33 | r := regexp.MustCompile(str) 34 | p.matchers = append(p.matchers, r) 35 | } 36 | log.Tracef("deleter: initialized %d matchers, %d exts, directories = %t", len(p.matchers), len(p.Extensions), p.Directories) 37 | return nil 38 | } 39 | 40 | func (p *Deleter) matchRegexps(s string) bool { 41 | for _, matcher := range p.matchers { 42 | if matcher.MatchString(s) { 43 | return true 44 | } 45 | } 46 | return false 47 | } 48 | 49 | func (p *Deleter) matchExts(s string) bool { 50 | for _, ext := range p.Extensions { 51 | if s == ext { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | func (p *Deleter) shouldDelete(m types.Item) bool { 59 | if m.FileType == types.Directory && p.Directories { 60 | return true 61 | } 62 | if p.matchExts(strings.Trim(path.Ext(m.SourcePath), ".")) { 63 | return true 64 | } 65 | if p.matchRegexps(m.SourcePath) { 66 | return true 67 | } 68 | return false 69 | } 70 | 71 | func (p *Deleter) Process(in <-chan types.Item, out chan<- types.Item) { 72 | log.Trace("started deleter processor") 73 | for m := range in { 74 | log.Tracef("deleter: received input %#v", m) 75 | if p.shouldDelete(m) { 76 | log.Infof("deleter: marking %s for delete", m.SourcePath) 77 | m.Delete = true 78 | } 79 | out <- m 80 | } 81 | } 82 | 83 | func init() { 84 | processor.Register(processor.Post, "deleter", func() processor.Processor { 85 | var defaultExtensions = []string{} 86 | defaultExtensions = append(defaultExtensions, types.ArchiveExtensions...) 87 | defaultExtensions = append(defaultExtensions, types.ExecutableExtensions...) 88 | defaultExtensions = append(defaultExtensions, types.ImageExtensions...) 89 | defaultExtensions = append(defaultExtensions, types.SubtitleExtensions...) 90 | defaultExtensions = append(defaultExtensions, types.TextExtensions...) 91 | return &Deleter{ 92 | Extensions: defaultExtensions, 93 | Directories: true, 94 | MatcherStrings: []string{}, 95 | } 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /plugin/processor/post/movie_path_solver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package post 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "path" 14 | 15 | "github.com/rbtr/pachinko/plugin/processor" 16 | "github.com/rbtr/pachinko/types" 17 | "github.com/rbtr/pachinko/types/metadata/movie" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | type MoviePathSolver struct { 22 | DestDir string `mapstructure:"dest-dir"` 23 | MovieDirs bool `mapstructure:"movie-dirs"` 24 | MoviesPrefix string `mapstructure:"movie-prefix"` 25 | OutputFormat string `mapstructure:"format"` 26 | } 27 | 28 | func (*MoviePathSolver) Init(context.Context) error { 29 | return nil 30 | } 31 | 32 | func (p *MoviePathSolver) Process(in <-chan types.Item, out chan<- types.Item) { 33 | log.Trace("started movie_destination processor") 34 | for m := range in { 35 | log.Tracef("movie_destination: received input %#v", m) 36 | if m.MediaType != movie.Movie { 37 | log.Debugf("movie_destination: %s, type [%s] != Movie, skipping", m.SourcePath, m.MediaType) 38 | } else { 39 | log.Infof("movie_destination: solving dest for %s", m.SourcePath) 40 | if p.MovieDirs { 41 | m.DestinationPath = path.Join( 42 | p.DestDir, 43 | p.MoviesPrefix, 44 | fmt.Sprintf("%s (%d)", m.MovieMetadata.Title, m.MovieMetadata.ReleaseYear), 45 | fmt.Sprintf("%s (%d)%s", m.MovieMetadata.Title, m.MovieMetadata.ReleaseYear, path.Ext(m.SourcePath)), 46 | ) 47 | } else { 48 | m.DestinationPath = path.Join( 49 | p.DestDir, 50 | p.MoviesPrefix, 51 | fmt.Sprintf("%s (%d)%s", m.MovieMetadata.Title, m.MovieMetadata.ReleaseYear, path.Ext(m.SourcePath)), 52 | ) 53 | } 54 | } 55 | out <- m 56 | } 57 | } 58 | 59 | func init() { 60 | processor.Register(processor.Post, "movie-path-solver", func() processor.Processor { 61 | return &MoviePathSolver{ 62 | DestDir: "/dest", 63 | MovieDirs: true, 64 | MoviesPrefix: "movies", 65 | OutputFormat: "not-implemented", 66 | } 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /plugin/processor/post/tv_path_solver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package post 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "path" 14 | 15 | "github.com/rbtr/pachinko/plugin/processor" 16 | "github.com/rbtr/pachinko/types" 17 | "github.com/rbtr/pachinko/types/metadata/tv" 18 | log "github.com/sirupsen/logrus" 19 | ) 20 | 21 | type TVPathSolver struct { 22 | DestDir string `mapstructure:"dest-dir"` 23 | EpisodeNames bool `mapstructure:"episode-names"` 24 | TVPrefix string `mapstructure:"tv-prefix"` 25 | SeasonDirs bool `mapstructure:"season-dirs"` 26 | OutputFormat string `mapstructure:"format"` 27 | } 28 | 29 | func (*TVPathSolver) Init(context.Context) error { 30 | return nil 31 | } 32 | 33 | func (p *TVPathSolver) Process(in <-chan types.Item, out chan<- types.Item) { 34 | log.Trace("started tv_destination processor") 35 | for m := range in { 36 | log.Tracef("tv_destination: received input %#v", m) 37 | if m.MediaType != tv.TV { 38 | log.Debugf("tv_destination: %s, type [%s] != TV, skipping", m.SourcePath, m.MediaType) 39 | } else { 40 | log.Infof("tv_destination: solving dest for %s", m.SourcePath) 41 | filename := "" 42 | if p.EpisodeNames && m.TVMetadata.Episode.Title != "" { 43 | filename = fmt.Sprintf("%s S%0.2dE%0.2d %s%s", 44 | m.TVMetadata.Name, 45 | m.TVMetadata.Episode.Season.Number, 46 | m.TVMetadata.Episode.Number, 47 | m.TVMetadata.Episode.Title, 48 | path.Ext(m.SourcePath)) 49 | } else { 50 | filename = fmt.Sprintf("%s S%0.2dE%0.2d%s", 51 | m.TVMetadata.Name, 52 | m.TVMetadata.Episode.Season.Number, 53 | m.TVMetadata.Episode.Number, 54 | path.Ext(m.SourcePath)) 55 | } 56 | 57 | if p.SeasonDirs { 58 | // => .../tv/Mr Robot/Season 01/Mr Robot S01E01.mkv 59 | m.DestinationPath = path.Join( 60 | p.DestDir, 61 | p.TVPrefix, 62 | m.TVMetadata.Name, 63 | fmt.Sprintf("Season %0.2d", m.TVMetadata.Episode.Season.Number), 64 | filename, 65 | ) 66 | } else { 67 | // => .../tv/Mr Robot/Mr Robot S01E01.mkv 68 | m.DestinationPath = path.Join( 69 | p.DestDir, 70 | p.TVPrefix, 71 | m.TVMetadata.Name, 72 | filename, 73 | ) 74 | } 75 | } 76 | out <- m 77 | } 78 | } 79 | 80 | func init() { 81 | processor.Register(processor.Post, "tv-path-solver", func() processor.Processor { 82 | return &TVPathSolver{ 83 | DestDir: "/dest", 84 | EpisodeNames: false, 85 | TVPrefix: "tv", 86 | SeasonDirs: true, 87 | OutputFormat: "not-implemented", 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /plugin/processor/pre/movie.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package tvmeta 9 | 10 | import ( 11 | "context" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/rbtr/pachinko/plugin/processor" 17 | "github.com/rbtr/pachinko/types" 18 | "github.com/rbtr/pachinko/types/metadata/movie" 19 | log "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var defaultMovieMatchers = []string{ 23 | `(?i)\b([\s\w:'.-]*)[\s.-]?(?:[\s\(.-]?(\d{4})[\s\).-]?)(?:\s*[\(\[].*[\)\]])?(?:\/|.[A-Za-z]{3})`, // matches "Name (YEAR)." 24 | } 25 | 26 | type MoviePreProcessor struct { 27 | MatcherStrings []string `mapstructure:"matchers"` 28 | Sanitize bool `mapstructure:"sanitize-name"` 29 | 30 | matchers []*regexp.Regexp 31 | } 32 | 33 | func (p *MoviePreProcessor) Init(context.Context) error { 34 | log.Trace("movie_path_metadata: initializing") 35 | for _, str := range p.MatcherStrings { 36 | r := regexp.MustCompile(str) 37 | p.matchers = append(p.matchers, r) 38 | } 39 | log.Tracef("movie_path_metadata: initialized %d matchers", len(p.matchers)) 40 | return nil 41 | } 42 | 43 | // extract uses the Movie regexp to extract metadata from the input. 44 | func (p *MoviePreProcessor) extractMetadata(m types.Item) types.Item { 45 | var title, year string 46 | for _, matcher := range p.matchers { 47 | subs := matcher.FindAllStringSubmatch(m.SourcePath, -1) 48 | if len(subs) == 0 { 49 | continue 50 | } 51 | if matches := subs[len(subs)-1]; matches != nil { 52 | if title == "" && strings.TrimSpace(matches[1]) != "" { 53 | log.Tracef("movie_path_metadata: %v extracting title %s from %s", matcher.String(), matches[1], m.SourcePath) 54 | title = strings.TrimSpace(matches[1]) 55 | if p.Sanitize { 56 | title = sanitizer.ReplaceAllString(title, " ") 57 | } 58 | } 59 | if year == "" && strings.TrimSpace(matches[2]) != "" { 60 | log.Tracef("movie_path_metadata: %v extracting year %s from %s", matcher.String(), matches[2], m.SourcePath) 61 | year = strings.TrimSpace(matches[2]) 62 | } 63 | if title != "" && year != "" { 64 | break 65 | } 66 | } 67 | } 68 | m.MovieMetadata.Title = title 69 | m.MovieMetadata.ReleaseYear, _ = strconv.Atoi(year) 70 | return m 71 | } 72 | 73 | // identify tests if the input is matched by any of the Movie regexp. 74 | func (p *MoviePreProcessor) identify(m types.Item) bool { 75 | for _, matcher := range p.matchers { 76 | if matcher.MatchString(m.SourcePath) { 77 | log.Tracef("movie_path_metadata: regexp %s matched %s", matcher, m.SourcePath) 78 | return true 79 | } 80 | log.Tracef("movie_path_metadata: regexp %s did not match %s", matcher, m.SourcePath) 81 | } 82 | log.Tracef("movie_path_metadata: %s did not match identifiers", m.SourcePath) 83 | return false 84 | } 85 | 86 | func (p *MoviePreProcessor) Process(in <-chan types.Item, out chan<- types.Item) { 87 | log.Trace("started movie_path_metadata processor") 88 | for m := range in { 89 | log.Tracef("movie_path_metadata: received input: %#v", m) 90 | if m.Category == types.Video { 91 | log.Infof("movie_path_metadata: %s category == video, testing for movie", m.SourcePath) 92 | if p.identify(m) { 93 | log.Infof("movie_path_metadata: %s is movie", m.SourcePath) 94 | m.MediaType = movie.Movie 95 | } 96 | } else { 97 | log.Debugf("movie_path_metadata: %s category [%s] != video, skipping", m.SourcePath, m.Category) 98 | } 99 | if m.MediaType == movie.Movie { 100 | log.Infof("movie_path_metadata: extracting metadata for %v", m) 101 | m = p.extractMetadata(m) 102 | } else { 103 | log.Debugf("movie_path_metadata: %s type [%s] != Movie, skipping", m.SourcePath, m.MediaType) 104 | } 105 | out <- m 106 | } 107 | } 108 | 109 | func init() { 110 | processor.Register(processor.Pre, "movie", func() processor.Processor { 111 | return &MoviePreProcessor{ 112 | MatcherStrings: defaultMovieMatchers, 113 | Sanitize: true, 114 | } 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /plugin/processor/pre/movie_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package tvmeta 9 | 10 | import ( 11 | "context" 12 | "testing" 13 | 14 | internaltesting "github.com/rbtr/pachinko/internal/testing" 15 | "github.com/rbtr/pachinko/types" 16 | "github.com/rbtr/pachinko/types/metadata/movie" 17 | ) 18 | 19 | func TestMoviePreProcessor_extractMetadata(t *testing.T) { 20 | tv := &MoviePreProcessor{MatcherStrings: defaultMovieMatchers, Sanitize: true} 21 | tv.Init(context.TODO()) 22 | for _, tt := range internaltesting.Movies { 23 | tt := tt 24 | for _, in := range tt.Inputs { 25 | in := in 26 | t.Run(tt.Name+"::"+in, func(t *testing.T) { 27 | t.Parallel() 28 | min := types.Item{SourcePath: in} 29 | mout := tv.extractMetadata(min) 30 | if mout.TVMetadata.Name != tt.Want.TVMetadata.Name { 31 | t.Errorf("got %s, want %s", mout.TVMetadata.Name, tt.Want.TVMetadata.Name) 32 | } 33 | if mout.TVMetadata.ReleaseYear != tt.Want.TVMetadata.ReleaseYear { 34 | t.Errorf("got %d, want %d", mout.TVMetadata.ReleaseYear, tt.Want.TVMetadata.ReleaseYear) 35 | } 36 | if mout.TVMetadata.Episode.Season.Number != tt.Want.TVMetadata.Episode.Season.Number { 37 | t.Errorf("got %d, want %d", mout.TVMetadata.Episode.Season.Number, tt.Want.TVMetadata.Episode.Season.Number) 38 | } 39 | if mout.TVMetadata.Episode.Number != tt.Want.TVMetadata.Episode.Number { 40 | t.Errorf("got %d, want %d", mout.TVMetadata.Episode.Number, tt.Want.TVMetadata.Episode.Number) 41 | } 42 | }) 43 | } 44 | } 45 | } 46 | 47 | func TestMoviePreProcessor_identify(t *testing.T) { 48 | p := &MoviePreProcessor{MatcherStrings: defaultMovieMatchers, Sanitize: true} 49 | _ = p.Init(context.TODO()) 50 | for _, tt := range internaltesting.Movies { 51 | tt := tt 52 | for _, in := range tt.Inputs { 53 | in := in 54 | t.Run(tt.Name+"::"+in, func(t *testing.T) { 55 | t.Parallel() 56 | min := types.Item{SourcePath: in} 57 | mout := p.identify(min) 58 | if mout == (movie.Movie != tt.Want.MediaType) { 59 | t.Errorf("got %t, want %s", mout, tt.Want.MediaType) 60 | } 61 | }) 62 | } 63 | } 64 | for _, tt := range internaltesting.NotMovies { 65 | tt := tt 66 | for _, in := range tt.Inputs { 67 | in := in 68 | t.Run(tt.Name+"::"+in, func(t *testing.T) { 69 | t.Parallel() 70 | min := types.Item{SourcePath: in} 71 | mout := p.identify(min) 72 | if mout == (movie.Movie != tt.Want.MediaType) { 73 | t.Errorf("got %t but shouldn't have", mout) 74 | } 75 | }) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /plugin/processor/pre/tv.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package tvmeta 9 | 10 | import ( 11 | "context" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/rbtr/pachinko/plugin/processor" 17 | "github.com/rbtr/pachinko/types" 18 | "github.com/rbtr/pachinko/types/metadata/tv" 19 | log "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var defaultTVMatchers = []string{ 23 | `(?i)\b([\s\w'.-]*)[\s.\/-]+(?:\((\d+)\))?[\s.\/-]?(\d{1,3})[x-](\d{1,3})`, // matches 1x1 and 1/1 patterns 24 | `(?i)\b([\s\w'.-]*?)?(?:[\s\(.\/-](\d{4})[\s\).\/-])?[\s\w.-]?(?:s+(\d+))(?:\.|\s|-|_|x)*(?:e+(\d+))`, // matches S00E00 patterns 25 | `(?i)\b([\s\w'.-]*)[\s.\/-]+(?:\((\d+)\))?[\s.\/-]?(?:season|series).?(\d+).?(?:episode)?[^\d(]?(\d+)`, // matches Season 00 patterns 26 | } 27 | 28 | var sanitizer = regexp.MustCompile(`[^'\w]`) 29 | 30 | type TVPreProcessor struct { 31 | MatcherStrings []string `mapstructure:"matchers"` 32 | Sanitize bool `mapstructure:"sanitize-name"` 33 | 34 | matchers []*regexp.Regexp 35 | } 36 | 37 | func (p *TVPreProcessor) Init(context.Context) error { 38 | log.Trace("tv_path_metadata: initializing") 39 | for _, str := range p.MatcherStrings { 40 | r := regexp.MustCompile(str) 41 | p.matchers = append(p.matchers, r) 42 | } 43 | log.Tracef("tv_path_metadata: initialized %d matchers", len(p.matchers)) 44 | return nil 45 | } 46 | 47 | // extract uses the TV regexp to extract metadata from the input. 48 | func (p *TVPreProcessor) extractMetadata(m types.Item) types.Item { 49 | var show, year, season, episode string 50 | for _, matcher := range p.matchers { 51 | subs := matcher.FindAllStringSubmatch(m.SourcePath, -1) 52 | if len(subs) == 0 { 53 | continue 54 | } 55 | if matches := subs[len(subs)-1]; matches != nil { 56 | if show == "" && strings.TrimSpace(matches[1]) != "" { 57 | log.Tracef("tv_path_metadata: %v extracting show name %s from %s", matcher.String(), matches[1], m.SourcePath) 58 | show = strings.TrimSpace(matches[1]) 59 | if p.Sanitize { 60 | show = sanitizer.ReplaceAllString(show, " ") 61 | } 62 | } 63 | if year == "" && strings.TrimSpace(matches[2]) != "" { 64 | log.Tracef("tv_path_metadata: %v extracting year %s from %s", matcher.String(), matches[2], m.SourcePath) 65 | year = strings.TrimSpace(matches[2]) 66 | } 67 | if season == "" && strings.TrimSpace(matches[3]) != "" { 68 | log.Tracef("tv_path_metadata: %v extracting season number %s from %s", matcher.String(), matches[3], m.SourcePath) 69 | season = strings.TrimSpace(matches[3]) 70 | } 71 | if episode == "" && strings.TrimSpace(matches[4]) != "" { 72 | log.Tracef("tv_path_metadata: %v extracting episode number %s from %s", matcher.String(), matches[4], m.SourcePath) 73 | episode = strings.TrimSpace(matches[4]) 74 | } 75 | if show != "" && year != "" && season != "" && episode != "" { 76 | break 77 | } 78 | } 79 | } 80 | m.TVMetadata.Name = show 81 | m.TVMetadata.ReleaseYear, _ = strconv.Atoi(year) 82 | m.TVMetadata.Season.Number, _ = strconv.Atoi(season) 83 | m.TVMetadata.Episode.Number, _ = strconv.Atoi(episode) 84 | return m 85 | } 86 | 87 | // identify tests if the input is matched by any of the TV regexp. 88 | func (p *TVPreProcessor) identify(m types.Item) bool { 89 | for _, matcher := range p.matchers { 90 | if matcher.MatchString(m.SourcePath) { 91 | log.Tracef("tv_path_metadata: regexp %s matched %s", matcher, m.SourcePath) 92 | return true 93 | } 94 | log.Tracef("tv_path_metadata: regexp %s did not match %s", matcher, m.SourcePath) 95 | } 96 | log.Tracef("tv_path_metadata: %s did not match identifiers", m.SourcePath) 97 | return false 98 | } 99 | 100 | func (p *TVPreProcessor) Process(in <-chan types.Item, out chan<- types.Item) { 101 | log.Trace("started tv_path_metadata processor") 102 | for m := range in { 103 | log.Tracef("tv_path_metadata: received input: %#v", m) 104 | if m.Category == types.Video { 105 | log.Infof("tv_path_metadata: %s category == video, testing for TV", m.SourcePath) 106 | if p.identify(m) { 107 | log.Infof("tv_path_metadata: %s is TV", m.SourcePath) 108 | m.MediaType = tv.TV 109 | } 110 | } else { 111 | log.Debugf("tv_path_metadata: %s category [%s] != video, skipping", m.SourcePath, m.Category) 112 | } 113 | if m.MediaType == tv.TV { 114 | log.Infof("tv_path_metadata: extracting metadata for %v", m) 115 | m = p.extractMetadata(m) 116 | } else { 117 | log.Debugf("tv_path_metadata: %s type [%s] != TV, skipping", m.SourcePath, m.MediaType) 118 | } 119 | out <- m 120 | } 121 | } 122 | 123 | func init() { 124 | processor.Register(processor.Pre, "tv", func() processor.Processor { 125 | return &TVPreProcessor{ 126 | MatcherStrings: defaultTVMatchers, 127 | Sanitize: true, 128 | } 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /plugin/processor/pre/tv_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package tvmeta 9 | 10 | import ( 11 | "context" 12 | "testing" 13 | 14 | internaltesting "github.com/rbtr/pachinko/internal/testing" 15 | "github.com/rbtr/pachinko/types" 16 | "github.com/rbtr/pachinko/types/metadata/tv" 17 | ) 18 | 19 | func TestTVPreProcessor_extractMetadata(t *testing.T) { 20 | tv := &TVPreProcessor{MatcherStrings: defaultTVMatchers, Sanitize: true} 21 | tv.Init(context.TODO()) 22 | for _, tt := range internaltesting.TV { 23 | tt := tt 24 | for _, in := range tt.Inputs { 25 | in := in 26 | t.Run(tt.Name+"::"+in, func(t *testing.T) { 27 | t.Parallel() 28 | min := types.Item{SourcePath: in} 29 | mout := tv.extractMetadata(min) 30 | if mout.TVMetadata.Name != tt.Want.TVMetadata.Name { 31 | t.Errorf("got %s, want %s", mout.TVMetadata.Name, tt.Want.TVMetadata.Name) 32 | } 33 | if mout.TVMetadata.ReleaseYear != tt.Want.TVMetadata.ReleaseYear { 34 | t.Errorf("got %d, want %d", mout.TVMetadata.ReleaseYear, tt.Want.TVMetadata.ReleaseYear) 35 | } 36 | if mout.TVMetadata.Episode.Season.Number != tt.Want.TVMetadata.Episode.Season.Number { 37 | t.Errorf("got %d, want %d", mout.TVMetadata.Episode.Season.Number, tt.Want.TVMetadata.Episode.Season.Number) 38 | } 39 | if mout.TVMetadata.Episode.Number != tt.Want.TVMetadata.Episode.Number { 40 | t.Errorf("got %d, want %d", mout.TVMetadata.Episode.Number, tt.Want.TVMetadata.Episode.Number) 41 | } 42 | }) 43 | } 44 | } 45 | } 46 | 47 | func TestTVPreProcessor_identify(t *testing.T) { 48 | p := &TVPreProcessor{MatcherStrings: defaultTVMatchers, Sanitize: true} 49 | _ = p.Init(context.TODO()) 50 | for _, tt := range internaltesting.TV { 51 | tt := tt 52 | for _, in := range tt.Inputs { 53 | in := in 54 | t.Run(tt.Name+"::"+in, func(t *testing.T) { 55 | t.Parallel() 56 | min := types.Item{SourcePath: in} 57 | mout := p.identify(min) 58 | if mout == (tv.TV != tt.Want.MediaType) { 59 | t.Errorf("got %t, want %s", mout, tt.Want.MediaType) 60 | } 61 | }) 62 | } 63 | } 64 | for _, tt := range internaltesting.NotTV { 65 | tt := tt 66 | for _, in := range tt.Inputs { 67 | in := in 68 | t.Run(tt.Name+"::"+in, func(t *testing.T) { 69 | t.Parallel() 70 | min := types.Item{SourcePath: in} 71 | mout := p.identify(min) 72 | if mout == (tv.TV != tt.Want.MediaType) { 73 | t.Errorf("got %t but shouldn't have", mout) 74 | } 75 | }) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /plugin/processor/types.go: -------------------------------------------------------------------------------- 1 | package processor 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rbtr/pachinko/types" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type Type string 11 | 12 | const ( 13 | Pre Type = "pre" 14 | Intra Type = "intra" 15 | Post Type = "post" 16 | ) 17 | 18 | // Types is a convenience for iterating all of the processor types. 19 | // The order of this slice is intentional! 20 | var Types []Type = []Type{Pre, Intra, Post} 21 | 22 | type Processor interface { 23 | Init(context.Context) error 24 | Process(<-chan types.Item, chan<- types.Item) 25 | } 26 | 27 | type Func func(<-chan types.Item, chan<- types.Item) 28 | 29 | func (Func) Init(context.Context) error { 30 | return nil 31 | } 32 | 33 | func (pf Func) Process(in <-chan types.Item, out chan<- types.Item) { 34 | pf(in, out) 35 | } 36 | 37 | func AppendFunc(ps []Processor, fs ...Func) []Processor { 38 | pfs := make([]Processor, len(fs)) 39 | for i := range fs { 40 | pfs[i] = fs[i] 41 | } 42 | return append(ps, pfs...) 43 | } 44 | 45 | var Registry map[Type]map[string](func() Processor) = map[Type]map[string](func() Processor){ 46 | Pre: {}, 47 | Intra: {}, 48 | Post: {}, 49 | } 50 | 51 | func Register(t Type, name string, initializer func() Processor) { 52 | if _, ok := Registry[t][name]; ok { 53 | log.Fatalf("processor registry already contains plugin named %s", name) 54 | } 55 | Registry[t][name] = initializer 56 | } 57 | -------------------------------------------------------------------------------- /types/category.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package types 9 | 10 | type Category string 11 | 12 | const ( 13 | Archive Category = "archive" 14 | Image Category = "image" 15 | Subtitle Category = "subtitle" 16 | Text Category = "text" 17 | Unknown Category = "" 18 | Video Category = "video" 19 | ) 20 | 21 | var ArchiveExtensions = []string{ 22 | "7z", 23 | "gz", 24 | "gzip", 25 | "rar", 26 | "tar", 27 | "zip", 28 | } 29 | 30 | var ExecutableExtensions = []string{ 31 | "exe", 32 | } 33 | 34 | var ImageExtensions = []string{ 35 | "bmp", 36 | "gif", 37 | "heic", 38 | "jpeg", 39 | "jpg", 40 | "png", 41 | "tiff", 42 | } 43 | 44 | var SubtitleExtensions = []string{ 45 | "srt", 46 | "sub", 47 | } 48 | 49 | var TextExtensions = []string{ 50 | "info", 51 | "nfo", 52 | "txt", 53 | "website", 54 | } 55 | 56 | var VideoExtensions = []string{ 57 | "avi", 58 | "divx", 59 | "m4v", 60 | "mkv", 61 | "mov", 62 | "mp4", 63 | "xvid", 64 | } 65 | -------------------------------------------------------------------------------- /types/item.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package types 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/rbtr/pachinko/types/metadata" 14 | "github.com/rbtr/pachinko/types/metadata/movie" 15 | "github.com/rbtr/pachinko/types/metadata/tv" 16 | "github.com/rbtr/pachinko/types/metadata/video" 17 | ) 18 | 19 | type FileType int 20 | 21 | const ( 22 | Directory FileType = iota 23 | File 24 | ) 25 | 26 | // Item is the container struct for a file flowing through the entire pipeline. 27 | type Item struct { 28 | Category Category 29 | Delete bool 30 | DestinationPath string 31 | FileType FileType 32 | Identifiers map[string]string 33 | MediaType metadata.MediaType 34 | MovieMetadata movie.Metadata 35 | SourcePath string 36 | TVMetadata tv.Metadata 37 | VideoMetadata video.Metadata 38 | } 39 | 40 | // String formats the Item struct. 41 | func (m *Item) String() string { 42 | if m.MediaType == tv.TV { 43 | return fmt.Sprintf("%s Season %d Episode %d", m.TVMetadata.Name, m.TVMetadata.Episode.Season.Number, m.TVMetadata.Episode.Number) 44 | } 45 | return m.SourcePath 46 | } 47 | -------------------------------------------------------------------------------- /types/matcher.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package types 9 | 10 | import "regexp" 11 | 12 | // AudioChannels regexp constants. 13 | var AudioChannels = map[string]*regexp.Regexp{ 14 | "2.0": regexp.MustCompile(`2\.0`), 15 | "5.1": regexp.MustCompile(`5\.1`), 16 | "7.1": regexp.MustCompile(`7\.1`), 17 | } 18 | 19 | // AudioFormats regexp constants. 20 | var AudioFormats = map[string]*regexp.Regexp{ 21 | "aac": regexp.MustCompile("aac"), 22 | } 23 | 24 | // ColorFormats regexp constants. 25 | var ColorFormats = map[string]*regexp.Regexp{ 26 | "8 bit": regexp.MustCompile(`8.bit`), 27 | "10 bit": regexp.MustCompile(`10.bit`), 28 | } 29 | 30 | // Resolutions regexp constants. 31 | var Resolutions = map[string]*regexp.Regexp{ 32 | "1080p": regexp.MustCompile(`\b1080p?`), 33 | "720p": regexp.MustCompile(`\b720p?`), 34 | "480p": regexp.MustCompile(`\b480p?`), 35 | } 36 | 37 | // Sources regexp constants. 38 | var Sources = map[string]*regexp.Regexp{ 39 | "bluray": regexp.MustCompile("bluray"), 40 | "dvd": regexp.MustCompile("dvd"), 41 | "hdtv": regexp.MustCompile("hdtv"), 42 | } 43 | 44 | // TVSeason regexp constants. 45 | var TVSeason = map[string]*regexp.Regexp{ 46 | "season": regexp.MustCompile("season|series"), 47 | } 48 | 49 | // VideoFormats regexp constants. 50 | var VideoFormats = map[string]*regexp.Regexp{ 51 | "hevc": regexp.MustCompile("hevc"), 52 | "h.264": regexp.MustCompile(`h\.?264`), 53 | "h.265": regexp.MustCompile(`h\.?265`), 54 | "mov": regexp.MustCompile(`\bmov\b`), 55 | "mpeg": regexp.MustCompile("mpeg"), 56 | "x264": regexp.MustCompile(`x\.?264`), 57 | "x265": regexp.MustCompile(`x\.?265`), 58 | } 59 | -------------------------------------------------------------------------------- /types/matcher_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package types 9 | 10 | import ( 11 | "regexp" 12 | "testing" 13 | ) 14 | 15 | func matchHelper(t *testing.T, name string, matcher map[string]*regexp.Regexp) { 16 | t.Run(name, func(t *testing.T) { 17 | for n, r := range matcher { 18 | if !r.MatchString(n) { 19 | t.Errorf("failed to match %s", n) 20 | } 21 | } 22 | }) 23 | } 24 | 25 | func TestMatchers(t *testing.T) { 26 | matchHelper(t, "AudioChannels", AudioChannels) 27 | matchHelper(t, "AudioFormats", AudioFormats) 28 | matchHelper(t, "ColorFormats", ColorFormats) 29 | matchHelper(t, "VideoFormats", VideoFormats) 30 | matchHelper(t, "Resolutions", Resolutions) 31 | matchHelper(t, "Sources", Sources) 32 | } 33 | -------------------------------------------------------------------------------- /types/metadata/movie/movie.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package movie 9 | 10 | import ( 11 | "github.com/rbtr/pachinko/types/metadata" 12 | ) 13 | 14 | // Movie defines the Movie Metadata enum type. 15 | const Movie metadata.MediaType = "movie" 16 | 17 | // Metadata contains movie metadata. 18 | type Metadata struct { 19 | Title string 20 | ReleaseYear int 21 | } 22 | -------------------------------------------------------------------------------- /types/metadata/tv/tv.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package tv 9 | 10 | import ( 11 | "time" 12 | 13 | "github.com/rbtr/pachinko/types/metadata" 14 | ) 15 | 16 | // TV defines the TV type enum. 17 | const TV metadata.MediaType = "tv" 18 | 19 | // Season contains the TV Season metadata. 20 | type Season struct { 21 | Title string 22 | Number int 23 | } 24 | 25 | // Episode contains the TV Episode metadata. 26 | type Episode struct { 27 | Title string 28 | Number int 29 | AbsoluteNumber int 30 | Season Season 31 | AirDate time.Time 32 | } 33 | 34 | // Metadata contains TV metadata. 35 | type Metadata struct { 36 | Name string 37 | ReleaseYear int 38 | Episode 39 | } 40 | -------------------------------------------------------------------------------- /types/metadata/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package metadata 9 | 10 | // MediaCategory is the broad categarization of the file: image, video, document, etc. 11 | type MediaCategory string 12 | 13 | // MediaType is the specific type of the contents of the file: tv, movie, subtitle, photo. 14 | type MediaType string 15 | -------------------------------------------------------------------------------- /types/metadata/video/video.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 The Pachinko Authors 3 | 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 7 | */ 8 | package video 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/rbtr/pachinko/types/metadata" 14 | ) 15 | 16 | // Video defines the Video category enum. 17 | const Video metadata.MediaCategory = "video" 18 | 19 | // AudioChannels contains video metadata. 20 | type AudioChannels struct { 21 | FullRange int 22 | LimitedRange int 23 | } 24 | 25 | // String formats the AudioChannels struct. 26 | func (audio *AudioChannels) String() string { 27 | return fmt.Sprintf("%d.%d", audio.FullRange, audio.LimitedRange) 28 | } 29 | 30 | // Resolution contains video metadata. 31 | type Resolution struct { 32 | Width, Height int 33 | } 34 | 35 | // String formats the Resolution struct. 36 | func (rez *Resolution) String() string { 37 | return fmt.Sprintf("%dx%d", rez.Width, rez.Height) 38 | } 39 | 40 | // Metadata contains Video metadata. 41 | type Metadata struct { 42 | Resolution Resolution 43 | AudioChannels AudioChannels 44 | } 45 | --------------------------------------------------------------------------------