├── .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 | [](https://cloud.drone.io/rbtr/pachinko)
16 | [](https://goreportcard.com/report/github.com/rbtr/pachinko)
17 | [](https://github.com/rbtr/pachinko/releases/latest)
18 | [](https://hub.docker.com/r/rbtr/pachinko)
19 | [](/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 |
--------------------------------------------------------------------------------