Merge pull request #1948 from clinton-hall/test

Add more tests (pylint, etc)
This commit is contained in:
Labrys of Knossos 2022-12-19 13:38:12 -05:00 committed by GitHub
commit c5a8dc664b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 2179 additions and 5112 deletions

1
.gitignore vendored
View file

@ -15,3 +15,4 @@
*.dist-info *.dist-info
*.egg-info *.egg-info
/.vscode /.vscode
/htmlcov/

View file

@ -10,12 +10,12 @@ repos:
- id: name-tests-test - id: name-tests-test
- id: requirements-txt-fixer - id: requirements-txt-fixer
- repo: https://github.com/asottile/add-trailing-comma - repo: https://github.com/asottile/add-trailing-comma
rev: v2.3.0 rev: v2.4.0
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
args: [--py36-plus] args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.3.0 rev: v3.3.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
@ -23,3 +23,16 @@ repos:
# rev: v2.0.0 # rev: v2.0.0
# hooks: # hooks:
# - id: autopep8 # - id: autopep8
- repo: local
hooks:
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
args:
[
"-rn", # Only display messages
"-sn", # Disable score
"--rcfile=.pylintrc.ini", # Link to your config file
]

528
.pylintrc.ini Normal file
View file

@ -0,0 +1,528 @@
[MAIN]
load-plugins=
pylint.extensions.broad_try_clause,
pylint.extensions.code_style,
pylint.extensions.emptystring,
pylint.extensions.comparetozero,
pylint.extensions.comparison_placement,
pylint.extensions.confusing_elif,
pylint.extensions.for_any_all,
pylint.extensions.consider_ternary_expression,
pylint.extensions.bad_builtin,
pylint.extensions.mccabe,
; pylint.extensions.dict_init_mutate,
pylint.extensions.docstyle,
; pylint.extensions.dunder,
pylint.extensions.check_elif,
pylint.extensions.empty_comment,
pylint.extensions.eq_without_hash,
pylint.extensions.private_import,
; pylint.extensions.magic_value,
pylint.extensions.redefined_variable_type,
pylint.extensions.no_self_use,
pylint.extensions.overlapping_exceptions,
pylint.extensions.docparams,
pylint.extensions.redefined_loop_name,
pylint.extensions.set_membership,
pylint.extensions.typing,
pylint.extensions.while_used,
[MESSAGES CONTROL]
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
; # --- FATAL ---------
; F0001, # fatal
; F0002, # astroid-error
; F0010, # parse-error
; F0011, # config-parse-error
; F0202, # method-check-failed
; # --- ERROR ---------
; E0001, # syntax-error
; E0011, # unrecognized-inline-option
; E0013, # bad-plugin-value
; E0014, # bad-configuration-SECTION
; E0015, # unrecognized-option
; E0100, # init-is-generator
; E0101, # return-in-init
; E0102, # function-redefined
; E0103, # not-in-loop
; E0104, # return-outside-function
; E0105, # yield-outside-function,
; E0106, # return-arg-in-generator
; E0107, # nonexistent-operator
; E0108, # duplicate-argument-name
; E0110, # abstract-class-instantiated
; E0111, # bad-reversed-sequence
; E0112, # too-many-star-expressions
; E0113, # invalid-star-assignment-target
; E0114, # star-needs-assignment-target
; E0115, # nonlocal-and-global
; E0116, # continue-in-finally
; E0117, # nonlocal-without-binding
; E0118, # used-prior-global-declaration
; E0119, # misplaced-format-function
; E0202, # method-hidden
; E0203, # access-member-before-definition
; E0211, # no-method-argument
; E0213, # no-self-argument
; E0236, # invalid-slots-object
; E0237, # assigning-non-slot
; E0238, # invalid-slots
; E0239, # inherit-non-class
; E0240, # inconsistent-mro
; E0241, # duplicate-bases
; E0242, # class-variable-slots-conflict
; E0243, # invalid-class-object
; E0244, # invalid-enum-extension
; E0301, # non-iterator-returned
; E0302, # unexpected-special-method-signature
; E0303, # invalid-length-returned
; E0304, # invalid-bool-returned
; E0305, # invalid-index-returned
; E0306, # invalid-repr-returned
; E0307, # invalid-str-returned
; E0308, # invalid-bytes-returned
; E0309, # invalid-hash-returned
; E0310, # invalid-length-hint-returned
; E0311, # invalid-format-returned
; E0312, # invalid-getnewargs-returned
; E0313, # invalid-getnewargs-ex-returned
; E0401, # import-error
; E0402, # relative-beyond-top-level
; E0601, # used-before-assignment
; E0602, # undefined-variable
; E0603, # undefined-all-variable
; E0604, # invalid-all-object
; E0605, # invalid-all-format
; E0611, # no-name-in-module
; E0633, # unpacking-non-sequence
; E0643, # potential-index-error
; E0701, # bad-except-order
; E0702, # raising-bad-type
; E0704, # misplaced-bare-raise
; E0705, # bad-exception-cause
; E0710, # raising-non-exception
; E0711, # notimplemented-raised
; E0712, # catching-non-exception
; E1003, # bad-super-call
; E1101, # no-member
; E1102, # not-callable
; E1111, # assignment-from-no-return
; E1120, # no-value-for-parameter
; E1121, # too-many-function-args
; E1123, # unexpected-keyword-arg
; E1124, # redundant-keyword-arg
; E1125, # missing-kwoa
; E1126, # invalid-sequence-index
; E1127, # invalid-slice-index
; E1128, # assignment-from-none
; E1129, # not-context-manager
; E1130, # invalid-unary-operand-type
; E1131, # unsupported-binary-operation
; E1132, # repeated-keyword
; E1133, # not-an-iterable
; E1134, # not-a-mapping
; E1135, # unsupported-membership-test
; E1136, # unsubscriptable-object
; E1137, # unsupported-assignment-operation
; E1138, # unsupported-delete-operation
; E1139, # invalid-metaclass
; E1141, # dict-iter-missing-items
; E1142, # await-outside-async
; E1143, # unhashable-member
; E1144, # invalid-slice-step
; E1200, # logging-unsupported-format
; E1201, # logging-format-truncated
; E1205, # logging-too-many-args
; E1206, # logging-too-few-args
; E1300, # bad-format-character
; E1301, # truncated-format-string
; E1302, # mixed-format-string
; E1303, # format-needs-mapping
; E1304, # missing-format-string-key
; E1305, # too-many-format-args
; E1306, # too-few-format-args
; E1307, # bad-string-format-type
; E1310, # bad-str-strip-call
; E1507, # invalid-envvar-value
; E1519, # singledispatch-method
; E1520, # singledispatchmethod-function
; E1700, # yield-inside-async-function
; E1701, # not-async-context-manager
; E2501, # invalid-unicode-codec
; E2502, # bidirectional-unicode
; E2510, # invalid-character-backspace
; E2511, # invalid-character-carriage-return
; E2512, # invalid-character-sub
; E2513, # invalid-character-esc
; E2514, # invalid-character-nul
; E2515, # invalid-character-zero-width-space
; E4702, # modified-iterating-dict
; E4703, # modified-iterating-set
; E6004, # broken-noreturn
; E6005, # broken-collections-callable
; # --- WARNING -------
; W0012, # unknown-option-value
; W0101, # unreachable
; W0102, # dangerous-default-value
; W0104, # pointless-statement
; W0105, # pointless-string-statement
; W0106, # expression-not-assigned
; W0107, # unnecessary-pass
; W0108, # unnecessary-lambda
; W0109, # duplicate-key
; W0120, # useless-else-on-loop
; W0122, # exec-used
; W0123, # eval-used
; W0124, # confusing-with-statement
; W0125, # using-constant-test
; W0126, # missing-parentheses-for-call-in-test
; W0127, # self-assigning-variable
; W0128, # redeclared-assigned-name
; W0129, # assert-on-string-literal
; W0130, # duplicate-value
; W0131, # named-expr-without-context
; W0141, # bad-builtin
; W0143, # comparison-with-callable
; W0149, # while-used
; W0150, # lost-exception
; W0160, # consider-ternary-expression
; W0177, # nan-comparison
; W0199, # assert-on-tuple
; W0201, # attribute-defined-outside-init
; W0211, # bad-staticmethod-argument
; W0212, # protected-access
; W0221, # arguments-differ
; W0222, # signature-differs
; W0223, # abstract-method
; W0231, # super-init-not-called
; W0233, # non-parent-init-called
; W0236, # invalid-overridden-method
; W0237, # arguments-renamed
; W0238, # unused-private-member
; W0239, # overridden-final-method
; W0240, # subclassed-final-class
; W0244, # redefined-slots-in-subclass
; W0245, # super-without-brackets
; W0246, # useless-parent-delegation
; W0301, # unnecessary-semicolon
; W0311, # bad-indentation
; W0401, # wildcard-import
; W0404, # reimported
; W0406, # import-self
; W0407, # preferred-module
; W0410, # misplaced-future
; W0416, # shadowed-import
; W0511, # fixme
; W0601, # global-variable-undefined
; W0602, # global-variable-not-assigned
; W0603, # global-statement
; W0604, # global-at-module-level
; W0611, # unused-import
; W0612, # unused-variable
; W0613, # unused-argument
; W0614, # unused-wildcard-import
; W0621, # redefined-outer-name
; W0622, # redefined-builtin
; W0631, # undefined-loop-variable
; W0632, # unbalanced-tuple-unpacking
; W0640, # cell-var-from-loop
; W0641, # possibly-unused-variable
; W0642, # self-cls-assignment
; W0644, # unbalanced-dict-unpacking
; W0702, # bare-except
; W0705, # duplicate-except
; W0706, # try-except-raise
; W0707, # raise-missing-from
; W0711, # binary-op-exception
; W0714, # overlapping-except
; W0715, # raising-format-tuple
; W0716, # wrong-exception-operation
; W0717, # too-many-try-statements
; W0718, # broad-exception-caught
; W0719, # broad-exception-raised
; W1113, # keyword-arg-before-vararg
; W1114, # arguments-out-of-order
; W1115, # non-str-assignment-to-dunder-name
; W1116, # isinstance-second-argument-not-valid-type
; W1201, # logging-not-lazy
; W1202, # logging-format-interpolation
; W1203, # logging-fstring-interpolation
; W1300, # bad-format-string-key
; W1301, # unused-format-string-key
; W1302, # bad-format-string
; W1303, # missing-format-argument-key
; W1304, # unused-format-string-argument
; W1305, # format-combined-specification
; W1306, # missing-format-attribute
; W1307, # invalid-format-index
; W1308, # duplicate-string-formatting-argument
; W1309, # f-string-without-interpolation
; W1310, # format-string-without-interpolation
; W1401, # anomalous-backslash-in-string
; W1402, # anomalous-unicode-escape-in-string
; W1404, # implicit-str-concat
; W1405, # inconsistent-quotes
; W1406, # redundant-u-string-prefix
; W1501, # bad-open-mode
; W1502, # boolean-datetime
; W1503, # redundant-unittest-assert
; W1506, # bad-thread-instantiation
; W1507, # shallow-copy-environ
; W1508, # invalid-envvar-default
; W1509, # subprocess-popen-preexec-fn
; W1510, # subprocess-run-check
; W1514, # unspecified-encoding
; W1515, # forgotten-debug-statement
; W1518, # method-cache-max-size-none
; W1641, # eq-without-hash
; W2101, # useless-with-lock
; W2301, # unnecessary-ellipsis
; W2402, # non-ascii-file-name
; W2601, # using-f-string-in-unsupported-version
; W2602, # using-final-decorator-in-unsupported-version
; W2901, # redefined-loop-name
; W3101, # missing-timeout
; W3201, # bad-dunder-name
; W3301, # nested-min-max
; W4701, # modified-iterating-list
; W4901, # deprecated-module
; W4902, # deprecated-method
; W4903, # deprecated-argument
; W4904, # deprecated-class
; W4905, # deprecated-decorator
; W6001, # deprecated-typing-alias
; W9005, # multiple-constructor-doc
; W9006, # missing-raises-doc
; W9008, # redundant-returns-doc
; W9010, # redundant-yields-doc
; W9011, # missing-return-doc
; W9012, # missing-return-type-doc
; W9013, # missing-yield-doc
; W9015, # missing-param-doc
; W9014, # missing-yield-type-doc
; W9016, # missing-type-doc
; W9017, # differing-param-doc
; W9018, # differing-type-doc
; W9019, # useless-param-doc
; W9020, # useless-type-doc
; W9021, # missing-any-param-doc
; # --- CONVENTION ----
; C0103, # invalid-name
; C0104, # disallowed-name
; C0105, # typevar-name-incorrect-variance
; C0112, # empty-docstring
; C0113, # unneeded-not
; C0114, # missing-module-docstring
; C0115, # missing-class-docstring
; C0116, # missing-function-docstring
; C0121, # singleton-comparison
; C0123, # unidiomatic-typecheck
; C0131, # typevar-double-variance
; C0132, # typevar-name-mismatch
; C0198, # bad-docstring-quotes
; C0199, # docstring-first-line-empty
; C0200, # consider-using-enumerate
; C0201, # consider-iterating-dictionary
; C0202, # bad-classmethod-argument
; C0203, # bad-mcs-method-argument
; C0204, # bad-mcs-classmethod-argument
; C0205, # single-string-used-for-slots
; C0206, # consider-using-dict-items
; C0207, # use-maxsplit-arg
; C0208, # use-sequence-for-iteration
; C0209, # consider-using-f-string
; C0301, # line-too-long
; C0302, # too-many-lines
; C0303, # trailing-whitespace
; C0304, # missing-final-newline
; C0305, # trailing-newlines
; C0321, # multiple-statements
; C0325, # superfluous-parens
; C0327, # mixed-line-endings
; C0328, # unexpected-line-ending-format
; C0401, # wrong-spelling-in-comment
; C0402, # wrong-spelling-in-docstring
; C0403, # invalid-characters-in-docstring
; C0410, # multiple-imports
; C0411, # wrong-import-order
; C0412, # ungrouped-imports
; C0413, # wrong-import-position
; C0414, # useless-import-alias
; C0415, # import-outside-toplevel
; C0501, # consider-using-any-or-all
; C1802, # use-implicit-booleaness-not-len
; C1803, # use-implicit-booleaness-not-comparison
; C1901, # compare-to-empty-string
; C2001, # compare-to-zero
; C2201, # misplaced-comparison-constant
; C2401, # non-ascii-name
; C2403, # non-ascii-module-import
; C2503, # bad-file-encoding
; C2701, # import-private-name
; C2801, # unnecessary-dunder-call
; C3001, # unnecessary-lambda-assignment
; C3002, # unnecessary-direct-lambda-call
; C3401, # dict-init-mutate
; # --- REFACTOR ------
; R0022, # useless-option-value
; R0123, # literal-comparison
; R0124, # comparison-with-itself
; R0133, # comparison-of-constants
; R0202, # no-classmethod-decorator
; R0203, # no-staticmethod-decorator
; R0204, # redefined-variable-type
; R0205, # useless-object-inheritance
; R0206, # property-with-parameters
; R0401, # cyclic-import
; R0402, # consider-using-from-import
; R0801, # duplicate-code
; R0901, # too-many-ancestors
; R0902, # too-many-instance-attributes
; R0903, # too-few-public-methods
; R0904, # too-many-public-methods
; R0911, # too-many-return-statements
; R0912, # too-many-branches
; R0913, # too-many-arguments
; R0914, # too-many-locals
; R0915, # too-many-statements
; R0916, # too-many-boolean-expressions
; R1260, # too-complex
; R1701, # consider-merging-isinstance
; R1702, # too-many-nested-blocks
; R1703, # simplifiable-if-statement
; R1704, # redefined-argument-from-local
; R1705, # no-else-return
; R1706, # consider-using-ternary
; R1707, # trailing-comma-tuple
; R1708, # stop-iteration-return
; R1709, # simplify-boolean-expression
; R1710, # inconsistent-return-statements
; R1711, # useless-return
; R1712, # consider-swap-variables
; R1713, # consider-using-join
; R1714, # consider-using-in
; R1715, # consider-using-get
; R1716, # chained-comparison
; R1717, # consider-using-dict-comprehension
; R1718, # consider-using-set-comprehension
; R1719, # simplifiable-if-expression
; R1720, # no-else-raise
; R1721, # unnecessary-comprehension
; R1722, # consider-using-sys-exit
; R1723, # no-else-break
; R1724, # no-else-continue
; R1725, # super-with-arguments
; R1726, # simplifiable-condition
; R1727, # condition-evals-to-constant
; R1728, # consider-using-generator
; R1729, # use-a-generator
; R1730, # consider-using-min-builtin
; R1731, # consider-using-max-builtin
; R1732, # consider-using-with
; R1733, # unnecessary-dict-index-lookup
; R1734, # use-list-literal
; R1735, # use-dict-literal
; R1736, # unnecessary-list-index-lookup
; R2004, # magic-value-comparison
; R2044, # empty-comment
; R5501, # else-if-used
; R5601, # confusing-consecutive-elif
; R6002, # consider-using-alias
; R6003, # consider-alternative-union-syntax
; R6006, # redundant-typehint-argument
; R6101, # consider-using-namedtuple-or-dataclass
; R6102, # consider-using-tuple
; R6103, # consider-using-assignment-expr
; R6104, # consider-using-augmented-assign
; R6201, # use-set-for-membership
; R6301, # no-self-use
; # --- INFORMATION ---
; I0001, # raw-checker-failed
; I0010, # bad-inline-option
; I0011, # locally-disabled
; I0013, # file-ignored
; I0020, # suppressed-message
; I0021, # useless-suppression
; I0022, # deprecated-pragma
; I0023, # use-symbolic-message-instead
; I1101, # c-extension-no-member
disable=
E1101, # no-member
W0141, # bad-builtin
W0149, # while-used
W0160, # consider-ternary-expression
W0201, # attribute-defined-outside-init
W0212, # protected-access
W0511, # fixme
W0601, # global-variable-undefined
W0602, # global-variable-not-assigned
W0603, # global-statement
W0612, # unused-variable
W0621, # redefined-outer-name
W0631, # undefined-loop-variable
W0703, # broad-except
W0717, # too-many-try-statements
W1202, # logging-format-interpolation
W1203, # logging-fstring-interpolation
W1404, # implicit-str-concat
W2901, # redefined-loop-name
W3101, # missing-timeout
W6001, # deprecated-typing-alias
W9016, # missing-type-do
C0103, # invalid-name
C0114, # missing-module-docstring
C0115, # missing-class-docstring
C0116, # missing-function-docstring
C0199, # docstring-first-line-empty
C0201, # consider-iterating-dictionary
C0206, # consider-using-dict-items
C0301, # line-too-long
C0415, # import-outside-toplevel
C1901, # compare-to-empty-string
C2001, # compare-to-zero
R0204, # redifined-variable-type
R0401, # cyclic-import
R0801, # duplicate-code
R0903, # too-few-public-methods
R0902, # too-many-instance-attributes
R0911, # too-many-return-statements
R0912, # too-many-branches
R0913, # too-many-arguments
R0914, # too-many-locals
R0915, # too-many-statements
R0916, # too-many-boolean-expressions
R1260, # too-complex
R1702, # too-many-nested-blocks
R1704, # redefined-argument-from-local
R1710, # inconsistent-return-statements
R5501, # else-if-used
R5601, # confusing-consecutive-elif
R6003, # consider-alternative-union-syntax
R6102, # consider-using-tuple
R6103, # consider-using-assignment-expr
I0011, # locally-disabled
I0020, # suppressed-message
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=

View file

@ -60,7 +60,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
log.debug(f'Determined Directory: {input_directory} | Name: {input_name} | Category: {input_category}') log.debug(f'Determined Directory: {input_directory} | Name: {input_name} | Category: {input_category}')
# auto-detect section # auto-detect SECTION
section = nzb2media.CFG.findsection(input_category).isenabled() section = nzb2media.CFG.findsection(input_category).isenabled()
if section is None: # Check for user_scripts for 'ALL' and 'UNCAT' if section is None: # Check for user_scripts for 'ALL' and 'UNCAT'
if usercat in nzb2media.CATEGORIES: if usercat in nzb2media.CATEGORIES:
@ -122,10 +122,9 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
log.debug(f'Scanning files in directory: {input_directory}') log.debug(f'Scanning files in directory: {input_directory}')
if section_name in ['HeadPhones', 'Lidarr']: if section_name in {'HeadPhones', 'Lidarr'}:
nzb2media.NOFLATTEN.extend( # Make sure we preserve folder structure for HeadPhones.
input_category, nzb2media.NOFLATTEN.extend(input_category)
) # Make sure we preserve folder structure for HeadPhones.
now = datetime.datetime.now() now = datetime.datetime.now()
@ -138,10 +137,10 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
log.debug(f'Found 1 file to process: {input_directory}') log.debug(f'Found 1 file to process: {input_directory}')
else: else:
log.debug(f'Found {len(input_files)} files in {input_directory}') log.debug(f'Found {len(input_files)} files in {input_directory}')
for inputFile in input_files: for input_file in input_files:
file_path = os.path.dirname(inputFile) file_path = os.path.dirname(input_file)
file_name, file_ext = os.path.splitext(os.path.basename(inputFile)) file_name, file_ext = os.path.splitext(os.path.basename(input_file))
full_file_name = os.path.basename(inputFile) full_file_name = os.path.basename(input_file)
target_file = nzb2media.os.path.join(output_destination, full_file_name) target_file = nzb2media.os.path.join(output_destination, full_file_name)
if input_category in nzb2media.NOFLATTEN: if input_category in nzb2media.NOFLATTEN:
@ -152,9 +151,9 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
log.debug(f'Setting outputDestination to {os.path.dirname(target_file)} to preserve folder structure') log.debug(f'Setting outputDestination to {os.path.dirname(target_file)} to preserve folder structure')
if root == 1: if root == 1:
if not found_file: if not found_file:
log.debug(f'Looking for {input_name} in: {inputFile}') log.debug(f'Looking for {input_name} in: {input_file}')
if any([ if any([
nzb2media.sanitize_name(input_name) in nzb2media.sanitize_name(inputFile), nzb2media.sanitize_name(input_name) in nzb2media.sanitize_name(input_file),
nzb2media.sanitize_name(file_name) in nzb2media.sanitize_name(input_name), nzb2media.sanitize_name(file_name) in nzb2media.sanitize_name(input_name),
]): ]):
found_file = True found_file = True
@ -163,8 +162,8 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
continue continue
if root == 2: if root == 2:
mtime_lapse = now - datetime.datetime.fromtimestamp(os.path.getmtime(inputFile)) mtime_lapse = now - datetime.datetime.fromtimestamp(os.path.getmtime(input_file))
ctime_lapse = now - datetime.datetime.fromtimestamp(os.path.getctime(inputFile)) ctime_lapse = now - datetime.datetime.fromtimestamp(os.path.getctime(input_file))
if not found_file: if not found_file:
log.debug('Looking for files with modified/created dates less than 5 minutes old.') log.debug('Looking for files with modified/created dates less than 5 minutes old.')
@ -176,10 +175,10 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
if torrent_no_link == 0: if torrent_no_link == 0:
try: try:
nzb2media.copy_link(inputFile, target_file, nzb2media.USE_LINK) nzb2media.copy_link(input_file, target_file, nzb2media.USE_LINK)
nzb2media.remove_read_only(target_file) nzb2media.remove_read_only(target_file)
except Exception: except Exception:
log.error(f'Failed to link: {inputFile} to {target_file}') log.error(f'Failed to link: {input_file} to {target_file}')
input_name, output_destination = convert_to_ascii(input_name, output_destination) input_name, output_destination = convert_to_ascii(input_name, output_destination)
@ -192,7 +191,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
nzb2media.flatten(output_destination) nzb2media.flatten(output_destination)
# Now check if video files exist in destination: # Now check if video files exist in destination:
if section_name in ['SickBeard', 'SiCKRAGE', 'NzbDrone', 'Sonarr', 'CouchPotato', 'Radarr', 'Watcher3']: if section_name in {'SickBeard', 'SiCKRAGE', 'NzbDrone', 'Sonarr', 'CouchPotato', 'Radarr', 'Watcher3'}:
num_videos = len( num_videos = len(
nzb2media.list_media_files(output_destination, media=True, audio=False, meta=False, archives=False), nzb2media.list_media_files(output_destination, media=True, audio=False, meta=False, archives=False),
) )
@ -232,7 +231,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
'Mylar': comics.process, 'Mylar': comics.process,
'Gamez': games.process, 'Gamez': games.process,
} }
if input_hash and section_name in ['SickBeard', 'SiCKRAGE', 'NzbDrone', 'Sonarr']: if input_hash and section_name in {'SickBeard', 'SiCKRAGE', 'NzbDrone', 'Sonarr'}:
input_hash = input_hash.upper() input_hash = input_hash.upper()
processor = process_map[section_name] processor = process_map[section_name]
result = processor( result = processor(
@ -358,4 +357,4 @@ def main(args):
if __name__ == '__main__': if __name__ == '__main__':
exit(main(sys.argv)) sys.exit(main(sys.argv))

7
dev-requirements.txt Normal file
View file

@ -0,0 +1,7 @@
black
bump2version
mypy
pre-commit
pylint[spelling]
pytest
tox

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,8 @@ import logging
import requests import requests
import nzb2media import nzb2media
import nzb2media.utils.common
from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import ProcessResult
from nzb2media.utils.common import flatten
from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.encoding import convert_to_ascii
from nzb2media.utils.network import server_responding from nzb2media.utils.network import server_responding
from nzb2media.utils.paths import remote_dir from nzb2media.utils.paths import remote_dir
@ -15,88 +15,42 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def process( def process(*, section: str, dir_name: str, input_name: str = '', input_category: str = '', **kwargs) -> ProcessResult:
*, log.debug(f'Unused kwargs: {kwargs}')
section: str,
dir_name: str,
input_name: str = '',
status: int = 0,
client_agent: str = 'manual',
download_id: str = '',
input_category: str = '',
failure_link: str = '',
) -> ProcessResult:
# Get configuration # Get configuration
if nzb2media.CFG is None: if nzb2media.CFG is None:
raise RuntimeError('Configuration not loaded.') raise RuntimeError('Configuration not loaded.')
cfg = nzb2media.CFG[section][input_category] cfg = nzb2media.CFG[section][input_category]
# Base URL # Base URL
ssl = int(cfg.get('ssl', 0)) ssl = int(cfg.get('ssl', 0))
scheme = 'https' if ssl else 'http' scheme = 'https' if ssl else 'http'
host = cfg['host'] host = cfg['host']
port = cfg['port'] port = cfg['port']
web_root = cfg.get('web_root', '') web_root = cfg.get('web_root', '')
# Authentication # Authentication
apikey = cfg.get('apikey', '') apikey = cfg.get('apikey', '')
# Params # Params
remote_path = int(cfg.get('remote_path', 0)) remote_path = int(cfg.get('remote_path', 0))
# Misc # Misc
# Begin processing # Begin processing
url = nzb2media.utils.common.create_url(scheme, host, port, web_root) url = nzb2media.utils.common.create_url(scheme, host, port, web_root)
if not server_responding(url): if not server_responding(url):
log.error('Server did not respond. Exiting') log.error('Server did not respond. Exiting')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - {section} did not respond.')
f'{section}: Failed to post-process - {section} did not respond.',
)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
params = {'apikey': apikey, 'cmd': 'forceProcess', 'dir': remote_dir(dir_name) if remote_path else dir_name}
params = {
'apikey': apikey,
'cmd': 'forceProcess',
'dir': remote_dir(dir_name) if remote_path else dir_name,
}
log.debug(f'Opening URL: {url} with params: {params}') log.debug(f'Opening URL: {url} with params: {params}')
try: try:
response = requests.get(url, params=params, verify=False, timeout=(30, 300)) response = requests.get(url, params=params, verify=False, timeout=(30, 300))
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to open URL') log.error('Unable to open URL')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to connect to {section}')
f'{section}: Failed to post-process - Unable to connect to '
f'{section}',
)
log.debug(response.text) log.debug(response.text)
if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
if response.status_code not in [
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {response.status_code}') log.error(f'Server returned status {response.status_code}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Server returned status {response.status_code}')
f'{section}: Failed to post-process - Server returned status ' if response.text == 'OK':
f'{response.status_code}', log.debug(f'SUCCESS: ForceProcess for {dir_name} has been started in LazyLibrarian')
) return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
elif response.text == 'OK': log.error(f'FAILED: ForceProcess of {dir_name} has Failed in LazyLibrarian')
log.debug( return ProcessResult.failure(f'{section}: Failed to post-process - Returned log from {section} was not as expected.')
f'SUCCESS: ForceProcess for {dir_name} has been started in LazyLibrarian',
)
return ProcessResult.success(
f'{section}: Successfully post-processed {input_name}',
)
else:
log.error(
f'FAILED: ForceProcess of {dir_name} has Failed in LazyLibrarian',
)
return ProcessResult.failure(
f'{section}: Failed to post-process - Returned log from {section} '
f'was not as expected.',
)

View file

@ -6,8 +6,8 @@ import os
import requests import requests
import nzb2media import nzb2media
import nzb2media.utils.common
from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import ProcessResult
from nzb2media.utils.common import flatten
from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.encoding import convert_to_ascii
from nzb2media.utils.network import server_responding from nzb2media.utils.network import server_responding
from nzb2media.utils.paths import remote_dir from nzb2media.utils.paths import remote_dir
@ -16,105 +16,58 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def process( def process(*, section: str, dir_name: str, input_name: str = '', input_category: str = '', status: int = 0, **kwargs) -> ProcessResult:
*, log.debug(f'Unused kwargs: {kwargs}')
section: str,
dir_name: str,
input_name: str = '',
status: int = 0,
client_agent: str = 'manual',
download_id: str = '',
input_category: str = '',
failure_link: str = '',
) -> ProcessResult:
# Get configuration # Get configuration
if nzb2media.CFG is None: if nzb2media.CFG is None:
raise RuntimeError('Configuration not loaded.') raise RuntimeError('Configuration not loaded.')
cfg = nzb2media.CFG[section][input_category] cfg = nzb2media.CFG[section][input_category]
# Base URL # Base URL
ssl = int(cfg.get('ssl', 0)) ssl = int(cfg.get('ssl', 0))
scheme = 'https' if ssl else 'http' scheme = 'https' if ssl else 'http'
host = cfg['host'] host = cfg['host']
port = cfg['port'] port = cfg['port']
web_root = cfg.get('web_root', '') web_root = cfg.get('web_root', '')
# Authentication # Authentication
apikey = cfg.get('apikey', '') apikey = cfg.get('apikey', '')
# Params # Params
remote_path = int(cfg.get('remote_path', 0)) remote_path = int(cfg.get('remote_path', 0))
# Misc # Misc
apc_version = '2.04' apc_version = '2.04'
comicrn_version = '1.01' comicrn_version = '1.01'
# Begin processing # Begin processing
url = nzb2media.utils.common.create_url(scheme, host, port, web_root) url = nzb2media.utils.common.create_url(scheme, host, port, web_root)
if not server_responding(url): if not server_responding(url):
log.error('Server did not respond. Exiting') log.error('Server did not respond. Exiting')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - {section} did not respond.')
f'{section}: Failed to post-process - {section} did not respond.',
)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
clean_name, ext = os.path.splitext(input_name) clean_name, ext = os.path.splitext(input_name)
if len(ext) == 4: # we assume this was a standard extension. if len(ext) == 4: # we assume this was a standard extension.
input_name = clean_name input_name = clean_name
params = {'cmd': 'forceProcess', 'apikey': apikey, 'nzb_folder': remote_dir(dir_name) if remote_path else dir_name}
params = {
'cmd': 'forceProcess',
'apikey': apikey,
'nzb_folder': remote_dir(dir_name) if remote_path else dir_name,
}
if input_name is not None: if input_name is not None:
params['nzb_name'] = input_name params['nzb_name'] = input_name
params['failed'] = int(status) params['failed'] = int(status)
params['apc_version'] = apc_version params['apc_version'] = apc_version
params['comicrn_version'] = comicrn_version params['comicrn_version'] = comicrn_version
success = False success = False
log.debug(f'Opening URL: {url}') log.debug(f'Opening URL: {url}')
try: try:
r = requests.post( response = requests.post(url, params=params, stream=True, verify=False, timeout=(30, 300))
url, params=params, stream=True, verify=False, timeout=(30, 300),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to open URL') log.error('Unable to open URL')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to connect to {section}')
f'{section}: Failed to post-process - Unable to connect to ' log.debug(response.text)
f'{section}', if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
) log.error(f'Server returned status {response.status_code}')
if r.status_code not in [ return ProcessResult.failure(f'{section}: Failed to post-process - Server returned status {response.status_code}')
requests.codes.ok, for line in response.text.split('\n'):
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {r.status_code}')
return ProcessResult.failure(
f'{section}: Failed to post-process - Server returned status '
f'{r.status_code}',
)
for line in r.text.split('\n'):
if line: if line:
log.debug(line) log.debug(line)
if 'Post Processing SUCCESSFUL' in line: if 'Post Processing SUCCESSFUL' in line:
success = True success = True
if success: if success:
log.debug('SUCCESS: This issue has been processed successfully') log.debug('SUCCESS: This issue has been processed successfully')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', log.warning('The issue does not appear to have successfully processed. ' 'Please check your Logs')
) return ProcessResult.failure(f'{section}: Failed to post-process - Returned log from {section} was not as expected.')
else:
log.warning(
'The issue does not appear to have successfully processed. '
'Please check your Logs',
)
return ProcessResult.failure(
f'{section}: Failed to post-process - Returned log from '
f'{section} was not as expected.',
)

View file

@ -34,27 +34,15 @@ class ProcessResult(typing.NamedTuple):
def command_complete(url, params, headers, section): def command_complete(url, params, headers, section):
try: try:
r = requests.get( respone = requests.get(url, params=params, headers=headers, stream=True, verify=False, timeout=(30, 60))
url,
params=params,
headers=headers,
stream=True,
verify=False,
timeout=(30, 60),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL: {url}') log.error(f'Unable to open URL: {url}')
return None return None
if r.status_code not in [ if respone.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
requests.codes.ok, log.error(f'Server returned status {respone.status_code}')
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {r.status_code}')
return None return None
else:
try: try:
return r.json()['status'] return respone.json()['status']
except (ValueError, KeyError): except (ValueError, KeyError):
# ValueError catches simplejson's JSONDecodeError and # ValueError catches simplejson's JSONDecodeError and
# json's ValueError # json's ValueError
@ -62,29 +50,17 @@ def command_complete(url, params, headers, section):
return None return None
def completed_download_handling(url2, headers, section='MAIN'): def completed_download_handling(url2, headers):
try: try:
r = requests.get( response = requests.get(url2, params={}, headers=headers, stream=True, verify=False, timeout=(30, 60))
url2,
params={},
headers=headers,
stream=True,
verify=False,
timeout=(30, 60),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL: {url2}') log.error(f'Unable to open URL: {url2}')
return False return False
if r.status_code not in [ if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
requests.codes.ok, log.error(f'Server returned status {response.status_code}')
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {r.status_code}')
return False return False
else:
try: try:
return r.json().get('enableCompletedDownloadHandling', False) return response.json().get('enableCompletedDownloadHandling', False)
except ValueError: except ValueError:
# ValueError catches simplejson's JSONDecodeError and json's ValueError # ValueError catches simplejson's JSONDecodeError and json's ValueError
return False return False

View file

@ -7,8 +7,8 @@ import shutil
import requests import requests
import nzb2media import nzb2media
import nzb2media.utils.common
from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import ProcessResult
from nzb2media.utils.common import flatten
from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.encoding import convert_to_ascii
from nzb2media.utils.network import server_responding from nzb2media.utils.network import server_responding
@ -16,72 +16,40 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def process( def process(*, section: str, dir_name: str, input_name: str = '', status: int = 0, input_category: str = '', **kwargs) -> ProcessResult:
*, log.debug(f'Unused kwargs: {kwargs}')
section: str,
dir_name: str,
input_name: str = '',
status: int = 0,
client_agent: str = 'manual',
download_id: str = '',
input_category: str = '',
failure_link: str = '',
) -> ProcessResult:
# Get configuration # Get configuration
if nzb2media.CFG is None: if nzb2media.CFG is None:
raise RuntimeError('Configuration not loaded.') raise RuntimeError('Configuration not loaded.')
cfg = nzb2media.CFG[section][input_category] cfg = nzb2media.CFG[section][input_category]
# Base URL # Base URL
ssl = int(cfg.get('ssl', 0)) ssl = int(cfg.get('ssl', 0))
scheme = 'https' if ssl else 'http' scheme = 'https' if ssl else 'http'
host = cfg['host'] host = cfg['host']
port = cfg['port'] port = cfg['port']
web_root = cfg.get('web_root', '') web_root = cfg.get('web_root', '')
# Authentication # Authentication
apikey = cfg.get('apikey', '') apikey = cfg.get('apikey', '')
# Params # Params
# Misc # Misc
library = cfg.get('library') library = cfg.get('library')
# Begin processing # Begin processing
url = nzb2media.utils.common.create_url(scheme, host, port, web_root) url = nzb2media.utils.common.create_url(scheme, host, port, web_root)
if not server_responding(url): if not server_responding(url):
log.error('Server did not respond. Exiting') log.error('Server did not respond. Exiting')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - {section} did not respond.')
f'{section}: Failed to post-process - {section} did not respond.',
)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
fields = input_name.split('-') fields = input_name.split('-')
gamez_id = fields[0].replace('[', '').replace(']', '').replace(' ', '') gamez_id = fields[0].replace('[', '').replace(']', '').replace(' ', '')
download_status = 'Downloaded' if status == 0 else 'Wanted' download_status = 'Downloaded' if status == 0 else 'Wanted'
params = {'api_key': apikey, 'mode': 'UPDATEREQUESTEDSTATUS', 'db_id': gamez_id, 'status': download_status}
params = {
'api_key': apikey,
'mode': 'UPDATEREQUESTEDSTATUS',
'db_id': gamez_id,
'status': download_status,
}
log.debug(f'Opening URL: {url}') log.debug(f'Opening URL: {url}')
try: try:
r = requests.get(url, params=params, verify=False, timeout=(30, 300)) resposne = requests.get(url, params=params, verify=False, timeout=(30, 300))
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to open URL') log.error('Unable to open URL')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to connect to {section}')
f'{section}: Failed to post-process - Unable to connect to ' result = resposne.json()
f'{section}',
)
result = r.json()
log.debug(result) log.debug(result)
if library: if library:
log.debug(f'moving files to library: {library}') log.debug(f'moving files to library: {library}')
@ -89,34 +57,15 @@ def process(
shutil.move(dir_name, os.path.join(library, input_name)) shutil.move(dir_name, os.path.join(library, input_name))
except Exception: except Exception:
log.error(f'Unable to move {dir_name} to {os.path.join(library, input_name)}') log.error(f'Unable to move {dir_name} to {os.path.join(library, input_name)}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to move files')
f'{section}: Failed to post-process - Unable to move files',
)
else: else:
log.error('No library specified to move files to. Please edit your configuration.') log.error('No library specified to move files to. Please edit your configuration.')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - No library defined in {section}')
f'{section}: Failed to post-process - No library defined in ' if resposne.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
f'{section}', log.error(f'Server returned status {resposne.status_code}')
) return ProcessResult.failure(f'{section}: Failed to post-process - Server returned status {resposne.status_code}')
if result['success']:
if r.status_code not in [
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {r.status_code}')
return ProcessResult.failure(
f'{section}: Failed to post-process - Server returned status '
f'{r.status_code}',
)
elif result['success']:
log.debug(f'SUCCESS: Status for {gamez_id} has been set to {download_status} in Gamez') log.debug(f'SUCCESS: Status for {gamez_id} has been set to {download_status} in Gamez')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}',
)
else:
log.error(f'FAILED: Status for {gamez_id} has NOT been updated in Gamez') log.error(f'FAILED: Status for {gamez_id} has NOT been updated in Gamez')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Returned log from {section} was not as expected.')
f'{section}: Failed to post-process - Returned log from {section} '
f'was not as expected.',
)

View file

@ -8,6 +8,7 @@ import time
import requests import requests
import nzb2media import nzb2media
import nzb2media.utils.common
from nzb2media import transcoder from nzb2media import transcoder
from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import ProcessResult
from nzb2media.auto_process.common import command_complete from nzb2media.auto_process.common import command_complete
@ -15,13 +16,14 @@ from nzb2media.auto_process.common import completed_download_handling
from nzb2media.plugins.subtitles import import_subs from nzb2media.plugins.subtitles import import_subs
from nzb2media.plugins.subtitles import rename_subs from nzb2media.plugins.subtitles import rename_subs
from nzb2media.scene_exceptions import process_all_exceptions from nzb2media.scene_exceptions import process_all_exceptions
from nzb2media.utils.common import flatten
from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.encoding import convert_to_ascii
from nzb2media.utils.files import extract_files
from nzb2media.utils.files import list_media_files from nzb2media.utils.files import list_media_files
from nzb2media.utils.identification import find_imdbid from nzb2media.utils.identification import find_imdbid
from nzb2media.utils.network import find_download from nzb2media.utils.network import find_download
from nzb2media.utils.network import server_responding from nzb2media.utils.network import server_responding
from nzb2media.utils.nzb import report_nzb from nzb2media.utils.nzb import report_nzb
from nzb2media.utils.paths import rchmod
from nzb2media.utils.paths import remote_dir from nzb2media.utils.paths import remote_dir
from nzb2media.utils.paths import remove_dir from nzb2media.utils.paths import remove_dir
@ -29,38 +31,24 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def process( def process(*, section: str, dir_name: str, input_name: str = '', status: int = 0, client_agent: str = 'manual', download_id: str = '', input_category: str = '', failure_link: str = '') -> ProcessResult:
*,
section: str,
dir_name: str,
input_name: str = '',
status: int = 0,
client_agent: str = 'manual',
download_id: str = '',
input_category: str = '',
failure_link: str = '',
) -> ProcessResult:
# Get configuration # Get configuration
if nzb2media.CFG is None: if nzb2media.CFG is None:
raise RuntimeError('Configuration not loaded.') raise RuntimeError('Configuration not loaded.')
cfg = nzb2media.CFG[section][input_category] cfg = nzb2media.CFG[section][input_category]
# Base URL # Base URL
ssl = int(cfg.get('ssl', 0)) ssl = int(cfg.get('ssl', 0))
scheme = 'https' if ssl else 'http' scheme = 'https' if ssl else 'http'
host = cfg['host'] host = cfg['host']
port = cfg['port'] port = cfg['port']
web_root = cfg.get('web_root', '') web_root = cfg.get('web_root', '')
# Authentication # Authentication
apikey = cfg.get('apikey', '') apikey = cfg.get('apikey', '')
omdbapikey = cfg.get('omdbapikey', '') omdbapikey = cfg.get('omdbapikey', '')
# Params # Params
delete_failed = int(cfg.get('delete_failed', 0)) delete_failed = int(cfg.get('delete_failed', 0))
remote_path = int(cfg.get('remote_path', 0)) remote_path = int(cfg.get('remote_path', 0))
wait_for = int(cfg.get('wait_for', 2)) wait_for = int(cfg.get('wait_for', 2))
# Misc # Misc
if status > 0 and nzb2media.NOEXTRACTFAILED: if status > 0 and nzb2media.NOEXTRACTFAILED:
extract = 0 extract = 0
@ -74,7 +62,6 @@ def process(
method = cfg.get('method', None) method = cfg.get('method', None)
if section != 'CouchPotato': if section != 'CouchPotato':
method = None method = None
# Begin processing # Begin processing
imdbid = find_imdbid(dir_name, input_name, omdbapikey) imdbid = find_imdbid(dir_name, input_name, omdbapikey)
if section == 'CouchPotato': if section == 'CouchPotato':
@ -99,10 +86,7 @@ def process(
release = None release = None
else: else:
log.error('Server did not respond. Exiting') log.error('Server did not respond. Exiting')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - {section} did not respond.')
f'{section}: Failed to post-process - {section} did not respond.',
)
# pull info from release found if available # pull info from release found if available
release_id = None release_id = None
media_id = None media_id = None
@ -117,48 +101,29 @@ def process(
release_status_old = release[release_id]['status'] release_status_old = release[release_id]['status']
except Exception: except Exception:
pass pass
if not os.path.isdir(dir_name) and os.path.isfile(dir_name): # If the input directory is a file, assume single file download and split dir/name.
if not os.path.isdir(dir_name) and os.path.isfile(
dir_name,
): # If the input directory is a file, assume single file download and split dir/name.
dir_name = os.path.split(os.path.normpath(dir_name))[0] dir_name = os.path.split(os.path.normpath(dir_name))[0]
specific_path = os.path.join(dir_name, str(input_name)) specific_path = os.path.join(dir_name, str(input_name))
clean_name = os.path.splitext(specific_path) clean_name = os.path.splitext(specific_path)
if clean_name[1] == '.nzb': if clean_name[1] == '.nzb':
specific_path = clean_name[0] specific_path = clean_name[0]
if os.path.isdir(specific_path): if os.path.isdir(specific_path):
dir_name = specific_path dir_name = specific_path
process_all_exceptions(input_name, dir_name) process_all_exceptions(input_name, dir_name)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
if not list_media_files(dir_name, media=True, audio=False, meta=False, archives=False) and list_media_files(dir_name, media=False, audio=False, meta=False, archives=True) and extract:
if (
not list_media_files(
dir_name, media=True, audio=False, meta=False, archives=False,
)
and list_media_files(
dir_name, media=False, audio=False, meta=False, archives=True,
)
and extract
):
log.debug(f'Checking for archives to extract in directory: {dir_name}') log.debug(f'Checking for archives to extract in directory: {dir_name}')
nzb2media.extract_files(dir_name) extract_files(dir_name)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
good_files = 0 good_files = 0
valid_files = 0 valid_files = 0
num_files = 0 num_files = 0
# Check video files for corruption # Check video files for corruption
for video in list_media_files( for video in list_media_files(dir_name, media=True, audio=False, meta=False, archives=False):
dir_name, media=True, audio=False, meta=False, archives=False,
):
num_files += 1 num_files += 1
if transcoder.is_video_good(video, status): if transcoder.is_video_good(video, status):
good_files += 1 good_files += 1
if not nzb2media.REQUIRE_LAN or transcoder.is_video_good( if not nzb2media.REQUIRE_LAN or transcoder.is_video_good(video, status, require_lan=nzb2media.REQUIRE_LAN):
video, status, require_lan=nzb2media.REQUIRE_LAN,
):
valid_files += 1 valid_files += 1
import_subs(video) import_subs(video)
rename_subs(dir_name) rename_subs(dir_name)
@ -169,163 +134,91 @@ def process(
elif num_files and valid_files < num_files: elif num_files and valid_files < num_files:
log.info('Status shown as success from Downloader, but corrupt video files found. Setting as failed.') log.info('Status shown as success from Downloader, but corrupt video files found. Setting as failed.')
status = 1 status = 1
if ( if 'NZBOP_VERSION' in os.environ and os.environ['NZBOP_VERSION'][0:5] >= '14.0':
'NZBOP_VERSION' in os.environ
and os.environ['NZBOP_VERSION'][0:5] >= '14.0'
):
print('[NZB] MARK=BAD') print('[NZB] MARK=BAD')
if good_files == num_files: if good_files == num_files:
log.debug(f'Video marked as failed due to missing required language: {nzb2media.REQUIRE_LAN}') log.debug(f'Video marked as failed due to missing required language: {nzb2media.REQUIRE_LAN}')
else: else:
log.debug('Video marked as failed due to missing playable audio or video') log.debug('Video marked as failed due to missing playable audio or video')
if ( if good_files < num_files and failure_link: # only report corrupt files
good_files < num_files and failure_link
): # only report corrupt files
failure_link += '&corrupt=true' failure_link += '&corrupt=true'
elif client_agent == 'manual': elif client_agent == 'manual':
log.warning(f'No media files found in directory {dir_name} to manually process.') log.warning(f'No media files found in directory {dir_name} to manually process.')
return ProcessResult( # Success (as far as this script is concerned)
message='', return ProcessResult.success()
status_code=0, # Success (as far as this script is concerned)
)
else: else:
log.warning(f'No media files found in directory {dir_name}. Processing this as a failed download') log.warning(f'No media files found in directory {dir_name}. Processing this as a failed download')
status = 1 status = 1
if ( if 'NZBOP_VERSION' in os.environ and os.environ['NZBOP_VERSION'][0:5] >= '14.0':
'NZBOP_VERSION' in os.environ
and os.environ['NZBOP_VERSION'][0:5] >= '14.0'
):
print('[NZB] MARK=BAD') print('[NZB] MARK=BAD')
if status == 0: if status == 0:
if nzb2media.TRANSCODE == 1: if nzb2media.TRANSCODE == 1:
result, new_dir_name = transcoder.transcode_directory(dir_name) result, new_dir_name = transcoder.transcode_directory(dir_name)
if result == 0: if result == 0:
log.debug(f'Transcoding succeeded for files in {dir_name}') log.debug(f'Transcoding succeeded for files in {dir_name}')
dir_name = new_dir_name dir_name = new_dir_name
log.debug(f'Config setting \'chmodDirectory\' currently set to {oct(chmod_directory)}') log.debug(f'Config setting \'chmodDirectory\' currently set to {oct(chmod_directory)}')
if chmod_directory: if chmod_directory:
log.info(f'Attempting to set the octal permission of \'{oct(chmod_directory)}\' on directory \'{dir_name}\'') log.info(f'Attempting to set the octal permission of \'{oct(chmod_directory)}\' on directory \'{dir_name}\'')
nzb2media.rchmod(dir_name, chmod_directory) rchmod(dir_name, chmod_directory)
else: else:
log.error(f'Transcoding failed for files in {dir_name}') log.error(f'Transcoding failed for files in {dir_name}')
return ProcessResult( return ProcessResult(message=f'{section}: Failed to post-process - Transcoding failed', status_code=1)
message=f'{section}: Failed to post-process - Transcoding failed', for video in list_media_files(dir_name, media=True, audio=False, meta=False, archives=False):
status_code=1,
)
for video in list_media_files(
dir_name, media=True, audio=False, meta=False, archives=False,
):
if not release and '.cp(tt' not in video and imdbid: if not release and '.cp(tt' not in video and imdbid:
video_name, video_ext = os.path.splitext(video) video_name, video_ext = os.path.splitext(video)
video2 = f'{video_name}.cp({imdbid}){video_ext}' video2 = f'{video_name}.cp({imdbid}){video_ext}'
if not ( if not (client_agent in [nzb2media.TORRENT_CLIENT_AGENT, 'manual'] and nzb2media.USE_LINK == 'move-sym'):
client_agent in [nzb2media.TORRENT_CLIENT_AGENT, 'manual']
and nzb2media.USE_LINK == 'move-sym'
):
log.debug(f'Renaming: {video} to: {video2}') log.debug(f'Renaming: {video} to: {video2}')
os.rename(video, video2) os.rename(video, video2)
if not apikey: # If only using Transcoder functions, exit here. if not apikey: # If only using Transcoder functions, exit here.
log.info('No CouchPotato or Radarr or Watcher3 apikey entered. Processing completed.') log.info('No CouchPotato or Radarr or Watcher3 apikey entered. Processing completed.')
return ProcessResult( return ProcessResult(message=f'{section}: Successfully post-processed {input_name}', status_code=0)
message=f'{section}: Successfully post-processed {input_name}', params = {'media_folder': remote_dir(dir_name) if remote_path else dir_name}
status_code=0,
)
params = {
'media_folder': remote_dir(dir_name) if remote_path else dir_name,
}
if download_id and release_id: if download_id and release_id:
params['downloader'] = downloader or client_agent params['downloader'] = downloader or client_agent
params['download_id'] = download_id params['download_id'] = download_id
if section == 'CouchPotato': if section == 'CouchPotato':
if method == 'manage': if method == 'manage':
command = 'manage.update' command = 'manage.update'
params.clear() params.clear()
else: else:
command = 'renamer.scan' command = 'renamer.scan'
url = f'{base_url}{command}' url = f'{base_url}{command}'
log.debug(f'Opening URL: {url} with PARAMS: {params}') log.debug(f'Opening URL: {url} with PARAMS: {params}')
log.debug(f'Starting {method} scan for {input_name}') log.debug(f'Starting {method} scan for {input_name}')
if section == 'Radarr': if section == 'Radarr':
payload = { payload = {'name': 'DownloadedMoviesScan', 'path': params['media_folder'], 'downloadClientId': download_id, 'importMode': import_mode}
'name': 'DownloadedMoviesScan',
'path': params['media_folder'],
'downloadClientId': download_id,
'importMode': import_mode,
}
if not download_id: if not download_id:
payload.pop('downloadClientId') payload.pop('downloadClientId')
log.debug(f'Opening URL: {base_url} with PARAMS: {payload}') log.debug(f'Opening URL: {base_url} with PARAMS: {payload}')
log.debug(f'Starting DownloadedMoviesScan scan for {input_name}') log.debug(f'Starting DownloadedMoviesScan scan for {input_name}')
if section == 'Watcher3': if section == 'Watcher3':
if input_name and os.path.isfile( if input_name and os.path.isfile(os.path.join(dir_name, input_name)):
os.path.join(dir_name, input_name), params['media_folder'] = os.path.join(params['media_folder'], input_name)
): payload = {'apikey': apikey, 'path': params['media_folder'], 'guid': download_id, 'mode': 'complete'}
params['media_folder'] = os.path.join(
params['media_folder'], input_name,
)
payload = {
'apikey': apikey,
'path': params['media_folder'],
'guid': download_id,
'mode': 'complete',
}
if not download_id: if not download_id:
payload.pop('guid') payload.pop('guid')
log.debug(f'Opening URL: {base_url} with PARAMS: {payload}') log.debug(f'Opening URL: {base_url} with PARAMS: {payload}')
log.debug(f'Starting postprocessing scan for {input_name}') log.debug(f'Starting postprocessing scan for {input_name}')
try: try:
if section == 'CouchPotato': if section == 'CouchPotato':
response = requests.get( response = requests.get(url, params=params, verify=False, timeout=(30, 1800))
url, params=params, verify=False, timeout=(30, 1800),
)
elif section == 'Watcher3': elif section == 'Watcher3':
response = requests.post( response = requests.post(base_url, data=payload, verify=False, timeout=(30, 1800))
base_url, data=payload, verify=False, timeout=(30, 1800),
)
else: else:
response = requests.post( response = requests.post(base_url, data=json.dumps(payload), headers=headers, stream=True, verify=False, timeout=(30, 1800))
base_url,
data=json.dumps(payload),
headers=headers,
stream=True,
verify=False,
timeout=(30, 1800),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to open URL') log.error('Unable to open URL')
return ProcessResult( return ProcessResult(message=f'{section}: Failed to post-process - Unable to connect to {section}', status_code=1)
message=f'{section}: Failed to post-process - Unable to connect to {section}',
status_code=1,
)
result = response.json() result = response.json()
if response.status_code not in [ if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {response.status_code}') log.error(f'Server returned status {response.status_code}')
return ProcessResult( return ProcessResult(message=f'{section}: Failed to post-process - Server returned status {response.status_code}', status_code=1)
message=f'{section}: Failed to post-process - Server returned status {response.status_code}', if section == 'CouchPotato' and result['success']:
status_code=1,
)
elif section == 'CouchPotato' and result['success']:
log.debug(f'SUCCESS: Finished {method} scan for folder {dir_name}') log.debug(f'SUCCESS: Finished {method} scan for folder {dir_name}')
if method == 'manage': if method == 'manage':
return ProcessResult( return ProcessResult(message=f'{section}: Successfully post-processed {input_name}', status_code=0)
message=f'{section}: Successfully post-processed {input_name}',
status_code=0,
)
elif section == 'Radarr': elif section == 'Radarr':
try: try:
scan_id = int(result['id']) scan_id = int(result['id'])
@ -337,168 +230,87 @@ def process(
update_movie_status = result['tasks']['update_movie_status'] update_movie_status = result['tasks']['update_movie_status']
log.debug(f'Watcher3 updated status to {section}') log.debug(f'Watcher3 updated status to {section}')
if update_movie_status == 'Finished': if update_movie_status == 'Finished':
return ProcessResult( return ProcessResult(message=f'{section}: Successfully post-processed {input_name}', status_code=status)
message=f'{section}: Successfully post-processed {input_name}', return ProcessResult(message=f'{section}: Failed to post-process - changed status to {update_movie_status}', status_code=1)
status_code=status,
)
else:
return ProcessResult(
message=f'{section}: Failed to post-process - changed status to {update_movie_status}',
status_code=1,
)
else: else:
log.error(f'FAILED: {method} scan was unable to finish for folder {dir_name}. exiting!') log.error(f'FAILED: {method} scan was unable to finish for folder {dir_name}. exiting!')
return ProcessResult( return ProcessResult(message=f'{section}: Failed to post-process - Server did not return success', status_code=1)
message=f'{section}: Failed to post-process - Server did not return success',
status_code=1,
)
else: else:
nzb2media.FAILED = True nzb2media.FAILED = True
log.debug(f'FAILED DOWNLOAD DETECTED FOR {input_name}') log.debug(f'FAILED DOWNLOAD DETECTED FOR {input_name}')
if failure_link: if failure_link:
report_nzb(failure_link, client_agent) report_nzb(failure_link, client_agent)
if section == 'Radarr': if section == 'Radarr':
log.debug(f'SUCCESS: Sending failed download to {section} for CDH processing') log.debug(f'SUCCESS: Sending failed download to {section} for CDH processing')
return ProcessResult( return ProcessResult(
message=f'{section}: Sending failed download back to {section}', message=f'{section}: Sending failed download back to {section}',
status_code=1, status_code=1, # Return as failed to flag this in the downloader.
# Return as failed to flag this in the downloader.
) # Return failed flag, but log the event as successful. ) # Return failed flag, but log the event as successful.
elif section == 'Watcher3': if section == 'Watcher3':
log.debug(f'Sending failed download to {section} for CDH processing') log.debug(f'Sending failed download to {section} for CDH processing')
path = remote_dir(dir_name) if remote_path else dir_name path = remote_dir(dir_name) if remote_path else dir_name
if input_name and os.path.isfile( if input_name and os.path.isfile(os.path.join(dir_name, input_name)):
os.path.join(dir_name, input_name),
):
path = os.path.join(path, input_name) path = os.path.join(path, input_name)
payload = { payload = {'apikey': apikey, 'path': path, 'guid': download_id, 'mode': 'failed'}
'apikey': apikey, response = requests.post(base_url, data=payload, verify=False, timeout=(30, 1800))
'path': path,
'guid': download_id,
'mode': 'failed',
}
response = requests.post(
base_url, data=payload, verify=False, timeout=(30, 1800),
)
result = response.json() result = response.json()
log.debug(f'Watcher3 response: {result}') log.debug(f'Watcher3 response: {result}')
if result['status'] == 'finished': if result['status'] == 'finished':
return ProcessResult( return ProcessResult(
message=f'{section}: Sending failed download back to {section}', message=f'{section}: Sending failed download back to {section}',
status_code=1, status_code=1, # Return as failed to flag this in the downloader.
# Return as failed to flag this in the downloader.
) # Return failed flag, but log the event as successful. ) # Return failed flag, but log the event as successful.
if delete_failed and os.path.isdir(dir_name) and not os.path.dirname(dir_name) == dir_name:
if (
delete_failed
and os.path.isdir(dir_name)
and not os.path.dirname(dir_name) == dir_name
):
log.debug(f'Deleting failed files and folder {dir_name}') log.debug(f'Deleting failed files and folder {dir_name}')
remove_dir(dir_name) remove_dir(dir_name)
if not release_id and not media_id: if not release_id and not media_id:
log.error(f'Could not find a downloaded movie in the database matching {input_name}, exiting!') log.error(f'Could not find a downloaded movie in the database matching {input_name}, exiting!')
return ProcessResult( msg = f'{section}: Failed to post-process - Failed download not found in {section}'
message='{0}: Failed to post-process - Failed download not found in {0}'.format(section), return ProcessResult(message=msg, status_code=1)
status_code=1,
)
if release_id: if release_id:
log.debug(f'Setting failed release {input_name} to ignored ...') log.debug(f'Setting failed release {input_name} to ignored ...')
url = f'{base_url}release.ignore' url = f'{base_url}release.ignore'
params = {'id': release_id} params = {'id': release_id}
log.debug(f'Opening URL: {url} with PARAMS: {params}') log.debug(f'Opening URL: {url} with PARAMS: {params}')
try: try:
response = requests.get( response = requests.get(url, params=params, verify=False, timeout=(30, 120))
url, params=params, verify=False,
timeout=(30, 120),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL {url}') log.error(f'Unable to open URL {url}')
return ProcessResult( msg = f'{section}: Failed to post-process - Unable to connect to {section}'
message='{0}: Failed to post-process - Unable to connect to {0}'.format(section), return ProcessResult(message=msg, status_code=1)
status_code=1,
)
result = response.json() result = response.json()
if response.status_code not in [ if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {response.status_code}') log.error(f'Server returned status {response.status_code}')
return ProcessResult( return ProcessResult(status_code=1, message=f'{section}: Failed to post-process - Server returned status {response.status_code}')
status_code=1, if result['success']:
message=f'{section}: Failed to post-process - Server returned status {response.status_code}',
)
elif result['success']:
log.debug(f'SUCCESS: {input_name} has been set to ignored ...') log.debug(f'SUCCESS: {input_name} has been set to ignored ...')
else: else:
log.warning(f'FAILED: Unable to set {input_name} to ignored!') log.warning(f'FAILED: Unable to set {input_name} to ignored!')
return ProcessResult( return ProcessResult(message=f'{section}: Failed to post-process - Unable to set {input_name} to ignored', status_code=1)
message=f'{section}: Failed to post-process - Unable to set {input_name} to ignored',
status_code=1,
)
log.debug('Trying to snatch the next highest ranked release.') log.debug('Trying to snatch the next highest ranked release.')
url = f'{base_url}movie.searcher.try_next' url = f'{base_url}movie.searcher.try_next'
log.debug(f'Opening URL: {url}') log.debug(f'Opening URL: {url}')
try: try:
response = requests.get( response = requests.get(url, params={'media_id': media_id}, verify=False, timeout=(30, 600))
url,
params={'media_id': media_id},
verify=False,
timeout=(30, 600),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL {url}') log.error(f'Unable to open URL {url}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to connect to {section}')
f'{section}: Failed to post-process - Unable to connect to '
f'{section}',
)
result = response.json() result = response.json()
if response.status_code not in [ if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {response.status_code}') log.error(f'Server returned status {response.status_code}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Server returned status {response.status_code}')
f'{section}: Failed to post-process - Server returned status ' if result['success']:
f'{response.status_code}',
)
elif result['success']:
log.debug('SUCCESS: Snatched the next highest release ...') log.debug('SUCCESS: Snatched the next highest release ...')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully snatched next highest release')
f'{section}: Successfully snatched next highest release',
)
else:
log.debug('SUCCESS: Unable to find a new release to snatch now. CP will keep searching!') log.debug('SUCCESS: Unable to find a new release to snatch now. CP will keep searching!')
return ProcessResult.success( return ProcessResult.success(f'{section}: No new release found now. {section} will keep searching')
f'{section}: No new release found now. '
f'{section} will keep searching',
)
# Added a release that was not in the wanted list so confirm rename # Added a release that was not in the wanted list so confirm rename
# successful by finding this movie media.list. # successful by finding this movie media.list.
if not release: if not release:
# we don't want to filter new releases based on this. # we don't want to filter new releases based on this.
download_id = '' download_id = ''
if no_status_check: if no_status_check:
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully processed but no change in status confirmed')
f'{section}: Successfully processed but no change in status '
f'confirmed',
)
# we will now check to see if CPS has finished renaming before returning to TorrentToMedia and unpausing. # we will now check to see if CPS has finished renaming before returning to TorrentToMedia and unpausing.
timeout = time.time() + 60 * wait_for timeout = time.time() + 60 * wait_for
while time.time() < timeout: # only wait 2 (default) minutes, then return. while time.time() < timeout: # only wait 2 (default) minutes, then return.
@ -512,20 +324,13 @@ def process(
try: try:
release_id = list(release.keys())[0] release_id = list(release.keys())[0]
release_status_new = release[release_id]['status'] release_status_new = release[release_id]['status']
if ( if release_status_old is None: # we didn't have a release before, but now we do.
release_status_old is None
): # we didn't have a release before, but now we do.
title = release[release_id]['title'] title = release[release_id]['title']
log.debug(f'SUCCESS: Movie {title} has now been added to CouchPotato with release status of [{str(release_status_new).upper()}]') log.debug(f'SUCCESS: Movie {title} has now been added to CouchPotato with release status of [{str(release_status_new).upper()}]')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}',
)
if release_status_new != release_status_old: if release_status_new != release_status_old:
log.debug(f'SUCCESS: Release {release_id} has now been marked with a status of [{str(release_status_new).upper()}]') log.debug(f'SUCCESS: Release {release_id} has now been marked with a status of [{str(release_status_new).upper()}]')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}',
)
except Exception: except Exception:
pass pass
elif scan_id: elif scan_id:
@ -533,54 +338,31 @@ def process(
command_status = command_complete(url, params, headers, section) command_status = command_complete(url, params, headers, section)
if command_status: if command_status:
log.debug(f'The Scan command return status: {command_status}') log.debug(f'The Scan command return status: {command_status}')
if command_status in ['completed']: if command_status in {'completed'}:
log.debug('The Scan command has completed successfully. Renaming was successful.') log.debug('The Scan command has completed successfully. Renaming was successful.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', if command_status in {'failed'}:
)
elif command_status in ['failed']:
log.debug('The Scan command has failed. Renaming was not successful.') log.debug('The Scan command has failed. Renaming was not successful.')
# return ProcessResult( # return ProcessResult(message='{0}: Failed to post-process {1}'.format(SECTION, input_name), status_code=1)
# message='{0}: Failed to post-process {1}'.format(section, input_name),
# status_code=1,
# )
if not os.path.isdir(dir_name): if not os.path.isdir(dir_name):
log.debug(f'SUCCESS: Input Directory [{dir_name}] has been processed and removed') log.debug(f'SUCCESS: Input Directory [{dir_name}] has been processed and removed')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', if not list_media_files(dir_name, media=True, audio=False, meta=False, archives=True):
)
elif not list_media_files(
dir_name, media=True, audio=False, meta=False, archives=True,
):
log.debug(f'SUCCESS: Input Directory [{dir_name}] has no remaining media files. This has been fully processed.') log.debug(f'SUCCESS: Input Directory [{dir_name}] has no remaining media files. This has been fully processed.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}',
)
# pause and let CouchPotatoServer/Radarr catch its breath # pause and let CouchPotatoServer/Radarr catch its breath
time.sleep(10 * wait_for) time.sleep(10 * wait_for)
# The status hasn't changed. we have waited wait_for minutes which is more than enough. uTorrent can resume seeding now. # The status hasn't changed. we have waited wait_for minutes which is more than enough. uTorrent can resume seeding now.
if section == 'Radarr' and completed_download_handling( if section == 'Radarr' and completed_download_handling(url2, headers):
url2, headers, section=section,
):
log.debug(f'The Scan command did not return status completed, but complete Download Handling is enabled. Passing back to {section}.') log.debug(f'The Scan command did not return status completed, but complete Download Handling is enabled. Passing back to {section}.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Complete DownLoad Handling is enabled. Passing back to {section}')
f'{section}: Complete DownLoad Handling is enabled. Passing back '
f'to {section}',
)
log.warning(f'{input_name} does not appear to have changed status after {wait_for} minutes, Please check your logs.') log.warning(f'{input_name} does not appear to have changed status after {wait_for} minutes, Please check your logs.')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - No change in status')
f'{section}: Failed to post-process - No change in status',
)
def get_release(base_url, imdb_id=None, download_id=None, release_id=None): def get_release(base_url, imdb_id=None, download_id=None, release_id=None):
results = {} results = {}
params = {} params = {}
# determine cmd and params to send to CouchPotato to get our results # determine cmd and params to send to CouchPotato to get our results
section = 'movies' section = 'movies'
cmd = 'media.list' cmd = 'media.list'
@ -588,29 +370,24 @@ def get_release(base_url, imdb_id=None, download_id=None, release_id=None):
section = 'media' section = 'media'
cmd = 'media.get' cmd = 'media.get'
params['id'] = release_id or imdb_id params['id'] = release_id or imdb_id
if not (release_id or imdb_id or download_id): if not (release_id or imdb_id or download_id):
log.debug('No information available to filter CP results') log.debug('No information available to filter CP results')
return results return results
url = f'{base_url}{cmd}' url = f'{base_url}{cmd}'
log.debug(f'Opening URL: {url} with PARAMS: {params}') log.debug(f'Opening URL: {url} with PARAMS: {params}')
try: try:
r = requests.get(url, params=params, verify=False, timeout=(30, 60)) response = requests.get(url, params=params, verify=False, timeout=(30, 60))
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL {url}') log.error(f'Unable to open URL {url}')
return results return results
try: try:
result = r.json() result = response.json()
except ValueError: except ValueError:
# ValueError catches simplejson's JSONDecodeError and json's ValueError # ValueError catches simplejson's JSONDecodeError and json's ValueError
log.error('CouchPotato returned the following non-json data') log.error('CouchPotato returned the following non-json data')
for line in r.iter_lines(): for line in response.iter_lines():
log.error(line) log.error(line)
return results return results
if not result['success']: if not result['success']:
if 'error' in result: if 'error' in result:
log.error(result['error']) log.error(result['error'])
@ -618,18 +395,15 @@ def get_release(base_url, imdb_id=None, download_id=None, release_id=None):
id_param = params['id'] id_param = params['id']
log.error(f'no media found for id {id_param}') log.error(f'no media found for id {id_param}')
return results return results
# Gather release info and return it back, no need to narrow results # Gather release info and return it back, no need to narrow results
if release_id: if release_id:
try: try:
cur_id = result[section]['_id'] key = result[section]['_id']
results[cur_id] = result[section] results[key] = result[section]
return results return results
except Exception: except Exception:
pass pass
# Gather release info and proceed with trying to narrow results to one release choice # Gather release info and proceed with trying to narrow results to one release choice
movies = result[section] movies = result[section]
if not isinstance(movies, list): if not isinstance(movies, list):
movies = [movies] movies = [movies]
@ -644,44 +418,34 @@ def get_release(base_url, imdb_id=None, download_id=None, release_id=None):
if release['status'] not in ['snatched', 'downloaded', 'done']: if release['status'] not in ['snatched', 'downloaded', 'done']:
continue continue
if download_id: if download_id:
if ( if download_id.lower() != release['download_info']['id'].lower():
download_id.lower()
!= release['download_info']['id'].lower()
):
continue continue
key = release['_id']
cur_id = release['_id'] results[key] = release
results[cur_id] = release results[key]['title'] = movie['title']
results[cur_id]['title'] = movie['title']
except Exception: except Exception:
continue continue
# Narrow results by removing old releases by comparing their last_edit field # Narrow results by removing old releases by comparing their last_edit field
if len(results) > 1: if len(results) > 1:
rem_id = set() rem_id = set()
for id1, x1 in results.items(): for key, val1 in results.items():
for x2 in results.values(): for val2 in results.values():
try: try:
if x2['last_edit'] > x1['last_edit']: if val2['last_edit'] > val1['last_edit']:
rem_id.add(id1) rem_id.add(key)
except Exception: except Exception:
continue continue
for id in rem_id: for ea_id in rem_id:
results.pop(id) results.pop(ea_id)
# Search downloads on clients for a match to try and narrow our results down to 1 # Search downloads on clients for a match to try and narrow our results down to 1
if len(results) > 1: if len(results) > 1:
rem_id = set() rem_id = set()
for cur_id, x in results.items(): for key, val1 in results.items():
try: try:
if not find_download( if not find_download(str(val1['download_info']['downloader']).lower(), val1['download_info']['id']):
str(x['download_info']['downloader']).lower(), rem_id.add(key)
x['download_info']['id'],
):
rem_id.add(cur_id)
except Exception: except Exception:
continue continue
for id in rem_id: for ea_id in rem_id:
results.pop(id) results.pop(ea_id)
return results return results

View file

@ -8,11 +8,12 @@ import time
import requests import requests
import nzb2media import nzb2media
import nzb2media.utils.common
from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import ProcessResult
from nzb2media.auto_process.common import command_complete from nzb2media.auto_process.common import command_complete
from nzb2media.scene_exceptions import process_all_exceptions from nzb2media.scene_exceptions import process_all_exceptions
from nzb2media.utils.common import flatten
from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.encoding import convert_to_ascii
from nzb2media.utils.files import extract_files
from nzb2media.utils.files import list_media_files from nzb2media.utils.files import list_media_files
from nzb2media.utils.network import server_responding from nzb2media.utils.network import server_responding
from nzb2media.utils.paths import remote_dir from nzb2media.utils.paths import remote_dir
@ -22,119 +23,65 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def process( def process(*, section: str, dir_name: str, input_name: str = '', status: int = 0, input_category: str = '', **kwargs) -> ProcessResult:
*, log.debug(f'Unused kwargs: {kwargs}')
section: str,
dir_name: str,
input_name: str = '',
status: int = 0,
client_agent: str = 'manual',
download_id: str = '',
input_category: str = '',
failure_link: str = '',
) -> ProcessResult:
# Get configuration # Get configuration
if nzb2media.CFG is None: if nzb2media.CFG is None:
raise RuntimeError('Configuration not loaded.') raise RuntimeError('Configuration not loaded.')
cfg = nzb2media.CFG[section][input_category] cfg = nzb2media.CFG[section][input_category]
# Base URL # Base URL
ssl = int(cfg.get('ssl', 0)) ssl = int(cfg.get('ssl', 0))
scheme = 'https' if ssl else 'http' scheme = 'https' if ssl else 'http'
host = cfg['host'] host = cfg['host']
port = cfg['port'] port = cfg['port']
web_root = cfg.get('web_root', '') web_root = cfg.get('web_root', '')
# Authentication # Authentication
apikey = cfg.get('apikey', '') apikey = cfg.get('apikey', '')
# Params # Params
delete_failed = int(cfg.get('delete_failed', 0)) delete_failed = int(cfg.get('delete_failed', 0))
remote_path = int(cfg.get('remote_path', 0)) remote_path = int(cfg.get('remote_path', 0))
wait_for = int(cfg.get('wait_for', 2)) wait_for = int(cfg.get('wait_for', 2))
# Misc # Misc
if status > 0 and nzb2media.NOEXTRACTFAILED: if status > 0 and nzb2media.NOEXTRACTFAILED:
extract = 0 extract = 0
else: else:
extract = int(cfg.get('extract', 0)) extract = int(cfg.get('extract', 0))
# Begin processing # Begin processing
route = f'{web_root}/api/v1' if section == 'Lidarr' else f'{web_root}/api' route = f'{web_root}/api/v1' if section == 'Lidarr' else f'{web_root}/api'
url = nzb2media.utils.common.create_url(scheme, host, port, route) url = nzb2media.utils.common.create_url(scheme, host, port, route)
if not server_responding(url): if not server_responding(url):
log.error('Server did not respond. Exiting') log.error('Server did not respond. Exiting')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - {section} did not respond.')
f'{section}: Failed to post-process - {section} did not respond.', if not os.path.isdir(dir_name) and os.path.isfile(dir_name): # If the input directory is a file, assume single file download and split dir/name.
)
if not os.path.isdir(dir_name) and os.path.isfile(
dir_name,
): # If the input directory is a file, assume single file download and split dir/name.
dir_name = os.path.split(os.path.normpath(dir_name))[0] dir_name = os.path.split(os.path.normpath(dir_name))[0]
specific_path = os.path.join(dir_name, str(input_name)) specific_path = os.path.join(dir_name, str(input_name))
clean_name = os.path.splitext(specific_path) clean_name = os.path.splitext(specific_path)
if clean_name[1] == '.nzb': if clean_name[1] == '.nzb':
specific_path = clean_name[0] specific_path = clean_name[0]
if os.path.isdir(specific_path): if os.path.isdir(specific_path):
dir_name = specific_path dir_name = specific_path
process_all_exceptions(input_name, dir_name) process_all_exceptions(input_name, dir_name)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
if not list_media_files(dir_name, media=False, audio=True, meta=False, archives=False) and list_media_files(dir_name, media=False, audio=False, meta=False, archives=True) and extract:
if (
not list_media_files(
dir_name, media=False, audio=True, meta=False, archives=False,
)
and list_media_files(
dir_name, media=False, audio=False, meta=False, archives=True,
)
and extract
):
log.debug(f'Checking for archives to extract in directory: {dir_name}') log.debug(f'Checking for archives to extract in directory: {dir_name}')
nzb2media.extract_files(dir_name) extract_files(dir_name)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
# if listMediaFiles(dir_name, media=False, audio=True, meta=False, archives=False) and status: # if listMediaFiles(dir_name, media=False, audio=True, meta=False, archives=False) and status:
# logger.info('Status shown as failed from Downloader, but valid video files found. Setting as successful.', section) # logger.info('Status shown as failed from Downloader, but valid video files found. Setting as successful.', SECTION)
# status = 0 # status = 0
if status == 0 and section == 'HeadPhones': if status == 0 and section == 'HeadPhones':
params = {'apikey': apikey, 'cmd': 'forceProcess', 'dir': remote_dir(dir_name) if remote_path else dir_name}
params = { res = force_process(params, url, apikey, input_name, dir_name, section, wait_for)
'apikey': apikey, if res.status_code in {0, 1}:
'cmd': 'forceProcess',
'dir': remote_dir(dir_name) if remote_path else dir_name,
}
res = force_process(
params, url, apikey, input_name, dir_name, section, wait_for,
)
if res.status_code in [0, 1]:
return res return res
params = {'apikey': apikey, 'cmd': 'forceProcess', 'dir': os.path.split(remote_dir(dir_name))[0] if remote_path else os.path.split(dir_name)[0]}
params = { res = force_process(params, url, apikey, input_name, dir_name, section, wait_for)
'apikey': apikey, if res.status_code in {0, 1}:
'cmd': 'forceProcess',
'dir': os.path.split(remote_dir(dir_name))[0]
if remote_path
else os.path.split(dir_name)[0],
}
res = force_process(
params, url, apikey, input_name, dir_name, section, wait_for,
)
if res.status_code in [0, 1]:
return res return res
# The status hasn't changed. uTorrent can resume seeding now. # The status hasn't changed. uTorrent can resume seeding now.
log.warning(f'The music album does not appear to have changed status after {wait_for} minutes. Please check your Logs') log.warning(f'The music album does not appear to have changed status after {wait_for} minutes. Please check your Logs')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - No change in wanted status')
f'{section}: Failed to post-process - No change in wanted status', if status == 0 and section == 'Lidarr':
)
elif status == 0 and section == 'Lidarr':
route = f'{web_root}/api/v1/command' route = f'{web_root}/api/v1/command'
url = nzb2media.utils.common.create_url(scheme, host, port, route) url = nzb2media.utils.common.create_url(scheme, host, port, route)
headers = {'X-Api-Key': apikey} headers = {'X-Api-Key': apikey}
@ -146,174 +93,102 @@ def process(
data = {'name': 'Rename', 'path': dir_name} data = {'name': 'Rename', 'path': dir_name}
try: try:
log.debug(f'Opening URL: {url} with data: {data}') log.debug(f'Opening URL: {url} with data: {data}')
r = requests.post( response = requests.post(url, data=json.dumps(data), headers=headers, stream=True, verify=False, timeout=(30, 1800))
url,
data=json.dumps(data),
headers=headers,
stream=True,
verify=False,
timeout=(30, 1800),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL: {url}') log.error(f'Unable to open URL: {url}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to connect to {section}')
f'{section}: Failed to post-process - Unable to connect to '
f'{section}',
)
try: try:
res = r.json() res = response.json()
scan_id = int(res['id']) scan_id = int(res['id'])
log.debug(f'Scan started with id: {scan_id}') log.debug(f'Scan started with id: {scan_id}')
except Exception as error: except Exception as error:
log.warning(f'No scan id was returned due to: {error}') log.warning(f'No scan id was returned due to: {error}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to start scan')
f'{section}: Failed to post-process - Unable to start scan', num = 0
)
n = 0
params = {} params = {}
url = f'{url}/{scan_id}' url = f'{url}/{scan_id}'
while n < 6: # set up wait_for minutes to see if command completes.. while num < 6: # set up wait_for minutes to see if command completes..
time.sleep(10 * wait_for) time.sleep(10 * wait_for)
command_status = command_complete(url, params, headers, section) command_status = command_complete(url, params, headers, section)
if command_status and command_status in ['completed', 'failed']: if command_status and command_status in {'completed', 'failed'}:
break break
n += 1 num += 1
if command_status: if command_status:
log.debug(f'The Scan command return status: {command_status}') log.debug(f'The Scan command return status: {command_status}')
if not os.path.exists(dir_name): if not os.path.exists(dir_name):
log.debug(f'The directory {dir_name} has been removed. Renaming was successful.') log.debug(f'The directory {dir_name} has been removed. Renaming was successful.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', if command_status and command_status in {'completed'}:
)
elif command_status and command_status in ['completed']:
log.debug('The Scan command has completed successfully. Renaming was successful.') log.debug('The Scan command has completed successfully. Renaming was successful.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', if command_status and command_status in {'failed'}:
)
elif command_status and command_status in ['failed']:
log.debug('The Scan command has failed. Renaming was not successful.') log.debug('The Scan command has failed. Renaming was not successful.')
# return ProcessResult.failure( # return ProcessResult.failure(f'{SECTION}: Failed to post-process {input_name}')
# f'{section}: Failed to post-process {input_name}'
# )
else: else:
log.debug(f'The Scan command did not return status completed. Passing back to {section} to attempt complete download handling.') log.debug(f'The Scan command did not return status completed. Passing back to {section} to attempt complete download handling.')
return ProcessResult( return ProcessResult(message=f'{section}: Passing back to {section} to attempt Complete Download Handling', status_code=status)
message=f'{section}: Passing back to {section} to attempt '
f'Complete Download Handling',
status_code=status,
)
else: else:
if section == 'Lidarr': if section == 'Lidarr':
log.debug(f'FAILED: The download failed. Sending failed download to {section} for CDH processing') log.debug(f'FAILED: The download failed. Sending failed download to {section} for CDH processing')
# Return as failed to flag this in the downloader. # Return as failed to flag this in the downloader.
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Download Failed. Sending back to {section}')
f'{section}: Download Failed. Sending back to {section}',
)
else:
log.warning('FAILED DOWNLOAD DETECTED') log.warning('FAILED DOWNLOAD DETECTED')
if ( if delete_failed and os.path.isdir(dir_name) and not os.path.dirname(dir_name) == dir_name:
delete_failed log.debug(f'Deleting failed files and folder {dir_name}')
and os.path.isdir(dir_name)
and not os.path.dirname(dir_name) == dir_name
):
log.postprocess(f'Deleting failed files and folder {dir_name}')
remove_dir(dir_name) remove_dir(dir_name)
# Return as failed to flag this in the downloader. # Return as failed to flag this in the downloader.
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process. {section} does not support failed downloads')
f'{section}: Failed to post-process. {section} does not '
f'support failed downloads',
)
return ProcessResult.failure() return ProcessResult.failure()
def get_status(url, apikey, dir_name): def get_status(url, apikey, dir_name):
log.debug(f'Attempting to get current status for release:{os.path.basename(dir_name)}') log.debug(f'Attempting to get current status for release:{os.path.basename(dir_name)}')
params = {'apikey': apikey, 'cmd': 'getHistory'}
params = {
'apikey': apikey,
'cmd': 'getHistory',
}
log.debug(f'Opening URL: {url} with PARAMS: {params}') log.debug(f'Opening URL: {url} with PARAMS: {params}')
try: try:
r = requests.get(url, params=params, verify=False, timeout=(30, 120)) response = requests.get(url, params=params, verify=False, timeout=(30, 120))
except requests.RequestException: except requests.RequestException:
log.error('Unable to open URL') log.error('Unable to open URL')
return None return None
try: try:
result = r.json() result = response.json()
except ValueError: except ValueError:
# ValueError catches simplejson's JSONDecodeError and json's ValueError # ValueError catches simplejson's JSONDecodeError and json's ValueError
return None return None
for album in result: for album in result:
if os.path.basename(dir_name) == album['FolderName']: if os.path.basename(dir_name) == album['FolderName']:
return album['Status'].lower() return album['Status'].lower()
def force_process( def force_process(params, url, apikey, input_name, dir_name, section, wait_for):
params, url, apikey, input_name, dir_name, section, wait_for,
):
release_status = get_status(url, apikey, dir_name) release_status = get_status(url, apikey, dir_name)
if not release_status: if not release_status:
log.error(f'Could not find a status for {input_name}, is it in the wanted list ?') log.error(f'Could not find a status for {input_name}, is it in the wanted list ?')
log.debug(f'Opening URL: {url} with PARAMS: {params}') log.debug(f'Opening URL: {url} with PARAMS: {params}')
try: try:
r = requests.get(url, params=params, verify=False, timeout=(30, 300)) response = requests.get(url, params=params, verify=False, timeout=(30, 300))
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL {url}') log.error(f'Unable to open URL {url}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to connect to {section}')
f'{section}: Failed to post-process - Unable to connect to ' log.debug(f'Result: {response.text}')
f'{section}', if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
) log.error(f'Server returned status {response.status_code}')
return ProcessResult.failure(f'{section}: Failed to post-process - Server returned status {response.status_code}')
log.debug(f'Result: {r.text}') if response.text == 'OK':
if r.status_code not in [
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {r.status_code}')
return ProcessResult.failure(
f'{section}: Failed to post-process - Server returned status {r.status_code}',
)
elif r.text == 'OK':
log.debug(f'SUCCESS: Post-Processing started for {input_name} in folder {dir_name} ...') log.debug(f'SUCCESS: Post-Processing started for {input_name} in folder {dir_name} ...')
else: else:
log.error(f'FAILED: Post-Processing has NOT started for {input_name} in folder {dir_name}. exiting!') log.error(f'FAILED: Post-Processing has NOT started for {input_name} in folder {dir_name}. exiting!')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Returned log from {section} was not as expected.')
f'{section}: Failed to post-process - Returned log from {section} '
f'was not as expected.',
)
# we will now wait for this album to be processed before returning to TorrentToMedia and unpausing. # we will now wait for this album to be processed before returning to TorrentToMedia and unpausing.
timeout = time.time() + 60 * wait_for timeout = time.time() + 60 * wait_for
while time.time() < timeout: while time.time() < timeout:
current_status = get_status(url, apikey, dir_name) current_status = get_status(url, apikey, dir_name)
if ( if current_status is not None and current_status != release_status: # Something has changed. CPS must have processed this movie.
current_status is not None and current_status != release_status
): # Something has changed. CPS must have processed this movie.
log.debug(f'SUCCESS: This release is now marked as status [{current_status}]') log.debug(f'SUCCESS: This release is now marked as status [{current_status}]')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}',
)
if not os.path.isdir(dir_name): if not os.path.isdir(dir_name):
log.debug(f'SUCCESS: The input directory {dir_name} has been removed Processing must have finished.') log.debug(f'SUCCESS: The input directory {dir_name} has been removed Processing must have finished.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}',
)
time.sleep(10 * wait_for) time.sleep(10 * wait_for)
# The status hasn't changed. # The status hasn't changed.
return ProcessResult( return ProcessResult(message='no change', status_code=2)
message='no change',
status_code=2,
)

View file

@ -12,6 +12,7 @@ from oauthlib.oauth2 import LegacyApplicationClient
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
import nzb2media import nzb2media
import nzb2media.utils.common
from nzb2media import transcoder from nzb2media import transcoder
from nzb2media.auto_process.common import ProcessResult from nzb2media.auto_process.common import ProcessResult
from nzb2media.auto_process.common import command_complete from nzb2media.auto_process.common import command_complete
@ -22,9 +23,11 @@ from nzb2media.plugins.subtitles import rename_subs
from nzb2media.scene_exceptions import process_all_exceptions from nzb2media.scene_exceptions import process_all_exceptions
from nzb2media.utils.common import flatten from nzb2media.utils.common import flatten
from nzb2media.utils.encoding import convert_to_ascii from nzb2media.utils.encoding import convert_to_ascii
from nzb2media.utils.files import extract_files
from nzb2media.utils.files import list_media_files from nzb2media.utils.files import list_media_files
from nzb2media.utils.network import server_responding from nzb2media.utils.network import server_responding
from nzb2media.utils.nzb import report_nzb from nzb2media.utils.nzb import report_nzb
from nzb2media.utils.paths import rchmod
from nzb2media.utils.paths import remote_dir from nzb2media.utils.paths import remote_dir
from nzb2media.utils.paths import remove_dir from nzb2media.utils.paths import remove_dir
@ -32,29 +35,17 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def process( def process(*, section: str, dir_name: str, input_name: str = '', status: int = 0, client_agent: str = 'manual', download_id: str = '', input_category: str = '', failure_link: str = '') -> ProcessResult:
*,
section: str,
dir_name: str,
input_name: str = '',
status: int = 0,
client_agent: str = 'manual',
download_id: str = '',
input_category: str = '',
failure_link: str = '',
) -> ProcessResult:
# Get configuration # Get configuration
if nzb2media.CFG is None: if nzb2media.CFG is None:
raise RuntimeError('Configuration not loaded.') raise RuntimeError('Configuration not loaded.')
cfg = nzb2media.CFG[section][input_category] cfg = nzb2media.CFG[section][input_category]
# Base URL # Base URL
ssl = int(cfg.get('ssl', 0)) ssl = int(cfg.get('ssl', 0))
scheme = 'https' if ssl else 'http' scheme = 'https' if ssl else 'http'
host = cfg['host'] host = cfg['host']
port = cfg['port'] port = cfg['port']
web_root = cfg.get('web_root', '') web_root = cfg.get('web_root', '')
# Authentication # Authentication
apikey = cfg.get('apikey', '') apikey = cfg.get('apikey', '')
username = cfg.get('username', '') username = cfg.get('username', '')
@ -62,12 +53,10 @@ def process(
api_version = int(cfg.get('api_version', 2)) api_version = int(cfg.get('api_version', 2))
sso_username = cfg.get('sso_username', '') sso_username = cfg.get('sso_username', '')
sso_password = cfg.get('sso_password', '') sso_password = cfg.get('sso_password', '')
# Params # Params
delete_failed = int(cfg.get('delete_failed', 0)) delete_failed = int(cfg.get('delete_failed', 0))
remote_path = int(cfg.get('remote_path', 0)) remote_path = int(cfg.get('remote_path', 0))
wait_for = int(cfg.get('wait_for', 2)) wait_for = int(cfg.get('wait_for', 2))
# Misc # Misc
if status > 0 and nzb2media.NOEXTRACTFAILED: if status > 0 and nzb2media.NOEXTRACTFAILED:
extract = 0 extract = 0
@ -80,13 +69,10 @@ def process(
force = int(cfg.get('force', 0)) force = int(cfg.get('force', 0))
delete_on = int(cfg.get('delete_on', 0)) delete_on = int(cfg.get('delete_on', 0))
ignore_subs = int(cfg.get('ignore_subs', 0)) ignore_subs = int(cfg.get('ignore_subs', 0))
# Begin processing # Begin processing
# Refactor into an OO structure. # Refactor into an OO structure.
# For now let's do botch the OO and the serialized code, until everything has been migrated. # For now let's do botch the OO and the serialized code, until everything has been migrated.
init_sickbeard = InitSickBeard(cfg, section, input_category) init_sickbeard = InitSickBeard(cfg, section, input_category)
url = nzb2media.utils.common.create_url(scheme, host, port, web_root) url = nzb2media.utils.common.create_url(scheme, host, port, web_root)
if server_responding(url): if server_responding(url):
# auto-detect correct fork # auto-detect correct fork
@ -98,27 +84,17 @@ def process(
fork, fork_params = 'None', {} fork, fork_params = 'None', {}
else: else:
log.error('Server did not respond. Exiting') log.error('Server did not respond. Exiting')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - {section} did not respond.')
f'{section}: Failed to post-process - {section} did not respond.', if client_agent == nzb2media.TORRENT_CLIENT_AGENT and nzb2media.USE_LINK == 'move-sym':
)
if (
client_agent == nzb2media.TORRENT_CLIENT_AGENT
and nzb2media.USE_LINK == 'move-sym'
):
process_method = 'symlink' process_method = 'symlink'
if not os.path.isdir(dir_name) and os.path.isfile( if not os.path.isdir(dir_name) and os.path.isfile(dir_name): # If the input directory is a file, assume single file download and split dir/name.
dir_name,
): # If the input directory is a file, assume single file download and split dir/name.
dir_name = os.path.split(os.path.normpath(dir_name))[0] dir_name = os.path.split(os.path.normpath(dir_name))[0]
specific_path = os.path.join(dir_name, str(input_name)) specific_path = os.path.join(dir_name, str(input_name))
clean_name = os.path.splitext(specific_path) clean_name = os.path.splitext(specific_path)
if clean_name[1] == '.nzb': if clean_name[1] == '.nzb':
specific_path = clean_name[0] specific_path = clean_name[0]
if os.path.isdir(specific_path): if os.path.isdir(specific_path):
dir_name = specific_path dir_name = specific_path
# Attempt to create the directory if it doesn't exist and ignore any # Attempt to create the directory if it doesn't exist and ignore any
# error stating that it already exists. This fixes a bug where SickRage # error stating that it already exists. This fixes a bug where SickRage
# won't process the directory because it doesn't exist. # won't process the directory because it doesn't exist.
@ -129,51 +105,27 @@ def process(
# Re-raise the error if it wasn't about the directory not existing # Re-raise the error if it wasn't about the directory not existing
if error.errno != errno.EEXIST: if error.errno != errno.EEXIST:
raise raise
if 'process_method' not in fork_params or (client_agent in {'nzbget', 'sabnzbd'} and nzb_extraction_by != 'Destination'):
if 'process_method' not in fork_params or (
client_agent in ['nzbget', 'sabnzbd']
and nzb_extraction_by != 'Destination'
):
if input_name: if input_name:
process_all_exceptions(input_name, dir_name) process_all_exceptions(input_name, dir_name)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
# Now check if tv files exist in destination. # Now check if tv files exist in destination.
if not list_media_files( if not list_media_files(dir_name, media=True, audio=False, meta=False, archives=False):
dir_name, media=True, audio=False, meta=False, archives=False, if list_media_files(dir_name, media=False, audio=False, meta=False, archives=True) and extract:
):
if (
list_media_files(
dir_name,
media=False,
audio=False,
meta=False,
archives=True,
)
and extract
):
log.debug(f'Checking for archives to extract in directory: {dir_name}') log.debug(f'Checking for archives to extract in directory: {dir_name}')
nzb2media.extract_files(dir_name) extract_files(dir_name)
input_name, dir_name = convert_to_ascii(input_name, dir_name) input_name, dir_name = convert_to_ascii(input_name, dir_name)
if list_media_files(dir_name, media=True, audio=False, meta=False, archives=False): # Check that a video exists. if not, assume failed.
if list_media_files(
dir_name, media=True, audio=False, meta=False, archives=False,
): # Check that a video exists. if not, assume failed.
flatten(dir_name) flatten(dir_name)
# Check video files for corruption # Check video files for corruption
good_files = 0 good_files = 0
valid_files = 0 valid_files = 0
num_files = 0 num_files = 0
for video in list_media_files( for video in list_media_files(dir_name, media=True, audio=False, meta=False, archives=False):
dir_name, media=True, audio=False, meta=False, archives=False,
):
num_files += 1 num_files += 1
if transcoder.is_video_good(video, status): if transcoder.is_video_good(video, status):
good_files += 1 good_files += 1
if not nzb2media.REQUIRE_LAN or transcoder.is_video_good( if not nzb2media.REQUIRE_LAN or transcoder.is_video_good(video, status, require_lan=nzb2media.REQUIRE_LAN):
video, status, require_lan=nzb2media.REQUIRE_LAN,
):
valid_files += 1 valid_files += 1
import_subs(video) import_subs(video)
rename_subs(dir_name) rename_subs(dir_name)
@ -184,18 +136,13 @@ def process(
if valid_files < num_files and status == 0: if valid_files < num_files and status == 0:
log.info('Found corrupt videos. Setting status Failed') log.info('Found corrupt videos. Setting status Failed')
status = 1 status = 1
if ( if 'NZBOP_VERSION' in os.environ and os.environ['NZBOP_VERSION'][0:5] >= '14.0':
'NZBOP_VERSION' in os.environ
and os.environ['NZBOP_VERSION'][0:5] >= '14.0'
):
print('[NZB] MARK=BAD') print('[NZB] MARK=BAD')
if good_files == num_files: if good_files == num_files:
log.debug(f'Video marked as failed due to missing required language: {nzb2media.REQUIRE_LAN}') log.debug(f'Video marked as failed due to missing required language: {nzb2media.REQUIRE_LAN}')
else: else:
log.debug('Video marked as failed due to missing playable audio or video') log.debug('Video marked as failed due to missing playable audio or video')
if ( if good_files < num_files and failure_link: # only report corrupt files
good_files < num_files and failure_link
): # only report corrupt files
failure_link += '&corrupt=true' failure_link += '&corrupt=true'
elif client_agent == 'manual': elif client_agent == 'manual':
log.warning(f'No media files found in directory {dir_name} to manually process.') log.warning(f'No media files found in directory {dir_name} to manually process.')
@ -211,36 +158,23 @@ def process(
else: else:
log.warning(f'No media files found in directory {dir_name}. Processing this as a failed download') log.warning(f'No media files found in directory {dir_name}. Processing this as a failed download')
status = 1 status = 1
if ( if 'NZBOP_VERSION' in os.environ and os.environ['NZBOP_VERSION'][0:5] >= '14.0':
'NZBOP_VERSION' in os.environ
and os.environ['NZBOP_VERSION'][0:5] >= '14.0'
):
print('[NZB] MARK=BAD') print('[NZB] MARK=BAD')
if status == 0 and nzb2media.TRANSCODE == 1: # only transcode successful downloads
if (
status == 0 and nzb2media.TRANSCODE == 1
): # only transcode successful downloads
result, new_dir_name = transcoder.transcode_directory(dir_name) result, new_dir_name = transcoder.transcode_directory(dir_name)
if result == 0: if result == 0:
log.debug(f'SUCCESS: Transcoding succeeded for files in {dir_name}') log.debug(f'SUCCESS: Transcoding succeeded for files in {dir_name}')
dir_name = new_dir_name dir_name = new_dir_name
log.debug(f'Config setting \'chmodDirectory\' currently set to {oct(chmod_directory)}') log.debug(f'Config setting \'chmodDirectory\' currently set to {oct(chmod_directory)}')
if chmod_directory: if chmod_directory:
log.info(f'Attempting to set the octal permission of \'{oct(chmod_directory)}\' on directory \'{dir_name}\'') log.info(f'Attempting to set the octal permission of \'{oct(chmod_directory)}\' on directory \'{dir_name}\'')
nzb2media.rchmod(dir_name, chmod_directory) rchmod(dir_name, chmod_directory)
else: else:
log.error(f'FAILED: Transcoding failed for files in {dir_name}') log.error(f'FAILED: Transcoding failed for files in {dir_name}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Transcoding failed')
f'{section}: Failed to post-process - Transcoding failed',
)
# Part of the refactor # Part of the refactor
if init_sickbeard.fork_obj: if init_sickbeard.fork_obj:
init_sickbeard.fork_obj.initialize( init_sickbeard.fork_obj.initialize(dir_name, input_name, status, client_agent='manual')
dir_name, input_name, status, client_agent='manual',
)
# configure SB params to pass # configure SB params to pass
# We don't want to remove params, for the Forks that have been refactored. # We don't want to remove params, for the Forks that have been refactored.
# As we don't want to duplicate this part of the code. # As we don't want to duplicate this part of the code.
@ -249,77 +183,57 @@ def process(
fork_params['proc_type'] = 'manual' fork_params['proc_type'] = 'manual'
if input_name is not None: if input_name is not None:
fork_params['nzbName'] = input_name fork_params['nzbName'] = input_name
for param in copy.copy(fork_params): for param in copy.copy(fork_params):
if param == 'failed': if param == 'failed':
if status > 1: status = min(status, 1)
status = 1
fork_params[param] = status fork_params[param] = status
if 'proc_type' in fork_params: if 'proc_type' in fork_params:
del fork_params['proc_type'] del fork_params['proc_type']
if 'type' in fork_params: if 'type' in fork_params:
del fork_params['type'] del fork_params['type']
if param == 'return_data': if param == 'return_data':
fork_params[param] = 0 fork_params[param] = 0
if 'quiet' in fork_params: if 'quiet' in fork_params:
del fork_params['quiet'] del fork_params['quiet']
if param == 'type': if param == 'type':
if ( if 'type' in fork_params: # only set if we haven't already deleted for 'failed' above.
'type' in fork_params
): # only set if we haven't already deleted for 'failed' above.
fork_params[param] = 'manual' fork_params[param] = 'manual'
if 'proc_type' in fork_params: if 'proc_type' in fork_params:
del fork_params['proc_type'] del fork_params['proc_type']
if param in {'dir_name', 'dir', 'proc_dir', 'process_directory', 'path'}:
if param in [
'dir_name',
'dir',
'proc_dir',
'process_directory',
'path',
]:
fork_params[param] = dir_name fork_params[param] = dir_name
if remote_path: if remote_path:
fork_params[param] = remote_dir(dir_name) fork_params[param] = remote_dir(dir_name)
if param == 'process_method': if param == 'process_method':
if process_method: if process_method:
fork_params[param] = process_method fork_params[param] = process_method
else: else:
del fork_params[param] del fork_params[param]
if param in {'force', 'force_replace'}:
if param in ['force', 'force_replace']:
if force: if force:
fork_params[param] = force fork_params[param] = force
else: else:
del fork_params[param] del fork_params[param]
if param in {'delete_on', 'delete'}:
if param in ['delete_on', 'delete']:
if delete_on: if delete_on:
fork_params[param] = delete_on fork_params[param] = delete_on
else: else:
del fork_params[param] del fork_params[param]
if param == 'ignore_subs': if param == 'ignore_subs':
if ignore_subs: if ignore_subs:
fork_params[param] = ignore_subs fork_params[param] = ignore_subs
else: else:
del fork_params[param] del fork_params[param]
if param == 'force_next': if param == 'force_next':
fork_params[param] = 1 fork_params[param] = 1
# delete any unused params so we don't pass them to SB by mistake # delete any unused params so we don't pass them to SB by mistake
[fork_params.pop(k) for k, v in list(fork_params.items()) if v is None] for key, val in list(fork_params.items()):
if val is None:
del fork_params[key]
if status == 0: if status == 0:
if section == 'NzbDrone' and not apikey: if section == 'NzbDrone' and not apikey:
log.info('No Sonarr apikey entered. Processing completed.') log.info('No Sonarr apikey entered. Processing completed.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}',
)
log.debug('SUCCESS: The download succeeded, sending a post-process request') log.debug('SUCCESS: The download succeeded, sending a post-process request')
else: else:
nzb2media.FAILED = True nzb2media.FAILED = True
@ -330,23 +244,14 @@ def process(
elif section == 'NzbDrone': elif section == 'NzbDrone':
log.debug(f'FAILED: The download failed. Sending failed download to {fork} for CDH processing') log.debug(f'FAILED: The download failed. Sending failed download to {fork} for CDH processing')
# Return as failed to flag this in the downloader. # Return as failed to flag this in the downloader.
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Download Failed. Sending back to {section}')
f'{section}: Download Failed. Sending back to {section}',
)
else: else:
log.debug(f'FAILED: The download failed. {fork} branch does not handle failed downloads. Nothing to process') log.debug(f'FAILED: The download failed. {fork} branch does not handle failed downloads. Nothing to process')
if ( if delete_failed and os.path.isdir(dir_name) and not os.path.dirname(dir_name) == dir_name:
delete_failed
and os.path.isdir(dir_name)
and not os.path.dirname(dir_name) == dir_name
):
log.debug(f'Deleting failed files and folder {dir_name}') log.debug(f'Deleting failed files and folder {dir_name}')
remove_dir(dir_name) remove_dir(dir_name)
# Return as failed to flag this in the downloader. # Return as failed to flag this in the downloader.
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process. {section} does not support failed downloads')
f'{section}: Failed to post-process. {section} does not support failed downloads',
)
route = '' route = ''
if section == 'SickBeard': if section == 'SickBeard':
if apikey: if apikey:
@ -370,20 +275,10 @@ def process(
# params = {'sortKey': 'series.title', 'page': 1, 'pageSize': 1, 'sortDir': 'asc'} # params = {'sortKey': 'series.title', 'page': 1, 'pageSize': 1, 'sortDir': 'asc'}
if remote_path: if remote_path:
log.debug(f'remote_path: {remote_dir(dir_name)}') log.debug(f'remote_path: {remote_dir(dir_name)}')
data = { data = {'name': 'DownloadedEpisodesScan', 'path': remote_dir(dir_name), 'downloadClientId': download_id, 'importMode': import_mode}
'name': 'DownloadedEpisodesScan',
'path': remote_dir(dir_name),
'downloadClientId': download_id,
'importMode': import_mode,
}
else: else:
log.debug(f'path: {dir_name}') log.debug(f'path: {dir_name}')
data = { data = {'name': 'DownloadedEpisodesScan', 'path': dir_name, 'downloadClientId': download_id, 'importMode': import_mode}
'name': 'DownloadedEpisodesScan',
'path': dir_name,
'downloadClientId': download_id,
'importMode': import_mode,
}
if not download_id: if not download_id:
data.pop('downloadClientId') data.pop('downloadClientId')
url = nzb2media.utils.common.create_url(scheme, host, port, route) url = nzb2media.utils.common.create_url(scheme, host, port, route)
@ -391,103 +286,35 @@ def process(
if section == 'SickBeard': if section == 'SickBeard':
if init_sickbeard.fork_obj: if init_sickbeard.fork_obj:
return init_sickbeard.fork_obj.api_call() return init_sickbeard.fork_obj.api_call()
else: session = requests.Session()
s = requests.Session() log.debug(f'Opening URL: {url} with params: {fork_params}')
log.debug(f'Opening URL: {url} with params: {fork_params}', section,
)
if not apikey and username and password: if not apikey and username and password:
login = f'{web_root}/login' login = f'{web_root}/login'
login_params = {'username': username, 'password': password} login_params = {'username': username, 'password': password}
response = s.get(login, verify=False, timeout=(30, 60)) response = session.get(login, verify=False, timeout=(30, 60))
if response.status_code in [401, 403] and response.cookies.get('_xsrf'): if response.status_code in {401, 403} and response.cookies.get('_xsrf'):
login_params['_xsrf'] = response.cookies.get('_xsrf') login_params['_xsrf'] = response.cookies.get('_xsrf')
s.post( session.post(login, data=login_params, stream=True, verify=False, timeout=(30, 60))
login, response = session.get(url, auth=(username, password), params=fork_params, stream=True, verify=False, timeout=(30, 1800))
data=login_params,
stream=True,
verify=False,
timeout=(30, 60),
)
response = s.get(
url,
auth=(username, password),
params=fork_params,
stream=True,
verify=False,
timeout=(30, 1800),
)
elif section == 'SiCKRAGE': elif section == 'SiCKRAGE':
s = requests.Session() session = requests.Session()
if api_version >= 2 and sso_username and sso_password: if api_version >= 2 and sso_username and sso_password:
oauth = OAuth2Session( oauth = OAuth2Session(client=LegacyApplicationClient(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID))
client=LegacyApplicationClient( oauth_token = oauth.fetch_token(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID, token_url=nzb2media.SICKRAGE_OAUTH_TOKEN_URL, username=sso_username, password=sso_password)
client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID, session.headers.update({'Authorization': 'Bearer ' + oauth_token['access_token']})
), params = {'path': fork_params['path'], 'failed': str(bool(fork_params['failed'])).lower(), 'processMethod': 'move', 'forceReplace': str(bool(fork_params['force_replace'])).lower(), 'returnData': str(bool(fork_params['return_data'])).lower(), 'delete': str(bool(fork_params['delete'])).lower(), 'forceNext': str(bool(fork_params['force_next'])).lower(), 'nzbName': fork_params['nzbName']}
)
oauth_token = oauth.fetch_token(
client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID,
token_url=nzb2media.SICKRAGE_OAUTH_TOKEN_URL,
username=sso_username,
password=sso_password,
)
s.headers.update(
{'Authorization': 'Bearer ' + oauth_token['access_token']},
)
params = {
'path': fork_params['path'],
'failed': str(bool(fork_params['failed'])).lower(),
'processMethod': 'move',
'forceReplace': str(
bool(fork_params['force_replace']),
).lower(),
'returnData': str(
bool(fork_params['return_data']),
).lower(),
'delete': str(bool(fork_params['delete'])).lower(),
'forceNext': str(bool(fork_params['force_next'])).lower(),
'nzbName': fork_params['nzbName'],
}
else: else:
params = fork_params params = fork_params
response = session.get(url, params=params, stream=True, verify=False, timeout=(30, 1800))
response = s.get(
url,
params=params,
stream=True,
verify=False,
timeout=(30, 1800),
)
elif section == 'NzbDrone': elif section == 'NzbDrone':
log.debug(f'Opening URL: {url} with data: {data}') log.debug(f'Opening URL: {url} with data: {data}')
response = requests.post( response = requests.post(url, data=json.dumps(data), headers=headers, stream=True, verify=False, timeout=(30, 1800))
url,
data=json.dumps(data),
headers=headers,
stream=True,
verify=False,
timeout=(30, 1800),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL: {url}') log.error(f'Unable to open URL: {url}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Unable to connect to {section}')
f'{section}: Failed to post-process - Unable to connect to ' if response.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
f'{section}',
)
if response.status_code not in [
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]:
log.error(f'Server returned status {response.status_code}') log.error(f'Server returned status {response.status_code}')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Server returned status {response.status_code}')
f'{section}: Failed to post-process - Server returned status '
f'{response.status_code}',
)
success = False success = False
queued = False queued = False
started = False started = False
@ -504,12 +331,8 @@ def process(
input_name = os.path.split(line)[1] input_name = os.path.split(line)[1]
if 'added to the queue' in line: if 'added to the queue' in line:
queued = True queued = True
if ( if 'Processing succeeded' in line or 'Successfully processed' in line:
'Processing succeeded' in line
or 'Successfully processed' in line
):
success = True success = True
if queued: if queued:
time.sleep(60) time.sleep(60)
elif section == 'SiCKRAGE': elif section == 'SiCKRAGE':
@ -528,63 +351,37 @@ def process(
log.warning(f'No scan id was returned due to: {error}') log.warning(f'No scan id was returned due to: {error}')
scan_id = None scan_id = None
started = False started = False
if status != 0 and delete_failed and not os.path.dirname(dir_name) == dir_name:
if (
status != 0
and delete_failed
and not os.path.dirname(dir_name) == dir_name
):
log.debug(f'Deleting failed files and folder {dir_name}') log.debug(f'Deleting failed files and folder {dir_name}')
remove_dir(dir_name) remove_dir(dir_name)
if success: if success:
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', if section == 'NzbDrone' and started:
) num = 0
elif section == 'NzbDrone' and started:
n = 0
params = {} params = {}
url = f'{url}/{scan_id}' url = f'{url}/{scan_id}'
while n < 6: # set up wait_for minutes to see if command completes.. while num < 6: # set up wait_for minutes to see if command completes..
time.sleep(10 * wait_for) time.sleep(10 * wait_for)
command_status = command_complete(url, params, headers, section) command_status = command_complete(url, params, headers, section)
if command_status and command_status in ['completed', 'failed']: if command_status and command_status in {'completed', 'failed'}:
break break
n += 1 num += 1
if command_status: if command_status:
log.debug(f'The Scan command return status: {command_status}') log.debug(f'The Scan command return status: {command_status}')
if not os.path.exists(dir_name): if not os.path.exists(dir_name):
log.debug(f'The directory {dir_name} has been removed. Renaming was successful.') log.debug(f'The directory {dir_name} has been removed. Renaming was successful.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', if command_status and command_status in {'completed'}:
)
elif command_status and command_status in ['completed']:
log.debug('The Scan command has completed successfully. Renaming was successful.') log.debug('The Scan command has completed successfully. Renaming was successful.')
return ProcessResult.success( return ProcessResult.success(f'{section}: Successfully post-processed {input_name}')
f'{section}: Successfully post-processed {input_name}', if command_status and command_status in {'failed'}:
)
elif command_status and command_status in ['failed']:
log.debug('The Scan command has failed. Renaming was not successful.') log.debug('The Scan command has failed. Renaming was not successful.')
# return ProcessResult.failure( # return ProcessResult.failure(f'{SECTION}: Failed to post-process {input_name}')
# f'{section}: Failed to post-process {input_name}' url2 = nzb2media.utils.common.create_url(scheme, host, port, route2)
# ) if completed_download_handling(url2, headers):
url2 = nzb2media.utils.common.create_url(scheme, host, port, route)
if completed_download_handling(url2, headers, section=section):
log.debug(f'The Scan command did not return status completed, but complete Download Handling is enabled. Passing back to {section}.') log.debug(f'The Scan command did not return status completed, but complete Download Handling is enabled. Passing back to {section}.')
return ProcessResult( return ProcessResult(message=f'{section}: Complete DownLoad Handling is enabled. Passing back to {section}', status_code=status)
message=f'{section}: Complete DownLoad Handling is enabled. '
f'Passing back to {section}',
status_code=status,
)
else:
log.warning('The Scan command did not return a valid status. Renaming was not successful.') log.warning('The Scan command did not return a valid status. Renaming was not successful.')
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process {input_name}')
f'{section}: Failed to post-process {input_name}',
)
else:
# We did not receive Success confirmation. # We did not receive Success confirmation.
return ProcessResult.failure( return ProcessResult.failure(f'{section}: Failed to post-process - Returned log from {section} was not as expected.')
f'{section}: Failed to post-process - Returned log from {section} '
f'was not as expected.',
)

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,6 @@ from nzb2media.utils.files import backup_versioned_file
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
MIN_DB_VERSION = 1 # oldest db version we support migrating from MIN_DB_VERSION = 1 # oldest db version we support migrating from
MAX_DB_VERSION = 2 MAX_DB_VERSION = 2
@ -26,8 +25,6 @@ def backup_database(version):
# = Main DB Migrations = # = Main DB Migrations =
# ====================== # ======================
# Add new migrations at the bottom of the list; subclass the previous migration. # Add new migrations at the bottom of the list; subclass the previous migration.
class InitialSchema(main_db.SchemaUpgrade): class InitialSchema(main_db.SchemaUpgrade):
def test(self): def test(self):
no_update = False no_update = False
@ -37,34 +34,19 @@ class InitialSchema(main_db.SchemaUpgrade):
return no_update return no_update
def execute(self): def execute(self):
if not self.has_table('downloads') and not self.has_table( if not self.has_table('downloads') and not self.has_table('db_version'):
'db_version', queries = ['CREATE TABLE db_version (db_version INTEGER);', 'CREATE TABLE downloads (input_directory TEXT, input_name TEXT, input_hash TEXT, input_id TEXT, client_agent TEXT, status INTEGER, last_update NUMERIC, CONSTRAINT pk_downloadID PRIMARY KEY (input_directory, input_name));', 'INSERT INTO db_version (db_version) VALUES (2);']
):
queries = [
'CREATE TABLE db_version (db_version INTEGER);',
'CREATE TABLE downloads (input_directory TEXT, input_name TEXT, input_hash TEXT, input_id TEXT, client_agent TEXT, status INTEGER, last_update NUMERIC, CONSTRAINT pk_downloadID PRIMARY KEY (input_directory, input_name));',
'INSERT INTO db_version (db_version) VALUES (2);',
]
for query in queries: for query in queries:
self.connection.action(query) self.connection.action(query)
else: else:
cur_db_version = self.check_db_version() cur_db_version = self.check_db_version()
if cur_db_version < MIN_DB_VERSION: if cur_db_version < MIN_DB_VERSION:
log.critical(f'Your database version ({cur_db_version}) is too old to migrate from what this version of nzbToMedia supports ({MIN_DB_VERSION}).\nPlease remove nzbtomedia.db file to begin fresh.') log.critical(f'Your database version ({cur_db_version}) is too old to migrate from what this version of nzbToMedia supports ({MIN_DB_VERSION}).\nPlease remove nzbtomedia.db file to begin fresh.')
sys.exit(1) sys.exit(1)
if cur_db_version > MAX_DB_VERSION: if cur_db_version > MAX_DB_VERSION:
log.critical(f'Your database version ({cur_db_version}) has been incremented past what this version of nzbToMedia supports ({MAX_DB_VERSION}).\nIf you have used other forks of nzbToMedia, your database may be unusable due to their modifications.') log.critical(f'Your database version ({cur_db_version}) has been incremented past what this version of nzbToMedia supports ({MAX_DB_VERSION}).\nIf you have used other forks of nzbToMedia, your database may be unusable due to their modifications.')
sys.exit(1) sys.exit(1)
if cur_db_version < MAX_DB_VERSION: # We need to upgrade. if cur_db_version < MAX_DB_VERSION: # We need to upgrade.
queries = [ queries = ['CREATE TABLE downloads2 (input_directory TEXT, input_name TEXT, input_hash TEXT, input_id TEXT, client_agent TEXT, status INTEGER, last_update NUMERIC, CONSTRAINT pk_downloadID PRIMARY KEY (input_directory, input_name));', 'INSERT INTO downloads2 SELECT * FROM downloads;', 'DROP TABLE IF EXISTS downloads;', 'ALTER TABLE downloads2 RENAME TO downloads;', 'INSERT INTO db_version (db_version) VALUES (2);']
'CREATE TABLE downloads2 (input_directory TEXT, input_name TEXT, input_hash TEXT, input_id TEXT, client_agent TEXT, status INTEGER, last_update NUMERIC, CONSTRAINT pk_downloadID PRIMARY KEY (input_directory, input_name));',
'INSERT INTO downloads2 SELECT * FROM downloads;',
'DROP TABLE IF EXISTS downloads;',
'ALTER TABLE downloads2 RENAME TO downloads;',
'INSERT INTO db_version (db_version) VALUES (2);',
]
for query in queries: for query in queries:
self.connection.action(query) self.connection.action(query)

View file

@ -6,8 +6,9 @@ import platform
import shutil import shutil
import stat import stat
import subprocess import subprocess
from subprocess import call from subprocess import DEVNULL
from subprocess import Popen from subprocess import Popen
from subprocess import call
from time import sleep from time import sleep
import nzb2media import nzb2media
@ -23,126 +24,56 @@ def extract(file_path, output_destination):
if not os.path.exists(nzb2media.SEVENZIP): if not os.path.exists(nzb2media.SEVENZIP):
log.error('EXTRACTOR: Could not find 7-zip, Exiting') log.error('EXTRACTOR: Could not find 7-zip, Exiting')
return False return False
wscriptlocation = os.path.join( wscriptlocation = os.path.join(os.environ['WINDIR'], 'system32', 'wscript.exe')
os.environ['WINDIR'], 'system32', 'wscript.exe', invislocation = os.path.join(nzb2media.APP_ROOT, 'nzb2media', 'extractor', 'bin', 'invisible.vbs')
) cmd_7zip = [wscriptlocation, invislocation, str(nzb2media.SHOWEXTRACT), nzb2media.SEVENZIP, 'x', '-y']
invislocation = os.path.join( ext_7zip = ['.rar', '.zip', '.tar.gz', 'tgz', '.tar.bz2', '.tbz', '.tar.lzma', '.tlz', '.7z', '.xz', '.gz']
nzb2media.APP_ROOT, 'nzb2media', 'extractor', 'bin', 'invisible.vbs',
)
cmd_7zip = [
wscriptlocation,
invislocation,
str(nzb2media.SHOWEXTRACT),
nzb2media.SEVENZIP,
'x',
'-y',
]
ext_7zip = [
'.rar',
'.zip',
'.tar.gz',
'tgz',
'.tar.bz2',
'.tbz',
'.tar.lzma',
'.tlz',
'.7z',
'.xz',
'.gz',
]
extract_commands = dict.fromkeys(ext_7zip, cmd_7zip) extract_commands = dict.fromkeys(ext_7zip, cmd_7zip)
# Using unix # Using unix
else: else:
required_cmds = [ required_cmds = ['unrar', 'unzip', 'tar', 'unxz', 'unlzma', '7zr', 'bunzip2', 'gunzip']
'unrar',
'unzip',
'tar',
'unxz',
'unlzma',
'7zr',
'bunzip2',
'gunzip',
]
# ## Possible future suport: # ## Possible future suport:
# gunzip: gz (cmd will delete original archive) # gunzip: gz (cmd will delete original archive)
# ## the following do not extract to dest dir # ## the following do not extract to destination dir
# '.xz': ['xz', '-d --keep'], # '.xz': ['xz', '-d --keep'],
# '.lzma': ['xz', '-d --format=lzma --keep'], # '.lzma': ['xz', '-d --format=lzma --keep'],
# '.bz2': ['bzip2', '-d --keep'], # '.bz2': ['bzip2', '-d --keep']
extract_commands = {'.rar': ['unrar', 'x', '-o+', '-y'], '.tar': ['tar', '-xf'], '.zip': ['unzip'], '.tar.gz': ['tar', '-xzf'], '.tgz': ['tar', '-xzf'], '.tar.bz2': ['tar', '-xjf'], '.tbz': ['tar', '-xjf'], '.tar.lzma': ['tar', '--lzma', '-xf'], '.tlz': ['tar', '--lzma', '-xf'], '.tar.xz': ['tar', '--xz', '-xf'], '.txz': ['tar', '--xz', '-xf'], '.7z': ['7zr', 'x'], '.gz': ['gunzip']}
extract_commands = {
'.rar': ['unrar', 'x', '-o+', '-y'],
'.tar': ['tar', '-xf'],
'.zip': ['unzip'],
'.tar.gz': ['tar', '-xzf'],
'.tgz': ['tar', '-xzf'],
'.tar.bz2': ['tar', '-xjf'],
'.tbz': ['tar', '-xjf'],
'.tar.lzma': ['tar', '--lzma', '-xf'],
'.tlz': ['tar', '--lzma', '-xf'],
'.tar.xz': ['tar', '--xz', '-xf'],
'.txz': ['tar', '--xz', '-xf'],
'.7z': ['7zr', 'x'],
'.gz': ['gunzip'],
}
# Test command exists and if not, remove # Test command exists and if not, remove
if not os.getenv('TR_TORRENT_DIR'): if not os.getenv('TR_TORRENT_DIR'):
devnull = open(os.devnull, 'w')
for cmd in required_cmds: for cmd in required_cmds:
if call( if call(['which', cmd], stdout=DEVNULL, stderr=DEVNULL):
['which', cmd], # note, returns 0 if exists, or 1 if doesn't exist.
stdout=devnull, for key, val in extract_commands.items():
stderr=devnull, if cmd in val[0]:
): # note, returns 0 if exists, or 1 if doesn't exist. if not call(['which', '7zr'], stdout=DEVNULL, stderr=DEVNULL):
for k, v in extract_commands.items(): # we do have '7zr'
if cmd in v[0]: extract_commands[key] = ['7zr', 'x', '-y']
if not call( elif not call(['which', '7z'], stdout=DEVNULL, stderr=DEVNULL):
['which', '7zr'], # we do have '7z'
stdout=devnull, extract_commands[key] = ['7z', 'x', '-y']
stderr=devnull, elif not call(['which', '7za'], stdout=DEVNULL, stderr=DEVNULL):
): # we do have '7zr' # we do have '7za'
extract_commands[k] = ['7zr', 'x', '-y'] extract_commands[key] = ['7za', 'x', '-y']
elif not call(
['which', '7z'], stdout=devnull, stderr=devnull,
): # we do have '7z'
extract_commands[k] = ['7z', 'x', '-y']
elif not call(
['which', '7za'],
stdout=devnull,
stderr=devnull,
): # we do have '7za'
extract_commands[k] = ['7za', 'x', '-y']
else: else:
log.error(f'EXTRACTOR: {cmd} not found, disabling support for {k}') log.error(f'EXTRACTOR: {cmd} not found, disabling support for {key}')
del extract_commands[k] del extract_commands[key]
devnull.close()
else: else:
log.warning('EXTRACTOR: Cannot determine which tool to use when called from Transmission') log.warning('EXTRACTOR: Cannot determine which tool to use when called from Transmission')
if not extract_commands: if not extract_commands:
log.warning('EXTRACTOR: No archive extracting programs found, plugin will be disabled') log.warning('EXTRACTOR: No archive extracting programs found, plugin will be disabled')
ext = os.path.splitext(file_path) ext = os.path.splitext(file_path)
cmd = [] cmd = []
if ext[1] in ('.gz', '.bz2', '.lzma'): if ext[1] in {'.gz', '.bz2', '.lzma'}:
# Check if this is a tar # Check if this is a tar
if os.path.splitext(ext[0])[1] == '.tar': if os.path.splitext(ext[0])[1] == '.tar':
cmd = extract_commands[f'.tar{ext[1]}'] cmd = extract_commands[f'.tar{ext[1]}']
else: # Try gunzip else: # Try gunzip
cmd = extract_commands[ext[1]] cmd = extract_commands[ext[1]]
elif ext[1] in ('.1', '.01', '.001') and os.path.splitext(ext[0])[1] in ( elif ext[1] in {'.1', '.01', '.001'} and os.path.splitext(ext[0])[1] in {'.rar', '.zip', '.7z'}:
'.rar',
'.zip',
'.7z',
):
cmd = extract_commands[os.path.splitext(ext[0])[1]] cmd = extract_commands[os.path.splitext(ext[0])[1]]
elif ext[1] in ( elif ext[1] in {'.cb7', '.cba', '.cbr', '.cbt', '.cbz'}:
'.cb7', # don't extract these comic book archives.
'.cba',
'.cbr',
'.cbt',
'.cbz',
): # don't extract these comic book archives.
return False return False
else: else:
if ext[1] in extract_commands: if ext[1] in extract_commands:
@ -150,23 +81,15 @@ def extract(file_path, output_destination):
else: else:
log.debug(f'EXTRACTOR: Unknown file type: {ext[1]}') log.debug(f'EXTRACTOR: Unknown file type: {ext[1]}')
return False return False
# Create outputDestination folder # Create outputDestination folder
nzb2media.make_dir(output_destination) nzb2media.make_dir(output_destination)
if nzb2media.PASSWORDS_FILE and os.path.isfile(os.path.normpath(nzb2media.PASSWORDS_FILE)):
if nzb2media.PASSWORDS_FILE and os.path.isfile( with open(os.path.normpath(nzb2media.PASSWORDS_FILE), encoding='utf-8') as fin:
os.path.normpath(nzb2media.PASSWORDS_FILE), passwords = [line.strip() for line in fin]
):
passwords = [
line.strip()
for line in open(os.path.normpath(nzb2media.PASSWORDS_FILE))
]
else: else:
passwords = [] passwords = []
log.info(f'Extracting {file_path} to {output_destination}') log.info(f'Extracting {file_path} to {output_destination}')
log.debug(f'Extracting {cmd} {file_path} {output_destination}') log.debug(f'Extracting {cmd} {file_path} {output_destination}')
orig_files = [] orig_files = []
orig_dirs = [] orig_dirs = []
for directory, subdirs, files in os.walk(output_destination): for directory, subdirs, files in os.walk(output_destination):
@ -174,13 +97,9 @@ def extract(file_path, output_destination):
orig_dirs.append(os.path.join(directory, subdir)) orig_dirs.append(os.path.join(directory, subdir))
for file in files: for file in files:
orig_files.append(os.path.join(directory, file)) orig_files.append(os.path.join(directory, file))
pwd = os.getcwd() # Get our Present Working Directory pwd = os.getcwd() # Get our Present Working Directory
os.chdir( # Not all unpack commands accept full paths, so just extract into this directory
output_destination, os.chdir(output_destination)
) # Not all unpack commands accept full paths, so just extract into this directory
devnull = open(os.devnull, 'w')
try: # now works same for nt and *nix try: # now works same for nt and *nix
info = None info = None
cmd.append(file_path) # add filePath to final cmd arg. cmd.append(file_path) # add filePath to final cmd arg.
@ -192,40 +111,30 @@ def extract(file_path, output_destination):
cmd2 = cmd cmd2 = cmd
if 'gunzip' not in cmd: # gunzip doesn't support password if 'gunzip' not in cmd: # gunzip doesn't support password
cmd2.append('-p-') # don't prompt for password. cmd2.append('-p-') # don't prompt for password.
p = Popen( with Popen(cmd2, stdout=DEVNULL, stderr=DEVNULL, startupinfo=info) as proc:
cmd2, stdout=devnull, stderr=devnull, startupinfo=info, res = proc.wait() # should extract files fine.
) # should extract files fine.
res = p.wait()
if res == 0: # Both Linux and Windows return 0 for successful. if res == 0: # Both Linux and Windows return 0 for successful.
log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination}') log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination}')
success = 1 success = 1
elif len(passwords) > 0 and not 'gunzip' in cmd: elif len(passwords) > 0 and 'gunzip' not in cmd:
log.info('EXTRACTOR: Attempting to extract with passwords') log.info('EXTRACTOR: Attempting to extract with passwords')
for password in passwords: for password in passwords:
if ( if password == '': # if edited in windows or otherwise if blank lines.
password == ''
): # if edited in windows or otherwise if blank lines.
continue continue
cmd2 = cmd cmd2 = cmd
# append password here. # append password here.
passcmd = f'-p{password}' passcmd = f'-p{password}'
cmd2.append(passcmd) cmd2.append(passcmd)
p = Popen( with Popen(cmd2, stdout=DEVNULL, stderr=DEVNULL, startupinfo=info) as proc:
cmd2, stdout=devnull, stderr=devnull, startupinfo=info, res = proc.wait() # should extract files fine.
) # should extract files fine.
res = p.wait()
if (res >= 0 and platform == 'Windows') or res == 0: if (res >= 0 and platform == 'Windows') or res == 0:
log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination} using password: {password}') log.info(f'EXTRACTOR: Extraction was successful for {file_path} to {output_destination} using password: {password}')
success = 1 success = 1
break break
else:
continue
except Exception: except Exception:
log.error(f'EXTRACTOR: Extraction failed for {file_path}. Could not call command {cmd}') log.error(f'EXTRACTOR: Extraction failed for {file_path}. Could not call command {cmd}')
os.chdir(pwd) os.chdir(pwd)
return False return False
devnull.close()
os.chdir(pwd) # Go back to our Original Working Directory os.chdir(pwd) # Go back to our Original Working Directory
if success: if success:
# sleep to let files finish writing to disk # sleep to let files finish writing to disk
@ -241,12 +150,9 @@ def extract(file_path, output_destination):
for file in files: for file in files:
if not os.path.join(directory, file) in orig_files: if not os.path.join(directory, file) in orig_files:
try: try:
shutil.copymode( shutil.copymode(file_path, os.path.join(directory, file))
file_path, os.path.join(directory, file),
)
except Exception: except Exception:
pass pass
return True return True
else:
log.error(f'EXTRACTOR: Extraction failed for {file_path}. Result was {res}') log.error(f'EXTRACTOR: Extraction failed for {file_path}. Result was {res}')
return False return False

View file

@ -2,63 +2,40 @@
~~~~~ ~~~~~
License for use and distribution License for use and distribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7-Zip Copyright (C) 1999-2018 Igor Pavlov. 7-Zip Copyright (C) 1999-2018 Igor Pavlov.
The licenses for files are: The licenses for files are:
1) 7z.dll: 1) 7z.dll:
- The "GNU LGPL" as main license for most of the code - The "GNU LGPL" as main license for most of the code
- The "GNU LGPL" with "unRAR license restriction" for some code - The "GNU LGPL" with "unRAR license restriction" for some code
- The "BSD 3-clause License" for some code - The "BSD 3-clause License" for some code
2) All other files: the "GNU LGPL". 2) All other files: the "GNU LGPL".
Redistributions in binary form must reproduce related license information from this file. Redistributions in binary form must reproduce related license information from this file.
Note: Note:
You can use 7-Zip on any computer, including a computer in a commercial You can use 7-Zip on any computer, including a computer in a commercial
organization. You don't need to register or pay for 7-Zip. organization. You don't need to register or pay for 7-Zip.
GNU LGPL information GNU LGPL information
-------------------- --------------------
This library is free software; you can redistribute it and/or This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details. Lesser General Public License for more details.
You can receive a copy of the GNU Lesser General Public License from You can receive a copy of the GNU Lesser General Public License from
http://www.gnu.org/ http://www.gnu.org/
BSD 3-clause License BSD 3-clause License
-------------------- --------------------
The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression. The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression.
That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, that also uses the "BSD 3-clause License":
that also uses the "BSD 3-clause License":
---- ----
Copyright (c) 2015-2016, Apple Inc. All rights reserved. Copyright (c) 2015-2016, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the distribution. in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived 3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived
from this software without specific prior written permission. from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
@ -66,25 +43,15 @@
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---- ----
unRAR license restriction unRAR license restriction
------------------------- -------------------------
The decompression engine for RAR archives was developed using source The decompression engine for RAR archives was developed using source
code of unRAR program. code of unRAR program.
All copyrights to original unRAR code are owned by Alexander Roshal. All copyrights to original unRAR code are owned by Alexander Roshal.
The license for original unRAR code has the following restriction: The license for original unRAR code has the following restriction:
The unRAR sources cannot be used to re-create the RAR compression algorithm, which is proprietary. Distribution of modified unRAR sources in separate form
The unRAR sources cannot be used to re-create the RAR compression algorithm,
which is proprietary. Distribution of modified unRAR sources in separate form
or as a part of other software is permitted, provided that it is clearly or as a part of other software is permitted, provided that it is clearly
stated in the documentation and source comments that the code may stated in the documentation and source comments that the code may
not be used to develop a RAR (WinRAR) compatible archiver. not be used to develop a RAR (WinRAR) compatible archiver.
-- --
Igor Pavlov Igor Pavlov

View file

@ -1,11 +1,9 @@
set args = WScript.Arguments set args = WScript.Arguments
num = args.Count num = args.Count
if num < 2 then if num < 2 then
WScript.Echo "Usage: [CScript | WScript] invis.vbs aScript.bat <visible or invisible 1/0> <some script arguments>" WScript.Echo "Usage: [CScript | WScript] invis.vbs aScript.bat <visible or invisible 1/0> <some script arguments>"
WScript.Quit 1 WScript.Quit 1
end if end if
sargs = "" sargs = ""
if num > 2 then if num > 2 then
sargs = " " sargs = " "
@ -14,8 +12,6 @@ if num > 2 then
sargs = sargs & anArg & " " sargs = sargs & anArg & " "
next next
end if end if
Set WshShell = WScript.CreateObject("WScript.Shell") Set WshShell = WScript.CreateObject("WScript.Shell")
returnValue = WshShell.Run("""" & args(1) & """" & sargs, args(0), True) returnValue = WshShell.Run("""" & args(1) & """" & sargs, args(0), True)
WScript.Quit(returnValue) WScript.Quit(returnValue)

View file

@ -2,63 +2,40 @@
~~~~~ ~~~~~
License for use and distribution License for use and distribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7-Zip Copyright (C) 1999-2018 Igor Pavlov. 7-Zip Copyright (C) 1999-2018 Igor Pavlov.
The licenses for files are: The licenses for files are:
1) 7z.dll: 1) 7z.dll:
- The "GNU LGPL" as main license for most of the code - The "GNU LGPL" as main license for most of the code
- The "GNU LGPL" with "unRAR license restriction" for some code - The "GNU LGPL" with "unRAR license restriction" for some code
- The "BSD 3-clause License" for some code - The "BSD 3-clause License" for some code
2) All other files: the "GNU LGPL". 2) All other files: the "GNU LGPL".
Redistributions in binary form must reproduce related license information from this file. Redistributions in binary form must reproduce related license information from this file.
Note: Note:
You can use 7-Zip on any computer, including a computer in a commercial You can use 7-Zip on any computer, including a computer in a commercial
organization. You don't need to register or pay for 7-Zip. organization. You don't need to register or pay for 7-Zip.
GNU LGPL information GNU LGPL information
-------------------- --------------------
This library is free software; you can redistribute it and/or This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details. Lesser General Public License for more details.
You can receive a copy of the GNU Lesser General Public License from You can receive a copy of the GNU Lesser General Public License from
http://www.gnu.org/ http://www.gnu.org/
BSD 3-clause License BSD 3-clause License
-------------------- --------------------
The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression. The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression.
That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, that also uses the "BSD 3-clause License":
that also uses the "BSD 3-clause License":
---- ----
Copyright (c) 2015-2016, Apple Inc. All rights reserved. Copyright (c) 2015-2016, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the distribution. in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived 3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived
from this software without specific prior written permission. from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
@ -66,25 +43,15 @@
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---- ----
unRAR license restriction unRAR license restriction
------------------------- -------------------------
The decompression engine for RAR archives was developed using source The decompression engine for RAR archives was developed using source
code of unRAR program. code of unRAR program.
All copyrights to original unRAR code are owned by Alexander Roshal. All copyrights to original unRAR code are owned by Alexander Roshal.
The license for original unRAR code has the following restriction: The license for original unRAR code has the following restriction:
The unRAR sources cannot be used to re-create the RAR compression algorithm, which is proprietary. Distribution of modified unRAR sources in separate form
The unRAR sources cannot be used to re-create the RAR compression algorithm,
which is proprietary. Distribution of modified unRAR sources in separate form
or as a part of other software is permitted, provided that it is clearly or as a part of other software is permitted, provided that it is clearly
stated in the documentation and source comments that the code may stated in the documentation and source comments that the code may
not be used to develop a RAR (WinRAR) compatible archiver. not be used to develop a RAR (WinRAR) compatible archiver.
-- --
Igor Pavlov Igor Pavlov

View file

@ -7,51 +7,36 @@ class GitHub:
"""Simple api wrapper for the Github API v3.""" """Simple api wrapper for the Github API v3."""
def __init__(self, github_repo_user, github_repo, branch='master'): def __init__(self, github_repo_user, github_repo, branch='master'):
self.github_repo_user = github_repo_user self.github_repo_user = github_repo_user
self.github_repo = github_repo self.github_repo = github_repo
self.branch = branch self.branch = branch
def _access_api(self, path, params=None): @staticmethod
def _access_api(path, params=None):
"""Access API at given an API path and optional parameters.""" """Access API at given an API path and optional parameters."""
url = 'https://api.github.com/{path}'.format(path='/'.join(path)) route = '/'.join(path)
url = f'https://api.github.com/{route}'
data = requests.get(url, params=params, verify=False) data = requests.get(url, params=params, verify=False)
return data.json() if data.ok else [] return data.json() if data.ok else []
def commits(self): def commits(self):
""" """
Get the 100 most recent commits from the specified user/repo/branch, starting from HEAD. Get the 100 most recent commits from the specified user/repo/branch, starting from HEAD.
user: The github username of the person whose repo you're querying user: The github username of the person whose repo you're querying
repo: The repo name to query repo: The repo name to query
branch: Optional, the branch name to show commits from branch: Optional, the branch name to show commits from
Returns a deserialized json object containing the commit info. See http://developer.github.com/v3/repos/commits/ Returns a deserialized json object containing the commit info. See http://developer.github.com/v3/repos/commits/
""" """
return self._access_api( return self._access_api(['repos', self.github_repo_user, self.github_repo, 'commits'], params={'per_page': 100, 'sha': self.branch})
['repos', self.github_repo_user, self.github_repo, 'commits'],
params={'per_page': 100, 'sha': self.branch},
)
def compare(self, base, head, per_page=1): def compare(self, base, head, per_page=1):
""" """
Get compares between base and head. Get compares between base and head.
user: The github username of the person whose repo you're querying user: The github username of the person whose repo you're querying
repo: The repo name to query repo: The repo name to query
base: Start compare from branch base: Start compare from branch
head: Current commit sha or branch name to compare head: Current commit sha or branch name to compare
per_page: number of items per page per_page: number of items per page
Returns a deserialized json object containing the compare info. See http://developer.github.com/v3/repos/commits/ Returns a deserialized json object containing the compare info. See http://developer.github.com/v3/repos/commits/
""" """
return self._access_api( return self._access_api(['repos', self.github_repo_user, self.github_repo, 'compare', f'{base}...{head}'], params={'per_page': per_page})
[
'repos',
self.github_repo_user,
self.github_repo,
'compare',
f'{base}...{head}',
],
params={'per_page': per_page},
)

View file

@ -14,9 +14,7 @@ log.addHandler(logging.NullHandler())
def db_filename(filename='nzbtomedia.db', suffix=None): def db_filename(filename='nzbtomedia.db', suffix=None):
""" """
Return the correct location of the database file. Return the correct location of the database file.
@param filename: The sqlite database filename to use. If not specified, will be made to be nzbtomedia.db
@param filename: The sqlite database filename to use. If not specified,
will be made to be nzbtomedia.db
@param suffix: The suffix to append to the filename. A '.' will be added @param suffix: The suffix to append to the filename. A '.' will be added
automatically, i.e. suffix='v0' will make dbfile.db.v0 automatically, i.e. suffix='v0' will make dbfile.db.v0
@return: the correct location of the database file. @return: the correct location of the database file.
@ -27,8 +25,7 @@ def db_filename(filename='nzbtomedia.db', suffix=None):
class DBConnection: class DBConnection:
def __init__(self, filename='nzbtomedia.db', suffix=None, row_type=None): def __init__(self, filename='nzbtomedia.db'):
self.filename = filename self.filename = filename
self.connection = sqlite3.connect(db_filename(filename), 20) self.connection = sqlite3.connect(db_filename(filename), 20)
self.connection.row_factory = sqlite3.Row self.connection.row_factory = sqlite3.Row
@ -37,22 +34,18 @@ class DBConnection:
result = None result = None
try: try:
result = self.select('SELECT db_version FROM db_version') result = self.select('SELECT db_version FROM db_version')
except sqlite3.OperationalError as e: except sqlite3.OperationalError as error:
if 'no such table: db_version' in e.args[0]: if 'no such table: db_version' in error.args[0]:
return 0 return 0
if result: if result:
return int(result[0]['db_version']) return int(result[0]['db_version'])
else:
return 0 return 0
def fetch(self, query, args=None): def fetch(self, query, args=None):
if query is None: if query is None:
return return
sql_result = None sql_result = None
attempt = 0 attempt = 0
while attempt < 5: while attempt < 5:
try: try:
if args is None: if args is None:
@ -65,14 +58,10 @@ class DBConnection:
cursor = self.connection.cursor() cursor = self.connection.cursor()
cursor.execute(query, args) cursor.execute(query, args)
sql_result = cursor.fetchone()[0] sql_result = cursor.fetchone()[0]
# get out of the connection attempt loop since we were successful # get out of the connection attempt loop since we were successful
break break
except sqlite3.OperationalError as error: except sqlite3.OperationalError as error:
if ( if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
'unable to open database file' in error.args[0]
or 'database is locked' in error.args[0]
):
log.warning(f'DB error: {error}') log.warning(f'DB error: {error}')
attempt += 1 attempt += 1
time.sleep(1) time.sleep(1)
@ -82,29 +71,24 @@ class DBConnection:
except sqlite3.DatabaseError as error: except sqlite3.DatabaseError as error:
log.error(f'Fatal error executing query: {error}') log.error(f'Fatal error executing query: {error}')
raise raise
return sql_result return sql_result
def mass_action(self, querylist, log_transaction=False): def mass_action(self, querylist, log_transaction=False):
if querylist is None: if querylist is None:
return return
sql_result = [] sql_result = []
attempt = 0 attempt = 0
while attempt < 5: while attempt < 5:
try: try:
for qu in querylist: for query in querylist:
if len(qu) == 1: if len(query) == 1:
if log_transaction: if log_transaction:
log.debug(qu[0]) log.debug(query[0])
sql_result.append(self.connection.execute(qu[0])) sql_result.append(self.connection.execute(query[0]))
elif len(qu) > 1: elif len(query) > 1:
if log_transaction: if log_transaction:
log.debug(f'{qu[0]} with args {qu[1]}') log.debug(f'{query[0]} with args {query[1]}')
sql_result.append( sql_result.append(self.connection.execute(query[0], query[1]))
self.connection.execute(qu[0], qu[1]),
)
self.connection.commit() self.connection.commit()
log.debug(f'Transaction with {len(querylist)} query\'s executed') log.debug(f'Transaction with {len(querylist)} query\'s executed')
return sql_result return sql_result
@ -112,10 +96,7 @@ class DBConnection:
sql_result = [] sql_result = []
if self.connection: if self.connection:
self.connection.rollback() self.connection.rollback()
if ( if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
'unable to open database file' in error.args[0]
or 'database is locked' in error.args[0]
):
log.warning(f'DB error: {error}') log.warning(f'DB error: {error}')
attempt += 1 attempt += 1
time.sleep(1) time.sleep(1)
@ -127,16 +108,13 @@ class DBConnection:
self.connection.rollback() self.connection.rollback()
log.error(f'Fatal error executing query: {error}') log.error(f'Fatal error executing query: {error}')
raise raise
return sql_result return sql_result
def action(self, query, args=None): def action(self, query, args=None):
if query is None: if query is None:
return return
sql_result = None sql_result = None
attempt = 0 attempt = 0
while attempt < 5: while attempt < 5:
try: try:
if args is None: if args is None:
@ -149,10 +127,7 @@ class DBConnection:
# get out of the connection attempt loop since we were successful # get out of the connection attempt loop since we were successful
break break
except sqlite3.OperationalError as error: except sqlite3.OperationalError as error:
if ( if 'unable to open database file' in error.args[0] or 'database is locked' in error.args[0]:
'unable to open database file' in error.args[0]
or 'database is locked' in error.args[0]
):
log.warning(f'DB error: {error}') log.warning(f'DB error: {error}')
attempt += 1 attempt += 1
time.sleep(1) time.sleep(1)
@ -162,16 +137,12 @@ class DBConnection:
except sqlite3.DatabaseError as error: except sqlite3.DatabaseError as error:
log.error(f'Fatal error executing query: {error}') log.error(f'Fatal error executing query: {error}')
raise raise
return sql_result return sql_result
def select(self, query, args=None): def select(self, query, args=None):
sql_results = self.action(query, args).fetchall() sql_results = self.action(query, args).fetchall()
if sql_results is None: if sql_results is None:
return [] return []
return sql_results return sql_results
def upsert(self, table_name, value_dict, key_dict): def upsert(self, table_name, value_dict, key_dict):
@ -180,27 +151,14 @@ class DBConnection:
changes_before = self.connection.total_changes changes_before = self.connection.total_changes
items = list(value_dict.values()) + list(key_dict.values()) items = list(value_dict.values()) + list(key_dict.values())
self.action( _params = ', '.join(gen_params(value_dict))
'UPDATE {table} ' _conditions = ' AND '.join(gen_params(key_dict))
'SET {params} ' self.action(f'UPDATE {table_name} SET {_params} WHERE {_conditions}', items)
'WHERE {conditions}'.format(
table=table_name,
params=', '.join(gen_params(value_dict)),
conditions=' AND '.join(gen_params(key_dict)),
),
items,
)
if self.connection.total_changes == changes_before: if self.connection.total_changes == changes_before:
self.action( _cols = ', '.join(map(str, value_dict.keys()))
'INSERT OR IGNORE INTO {table} ({columns}) ' values = list(value_dict.values())
'VALUES ({values})'.format( _vals = ', '.join(['?'] * len(values))
table=table_name, self.action(f'INSERT OR IGNORE INTO {table_name} ({_cols}) VALUES ({_vals})', values)
columns=', '.join(map(str, value_dict.keys())),
values=', '.join(['?'] * len(value_dict.values())),
),
list(value_dict.values()),
)
def table_info(self, table_name): def table_info(self, table_name):
# FIXME ? binding is not supported here, but I cannot find a way to escape a string manually # FIXME ? binding is not supported here, but I cannot find a way to escape a string manually
@ -223,17 +181,13 @@ class DBSanityCheck:
# =============== # ===============
# = Upgrade API = # = Upgrade API =
# =============== # ===============
def upgrade_database(connection, schema): def upgrade_database(connection, schema):
log.info('Checking database structure...') log.info('Checking database structure...')
_process_upgrade(connection, schema) _process_upgrade(connection, schema)
def pretty_name(class_name): def pretty_name(class_name):
return ' '.join( return ' '.join([x.group() for x in re.finditer('([A-Z])([a-z0-9]+)', class_name)])
[x.group() for x in re.finditer('([A-Z])([a-z0-9]+)', class_name)],
)
def _process_upgrade(connection, upgrade_class): def _process_upgrade(connection, upgrade_class):
@ -244,16 +198,13 @@ def _process_upgrade(connection, upgrade_class):
try: try:
instance.execute() instance.execute()
except sqlite3.DatabaseError as error: except sqlite3.DatabaseError as error:
print( print(f'Error in {upgrade_class.__name__}: {error}')
f'Error in {upgrade_class.__name__}: {error}',
)
raise raise
log.debug(f'{upgrade_class.__name__} upgrade completed') log.debug(f'{upgrade_class.__name__} upgrade completed')
else: else:
log.debug(f'{upgrade_class.__name__} upgrade not required') log.debug(f'{upgrade_class.__name__} upgrade not required')
for upgrade_sub_class in upgrade_class.__subclasses__():
for upgradeSubClass in upgrade_class.__subclasses__(): _process_upgrade(connection, upgrade_sub_class)
_process_upgrade(connection, upgradeSubClass)
# Base migration class. All future DB changes should be subclassed from this class # Base migration class. All future DB changes should be subclassed from this class
@ -262,15 +213,7 @@ class SchemaUpgrade:
self.connection = connection self.connection = connection
def has_table(self, table_name): def has_table(self, table_name):
return ( return len(self.connection.action('SELECT 1 FROM sqlite_master WHERE name = ?;', (table_name,)).fetchall()) > 0
len(
self.connection.action(
'SELECT 1 FROM sqlite_master WHERE name = ?;',
(table_name,),
).fetchall(),
)
> 0
)
def has_column(self, table_name, column): def has_column(self, table_name, column):
return column in self.connection.table_info(table_name) return column in self.connection.table_info(table_name)
@ -283,12 +226,9 @@ class SchemaUpgrade:
result = self.connection.select('SELECT db_version FROM db_version') result = self.connection.select('SELECT db_version FROM db_version')
if result: if result:
return int(result[-1]['db_version']) return int(result[-1]['db_version'])
else:
return 0 return 0
def inc_db_version(self): def inc_db_version(self):
new_version = self.check_db_version() + 1 new_version = self.check_db_version() + 1
self.connection.action( self.connection.action('UPDATE db_version SET db_version = ?', [new_version])
'UPDATE db_version SET db_version = ?', [new_version],
)
return new_version return new_version

View file

@ -19,12 +19,7 @@ class PyMedusa(SickBeard):
@property @property
def url(self): def url(self):
route = f'{self.sb_init.web_root}/home/postprocess/processEpisode' route = f'{self.sb_init.web_root}/home/postprocess/processEpisode'
return nzb2media.utils.common.create_url( return nzb2media.utils.common.create_url(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, route)
self.sb_init.protocol,
self.sb_init.host,
self.sb_init.port,
route,
)
class PyMedusaApiV1(SickBeard): class PyMedusaApiV1(SickBeard):
@ -33,54 +28,25 @@ class PyMedusaApiV1(SickBeard):
@property @property
def url(self) -> str: def url(self) -> str:
route = f'{self.sb_init.web_root}/api/{self.sb_init.apikey}/' route = f'{self.sb_init.web_root}/api/{self.sb_init.apikey}/'
return nzb2media.utils.common.create_url( return nzb2media.utils.common.create_url(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, route)
self.sb_init.protocol,
self.sb_init.host,
self.sb_init.port,
route,
)
def api_call(self) -> ProcessResult: def api_call(self) -> ProcessResult:
self._process_fork_prarams() self._process_fork_prarams()
log.debug(f'Opening URL: {self.url} with params: {self.sb_init.fork_params}') log.debug(f'Opening URL: {self.url} with params: {self.sb_init.fork_params}')
try: try:
response = self.session.get( response = self.session.get(self.url, auth=(self.sb_init.username, self.sb_init.password), params=self.sb_init.fork_params, stream=True, verify=False, timeout=(30, 1800))
self.url,
auth=(self.sb_init.username, self.sb_init.password),
params=self.sb_init.fork_params,
stream=True,
verify=False,
timeout=(30, 1800),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL: {self.url}') log.error(f'Unable to open URL: {self.url}')
return ProcessResult.failure( return ProcessResult.failure(f'{self.sb_init.section}: Failed to post-process - Unable to connect to {self.sb_init.section}')
f'{self.sb_init.section}: Failed to post-process - Unable to ' successful_status_codes = [requests.codes.ok, requests.codes.created, requests.codes.accepted]
f'connect to {self.sb_init.section}',
)
successful_status_codes = [
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]
if response.status_code not in successful_status_codes: if response.status_code not in successful_status_codes:
log.error(f'Server returned status {response.status_code}') log.error(f'Server returned status {response.status_code}')
result = ProcessResult.failure( result = ProcessResult.failure(f'{self.sb_init.section}: Failed to post-process - Server returned status {response.status_code}')
f'{self.sb_init.section}: Failed to post-process - Server '
f'returned status {response.status_code}',
)
elif response.json()['result'] == 'success': elif response.json()['result'] == 'success':
result = ProcessResult.success( result = ProcessResult.success(f'{self.sb_init.section}: Successfully post-processed {self.input_name}')
f'{self.sb_init.section}: Successfully post-processed '
f'{self.input_name}',
)
else: else:
# We did not receive Success confirmation. # We did not receive Success confirmation.
result = ProcessResult.failure( result = ProcessResult.failure(f'{self.sb_init.section}: Failed to post-process - Returned log from {self.sb_init.section} was not as expected.')
f'{self.sb_init.section}: Failed to post-process - Returned '
f'log from {self.sb_init.section} was not as expected.',
)
return result return result
@ -89,25 +55,16 @@ class PyMedusaApiV2(SickBeard):
def __init__(self, sb_init): def __init__(self, sb_init):
super().__init__(sb_init) super().__init__(sb_init)
# Check for an apikey # Check for an apikey
# This is required with using fork = medusa-apiv2 # This is required with using fork = medusa-apiv2
if not sb_init.apikey: if not sb_init.apikey:
log.error( log.error('For the SECTION SickBeard `fork = medusa-apiv2` you also ' 'need to configure an `apikey`')
'For the section SickBeard `fork = medusa-apiv2` you also '
'need to configure an `apikey`',
)
raise ValueError('Missing apikey for fork: medusa-apiv2') raise ValueError('Missing apikey for fork: medusa-apiv2')
@property @property
def url(self): def url(self):
route = f'{self.sb_init.web_root}/api/v2/postprocess' route = f'{self.sb_init.web_root}/api/v2/postprocess'
return nzb2media.utils.common.create_url( return nzb2media.utils.common.create_url(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, route)
self.sb_init.protocol,
self.sb_init.host,
self.sb_init.port,
route,
)
def _get_identifier_status(self, url): def _get_identifier_status(self, url):
# Loop through requesting medusa for the status on the queueitem. # Loop through requesting medusa for the status on the queueitem.
@ -116,12 +73,10 @@ class PyMedusaApiV2(SickBeard):
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to get postprocess identifier status') log.error('Unable to get postprocess identifier status')
return False return False
try: try:
jdata = response.json() jdata = response.json()
except ValueError: except ValueError:
return False return False
return jdata return jdata
def api_call(self) -> ProcessResult: def api_call(self) -> ProcessResult:
@ -130,29 +85,15 @@ class PyMedusaApiV2(SickBeard):
payload = self.sb_init.fork_params payload = self.sb_init.fork_params
payload['resource'] = self.sb_init.fork_params['nzbName'] payload['resource'] = self.sb_init.fork_params['nzbName']
del payload['nzbName'] del payload['nzbName']
# Update the session with the x-api-key # Update the session with the x-api-key
headers = { headers = {'x-api-key': self.sb_init.apikey, 'Content-type': 'application/json'}
'x-api-key': self.sb_init.apikey,
'Content-type': 'application/json',
}
self.session.headers.update(headers) self.session.headers.update(headers)
# Send postprocess request # Send postprocess request
try: try:
response = self.session.post( response = self.session.post(self.url, json=payload, verify=False, timeout=(30, 1800))
self.url,
json=payload,
verify=False,
timeout=(30, 1800),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to send postprocess request') log.error('Unable to send postprocess request')
return ProcessResult.failure( return ProcessResult.failure(f'{self.sb_init.section}: Unable to send postprocess request to PyMedusa')
f'{self.sb_init.section}: Unable to send postprocess request '
f'to PyMedusa',
)
# Get UUID # Get UUID
if response: if response:
try: try:
@ -162,43 +103,32 @@ class PyMedusaApiV2(SickBeard):
return ProcessResult.failure('No data returned from provider') return ProcessResult.failure('No data returned from provider')
else: else:
jdata = {} jdata = {}
status = jdata.get('status', None) status = jdata.get('status', None)
if status != 'success': if status != 'success':
return ProcessResult.failure() return ProcessResult.failure()
wait_for = int(self.sb_init.config.get('wait_for', 2)) wait_for = int(self.sb_init.config.get('wait_for', 2))
n = 0 num = 0
response = {} response = {}
queue_item_identifier = jdata['queueItem']['identifier'] queue_item_identifier = jdata['queueItem']['identifier']
url = f'{self.url}/{queue_item_identifier}' url = f'{self.url}/{queue_item_identifier}'
while n < 12: # set up wait_for minutes to see if command completes.. while num < 12: # set up wait_for minutes to see if command completes..
time.sleep(5 * wait_for) time.sleep(5 * wait_for)
response = self._get_identifier_status(url) response = self._get_identifier_status(url)
if response and response.get('success'): if response and response.get('success'):
break break
if 'error' in response: if 'error' in response:
break break
n += 1 num += 1
# Log Medusa's PP logs here. # Log Medusa's PP logs here.
if response.get('output'): if response.get('output'):
for line in response['output']: for line in response['output']:
log.postprocess(line) log.debug(line)
# For now this will most likely always be True. # For now this will most likely always be True.
# In the future we could return an exit state for when the PP in # In the future we could return an exit state for when the PP in
# medusa didn't yield an expected result. # medusa didn't yield an expected result.
if response.get('success'): if response.get('success'):
result = ProcessResult.success( result = ProcessResult.success(f'{self.sb_init.section}: Successfully post-processed {self.input_name}')
f'{self.sb_init.section}: Successfully post-processed '
f'{self.input_name}',
)
else: else:
# We did not receive Success confirmation. # We did not receive Success confirmation.
result = ProcessResult.failure( result = ProcessResult.failure(f'{self.sb_init.section}: Failed to post-process - Returned log from {self.sb_init.section} was not as expected.')
f'{self.sb_init.section}: Failed to post-process - Returned '
f'log from {self.sb_init.section} was not as expected.',
)
return result return result

View file

@ -17,7 +17,6 @@ log.addHandler(logging.NullHandler())
class InitSickBeard: class InitSickBeard:
"""SickBeard init class. """SickBeard init class.
Used to determine which SickBeard fork object to initialize. Used to determine which SickBeard fork object to initialize.
""" """
@ -26,7 +25,6 @@ class InitSickBeard:
self.config = cfg self.config = cfg
self.section = section self.section = section
self.input_category = input_category self.input_category = input_category
self.host = cfg['host'] self.host = cfg['host']
self.port = cfg['port'] self.port = cfg['port']
self.ssl = int(cfg.get('ssl', 0)) self.ssl = int(cfg.get('ssl', 0))
@ -38,169 +36,70 @@ class InitSickBeard:
self.api_version = int(cfg.get('api_version', 2)) self.api_version = int(cfg.get('api_version', 2))
self.sso_username = cfg.get('sso_username', '') self.sso_username = cfg.get('sso_username', '')
self.sso_password = cfg.get('sso_password', '') self.sso_password = cfg.get('sso_password', '')
self.fork = '' self.fork = ''
self.fork_params = None self.fork_params = None
self.fork_obj = None self.fork_obj = None
replace = {'medusa': 'Medusa', 'medusa-api': 'Medusa-api', 'sickbeard-api': 'SickBeard-api', 'sickgear': 'SickGear', 'sickchill': 'SickChill', 'stheno': 'Stheno'}
replace = {
'medusa': 'Medusa',
'medusa-api': 'Medusa-api',
'sickbeard-api': 'SickBeard-api',
'sickgear': 'SickGear',
'sickchill': 'SickChill',
'stheno': 'Stheno',
}
_val = cfg.get('fork', 'auto') _val = cfg.get('fork', 'auto')
f1 = replace.get(_val, _val) fork_name = replace.get(_val, _val)
try: try:
self.fork = f1, nzb2media.FORKS[f1] self.fork = fork_name, nzb2media.FORKS[fork_name]
except KeyError: except KeyError:
self.fork = 'auto' self.fork = 'auto'
self.protocol = 'https://' if self.ssl else 'http://' self.protocol = 'https://' if self.ssl else 'http://'
def auto_fork(self): def auto_fork(self):
# auto-detect correct section # auto-detect correct SECTION
# config settings # config settings
if nzb2media.FORK_SET: if nzb2media.FORK_SET:
# keep using determined fork for multiple (manual) post-processing # keep using determined fork for multiple (manual) post-processing
log.info( log.info(f'{self.section}:{self.input_category} fork already set to {nzb2media.FORK_SET[0]}')
f'{self.section}:{self.input_category} fork already set to '
f'{nzb2media.FORK_SET[0]}',
)
return nzb2media.FORK_SET[0], nzb2media.FORK_SET[1] return nzb2media.FORK_SET[0], nzb2media.FORK_SET[1]
cfg = dict(nzb2media.CFG[self.section][self.input_category]) cfg = dict(nzb2media.CFG[self.section][self.input_category])
replace = {'medusa': 'Medusa', 'medusa-api': 'Medusa-api', 'medusa-apiv1': 'Medusa-api', 'medusa-apiv2': 'Medusa-apiv2', 'sickbeard-api': 'SickBeard-api', 'sickgear': 'SickGear', 'sickchill': 'SickChill', 'stheno': 'Stheno'}
replace = {
'medusa': 'Medusa',
'medusa-api': 'Medusa-api',
'medusa-apiv1': 'Medusa-api',
'medusa-apiv2': 'Medusa-apiv2',
'sickbeard-api': 'SickBeard-api',
'sickgear': 'SickGear',
'sickchill': 'SickChill',
'stheno': 'Stheno',
}
_val = cfg.get('fork', 'auto') _val = cfg.get('fork', 'auto')
f1 = replace.get(_val.lower(), _val) fork_name = replace.get(_val.lower(), _val)
try: try:
self.fork = f1, nzb2media.FORKS[f1] self.fork = fork_name, nzb2media.FORKS[fork_name]
except KeyError: except KeyError:
self.fork = 'auto' self.fork = 'auto'
protocol = 'https://' if self.ssl else 'http://' protocol = 'https://' if self.ssl else 'http://'
if self.section == 'NzbDrone': if self.section == 'NzbDrone':
log.info(f'Attempting to verify {self.input_category} fork') log.info(f'Attempting to verify {self.input_category} fork')
url = nzb2media.utils.common.create_url( url = nzb2media.utils.common.create_url(scheme=protocol, host=self.host, port=self.port, path=f'{self.web_root}/api/rootfolder')
scheme=protocol,
host=self.host,
port=self.port,
path=f'{self.web_root}/api/rootfolder',
)
headers = {'X-Api-Key': self.apikey} headers = {'X-Api-Key': self.apikey}
try: try:
response = requests.get( response = requests.get(url, headers=headers, stream=True, verify=False)
url,
headers=headers,
stream=True,
verify=False,
)
except requests.ConnectionError: except requests.ConnectionError:
log.warning( log.warning(f'Could not connect to {self.section}:{self.input_category} to verify fork!')
f'Could not connect to {self.section}:'
f'{self.input_category} to verify fork!',
)
if not response.ok: if not response.ok:
log.warning( log.warning(f'Connection to {self.section}:{self.input_category} failed! Check your configuration')
f'Connection to {self.section}:{self.input_category} '
f'failed! Check your configuration',
)
self.fork = ['default', {}] self.fork = ['default', {}]
elif self.section == 'SiCKRAGE': elif self.section == 'SiCKRAGE':
log.info(f'Attempting to verify {self.input_category} fork') log.info(f'Attempting to verify {self.input_category} fork')
if self.api_version >= 2: if self.api_version >= 2:
url = nzb2media.utils.common.create_url( url = nzb2media.utils.common.create_url(scheme=protocol, host=self.host, port=self.port, path=f'{self.web_root}/api/v{self.api_version}/ping')
scheme=protocol,
host=self.host,
port=self.port,
path=f'{self.web_root}/api/v{self.api_version}/ping',
)
api_params = {} api_params = {}
else: else:
api_version = f'v{self.api_version}' api_version = f'v{self.api_version}'
url = nzb2media.utils.common.create_url( url = nzb2media.utils.common.create_url(scheme=protocol, host=self.host, port=self.port, path=f'{self.web_root}/api/{api_version}/{self.apikey}/')
scheme=protocol,
host=self.host,
port=self.port,
path=f'{self.web_root}/api/{api_version}/{self.apikey}/',
)
api_params = {'cmd': 'postprocess', 'help': '1'} api_params = {'cmd': 'postprocess', 'help': '1'}
try: try:
if ( if self.api_version >= 2 and self.sso_username and self.sso_password:
self.api_version >= 2 oauth = OAuth2Session(client=LegacyApplicationClient(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID))
and self.sso_username oauth_token = oauth.fetch_token(client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID, token_url=nzb2media.SICKRAGE_OAUTH_TOKEN_URL, username=self.sso_username, password=self.sso_password)
and self.sso_password
):
oauth = OAuth2Session(
client=LegacyApplicationClient(
client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID,
),
)
oauth_token = oauth.fetch_token(
client_id=nzb2media.SICKRAGE_OAUTH_CLIENT_ID,
token_url=nzb2media.SICKRAGE_OAUTH_TOKEN_URL,
username=self.sso_username,
password=self.sso_password,
)
token = oauth_token['access_token'] token = oauth_token['access_token']
response = requests.get( response = requests.get(url, headers={'Authorization': f'Bearer {token}'}, stream=True, verify=False)
url,
headers={f'Authorization': f'Bearer {token}'},
stream=True,
verify=False,
)
else: else:
response = requests.get( response = requests.get(url, params=api_params, stream=True, verify=False)
url,
params=api_params,
stream=True,
verify=False,
)
if not response.ok: if not response.ok:
log.warning( log.warning(f'Connection to {self.section}:{self.input_category} failed! Check your configuration')
f'Connection to {self.section}:{self.input_category} '
f'failed! Check your configuration',
)
except requests.ConnectionError: except requests.ConnectionError:
log.warning( log.warning(f'Could not connect to {self.section}:{self.input_category} to verify API version!')
f'Could not connect to {self.section}:' params = {'path': None, 'failed': None, 'process_method': None, 'force_replace': None, 'return_data': None, 'type': None, 'delete': None, 'force_next': None, 'is_priority': None}
f'{self.input_category} to verify API version!',
)
params = {
'path': None,
'failed': None,
'process_method': None,
'force_replace': None,
'return_data': None,
'type': None,
'delete': None,
'force_next': None,
'is_priority': None,
}
self.fork = ['default', params] self.fork = ['default', params]
elif self.fork == 'auto': elif self.fork == 'auto':
self.detect_fork() self.detect_fork()
log.info(f'{self.section}:{self.input_category} fork set to {self.fork[0]}') log.info(f'{self.section}:{self.input_category} fork set to {self.fork[0]}')
nzb2media.FORK_SET = self.fork nzb2media.FORK_SET = self.fork
self.fork, self.fork_params = self.fork[0], self.fork[1] self.fork, self.fork_params = self.fork[0], self.fork[1]
@ -209,14 +108,13 @@ class InitSickBeard:
return self.fork, self.fork_params return self.fork, self.fork_params
@staticmethod @staticmethod
def _api_check(r, params, rem_params): def _api_check(response, params, rem_params):
try: try:
json_data = r.json() json_data = response.json()
except ValueError: except ValueError:
log.error('Failed to get JSON data from response') log.error('Failed to get JSON data from response')
log.debug('Response received') log.debug('Response received')
raise raise
try: try:
json_data = json_data['data'] json_data = json_data['data']
except KeyError: except KeyError:
@ -227,16 +125,15 @@ class InitSickBeard:
if isinstance(json_data, str): if isinstance(json_data, str):
return rem_params, False return rem_params, False
json_data = json_data.get('data', json_data) json_data = json_data.get('data', json_data)
try: try:
optional_parameters = json_data['optionalParameters'].keys() optional_parameters = json_data['optionalParameters'].keys()
# Find excess parameters # Find excess parameters
excess_parameters = set(params).difference(optional_parameters) excess_parameters = set(params).difference(optional_parameters)
excess_parameters.remove('cmd') # Don't remove cmd from api params excess_parameters.remove('cmd') # Don't remove cmd from api params
log.debug(f'Removing excess parameters: ' f'{sorted(excess_parameters)}') log.debug(f'Removing excess parameters: {sorted(excess_parameters)}')
rem_params.extend(excess_parameters) rem_params.extend(excess_parameters)
return rem_params, True return rem_params, True
except: except Exception:
log.error('Failed to identify optionalParameters') log.error('Failed to identify optionalParameters')
return rem_params, False return rem_params, False
@ -249,56 +146,26 @@ class InitSickBeard:
# Define the order to test. # Define the order to test.
# Default must be first since default fork doesn't reject parameters. # Default must be first since default fork doesn't reject parameters.
# Then in order of most unique parameters. # Then in order of most unique parameters.
if self.apikey: if self.apikey:
url = nzb2media.utils.common.create_url( url = nzb2media.utils.common.create_url(scheme=self.protocol, host=self.host, port=self.port, path=f'{self.web_root}/api/{self.apikey}/')
scheme=self.protocol,
host=self.host,
port=self.port,
path=f'{self.web_root}/api/{self.apikey}/',
)
api_params = {'cmd': 'sg.postprocess', 'help': '1'} api_params = {'cmd': 'sg.postprocess', 'help': '1'}
else: else:
url = nzb2media.utils.common.create_url( url = nzb2media.utils.common.create_url(scheme=self.protocol, host=self.host, port=self.port, path=f'{self.web_root}/home/postprocess')
scheme=self.protocol,
host=self.host,
port=self.port,
path=f'{self.web_root}/home/postprocess',
)
api_params = {} api_params = {}
# attempting to auto-detect fork # attempting to auto-detect fork
try: try:
session = requests.Session() session = requests.Session()
if not self.apikey and self.username and self.password: if not self.apikey and self.username and self.password:
login = nzb2media.utils.common.create_url( login = nzb2media.utils.common.create_url(scheme=self.protocol, host=self.host, port=self.port, path=f'{self.web_root}/login')
scheme=self.protocol, login_params = {'username': self.username, 'password': self.password}
host=self.host,
port=self.port,
path=f'{self.web_root}/login',
)
login_params = {
'username': self.username,
'password': self.password,
}
response = session.get(login, verify=False, timeout=(30, 60)) response = session.get(login, verify=False, timeout=(30, 60))
if response.status_code in [401, 403] and response.cookies.get('_xsrf'): if response.status_code in {401, 403} and response.cookies.get('_xsrf'):
login_params['_xsrf'] = response.cookies.get('_xsrf') login_params['_xsrf'] = response.cookies.get('_xsrf')
session.post(login, data=login_params, stream=True, verify=False) session.post(login, data=login_params, stream=True, verify=False)
response = session.get( response = session.get(url, auth=(self.username, self.password), params=api_params, verify=False)
url,
auth=(self.username, self.password),
params=api_params,
verify=False,
)
except requests.ConnectionError: except requests.ConnectionError:
log.info( log.info(f'Could not connect to {self.section}:{self.input_category} to perform auto-fork detection!')
f'Could not connect to {self.section}:{self.input_category} '
f'to perform auto-fork detection!',
)
response = [] response = []
if response and response.ok: if response and response.ok:
if self.apikey: if self.apikey:
rem_params, found = self._api_check(response, params, rem_params) rem_params, found = self._api_check(response, params, rem_params)
@ -308,78 +175,44 @@ class InitSickBeard:
api_params = {'cmd': 'help', 'subject': 'postprocess'} api_params = {'cmd': 'help', 'subject': 'postprocess'}
try: try:
if not self.apikey and self.username and self.password: if not self.apikey and self.username and self.password:
response = session.get( response = session.get(url, auth=(self.username, self.password), params=api_params, verify=False)
url,
auth=(self.username, self.password),
params=api_params,
verify=False,
)
else: else:
response = session.get(url, params=api_params, verify=False) response = session.get(url, params=api_params, verify=False)
except requests.ConnectionError: except requests.ConnectionError:
log.info( log.info(f'Could not connect to {self.section}:{self.input_category} to perform auto-fork detection!')
f'Could not connect to {self.section}:'
f'{self.input_category} to perform auto-fork '
f'detection!',
)
rem_params, found = self._api_check(response, params, rem_params) rem_params, found = self._api_check(response, params, rem_params)
params['cmd'] = 'postprocess' params['cmd'] = 'postprocess'
else: else:
# Find excess parameters # Find excess parameters
rem_params.extend( rem_params.extend(param for param in params if f'name="{param}"' not in response.text)
param
for param in params
if f'name="{param}"' not in response.text
)
# Remove excess params # Remove excess params
for param in rem_params: for param in rem_params:
params.pop(param) params.pop(param)
for fork in sorted(nzb2media.FORKS, reverse=False): for fork in sorted(nzb2media.FORKS, reverse=False):
if params == fork[1]: if params == fork[1]:
detected = True detected = True
break break
if detected: if detected:
self.fork = fork self.fork = fork
log.info( log.info(f'{self.section}:{self.input_category} fork auto-detection successful ...')
f'{self.section}:{self.input_category} fork auto-detection '
f'successful ...',
)
elif rem_params: elif rem_params:
log.info( log.info(f'{self.section}:{self.input_category} fork auto-detection found custom params {params}')
f'{self.section}:{self.input_category} fork auto-detection '
f'found custom params {params}',
)
self.fork = ['custom', params] self.fork = ['custom', params]
else: else:
log.info( log.info(f'{self.section}:{self.input_category} fork auto-detection failed')
f'{self.section}:{self.input_category} fork auto-detection ' self.fork = list(nzb2media.FORKS.items())[list(nzb2media.FORKS.keys()).index(nzb2media.FORK_DEFAULT)]
f'failed',
)
self.fork = list(nzb2media.FORKS.items())[
list(nzb2media.FORKS.keys()).index(nzb2media.FORK_DEFAULT)
]
def _init_fork(self): def _init_fork(self):
# These need to be imported here, to prevent a circular import. # These need to be imported here, to prevent a circular import.
from .pymedusa import PyMedusa, PyMedusaApiV1, PyMedusaApiV2 from .pymedusa import PyMedusa, PyMedusaApiV1, PyMedusaApiV2
mapped_forks = { mapped_forks = {'Medusa': PyMedusa, 'Medusa-api': PyMedusaApiV1, 'Medusa-apiv2': PyMedusaApiV2}
'Medusa': PyMedusa,
'Medusa-api': PyMedusaApiV1,
'Medusa-apiv2': PyMedusaApiV2,
}
log.debug(f'Create object for fork {self.fork}') log.debug(f'Create object for fork {self.fork}')
if self.fork and mapped_forks.get(self.fork): if self.fork and mapped_forks.get(self.fork):
# Create the fork object and pass self (SickBeardInit) to it for all the data, like Config. # Create the fork object and pass self (SickBeardInit) to it for all the data, like Config.
self.fork_obj = mapped_forks[self.fork](self) self.fork_obj = mapped_forks[self.fork](self)
else: else:
log.info( log.info(f'{self.section}:{self.input_category} Could not create a fork object for {self.fork}. Probaly class not added yet.')
f'{self.section}:{self.input_category} Could not create a '
f'fork object for {self.fork}. Probaly class not added yet.',
)
class SickBeard: class SickBeard:
@ -391,17 +224,12 @@ class SickBeard:
"""SB constructor.""" """SB constructor."""
self.sb_init = sb_init self.sb_init = sb_init
self.session = requests.Session() self.session = requests.Session()
self.failed = None self.failed = None
self.status = None self.status = None
self.input_name = None self.input_name = None
self.dir_name = None self.dir_name = None
self.delete_failed = int(self.sb_init.config.get('delete_failed', 0)) self.delete_failed = int(self.sb_init.config.get('delete_failed', 0))
self.nzb_extraction_by = self.sb_init.config.get( self.nzb_extraction_by = self.sb_init.config.get('nzbExtractionBy', 'Downloader')
'nzbExtractionBy',
'Downloader',
)
self.process_method = self.sb_init.config.get('process_method') self.process_method = self.sb_init.config.get('process_method')
self.remote_path = int(self.sb_init.config.get('remote_path', 0)) self.remote_path = int(self.sb_init.config.get('remote_path', 0))
self.wait_for = int(self.sb_init.config.get('wait_for', 2)) self.wait_for = int(self.sb_init.config.get('wait_for', 2))
@ -409,22 +237,13 @@ class SickBeard:
self.delete_on = int(self.sb_init.config.get('delete_on', 0)) self.delete_on = int(self.sb_init.config.get('delete_on', 0))
self.ignore_subs = int(self.sb_init.config.get('ignore_subs', 0)) self.ignore_subs = int(self.sb_init.config.get('ignore_subs', 0))
self.is_priority = int(self.sb_init.config.get('is_priority', 0)) self.is_priority = int(self.sb_init.config.get('is_priority', 0))
# get importmode, default to 'Move' for consistency with legacy # get importmode, default to 'Move' for consistency with legacy
self.import_mode = self.sb_init.config.get('importMode', 'Move') self.import_mode = self.sb_init.config.get('importMode', 'Move')
# Keep track of result state # Keep track of result state
self.success = False self.success = False
def initialize( def initialize(self, dir_name, input_name=None, failed=False, client_agent='manual'):
self,
dir_name,
input_name=None,
failed=False,
client_agent='manual',
):
"""We need to call this explicitely because we need some variables. """We need to call this explicitely because we need some variables.
We can't pass these directly through the constructor. We can't pass these directly through the constructor.
""" """
self.dir_name = dir_name self.dir_name = dir_name
@ -435,10 +254,7 @@ class SickBeard:
self.extract = 0 self.extract = 0
else: else:
self.extract = int(self.sb_init.config.get('extract', 0)) self.extract = int(self.sb_init.config.get('extract', 0))
if ( if client_agent == nzb2media.TORRENT_CLIENT_AGENT and nzb2media.USE_LINK == 'move-sym':
client_agent == nzb2media.TORRENT_CLIENT_AGENT
and nzb2media.USE_LINK == 'move-sym'
):
self.process_method = 'symlink' self.process_method = 'symlink'
@property @property
@ -447,12 +263,7 @@ class SickBeard:
route = f'{self.sb_init.web_root}/api/{self.sb_init.apikey}/' route = f'{self.sb_init.web_root}/api/{self.sb_init.apikey}/'
else: else:
route = f'{self.sb_init.web_root}/home/postprocess/processEpisode' route = f'{self.sb_init.web_root}/home/postprocess/processEpisode'
return nzb2media.utils.common.create_url( return nzb2media.utils.common.create_url(scheme=self.sb_init.protocol, host=self.sb_init.host, port=self.sb_init.port, path=route)
scheme=self.sb_init.protocol,
host=self.sb_init.host,
port=self.sb_init.port,
path=route,
)
def _process_fork_prarams(self): def _process_fork_prarams(self):
# configure SB params to pass # configure SB params to pass
@ -461,172 +272,109 @@ class SickBeard:
fork_params['proc_type'] = 'manual' fork_params['proc_type'] = 'manual'
if self.input_name is not None: if self.input_name is not None:
fork_params['nzbName'] = self.input_name fork_params['nzbName'] = self.input_name
for param in copy.copy(fork_params): for param in copy.copy(fork_params):
if param == 'failed': if param == 'failed':
if self.failed > 1: self.failed = min(self.failed, 1)
self.failed = 1
fork_params[param] = self.failed fork_params[param] = self.failed
if 'proc_type' in fork_params: if 'proc_type' in fork_params:
del fork_params['proc_type'] del fork_params['proc_type']
if 'type' in fork_params: if 'type' in fork_params:
del fork_params['type'] del fork_params['type']
if param == 'return_data': if param == 'return_data':
fork_params[param] = 0 fork_params[param] = 0
if 'quiet' in fork_params: if 'quiet' in fork_params:
del fork_params['quiet'] del fork_params['quiet']
if param == 'type': if param == 'type':
if 'type' in fork_params: if 'type' in fork_params:
# Set if we haven't already deleted for 'failed' above. # Set if we haven't already deleted for 'failed' above.
fork_params[param] = 'manual' fork_params[param] = 'manual'
if 'proc_type' in fork_params: if 'proc_type' in fork_params:
del fork_params['proc_type'] del fork_params['proc_type']
if param in {'dir_name', 'dir', 'proc_dir', 'process_directory', 'path'}:
if param in [
'dir_name',
'dir',
'proc_dir',
'process_directory',
'path',
]:
fork_params[param] = self.dir_name fork_params[param] = self.dir_name
if self.remote_path: if self.remote_path:
fork_params[param] = remote_dir(self.dir_name) fork_params[param] = remote_dir(self.dir_name)
# SickChill allows multiple path types. Only retunr 'path' # SickChill allows multiple path types. Only retunr 'path'
if param == 'proc_dir' and 'path' in fork_params: if param == 'proc_dir' and 'path' in fork_params:
del fork_params['proc_dir'] del fork_params['proc_dir']
if param == 'process_method': if param == 'process_method':
if self.process_method: if self.process_method:
fork_params[param] = self.process_method fork_params[param] = self.process_method
else: else:
del fork_params[param] del fork_params[param]
if param in {'force', 'force_replace'}:
if param in ['force', 'force_replace']:
if self.force: if self.force:
fork_params[param] = self.force fork_params[param] = self.force
else: else:
del fork_params[param] del fork_params[param]
if param in {'delete_on', 'delete'}:
if param in ['delete_on', 'delete']:
if self.delete_on: if self.delete_on:
fork_params[param] = self.delete_on fork_params[param] = self.delete_on
else: else:
del fork_params[param] del fork_params[param]
if param == 'ignore_subs': if param == 'ignore_subs':
if self.ignore_subs: if self.ignore_subs:
fork_params[param] = self.ignore_subs fork_params[param] = self.ignore_subs
else: else:
del fork_params[param] del fork_params[param]
if param == 'is_priority': if param == 'is_priority':
if self.is_priority: if self.is_priority:
fork_params[param] = self.is_priority fork_params[param] = self.is_priority
else: else:
del fork_params[param] del fork_params[param]
if param == 'force_next': if param == 'force_next':
fork_params[param] = 1 fork_params[param] = 1
# delete any unused params so we don't pass them to SB by mistake # delete any unused params so we don't pass them to SB by mistake
[fork_params.pop(k) for k, v in list(fork_params.items()) if v is None] for key, value in list(fork_params.items()):
if value is None:
del fork_params[key]
def api_call(self) -> ProcessResult: def api_call(self) -> ProcessResult:
"""Perform a base sickbeard api call.""" """Perform a base sickbeard api call."""
self._process_fork_prarams() self._process_fork_prarams()
log.debug(f'Opening URL: {self.url} with params: {self.sb_init.fork_params}') log.debug(f'Opening URL: {self.url} with params: {self.sb_init.fork_params}')
try: try:
if ( if not self.sb_init.apikey and self.sb_init.username and self.sb_init.password:
not self.sb_init.apikey
and self.sb_init.username
and self.sb_init.password
):
# If not using the api, we need to login using user/pass first. # If not using the api, we need to login using user/pass first.
route = f'{self.sb_init.web_root}/login' route = f'{self.sb_init.web_root}/login'
login = nzb2media.utils.common.create_url( login = nzb2media.utils.common.create_url(self.sb_init.protocol, self.sb_init.host, self.sb_init.port, route)
self.sb_init.protocol, login_params = {'username': self.sb_init.username, 'password': self.sb_init.password}
self.sb_init.host,
self.sb_init.port,
route,
)
login_params = {
'username': self.sb_init.username,
'password': self.sb_init.password,
}
response = self.session.get(login, verify=False, timeout=(30, 60)) response = self.session.get(login, verify=False, timeout=(30, 60))
if response.status_code in [401, 403] and response.cookies.get('_xsrf'): if response.status_code in {401, 403} and response.cookies.get('_xsrf'):
login_params['_xsrf'] = response.cookies.get('_xsrf') login_params['_xsrf'] = response.cookies.get('_xsrf')
self.session.post( self.session.post(login, data=login_params, stream=True, verify=False, timeout=(30, 60))
login, response = self.session.get(self.url, auth=(self.sb_init.username, self.sb_init.password), params=self.sb_init.fork_params, stream=True, verify=False, timeout=(30, 1800))
data=login_params,
stream=True,
verify=False,
timeout=(30, 60),
)
response = self.session.get(
self.url,
auth=(self.sb_init.username, self.sb_init.password),
params=self.sb_init.fork_params,
stream=True,
verify=False,
timeout=(30, 1800),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL: {self.url}') log.error(f'Unable to open URL: {self.url}')
result = ProcessResult.failure( result = ProcessResult.failure(f'{self.sb_init.section}: Failed to post-process - Unable to connect to {self.sb_init.section}')
f'{self.sb_init.section}: Failed to post-process - Unable to '
f'connect to {self.sb_init.section}',
)
else: else:
successful_statuses = [ successful_statuses = [requests.codes.ok, requests.codes.created, requests.codes.accepted]
requests.codes.ok,
requests.codes.created,
requests.codes.accepted,
]
if response.status_code not in successful_statuses: if response.status_code not in successful_statuses:
log.error(f'Server returned status {response.status_code}') log.error(f'Server returned status {response.status_code}')
result = ProcessResult.failure( result = ProcessResult.failure(f'{self.sb_init.section}: Failed to post-process - Server returned status {response.status_code}')
f'{self.sb_init.section}: Failed to post-process - Server '
f'returned status {response.status_code}',
)
else: else:
result = self.process_response(response) result = self.process_response(response)
return result return result
def process_response(self, response: requests.Response) -> ProcessResult: def process_response(self, response: requests.Response) -> ProcessResult:
"""Iterate over the lines returned, and log. """Iterate over the lines returned, and log.
:param response: Streamed Requests response object. :param response: Streamed Requests response object.
This method will need to be overwritten in the forks, for alternative response handling. This method will need to be overwritten in the forks, for alternative response handling.
""" """
for line in response.iter_lines(): for line in response.iter_lines():
if line: if line:
line = line.decode('utf-8') line = line.decode('utf-8')
log.postprocess(line) log.debug(line)
# if 'Moving file from' in line: # if 'Moving file from' in line:
# input_name = os.path.split(line)[1] # input_name = os.path.split(line)[1]
# if 'added to the queue' in line: # if 'added to the queue' in line:
# queued = True # queued = True
# For the refactoring i'm only considering vanilla sickbeard, # For the refactoring i'm only considering vanilla sickbeard, # as for the base class.
# as for the base class. if 'Processing succeeded' in line or 'Successfully processed' in line:
if (
'Processing succeeded' in line
or 'Successfully processed' in line
):
self.success = True self.success = True
if self.success: if self.success:
result = ProcessResult.success( result = ProcessResult.success(f'{self.sb_init.section}: Successfully post-processed {self.input_name}')
f'{self.sb_init.section}: Successfully post-processed '
f'{self.input_name}',
)
else: else:
# We did not receive Success confirmation. # We did not receive Success confirmation.
result = ProcessResult.failure( result = ProcessResult.failure(f'{self.sb_init.section}: Failed to post-process - Returned log from {self.sb_init.section} was not as expected.')
f'{self.sb_init.section}: Failed to post-process - Returned '
f'log from {self.sb_init.section} was not as expected.',
)
return result return result

View file

@ -8,13 +8,11 @@ def configure_nzbs(config):
nzb2media.NZB_CLIENT_AGENT = nzb_config['clientAgent'] # sabnzbd nzb2media.NZB_CLIENT_AGENT = nzb_config['clientAgent'] # sabnzbd
nzb2media.NZB_DEFAULT_DIRECTORY = nzb_config['default_downloadDirectory'] nzb2media.NZB_DEFAULT_DIRECTORY = nzb_config['default_downloadDirectory']
nzb2media.NZB_NO_MANUAL = int(nzb_config['no_manual'], 0) nzb2media.NZB_NO_MANUAL = int(nzb_config['no_manual'], 0)
configure_sabnzbd(nzb_config) configure_sabnzbd(nzb_config)
def configure_sabnzbd(config): def configure_sabnzbd(config):
nzb2media.SABNZBD_HOST = config['sabnzbd_host'] nzb2media.SABNZBD_HOST = config['sabnzbd_host']
nzb2media.SABNZBD_PORT = int( # defaults to accommodate NzbGet
config['sabnzbd_port'] or 8080, nzb2media.SABNZBD_PORT = int(config['sabnzbd_port'] or 8080)
) # defaults to accommodate NzbGet
nzb2media.SABNZBD_APIKEY = config['sabnzbd_apikey'] nzb2media.SABNZBD_APIKEY = config['sabnzbd_apikey']

View file

@ -16,27 +16,18 @@ def configure_plex(config):
nzb2media.PLEX_PORT = config['Plex']['plex_port'] nzb2media.PLEX_PORT = config['Plex']['plex_port']
nzb2media.PLEX_TOKEN = config['Plex']['plex_token'] nzb2media.PLEX_TOKEN = config['Plex']['plex_token']
plex_section = config['Plex']['plex_sections'] or [] plex_section = config['Plex']['plex_sections'] or []
if plex_section: if plex_section:
if isinstance(plex_section, list): if isinstance(plex_section, list):
plex_section = ','.join( plex_section = ','.join(plex_section) # fix in case this imported as list.
plex_section, plex_section = [tuple(item.split(',')) for item in plex_section.split('|')]
) # fix in case this imported as list.
plex_section = [
tuple(item.split(',')) for item in plex_section.split('|')
]
nzb2media.PLEX_SECTION = plex_section nzb2media.PLEX_SECTION = plex_section
def plex_update(category): def plex_update(category):
if nzb2media.FAILED: if nzb2media.FAILED:
return return
url = '{scheme}://{host}:{port}/library/sections/'.format( scheme = 'https' if nzb2media.PLEX_SSL else 'http'
scheme='https' if nzb2media.PLEX_SSL else 'http', url = f'{scheme}://{nzb2media.PLEX_HOST}:{nzb2media.PLEX_PORT}/library/sections/'
host=nzb2media.PLEX_HOST,
port=nzb2media.PLEX_PORT,
)
section = None section = None
if not nzb2media.PLEX_SECTION: if not nzb2media.PLEX_SECTION:
return return
@ -44,10 +35,9 @@ def plex_update(category):
for item in nzb2media.PLEX_SECTION: for item in nzb2media.PLEX_SECTION:
if item[0] == category: if item[0] == category:
section = item[1] section = item[1]
if section: if section:
url = f'{url}{section}/refresh?X-Plex-Token={nzb2media.PLEX_TOKEN}' url = f'{url}{section}/refresh?X-Plex-Token={nzb2media.PLEX_TOKEN}'
requests.get(url, timeout=(60, 120), verify=False) requests.get(url, timeout=(60, 120), verify=False)
log.debug('Plex Library has been refreshed.') log.debug('Plex Library has been refreshed.')
else: else:
log.debug('Could not identify section for plex update') log.debug('Could not identify SECTION for plex update')

View file

@ -7,41 +7,36 @@ import re
import subliminal import subliminal
from babelfish import Language from babelfish import Language
import nzb2media from nzb2media import GETSUBS
from nzb2media import SLANGUAGES
from nzb2media.utils.files import list_media_files
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def import_subs(filename): def import_subs(filename):
if not nzb2media.GETSUBS: if not GETSUBS:
return return
try: try:
subliminal.region.configure( subliminal.region.configure('dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'})
'dogpile.cache.dbm', arguments={'filename': 'cachefile.dbm'},
)
except Exception: except Exception:
pass pass
languages = set() languages = set()
for item in nzb2media.SLANGUAGES: for item in SLANGUAGES:
try: try:
languages.add(Language(item)) languages.add(Language(item))
except Exception: except Exception:
pass pass
if not languages: if not languages:
return return
log.info(f'Attempting to download subtitles for {filename}') log.info(f'Attempting to download subtitles for {filename}')
try: try:
video = subliminal.scan_video(filename) video = subliminal.scan_video(filename)
subtitles = subliminal.download_best_subtitles({video}, languages) subtitles = subliminal.download_best_subtitles({video}, languages)
subliminal.save_subtitles(video, subtitles[video]) subliminal.save_subtitles(video, subtitles[video])
for subtitle in subtitles[video]: for subtitle in subtitles[video]:
subtitle_path = subliminal.subtitle.get_subtitle_path( subtitle_path = subliminal.subtitle.get_subtitle_path(video.name, subtitle.language)
video.name, subtitle.language,
)
os.chmod(subtitle_path, 0o644) os.chmod(subtitle_path, 0o644)
except Exception as error: except Exception as error:
log.error(f'Failed to download subtitles for {filename} due to: {error}') log.error(f'Failed to download subtitles for {filename} due to: {error}')
@ -50,31 +45,22 @@ def import_subs(filename):
def rename_subs(path): def rename_subs(path):
filepaths = [] filepaths = []
sub_ext = ['.srt', '.sub', '.idx'] sub_ext = ['.srt', '.sub', '.idx']
vidfiles = nzb2media.list_media_files( vidfiles = list_media_files(path, media=True, audio=False, meta=False, archives=False)
path, media=True, audio=False, meta=False, archives=False, if not vidfiles or len(vidfiles) > 1: # If there is more than 1 video file, or no video files, we can't rename subs.
)
if (
not vidfiles or len(vidfiles) > 1
): # If there is more than 1 video file, or no video files, we can't rename subs.
return return
name = os.path.splitext(os.path.split(vidfiles[0])[1])[0] name = os.path.splitext(os.path.split(vidfiles[0])[1])[0]
for directory, _, filenames in os.walk(path): for directory, _, filenames in os.walk(path):
for filename in filenames: for filename in filenames:
filepaths.extend([os.path.join(directory, filename)]) filepaths.extend([os.path.join(directory, filename)])
subfiles = [ subfiles = [item for item in filepaths if os.path.splitext(item)[1] in sub_ext]
item for item in filepaths if os.path.splitext(item)[1] in sub_ext
]
subfiles.sort() # This should sort subtitle names by language (alpha) and Number (where multiple) subfiles.sort() # This should sort subtitle names by language (alpha) and Number (where multiple)
renamed = [] renamed = []
for sub in subfiles: for sub in subfiles:
subname, ext = os.path.splitext(os.path.basename(sub)) subname, ext = os.path.splitext(os.path.basename(sub))
if ( if name in subname:
name in subname continue # The sub file name already includes the video name.
): # The sub file name already includes the video name. # find whole words in string
continue words = re.findall('[a-zA-Z]+', str(subname))
words = re.findall(
'[a-zA-Z]+', str(subname),
) # find whole words in string
# parse the words for language descriptors. # parse the words for language descriptors.
lan = None lan = None
for word in words: for word in words:
@ -87,7 +73,7 @@ def rename_subs(path):
lan = Language.fromname(word.lower()) lan = Language.fromname(word.lower())
if lan: if lan:
break break
except: # if we didn't find a language, try next word. except Exception: # if we didn't find a language, try next word.
continue continue
# rename the sub file as name.lan.ext # rename the sub file as name.lan.ext
if not lan: if not lan:
@ -95,12 +81,10 @@ def rename_subs(path):
new_sub_name = name new_sub_name = name
else: else:
new_sub_name = f'{name}.{str(lan)}' new_sub_name = f'{name}.{str(lan)}'
new_sub = os.path.join( # full path and name less ext
directory, new_sub_name, new_sub = os.path.join(directory, new_sub_name)
) # full path and name less ext if f'{new_sub}{ext}' in renamed:
if ( # If duplicate names, add unique number before ext.
f'{new_sub}{ext}' in renamed
): # If duplicate names, add unique number before ext.
for i in range(1, len(renamed) + 1): for i in range(1, len(renamed) + 1):
if f'{new_sub}.{i}{ext}' in renamed: if f'{new_sub}.{i}{ext}' in renamed:
continue continue

View file

@ -16,13 +16,8 @@ log.addHandler(logging.NullHandler())
def process(): def process():
# Perform Manual Post-Processing # Perform Manual Post-Processing
log.warning('Invalid number of arguments received from client, Switching to manual run mode ...') log.warning('Invalid number of arguments received from client, Switching to manual run mode ...')
# Post-Processing Result # Post-Processing Result
result = ProcessResult( result = ProcessResult(message='', status_code=0)
message='',
status_code=0,
)
for section, subsections in nzb2media.SECTIONS.items(): for section, subsections in nzb2media.SECTIONS.items():
for subsection in subsections: for subsection in subsections:
if not nzb2media.CFG[section][subsection].isenabled(): if not nzb2media.CFG[section][subsection].isenabled():
@ -30,38 +25,19 @@ def process():
for dir_name in get_dirs(section, subsection, link='move'): for dir_name in get_dirs(section, subsection, link='move'):
log.info(f'Starting manual run for {section}:{subsection} - Folder: {dir_name}') log.info(f'Starting manual run for {section}:{subsection} - Folder: {dir_name}')
log.info(f'Checking database for download info for {os.path.basename(dir_name)} ...') log.info(f'Checking database for download info for {os.path.basename(dir_name)} ...')
nzb2media.DOWNLOAD_INFO = get_download_info(os.path.basename(dir_name), 0)
nzb2media.DOWNLOAD_INFO = get_download_info(
os.path.basename(dir_name),
0,
)
if nzb2media.DOWNLOAD_INFO: if nzb2media.DOWNLOAD_INFO:
log.info(f'Found download info for {os.path.basename(dir_name)}, setting variables now ...') log.info(f'Found download info for {os.path.basename(dir_name)}, setting variables now ...')
client_agent = ( client_agent = nzb2media.DOWNLOAD_INFO[0]['client_agent'] or 'manual'
nzb2media.DOWNLOAD_INFO[0]['client_agent'] or 'manual'
)
download_id = nzb2media.DOWNLOAD_INFO[0]['input_id'] or '' download_id = nzb2media.DOWNLOAD_INFO[0]['input_id'] or ''
else: else:
log.info(f'Unable to locate download info for {os.path.basename(dir_name)}, continuing to try and process this release ...') log.info(f'Unable to locate download info for {os.path.basename(dir_name)}, continuing to try and process this release ...')
client_agent = 'manual' client_agent = 'manual'
download_id = '' download_id = ''
if client_agent and client_agent.lower() not in nzb2media.NZB_CLIENTS:
if (
client_agent
and client_agent.lower() not in nzb2media.NZB_CLIENTS
):
continue continue
input_name = os.path.basename(dir_name) input_name = os.path.basename(dir_name)
results = nzb.process(dir_name, input_name, 0, client_agent=client_agent, download_id=download_id or None, input_category=subsection)
results = nzb.process(
dir_name,
input_name,
0,
client_agent=client_agent,
download_id=download_id or None,
input_category=subsection,
)
if results.status_code != 0: if results.status_code != 0:
log.error(f'A problem was reported when trying to perform a manual run for {section}:{subsection}.') log.error(f'A problem was reported when trying to perform a manual run for {section}:{subsection}.')
result = results result = results

View file

@ -25,22 +25,10 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def process( def process(input_directory, input_name=None, status=0, client_agent='manual', download_id=None, input_category=None, failure_link=None):
input_directory,
input_name=None,
status=0,
client_agent='manual',
download_id=None,
input_category=None,
failure_link=None,
):
if nzb2media.SAFE_MODE and input_directory == nzb2media.NZB_DEFAULT_DIRECTORY: if nzb2media.SAFE_MODE and input_directory == nzb2media.NZB_DEFAULT_DIRECTORY:
log.error(f'The input directory:[{input_directory}] is the Default Download Directory. Please configure category directories to prevent processing of other media.') log.error(f'The input directory:[{input_directory}] is the Default Download Directory. Please configure category directories to prevent processing of other media.')
return ProcessResult( return ProcessResult(message='', status_code=-1)
message='',
status_code=-1,
)
if not download_id and client_agent == 'sabnzbd': if not download_id and client_agent == 'sabnzbd':
download_id = get_nzoid(input_name) download_id = get_nzoid(input_name)
if client_agent != 'manual' and not nzb2media.DOWNLOAD_INFO: if client_agent != 'manual' and not nzb2media.DOWNLOAD_INFO:
@ -54,16 +42,9 @@ def process(
except Exception: except Exception:
pass pass
control_value_dict = {'input_directory': input_directory1} control_value_dict = {'input_directory': input_directory1}
new_value_dict = { new_value_dict = {'input_name': input_name1, 'input_hash': download_id, 'input_id': download_id, 'client_agent': client_agent, 'status': 0, 'last_update': datetime.date.today().toordinal()}
'input_name': input_name1,
'input_hash': download_id,
'input_id': download_id,
'client_agent': client_agent,
'status': 0,
'last_update': datetime.date.today().toordinal(),
}
my_db.upsert('downloads', new_value_dict, control_value_dict) my_db.upsert('downloads', new_value_dict, control_value_dict)
# auto-detect section # auto-detect SECTION
if input_category is None: if input_category is None:
input_category = 'UNCAT' input_category = 'UNCAT'
usercat = input_category usercat = input_category
@ -72,36 +53,23 @@ def process(
section = nzb2media.CFG.findsection('ALL').isenabled() section = nzb2media.CFG.findsection('ALL').isenabled()
if section is None: if section is None:
log.error(f'Category:[{input_category}] is not defined or is not enabled. Please rename it or ensure it is enabled for the appropriate section in your autoProcessMedia.cfg and try again.') log.error(f'Category:[{input_category}] is not defined or is not enabled. Please rename it or ensure it is enabled for the appropriate section in your autoProcessMedia.cfg and try again.')
return ProcessResult( return ProcessResult(message='', status_code=-1)
message='',
status_code=-1,
)
else:
usercat = 'ALL' usercat = 'ALL'
if len(section) > 1: if len(section) > 1:
log.error(f'Category:[{input_category}] is not unique, {section.keys()} are using it. Please rename it or disable all other sections using the same category name in your autoProcessMedia.cfg and try again.') log.error(f'Category:[{input_category}] is not unique, {section.keys()} are using it. Please rename it or disable all other sections using the same category name in your autoProcessMedia.cfg and try again.')
return ProcessResult( return ProcessResult(message='', status_code=-1)
message='',
status_code=-1,
)
if section: if section:
section_name = section.keys()[0] section_name = section.keys()[0]
log.info(f'Auto-detected SECTION:{section_name}') log.info(f'Auto-detected SECTION:{section_name}')
else: else:
log.error(f'Unable to locate a section with subsection:{input_category} enabled in your autoProcessMedia.cfg, exiting!') log.error(f'Unable to locate a section with subsection:{input_category} enabled in your autoProcessMedia.cfg, exiting!')
return ProcessResult( return ProcessResult(status_code=-1, message='')
status_code=-1,
message='',
)
cfg = dict(nzb2media.CFG[section_name][usercat]) cfg = dict(nzb2media.CFG[section_name][usercat])
extract = int(cfg.get('extract', 0)) extract = int(cfg.get('extract', 0))
try: try:
if int(cfg.get('remote_path')) and not nzb2media.REMOTE_PATHS: if int(cfg.get('remote_path')) and not nzb2media.REMOTE_PATHS:
log.error(f'Remote Path is enabled for {section_name}:{input_category} but no Network mount points are defined. Please check your autoProcessMedia.cfg, exiting!') log.error(f'Remote Path is enabled for {section_name}:{input_category} but no Network mount points are defined. Please check your autoProcessMedia.cfg, exiting!')
return ProcessResult( return ProcessResult(status_code=-1, message='')
status_code=-1,
message='',
)
except Exception: except Exception:
remote_path = cfg.get('remote_path') remote_path = cfg.get('remote_path')
log.error(f'Remote Path {remote_path} is not valid for {section_name}:{input_category} Please set this to either 0 to disable or 1 to enable!') log.error(f'Remote Path {remote_path} is not valid for {section_name}:{input_category} Please set this to either 0 to disable or 1 to enable!')
@ -111,47 +79,17 @@ def process(
extract_files(input_directory) extract_files(input_directory)
log.info(f'Calling {section_name}:{input_category} to post-process:{input_name}') log.info(f'Calling {section_name}:{input_category} to post-process:{input_name}')
if section_name == 'UserScript': if section_name == 'UserScript':
result = external_script( result = external_script(input_directory, input_name, input_category, section[usercat])
input_directory, input_name, input_category, section[usercat],
)
else: else:
process_map = { process_map = {'CouchPotato': movies.process, 'Radarr': movies.process, 'Watcher3': movies.process, 'SickBeard': tv.process, 'SiCKRAGE': tv.process, 'NzbDrone': tv.process, 'Sonarr': tv.process, 'LazyLibrarian': books.process, 'HeadPhones': music.process, 'Lidarr': music.process, 'Mylar': comics.process, 'Gamez': games.process}
'CouchPotato': movies.process,
'Radarr': movies.process,
'Watcher3': movies.process,
'SickBeard': tv.process,
'SiCKRAGE': tv.process,
'NzbDrone': tv.process,
'Sonarr': tv.process,
'LazyLibrarian': books.process,
'HeadPhones': music.process,
'Lidarr': music.process,
'Mylar': comics.process,
'Gamez': games.process,
}
processor = process_map[section_name] processor = process_map[section_name]
result = processor( result = processor(section=section_name, dir_name=input_directory, input_name=input_name, status=status, client_agent=client_agent, download_id=download_id, input_category=input_category, failure_link=failure_link)
section=section_name,
dir_name=input_directory,
input_name=input_name,
status=status,
client_agent=client_agent,
download_id=download_id,
input_category=input_category,
failure_link=failure_link,
)
plex_update(input_category) plex_update(input_category)
if result.status_code == 0: if result.status_code == 0:
if client_agent != 'manual': if client_agent != 'manual':
# update download status in our DB # update download status in our DB
update_download_info_status(input_name, 1) update_download_info_status(input_name, 1)
if section_name not in [ if section_name not in ['UserScript', 'NzbDrone', 'Sonarr', 'Radarr', 'Lidarr']:
'UserScript',
'NzbDrone',
'Sonarr',
'Radarr',
'Lidarr',
]:
# cleanup our processing folders of any misc unwanted files and # cleanup our processing folders of any misc unwanted files and
# empty directories # empty directories
clean_dir(input_directory, section_name, input_category) clean_dir(input_directory, section_name, input_category)

View file

@ -13,19 +13,12 @@ log.addHandler(logging.NullHandler())
def parse_download_id(): def parse_download_id():
"""Parse nzbget download_id from environment.""" """Parse nzbget download_id from environment."""
download_id_keys = [ download_id_keys = ['NZBPR_COUCHPOTATO', 'NZBPR_DRONE', 'NZBPR_SONARR', 'NZBPR_RADARR', 'NZBPR_LIDARR']
'NZBPR_COUCHPOTATO',
'NZBPR_DRONE',
'NZBPR_SONARR',
'NZBPR_RADARR',
'NZBPR_LIDARR',
]
for download_id_key in download_id_keys: for download_id_key in download_id_keys:
try: try:
return os.environ[download_id_key] return os.environ[download_id_key]
except KeyError: except KeyError:
pass pass
else:
return '' return ''
@ -46,7 +39,7 @@ def _parse_total_status():
def _parse_par_status(): def _parse_par_status():
"""Parse nzbget par status from environment.""" """Parse nzbget par status from environment."""
par_status = os.environ['NZBPP_PARSTATUS'] par_status = os.environ['NZBPP_PARSTATUS']
if par_status == '1' or par_status == '4': if par_status in {'1', '4'}:
log.warning('Par-repair failed, setting status \'failed\'') log.warning('Par-repair failed, setting status \'failed\'')
return 1 return 1
return 0 return 0
@ -102,12 +95,4 @@ def process():
status = parse_status() status = parse_status()
download_id = parse_download_id() download_id = parse_download_id()
failure_link = parse_failure_link() failure_link = parse_failure_link()
return nzb.process( return nzb.process(input_directory=os.environ['NZBPP_DIRECTORY'], input_name=os.environ['NZBPP_NZBNAME'], status=status, client_agent='nzbget', download_id=download_id, input_category=os.environ['NZBPP_CATEGORY'], failure_link=failure_link)
input_directory=os.environ['NZBPP_DIRECTORY'],
input_name=os.environ['NZBPP_NZBNAME'],
status=status,
client_agent='nzbget',
download_id=download_id,
input_category=os.environ['NZBPP_CATEGORY'],
failure_link=failure_link,
)

View file

@ -7,26 +7,17 @@ from nzb2media.processor import nzb
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
MINIMUM_ARGUMENTS = 8 MINIMUM_ARGUMENTS = 8
def process_script(): def process_script():
version = os.environ['SAB_VERSION'] version = os.environ['SAB_VERSION']
log.info(f'Script triggered from SABnzbd {version}.') log.info(f'Script triggered from SABnzbd {version}.')
return nzb.process( return nzb.process(input_directory=os.environ['SAB_COMPLETE_DIR'], input_name=os.environ['SAB_FINAL_NAME'], status=int(os.environ['SAB_PP_STATUS']), client_agent='sabnzbd', download_id=os.environ['SAB_NZO_ID'], input_category=os.environ['SAB_CAT'], failure_link=os.environ['SAB_FAILURE_URL'])
input_directory=os.environ['SAB_COMPLETE_DIR'],
input_name=os.environ['SAB_FINAL_NAME'],
status=int(os.environ['SAB_PP_STATUS']),
client_agent='sabnzbd',
download_id=os.environ['SAB_NZO_ID'],
input_category=os.environ['SAB_CAT'],
failure_link=os.environ['SAB_FAILURE_URL'],
)
def process(args): def process(args):
""" """Process job from SABnzb.
SABnzbd arguments: SABnzbd arguments:
1. The final directory of the job (full path) 1. The final directory of the job (full path)
2. The original name of the NZB file 2. The original name of the NZB file
@ -43,12 +34,4 @@ def process(args):
""" """
version = '0.7.17+' if len(args) > MINIMUM_ARGUMENTS else '' version = '0.7.17+' if len(args) > MINIMUM_ARGUMENTS else ''
log.info(f'Script triggered from SABnzbd {version}') log.info(f'Script triggered from SABnzbd {version}')
return nzb.process( return nzb.process(input_directory=args[1], input_name=args[2], status=int(args[7]), input_category=args[5], client_agent='sabnzbd', download_id='', failure_link=''.join(args[8:]))
input_directory=args[1],
input_name=args[2],
status=int(args[7]),
input_category=args[5],
client_agent='sabnzbd',
download_id='',
failure_link=''.join(args[8:]),
)

View file

@ -2,76 +2,24 @@ from __future__ import annotations
import logging import logging
import os import os
import platform
import re import re
import shlex import shlex
import subprocess import subprocess
from subprocess import DEVNULL
import nzb2media import nzb2media
from nzb2media.utils.files import list_media_files from nzb2media.utils.files import list_media_files
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
reverse_list = [r'\.\d{2}e\d{2}s\.', r'\.[pi]0801\.', r'\.p027\.', r'\.[pi]675\.', r'\.[pi]084\.', r'\.p063\.', r'\b[45]62[xh]\.', r'\.yarulb\.', r'\.vtd[hp]\.', r'\.ld[.-]?bew\.', r'\.pir.?(dov|dvd|bew|db|rb)\.', r'\brdvd\.', r'\.vts\.', r'\.reneercs\.', r'\.dcv\.', r'\b(pir|mac)dh\b', r'\.reporp\.', r'\.kcaper\.', r'\.lanretni\.', r'\b3ca\b', r'\.cstn\.']
reverse_list = [
r'\.\d{2}e\d{2}s\.',
r'\.[pi]0801\.',
r'\.p027\.',
r'\.[pi]675\.',
r'\.[pi]084\.',
r'\.p063\.',
r'\b[45]62[xh]\.',
r'\.yarulb\.',
r'\.vtd[hp]\.',
r'\.ld[.-]?bew\.',
r'\.pir.?(dov|dvd|bew|db|rb)\.',
r'\brdvd\.',
r'\.vts\.',
r'\.reneercs\.',
r'\.dcv\.',
r'\b(pir|mac)dh\b',
r'\.reporp\.',
r'\.kcaper\.',
r'\.lanretni\.',
r'\b3ca\b',
r'\.cstn\.',
]
reverse_pattern = re.compile('|'.join(reverse_list), flags=re.IGNORECASE) reverse_pattern = re.compile('|'.join(reverse_list), flags=re.IGNORECASE)
season_pattern = re.compile(r'(.*\.\d{2}e\d{2}s\.)(.*)', flags=re.IGNORECASE) season_pattern = re.compile(r'(.*\.\d{2}e\d{2}s\.)(.*)', flags=re.IGNORECASE)
word_pattern = re.compile(r'([^A-Z0-9]*[A-Z0-9]+)') word_pattern = re.compile(r'([^A-Z0-9]*[A-Z0-9]+)')
media_list = [ media_list = [r'\.s\d{2}e\d{2}\.', r'\.1080[pi]\.', r'\.720p\.', r'\.576[pi]', r'\.480[pi]\.', r'\.360p\.', r'\.[xh]26[45]\b', r'\.bluray\.', r'\.[hp]dtv\.', r'\.web[.-]?dl\.', r'\.(vod|dvd|web|bd|br).?rip\.', r'\.dvdr\b', r'\.stv\.', r'\.screener\.', r'\.vcd\.', r'\bhd(cam|rip)\b', r'\.proper\.', r'\.repack\.', r'\.internal\.', r'\bac3\b', r'\.ntsc\.', r'\.pal\.', r'\.secam\.', r'\bdivx\b', r'\bxvid\b']
r'\.s\d{2}e\d{2}\.',
r'\.1080[pi]\.',
r'\.720p\.',
r'\.576[pi]',
r'\.480[pi]\.',
r'\.360p\.',
r'\.[xh]26[45]\b',
r'\.bluray\.',
r'\.[hp]dtv\.',
r'\.web[.-]?dl\.',
r'\.(vod|dvd|web|bd|br).?rip\.',
r'\.dvdr\b',
r'\.stv\.',
r'\.screener\.',
r'\.vcd\.',
r'\bhd(cam|rip)\b',
r'\.proper\.',
r'\.repack\.',
r'\.internal\.',
r'\bac3\b',
r'\.ntsc\.',
r'\.pal\.',
r'\.secam\.',
r'\bdivx\b',
r'\bxvid\b',
]
media_pattern = re.compile('|'.join(media_list), flags=re.IGNORECASE) media_pattern = re.compile('|'.join(media_list), flags=re.IGNORECASE)
garbage_name = re.compile(r'^[a-zA-Z0-9]*$') garbage_name = re.compile(r'^[a-zA-Z0-9]*$')
char_replace = [ char_replace = [[r'(\w)1\.(\w)', r'\1i\2']]
[r'(\w)1\.(\w)', r'\1i\2'],
]
def process_all_exceptions(name, dirname): def process_all_exceptions(name, dirname):
@ -112,11 +60,7 @@ def strip_groups(filename):
def rename_file(filename, newfile_path): def rename_file(filename, newfile_path):
if os.path.isfile(newfile_path): if os.path.isfile(newfile_path):
newfile_path = ( newfile_path = os.path.splitext(newfile_path)[0] + '.NTM' + os.path.splitext(newfile_path)[1]
os.path.splitext(newfile_path)[0]
+ '.NTM'
+ os.path.splitext(newfile_path)[1]
)
log.error(f'Replacing file name {filename} with download name {newfile_path}') log.error(f'Replacing file name {filename} with download name {newfile_path}')
try: try:
os.rename(filename, newfile_path) os.rename(filename, newfile_path)
@ -126,10 +70,7 @@ def rename_file(filename, newfile_path):
def replace_filename(filename, dirname, name): def replace_filename(filename, dirname, name):
head, file_extension = os.path.splitext(os.path.basename(filename)) head, file_extension = os.path.splitext(os.path.basename(filename))
if ( if media_pattern.search(os.path.basename(dirname).replace(' ', '.')) is not None:
media_pattern.search(os.path.basename(dirname).replace(' ', '.'))
is not None
):
newname = os.path.basename(dirname).replace(' ', '.') newname = os.path.basename(dirname).replace(' ', '.')
log.debug(f'Replacing file name {head} with directory name {newname}') log.debug(f'Replacing file name {head} with directory name {newname}')
elif media_pattern.search(name.replace(' ', '.').lower()) is not None: elif media_pattern.search(name.replace(' ', '.').lower()) is not None:
@ -143,21 +84,21 @@ def replace_filename(filename, dirname, name):
return newfile_path return newfile_path
def reverse_filename(filename, dirname, name): def reverse_filename(filename, dirname):
head, file_extension = os.path.splitext(os.path.basename(filename)) head, file_extension = os.path.splitext(os.path.basename(filename))
na_parts = season_pattern.search(head) na_parts = season_pattern.search(head)
if na_parts is not None: if na_parts is not None:
word_p = word_pattern.findall(na_parts.group(2)) match = word_pattern.findall(na_parts.group(2))
if word_p: if match:
new_words = '' new_words = ''
for wp in word_p: for group in match:
if wp[0] == '.': if group[0] == '.':
new_words += '.' new_words += '.'
new_words += re.sub(r'\W', '', wp) new_words += re.sub(r'\W', '', group)
else: else:
new_words = na_parts.group(2) new_words = na_parts.group(2)
for cr in char_replace: for each_char in char_replace:
new_words = re.sub(cr[0], cr[1], new_words) new_words = re.sub(each_char[0], each_char[1], new_words)
newname = new_words[::-1] + na_parts.group(1)[::-1] newname = new_words[::-1] + na_parts.group(1)[::-1]
else: else:
newname = head[::-1].title() newname = head[::-1].title()
@ -177,7 +118,8 @@ def rename_script(dirname):
dirname = directory dirname = directory
break break
if rename_file: if rename_file:
rename_lines = [line.strip() for line in open(rename_file)] with open(rename_file, encoding='utf-8') as fin:
rename_lines = [line.strip() for line in fin]
for line in rename_lines: for line in rename_lines:
if re.search('^(mv|Move)', line, re.IGNORECASE): if re.search('^(mv|Move)', line, re.IGNORECASE):
cmd = shlex.split(line)[1:] cmd = shlex.split(line)[1:]
@ -185,9 +127,7 @@ def rename_script(dirname):
continue continue
if len(cmd) == 2 and os.path.isfile(os.path.join(dirname, cmd[0])): if len(cmd) == 2 and os.path.isfile(os.path.join(dirname, cmd[0])):
orig = os.path.join(dirname, cmd[0]) orig = os.path.join(dirname, cmd[0])
dest = os.path.join( dest = os.path.join(dirname, cmd[1].split('\\')[-1].split('/')[-1])
dirname, cmd[1].split('\\')[-1].split('/')[-1],
)
if os.path.isfile(dest): if os.path.isfile(dest):
continue continue
log.debug(f'Renaming file {orig} to {dest}') log.debug(f'Renaming file {orig} to {dest}')
@ -212,10 +152,6 @@ def par2(dirname):
if nzb2media.PAR2CMD and parfile: if nzb2media.PAR2CMD and parfile:
pwd = os.getcwd() # Get our Present Working Directory pwd = os.getcwd() # Get our Present Working Directory
os.chdir(dirname) # set directory to run par on. os.chdir(dirname) # set directory to run par on.
if platform.system() == 'Windows':
bitbucket = open('NUL')
else:
bitbucket = open('/dev/null')
log.info(f'Running par2 on file {parfile}.') log.info(f'Running par2 on file {parfile}.')
command = [nzb2media.PAR2CMD, 'r', parfile, '*'] command = [nzb2media.PAR2CMD, 'r', parfile, '*']
cmd = '' cmd = ''
@ -223,9 +159,7 @@ def par2(dirname):
cmd = f'{cmd} {item}' cmd = f'{cmd} {item}'
log.debug(f'calling command:{cmd}') log.debug(f'calling command:{cmd}')
try: try:
proc = subprocess.Popen( with subprocess.Popen(command, stdout=DEVNULL, stderr=DEVNULL) as proc:
command, stdout=bitbucket, stderr=bitbucket,
)
proc.communicate() proc.communicate()
result = proc.returncode result = proc.returncode
except Exception: except Exception:
@ -233,7 +167,6 @@ def par2(dirname):
if result == 0: if result == 0:
log.info('par2 file processing succeeded') log.info('par2 file processing succeeded')
os.chdir(pwd) os.chdir(pwd)
bitbucket.close()
# dict for custom groups # dict for custom groups

114
nzb2media/tool.py Normal file
View file

@ -0,0 +1,114 @@
from __future__ import annotations
import itertools
import logging
import os
import pathlib
import shutil
import typing
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
def in_path(name: str) -> pathlib.Path | None:
"""Find tool if its on the system loc."""
log.debug(f'Searching for {name} on system path')
path = shutil.which(name)
if not path:
return None
return pathlib.Path(path)
def at_location(root: pathlib.Path, name: str) -> pathlib.Path | None:
"""Return tool if its at given loc."""
log.debug(f'Searching for {name} at {root}')
if not name:
raise ValueError('name is required')
path = root / name
if path.exists() or os.access(path, os.X_OK):
return path
return None
def find(root: pathlib.Path | None, *names) -> pathlib.Path | None:
"""Try to find a tool.
Look in target location first, then system path,
and finally check the current working directory.
"""
if not names:
raise ValueError('At least one name is required.')
# look in target location first
if root:
found_at_location: typing.Iterable[pathlib.Path | None] = (at_location(root, name) for name in names)
else:
found_at_location = []
# look on system path second
found_on_path = (in_path(name) for name in names)
found = itertools.chain(found_at_location, found_on_path)
for path in found:
if path is not None:
log.info(f'Found at {path}')
return path
# finally check current working directory
cwd = pathlib.Path.cwd()
log.debug(f'Falling back on current working directory: {cwd}')
found_in_working_directory = (at_location(cwd, name) for name in names)
for path in found_in_working_directory:
if path is not None:
log.info(f'Found {path}')
return path
return None
def find_transcoder(root: pathlib.Path | None = None) -> pathlib.Path | None:
"""Find a tool for transcoding."""
log.info('Searching for transcoding tool.')
names = ('ffmpeg', 'avconv')
found = find(root, *names)
if not found:
log.debug(f'Failed to locate any of the following: {names}')
log.warning('Transcoding disabled!')
log.warning('Install ffmpeg with x264 support to enable this feature.')
return found
def find_video_corruption_detector(root: pathlib.Path | None = None) -> pathlib.Path | None:
"""Find a tool for detecting video corruption."""
log.info('Searching for video corruption detection tool.')
names = ('ffprobe', 'avprobe')
found = find(root, *names)
if not found:
log.debug(f'Failed to locate any of the following: {names}')
log.warning('Video corruption detection disabled!')
log.warning('Install ffmpeg with x264 support to enable this feature.')
return found
def find_archive_repairer(root: pathlib.Path | None = None) -> pathlib.Path | None:
"""Find a tool for repairing and renaming archives."""
log.info('Searching for file repair and renaming tool.')
names = ('par2',)
found = find(root, *names)
if not found:
log.debug(f'Failed to locate any of the following: {names}')
log.warning('Archive repair and renaming disabled!')
log.warning('Install a parity archive repair tool to enable this feature.')
return found
def find_unzip(root: pathlib.Path | None = None) -> pathlib.Path | None:
"""Find a tool for unzipping archives."""
log.info('Searching for an unzipping tool.')
names = ('7z', '7zr', '7za')
found = find(root, *names)
if not found:
log.debug(f'Failed to locate any of the following: {names}')
log.warning('Transcoding of disk images and extraction zip files will not be possible!')
return found

View file

@ -6,17 +6,10 @@ from nzb2media.utils.torrent import create_torrent_class
def configure_torrents(config): def configure_torrents(config):
torrent_config = config['Torrent'] torrent_config = config['Torrent']
nzb2media.TORRENT_CLIENT_AGENT = torrent_config[ nzb2media.TORRENT_CLIENT_AGENT = torrent_config['clientAgent'] # utorrent | deluge | transmission | rtorrent | vuze | qbittorrent | synods | other
'clientAgent' nzb2media.OUTPUT_DIRECTORY = torrent_config['outputDirectory'] # /abs/path/to/complete/
] # utorrent | deluge | transmission | rtorrent | vuze | qbittorrent | synods | other nzb2media.TORRENT_DEFAULT_DIRECTORY = torrent_config['default_downloadDirectory']
nzb2media.OUTPUT_DIRECTORY = torrent_config[
'outputDirectory'
] # /abs/path/to/complete/
nzb2media.TORRENT_DEFAULT_DIRECTORY = torrent_config[
'default_downloadDirectory'
]
nzb2media.TORRENT_NO_MANUAL = int(torrent_config['no_manual'], 0) nzb2media.TORRENT_NO_MANUAL = int(torrent_config['no_manual'], 0)
configure_torrent_linking(torrent_config) configure_torrent_linking(torrent_config)
configure_flattening(torrent_config) configure_flattening(torrent_config)
configure_torrent_deletion(torrent_config) configure_torrent_deletion(torrent_config)
@ -41,9 +34,7 @@ def configure_flattening(config):
def configure_torrent_categories(config): def configure_torrent_categories(config):
nzb2media.CATEGORIES = config[ nzb2media.CATEGORIES = config['categories'] # music,music_videos,pictures,software
'categories'
] # music,music_videos,pictures,software
if isinstance(nzb2media.CATEGORIES, str): if isinstance(nzb2media.CATEGORIES, str):
nzb2media.CATEGORIES = nzb2media.CATEGORIES.split(',') nzb2media.CATEGORIES = nzb2media.CATEGORIES.split(',')
@ -62,9 +53,7 @@ def configure_torrent_deletion(config):
def configure_utorrent(config): def configure_utorrent(config):
nzb2media.UTORRENT_WEB_UI = config[ nzb2media.UTORRENT_WEB_UI = config['uTorrentWEBui'] # http://localhost:8090/gui/
'uTorrentWEBui'
] # http://localhost:8090/gui/
nzb2media.UTORRENT_USER = config['uTorrentUSR'] # mysecretusr nzb2media.UTORRENT_USER = config['uTorrentUSR'] # mysecretusr
nzb2media.UTORRENT_PASSWORD = config['uTorrentPWD'] # mysecretpwr nzb2media.UTORRENT_PASSWORD = config['uTorrentPWD'] # mysecretpwr

View file

@ -16,7 +16,6 @@ def configure_client():
port = nzb2media.DELUGE_PORT port = nzb2media.DELUGE_PORT
user = nzb2media.DELUGE_USER user = nzb2media.DELUGE_USER
password = nzb2media.DELUGE_PASSWORD password = nzb2media.DELUGE_PASSWORD
log.debug(f'Connecting to {agent}: http://{host}:{port}') log.debug(f'Connecting to {agent}: http://{host}:{port}')
client = DelugeRPCClient(host, port, user, password) client = DelugeRPCClient(host, port, user, password)
try: try:

View file

@ -16,7 +16,6 @@ def configure_client():
port = nzb2media.QBITTORRENT_PORT port = nzb2media.QBITTORRENT_PORT
user = nzb2media.QBITTORRENT_USER user = nzb2media.QBITTORRENT_USER
password = nzb2media.QBITTORRENT_PASSWORD password = nzb2media.QBITTORRENT_PASSWORD
log.debug(f'Connecting to {agent}: http://{host}:{port}') log.debug(f'Connecting to {agent}: http://{host}:{port}')
client = qBittorrentClient(f'http://{host}:{port}/') client = qBittorrentClient(f'http://{host}:{port}/')
try: try:

View file

@ -15,7 +15,6 @@ def configure_client():
port = nzb2media.SYNO_PORT port = nzb2media.SYNO_PORT
user = nzb2media.SYNO_USER user = nzb2media.SYNO_USER
password = nzb2media.SYNO_PASSWORD password = nzb2media.SYNO_PASSWORD
log.debug(f'Connecting to {agent}: http://{host}:{port}') log.debug(f'Connecting to {agent}: http://{host}:{port}')
try: try:
client = DownloadStation(host, port, user, password) client = DownloadStation(host, port, user, password)

View file

@ -16,7 +16,6 @@ def configure_client():
port = nzb2media.TRANSMISSION_PORT port = nzb2media.TRANSMISSION_PORT
user = nzb2media.TRANSMISSION_USER user = nzb2media.TRANSMISSION_USER
password = nzb2media.TRANSMISSION_PASSWORD password = nzb2media.TRANSMISSION_PASSWORD
log.debug(f'Connecting to {agent}: http://{host}:{port}') log.debug(f'Connecting to {agent}: http://{host}:{port}')
try: try:
client = TransmissionClient(host, port, user, password) client = TransmissionClient(host, port, user, password)

View file

@ -15,11 +15,11 @@ def configure_client():
web_ui = nzb2media.UTORRENT_WEB_UI web_ui = nzb2media.UTORRENT_WEB_UI
user = nzb2media.UTORRENT_USER user = nzb2media.UTORRENT_USER
password = nzb2media.UTORRENT_PASSWORD password = nzb2media.UTORRENT_PASSWORD
log.debug(f'Connecting to {agent}: {web_ui}') log.debug(f'Connecting to {agent}: {web_ui}')
try: try:
client = UTorrentClient(web_ui, user, password) client = UTorrentClient(web_ui, user, password)
except Exception: except Exception:
log.error('Failed to connect to uTorrent') log.error('Failed to connect to uTorrent')
return None
else: else:
return client return client

File diff suppressed because it is too large Load diff

View file

@ -18,27 +18,17 @@ log.addHandler(logging.NullHandler())
def external_script(output_destination, torrent_name, torrent_label, settings): def external_script(output_destination, torrent_name, torrent_label, settings):
final_result = 0 # start at 0. final_result = 0 # start at 0.
num_files = 0 num_files = 0
nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = settings.get( nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = settings.get('user_script_mediaExtensions', '')
'user_script_mediaExtensions', '',
)
try: try:
if isinstance(nzb2media.USER_SCRIPT_MEDIAEXTENSIONS, str): if isinstance(nzb2media.USER_SCRIPT_MEDIAEXTENSIONS, str):
nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = ( nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = nzb2media.USER_SCRIPT_MEDIAEXTENSIONS.lower().split(',')
nzb2media.USER_SCRIPT_MEDIAEXTENSIONS.lower().split(',')
)
except Exception: except Exception:
log.error('user_script_mediaExtensions could not be set') log.error('user_script_mediaExtensions could not be set')
nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = [] nzb2media.USER_SCRIPT_MEDIAEXTENSIONS = []
nzb2media.USER_SCRIPT = settings.get('user_script_path', '') nzb2media.USER_SCRIPT = settings.get('user_script_path', '')
if not nzb2media.USER_SCRIPT or nzb2media.USER_SCRIPT == 'None': if not nzb2media.USER_SCRIPT or nzb2media.USER_SCRIPT == 'None':
# do nothing and return success. This allows the user an option to Link files only and not run a script. # do nothing and return success. This allows the user an option to Link files only and not run a script.
return ProcessResult( return ProcessResult(status_code=0, message='No user script defined')
status_code=0,
message='No user script defined',
)
nzb2media.USER_SCRIPT_PARAM = settings.get('user_script_param', '') nzb2media.USER_SCRIPT_PARAM = settings.get('user_script_param', '')
try: try:
if isinstance(nzb2media.USER_SCRIPT_PARAM, str): if isinstance(nzb2media.USER_SCRIPT_PARAM, str):
@ -46,115 +36,81 @@ def external_script(output_destination, torrent_name, torrent_label, settings):
except Exception: except Exception:
log.error('user_script_params could not be set') log.error('user_script_params could not be set')
nzb2media.USER_SCRIPT_PARAM = [] nzb2media.USER_SCRIPT_PARAM = []
nzb2media.USER_SCRIPT_SUCCESSCODES = settings.get('user_script_successCodes', 0) nzb2media.USER_SCRIPT_SUCCESSCODES = settings.get('user_script_successCodes', 0)
try: try:
if isinstance(nzb2media.USER_SCRIPT_SUCCESSCODES, str): if isinstance(nzb2media.USER_SCRIPT_SUCCESSCODES, str):
nzb2media.USER_SCRIPT_SUCCESSCODES = ( nzb2media.USER_SCRIPT_SUCCESSCODES = nzb2media.USER_SCRIPT_SUCCESSCODES.split(',')
nzb2media.USER_SCRIPT_SUCCESSCODES.split(',')
)
except Exception: except Exception:
log.error('user_script_successCodes could not be set') log.error('user_script_successCodes could not be set')
nzb2media.USER_SCRIPT_SUCCESSCODES = 0 nzb2media.USER_SCRIPT_SUCCESSCODES = 0
nzb2media.USER_SCRIPT_CLEAN = int(settings.get('user_script_clean', 1)) nzb2media.USER_SCRIPT_CLEAN = int(settings.get('user_script_clean', 1))
nzb2media.USER_SCRIPT_RUNONCE = int(settings.get('user_script_runOnce', 1)) nzb2media.USER_SCRIPT_RUNONCE = int(settings.get('user_script_runOnce', 1))
if nzb2media.CHECK_MEDIA: if nzb2media.CHECK_MEDIA:
for video in list_media_files( for video in list_media_files(output_destination, media=True, audio=False, meta=False, archives=False):
output_destination,
media=True,
audio=False,
meta=False,
archives=False,
):
if transcoder.is_video_good(video, 0): if transcoder.is_video_good(video, 0):
import_subs(video) import_subs(video)
else: else:
log.info(f'Corrupt video file found {video}. Deleting.') log.info(f'Corrupt video file found {video}. Deleting.')
os.unlink(video) os.unlink(video)
for dirpath, _, filenames in os.walk(output_destination): for dirpath, _, filenames in os.walk(output_destination):
for file in filenames: for file in filenames:
file_path = nzb2media.os.path.join(dirpath, file) file_path = nzb2media.os.path.join(dirpath, file)
file_name, file_extension = os.path.splitext(file) file_name, file_extension = os.path.splitext(file)
log.debug(f'Checking file {file} to see if this should be processed.') log.debug(f'Checking file {file} to see if this should be processed.')
if file_extension in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS or 'all' in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS:
if (
file_extension in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS
or 'all' in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS
):
num_files += 1 num_files += 1
if ( if nzb2media.USER_SCRIPT_RUNONCE == 1 and num_files > 1: # we have already run once, so just continue to get number of files.
nzb2media.USER_SCRIPT_RUNONCE == 1 and num_files > 1
): # we have already run once, so just continue to get number of files.
continue continue
command = [nzb2media.USER_SCRIPT] command = [nzb2media.USER_SCRIPT]
for param in nzb2media.USER_SCRIPT_PARAM: for param in nzb2media.USER_SCRIPT_PARAM:
if param == 'FN': if param == 'FN':
command.append(f'{file}') command.append(f'{file}')
continue continue
elif param == 'FP': if param == 'FP':
command.append(f'{file_path}') command.append(f'{file_path}')
continue continue
elif param == 'TN': if param == 'TN':
command.append(f'{torrent_name}') command.append(f'{torrent_name}')
continue continue
elif param == 'TL': if param == 'TL':
command.append(f'{torrent_label}') command.append(f'{torrent_label}')
continue continue
elif param == 'DN': if param == 'DN':
if nzb2media.USER_SCRIPT_RUNONCE == 1: if nzb2media.USER_SCRIPT_RUNONCE == 1:
command.append(f'{output_destination}') command.append(f'{output_destination}')
else: else:
command.append(f'{dirpath}') command.append(f'{dirpath}')
continue continue
else:
command.append(param) command.append(param)
continue
cmd = '' cmd = ''
for item in command: for item in command:
cmd = f'{cmd} {item}' cmd = f'{cmd} {item}'
log.info(f'Running script {cmd} on file {file_path}.') log.info(f'Running script {cmd} on file {file_path}.')
try: try:
p = Popen(command) with Popen(command) as proc:
res = p.wait() res = proc.wait()
if ( except Exception:
str(res) in nzb2media.USER_SCRIPT_SUCCESSCODES log.error(f'UserScript {command[0]} has failed')
): # Linux returns 0 for successful. result = 1
else:
if str(res) in nzb2media.USER_SCRIPT_SUCCESSCODES:
# Linux returns 0 for successful.
log.info(f'UserScript {command[0]} was successfull') log.info(f'UserScript {command[0]} was successfull')
result = 0 result = 0
else: else:
log.error(f'UserScript {command[0]} has failed with return code: {res}') log.error(f'UserScript {command[0]} has failed with return code: {res}')
log.info(f'If the UserScript completed successfully you should add {res} to the user_script_successCodes') log.info(f'If the UserScript completed successfully you should add {res} to the user_script_successCodes')
result = int(1) result = 1
except Exception:
log.error(f'UserScript {command[0]} has failed')
result = int(1)
final_result += result final_result += result
num_files_new = 0 num_files_new = 0
for _, _, filenames in os.walk(output_destination): for _, _, filenames in os.walk(output_destination):
for file in filenames: for file in filenames:
file_name, file_extension = os.path.splitext(file) file_name, file_extension = os.path.splitext(file)
if file_extension in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS or nzb2media.USER_SCRIPT_MEDIAEXTENSIONS == 'ALL':
if (
file_extension in nzb2media.USER_SCRIPT_MEDIAEXTENSIONS
or nzb2media.USER_SCRIPT_MEDIAEXTENSIONS == 'ALL'
):
num_files_new += 1 num_files_new += 1
if nzb2media.USER_SCRIPT_CLEAN == int(1) and num_files_new == 0 and final_result == 0:
if (
nzb2media.USER_SCRIPT_CLEAN == int(1)
and num_files_new == 0
and final_result == 0
):
log.info(f'All files have been processed. Cleaning outputDirectory {output_destination}') log.info(f'All files have been processed. Cleaning outputDirectory {output_destination}')
remove_dir(output_destination) remove_dir(output_destination)
elif nzb2media.USER_SCRIPT_CLEAN == int(1) and num_files_new != 0: elif nzb2media.USER_SCRIPT_CLEAN == int(1) and num_files_new != 0:
log.info(f'{num_files} files were processed, but {num_files_new} still remain. outputDirectory will not be cleaned.') log.info(f'{num_files} files were processed, but {num_files_new} still remain. outputDirectory will not be cleaned.')
return ProcessResult( return ProcessResult(status_code=final_result, message='User Script Completed')
status_code=final_result,
message='User Script Completed',
)

View file

@ -15,9 +15,7 @@ log.addHandler(logging.NullHandler())
def flatten(output_destination): def flatten(output_destination):
return flatten_dir( return flatten_dir(output_destination, list_media_files(output_destination))
output_destination, list_media_files(output_destination),
)
def clean_dir(path, section, subsection): def clean_dir(path, section, subsection):
@ -25,9 +23,7 @@ def clean_dir(path, section, subsection):
min_size = int(cfg.get('minSize', 0)) min_size = int(cfg.get('minSize', 0))
delete_ignored = int(cfg.get('delete_ignored', 0)) delete_ignored = int(cfg.get('delete_ignored', 0))
try: try:
files = list_media_files( files = list_media_files(path, min_size=min_size, delete_ignored=delete_ignored)
path, min_size=min_size, delete_ignored=delete_ignored,
)
except Exception: except Exception:
files = [] files = []
return clean_directory(path, files) return clean_directory(path, files)
@ -35,72 +31,45 @@ def clean_dir(path, section, subsection):
def process_dir(path, link): def process_dir(path, link):
folders = [] folders = []
log.info(f'Searching {path} for mediafiles to post-process ...') log.info(f'Searching {path} for mediafiles to post-process ...')
dir_contents = os.listdir(path) dir_contents = os.listdir(path)
# search for single files and move them into their own folder for post-processing # search for single files and move them into their own folder for post-processing
# Generate list of sync files # Generate list of sync files
sync_files = ( sync_files = (item for item in dir_contents if os.path.splitext(item)[1] in {'.!sync', '.bts'})
item
for item in dir_contents
if os.path.splitext(item)[1] in ['.!sync', '.bts']
)
# Generate a list of file paths # Generate a list of file paths
filepaths = ( filepaths = (os.path.join(path, item) for item in dir_contents if item not in {'Thumbs.db', 'thumbs.db'})
os.path.join(path, item)
for item in dir_contents
if item not in ['Thumbs.db', 'thumbs.db']
)
# Generate a list of media files # Generate a list of media files
mediafiles = (item for item in filepaths if os.path.isfile(item)) mediafiles = (item for item in filepaths if os.path.isfile(item))
if not any(sync_files): if not any(sync_files):
for mediafile in mediafiles: for mediafile in mediafiles:
try: try:
move_file(mediafile, path, link) move_file(mediafile, path, link)
except Exception as error: except Exception as error:
log.error(f'Failed to move {os.path.split(mediafile)[1]} to its own directory: {error}') log.error(f'Failed to move {os.path.split(mediafile)[1]} to its own directory: {error}')
# removeEmptyFolders(path, removeRoot=False) # removeEmptyFolders(path, removeRoot=False)
# Generate all path contents # Generate all path contents
path_contents = (os.path.join(path, item) for item in os.listdir(path)) path_contents = (os.path.join(path, item) for item in os.listdir(path))
# Generate all directories from path contents # Generate all directories from path contents
directories = (path for path in path_contents if os.path.isdir(path)) directories = (path for path in path_contents if os.path.isdir(path))
for directory in directories: for directory in directories:
dir_contents = os.listdir(directory) dir_contents = os.listdir(directory)
sync_files = ( sync_files = (item for item in dir_contents if os.path.splitext(item)[1] in {'.!sync', '.bts'})
item
for item in dir_contents
if os.path.splitext(item)[1] in ['.!sync', '.bts']
)
if not any(dir_contents) or any(sync_files): if not any(dir_contents) or any(sync_files):
continue continue
folders.append(directory) folders.append(directory)
return folders return folders
def get_dirs(section, subsection, link='hard'): def get_dirs(section, subsection, link='hard'):
to_return = [] to_return = []
watch_directory = nzb2media.CFG[section][subsection]['watch_dir'] watch_directory = nzb2media.CFG[section][subsection]['watch_dir']
directory = os.path.join(watch_directory, subsection) directory = os.path.join(watch_directory, subsection)
if not os.path.exists(directory): if not os.path.exists(directory):
directory = watch_directory directory = watch_directory
try: try:
to_return.extend(process_dir(directory, link)) to_return.extend(process_dir(directory, link))
except Exception as error: except Exception as error:
log.error(f'Failed to add directories from {watch_directory} for post-processing: {error}') log.error(f'Failed to add directories from {watch_directory} for post-processing: {error}')
if nzb2media.USE_LINK == 'move': if nzb2media.USE_LINK == 'move':
try: try:
output_directory = os.path.join(nzb2media.OUTPUT_DIRECTORY, subsection) output_directory = os.path.join(nzb2media.OUTPUT_DIRECTORY, subsection)
@ -108,20 +77,12 @@ def get_dirs(section, subsection, link='hard'):
to_return.extend(process_dir(output_directory, link)) to_return.extend(process_dir(output_directory, link))
except Exception as error: except Exception as error:
log.error(f'Failed to add directories from {nzb2media.OUTPUT_DIRECTORY} for post-processing: {error}') log.error(f'Failed to add directories from {nzb2media.OUTPUT_DIRECTORY} for post-processing: {error}')
if not to_return: if not to_return:
log.debug(f'No directories identified in {section}:{subsection} for post-processing') log.debug(f'No directories identified in {section}:{subsection} for post-processing')
return list(set(to_return)) return list(set(to_return))
def create_url( def create_url(scheme: str, host: str, port: int | None = None, path: str = '', query: str = '') -> str:
scheme: str,
host: str,
port: int | None = None,
path: str = '',
query: str = '',
) -> str:
"""Create a url from its component parts.""" """Create a url from its component parts."""
netloc = host if port is None else f'{host}:{port}' netloc = host if port is None else f'{host}:{port}'
fragments = '' fragments = ''

View file

@ -7,7 +7,6 @@ from nzb2media import main_db
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
database = main_db.DBConnection() database = main_db.DBConnection()

View file

@ -15,37 +15,38 @@ def char_replace(name_in):
# UTF-8: 1st hex code 0xC2-0xC3 followed by a 2nd hex code 0xA1-0xFF # UTF-8: 1st hex code 0xC2-0xC3 followed by a 2nd hex code 0xA1-0xFF
# ISO-8859-15: 0xA6-0xFF # ISO-8859-15: 0xA6-0xFF
# The function will detect if Name contains a special character # The function will detect if Name contains a special character
# If there is special character, detects if it is a UTF-8, CP850 or ISO-8859-15 encoding # If there is special character, detects if it is a UTF-8, CP850 or
# ISO-8859-15 encoding
encoded = False encoded = False
encoding = None encoding = None
if isinstance(name_in, str): if isinstance(name_in, str):
return encoded, name_in return encoded, name_in
name = bytes(name_in) name = bytes(name_in)
for Idx in range(len(name)): for idx, character in enumerate(name):
# print('Trying to intuit the encoding') # print('Trying to intuit the encoding')
# /!\ detection is done 2char by 2char for UTF-8 special character # /!\ detection is done 2char by 2char for UTF-8 special character
if (len(name) != 1) & (Idx < (len(name) - 1)): try:
# Detect UTF-8 next_character = name[idx + 1]
if ((name[Idx] == 0xC2) | (name[Idx] == 0xC3)) & ( except IndexError:
(name[Idx + 1] >= 0xA0) & (name[Idx + 1] <= 0xFF)
):
encoding = 'utf-8'
break
# Detect CP850 # Detect CP850
elif (name[Idx] >= 0x80) & (name[Idx] <= 0xA5): if (character >= 0x80) & (character <= 0xA5):
encoding = 'cp850' encoding = 'cp850'
break break
# Detect ISO-8859-15 # Detect ISO-8859-15
elif (name[Idx] >= 0xA6) & (name[Idx] <= 0xFF): if (character >= 0xA6) & (character <= 0xFF):
encoding = 'iso-8859-15' encoding = 'iso-8859-15'
break break
else: else:
# Detect UTF-8
if ((character == 0xC2) | (character == 0xC3)) & ((next_character >= 0xA0) & (next_character <= 0xFF)):
encoding = 'utf-8'
break
# Detect CP850 # Detect CP850
if (name[Idx] >= 0x80) & (name[Idx] <= 0xA5): if (character >= 0x80) & (character <= 0xA5):
encoding = 'cp850' encoding = 'cp850'
break break
# Detect ISO-8859-15 # Detect ISO-8859-15
elif (name[Idx] >= 0xA6) & (name[Idx] <= 0xFF): if (character >= 0xA6) & (character <= 0xFF):
encoding = 'iso-8859-15' encoding = 'iso-8859-15'
break break
if encoding: if encoding:
@ -57,19 +58,13 @@ def char_replace(name_in):
def convert_to_ascii(input_name, dir_name): def convert_to_ascii(input_name, dir_name):
ascii_convert = int(nzb2media.CFG['ASCII']['convert']) ascii_convert = int(nzb2media.CFG['ASCII']['convert'])
if ( if ascii_convert == 0 or os.name == 'nt': # just return if we don't want to convert or on windows os and '\' is replaced!.
ascii_convert == 0 or os.name == 'nt'
): # just return if we don't want to convert or on windows os and '\' is replaced!.
return input_name, dir_name return input_name, dir_name
encoded, input_name = char_replace(input_name) encoded, input_name = char_replace(input_name)
directory, base = os.path.split(dir_name) directory, base = os.path.split(dir_name)
if not base: # ended with '/' if not base: # ended with '/'
directory, base = os.path.split(directory) directory, base = os.path.split(directory)
encoded, base2 = char_replace(base) encoded, base2 = char_replace(base)
if encoded: if encoded:
dir_name = os.path.join(directory, base2) dir_name = os.path.join(directory, base2)
@ -77,25 +72,16 @@ def convert_to_ascii(input_name, dir_name):
os.rename(os.path.join(directory, base), dir_name) os.rename(os.path.join(directory, base), dir_name)
if 'NZBOP_SCRIPTDIR' in os.environ: if 'NZBOP_SCRIPTDIR' in os.environ:
print(f'[NZB] DIRECTORY={dir_name}') print(f'[NZB] DIRECTORY={dir_name}')
for dirname, dirnames, _ in os.walk(dir_name, topdown=False): for dirname, dirnames, _ in os.walk(dir_name, topdown=False):
for subdirname in dirnames: for subdirname in dirnames:
encoded, subdirname2 = char_replace(subdirname) encoded, subdirname2 = char_replace(subdirname)
if encoded: if encoded:
log.info(f'Renaming directory to: {subdirname2}.') log.info(f'Renaming directory to: {subdirname2}.')
os.rename( os.rename(os.path.join(dirname, subdirname), os.path.join(dirname, subdirname2))
os.path.join(dirname, subdirname),
os.path.join(dirname, subdirname2),
)
for dirname, _, filenames in os.walk(dir_name): for dirname, _, filenames in os.walk(dir_name):
for filename in filenames: for filename in filenames:
encoded, filename2 = char_replace(filename) encoded, filename2 = char_replace(filename)
if encoded: if encoded:
log.info(f'Renaming file to: {filename2}.') log.info(f'Renaming file to: {filename2}.')
os.rename( os.rename(os.path.join(dirname, filename), os.path.join(dirname, filename2))
os.path.join(dirname, filename),
os.path.join(dirname, filename2),
)
return input_name, dir_name return input_name, dir_name

View file

@ -28,66 +28,47 @@ def move_file(filename, path, link):
file_ext = os.path.splitext(filename)[1] file_ext = os.path.splitext(filename)[1]
try: try:
if file_ext in nzb2media.AUDIO_CONTAINER: if file_ext in nzb2media.AUDIO_CONTAINER:
f = mediafile.MediaFile(filename) guess = mediafile.MediaFile(filename)
# get artist and album info # get artist and album info
artist = f.artist artist = guess.artist
album = f.album album = guess.album
# create new path # create new path
new_path = os.path.join( new_path = os.path.join(path, f'{sanitize_name(artist)} - {sanitize_name(album)}')
path, f'{sanitize_name(artist)} - {sanitize_name(album)}',
)
elif file_ext in nzb2media.MEDIA_CONTAINER: elif file_ext in nzb2media.MEDIA_CONTAINER:
f = guessit.guessit(filename) guess = guessit.guessit(filename)
# get title # get title
title = f.get('series') or f.get('title') title = guess.get('series') or guess.get('title')
if not title: if not title:
title = os.path.splitext(os.path.basename(filename))[0] title = os.path.splitext(os.path.basename(filename))[0]
new_path = os.path.join(path, sanitize_name(title)) new_path = os.path.join(path, sanitize_name(title))
except Exception as error: except Exception as error:
log.error(f'Exception parsing name for media file: {os.path.split(filename)[1]}: {error}') log.error(f'Exception parsing name for media file: {os.path.split(filename)[1]}: {error}')
if not new_path: if not new_path:
title = os.path.splitext(os.path.basename(filename))[0] title = os.path.splitext(os.path.basename(filename))[0]
new_path = os.path.join(path, sanitize_name(title)) new_path = os.path.join(path, sanitize_name(title))
# # Removed as encoding of directory no-longer required # # Removed as encoding of directory no-longer required
# try: # try:
# new_path = new_path.encode(nzb2media.SYS_ENCODING) # new_path = new_path.encode(nzb2media.SYS_ENCODING)
# except Exception: # except Exception:
# pass # pass
# Just fail-safe incase we already have afile with this clean-name (was actually a bug from earlier code, but let's be safe). # Just fail-safe incase we already have afile with this clean-name (was actually a bug from earlier code, but let's be safe).
if os.path.isfile(new_path): if os.path.isfile(new_path):
new_path2 = os.path.join( new_path2 = os.path.join(os.path.join(os.path.split(new_path)[0], 'new'), os.path.split(new_path)[1])
os.path.join(os.path.split(new_path)[0], 'new'),
os.path.split(new_path)[1],
)
new_path = new_path2 new_path = new_path2
# create new path if it does not exist # create new path if it does not exist
if not os.path.exists(new_path): if not os.path.exists(new_path):
make_dir(new_path) make_dir(new_path)
newfile = os.path.join(new_path, sanitize_name(os.path.split(filename)[1]))
newfile = os.path.join(
new_path, sanitize_name(os.path.split(filename)[1]),
)
try: try:
newfile = newfile.encode(nzb2media.SYS_ENCODING) newfile = newfile.encode(nzb2media.SYS_ENCODING)
except Exception: except Exception:
pass pass
# link file to its new path # link file to its new path
copy_link(filename, newfile, link) copy_link(filename, newfile, link)
def is_min_size(input_name, min_size): def is_min_size(input_name, min_size) -> bool:
file_name, file_ext = os.path.splitext(os.path.basename(input_name)) file_name, file_ext = os.path.splitext(os.path.basename(input_name))
# audio files we need to check directory size not file size # audio files we need to check directory size not file size
input_size = os.path.getsize(input_name) input_size = os.path.getsize(input_name)
if file_ext in nzb2media.AUDIO_CONTAINER: if file_ext in nzb2media.AUDIO_CONTAINER:
@ -96,10 +77,10 @@ def is_min_size(input_name, min_size):
except Exception: except Exception:
log.error(f'Failed to get file size for {input_name}') log.error(f'Failed to get file size for {input_name}')
return True return True
# Ignore files under a certain size # Ignore files under a certain size
if input_size > min_size * 1048576: if input_size > min_size * 1048576:
return True return True
return False
def is_archive_file(filename): def is_archive_file(filename):
@ -110,59 +91,27 @@ def is_archive_file(filename):
return False return False
def is_media_file( def is_media_file(mediafile, media=True, audio=True, meta=True, archives=True, other=False, otherext=None):
mediafile,
media=True,
audio=True,
meta=True,
archives=True,
other=False,
otherext=None,
):
if otherext is None: if otherext is None:
otherext = [] otherext = []
file_name, file_ext = os.path.splitext(mediafile) file_name, file_ext = os.path.splitext(mediafile)
try: try:
# ignore MAC OS's 'resource fork' files # ignore MAC OS's 'resource fork' files
if file_name.startswith('._'): if file_name.startswith('._'):
return False return False
except Exception: except Exception:
pass pass
return any([(media and file_ext.lower() in nzb2media.MEDIA_CONTAINER), (audio and file_ext.lower() in nzb2media.AUDIO_CONTAINER), (meta and file_ext.lower() in nzb2media.META_CONTAINER), (archives and is_archive_file(mediafile)), (other and (file_ext.lower() in otherext or 'all' in otherext))])
return any(
[
(media and file_ext.lower() in nzb2media.MEDIA_CONTAINER),
(audio and file_ext.lower() in nzb2media.AUDIO_CONTAINER),
(meta and file_ext.lower() in nzb2media.META_CONTAINER),
(archives and is_archive_file(mediafile)),
(other and (file_ext.lower() in otherext or 'all' in otherext)),
],
)
def list_media_files( def list_media_files(path, min_size=0, delete_ignored=0, media=True, audio=True, meta=True, archives=True, other=False, otherext=None):
path,
min_size=0,
delete_ignored=0,
media=True,
audio=True,
meta=True,
archives=True,
other=False,
otherext=None,
):
if otherext is None: if otherext is None:
otherext = [] otherext = []
files = [] files = []
if not os.path.isdir(path): if not os.path.isdir(path):
if os.path.isfile(path): # Single file downloads. if os.path.isfile(path): # Single file downloads.
cur_file = os.path.split(path)[1] cur_file = os.path.split(path)[1]
if is_media_file( if is_media_file(cur_file, media, audio, meta, archives, other, otherext):
cur_file, media, audio, meta, archives, other, otherext,
):
# Optionally ignore sample files # Optionally ignore sample files
if is_sample(path) or not is_min_size(path, min_size): if is_sample(path) or not is_min_size(path, min_size):
if delete_ignored == 1: if delete_ignored == 1:
@ -173,33 +122,15 @@ def list_media_files(
pass pass
else: else:
files.append(path) files.append(path)
return files return files
for cur_file in os.listdir(path): for cur_file in os.listdir(path):
full_cur_file = os.path.join(path, cur_file) full_cur_file = os.path.join(path, cur_file)
# if it's a folder do it recursively # if it's a folder do it recursively
if os.path.isdir(full_cur_file) and not cur_file.startswith('.'): if os.path.isdir(full_cur_file) and not cur_file.startswith('.'):
files += list_media_files( files += list_media_files(full_cur_file, min_size, delete_ignored, media, audio, meta, archives, other, otherext)
full_cur_file, elif is_media_file(cur_file, media, audio, meta, archives, other, otherext):
min_size,
delete_ignored,
media,
audio,
meta,
archives,
other,
otherext,
)
elif is_media_file(
cur_file, media, audio, meta, archives, other, otherext,
):
# Optionally ignore sample files # Optionally ignore sample files
if is_sample(full_cur_file) or not is_min_size( if is_sample(full_cur_file) or not is_min_size(full_cur_file, min_size):
full_cur_file, min_size,
):
if delete_ignored == 1: if delete_ignored == 1:
try: try:
os.unlink(full_cur_file) os.unlink(full_cur_file)
@ -207,51 +138,41 @@ def list_media_files(
except Exception: except Exception:
pass pass
continue continue
files.append(full_cur_file) files.append(full_cur_file)
return sorted(files, key=len) return sorted(files, key=len)
def extract_files(src, dst=None, keep_archive=None): def extract_files(src, dst=None, keep_archive=None):
extracted_folder = [] extracted_folder = []
extracted_archive = [] extracted_archive = []
for input_file in list_media_files(src, media=False, audio=False, meta=False, archives=True):
for inputFile in list_media_files( dir_path = os.path.dirname(input_file)
src, media=False, audio=False, meta=False, archives=True, full_file_name = os.path.basename(input_file)
):
dir_path = os.path.dirname(inputFile)
full_file_name = os.path.basename(inputFile)
archive_name = os.path.splitext(full_file_name)[0] archive_name = os.path.splitext(full_file_name)[0]
archive_name = re.sub(r'part[0-9]+', '', archive_name) archive_name = re.sub(r'part[0-9]+', '', archive_name)
if dir_path in extracted_folder and archive_name in extracted_archive: if dir_path in extracted_folder and archive_name in extracted_archive:
continue # no need to extract this, but keep going to look for other archives and sub directories. continue # no need to extract this, but keep going to look for other archives and sub directories.
try: try:
if extractor.extract(inputFile, dst or dir_path): if extractor.extract(input_file, dst or dir_path):
extracted_folder.append(dir_path) extracted_folder.append(dir_path)
extracted_archive.append(archive_name) extracted_archive.append(archive_name)
except Exception: except Exception:
log.error(f'Extraction failed for: {full_file_name}') log.error(f'Extraction failed for: {full_file_name}')
for folder in extracted_folder: for folder in extracted_folder:
for inputFile in list_media_files( for input_file in list_media_files(folder, media=False, audio=False, meta=False, archives=True):
folder, media=False, audio=False, meta=False, archives=True, full_file_name = os.path.basename(input_file)
):
full_file_name = os.path.basename(inputFile)
archive_name = os.path.splitext(full_file_name)[0] archive_name = os.path.splitext(full_file_name)[0]
archive_name = re.sub(r'part[0-9]+', '', archive_name) archive_name = re.sub(r'part[0-9]+', '', archive_name)
if archive_name not in extracted_archive or keep_archive: if archive_name not in extracted_archive or keep_archive:
continue # don't remove if we haven't extracted this archive, or if we want to preserve them. continue # don't remove if we haven't extracted this archive, or if we want to preserve them.
log.info(f'Removing extracted archive {full_file_name} from folder {folder} ...') log.info(f'Removing extracted archive {full_file_name} from folder {folder} ...')
try: try:
if not os.access(inputFile, os.W_OK): if not os.access(input_file, os.W_OK):
os.chmod(inputFile, stat.S_IWUSR) os.chmod(input_file, stat.S_IWUSR)
os.remove(inputFile) os.remove(input_file)
time.sleep(1) time.sleep(1)
except Exception as error: except Exception as error:
log.error(f'Unable to remove file {inputFile} due to: {error}') log.error(f'Unable to remove file {input_file} due to: {error}')
def backup_versioned_file(old_file, version): def backup_versioned_file(old_file, version):

View file

@ -13,33 +13,30 @@ log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
def find_imdbid(dir_name, input_name, omdb_api_key): def find_imdbid(dir_name, input_name, omdb_api_key) -> str:
imdbid = None imdbid = ''
log.info(f'Attemping imdbID lookup for {input_name}') log.info(f'Attemping imdbID lookup for {input_name}')
# find imdbid in dirName # find imdbid in dirName
log.info('Searching folder and file names for imdbID ...') log.info('Searching folder and file names for imdbID ...')
m = re.search(r'\b(tt\d{7,8})\b', dir_name + input_name) match = re.search(r'\b(tt\d{7,8})\b', dir_name + input_name)
if m: if match:
imdbid = m.group(1) imdbid = match.group(1)
log.info(f'Found imdbID [{imdbid}]') log.info(f'Found imdbID [{imdbid}]')
return imdbid return imdbid
if os.path.isdir(dir_name): if os.path.isdir(dir_name):
for file in os.listdir(dir_name): for file in os.listdir(dir_name):
m = re.search(r'\b(tt\d{7,8})\b', file) match = re.search(r'\b(tt\d{7,8})\b', file)
if m: if match:
imdbid = m.group(1) imdbid = match.group(1)
log.info(f'Found imdbID [{imdbid}] via file name') log.info(f'Found imdbID [{imdbid}] via file name')
return imdbid return imdbid
if 'NZBPR__DNZB_MOREINFO' in os.environ: if 'NZBPR__DNZB_MOREINFO' in os.environ:
dnzb_more_info = os.environ.get('NZBPR__DNZB_MOREINFO', '') dnzb_more_info = os.environ.get('NZBPR__DNZB_MOREINFO', '')
if dnzb_more_info != '': if dnzb_more_info != '':
regex = re.compile( regex = re.compile(r'^http://www.imdb.com/title/(tt[0-9]+)/$', re.IGNORECASE)
r'^http://www.imdb.com/title/(tt[0-9]+)/$', re.IGNORECASE, match = regex.match(dnzb_more_info)
) if match:
m = regex.match(dnzb_more_info) imdbid = match.group(1)
if m:
imdbid = m.group(1)
log.info(f'Found imdbID [{imdbid}] from DNZB-MoreInfo') log.info(f'Found imdbID [{imdbid}] from DNZB-MoreInfo')
return imdbid return imdbid
log.info('Searching IMDB for imdbID ...') log.info('Searching IMDB for imdbID ...')
@ -52,81 +49,57 @@ def find_imdbid(dir_name, input_name, omdb_api_key):
title = None title = None
if 'title' in guess: if 'title' in guess:
title = guess['title'] title = guess['title']
# Movie Year # Movie Year
year = None year = None
if 'year' in guess: if 'year' in guess:
year = guess['year'] year = guess['year']
url = 'http://www.omdbapi.com' url = 'http://www.omdbapi.com'
if not omdb_api_key: if not omdb_api_key:
log.info('Unable to determine imdbID: No api key provided for omdbapi.com.') log.info('Unable to determine imdbID: No api key provided for omdbapi.com.')
return return ''
log.debug(f'Opening URL: {url}') log.debug(f'Opening URL: {url}')
try: try:
r = requests.get( response = requests.get(url, params={'apikey': omdb_api_key, 'y': year, 't': title}, verify=False, timeout=(60, 300))
url,
params={'apikey': omdb_api_key, 'y': year, 't': title},
verify=False,
timeout=(60, 300),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error(f'Unable to open URL {url}') log.error(f'Unable to open URL {url}')
return return ''
try: try:
results = r.json() results = response.json()
except Exception: except Exception:
log.error('No json data returned from omdbapi.com') log.error('No json data returned from omdbapi.com')
try: try:
imdbid = results['imdbID'] imdbid = results['imdbID']
except Exception: except Exception:
log.error('No imdbID returned from omdbapi.com') log.error('No imdbID returned from omdbapi.com')
if imdbid: if imdbid:
log.info(f'Found imdbID [{imdbid}]') log.info(f'Found imdbID [{imdbid}]')
return imdbid return imdbid
log.warning(f'Unable to find a imdbID for {input_name}') log.warning(f'Unable to find a imdbID for {input_name}')
return imdbid return imdbid
def category_search( def category_search(input_directory, input_name, input_category, root, categories):
input_directory, input_name, input_category, root, categories,
):
tordir = False tordir = False
if input_directory is None: # =Nothing to process here. if input_directory is None: # =Nothing to process here.
return input_directory, input_name, input_category, root return input_directory, input_name, input_category, root
pathlist = os.path.normpath(input_directory).split(os.sep) pathlist = os.path.normpath(input_directory).split(os.sep)
if input_category and input_category in pathlist: if input_category and input_category in pathlist:
log.debug(f'SEARCH: Found the Category: {input_category} in directory structure') log.debug(f'SEARCH: Found the Category: {input_category} in directory structure')
elif input_category: elif input_category:
log.debug(f'SEARCH: Could not find the category: {input_category} in the directory structure') log.debug(f'SEARCH: Could not find the category: {input_category} in the directory structure')
else: else:
try: try:
input_category = list(set(pathlist) & set(categories))[ input_category = list(set(pathlist) & set(categories))[-1] # assume last match is most relevant category.
-1
] # assume last match is most relevant category.
log.debug(f'SEARCH: Found Category: {input_category} in directory structure') log.debug(f'SEARCH: Found Category: {input_category} in directory structure')
except IndexError: except IndexError:
input_category = '' input_category = ''
log.debug('SEARCH: Could not find a category in the directory structure') log.debug('SEARCH: Could not find a category in the directory structure')
if not os.path.isdir(input_directory) and os.path.isfile( if not os.path.isdir(input_directory) and os.path.isfile(input_directory):
input_directory, # If the input directory is a file
): # If the input directory is a file
if not input_name: if not input_name:
input_name = os.path.split(os.path.normpath(input_directory))[1] input_name = os.path.split(os.path.normpath(input_directory))[1]
return input_directory, input_name, input_category, root return input_directory, input_name, input_category, root
if input_category and os.path.isdir( if input_category and os.path.isdir(os.path.join(input_directory, input_category)):
os.path.join(input_directory, input_category),
):
log.info(f'SEARCH: Found category directory {input_category} in input directory directory {input_directory}') log.info(f'SEARCH: Found category directory {input_category} in input directory directory {input_directory}')
input_directory = os.path.join(input_directory, input_category) input_directory = os.path.join(input_directory, input_category)
log.info(f'SEARCH: Setting input_directory to {input_directory}') log.info(f'SEARCH: Setting input_directory to {input_directory}')
@ -135,53 +108,36 @@ def category_search(
input_directory = os.path.join(input_directory, input_name) input_directory = os.path.join(input_directory, input_name)
log.info(f'SEARCH: Setting input_directory to {input_directory}') log.info(f'SEARCH: Setting input_directory to {input_directory}')
tordir = True tordir = True
elif input_name and os.path.isdir( elif input_name and os.path.isdir(os.path.join(input_directory, sanitize_name(input_name))):
os.path.join(input_directory, sanitize_name(input_name)),
):
log.info(f'SEARCH: Found torrent directory {sanitize_name(input_name)} in input directory directory {input_directory}') log.info(f'SEARCH: Found torrent directory {sanitize_name(input_name)} in input directory directory {input_directory}')
input_directory = os.path.join( input_directory = os.path.join(input_directory, sanitize_name(input_name))
input_directory, sanitize_name(input_name),
)
log.info(f'SEARCH: Setting input_directory to {input_directory}') log.info(f'SEARCH: Setting input_directory to {input_directory}')
tordir = True tordir = True
elif input_name and os.path.isfile( elif input_name and os.path.isfile(os.path.join(input_directory, input_name)):
os.path.join(input_directory, input_name),
):
log.info(f'SEARCH: Found torrent file {input_name} in input directory directory {input_directory}') log.info(f'SEARCH: Found torrent file {input_name} in input directory directory {input_directory}')
input_directory = os.path.join(input_directory, input_name) input_directory = os.path.join(input_directory, input_name)
log.info(f'SEARCH: Setting input_directory to {input_directory}') log.info(f'SEARCH: Setting input_directory to {input_directory}')
tordir = True tordir = True
elif input_name and os.path.isfile( elif input_name and os.path.isfile(os.path.join(input_directory, sanitize_name(input_name))):
os.path.join(input_directory, sanitize_name(input_name)),
):
log.info(f'SEARCH: Found torrent file {sanitize_name(input_name)} in input directory directory {input_directory}') log.info(f'SEARCH: Found torrent file {sanitize_name(input_name)} in input directory directory {input_directory}')
input_directory = os.path.join( input_directory = os.path.join(input_directory, sanitize_name(input_name))
input_directory, sanitize_name(input_name),
)
log.info(f'SEARCH: Setting input_directory to {input_directory}') log.info(f'SEARCH: Setting input_directory to {input_directory}')
tordir = True tordir = True
elif input_name and os.path.isdir(input_directory): elif input_name and os.path.isdir(input_directory):
for file in os.listdir(input_directory): for file in os.listdir(input_directory):
if os.path.splitext(file)[0] in [ if os.path.splitext(file)[0] in [input_name, sanitize_name(input_name)]:
input_name,
sanitize_name(input_name),
]:
log.info(f'SEARCH: Found torrent file {file} in input directory directory {input_directory}') log.info(f'SEARCH: Found torrent file {file} in input directory directory {input_directory}')
input_directory = os.path.join(input_directory, file) input_directory = os.path.join(input_directory, file)
log.info(f'SEARCH: Setting input_directory to {input_directory}') log.info(f'SEARCH: Setting input_directory to {input_directory}')
input_name = file input_name = file
tordir = True tordir = True
break break
# This looks for the .cp(tt imdb id in the path.
imdbid = [ imdbid = [item for item in pathlist if '.cp(tt' in item]
item for item in pathlist if '.cp(tt' in item
] # This looks for the .cp(tt imdb id in the path.
if imdbid and '.cp(tt' not in input_name: if imdbid and '.cp(tt' not in input_name:
input_name = imdbid[ input_name = imdbid[0]
0 # This ensures the imdb id is preserved and passed to CP
] # This ensures the imdb id is preserved and passed to CP
tordir = True tordir = True
if input_category and not tordir: if input_category and not tordir:
try: try:
index = pathlist.index(input_category) index = pathlist.index(input_category)
@ -192,7 +148,6 @@ def category_search(
input_name = pathlist[index + 1] input_name = pathlist[index + 1]
except ValueError: except ValueError:
pass pass
if input_name and not tordir: if input_name and not tordir:
if input_name in pathlist or sanitize_name(input_name) in pathlist: if input_name in pathlist or sanitize_name(input_name) in pathlist:
log.info(f'SEARCH: Found torrent directory {input_name} in the directory structure') log.info(f'SEARCH: Found torrent directory {input_name} in the directory structure')
@ -201,9 +156,7 @@ def category_search(
root = 1 root = 1
if not tordir: if not tordir:
root = 2 root = 2
if root > 0: if root > 0:
log.info('SEARCH: Could not find a unique directory for this download. Assume a common directory.') log.info('SEARCH: Could not find a unique directory for this download. Assume a common directory.')
log.info('SEARCH: We will try and determine which files to process, individually') log.info('SEARCH: We will try and determine which files to process, individually')
return input_directory, input_name, input_category, root return input_directory, input_name, input_category, root

View file

@ -14,32 +14,26 @@ log.addHandler(logging.NullHandler())
try: try:
from jaraco.windows.filesystem import islink, readlink from jaraco.windows.filesystem import islink, readlink
except ImportError: except ImportError:
if os.name == 'nt': if os.name != 'nt':
raise
else:
from os.path import islink from os.path import islink
from os import readlink from os import readlink
else:
raise
def copy_link(src, target_link, use_link): def copy_link(src, target_link, use_link):
log.info(f'MEDIAFILE: [{os.path.basename(target_link)}]') log.info(f'MEDIAFILE: [{os.path.basename(target_link)}]')
log.info(f'SOURCE FOLDER: [{os.path.dirname(src)}]') log.info(f'SOURCE FOLDER: [{os.path.dirname(src)}]')
log.info(f'TARGET FOLDER: [{os.path.dirname(target_link)}]') log.info(f'TARGET FOLDER: [{os.path.dirname(target_link)}]')
if src != target_link and os.path.exists(target_link): if src != target_link and os.path.exists(target_link):
log.info('MEDIAFILE already exists in the TARGET folder, skipping ...') log.info('MEDIAFILE already exists in the TARGET folder, skipping ...')
return True return True
elif ( if src == target_link and os.path.isfile(target_link) and os.path.isfile(src):
src == target_link
and os.path.isfile(target_link)
and os.path.isfile(src)
):
log.info('SOURCE AND TARGET files are the same, skipping ...') log.info('SOURCE AND TARGET files are the same, skipping ...')
return True return True
elif src == os.path.dirname(target_link): if src == os.path.dirname(target_link):
log.info('SOURCE AND TARGET folders are the same, skipping ...') log.info('SOURCE AND TARGET folders are the same, skipping ...')
return True return True
make_dir(os.path.dirname(target_link)) make_dir(os.path.dirname(target_link))
try: try:
if use_link == 'dir': if use_link == 'dir':
@ -50,47 +44,41 @@ def copy_link(src, target_link, use_link):
log.info('Directory junction linking SOURCE FOLDER -> TARGET FOLDER') log.info('Directory junction linking SOURCE FOLDER -> TARGET FOLDER')
linktastic.dirlink(src, target_link) linktastic.dirlink(src, target_link)
return True return True
elif use_link == 'hard': if use_link == 'hard':
log.info('Hard linking SOURCE MEDIAFILE -> TARGET FOLDER') log.info('Hard linking SOURCE MEDIAFILE -> TARGET FOLDER')
linktastic.link(src, target_link) linktastic.link(src, target_link)
return True return True
elif use_link == 'sym': if use_link == 'sym':
log.info('Sym linking SOURCE MEDIAFILE -> TARGET FOLDER') log.info('Sym linking SOURCE MEDIAFILE -> TARGET FOLDER')
linktastic.symlink(src, target_link) linktastic.symlink(src, target_link)
return True return True
elif use_link == 'move-sym': if use_link == 'move-sym':
log.info('Sym linking SOURCE MEDIAFILE -> TARGET FOLDER') log.info('Sym linking SOURCE MEDIAFILE -> TARGET FOLDER')
shutil.move(src, target_link) shutil.move(src, target_link)
linktastic.symlink(target_link, src) linktastic.symlink(target_link, src)
return True return True
elif use_link == 'move': if use_link == 'move':
log.info('Moving SOURCE MEDIAFILE -> TARGET FOLDER') log.info('Moving SOURCE MEDIAFILE -> TARGET FOLDER')
shutil.move(src, target_link) shutil.move(src, target_link)
return True return True
except Exception as error: except Exception as error:
log.warning(f'Error: {error}, copying instead ... ') log.warning(f'Error: {error}, copying instead ... ')
log.info('Copying SOURCE MEDIAFILE -> TARGET FOLDER') log.info('Copying SOURCE MEDIAFILE -> TARGET FOLDER')
shutil.copy(src, target_link) shutil.copy(src, target_link)
return True return True
def replace_links(link, max_depth=10): def replace_links(link, max_depth=10):
link_depth = 0 link_depth = 0
target = link target = link
for attempt in range(0, max_depth): for attempt in range(0, max_depth):
if not islink(target): if not islink(target):
break break
target = readlink(target) target = readlink(target)
link_depth = attempt link_depth = attempt
if not link_depth: if not link_depth:
log.debug(f'{link} is not a link') log.debug(f'{link} is not a link')
elif link_depth > max_depth or ( elif link_depth > max_depth or (link_depth == max_depth and islink(target)):
link_depth == max_depth and islink(target)
):
log.warning(f'Exceeded maximum depth {max_depth} while following link {link}') log.warning(f'Exceeded maximum depth {max_depth} while following link {link}')
else: else:
log.info(f'Changing sym-link: {link} to point directly to file: {target}') log.info(f'Changing sym-link: {link} to point directly to file: {target}')

View file

@ -6,7 +6,6 @@ import re
def sanitize_name(name): def sanitize_name(name):
""" """
Remove bad chars from the filename. Remove bad chars from the filename.
>>> sanitize_name('a/b/c') >>> sanitize_name('a/b/c')
'a-b-c' 'a-b-c'
>>> sanitize_name('abc') >>> sanitize_name('abc')
@ -18,24 +17,20 @@ def sanitize_name(name):
""" """
name = re.sub(r'[\\/*]', '-', name) name = re.sub(r'[\\/*]', '-', name)
name = re.sub(r'[:\'<>|?]', '', name) name = re.sub(r'[:\'<>|?]', '', name)
# remove leading/trailing periods and spaces # remove leading/trailing periods and spaces
name = name.strip(' .') name = name.strip(' .')
return name return name
def clean_file_name(filename): def clean_file_name(filename):
""" """
Clean up nzb name by removing any . and _ characters and trailing hyphens. Clean up nzb name by removing any . and _ characters and trailing hyphens.
Is basically equivalent to replacing all _ and . with a Is basically equivalent to replacing all _ and . with a
space, but handles decimal numbers in string, for example: space, but handles decimal numbers in string, for example:
""" """
filename = re.sub(r'(\D)\.(?!\s)(\D)', r'\1 \2', filename) filename = re.sub(r'(\D)\.(?!\s)(\D)', r'\1 \2', filename)
filename = re.sub( # if it ends in a year then don't keep the dot
r'(\d)\.(\d{4})', r'\1 \2', filename, filename = re.sub(r'(\d)\.(\d{4})', r'\1 \2', filename)
) # if it ends in a year then don't keep the dot
filename = re.sub(r'(\D)\.(?!\s)', r'\1 ', filename) filename = re.sub(r'(\D)\.(?!\s)', r'\1 ', filename)
filename = re.sub(r'\.(?!\s)(\D)', r' \1', filename) filename = re.sub(r'\.(?!\s)(\D)', r' \1', filename)
filename = filename.replace('_', ' ') filename = filename.replace('_', ' ')
@ -44,7 +39,8 @@ def clean_file_name(filename):
return filename.strip() return filename.strip()
def is_sample(input_name): def is_sample(input_name) -> bool:
# Ignore 'sample' in files # Ignore 'sample' in files
if re.search('(^|[\\W_])sample\\d*[\\W_]', input_name.lower()): if re.search('(^|[\\W_])sample\\d*[\\W_]', input_name.lower()):
return True return True
return False

View file

@ -26,12 +26,10 @@ def wake_on_lan(ethernet_address):
"""Send a WakeOnLan request.""" """Send a WakeOnLan request."""
# Create the WoL magic packet # Create the WoL magic packet
magic_packet = make_wake_on_lan_packet(ethernet_address) magic_packet = make_wake_on_lan_packet(ethernet_address)
# ...and send it to the broadcast address using UDP # ...and send it to the broadcast address using UDP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as connection: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as connection:
connection.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) connection.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
connection.sendto(magic_packet, ('<broadcast>', 9)) connection.sendto(magic_packet, ('<broadcast>', 9))
log.info(f'WakeOnLan sent for mac: {ethernet_address}') log.info(f'WakeOnLan sent for mac: {ethernet_address}')
@ -52,9 +50,7 @@ def wake_up():
port = int(wol['port']) port = int(wol['port'])
mac = wol['mac'] mac = wol['mac']
max_attempts = 4 max_attempts = 4
log.info('Trying to wake On lan.') log.info('Trying to wake On lan.')
for attempt in range(0, max_attempts): for attempt in range(0, max_attempts):
log.info(f'Attempt {attempt + 1} of {max_attempts}') log.info(f'Attempt {attempt + 1} of {max_attempts}')
if test_connection(host, port) == 'Up': if test_connection(host, port) == 'Up':
@ -66,7 +62,6 @@ def wake_up():
if test_connection(host, port) == 'Down': # final check. if test_connection(host, port) == 'Down': # final check.
msg = 'System with mac: {0} has not woken after {1} attempts.' msg = 'System with mac: {0} has not woken after {1} attempts.'
log.warning(msg.format(mac, max_attempts)) log.warning(msg.format(mac, max_attempts))
log.info('Continuing with the rest of the script.') log.info('Continuing with the rest of the script.')
@ -108,21 +103,13 @@ def find_download(client_agent, download_id):
else: else:
base_url = f'http://{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api' base_url = f'http://{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api'
url = base_url url = base_url
params = { params = {'apikey': nzb2media.SABNZBD_APIKEY, 'mode': 'get_files', 'output': 'json', 'value': download_id}
'apikey': nzb2media.SABNZBD_APIKEY,
'mode': 'get_files',
'output': 'json',
'value': download_id,
}
try: try:
r = requests.get( response = requests.get(url, params=params, verify=False, timeout=(30, 120))
url, params=params, verify=False, timeout=(30, 120),
)
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to open URL') log.error('Unable to open URL')
return False # failure return False # failure
result = response.json()
result = r.json()
if result['files']: if result['files']:
return True return True
return False return False

View file

@ -20,42 +20,28 @@ def get_nzoid(input_name):
else: else:
base_url = f'http://{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api' base_url = f'http://{nzb2media.SABNZBD_HOST}:{nzb2media.SABNZBD_PORT}/api'
url = base_url url = base_url
params = { params = {'apikey': nzb2media.SABNZBD_APIKEY, 'mode': 'queue', 'output': 'json'}
'apikey': nzb2media.SABNZBD_APIKEY,
'mode': 'queue',
'output': 'json',
}
try: try:
r = requests.get(url, params=params, verify=False, timeout=(30, 120)) response = requests.get(url, params=params, verify=False, timeout=(30, 120))
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to open URL') log.error('Unable to open URL')
return nzoid # failure return nzoid # failure
try: try:
result = r.json() result = response.json()
clean_name = os.path.splitext(os.path.split(input_name)[1])[0] clean_name = os.path.splitext(os.path.split(input_name)[1])[0]
slots.extend( slots.extend([(slot['nzo_id'], slot['filename']) for slot in result['queue']['slots']])
[
(slot['nzo_id'], slot['filename'])
for slot in result['queue']['slots']
],
)
except Exception: except Exception:
log.warning('Data from SABnzbd queue could not be parsed') log.warning('Data from SABnzbd queue could not be parsed')
params['mode'] = 'history' params['mode'] = 'history'
try: try:
r = requests.get(url, params=params, verify=False, timeout=(30, 120)) response = requests.get(url, params=params, verify=False, timeout=(30, 120))
except requests.ConnectionError: except requests.ConnectionError:
log.error('Unable to open URL') log.error('Unable to open URL')
return nzoid # failure return nzoid # failure
try: try:
result = r.json() result = response.json()
clean_name = os.path.splitext(os.path.split(input_name)[1])[0] clean_name = os.path.splitext(os.path.split(input_name)[1])[0]
slots.extend( slots.extend([(slot['nzo_id'], slot['name']) for slot in result['history']['slots']])
[
(slot['nzo_id'], slot['name'])
for slot in result['history']['slots']
],
)
except Exception: except Exception:
log.warning('Data from SABnzbd history could not be parsed') log.warning('Data from SABnzbd history could not be parsed')
try: try:

View file

@ -14,8 +14,7 @@ def parse_other(args):
def parse_rtorrent(args): def parse_rtorrent(args):
# rtorrent usage: system.method.set_key = event.download.finished,TorrentToMedia, # rtorrent usage: system.method.set_key = event.download.finished,TorrentToMedia, # 'execute={/path/to/nzbToMedia/TorrentToMedia.py,\'$d.get_base_path=\',\'$d.get_name=\',\'$d.get_custom1=\',\'$d.get_hash=\'}'
# 'execute={/path/to/nzbToMedia/TorrentToMedia.py,\'$d.get_base_path=\',\'$d.get_name=\',\'$d.get_custom1=\',\'$d.get_hash=\'}'
input_directory = os.path.normpath(args[1]) input_directory = os.path.normpath(args[1])
try: try:
input_name = args[2] input_name = args[2]
@ -33,7 +32,6 @@ def parse_rtorrent(args):
input_id = args[4] input_id = args[4]
except Exception: except Exception:
input_id = '' input_id = ''
return input_directory, input_name, input_category, input_hash, input_id return input_directory, input_name, input_category, input_hash, input_id
@ -53,7 +51,6 @@ def parse_utorrent(args):
input_id = args[4] input_id = args[4]
except Exception: except Exception:
input_id = '' input_id = ''
return input_directory, input_name, input_category, input_hash, input_id return input_directory, input_name, input_category, input_hash, input_id
@ -64,17 +61,13 @@ def parse_deluge(args):
input_hash = args[1] input_hash = args[1]
input_id = args[1] input_id = args[1]
try: try:
input_category = ( input_category = nzb2media.TORRENT_CLASS.core.get_torrent_status(input_id, ['label']).get(b'label').decode()
nzb2media.TORRENT_CLASS.core.get_torrent_status(input_id, ['label'])
.get(b'label')
.decode()
)
except Exception: except Exception:
input_category = '' input_category = ''
return input_directory, input_name, input_category, input_hash, input_id return input_directory, input_name, input_category, input_hash, input_id
def parse_transmission(args): def parse_transmission():
# Transmission usage: call TorrenToMedia.py (%TR_TORRENT_DIR% %TR_TORRENT_NAME% is passed on as environmental variables) # Transmission usage: call TorrenToMedia.py (%TR_TORRENT_DIR% %TR_TORRENT_NAME% is passed on as environmental variables)
input_directory = os.path.normpath(os.getenv('TR_TORRENT_DIR')) input_directory = os.path.normpath(os.getenv('TR_TORRENT_DIR'))
input_name = os.getenv('TR_TORRENT_NAME') input_name = os.getenv('TR_TORRENT_NAME')
@ -84,7 +77,7 @@ def parse_transmission(args):
return input_directory, input_name, input_category, input_hash, input_id return input_directory, input_name, input_category, input_hash, input_id
def parse_synods(args): def parse_synods():
# Synology/Transmission usage: call TorrenToMedia.py (%TR_TORRENT_DIR% %TR_TORRENT_NAME% is passed on as environmental variables) # Synology/Transmission usage: call TorrenToMedia.py (%TR_TORRENT_DIR% %TR_TORRENT_NAME% is passed on as environmental variables)
input_directory = '' input_directory = ''
input_id = '' input_id = ''
@ -92,13 +85,7 @@ def parse_synods(args):
input_name = os.getenv('TR_TORRENT_NAME') input_name = os.getenv('TR_TORRENT_NAME')
input_hash = os.getenv('TR_TORRENT_HASH') input_hash = os.getenv('TR_TORRENT_HASH')
if not input_name: # No info passed. Assume manual download. if not input_name: # No info passed. Assume manual download.
return ( return input_directory, input_name, input_category, input_hash, input_id
input_directory,
input_name,
input_category,
input_hash,
input_id,
)
torrent_id = os.getenv('TR_TORRENT_ID') torrent_id = os.getenv('TR_TORRENT_ID')
input_id = f'dbid_{torrent_id}' input_id = f'dbid_{torrent_id}'
# res = nzb2media.TORRENT_CLASS.tasks_list(additional_param='detail') # res = nzb2media.TORRENT_CLASS.tasks_list(additional_param='detail')
@ -110,7 +97,7 @@ def parse_synods(args):
task = [task for task in tasks if task['id'] == input_id][0] task = [task for task in tasks if task['id'] == input_id][0]
input_id = task['id'] input_id = task['id']
input_directory = task['additional']['detail']['destination'] input_directory = task['additional']['detail']['destination']
except: except Exception:
log.error('unable to find download details in Synology DS') log.error('unable to find download details in Synology DS')
# Syno paths appear to be relative. Let's test to see if the returned path exists, and if not append to /volume1/ # Syno paths appear to be relative. Let's test to see if the returned path exists, and if not append to /volume1/
if not os.path.isdir(input_directory): if not os.path.isdir(input_directory):
@ -152,7 +139,6 @@ def parse_vuze(args):
input_name = cur_input[5] input_name = cur_input[5]
except Exception: except Exception:
pass pass
return input_directory, input_name, input_category, input_hash, input_id return input_directory, input_name, input_category, input_hash, input_id
@ -186,22 +172,11 @@ def parse_qbittorrent(args):
input_id = cur_input[3].replace('\'', '') input_id = cur_input[3].replace('\'', '')
except Exception: except Exception:
input_id = '' input_id = ''
return input_directory, input_name, input_category, input_hash, input_id return input_directory, input_name, input_category, input_hash, input_id
def parse_args(client_agent, args): def parse_args(client_agent, args):
clients = { clients = {'other': parse_other, 'rtorrent': parse_rtorrent, 'utorrent': parse_utorrent, 'deluge': parse_deluge, 'transmission': parse_transmission, 'qbittorrent': parse_qbittorrent, 'vuze': parse_vuze, 'synods': parse_synods}
'other': parse_other,
'rtorrent': parse_rtorrent,
'utorrent': parse_utorrent,
'deluge': parse_deluge,
'transmission': parse_transmission,
'qbittorrent': parse_qbittorrent,
'vuze': parse_vuze,
'synods': parse_synods,
}
try: try:
return clients[client_agent](args) return clients[client_agent](args)
except Exception: except Exception:

View file

@ -16,12 +16,9 @@ log.addHandler(logging.NullHandler())
def onerror(func, path, exc_info): def onerror(func, path, exc_info):
""" """
Error handler for ``shutil.rmtree``. Error handler for ``shutil.rmtree``.
If the error is due to an access error (read only file) If the error is due to an access error (read only file)
it attempts to add write permission and then retries. it attempts to add write permission and then retries.
If the error is for another reason it re-raises the error. If the error is for another reason it re-raises the error.
Usage : ``shutil.rmtree(path, onerror=onerror)`` Usage : ``shutil.rmtree(path, onerror=onerror)``
""" """
if not os.access(path, os.W_OK): if not os.access(path, os.W_OK):
@ -29,7 +26,7 @@ def onerror(func, path, exc_info):
os.chmod(path, stat.S_IWUSR) os.chmod(path, stat.S_IWUSR)
func(path) func(path)
else: else:
raise Exception raise Exception(exc_info)
def remove_dir(dir_name): def remove_dir(dir_name):
@ -69,26 +66,21 @@ def remote_dir(path):
def get_dir_size(input_path): def get_dir_size(input_path):
prepend = partial(os.path.join, input_path) prepend = partial(os.path.join, input_path)
return sum( return sum((os.path.getsize(f) if os.path.isfile(f) else get_dir_size(f)) for f in map(prepend, os.listdir(input_path)))
(os.path.getsize(f) if os.path.isfile(f) else get_dir_size(f))
for f in map(prepend, os.listdir(input_path))
)
def remove_empty_folders(path, remove_root=True): def remove_empty_folders(path, remove_root=True):
"""Remove empty folders.""" """Remove empty folders."""
if not os.path.isdir(path): if not os.path.isdir(path):
return return
# remove empty subfolders # remove empty subfolders
log.debug(f'Checking for empty folders in:{path}') log.debug(f'Checking for empty folders in:{path}')
files = os.listdir(path) files = os.listdir(path)
if len(files): if len(files):
for f in files: for each_file in files:
fullpath = os.path.join(path, f) fullpath = os.path.join(path, each_file)
if os.path.isdir(fullpath): if os.path.isdir(fullpath):
remove_empty_folders(fullpath) remove_empty_folders(fullpath)
# if folder empty, delete it # if folder empty, delete it
files = os.listdir(path) files = os.listdir(path)
if len(files) == 0 and remove_root: if len(files) == 0 and remove_root:
@ -111,16 +103,16 @@ def remove_read_only(filename):
def flatten_dir(destination, files): def flatten_dir(destination, files):
log.info(f'FLATTEN: Flattening directory: {destination}') log.info(f'FLATTEN: Flattening directory: {destination}')
for outputFile in files: for output_file in files:
dir_path = os.path.dirname(outputFile) dir_path = os.path.dirname(output_file)
file_name = os.path.basename(outputFile) file_name = os.path.basename(output_file)
if dir_path == destination: if dir_path == destination:
continue continue
target = os.path.join(destination, file_name) target = os.path.join(destination, file_name)
try: try:
shutil.move(outputFile, target) shutil.move(output_file, target)
except Exception: except Exception:
log.error(f'Could not flatten {outputFile}') log.error(f'Could not flatten {output_file}')
remove_empty_folders(destination) # Cleanup empty directories remove_empty_folders(destination) # Cleanup empty directories
@ -128,16 +120,13 @@ def clean_directory(path, files):
if not os.path.exists(path): if not os.path.exists(path):
log.info(f'Directory {path} has been processed and removed ...') log.info(f'Directory {path} has been processed and removed ...')
return return
if nzb2media.FORCE_CLEAN and not nzb2media.FAILED: if nzb2media.FORCE_CLEAN and not nzb2media.FAILED:
log.info(f'Doing Forceful Clean of {path}') log.info(f'Doing Forceful Clean of {path}')
remove_dir(path) remove_dir(path)
return return
if files: if files:
log.info(f'Directory {path} still contains {len(files)} unprocessed file(s), skipping ...') log.info(f'Directory {path} still contains {len(files)} unprocessed file(s), skipping ...')
return return
log.info(f'Directory {path} has been processed, removing ...') log.info(f'Directory {path} has been processed, removing ...')
try: try:
shutil.rmtree(path, onerror=onerror) shutil.rmtree(path, onerror=onerror)
@ -150,9 +139,8 @@ def rchmod(path, mod):
os.chmod(path, mod) os.chmod(path, mod)
if not os.path.isdir(path): if not os.path.isdir(path):
return # Skip files return # Skip files
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
for d in dirs: for each_dir in dirs:
os.chmod(os.path.join(root, d), mod) os.chmod(os.path.join(root, each_dir), mod)
for f in files: for each_file in files:
os.chmod(os.path.join(root, f), mod) os.chmod(os.path.join(root, each_file), mod)

View file

@ -8,13 +8,16 @@ import sys
import typing import typing
import nzb2media import nzb2media
from nzb2media import APP_FILENAME
from nzb2media import SYS_ARGV
from nzb2media import version_check
if os.name == 'nt': if os.name == 'nt':
# pylint: disable-next=no-name-in-module
from win32event import CreateMutex from win32event import CreateMutex
from win32api import CloseHandle, GetLastError
# pylint: disable-next=no-name-in-module
from win32api import CloseHandle
# pylint: disable-next=no-name-in-module
from win32api import GetLastError
from winerror import ERROR_ALREADY_EXISTS from winerror import ERROR_ALREADY_EXISTS
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -27,23 +30,22 @@ class WindowsProcess:
# {D0E858DF-985E-4907-B7FB-8D732C3FC3B9} # {D0E858DF-985E-4907-B7FB-8D732C3FC3B9}
_path_str = os.fspath(nzb2media.PID_FILE).replace('\\', '/') _path_str = os.fspath(nzb2media.PID_FILE).replace('\\', '/')
self.mutexname = f'nzbtomedia_{_path_str}' self.mutexname = f'nzbtomedia_{_path_str}'
self.CreateMutex = CreateMutex self.create_mutex = CreateMutex
self.CloseHandle = CloseHandle self.close_handle = CloseHandle
self.GetLastError = GetLastError self.get_last_error = GetLastError
self.ERROR_ALREADY_EXISTS = ERROR_ALREADY_EXISTS self.error_already_exists = ERROR_ALREADY_EXISTS
def alreadyrunning(self): def alreadyrunning(self):
self.mutex = self.CreateMutex(None, 0, self.mutexname) self.mutex = self.create_mutex(None, 0, self.mutexname)
self.lasterror = self.GetLastError() self.lasterror = self.get_last_error()
if self.lasterror == self.ERROR_ALREADY_EXISTS: if self.lasterror == self.error_already_exists:
self.CloseHandle(self.mutex) self.close_handle(self.mutex)
return True return True
else:
return False return False
def __del__(self): def __del__(self):
if self.mutex: if self.mutex:
self.CloseHandle(self.mutex) self.close_handle(self.mutex)
class PosixProcess: class PosixProcess:
@ -57,16 +59,16 @@ class PosixProcess:
self.lock_socket.bind(f'\0{self.pidpath}') self.lock_socket.bind(f'\0{self.pidpath}')
self.lasterror = False self.lasterror = False
return self.lasterror return self.lasterror
except OSError as e: except OSError as error:
if 'Address already in use' in str(e): if 'Address already in use' in str(error):
self.lasterror = True self.lasterror = True
return self.lasterror return self.lasterror
except AttributeError: except AttributeError:
pass pass
if os.path.exists(self.pidpath): if self.pidpath.exists():
# Make sure it is not a 'stale' pidFile # Make sure it is not a 'stale' pidFile
try: try:
pid = int(open(self.pidpath).read().strip()) pid = int(self.pidpath.read_text().strip())
except Exception: except Exception:
pid = None pid = None
# Check list of running pids, if not running it is stale so overwrite # Check list of running pids, if not running it is stale so overwrite
@ -80,11 +82,10 @@ class PosixProcess:
self.lasterror = False self.lasterror = False
else: else:
self.lasterror = False self.lasterror = False
if not self.lasterror: if not self.lasterror:
# Write my pid into pidFile to keep multiple copies of program from running # Write my pid into pidFile to keep multiple copies of program
with self.pidpath.open(mode='w') as fp: # from running
fp.write(os.getpid()) self.pidpath.write_text(os.getpid())
return self.lasterror return self.lasterror
def __del__(self): def __del__(self):
@ -103,19 +104,15 @@ else:
def restart(): def restart():
install_type = version_check.CheckVersion().install_type install_type = nzb2media.version_check.CheckVersion().install_type
status = 0 status = 0
popen_list = [] popen_list = []
if install_type in {'git', 'source'}:
if install_type in ('git', 'source'): popen_list = [sys.executable, nzb2media.APP_FILENAME]
popen_list = [sys.executable, APP_FILENAME]
if popen_list: if popen_list:
popen_list += SYS_ARGV popen_list += nzb2media.SYS_ARGV
log.info(f'Restarting nzbToMedia with {popen_list}') log.info(f'Restarting nzbToMedia with {popen_list}')
p = subprocess.Popen(popen_list, cwd=os.getcwd()) with subprocess.Popen(popen_list, cwd=os.getcwd()) as proc:
p.wait() proc.wait()
status = p.returncode status = proc.returncode
os._exit(status) os._exit(status)

View file

@ -12,25 +12,18 @@ from nzb2media.torrent import utorrent
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler()) log.addHandler(logging.NullHandler())
torrent_clients = {'deluge': deluge, 'qbittorrent': qbittorrent, 'transmission': transmission, 'utorrent': utorrent, 'synods': synology}
torrent_clients = {
'deluge': deluge,
'qbittorrent': qbittorrent,
'transmission': transmission,
'utorrent': utorrent,
'synods': synology,
}
def create_torrent_class(client_agent): def create_torrent_class(client_agent) -> object | None:
if not nzb2media.APP_NAME == 'TorrentToMedia.py': if nzb2media.APP_NAME != 'TorrentToMedia.py':
return # Skip loading Torrent for NZBs. return None # Skip loading Torrent for NZBs.
try: try:
agent = torrent_clients[client_agent] agent = torrent_clients[client_agent]
except KeyError: except KeyError:
return return None
else: else:
deluge.configure_client()
return agent.configure_client() return agent.configure_client()
@ -53,7 +46,7 @@ def pause_torrent(client_agent, input_hash, input_id, input_name):
def resume_torrent(client_agent, input_hash, input_id, input_name): def resume_torrent(client_agent, input_hash, input_id, input_name):
if not nzb2media.TORRENT_RESUME == 1: if nzb2media.TORRENT_RESUME != 1:
return return
log.debug(f'Starting torrent {input_name} in {client_agent}') log.debug(f'Starting torrent {input_name} in {client_agent}')
try: try:

View file

@ -11,6 +11,7 @@ import stat
import subprocess import subprocess
import tarfile import tarfile
import traceback import traceback
from subprocess import PIPE, STDOUT
from urllib.request import urlretrieve from urllib.request import urlretrieve
import nzb2media import nzb2media
@ -27,7 +28,6 @@ class CheckVersion:
self.install_type = self.find_install_type() self.install_type = self.find_install_type()
self.installed_version = None self.installed_version = None
self.installed_branch = None self.installed_branch = None
if self.install_type == 'git': if self.install_type == 'git':
self.updater = GitUpdateManager() self.updater = GitUpdateManager()
elif self.install_type == 'source': elif self.install_type == 'source':
@ -38,10 +38,10 @@ class CheckVersion:
def run(self): def run(self):
self.check_for_new_version() self.check_for_new_version()
def find_install_type(self): @staticmethod
def find_install_type():
""" """
Determine how this copy of SB was installed. Determine how this copy of SB was installed.
returns: type of installation. Possible values are: returns: type of installation. Possible values are:
'win': any compiled windows build 'win': any compiled windows build
'git': running from source using git 'git': running from source using git
@ -52,27 +52,22 @@ class CheckVersion:
install_type = 'git' install_type = 'git'
else: else:
install_type = 'source' install_type = 'source'
return install_type return install_type
def check_for_new_version(self, force=False): def check_for_new_version(self, force=False):
""" """
Check the internet for a newer version. Check the internet for a newer version.
returns: bool, True for new version or False for no new version. returns: bool, True for new version or False for no new version.
force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced
""" """
if not nzb2media.VERSION_NOTIFY and not force: if not nzb2media.VERSION_NOTIFY and not force:
log.info('Version checking is disabled, not checking for the newest version') log.info('Version checking is disabled, not checking for the newest version')
return False return False
log.info(f'Checking if {self.install_type} needs an update') log.info(f'Checking if {self.install_type} needs an update')
if not self.updater.need_update(): if not self.updater.need_update():
nzb2media.NEWEST_VERSION_STRING = None nzb2media.NEWEST_VERSION_STRING = None
log.info('No update needed') log.info('No update needed')
return False return False
self.updater.set_newest_text() self.updater.set_newest_text()
return True return True
@ -83,13 +78,16 @@ class CheckVersion:
class UpdateManager: class UpdateManager:
def get_github_repo_user(self): @staticmethod
def get_github_repo_user():
return nzb2media.GIT_USER return nzb2media.GIT_USER
def get_github_repo(self): @staticmethod
def get_github_repo():
return nzb2media.GIT_REPO return nzb2media.GIT_REPO
def get_github_branch(self): @staticmethod
def get_github_branch():
return nzb2media.GIT_BRANCH return nzb2media.GIT_BRANCH
@ -99,127 +97,87 @@ class GitUpdateManager(UpdateManager):
self.github_repo_user = self.get_github_repo_user() self.github_repo_user = self.get_github_repo_user()
self.github_repo = self.get_github_repo() self.github_repo = self.get_github_repo()
self.branch = self._find_git_branch() self.branch = self._find_git_branch()
self._cur_commit_hash = None self._cur_commit_hash = None
self._newest_commit_hash = None self._newest_commit_hash = None
self._num_commits_behind = 0 self._num_commits_behind = 0
self._num_commits_ahead = 0 self._num_commits_ahead = 0
def _git_error(self): @staticmethod
def _git_error():
log.debug('Unable to find your git executable - Set git_path in your autoProcessMedia.cfg OR delete your .git folder and run from source to enable updates.') log.debug('Unable to find your git executable - Set git_path in your autoProcessMedia.cfg OR delete your .git folder and run from source to enable updates.')
def _find_working_git(self): def _find_working_git(self):
test_cmd = 'version' test_cmd = 'version'
if nzb2media.GIT_PATH: if nzb2media.GIT_PATH:
main_git = f'"{nzb2media.GIT_PATH}"' main_git = f'"{nzb2media.GIT_PATH}"'
else: else:
main_git = 'git' main_git = 'git'
log.debug(f'Checking if we can use git commands: {main_git} {test_cmd}')
log.debug('Checking if we can use git commands: {git} {cmd}'.format(git=main_git, cmd=test_cmd))
output, err, exit_status = self._run_git(main_git, test_cmd) output, err, exit_status = self._run_git(main_git, test_cmd)
if exit_status == 0: if exit_status == 0:
log.debug(f'Using: {main_git}') log.debug(f'Using: {main_git}')
return main_git return main_git
else:
log.debug(f'Not using: {main_git}') log.debug(f'Not using: {main_git}')
# trying alternatives # trying alternatives
alternative_git = [] alternative_git = []
# osx people who start SB from launchd have a broken path, so try a hail-mary attempt for them # osx people who start SB from launchd have a broken path, so try a hail-mary attempt for them
if platform.system().lower() == 'darwin': if platform.system().lower() == 'darwin':
alternative_git.append('/usr/local/git/bin/git') alternative_git.append('/usr/local/git/bin/git')
if platform.system().lower() == 'windows': if platform.system().lower() == 'windows':
if main_git != main_git.lower(): if main_git != main_git.lower():
alternative_git.append(main_git.lower()) alternative_git.append(main_git.lower())
if alternative_git: if alternative_git:
log.debug('Trying known alternative git locations') log.debug('Trying known alternative git locations')
for cur_git in alternative_git: for cur_git in alternative_git:
log.debug('Checking if we can use git commands: {git} {cmd}'.format(git=cur_git, cmd=test_cmd)) log.debug(f'Checking if we can use git commands: {cur_git} {test_cmd}')
output, err, exit_status = self._run_git(cur_git, test_cmd) output, err, exit_status = self._run_git(cur_git, test_cmd)
if exit_status == 0: if exit_status == 0:
log.debug(f'Using: {cur_git}') log.debug(f'Using: {cur_git}')
return cur_git return cur_git
else:
log.debug(f'Not using: {cur_git}') log.debug(f'Not using: {cur_git}')
# Still haven't found a working git # Still haven't found a working git
log.debug( log.debug('Unable to find your git executable - Set git_path in your autoProcessMedia.cfg OR delete your .git folder and run from source to enable updates.')
'Unable to find your git executable - '
'Set git_path in your autoProcessMedia.cfg OR '
'delete your .git folder and run from source to enable updates.',
)
return None return None
def _run_git(self, git_path, args): @staticmethod
def _run_git(git_path, args):
output = None result = ''
err = None proc_err = ''
if not git_path: if not git_path:
log.debug('No git specified, can\'t use git commands') log.debug('No git specified, can\'t use git commands')
exit_status = 1 proc_status = 1
return output, err, exit_status return result, proc_err, proc_status
cmd = f'{git_path} {args}' cmd = f'{git_path} {args}'
try: try:
log.debug('Executing {cmd} with your shell in {directory}'.format(cmd=cmd, directory=nzb2media.APP_ROOT)) log.debug(f'Executing {cmd} with your shell in {nzb2media.APP_ROOT}')
p = subprocess.Popen( with subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True, cwd=nzb2media.APP_ROOT) as proc:
cmd, proc_out, proc_err = proc.communicate()
stdin=subprocess.PIPE, proc_status = proc.returncode
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
cwd=nzb2media.APP_ROOT,
)
output, err = p.communicate()
exit_status = p.returncode
output = output.decode('utf-8')
if output:
output = output.strip()
if nzb2media.LOG_GIT: if nzb2media.LOG_GIT:
log.debug(f'git output: {output}') msg = proc_out.decode('utf-8').strip()
log.debug(f'git output: {msg}')
except OSError: except OSError:
log.error(f'Command {cmd} didn\'t work') log.error(f'Command {cmd} didn\'t work')
exit_status = 1 proc_status = 1
proc_status = 128 if ('fatal:' in result) or proc_err else proc_status
exit_status = 128 if ('fatal:' in output) or err else exit_status if proc_status == 0:
if exit_status == 0:
log.debug(f'{cmd} : returned successful') log.debug(f'{cmd} : returned successful')
exit_status = 0 proc_status = 0
elif nzb2media.LOG_GIT and exit_status in (1, 128): elif nzb2media.LOG_GIT and proc_status in {1, 128}:
log.debug(f'{cmd} returned : {output}') log.debug(f'{cmd} returned : {result}')
else: else:
if nzb2media.LOG_GIT: if nzb2media.LOG_GIT:
log.debug('{cmd} returned : {output}, treat as error for now'.format(cmd=cmd, output=output)) log.debug(f'{cmd} returned : {result}, treat as error for now')
exit_status = 1 proc_status = 1
return result, proc_err, proc_status
return output, err, exit_status
def _find_installed_version(self): def _find_installed_version(self):
""" """
Attempt to find the currently installed version of Sick Beard. Attempt to find the currently installed version of Sick Beard.
Uses git show to get commit version. Uses git show to get commit version.
Returns: True for success or False for failure Returns: True for success or False for failure
""" """
output, err, exit_status = self._run_git( output, err, exit_status = self._run_git(self._git_path, 'rev-parse HEAD')
self._git_path, 'rev-parse HEAD',
) # @UnusedVariable
if exit_status == 0 and output: if exit_status == 0 and output:
cur_commit_hash = output.strip() cur_commit_hash = output.strip()
if not re.match('^[a-z0-9]+$', cur_commit_hash): if not re.match('^[a-z0-9]+$', cur_commit_hash):
@ -229,14 +187,11 @@ class GitUpdateManager(UpdateManager):
if self._cur_commit_hash: if self._cur_commit_hash:
nzb2media.NZBTOMEDIA_VERSION = self._cur_commit_hash nzb2media.NZBTOMEDIA_VERSION = self._cur_commit_hash
return True return True
else:
return False return False
def _find_git_branch(self): def _find_git_branch(self):
nzb2media.NZBTOMEDIA_BRANCH = self.get_github_branch() nzb2media.NZBTOMEDIA_BRANCH = self.get_github_branch()
branch_info, err, exit_status = self._run_git( branch_info, err, exit_status = self._run_git(self._git_path, 'symbolic-ref -q HEAD')
self._git_path, 'symbolic-ref -q HEAD',
) # @UnusedVariable
if exit_status == 0 and branch_info: if exit_status == 0 and branch_info:
branch = branch_info.strip().replace('refs/heads/', '', 1) branch = branch_info.strip().replace('refs/heads/', '', 1)
if branch: if branch:
@ -247,7 +202,6 @@ class GitUpdateManager(UpdateManager):
def _check_github_for_update(self): def _check_github_for_update(self):
""" """
Check Github for a new version. Check Github for a new version.
Uses git commands to check if there is a newer version than Uses git commands to check if there is a newer version than
the provided commit hash. If there is a newer version it the provided commit hash. If there is a newer version it
sets _num_commits_behind. sets _num_commits_behind.
@ -255,55 +209,39 @@ class GitUpdateManager(UpdateManager):
self._newest_commit_hash = None self._newest_commit_hash = None
self._num_commits_behind = 0 self._num_commits_behind = 0
self._num_commits_ahead = 0 self._num_commits_ahead = 0
# get all new info from github # get all new info from github
output, err, exit_status = self._run_git( output, err, exit_status = self._run_git(self._git_path, 'fetch origin')
self._git_path, 'fetch origin',
)
if not exit_status == 0: if not exit_status == 0:
log.error('Unable to contact github, can\'t check for update') log.error('Unable to contact github, can\'t check for update')
return return
# get latest commit_hash from remote # get latest commit_hash from remote
output, err, exit_status = self._run_git( output, err, exit_status = self._run_git(self._git_path, 'rev-parse --verify --quiet \'@{upstream}\'')
self._git_path, 'rev-parse --verify --quiet \'@{upstream}\'',
)
if exit_status == 0 and output: if exit_status == 0 and output:
cur_commit_hash = output.strip() cur_commit_hash = output.strip()
if not re.match('^[a-z0-9]+$', cur_commit_hash): if not re.match('^[a-z0-9]+$', cur_commit_hash):
log.debug('Output doesn\'t look like a hash, not using it') log.debug('Output doesn\'t look like a hash, not using it')
return return
else:
self._newest_commit_hash = cur_commit_hash self._newest_commit_hash = cur_commit_hash
else: else:
log.debug('git didn\'t return newest commit hash') log.debug('git didn\'t return newest commit hash')
return return
# get number of commits behind and ahead (option --count not supported git < 1.7.2) # get number of commits behind and ahead (option --count not supported git < 1.7.2)
output, err, exit_status = self._run_git( output, err, exit_status = self._run_git(self._git_path, 'rev-list --left-right \'@{upstream}\'...HEAD')
self._git_path, 'rev-list --left-right \'@{upstream}\'...HEAD',
)
if exit_status == 0 and output: if exit_status == 0 and output:
try: try:
self._num_commits_behind = int(output.count('<')) self._num_commits_behind = int(output.count('<'))
self._num_commits_ahead = int(output.count('>')) self._num_commits_ahead = int(output.count('>'))
except Exception: except Exception:
log.debug('git didn\'t return numbers for behind and ahead, not using it') log.debug('git didn\'t return numbers for behind and ahead, not using it')
return return
log.debug(f'cur_commit = {self._cur_commit_hash} % (newest_commit)= {self._newest_commit_hash}, num_commits_behind = {self._num_commits_behind}, num_commits_ahead = {self._num_commits_ahead}')
log.debug('cur_commit = {current} % (newest_commit)= {new}, num_commits_behind = {x}, num_commits_ahead = {y}'.format(current=self._cur_commit_hash, new=self._newest_commit_hash, x=self._num_commits_behind, y=self._num_commits_ahead))
def set_newest_text(self): def set_newest_text(self):
if self._num_commits_ahead: if self._num_commits_ahead:
log.error('Local branch is ahead of {branch}. Automatic update not possible.'.format(branch=self.branch)) log.error(f'Local branch is ahead of {self.branch}. Automatic update not possible.')
elif self._num_commits_behind: elif self._num_commits_behind:
log.info('There is a newer version available (you\'re {x} commit{s} behind)'.format(x=self._num_commits_behind, s='s' if self._num_commits_behind > 1 else '')) _plural = 's' if self._num_commits_behind > 1 else ''
log.info(f'There is a newer version available (you\'re {self._num_commits_behind} commit{_plural} behind)')
else: else:
return return
@ -311,35 +249,26 @@ class GitUpdateManager(UpdateManager):
if not self._find_installed_version(): if not self._find_installed_version():
log.error('Unable to determine installed version via git, please check your logs!') log.error('Unable to determine installed version via git, please check your logs!')
return False return False
if not self._cur_commit_hash: if not self._cur_commit_hash:
return True return True
else:
try: try:
self._check_github_for_update() self._check_github_for_update()
except Exception as error: except Exception as error:
log.error(f'Unable to contact github, can\'t check for update: {error!r}') log.error(f'Unable to contact github, can\'t check for update: {error!r}')
return False return False
if self._num_commits_behind > 0: if self._num_commits_behind > 0:
return True return True
return False return False
def update(self): def update(self):
""" """Check git for a new version.
Check git for a new version.
Calls git pull origin <branch> in order to update Sick Beard. Calls git pull origin <branch> in order to update Sick Beard.
Returns a bool depending on the call's success. Returns a bool depending on the call's success.
""" """
output, err, exit_status = self._run_git( output, err, exit_status = self._run_git(self._git_path, f'pull origin {self.branch}')
self._git_path, f'pull origin {self.branch}',
) # @UnusedVariable
if exit_status == 0: if exit_status == 0:
return True return True
return False return False
@ -348,199 +277,146 @@ class SourceUpdateManager(UpdateManager):
self.github_repo_user = self.get_github_repo_user() self.github_repo_user = self.get_github_repo_user()
self.github_repo = self.get_github_repo() self.github_repo = self.get_github_repo()
self.branch = self.get_github_branch() self.branch = self.get_github_branch()
self._cur_commit_hash = None self._cur_commit_hash = None
self._newest_commit_hash = None self._newest_commit_hash = None
self._num_commits_behind = 0 self._num_commits_behind = 0
def _find_installed_version(self): def _find_installed_version(self):
version_file = os.path.join(nzb2media.APP_ROOT, 'version.txt') version_file = os.path.join(nzb2media.APP_ROOT, 'version.txt')
if not os.path.isfile(version_file): if not os.path.isfile(version_file):
self._cur_commit_hash = None self._cur_commit_hash = None
return return
try: try:
with open(version_file) as fp: with open(version_file, encoding='utf-8') as fin:
self._cur_commit_hash = fp.read().strip(' \n\r') self._cur_commit_hash = fin.read().strip(' \n\r')
except OSError as error: except OSError as error:
log.debug(f'Unable to open \'version.txt\': {error}') log.debug(f'Unable to open \'version.txt\': {error}')
if not self._cur_commit_hash: if not self._cur_commit_hash:
self._cur_commit_hash = None self._cur_commit_hash = None
else: else:
nzb2media.NZBTOMEDIA_VERSION = self._cur_commit_hash nzb2media.NZBTOMEDIA_VERSION = self._cur_commit_hash
def need_update(self): def need_update(self):
self._find_installed_version() self._find_installed_version()
try: try:
self._check_github_for_update() self._check_github_for_update()
except Exception as error: except Exception as error:
log.error(f'Unable to contact github, can\'t check for update: {error!r}') log.error(f'Unable to contact github, can\'t check for update: {error!r}')
return False return False
if not self._cur_commit_hash or self._num_commits_behind > 0: if not self._cur_commit_hash or self._num_commits_behind > 0:
return True return True
return False return False
def _check_github_for_update(self): def _check_github_for_update(self):
""" """ Check Github for a new version.
Check Github for a new version.
Uses pygithub to ask github if there is a newer version than Uses pygithub to ask github if there is a newer version than
the provided commit hash. If there is a newer version it sets the provided commit hash. If there is a newer version it sets
Sick Beard's version text. Sick Beard's version text.
commit_hash: hash that we're checking against commit_hash: hash that we're checking against
""" """
self._num_commits_behind = 0 self._num_commits_behind = 0
self._newest_commit_hash = None self._newest_commit_hash = None
repository = github.GitHub(self.github_repo_user, self.github_repo, self.branch)
gh = github.GitHub(
self.github_repo_user, self.github_repo, self.branch,
)
# try to get newest commit hash and commits behind directly by # try to get newest commit hash and commits behind directly by
# comparing branch and current commit # comparing branch and current commit
if self._cur_commit_hash: if self._cur_commit_hash:
branch_compared = gh.compare( branch_compared = repository.compare(base=self.branch, head=self._cur_commit_hash)
base=self.branch, head=self._cur_commit_hash,
)
if 'base_commit' in branch_compared: if 'base_commit' in branch_compared:
self._newest_commit_hash = branch_compared['base_commit'][ self._newest_commit_hash = branch_compared['base_commit']['sha']
'sha'
]
if 'behind_by' in branch_compared: if 'behind_by' in branch_compared:
self._num_commits_behind = int(branch_compared['behind_by']) self._num_commits_behind = int(branch_compared['behind_by'])
# fall back and iterate over last 100 (items per page in gh_api) commits # fall back and iterate over last 100 (items per page in gh_api) commits
if not self._newest_commit_hash: if not self._newest_commit_hash:
for cur_commit in repository.commits():
for curCommit in gh.commits():
if not self._newest_commit_hash: if not self._newest_commit_hash:
self._newest_commit_hash = curCommit['sha'] self._newest_commit_hash = cur_commit['sha']
if not self._cur_commit_hash: if not self._cur_commit_hash:
break break
if cur_commit['sha'] == self._cur_commit_hash:
if curCommit['sha'] == self._cur_commit_hash:
break break
# when _cur_commit_hash doesn't match anything _num_commits_behind == 100 # when _cur_commit_hash doesn't match anything _num_commits_behind == 100
self._num_commits_behind += 1 self._num_commits_behind += 1
log.debug(f'cur_commit = {self._cur_commit_hash} % (newest_commit)= {self._newest_commit_hash}, num_commits_behind = {self._num_commits_behind}')
log.debug('cur_commit = {current} % (newest_commit)= {new}, num_commits_behind = {x}'.format(current=self._cur_commit_hash, new=self._newest_commit_hash, x=self._num_commits_behind))
def set_newest_text(self): def set_newest_text(self):
# if we're up to date then don't set this # if we're up to date then don't set this
nzb2media.NEWEST_VERSION_STRING = None nzb2media.NEWEST_VERSION_STRING = None
if not self._cur_commit_hash: if not self._cur_commit_hash:
log.error('Unknown current version number, don\'t know if we should update or not') log.error('Unknown current version number, don\'t know if we should update or not')
elif self._num_commits_behind > 0: elif self._num_commits_behind > 0:
log.info('There is a newer version available (you\'re {x} commit{s} behind)'.format(x=self._num_commits_behind, s='s' if self._num_commits_behind > 1 else '')) _plural = 's' if self._num_commits_behind > 1 else ''
log.info(f'There is a newer version available (you\'re {self._num_commits_behind} commit{_plural} behind)')
else: else:
return return
def update(self): def update(self):
"""Download and install latest source tarball from github.""" """Download and install latest source tarball from github."""
tar_download_url = ( tar_download_url = f'https://github.com/{self.github_repo_user}/{self.github_repo}/tarball/{self.branch}'
'https://github.com/{org}/{repo}/tarball/{branch}'.format(org=self.github_repo_user, repo=self.github_repo, branch=self.branch)
)
version_path = os.path.join(nzb2media.APP_ROOT, 'version.txt') version_path = os.path.join(nzb2media.APP_ROOT, 'version.txt')
try: try:
# prepare the update dir # prepare the update dir
sb_update_dir = os.path.join(nzb2media.APP_ROOT, 'sb-update') sb_update_dir = os.path.join(nzb2media.APP_ROOT, 'sb-update')
if os.path.isdir(sb_update_dir): if os.path.isdir(sb_update_dir):
log.info(f'Clearing out update folder {sb_update_dir} before extracting') log.info(f'Clearing out update folder {sb_update_dir} before extracting')
shutil.rmtree(sb_update_dir) shutil.rmtree(sb_update_dir)
log.info(f'Creating update folder {sb_update_dir} before extracting') log.info(f'Creating update folder {sb_update_dir} before extracting')
os.makedirs(sb_update_dir) os.makedirs(sb_update_dir)
# retrieve file # retrieve file
log.info(f'Downloading update from {tar_download_url!r}') log.info(f'Downloading update from {tar_download_url!r}')
tar_download_path = os.path.join( tar_download_path = os.path.join(sb_update_dir, 'nzbtomedia-update.tar')
sb_update_dir, 'nzbtomedia-update.tar',
)
urlretrieve(tar_download_url, tar_download_path) urlretrieve(tar_download_url, tar_download_path)
if not os.path.isfile(tar_download_path): if not os.path.isfile(tar_download_path):
log.error('Unable to retrieve new version from {url}, can\'t update'.format(url=tar_download_url)) log.error(f'Unable to retrieve new version from {tar_download_url}, can\'t update')
return False return False
if not tarfile.is_tarfile(tar_download_path): if not tarfile.is_tarfile(tar_download_path):
log.error('Retrieved version from {url} is corrupt, can\'t update'.format(url=tar_download_url)) log.error(f'Retrieved version from {tar_download_url} is corrupt, can\'t update')
return False return False
# extract to sb-update dir # extract to sb-update dir
log.info(f'Extracting file {tar_download_path}') log.info(f'Extracting file {tar_download_path}')
tar = tarfile.open(tar_download_path) with tarfile.open(tar_download_path) as tar:
tar.extractall(sb_update_dir) tar.extractall(sb_update_dir)
tar.close()
# delete .tar.gz # delete .tar.gz
log.info(f'Deleting file {tar_download_path}') log.info(f'Deleting file {tar_download_path}')
os.remove(tar_download_path) os.remove(tar_download_path)
# find update dir name # find update dir name
update_dir_contents = [ update_dir_contents = [x for x in os.listdir(sb_update_dir) if os.path.isdir(os.path.join(sb_update_dir, x))]
x
for x in os.listdir(sb_update_dir)
if os.path.isdir(os.path.join(sb_update_dir, x))
]
if len(update_dir_contents) != 1: if len(update_dir_contents) != 1:
log.error(f'Invalid update data, update failed: {update_dir_contents}') log.error(f'Invalid update data, update failed: {update_dir_contents}')
return False return False
content_dir = os.path.join(sb_update_dir, update_dir_contents[0]) content_dir = os.path.join(sb_update_dir, update_dir_contents[0])
# walk temp folder and move files to main folder # walk temp folder and move files to main folder
log.info('Moving files from {source} to {destination}'.format(source=content_dir, destination=nzb2media.APP_ROOT)) log.info(f'Moving files from {content_dir} to {nzb2media.APP_ROOT}')
for dirname, _, filenames in os.walk( for dirname, _, filenames in os.walk(content_dir):
content_dir,
): # @UnusedVariable
dirname = dirname[len(content_dir) + 1:] dirname = dirname[len(content_dir) + 1:]
for curfile in filenames: for curfile in filenames:
old_path = os.path.join(content_dir, dirname, curfile) old_path = os.path.join(content_dir, dirname, curfile)
new_path = os.path.join(nzb2media.APP_ROOT, dirname, curfile) new_path = os.path.join(nzb2media.APP_ROOT, dirname, curfile)
# Avoid DLL access problem on WIN32/64 # Avoid DLL access problem on WIN32/64
# These files needing to be updated manually # These files needing to be updated manually
# or find a way to kill the access from memory # or find a way to kill the access from memory
if curfile in ('unrar.dll', 'unrar64.dll'): if curfile in {'unrar.dll', 'unrar64.dll'}:
try: try:
os.chmod(new_path, stat.S_IWRITE) os.chmod(new_path, stat.S_IWRITE)
os.remove(new_path) os.remove(new_path)
os.renames(old_path, new_path) os.renames(old_path, new_path)
except Exception as error: except Exception as error:
log.debug('Unable to update {path}: {msg}'.format(path=new_path, msg=error)) log.debug(f'Unable to update {new_path}: {error}')
# Trash the updated file without moving in new path # Trash the updated file without moving in new path
os.remove(old_path) os.remove(old_path)
continue continue
if os.path.isfile(new_path): if os.path.isfile(new_path):
os.remove(new_path) os.remove(new_path)
os.renames(old_path, new_path) os.renames(old_path, new_path)
# update version.txt with commit hash # update version.txt with commit hash
try: try:
with open(version_path, 'w') as ver_file: with open(version_path, 'w', encoding='utf-8') as ver_file:
ver_file.write(self._newest_commit_hash) ver_file.write(self._newest_commit_hash)
except OSError as error: except OSError as error:
log.error('Unable to write version file, update not complete: {msg}'.format(msg=error),) log.error(f'Unable to write version file, update not complete: {error}')
return False return False
except Exception as error: except Exception as error:
log.error(f'Error while trying to update: {error}') log.error(f'Error while trying to update: {error}')
log.debug(f'Traceback: {traceback.format_exc()}') log.debug(f'Traceback: {traceback.format_exc()}')
return False return False
return True return True

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'CouchPotato' SECTION = 'CouchPotato'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'Gamez' SECTION = 'Gamez'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'HeadPhones' SECTION = 'HeadPhones'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'LazyLibrarian' SECTION = 'LazyLibrarian'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'Lidarr' SECTION = 'Lidarr'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -65,4 +65,4 @@ def main(args, section=None):
if __name__ == '__main__': if __name__ == '__main__':
exit(main(sys.argv)) sys.exit(main(sys.argv))

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'Mylar' SECTION = 'Mylar'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'NzbDrone' SECTION = 'NzbDrone'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'Radarr' SECTION = 'Radarr'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'SiCKRAGE' SECTION = 'SiCKRAGE'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'SickBeard' SECTION = 'SickBeard'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -2,6 +2,6 @@ import sys
import nzbToMedia import nzbToMedia
section = 'Watcher3' SECTION = 'Watcher3'
result = nzbToMedia.main(sys.argv, section) result = nzbToMedia.main(sys.argv, SECTION)
sys.exit(result) sys.exit(result)

View file

@ -1,2 +0,0 @@
from __future__ import annotations
__author__ = 'Justin'

172
tests/import_test.py Normal file
View file

@ -0,0 +1,172 @@
def test_auto_process_imports():
import nzb2media.auto_process
assert nzb2media.auto_process
import nzb2media.auto_process.books
assert nzb2media.auto_process.books
import nzb2media.auto_process.comics
assert nzb2media.auto_process.comics
import nzb2media.auto_process.common
assert nzb2media.auto_process.common
import nzb2media.auto_process.games
assert nzb2media.auto_process.games
import nzb2media.auto_process.movies
assert nzb2media.auto_process.movies
import nzb2media.auto_process.music
assert nzb2media.auto_process.music
import nzb2media.auto_process.tv
assert nzb2media.auto_process.tv
def test_import_extractor():
import nzb2media.extractor
assert nzb2media.extractor
def test_import_managers():
import nzb2media.managers
assert nzb2media.managers
import nzb2media.managers.pymedusa
assert nzb2media.managers.pymedusa
import nzb2media.managers.sickbeard
assert nzb2media.managers.sickbeard
def test_import_nzb():
import nzb2media.nzb
assert nzb2media.nzb
import nzb2media.nzb.configuration
assert nzb2media.nzb.configuration
def test_import_plugins():
import nzb2media.plugins
assert nzb2media.plugins
import nzb2media.plugins.plex
assert nzb2media.plugins.plex
import nzb2media.plugins.subtitles
assert nzb2media.plugins.subtitles
def test_import_processor():
import nzb2media.processor
assert nzb2media.processor
import nzb2media.processor.manual
assert nzb2media.processor.manual
import nzb2media.processor.nzb
assert nzb2media.processor.nzb
import nzb2media.processor.nzbget
assert nzb2media.processor.nzbget
import nzb2media.processor.sab
assert nzb2media.processor.sab
def test_import_torrent():
import nzb2media.torrent
assert nzb2media.torrent
import nzb2media.torrent.configuration
assert nzb2media.torrent.configuration
import nzb2media.torrent.deluge
assert nzb2media.torrent.deluge
import nzb2media.torrent.qbittorrent
assert nzb2media.torrent.qbittorrent
import nzb2media.torrent.synology
assert nzb2media.torrent.synology
import nzb2media.torrent.transmission
assert nzb2media.torrent.transmission
import nzb2media.torrent.utorrent
assert nzb2media.torrent.utorrent
def test_import_utils():
import nzb2media.utils
assert nzb2media.utils
import nzb2media.utils.common
assert nzb2media.utils.common
import nzb2media.utils.download_info
assert nzb2media.utils.download_info
import nzb2media.utils.encoding
assert nzb2media.utils.encoding
import nzb2media.utils.files
assert nzb2media.utils.files
import nzb2media.utils.identification
assert nzb2media.utils.identification
import nzb2media.utils.links
assert nzb2media.utils.links
import nzb2media.utils.naming
assert nzb2media.utils.naming
import nzb2media.utils.network
assert nzb2media.utils.network
import nzb2media.utils.nzb
assert nzb2media.utils.nzb
import nzb2media.utils.parsers
assert nzb2media.utils.parsers
import nzb2media.utils.paths
assert nzb2media.utils.paths
import nzb2media.utils.processes
assert nzb2media.utils.processes
import nzb2media.utils.torrent
assert nzb2media.utils.torrent
def test_import_nzb2media():
import nzb2media
assert nzb2media
import nzb2media.configuration
assert nzb2media.configuration
import nzb2media.databases
assert nzb2media.databases
import nzb2media.github_api
assert nzb2media.github_api
import nzb2media.main_db
assert nzb2media.main_db
import nzb2media.scene_exceptions
assert nzb2media.scene_exceptions
import nzb2media.transcoder
assert nzb2media.transcoder
import nzb2media.user_scripts
assert nzb2media.user_scripts
import nzb2media.version_check
assert nzb2media.version_check

7
tests/tool_test.py Normal file
View file

@ -0,0 +1,7 @@
import nzb2media.tool
def test_tool_in_path():
ffmpeg = nzb2media.tool.in_path('ffmpeg')
avprobe = nzb2media.tool.in_path('avprobe')
assert ffmpeg or avprobe

View file

@ -27,7 +27,7 @@ deps =
pytest-cov pytest-cov
-rrequirements.txt -rrequirements.txt
commands = commands =
{posargs:pytest --cov --cov-report=term-missing --cov-branch tests} {posargs:pytest -vvv --cov --cov-report=term-missing --cov-branch tests}
[flake8] [flake8]
max-line-length = 79 max-line-length = 79
@ -47,8 +47,10 @@ exclude =
ignore = ignore =
; -- flake8 -- ; -- flake8 --
; E501 line too long ; E501 line too long
; E722 do not use bare 'except' (duplicates B001)
; W503 line break before binary operator
; W505 doc line too long ; W505 doc line too long
E501, W505 E501, E722, W503, W505
; -- flake8-docstrings -- ; -- flake8-docstrings --
; D100 Missing docstring in public module ; D100 Missing docstring in public module