├── requirements.txt ├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── 3_question.md │ ├── 2_enhancement_request.md │ └── 1_bug_report.md ├── dev-requirements.txt ├── test ├── var │ └── plain.nzb.gz ├── TestBase.py ├── test_utils.py ├── test_config.py ├── test_database.py ├── test_feedscript.py ├── test_schedulerscript.py ├── test_sabpostprocess.py ├── test_scanscript.py └── test_multiscript.py ├── MANIFEST.in ├── .coveragerc ├── .travis.yml ├── setup.cfg ├── .gitignore ├── nzbget ├── __init__.py ├── PostProcessCommon.py ├── Utils.py ├── Logger.py ├── SchedulerScript.py ├── FeedScript.py ├── ScanScript.py ├── Database.py └── QueueScript.py ├── setup.py ├── packaging └── python-nzbget.spec └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml>=2.7 2 | six 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: caronc 2 | custom: ['https://www.paypal.me/lead2gold', ] 3 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | flake8 3 | mock 4 | pytest 5 | pytest-cov 6 | tox 7 | -------------------------------------------------------------------------------- /test/var/plain.nzb.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caronc/pynzbget/HEAD/test/var/plain.nzb.gz -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include requirements.txt 4 | recursive-include test * 5 | global-exclude *.pyc 6 | global-exclude __pycache__ 7 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description: 2 | **Related issue (if applicable):** # 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Support Question 3 | about: Ask a question about pynzbget 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | :question: **Question** 11 | 12 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | disable_warnings = no-data-collected 3 | branch = True 4 | source = 5 | nzbget 6 | 7 | [paths] 8 | source = 9 | nzbget 10 | .tox/*/lib/python*/site-packages/nzbget 11 | .tox/pypy/site-packages/nzbget 12 | 13 | [report] 14 | show_missing = True 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | 4 | python: 5 | - "2.7" 6 | 7 | install: 8 | - pip install . 9 | - pip install codecov 10 | - pip install -r dev-requirements.txt 11 | - pip install -r requirements.txt 12 | 13 | # run tests 14 | script: 15 | - coverage run -m pytest -vv 16 | 17 | after_success: 18 | - codecov 19 | 20 | notifications: 21 | email: false 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Enhancement Request 3 | about: Got a great idea? Let us know! 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | :bulb: **The Idea** 11 | 12 | 13 | :hammer: **Breaking Feature** 14 | 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | # ensure LICENSE is included in wheel metadata 6 | license_file = LICENSE 7 | 8 | [pycodestyle] 9 | # We exclude packages we don't maintain 10 | exclude = .eggs,.tox 11 | ignore = E722,W503,W504 12 | statistics = true 13 | 14 | [flake8] 15 | # We exclude packages we don't maintain 16 | exclude = .eggs,.tox 17 | ignore = E722,W503,W504 18 | statistics = true 19 | 20 | [aliases] 21 | test=pytest 22 | 23 | [tool:pytest] 24 | addopts = --verbose -ra 25 | python_files = test/test_*.py 26 | filterwarnings = 27 | once::Warning 28 | strict = true 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Report any errors and problems 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | :beetle: **Describe the bug** 11 | 12 | 13 | :bulb: **Screenshots and Logs** 14 | 15 | 16 | 17 | :computer: **Your System Details:** 18 | - OS: [e.g. RedHat v8.0] 19 | - Python Version: [e.g. Python v2.7] 20 | 21 | :crystal_ball: **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # vi swap files 7 | .*.sw? 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .pytest_cache/ 18 | .eggs/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Allow RPM SPEC files despite pyInstaller ignore 33 | !packaging/*.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | -------------------------------------------------------------------------------- /nzbget/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # simplify importing of comonly used modules 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | __title__ = 'pynzbget' 18 | __version__ = '0.6.4' 19 | __author__ = 'Chris Caron ' 20 | __license__ = 'GPLv3' 21 | __copywrite__ = 'Copyright 2014-2019 Chris Caron ' 22 | 23 | from .ScriptBase import * 24 | from .SchedulerScript import * 25 | from .PostProcessScript import * 26 | from .SABPostProcessScript import * 27 | from .FeedScript import * 28 | from .ScanScript import * 29 | from .QueueScript import * 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | install_requires = open('requirements.txt').readlines() 4 | setup( 5 | name='pynzbget', 6 | version='0.6.4', 7 | description="Provides a framework for NZBGet and SABnzbd script" 8 | "development.", 9 | license='GPLv3', 10 | long_description=open('README.md').read(), 11 | long_description_content_type='text/markdown', 12 | py_modules=['nzbget'], 13 | classifiers=[ 14 | 'Development Status :: 5 - Production/Stable', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 17 | 'Operating System :: OS Independent', 18 | 'Natural Language :: English', 19 | 'Programming Language :: Python', 20 | 'Programming Language :: Python :: 2.7', 21 | 'Programming Language :: Python :: 3', 22 | 'Topic :: Software Development :: Libraries :: Python Modules', 23 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 24 | ], 25 | keywords='nzbget,postprocess,framework,scripts,nzb,sabnzbd', 26 | author='Chris Caron', 27 | author_email='lead2gold@gmail.com', 28 | url='http://github.com/caronc/pynzbget', 29 | install_requires=install_requires, 30 | requires=['lxml', 'sqlite3'], 31 | setup_requires=["pytest-runner", ], 32 | tests_require=["pytest", ], 33 | packages=find_packages(), 34 | python_requires='>=2.7', 35 | ) 36 | -------------------------------------------------------------------------------- /test/TestBase.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A base testing class/library to help set some common testing vars 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | 17 | import os 18 | from getpass import getuser 19 | from tempfile import gettempdir 20 | from os.path import join 21 | from os import makedirs 22 | from shutil import rmtree 23 | 24 | from nzbget.ScriptBase import SYS_OPTS_RE 25 | from nzbget.ScriptBase import CFG_OPTS_RE 26 | from nzbget.ScriptBase import SHR_OPTS_RE 27 | 28 | TEMP_DIRECTORY = join( 29 | gettempdir(), 30 | 'nzbget-test-%s' % getuser(), 31 | ) 32 | 33 | 34 | class TestBase(object): 35 | def setup_method(self): 36 | """This method is run once before _each_ test method is executed""" 37 | try: 38 | rmtree(TEMP_DIRECTORY) 39 | except: 40 | pass 41 | makedirs(TEMP_DIRECTORY, 0o700) 42 | 43 | # Ensure we're residing in this directory 44 | os.chdir(TEMP_DIRECTORY) 45 | 46 | def teardown_method(self): 47 | """This method is run once after _each_ test method is executed""" 48 | # Clean out System Environment 49 | for k in os.environ.keys(): 50 | if SYS_OPTS_RE.match(k): 51 | del os.environ[k] 52 | continue 53 | if CFG_OPTS_RE.match(k): 54 | del os.environ[k] 55 | continue 56 | if SHR_OPTS_RE.match(k): 57 | del os.environ[k] 58 | continue 59 | 60 | try: 61 | rmtree(TEMP_DIRECTORY) 62 | except: 63 | pass 64 | -------------------------------------------------------------------------------- /nzbget/PostProcessCommon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | import re 5 | 6 | # Obfuscated Expression 7 | OBFUSCATED_PATH_RE = re.compile( 8 | '^[a-z0-9]+$', 9 | re.IGNORECASE, 10 | ) 11 | OBFUSCATED_FILE_RE = re.compile( 12 | '^[a-z0-9]+\.[a-z0-9]+$', 13 | re.IGNORECASE, 14 | ) 15 | 16 | 17 | class TOTAL_STATUS(object): 18 | """Cumulative (Total) Status of NZB Processing 19 | """ 20 | # everything OK 21 | SUCCESS = 'SUCCESS' 22 | # download is damaged but probably can be repaired; user intervention is 23 | # required; 24 | WARNING = 'WARNING' 25 | # download has failed or a serious error occurred during 26 | # post-processing (unpack, par); 27 | FAILURE = 'FAILURE' 28 | # download was deleted; post-processing scripts are usually not called in 29 | # this case; however it's possible to force calling scripts with command 30 | # "post-process again". 31 | DELETED = 'DELETED' 32 | 33 | # TOTALSTATUS Delimiter 34 | TOTALSTATUS_DELIMITER = '/' 35 | 36 | 37 | class SCRIPT_STATUS(object): 38 | """Summary status of the scripts executed before the current one 39 | """ 40 | # no other scripts were executed yet or all of them have ended with an exit 41 | # code of: NONE 42 | NONE = 'NONE' 43 | # all other scripts have ended with exit code "SUCCESS" 44 | SUCCESS = 'SUCCESS' 45 | # at least one of the script has failed 46 | FAILURE = 'FAILURE' 47 | 48 | 49 | class PAR_STATUS(object): 50 | """This is a depricated flag (as of NZBGet v13) but previously 51 | provides the status of the par-check of the downloaded content. 52 | """ 53 | # not checked: par-check is disabled or nzb-file does not contain 54 | # any par-files 55 | SKIPPED = 0 56 | # checked and failed to repair 57 | FAILURE = 1 58 | # checked and successfully repaired 59 | SUCCESS = 2 60 | # checked and can be repaired but repair is disabled 61 | DISABLED = 3 62 | 63 | 64 | class UNPACK_STATUS(object): 65 | """This is a depricated flag (as of NZBGet v13) but previously 66 | provides the status of the unpacking of the downloaded content. 67 | """ 68 | # unpack is disabled or was skipped due to nzb-file properties 69 | # or due to errors during par-check 70 | SKIPPED = 0 71 | # unpack failed 72 | FAILURE = 1 73 | # unpack was successful 74 | SUCCESS = 2 75 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for an SQLite 3 wrapper Class written for NZBGet 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import sys 18 | from os.path import dirname 19 | from os.path import join 20 | sys.path.insert(0, join(dirname(dirname(__file__)), 'nzbget')) 21 | 22 | from Utils import os_path_split 23 | from Utils import tidy_path 24 | from TestBase import TestBase 25 | 26 | 27 | class TestUtils(TestBase): 28 | def test_os_path_split(self): 29 | assert os_path_split('C:\\My Directory\\SubDirectory') == [ 30 | 'C:', 31 | 'My Directory', 32 | 'SubDirectory', 33 | ] 34 | assert os_path_split('relative/nix/pathname') == [ 35 | 'relative', 36 | 'nix', 37 | 'pathname', 38 | ] 39 | assert os_path_split('/absolute/nix/pathname') == [ 40 | '', 41 | 'absolute', 42 | 'nix', 43 | 'pathname', 44 | ] 45 | assert os_path_split('') == [] 46 | assert os_path_split('/') == ['', ] 47 | assert os_path_split('\\') == ['', ] 48 | 49 | # Weird Paths are fixed 50 | assert os_path_split('///////////') == ['', ] 51 | assert os_path_split('\\\\\\\\\\') == ['', ] 52 | 53 | # Trailing slashes are removed 54 | assert os_path_split('relative/nix/pathname/') == [ 55 | 'relative', 56 | 'nix', 57 | 'pathname', 58 | ] 59 | 60 | def test_tidy_path(self): 61 | 62 | # No Change 63 | assert tidy_path('C:\\My Directory\\SubDirectory') == \ 64 | 'C:\\My Directory\\SubDirectory' 65 | 66 | assert tidy_path('C:\\\\\My Directory\\\\\\SubDirectory ') == \ 67 | 'C:\\My Directory\\SubDirectory' 68 | 69 | assert tidy_path('C:\\') == 'C:\\' 70 | assert tidy_path('C:\\\\\\') == 'C:\\' 71 | assert tidy_path('/') == '/' 72 | assert tidy_path('///////////') == '/' 73 | assert tidy_path('////path///////') == '/path' 74 | assert tidy_path('////path/with spaces//////') == \ 75 | '/path/with spaces' 76 | 77 | # Network Paths 78 | assert tidy_path('\\\\network\\\\path\\ ') == \ 79 | '\\\\network\\path' 80 | -------------------------------------------------------------------------------- /nzbget/Utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # Some common utilities that may prove useful when processing downloads 4 | # 5 | # Copyright (C) 2014 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import re 18 | from os.path import expanduser 19 | 20 | try: 21 | from xml.sax.saxutils import unescape 22 | SAX_UNESCAPE = True 23 | except ImportError: 24 | SAX_UNESCAPE = False 25 | from HTMLParser import HTMLParser 26 | 27 | # Pre-Escape content since we reference it so much 28 | ESCAPED_PATH_SEPARATOR = re.escape('\\/') 29 | ESCAPED_WIN_PATH_SEPARATOR = re.escape('\\') 30 | ESCAPED_NUX_PATH_SEPARATOR = re.escape('/') 31 | 32 | TIDY_WIN_PATH_RE = re.compile( 33 | '(^[%s]{2}|[^%s\s][%s]|[\s][%s]{2}])([%s]+)' % ( 34 | ESCAPED_WIN_PATH_SEPARATOR, 35 | ESCAPED_WIN_PATH_SEPARATOR, 36 | ESCAPED_WIN_PATH_SEPARATOR, 37 | ESCAPED_WIN_PATH_SEPARATOR, 38 | ESCAPED_WIN_PATH_SEPARATOR, 39 | )) 40 | TIDY_WIN_TRIM_RE = re.compile( 41 | '^(.+[^:][^%s])[\s%s]*$' %( 42 | ESCAPED_WIN_PATH_SEPARATOR, 43 | ESCAPED_WIN_PATH_SEPARATOR, 44 | )) 45 | 46 | TIDY_NUX_PATH_RE = re.compile( 47 | '([%s])([%s]+)' % ( 48 | ESCAPED_NUX_PATH_SEPARATOR, 49 | ESCAPED_NUX_PATH_SEPARATOR, 50 | )) 51 | TIDY_NUX_TRIM_RE = re.compile( 52 | '([^%s])[\s%s]+$' % ( 53 | ESCAPED_NUX_PATH_SEPARATOR, 54 | ESCAPED_NUX_PATH_SEPARATOR, 55 | )) 56 | 57 | def os_path_split(path): 58 | """splits a path into a list by it's path delimiter 59 | 60 | hence: split_path('/etc/test/file') outputs: 61 | ['', 'etc', 'test', 'file'] 62 | 63 | relative paths don't have the blank entry at the head 64 | of the string: 65 | hence: split_path('relative/file') outputs: 66 | ['relative', 'file'] 67 | 68 | Paths can be reassembed as follows: 69 | assert '/'.join(split_path('/etc/test/file')) == \ 70 | '/etc/test/file' 71 | 72 | assert '/'.join(split_path('relative/file')) == \ 73 | 'relative/file' 74 | """ 75 | path = path.strip() 76 | if not path: 77 | return [] 78 | 79 | p_list = re.split('[%s]+' % ESCAPED_PATH_SEPARATOR, path) 80 | try: 81 | # remove trailing slashes 82 | while not p_list[-1] and len(p_list) > 1: 83 | p_list.pop() 84 | except IndexError: 85 | # Nothing passed in 86 | return [] 87 | return p_list 88 | 89 | def tidy_path(path): 90 | """take a filename and or directory and attempts to tidy it up by removing 91 | trailing slashes and correcting any formatting issues. 92 | 93 | For example: ////absolute//path// becomes: 94 | /absolute/path 95 | 96 | """ 97 | # Windows 98 | path = TIDY_WIN_PATH_RE.sub('\\1', path.strip()) 99 | # Linux 100 | path = TIDY_NUX_PATH_RE.sub('\\1', path.strip()) 101 | 102 | # Linux Based Trim 103 | path = TIDY_NUX_TRIM_RE.sub('\\1', path.strip()) 104 | # Windows Based Trim 105 | path = expanduser(TIDY_WIN_TRIM_RE.sub('\\1', path.strip())) 106 | return path 107 | 108 | def unescape_xml(content): 109 | """ 110 | Escapes XML content into it's regular string value 111 | """ 112 | 113 | if SAX_UNESCAPE: 114 | return unescape(content) 115 | return HTMLParser().unescape(content) 116 | -------------------------------------------------------------------------------- /test/test_config.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for the Config Tests Class 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import os 18 | from os.path import join 19 | 20 | from TestBase import TestBase 21 | from TestBase import TEMP_DIRECTORY 22 | 23 | from nzbget.ScriptBase import SYS_ENVIRO_ID 24 | from nzbget.ScriptBase import TEST_COMMAND 25 | from nzbget.ScriptBase import EXIT_CODE 26 | from nzbget.ScriptBase import SHELL_EXIT_CODE 27 | from nzbget.ScriptBase import SCRIPT_MODE 28 | from nzbget.PostProcessScript import PostProcessScript 29 | 30 | from nzbget.Logger import VERY_VERBOSE_DEBUG 31 | 32 | # Some constants to work with 33 | DIRECTORY = TEMP_DIRECTORY 34 | 35 | # For validation 36 | SCRIPTDIR = join(TEMP_DIRECTORY, 'scripts') 37 | 38 | VERSION = 18 39 | 40 | 41 | class TestConfigScript(TestBase): 42 | def setup_method(self): 43 | """This method is run once before _each_ test method is executed""" 44 | super(TestConfigScript, self).setup_method() 45 | 46 | # Create some environment variables 47 | os.environ['%sSCRIPTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 48 | os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 49 | os.environ['%sVERSION' % SYS_ENVIRO_ID] = str(VERSION) 50 | 51 | def teardown_method(self): 52 | """This method is run once after _each_ test method is executed""" 53 | # Eliminate any variables defined 54 | if '%sSCRIPTDIR' % SYS_ENVIRO_ID in os.environ: 55 | del os.environ['%sSCRIPTDIR' % SYS_ENVIRO_ID] 56 | del os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] 57 | del os.environ['%sVERSION' % SYS_ENVIRO_ID] 58 | 59 | # common 60 | super(TestConfigScript, self).teardown_method() 61 | 62 | def test_action_detection(self): 63 | """ 64 | A Series of tests to show how the detection works 65 | """ 66 | os.environ[TEST_COMMAND] = 'MyTestAction' 67 | 68 | class MyTestObj(PostProcessScript): 69 | # Definition = action_ 70 | def action_MyTestAction(self, *args, **kwargs): 71 | """ 72 | Test object 73 | """ 74 | return True 75 | 76 | # Create our Object 77 | obj = MyTestObj( 78 | logger=False, 79 | debug=VERY_VERBOSE_DEBUG, 80 | ) 81 | 82 | assert(obj.script_mode == SCRIPT_MODE.CONFIG_ACTION) 83 | assert(obj.run() == EXIT_CODE.SUCCESS) 84 | 85 | # Now we try with the lower case version 86 | class MyTestObj(PostProcessScript): 87 | # Lower Case 88 | def action_mytestaction(self, *args, **kwargs): 89 | """ 90 | Test object 91 | """ 92 | return True 93 | 94 | # Create our Object 95 | obj = MyTestObj( 96 | logger=False, 97 | debug=VERY_VERBOSE_DEBUG, 98 | ) 99 | 100 | assert(obj.run() == EXIT_CODE.SUCCESS) 101 | assert(obj.script_mode == SCRIPT_MODE.CONFIG_ACTION) 102 | 103 | # We do nothing if neither of these functions are present. 104 | # Now we try with the lower case version 105 | class MyTestObj(PostProcessScript): 106 | pass 107 | 108 | # Create our Object 109 | obj = MyTestObj( 110 | logger=False, 111 | debug=VERY_VERBOSE_DEBUG, 112 | ) 113 | 114 | assert(obj.script_mode == SCRIPT_MODE.NONE) 115 | assert(obj.run() == SHELL_EXIT_CODE.SUCCESS) 116 | 117 | # If the environment variable doesn't exist at all 118 | # Then for sure we don't run either test function 119 | del os.environ[TEST_COMMAND] 120 | 121 | # Now we try with the lower case version 122 | class MyTestObj(PostProcessScript): 123 | # Lower Case 124 | def action_mytestaction(self, *args, **kwargs): 125 | """ 126 | Test object 127 | """ 128 | return True 129 | 130 | # Definition = action_ 131 | def action_MyTestAction(self, *args, **kwargs): 132 | """ 133 | Test object 134 | """ 135 | return True 136 | 137 | # Create our Object 138 | obj = MyTestObj( 139 | logger=False, 140 | debug=VERY_VERBOSE_DEBUG, 141 | ) 142 | 143 | assert(obj.script_mode == SCRIPT_MODE.NONE) 144 | assert(obj.run() == SHELL_EXIT_CODE.SUCCESS) 145 | -------------------------------------------------------------------------------- /test/test_database.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for an SQLite 3 wrapper Class written for NZBGet 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | from TestBase import TestBase 18 | from TestBase import TEMP_DIRECTORY 19 | 20 | from os import unlink 21 | from os.path import join 22 | 23 | from nzbget.Database import Database 24 | from nzbget.Database import Category 25 | from nzbget.Database import NZBGET_DATABASE_VERSION 26 | from nzbget.Logger import VERY_VERBOSE_DEBUG 27 | 28 | # Temporary Directory 29 | DATABASE = join(TEMP_DIRECTORY, 'nzbget_test.db') 30 | 31 | KEY = 'The.Perfect.Name.nzb' 32 | 33 | 34 | class TestDatabase(TestBase): 35 | def teardown_method(self): 36 | """This method is run once after _each_ test method is executed""" 37 | try: 38 | unlink(DATABASE) 39 | except: 40 | pass 41 | 42 | # common 43 | super(TestDatabase, self).teardown_method() 44 | 45 | def test_schema_init(self): 46 | 47 | db = Database( 48 | container=KEY, 49 | database=DATABASE, 50 | reset=True, 51 | 52 | debug=VERY_VERBOSE_DEBUG, 53 | ) 54 | assert db._get_version() == NZBGET_DATABASE_VERSION 55 | assert db._schema_okay() 56 | 57 | def test_key_manip(self): 58 | 59 | db = Database( 60 | container=KEY, 61 | database=DATABASE, 62 | reset=True, 63 | 64 | debug=VERY_VERBOSE_DEBUG, 65 | ) 66 | # New Keys 67 | assert db.set('MY_KEY', 'MY_VALUE') 68 | assert db.get('MY_KEY') == 'MY_VALUE' 69 | # Updates 70 | assert db.set('MY_KEY', 'MY_NEW_VALUE') 71 | assert db.get('MY_KEY') == 'MY_NEW_VALUE' 72 | # Other Keys 73 | assert db.set('MY_OTHER_KEY', 'MY_OTHER_VALUE') 74 | assert db.get('MY_OTHER_KEY') == 'MY_OTHER_VALUE' 75 | 76 | del db 77 | # Content saved across sessions 78 | db = Database( 79 | container=KEY, 80 | database=DATABASE, 81 | 82 | debug=VERY_VERBOSE_DEBUG, 83 | ) 84 | assert db.get('MY_KEY') == 'MY_NEW_VALUE' 85 | assert db.get('MY_OTHER_KEY') == 'MY_OTHER_VALUE' 86 | 87 | # Removing 88 | assert db.unset('MY_OTHER_KEY') 89 | assert db.get('MY_OTHER_KEY', 'MISSING') == 'MISSING' 90 | 91 | def test_category_manip(self): 92 | 93 | db = Database( 94 | container=KEY, 95 | database=DATABASE, 96 | reset=True, 97 | 98 | debug=VERY_VERBOSE_DEBUG, 99 | ) 100 | # New Keys 101 | assert db.set('MY_KEY', 'MY_VALUE') 102 | assert db.get('MY_KEY') == 'MY_VALUE' 103 | assert db.set('MY_KEY', 'MY_NEW_VALUE', category=Category.NZB) 104 | # No change in key 105 | assert db.get('MY_KEY') == 'MY_VALUE' 106 | # However, different category has different key mapped 107 | assert db.get('MY_KEY', category=Category.NZB) == 'MY_NEW_VALUE' 108 | 109 | # Updates 110 | assert db.set('MY_KEY', 'ANOTHER_VALUE', category=Category.NZB) 111 | assert db.get('MY_KEY', category=Category.NZB) == 'ANOTHER_VALUE' 112 | del db 113 | 114 | # Content saved across sessions 115 | db = Database( 116 | container=KEY, 117 | database=DATABASE, 118 | 119 | debug=VERY_VERBOSE_DEBUG, 120 | ) 121 | assert db.get('MY_KEY') == 'MY_VALUE' 122 | assert db.get('MY_KEY', category=Category.NZB) == 'ANOTHER_VALUE' 123 | 124 | def test_key_purges01(self): 125 | 126 | db = Database( 127 | container=KEY, 128 | database=DATABASE, 129 | reset=True, 130 | 131 | debug=VERY_VERBOSE_DEBUG, 132 | ) 133 | 134 | assert db.set('MY_KEY', 'MY_VALUE') 135 | assert db.get('MY_KEY') == 'MY_VALUE' 136 | 137 | assert db.set('MY_OTHER_KEY', 'MY_OTHER_VALUE') 138 | assert db.get('MY_OTHER_KEY') == 'MY_OTHER_VALUE' 139 | 140 | # purge entries (0 = all) 141 | 142 | def test_key_purges02(self): 143 | 144 | db = Database( 145 | container=KEY, 146 | database=DATABASE, 147 | reset=True, 148 | 149 | debug=VERY_VERBOSE_DEBUG, 150 | ) 151 | 152 | assert db.set('MY_KEY', 'MY_VALUE') 153 | assert db.get('MY_KEY') == 'MY_VALUE' 154 | 155 | assert db.set('MY_OTHER_KEY', 'MY_OTHER_VALUE') 156 | assert db.get('MY_OTHER_KEY') == 'MY_OTHER_VALUE' 157 | 158 | # purge entries (0 = all) 159 | db.prune(0) 160 | assert db.get('MY_KEY') is None 161 | assert db.get('MY_OTHER_KEY') is None 162 | -------------------------------------------------------------------------------- /packaging/python-nzbget.spec: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2019 Chris Caron 2 | # All rights reserved. 3 | # 4 | # This code is licensed under the MIT License. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files(the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions : 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | ############################################################################### 24 | %global with_python2 1 25 | %global with_python3 1 26 | 27 | %if 0%{?fedora} || 0%{?rhel} >= 8 28 | # Python v2 Support dropped 29 | %global with_python2 0 30 | %endif # fedora and/or rhel7 31 | 32 | %if 0%{?_module_build} 33 | %bcond_with tests 34 | %else 35 | # When bootstrapping Python, we cannot test this yet 36 | %bcond_without tests 37 | %endif # module_build 38 | 39 | %if 0%{?rhel} && 0%{?rhel} <= 7 40 | %global with_python3 0 41 | %endif # using rhel7 42 | 43 | %global pypi_name pynzbget 44 | %global pkg_name nzbget 45 | 46 | %global common_description %{expand: \ 47 | A python wrapper to simplify the handling of NZBGet and SABnzbd Scripts} 48 | 49 | Name: python-%{pkg_name} 50 | Version: 0.6.4 51 | Release: 1%{?dist} 52 | Summary: Simplify the development and deployment of NZBGet and SABnzbd scripts 53 | License: GPLv3 54 | URL: https://github.com/caronc/%{pypi_name} 55 | Source0: %{url}/archive/v%{version}/%{pypi_name}-%{version}.tar.gz 56 | # this patch allows version of requests that ships with RHEL v7 to 57 | # correctly handle test coverage. It also removes reference to a 58 | # extra check not supported in py.test in EPEL7 builds 59 | BuildArch: noarch 60 | 61 | %description %{common_description} 62 | 63 | %if 0%{?with_python2} 64 | %package -n python2-%{pkg_name} 65 | Summary: Simplify the development and deployment of NZBGet and SABnzbd scripts 66 | %{?python_provide:%python_provide python2-%{pkg_name}} 67 | 68 | BuildRequires: sqlite 69 | BuildRequires: python2-devel 70 | BuildRequires: python-six 71 | %if 0%{?rhel} && 0%{?rhel} <= 7 72 | BuildRequires: python-lxml 73 | %else 74 | BuildRequires: python2-lxml 75 | %endif # using rhel7 76 | 77 | Requires: sqlite 78 | Requires: python-six 79 | %if 0%{?rhel} && 0%{?rhel} <= 7 80 | Requires: python-lxml 81 | %else 82 | Requires: python2-lxml 83 | %endif # using rhel7 84 | 85 | %if %{with tests} 86 | BuildRequires: python-mock 87 | BuildRequires: python2-pytest-runner 88 | BuildRequires: python2-pytest 89 | %endif # with_tests 90 | 91 | %description -n python2-%{pkg_name} %{common_description} 92 | %endif # with_python2 93 | 94 | %if 0%{?with_python3} 95 | %package -n python%{python3_pkgversion}-%{pkg_name} 96 | Summary: Simplify the development and deployment of NZBGet and SABnzbd scripts 97 | %{?python_provide:%python_provide python%{python3_pkgversion}-%{pkg_name}} 98 | 99 | BuildRequires: python%{python3_pkgversion}-devel 100 | BuildRequires: python%{python3_pkgversion}-six 101 | BuildRequires: python%{python3_pkgversion}-lxml 102 | Requires: python%{python3_pkgversion}-six 103 | Requires: python%{python3_pkgversion}-lxml 104 | 105 | %if %{with tests} 106 | BuildRequires: python%{python3_pkgversion}-mock 107 | BuildRequires: python%{python3_pkgversion}-pytest 108 | BuildRequires: python%{python3_pkgversion}-pytest-runner 109 | %endif # with_tests 110 | 111 | %description -n python%{python3_pkgversion}-%{pkg_name} %{common_description} 112 | %endif # with_python3 113 | 114 | %prep 115 | %setup -q -n %{pypi_name}-%{version} 116 | 117 | %build 118 | %if 0%{?with_python2} 119 | %py2_build 120 | %endif # with_python2 121 | %if 0%{?with_python3} 122 | %py3_build 123 | %endif # with_python3 124 | 125 | %install 126 | %if 0%{?with_python2} 127 | %py2_install 128 | %endif # with_python2 129 | %if 0%{?with_python3} 130 | %py3_install 131 | %endif # with_python3 132 | 133 | %if %{with tests} 134 | %if 0%{?rhel} && 0%{?rhel} <= 7 135 | # Can not do testing with RHEL7 because the version of py.test is too old 136 | %else 137 | %check 138 | %if 0%{?with_python2} 139 | PYTHONPATH=%{buildroot}%{python2_sitelib} py.test 140 | %endif # with_python2 141 | %if 0%{?with_python3} 142 | PYTHONPATH=%{buildroot}%{python3_sitelib} py.test-%{python3_version} 143 | %endif # with_python3 144 | %endif # rhel7 145 | %endif # with_tests 146 | %if 0%{?with_python2} 147 | %files -n python2-%{pkg_name} 148 | %license LICENSE 149 | %doc README.md 150 | %{python2_sitelib}/%{pkg_name} 151 | %{python2_sitelib}/*.egg-info 152 | %endif # with_python2 153 | 154 | %if 0%{?with_python3} 155 | %files -n python%{python3_pkgversion}-%{pkg_name} 156 | %license LICENSE 157 | %doc README.md 158 | %{python3_sitelib}/%{pkg_name} 159 | %{python3_sitelib}/*.egg-info 160 | %endif # with_python3 161 | 162 | %changelog 163 | * Fri Sep 20 2019 Chris Caron - 0.6.4-1 164 | - Updated to v0.6.4 165 | * Fri Jun 14 2019 Chris Caron - 0.6.3-1 166 | - Initial release of v0.6.3 167 | -------------------------------------------------------------------------------- /test/test_feedscript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for the FeedScript Class 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import os 18 | 19 | from TestBase import TestBase 20 | from TestBase import TEMP_DIRECTORY 21 | 22 | from nzbget.ScriptBase import CFG_ENVIRO_ID 23 | from nzbget.ScriptBase import SYS_ENVIRO_ID 24 | 25 | from nzbget.FeedScript import FeedScript 26 | from nzbget.FeedScript import FEED_ENVIRO_ID 27 | 28 | from nzbget.Logger import VERY_VERBOSE_DEBUG 29 | 30 | # Some constants to work with 31 | FEEDID = "1" 32 | FEED_FILENAME = 'MyTest.nzb' 33 | 34 | 35 | class TestFeedScript(TestBase): 36 | def setup_method(self): 37 | """This method is run once before _each_ test method is executed""" 38 | super(TestFeedScript, self).setup_method() 39 | 40 | # Create some environment variables 41 | os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 42 | os.environ['%sFEEDID' % FEED_ENVIRO_ID] = FEEDID 43 | os.environ['%sFILENAME' % FEED_ENVIRO_ID] = FEED_FILENAME 44 | 45 | def teardown_method(self): 46 | """This method is run once after _each_ test method is executed""" 47 | # Eliminate any variables defined 48 | del os.environ['%sFEEDID' % FEED_ENVIRO_ID] 49 | del os.environ['%sFILENAME' % FEED_ENVIRO_ID] 50 | 51 | # common 52 | super(TestFeedScript, self).teardown_method() 53 | 54 | def test_environment_varable_init(self): 55 | """ 56 | Testing NZBGet Script initialization using environment variables 57 | """ 58 | # a NZB Logger set to False uses stderr 59 | script = FeedScript(logger=False, debug=VERY_VERBOSE_DEBUG) 60 | assert script.feedid == int(FEEDID) 61 | assert script.filename == FEED_FILENAME 62 | 63 | assert script.system['FEEDID'] == int(FEEDID) 64 | assert script.system['FILENAME'] == FEED_FILENAME 65 | 66 | assert script.get('TEMPDIR') == TEMP_DIRECTORY 67 | assert script.get('FEEDID') == int(FEEDID) 68 | assert script.get('FILENAME') == FEED_FILENAME 69 | 70 | assert len(script.config) == 1 71 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 72 | 73 | assert os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] == TEMP_DIRECTORY 74 | assert os.environ['%sFEEDID' % FEED_ENVIRO_ID] == FEEDID 75 | assert os.environ['%sFILENAME' % FEED_ENVIRO_ID] == FEED_FILENAME 76 | 77 | def test_environment_override(self): 78 | """ 79 | Testing NZBGet Script initialization using forced variables that 80 | should take priority over global ones 81 | """ 82 | feedid = int(FEEDID) + 1 83 | filename = 'TEST_OVERLOAD_%s' % FEED_FILENAME 84 | script = FeedScript( 85 | logger=False, 86 | debug=VERY_VERBOSE_DEBUG, 87 | feedid=feedid, 88 | filename=filename, 89 | ) 90 | 91 | assert script.feedid == feedid 92 | 93 | assert script.system['TEMPDIR'] == TEMP_DIRECTORY 94 | assert script.system['FEEDID'] == feedid 95 | assert script.system['FILENAME'] == filename 96 | 97 | assert len(script.config) == 1 98 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 99 | 100 | assert os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] == TEMP_DIRECTORY 101 | assert os.environ['%sFEEDID' % FEED_ENVIRO_ID] == str(feedid) 102 | assert os.environ['%sFILENAME' % FEED_ENVIRO_ID] == filename 103 | 104 | def test_get_feed(self): 105 | 106 | script = FeedScript(logger=False, debug=VERY_VERBOSE_DEBUG) 107 | feed = script.get_feed() 108 | assert len(feed) == 0 109 | 110 | def test_set_and_get(self): 111 | # a NZB Logger set to False uses stderr 112 | script = FeedScript(logger=False, debug=VERY_VERBOSE_DEBUG) 113 | 114 | KEY = 'MY_VAR' 115 | VALUE = 'MY_VALUE' 116 | 117 | # Value does not exist yet 118 | assert script.get(KEY) is None 119 | assert script.get(KEY, 'Default') == 'Default' 120 | assert script.set(KEY, VALUE) is True 121 | assert script.get(KEY, 'Default') == VALUE 122 | 123 | def test_config_varable_init(self): 124 | valid_entries = { 125 | 'MY_CONFIG_ENTRY': 'Option A', 126 | 'ENTRY_WITH_234_NUMBERS': 'Option B', 127 | '123443': 'Option C', 128 | } 129 | invalid_entries = { 130 | 'CONFIG_ENtry_skipped': 'Option', 131 | 'CONFIG_ENtry_$#': 'Option', 132 | # Empty 133 | '': 'Option', 134 | } 135 | for k, v in valid_entries.items(): 136 | os.environ['%s%s' % (CFG_ENVIRO_ID, k)] = v 137 | for k, v in invalid_entries.items(): 138 | os.environ['%s%s' % (CFG_ENVIRO_ID, k)] = v 139 | 140 | script = FeedScript() 141 | for k, v in valid_entries.items(): 142 | assert k in script.config 143 | assert script.config[k] == v 144 | 145 | for k in invalid_entries.keys(): 146 | assert k not in script.config 147 | 148 | # Cleanup 149 | for k, v in valid_entries.items(): 150 | del os.environ['%s%s' % (CFG_ENVIRO_ID, k)] 151 | for k, v in invalid_entries.items(): 152 | del os.environ['%s%s' % (CFG_ENVIRO_ID, k)] 153 | -------------------------------------------------------------------------------- /test/test_schedulerscript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for the SchedulerScript Class 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import os 18 | 19 | from TestBase import TestBase 20 | from TestBase import TEMP_DIRECTORY 21 | 22 | from nzbget.ScriptBase import CFG_ENVIRO_ID 23 | from nzbget.ScriptBase import SYS_ENVIRO_ID 24 | from nzbget.SchedulerScript import SchedulerScript 25 | from nzbget.SchedulerScript import SCHEDULER_ENVIRO_ID 26 | from nzbget.SchedulerScript import TASK_ENVIRO_ID 27 | 28 | from nzbget.Logger import VERY_VERBOSE_DEBUG 29 | 30 | # Some constants to work with 31 | TASKID = "1" 32 | TASK_PARAM = 'MyScript.py' 33 | TASK_TIME = '00:00,00:15' 34 | 35 | 36 | class TestSchedulerScript(TestBase): 37 | def setup_method(self): 38 | """This method is run once before _each_ test method is executed""" 39 | super(TestSchedulerScript, self).setup_method() 40 | 41 | # Create some environment variables 42 | os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 43 | os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] = TASKID 44 | os.environ['%s%s%s_PARAM' % ( 45 | SCHEDULER_ENVIRO_ID, TASK_ENVIRO_ID, TASKID)] = TASK_PARAM 46 | os.environ['%s%s%s_TIME' % ( 47 | SCHEDULER_ENVIRO_ID, TASK_ENVIRO_ID, TASKID)] = TASK_TIME 48 | 49 | def teardown_method(self): 50 | """This method is run once after _each_ test method is executed""" 51 | # Eliminate any variables defined 52 | del os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] 53 | del os.environ['%s%s%s_PARAM' % ( 54 | SCHEDULER_ENVIRO_ID, TASK_ENVIRO_ID, TASKID)] 55 | del os.environ['%s%s%s_TIME' % ( 56 | SCHEDULER_ENVIRO_ID, TASK_ENVIRO_ID, TASKID)] 57 | 58 | # common 59 | super(TestSchedulerScript, self).teardown_method() 60 | 61 | def test_environment_varable_init(self): 62 | """ 63 | Testing NZBGet Script initialization using environment variables 64 | """ 65 | # a NZB Logger set to False uses stderr 66 | script = SchedulerScript(logger=False, debug=VERY_VERBOSE_DEBUG) 67 | assert script.taskid == int(TASKID) 68 | 69 | assert script.system['TASKID'] == int(TASKID) 70 | assert script.system['%s%s_PARAM' % ( 71 | TASK_ENVIRO_ID, TASKID)] == TASK_PARAM 72 | assert script.system['%s%s_TIME' % ( 73 | TASK_ENVIRO_ID, TASKID)] == TASK_TIME 74 | 75 | assert script.get('TEMPDIR') == TEMP_DIRECTORY 76 | assert script.get('TASKID') == int(TASKID) 77 | assert script.get('%s%s_PARAM' % ( 78 | TASK_ENVIRO_ID, TASKID)) == TASK_PARAM 79 | assert script.get('%s%s_TIME' % ( 80 | TASK_ENVIRO_ID, TASKID)) == TASK_TIME 81 | 82 | assert len(script.config) == 1 83 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 84 | 85 | assert os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] == TEMP_DIRECTORY 86 | assert os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] == TASKID 87 | assert os.environ['%s%s%s_PARAM' % ( 88 | SCHEDULER_ENVIRO_ID, TASK_ENVIRO_ID, TASKID)] == TASK_PARAM 89 | assert os.environ['%s%s%s_TIME' % ( 90 | SCHEDULER_ENVIRO_ID, TASK_ENVIRO_ID, TASKID)] == TASK_TIME 91 | 92 | def test_environment_override(self): 93 | """ 94 | Testing NZBGet Script initialization using forced variables that 95 | should take priority over global ones 96 | """ 97 | taskid = int(TASKID) + 1 98 | script = SchedulerScript( 99 | logger=False, 100 | debug=VERY_VERBOSE_DEBUG, 101 | 102 | taskid=taskid, 103 | ) 104 | 105 | assert script.taskid == taskid 106 | 107 | assert script.system['TEMPDIR'] == TEMP_DIRECTORY 108 | assert script.system['TASKID'] == taskid 109 | 110 | assert len(script.config) == 1 111 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 112 | 113 | assert os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] == TEMP_DIRECTORY 114 | assert os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] == str(taskid) 115 | 116 | def test_get_task(self): 117 | script = SchedulerScript(logger=False, debug=VERY_VERBOSE_DEBUG) 118 | task = script.get_task() 119 | assert len(task) == 2 120 | 121 | def test_set_and_get(self): 122 | # a NZB Logger set to False uses stderr 123 | script = SchedulerScript(logger=False, debug=VERY_VERBOSE_DEBUG) 124 | 125 | KEY = 'MY_VAR' 126 | VALUE = 'MY_VALUE' 127 | 128 | # Value doe snot exist yet 129 | assert script.get(KEY) is None 130 | assert script.get(KEY, 'Default') == 'Default' 131 | assert script.set(KEY, VALUE) is True 132 | assert script.get(KEY, 'Default') == VALUE 133 | 134 | def test_config_varable_init(self): 135 | valid_entries = { 136 | 'MY_CONFIG_ENTRY': 'Option A', 137 | 'ENTRY_WITH_234_NUMBERS': 'Option B', 138 | '123443': 'Option C', 139 | } 140 | invalid_entries = { 141 | 'CONFIG_ENtry_skipped': 'Option', 142 | 'CONFIG_ENtry_$#': 'Option', 143 | # Empty 144 | '': 'Option', 145 | } 146 | for k, v in valid_entries.items(): 147 | os.environ['%s%s' % (CFG_ENVIRO_ID, k)] = v 148 | for k, v in invalid_entries.items(): 149 | os.environ['%s%s' % (CFG_ENVIRO_ID, k)] = v 150 | 151 | script = SchedulerScript() 152 | for k, v in valid_entries.items(): 153 | assert k in script.config 154 | assert script.config[k] == v 155 | 156 | for k in invalid_entries.keys(): 157 | assert k not in script.config 158 | 159 | # Cleanup 160 | for k, v in valid_entries.items(): 161 | del os.environ['%s%s' % (CFG_ENVIRO_ID, k)] 162 | for k, v in invalid_entries.items(): 163 | del os.environ['%s%s' % (CFG_ENVIRO_ID, k)] 164 | -------------------------------------------------------------------------------- /nzbget/Logger.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A scripting wrapper for NZBGet Post and Pre Processing 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | """ 18 | This class simplifies the setup of the logging to screen or file. It was 19 | written to simplify the logging to and from an NZBGet Script 20 | """ 21 | import sys 22 | import six 23 | import logging 24 | import logging.handlers 25 | from logging import Logger 26 | 27 | from os import getpid 28 | 29 | # Monkey Patch 30 | logging.raiseExceptions = 0 31 | 32 | # Logging Levels 33 | DETAIL = 19 34 | DEBUG = logging.DEBUG 35 | VERBOSE_DEBUG = logging.DEBUG-1 36 | VERY_VERBOSE_DEBUG = logging.DEBUG-2 37 | 38 | # Ensure Levels are Globally Added To logging module 39 | logging.addLevelName(DETAIL, "DETAIL") 40 | logging.addLevelName(VERBOSE_DEBUG, "VDEBUG") 41 | logging.addLevelName(VERY_VERBOSE_DEBUG, "VVDEBUG") 42 | 43 | def detail(self, message, *args, **kwargs): 44 | # logger takes its '*args' as 'args'. 45 | self._log(DETAIL, message, args, **kwargs) 46 | 47 | def vdebug(self, message, *args, **kwargs): 48 | # logger takes its '*args' as 'args'. 49 | self._log(VERBOSE_DEBUG, message, args, **kwargs) 50 | 51 | def vvdebug(self, message, *args, **kwargs): 52 | # logger takes its '*args' as 'args'. 53 | self._log(VERY_VERBOSE_DEBUG, message, args, **kwargs) 54 | 55 | logging.Logger.detail = detail 56 | logging.Logger.vdebug = vdebug 57 | logging.Logger.vvdebug = vvdebug 58 | 59 | def destroy_logger(name=None): 60 | """ 61 | Destroys any log files assiated with name and/or logger 62 | """ 63 | if name is None: 64 | name = __name__ 65 | 66 | if isinstance(name, Logger): 67 | logger = name 68 | elif isinstance(name, six.string_types): 69 | logger = logging.getLogger(name) 70 | else: 71 | # not supported 72 | return 73 | 74 | logger = name 75 | if hasattr(logger, 'handlers'): 76 | # Destroy Logger 77 | for l in logger.handlers: 78 | logger.removeHandler(l) 79 | l.flush() 80 | try: 81 | l.close() 82 | except KeyError: 83 | # https://bugzilla.redhat.com/show_bug.cgi?id=573782 84 | # Bug 573782 - python logging's fileConfig causes 85 | # KeyError on shutdown 86 | pass 87 | 88 | def init_logger(name=None, logger=True, debug=False, nzbget_mode=True, 89 | daily=False, bytecount=5242880, logcount=3, encoding=None): 90 | """ 91 | Generate Logger 92 | 93 | name: Defines a name to help identify each log entry 94 | logger: - If set to 'None', then logging is disabled. 95 | - If set to 'True', then content is sent to stdout 96 | is used. 97 | - If set to 'False', then content is sent to 98 | stderr 99 | - If set to a string, then the string is presumed 100 | to be the filename where the logging should be 101 | sent. 102 | - If defined as a logger, then that is presumed 103 | to be the log file to use. 104 | debug: Enable extra debug log entries, if this is an interger 105 | value, then that is the log level set 106 | nzbget_mode: Log formatting does not include date/time, pid etc 107 | because nzbget wraps all this for us. 108 | daily: Rotate logs by day (instead of by size) 109 | encoding: Can be set to something like 'bz2' to have all 110 | content created compressed 111 | """ 112 | 113 | if isinstance(logger, Logger): 114 | # Update handlers only 115 | for l in logger.handlers: 116 | if not nzbget_mode: 117 | l.setFormatter(logging. \ 118 | Formatter("%(asctime)s - " + str(getpid()) + \ 119 | " - %(levelname)s - %(message)s")) 120 | else: 121 | l.setFormatter(logging. \ 122 | Formatter("[%(levelname)s] %(message)s")) 123 | 124 | # Support NZBGET Detail Messages 125 | logging.addLevelName(DETAIL, 'DETAIL') 126 | 127 | if not nzbget_mode: 128 | logging.addLevelName(logging.DEBUG, 'DEBUG') 129 | logging.addLevelName(VERBOSE_DEBUG, 'VDEBUG') 130 | logging.addLevelName(VERY_VERBOSE_DEBUG, 'VVDEBUG') 131 | 132 | else: 133 | # Level Name for [debug] has to be [info] or it simply won't print 134 | logging.addLevelName(logging.DEBUG, 'INFO] [DEBUG') 135 | logging.addLevelName(VERBOSE_DEBUG, 'INFO] [VDEBUG') 136 | logging.addLevelName(VERY_VERBOSE_DEBUG, 'INFO] [VVDEBUG') 137 | 138 | return logger 139 | 140 | if name is None: 141 | # Namespace 142 | name = __name__ 143 | 144 | # Perpare Logger 145 | _logger = logging.getLogger(name) 146 | 147 | # Ensure the logger isn't already initialized 148 | # to limit the number of handlers to 1, we sweep anything 149 | # that may or may not have already been created 150 | if isinstance(_logger, Logger): 151 | for l in _logger.handlers: 152 | _logger.removeHandler(l) 153 | l.flush() 154 | l.close() 155 | 156 | if logger is None: 157 | # Create a dummy log file without a handler 158 | _logger.setLevel(logging.CRITICAL) 159 | #logging.disable(logging.ERROR) 160 | # no logging handler nessisary 161 | return _logger 162 | 163 | elif isinstance(logger, six.string_types): 164 | # prepare rotating handler using the log file specified 165 | if not daily: 166 | h1 = logging.handlers.RotatingFileHandler( 167 | logger, 168 | maxBytes=bytecount, 169 | backupCount=logcount, 170 | encoding=encoding, 171 | ) 172 | else: 173 | h1 = logging.handlers.TimedRotatingFileHandler( 174 | logger, 175 | when='midnight', 176 | interval=1, 177 | backupCount=logcount, 178 | encoding=encoding, 179 | ) 180 | 181 | elif logger: 182 | # stdout 183 | h1 = logging.StreamHandler(sys.stdout) 184 | else: 185 | # stderr 186 | h1 = logging.StreamHandler(sys.stderr) 187 | 188 | if debug is True: 189 | _logger.setLevel(logging.DEBUG) 190 | h1.setLevel(logging.DEBUG) 191 | 192 | elif debug in (False, None): 193 | # Default 194 | _logger.setLevel(DETAIL) 195 | h1.setLevel(DETAIL) 196 | else: 197 | try: 198 | debug = int(debug) 199 | _logger.setLevel(debug) 200 | h1.setLevel(debug) 201 | 202 | except (ValueError, TypeError): 203 | # Default 204 | _logger.setLevel(DETAIL) 205 | h1.setLevel(DETAIL) 206 | 207 | # Format logger 208 | if not nzbget_mode: 209 | h1.setFormatter(logging. \ 210 | Formatter("%(asctime)s - " + str(getpid()) + 211 | " - %(levelname)s - %(message)s")) 212 | logging.addLevelName(logging.DEBUG, 'DEBUG') 213 | logging.addLevelName(VERBOSE_DEBUG, 'VDEBUG') 214 | logging.addLevelName(VERY_VERBOSE_DEBUG, 'VVDEBUG') 215 | 216 | else: 217 | h1.setFormatter(logging. \ 218 | Formatter("[%(levelname)s] %(message)s")) 219 | # Level Name for [debug] has to be [info] or it simply won't print 220 | logging.addLevelName(logging.DEBUG, 'INFO] [DEBUG') 221 | logging.addLevelName(VERBOSE_DEBUG, 'INFO] [VDEBUG') 222 | logging.addLevelName(VERY_VERBOSE_DEBUG, 'INFO] [VVDEBUG') 223 | 224 | # Add Handler 225 | _logger.addHandler(h1) 226 | 227 | return _logger 228 | -------------------------------------------------------------------------------- /test/test_sabpostprocess.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for the SABnzbd SABPostProcessScript Class 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import os 18 | from os import makedirs 19 | from os.path import basename 20 | from os.path import dirname 21 | from os.path import isfile 22 | from os.path import join 23 | 24 | from TestBase import TestBase 25 | from TestBase import TEMP_DIRECTORY 26 | 27 | from nzbget.ScriptBase import SAB_ENVIRO_ID 28 | from nzbget.ScriptBase import SHELL_EXIT_CODE 29 | 30 | from nzbget.SABPostProcessScript import SABPostProcessScript 31 | from nzbget.SABPostProcessScript import PP_STATUS 32 | from nzbget.Logger import VERY_VERBOSE_DEBUG 33 | 34 | 35 | from shutil import rmtree 36 | 37 | # Some constants to work with 38 | DIRECTORY = TEMP_DIRECTORY 39 | COMPLETE_DIRECTORY = TEMP_DIRECTORY 40 | NZBNAME = 'A.Great.Movie' 41 | NZBFILENAME = join(TEMP_DIRECTORY, 'A.Great.Movie.nzb') 42 | CATEGORY = 'movie' 43 | VERSION = '2.1' 44 | SEARCH_DIR = join(TEMP_DIRECTORY, 'file_listing') 45 | 46 | # For validation 47 | STATUS = str(PP_STATUS.SUCCESS) 48 | 49 | 50 | class TestSABPostProcessScript(TestBase): 51 | def setup_method(self): 52 | """This method is run once before _each_ test method is executed""" 53 | super(TestSABPostProcessScript, self).setup_method() 54 | 55 | # Create NZBFILE 56 | _f = open(NZBFILENAME, 'w') 57 | _f.write(""" 58 | 59 | 60 | 61 | 62 | Movies > SD 63 | A.Great.Movie.1983.DVDRip.x264-AWESOME 64 | A Great Movie 65 | 1983 66 | 67 | A 68 | 69 | """) 70 | _f.close() 71 | 72 | # Create some environment variables 73 | os.environ['%sTEMPDIR' % SAB_ENVIRO_ID] = TEMP_DIRECTORY 74 | os.environ['%sVERSION' % SAB_ENVIRO_ID] = str(VERSION) 75 | os.environ['%sDIRECTORY' % SAB_ENVIRO_ID] = DIRECTORY 76 | os.environ['%sCOMPLETE_DIR' % SAB_ENVIRO_ID] = COMPLETE_DIRECTORY 77 | os.environ['%sNZBNAME' % SAB_ENVIRO_ID] = NZBFILENAME 78 | os.environ['%sFILENAME' % SAB_ENVIRO_ID] = NZBNAME 79 | os.environ['%sCAT' % SAB_ENVIRO_ID] = CATEGORY 80 | os.environ['%sPP_STATUS' % SAB_ENVIRO_ID] = STATUS 81 | 82 | # ensure directory doesn't exist 83 | try: 84 | rmtree(SEARCH_DIR) 85 | except: 86 | pass 87 | makedirs(SEARCH_DIR) 88 | 89 | def teardown_method(self): 90 | """This method is run once after _each_ test method is executed""" 91 | # Eliminate any variables defined 92 | del os.environ['%sTEMPDIR' % SAB_ENVIRO_ID] 93 | if '%sVERSION' % SAB_ENVIRO_ID in os.environ: 94 | del os.environ['%sVERSION' % SAB_ENVIRO_ID] 95 | del os.environ['%sDIRECTORY' % SAB_ENVIRO_ID] 96 | del os.environ['%sNZBNAME' % SAB_ENVIRO_ID] 97 | if '%sCOMPLETED_DIR' % SAB_ENVIRO_ID in os.environ: 98 | del os.environ['%sCOMPLETED_DIR' % SAB_ENVIRO_ID] 99 | del os.environ['%sCAT' % SAB_ENVIRO_ID] 100 | del os.environ['%sPP_STATUS' % SAB_ENVIRO_ID] 101 | 102 | try: 103 | rmtree(SEARCH_DIR) 104 | except: 105 | pass 106 | 107 | # common 108 | super(TestSABPostProcessScript, self).teardown_method() 109 | 110 | def test_main_returns(self): 111 | # a NZB Logger set to False uses stderr 112 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 113 | assert script.run() == SHELL_EXIT_CODE.SUCCESS 114 | 115 | class TestSABPostProcessMain(SABPostProcessScript): 116 | def sabnzbd_postprocess_main(self, *args, **kwargs): 117 | return None 118 | 119 | script = TestSABPostProcessMain(logger=False, debug=VERY_VERBOSE_DEBUG) 120 | assert script.run() == SHELL_EXIT_CODE.NONE 121 | 122 | del script 123 | # However we always pass in we validate our environment correctly 124 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 125 | assert script.run() == SHELL_EXIT_CODE.SUCCESS 126 | 127 | # If we fail to validate, then we flat out fail 128 | del os.environ['%sVERSION' % SAB_ENVIRO_ID] 129 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 130 | assert script.run() == SHELL_EXIT_CODE.FAILURE 131 | 132 | def test_environment_varable_init(self): 133 | """ 134 | Testing NZBGet Script initialization using environment variables 135 | """ 136 | # a NZB Logger set to False uses stderr 137 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 138 | assert script.directory == DIRECTORY 139 | assert script.nzbname == NZBNAME 140 | assert script.nzbfilename == NZBFILENAME 141 | assert script.category == CATEGORY 142 | 143 | assert script.system['TEMPDIR'] == TEMP_DIRECTORY 144 | assert script.system['VERSION'] == str(VERSION) 145 | assert script.system['DIRECTORY'] == DIRECTORY 146 | assert script.system['COMPLETE_DIR'] == COMPLETE_DIRECTORY 147 | assert script.system['FILENAME'] == NZBNAME 148 | assert script.system['NZBNAME'] == NZBFILENAME 149 | assert script.system['CAT'] == CATEGORY 150 | assert script.system['PP_STATUS'] == STATUS 151 | 152 | assert script.get('TEMPDIR') == TEMP_DIRECTORY 153 | assert script.get('VERSION') == str(VERSION) 154 | assert script.get('DIRECTORY') == DIRECTORY 155 | assert script.get('COMPLETE_DIR') == COMPLETE_DIRECTORY 156 | assert script.get('FILENAME') == NZBNAME 157 | assert script.get('NZBNAME') == NZBFILENAME 158 | assert script.get('CAT') == CATEGORY 159 | assert script.get('PP_STATUS') == STATUS 160 | 161 | assert len(script.config) == 1 162 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 163 | 164 | assert script.nzbheaders['MOVIEYEAR'] == '1983' 165 | assert script.nzbheaders['NAME'] == \ 166 | 'A.Great.Movie.1983.DVDRip.x264-AWESOME' 167 | assert script.nzbheaders['PROPERNAME'] == 'A Great Movie' 168 | assert script.nzbheaders['CATEGORY'] == 'Movies > SD' 169 | 170 | assert script.nzb_get('movieyear') == '1983' 171 | assert script.nzb_get('name') == \ 172 | 'A.Great.Movie.1983.DVDRip.x264-AWESOME' 173 | assert script.nzb_get('propername') == 'A Great Movie' 174 | assert script.nzb_get('category') == 'Movies > SD' 175 | 176 | assert os.environ['%sTEMPDIR' % SAB_ENVIRO_ID] == TEMP_DIRECTORY 177 | assert os.environ['%sDIRECTORY' % SAB_ENVIRO_ID] == DIRECTORY 178 | assert os.environ['%sCOMPLETE_DIR' % SAB_ENVIRO_ID] == COMPLETE_DIRECTORY 179 | assert os.environ['%sFILENAME' % SAB_ENVIRO_ID] == NZBNAME 180 | assert os.environ['%sNZBNAME' % SAB_ENVIRO_ID] == NZBFILENAME 181 | assert os.environ['%sCAT' % SAB_ENVIRO_ID] == CATEGORY 182 | assert os.environ['%sPP_STATUS' % SAB_ENVIRO_ID] == STATUS 183 | 184 | def test_environment_override(self): 185 | """ 186 | Testing NZBGet Script initialization using forced variables that 187 | should take priority over global ones 188 | """ 189 | directory = join(DIRECTORY, 'a', 'deeper', 'path') 190 | # ensure directory doesn't exist 191 | try: 192 | rmtree(directory) 193 | except: 194 | pass 195 | makedirs(directory) 196 | 197 | nzbname = '%s.with.more.content' % NZBNAME 198 | nzbfilename = join(directory, basename(NZBFILENAME)) 199 | category = '%s2' % CATEGORY 200 | status = str(PP_STATUS.FAILURE) 201 | 202 | script = SABPostProcessScript( 203 | logger=False, 204 | debug=VERY_VERBOSE_DEBUG, 205 | 206 | directory=directory, 207 | nzbname=nzbname, 208 | nzbfilename=nzbfilename, 209 | category=category, 210 | status=status, 211 | ) 212 | 213 | assert script.directory == directory 214 | assert script.nzbname == nzbname 215 | assert script.nzbfilename == nzbfilename 216 | assert script.category == category 217 | assert str(script.status) == status 218 | 219 | assert script.system['TEMPDIR'] == TEMP_DIRECTORY 220 | assert script.system['DIRECTORY'] == directory 221 | assert script.system['FILENAME'] == nzbname 222 | assert script.system['NZBNAME'] == nzbfilename 223 | assert script.system['CAT'] == category 224 | assert script.system['PP_STATUS'] == status 225 | 226 | assert len(script.config) == 1 227 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 228 | assert script.shared == {} 229 | assert script.nzbheaders == {} 230 | 231 | assert os.environ['%sTEMPDIR' % SAB_ENVIRO_ID] == TEMP_DIRECTORY 232 | assert os.environ['%sDIRECTORY' % SAB_ENVIRO_ID] == directory 233 | assert os.environ['%sFILENAME' % SAB_ENVIRO_ID] == nzbname 234 | assert os.environ['%sNZBNAME' % SAB_ENVIRO_ID] == nzbfilename 235 | assert os.environ['%sCAT' % SAB_ENVIRO_ID] == category 236 | assert os.environ['%sPP_STATUS' % SAB_ENVIRO_ID] == status 237 | 238 | # cleanup 239 | try: 240 | rmtree(directory) 241 | except: 242 | pass 243 | 244 | def test_validation(self): 245 | 246 | # This will fail because it's looking for the VERSION 247 | # variable defined with SABnzbd v2.1 248 | # a NZB Logger set to False uses stderr 249 | if '%sVERSION' % SAB_ENVIRO_ID in os.environ: 250 | del os.environ['%sVERSION' % SAB_ENVIRO_ID] 251 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 252 | assert not script.validate() 253 | del script 254 | 255 | # Now let's set it and try again 256 | os.environ['%sVERSION' % SAB_ENVIRO_ID] = VERSION 257 | # a NZB Logger set to False uses stderr 258 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 259 | assert script.validate() 260 | del script 261 | 262 | # Now we'll try arguments 263 | os.environ['%sVALUE_A' % SAB_ENVIRO_ID] = 'A' 264 | os.environ['%sVALUE_B' % SAB_ENVIRO_ID] = 'B' 265 | os.environ['%sVALUE_C' % SAB_ENVIRO_ID] = 'C' 266 | 267 | # a NZB Logger set to False uses stderr 268 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 269 | assert script.validate(keys=( 270 | 'Value_A', 'VALUE_B', 'value_c' 271 | )) 272 | del os.environ['%sVALUE_A' % SAB_ENVIRO_ID] 273 | del os.environ['%sVALUE_B' % SAB_ENVIRO_ID] 274 | del os.environ['%sVALUE_C' % SAB_ENVIRO_ID] 275 | 276 | def test_gzipped_nzbfiles(self): 277 | """ 278 | Test gziped nzbfiles 279 | """ 280 | os.environ['%sORIG_NZB_GZ' % SAB_ENVIRO_ID] = join( 281 | dirname(__file__), 'var', 'plain.nzb.gz') 282 | 283 | # Create our object 284 | script = SABPostProcessScript(logger=False, debug=VERY_VERBOSE_DEBUG) 285 | 286 | # We have a temporary file now 287 | tmp_file = script._sab_temp_nzb 288 | assert(isfile(tmp_file) is True) 289 | 290 | # Running our script will just return zero 291 | assert(script.run() == 0) 292 | 293 | # But not after we exit, our tmp_file would have been cleaned up 294 | assert(isfile(tmp_file) is False) 295 | 296 | # We should be able to uncompress this file 297 | 298 | del os.environ['%sORIG_NZB_GZ' % SAB_ENVIRO_ID] 299 | -------------------------------------------------------------------------------- /nzbget/SchedulerScript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A scripting wrapper for NZBGet's Scheduler Scripting 4 | # 5 | # Copyright (C) 2014 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | """ 18 | This class was intended to make writing NZBGet Scripts easier to manage and 19 | write by handling the common error handling and provide the most reused code 20 | in a re-usable container. It was initially written to work with NZBGet v13 21 | but provides most backwards compatibility. 22 | 23 | It was designed to be inheritied as a base class requiring you to only write 24 | the main() function which should preform the task you are intending. 25 | 26 | It looks after fetching all of the environment variables and will parse 27 | the meta information out of the NZB-File. 28 | 29 | It allows you to set variables that other scripts can access if they need to 30 | using the set() and get() variables. This is done through a simply self 31 | maintained hash table type structure within a sqlite database. All the 32 | wrapper functions are already written. If you call 'set('MYKEY', 1') 33 | you can call get('MYKEY') in another script and continue working 34 | 35 | push() functions written to pass information back to nzbget using it's 36 | processing engine. 37 | 38 | all exceptions are now automatically handled and logging can be easily 39 | changed from stdout, to stderr or to a file. 40 | 41 | Test suite built in (using python-nose) to ensure old global variables 42 | will still work as well as make them easier to access and manipulate. 43 | 44 | Some inline documentation was based on content provided at: 45 | - http://nzbget.net/Extension_scripts 46 | 47 | 48 | ############################################################################ 49 | Schedule Script Usage/Example 50 | ############################################################################ 51 | 52 | ############################################################################ 53 | ### NZBGET SCHEDULER SCRIPT ### 54 | # 55 | # Describe your Schedule Script here 56 | # Author: Chris Caron 57 | # 58 | 59 | ############################################################################ 60 | ### OPTIONS ### 61 | 62 | # 63 | # Enable NZBGet debug logging (yes, no) 64 | # Debug=no 65 | # 66 | 67 | ### NZBGET SCHEDULER SCRIPT ### 68 | ############################################################################ 69 | 70 | from nzbget import SchedulerScript 71 | 72 | # Now define your class while inheriting the rest 73 | class MySchedulerScript(SchedulerScript): 74 | def main(self, *args, **kwargs): 75 | 76 | # Version Checking, Environment Variables Present, etc 77 | if not self.validate(): 78 | # No need to document a failure, validate will do that 79 | # on the reason it failed anyway 80 | return False 81 | 82 | # write all of your code here you would have otherwise put in the 83 | # script 84 | 85 | # All system environment variables (NZBOP_.*) as well as Post 86 | # Process script specific content (NZBSP_.*) 87 | # following dictionary (without the NZBOP_ or NZBSP_ prefix): 88 | print('TEMPDIR (directory is: %s' % self.get('TEMPDIR')) 89 | print('DESTDIR %s' self.get('DESTDIR')) 90 | 91 | # Set any variable you want by any key. Note that if you use 92 | # keys that were defined by the system (such as CATEGORY, DIRECTORY, 93 | # etc, you may have some undesirable results. Try to avoid reusing 94 | # system variables already defined (identified above): 95 | self.set('MY_KEY', 'MY_VALUE') 96 | 97 | # You can fetch it back; this will also set an entry in the 98 | # sqlite database for each hash references that can be pulled from 99 | # another script that simply calls self.get('MY_KEY') 100 | print(self.get('MY_KEY')) # prints MY_VALUE 101 | 102 | # You can also use push() which is similar to set() 103 | # except that it interacts with the NZBGet Server and does not use 104 | # the sqlite database. This can only be reached across other 105 | # scripts if the calling application is NZBGet itself 106 | self.push('ANOTHER_KEY', 'ANOTHER_VALUE') 107 | 108 | # You can still however locally retrieve what you set using push() 109 | # with the get() function 110 | print(self.get('ANOTHER_KEY')) # prints ANOTHER_VALUE 111 | 112 | # Your script configuration files (NZBNP_.*) are here in this 113 | # dictionary (again without the NZBNP_ prefix): 114 | # assume you defined `Debug=no` in the first 10K of your SchedulerScript 115 | # NZBGet translates this to `NZBNP_DEBUG` which can be retrieved 116 | # as follows: 117 | print('DEBUG %s' self.get('DEBUG')) 118 | 119 | # Returns have been made easy. Just return: 120 | # * True if everything was successful 121 | # * False if there was a problem 122 | # * None if you want to report that you've just gracefully 123 | skipped processing (this is better then False) 124 | in some circumstances. This is neither a failure or a 125 | success status. 126 | 127 | # Feel free to use the actual exit codes as well defined by 128 | # NZBGet on their website. They have also been defined here 129 | # from nzbget.ScriptBase import EXIT_CODE 130 | 131 | return True 132 | 133 | # Call your script as follows: 134 | if __name__ == "__main__": 135 | from sys import exit 136 | 137 | # Create an instance of your Script 138 | myscript = MySchedulerScript() 139 | 140 | # call run() and exit() using it's returned value 141 | exit(myscript.run()) 142 | 143 | """ 144 | import re 145 | from os import environ 146 | 147 | # Relative Includes 148 | from .ScriptBase import ScriptBase 149 | from .ScriptBase import NZBGET_BOOL_FALSE 150 | from .ScriptBase import SCRIPT_MODE 151 | from .PostProcessScript import POSTPROC_ENVIRO_ID 152 | 153 | # Environment variable that prefixes all NZBGET options being passed into 154 | # scripts with respect to the NZB-File (used in Scan Scripts) 155 | SCHEDULER_ENVIRO_ID = 'NZBSP_' 156 | TASK_ENVIRO_ID = 'TASK' 157 | 158 | # Precompile Regulare Expression for Speed 159 | SCHEDULER_OPTS_RE = re.compile('^%s([A-Z0-9_]+)$' % SCHEDULER_ENVIRO_ID) 160 | 161 | class SchedulerScript(ScriptBase): 162 | def __init__(self, *args, **kwargs): 163 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 164 | # Multi-Script Support 165 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 166 | if not hasattr(self, 'script_dict'): 167 | # Only define once 168 | self.script_dict = {} 169 | self.script_dict[SCRIPT_MODE.SCHEDULER] = self 170 | 171 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 172 | # Initialize Parent 173 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 174 | super(SchedulerScript, self).__init__(*args, **kwargs) 175 | 176 | def scheduler_init(self, *args, **kwargs): 177 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 178 | # Fetch Script Specific Arguments 179 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 180 | taskid = kwargs.get('taskid') 181 | 182 | # Fetch/Load Scan Script Configuration 183 | script_config = dict([(SCHEDULER_OPTS_RE.match(k).group(1), v.strip()) \ 184 | for (k, v) in environ.items() if SCHEDULER_OPTS_RE.match(k)]) 185 | 186 | if self.vvdebug: 187 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 188 | # Print Global Script Varables to help debugging process 189 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 190 | for k, v in script_config.items(): 191 | self.logger.vvdebug('%s%s=%s' % (SCHEDULER_ENVIRO_ID, k, v)) 192 | 193 | # Merge Script Configuration With System Config 194 | script_config.update(self.system) 195 | self.system = script_config 196 | 197 | # self.taskid 198 | # This is the Task Identifier passed in from NZBGet 199 | if taskid is None: 200 | self.taskid = environ.get( 201 | '%sTASKID' % SCHEDULER_ENVIRO_ID, 202 | ) 203 | else: 204 | self.taskid = taskid 205 | 206 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 207 | # Error Handling 208 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 209 | try: 210 | self.taskid = int(self.taskid) 211 | self.logger.info('Task ID assigned: %d' % self.taskid) 212 | except (ValueError, TypeError): 213 | # Default is 0 214 | self.taskid = 0 215 | self.logger.warning('No Task ID was assigned') 216 | 217 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 218 | # Enforce system/global variables for script processing 219 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 220 | self.system['TASKID'] = self.taskid 221 | if isinstance(self.taskid, int) and self.taskid > 0: 222 | environ['%sTASKID' % SCHEDULER_ENVIRO_ID] = str(self.taskid) 223 | 224 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 225 | # Debug Flag Check 226 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 227 | def scheduler_debug(self, *args, **kwargs): 228 | """Uses the environment variables to detect if debug mode is set 229 | """ 230 | return self.parse_bool( 231 | environ.get('%sDEBUG' % SCHEDULER_ENVIRO_ID, NZBGET_BOOL_FALSE), 232 | ) 233 | 234 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 235 | # Validatation 236 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 237 | def scheduler_validate(self, keys=None, min_version=11, *args, **kargs): 238 | """validate against environment variables 239 | """ 240 | is_okay = super(SchedulerScript, self)._validate( 241 | keys=keys, 242 | min_version=min_version, 243 | ) 244 | 245 | required_opts = set(( 246 | 'TASKID', 247 | )) 248 | 249 | found_opts = set(self.system) & required_opts 250 | if found_opts != required_opts: 251 | missing_opts = list(required_opts ^ found_opts) 252 | self.logger.error( 253 | 'Validation - (v11) Directives not set: %s' % \ 254 | missing_opts.join(', ') 255 | ) 256 | is_okay = False 257 | 258 | return is_okay 259 | 260 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 261 | # Sanity 262 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 263 | def scheduler_sanity_check(self, *args, **kargs): 264 | """Sanity checking to ensure this really is a post_process script 265 | """ 266 | return ('%sDIRECTORY' % POSTPROC_ENVIRO_ID not in environ) and \ 267 | ('%sTASKID' % SCHEDULER_ENVIRO_ID in environ) 268 | 269 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 270 | # Tasks 271 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 272 | def get_task(self, taskid=None): 273 | """Returns a dictionary of task details identified by the id 274 | specified. If no id is specified, then the current task is 275 | detected and returned. 276 | """ 277 | if taskid is None: 278 | # assume default 279 | taskid = self.taskid 280 | 281 | if not isinstance(taskid, int): 282 | try: 283 | taskid = int(taskid) 284 | except (ValueError, TypeError): 285 | # can't be typecasted to an integer 286 | return {} 287 | 288 | if taskid <= 0: 289 | # No task defined 290 | return {} 291 | 292 | # Precompile Regulare Expression for Speed 293 | task_re = re.compile('^%s%s%d_([A-Z0-9_]+)$' % ( 294 | SCHEDULER_ENVIRO_ID, 295 | TASK_ENVIRO_ID, 296 | taskid, 297 | )) 298 | 299 | self.logger.debug('Looking for %s%s%d_([A-Z0-9_]+)$' % ( 300 | SCHEDULER_ENVIRO_ID, 301 | TASK_ENVIRO_ID, 302 | taskid, 303 | )) 304 | 305 | # Fetch Task related content 306 | return dict([(task_re.match(k).group(1), v.strip()) \ 307 | for (k, v) in environ.items() if task_re.match(k)]) 308 | -------------------------------------------------------------------------------- /test/test_scanscript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for the ScanScript Class 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import os 18 | import sys 19 | from os.path import join 20 | 21 | from TestBase import TestBase 22 | from TestBase import TEMP_DIRECTORY 23 | 24 | from nzbget.ScriptBase import CFG_ENVIRO_ID 25 | from nzbget.ScriptBase import SYS_ENVIRO_ID 26 | from nzbget.ScriptBase import NZBGET_MSG_PREFIX 27 | 28 | from nzbget.ScanScript import ScanScript 29 | from nzbget.ScanScript import SCAN_ENVIRO_ID 30 | from nzbget.ScanScript import PRIORITY 31 | 32 | 33 | from nzbget.Logger import VERY_VERBOSE_DEBUG 34 | 35 | try: 36 | # Python v2.7 37 | from StringIO import StringIO 38 | except ImportError: 39 | # Python v3.x 40 | from io import StringIO 41 | 42 | # Some constants to work with 43 | DIRECTORY = TEMP_DIRECTORY 44 | NZBNAME = 'The.Perfect.Name' 45 | FILENAME = join(TEMP_DIRECTORY, 'nzbget/the/The.Perfect.Name.nzb') 46 | CATEGORY = 'movie' 47 | _PRIORITY = PRIORITY.NORMAL 48 | TOP = False 49 | PAUSED = False 50 | 51 | 52 | class TestScanScript(TestBase): 53 | def setup_method(self): 54 | """This method is run once before _each_ test method is executed""" 55 | super(TestScanScript, self).setup_method() 56 | 57 | # Create some environment variables 58 | os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 59 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = DIRECTORY 60 | os.environ['%sNZBNAME' % SCAN_ENVIRO_ID] = NZBNAME 61 | os.environ['%sFILENAME' % SCAN_ENVIRO_ID] = FILENAME 62 | os.environ['%sCATEGORY' % SCAN_ENVIRO_ID] = CATEGORY 63 | os.environ['%sPRIORITY' % SCAN_ENVIRO_ID] = str(_PRIORITY) 64 | os.environ['%sTOP' % SCAN_ENVIRO_ID] = str(int(TOP)) 65 | os.environ['%sPAUSED' % SCAN_ENVIRO_ID] = str(int(PAUSED)) 66 | 67 | def teardown_method(self): 68 | """This method is run once after _each_ test method is executed""" 69 | # Eliminate any variables defined 70 | del os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] 71 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 72 | del os.environ['%sNZBNAME' % SCAN_ENVIRO_ID] 73 | del os.environ['%sFILENAME' % SCAN_ENVIRO_ID] 74 | del os.environ['%sCATEGORY' % SCAN_ENVIRO_ID] 75 | del os.environ['%sPRIORITY' % SCAN_ENVIRO_ID] 76 | del os.environ['%sTOP' % SCAN_ENVIRO_ID] 77 | del os.environ['%sPAUSED' % SCAN_ENVIRO_ID] 78 | 79 | # common 80 | super(TestScanScript, self).teardown_method() 81 | 82 | def test_environment_varable_init(self): 83 | """ 84 | Testing NZBGet Script initialization using environment variables 85 | """ 86 | # a NZB Logger set to False uses stderr 87 | script = ScanScript(logger=False, debug=VERY_VERBOSE_DEBUG) 88 | assert script.directory == DIRECTORY 89 | assert script.nzbname == NZBNAME 90 | assert script.filename == FILENAME 91 | assert script.category == CATEGORY 92 | assert script.priority == _PRIORITY 93 | assert script.top == TOP 94 | assert script.paused == PAUSED 95 | 96 | assert script.system['TEMPDIR'] == TEMP_DIRECTORY 97 | assert script.system['DIRECTORY'] == DIRECTORY 98 | assert script.system['NZBNAME'] == NZBNAME 99 | assert script.system['FILENAME'] == FILENAME 100 | assert script.system['CATEGORY'] == CATEGORY 101 | assert script.system['PRIORITY'] == _PRIORITY 102 | assert script.system['TOP'] == TOP 103 | assert script.system['PAUSED'] == PAUSED 104 | 105 | assert script.get('TEMPDIR') == TEMP_DIRECTORY 106 | assert script.get('DIRECTORY') == DIRECTORY 107 | assert script.get('NZBNAME') == NZBNAME 108 | assert script.get('FILENAME') == FILENAME 109 | assert script.get('CATEGORY') == CATEGORY 110 | assert script.get('PRIORITY') == _PRIORITY 111 | assert script.get('TOP') == TOP 112 | assert script.get('PAUSED') == PAUSED 113 | 114 | assert len(script.config) == 1 115 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 116 | 117 | assert os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] == TEMP_DIRECTORY 118 | assert os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] == DIRECTORY 119 | assert os.environ['%sNZBNAME' % SCAN_ENVIRO_ID] == NZBNAME 120 | assert os.environ['%sFILENAME' % SCAN_ENVIRO_ID] == FILENAME 121 | assert os.environ['%sCATEGORY' % SCAN_ENVIRO_ID] == CATEGORY 122 | assert os.environ['%sPRIORITY' % SCAN_ENVIRO_ID] == str(_PRIORITY) 123 | assert os.environ['%sTOP' % SCAN_ENVIRO_ID] == str(int(TOP)) 124 | assert os.environ['%sPAUSED' % SCAN_ENVIRO_ID] == str(int(PAUSED)) 125 | 126 | def test_environment_override(self): 127 | """ 128 | Testing NZBGet Script initialization using forced variables that 129 | should take priority over global ones 130 | """ 131 | directory = join(DIRECTORY, 'a', 'deeper', 'path') 132 | nzbname = '%s.with.more.detail' % NZBNAME 133 | filename = '%s.with.more.detail' % FILENAME 134 | category = '%s.with.more.detail' % CATEGORY 135 | priority = PRIORITY.FORCE 136 | top = not TOP 137 | paused = not PAUSED 138 | 139 | script = ScanScript( 140 | logger=False, 141 | debug=VERY_VERBOSE_DEBUG, 142 | 143 | directory=directory, 144 | nzbname=nzbname, 145 | filename=filename, 146 | category=category, 147 | priority=priority, 148 | top=top, 149 | paused=paused, 150 | ) 151 | 152 | assert script.directory == directory 153 | assert script.nzbname == nzbname 154 | assert script.filename == filename 155 | assert script.category == category 156 | assert script.priority == priority 157 | assert script.top == top 158 | assert script.paused == paused 159 | 160 | assert script.system['TEMPDIR'] == TEMP_DIRECTORY 161 | assert script.system['DIRECTORY'] == directory 162 | assert script.system['NZBNAME'] == nzbname 163 | assert script.system['FILENAME'] == filename 164 | assert script.system['CATEGORY'] == category 165 | assert script.system['PRIORITY'] == priority 166 | assert script.system['TOP'] == top 167 | assert script.system['PAUSED'] == paused 168 | 169 | assert len(script.config) == 1 170 | assert script.config.get('DEBUG') == VERY_VERBOSE_DEBUG 171 | 172 | assert os.environ['%sTEMPDIR' % SYS_ENVIRO_ID] == TEMP_DIRECTORY 173 | assert os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] == directory 174 | assert os.environ['%sNZBNAME' % SCAN_ENVIRO_ID] == nzbname 175 | assert os.environ['%sFILENAME' % SCAN_ENVIRO_ID] == filename 176 | assert os.environ['%sCATEGORY' % SCAN_ENVIRO_ID] == category 177 | assert os.environ['%sPRIORITY' % SCAN_ENVIRO_ID] == str(priority) 178 | assert os.environ['%sTOP' % SCAN_ENVIRO_ID] == str(int(top)) 179 | assert os.environ['%sPAUSED' % SCAN_ENVIRO_ID] == str(int(paused)) 180 | 181 | def test_set_and_get(self): 182 | # a NZB Logger set to False uses stderr 183 | script = ScanScript(logger=False, debug=VERY_VERBOSE_DEBUG) 184 | 185 | KEY = 'MY_VAR' 186 | VALUE = 'MY_VALUE' 187 | 188 | # Value doe snot exist yet 189 | assert script.get(KEY) is None 190 | assert script.get(KEY, 'Default') == 'Default' 191 | assert script.set(KEY, VALUE) is True 192 | assert script.get(KEY, 'Default') == VALUE 193 | 194 | def test_config_varable_init(self): 195 | valid_entries = { 196 | 'MY_CONFIG_ENTRY': 'Option A', 197 | 'ENTRY_WITH_234_NUMBERS': 'Option B', 198 | '123443': 'Option C', 199 | } 200 | invalid_entries = { 201 | 'CONFIG_ENtry_skipped': 'Option', 202 | 'CONFIG_ENtry_$#': 'Option', 203 | # Empty 204 | '': 'Option', 205 | } 206 | for k, v in valid_entries.items(): 207 | os.environ['%s%s' % (CFG_ENVIRO_ID, k)] = v 208 | for k, v in invalid_entries.items(): 209 | os.environ['%s%s' % (CFG_ENVIRO_ID, k)] = v 210 | 211 | script = ScanScript() 212 | for k, v in valid_entries.items(): 213 | assert k in script.config 214 | assert script.config[k] == v 215 | 216 | for k in invalid_entries.keys(): 217 | assert k not in script.config 218 | 219 | # Cleanup 220 | for k, v in valid_entries.items(): 221 | del os.environ['%s%s' % (CFG_ENVIRO_ID, k)] 222 | for k, v in invalid_entries.items(): 223 | del os.environ['%s%s' % (CFG_ENVIRO_ID, k)] 224 | 225 | def test_nzbget_push_nzbname(self): 226 | 227 | # a NZB Logger set to False uses stderr 228 | script = ScanScript(logger=False, debug=VERY_VERBOSE_DEBUG) 229 | 230 | nzbname = '%s.but.with.more.content' % NZBNAME 231 | 232 | # Keep a handle on the real standard output 233 | stdout = sys.stdout 234 | sys.stdout = StringIO() 235 | script.push_nzbname(nzbname) 236 | 237 | # extract data 238 | sys.stdout.seek(0) 239 | output = sys.stdout.read().strip() 240 | 241 | # return stdout back to how it was 242 | sys.stdout = stdout 243 | assert output == '%s%s=%s' % ( 244 | NZBGET_MSG_PREFIX, 245 | 'NZBNAME', 246 | nzbname, 247 | ) 248 | assert script.nzbname == nzbname 249 | assert script.system['NZBNAME'] == nzbname 250 | 251 | def test_nzbget_push_category(self): 252 | 253 | # a NZB Logger set to False uses stderr 254 | script = ScanScript(logger=False, debug=VERY_VERBOSE_DEBUG) 255 | 256 | category = '%s100' % CATEGORY 257 | 258 | # Keep a handle on the real standard output 259 | stdout = sys.stdout 260 | sys.stdout = StringIO() 261 | script.push_category(category) 262 | 263 | # extract data 264 | sys.stdout.seek(0) 265 | output = sys.stdout.read().strip() 266 | 267 | # return stdout back to how it was 268 | sys.stdout = stdout 269 | assert output == '%s%s=%s' % ( 270 | NZBGET_MSG_PREFIX, 271 | 'CATEGORY', 272 | category, 273 | ) 274 | assert script.category == category 275 | assert script.system['CATEGORY'] == category 276 | 277 | def test_nzbget_push_priority(self): 278 | 279 | # a NZB Logger set to False uses stderr 280 | script = ScanScript(logger=False, debug=VERY_VERBOSE_DEBUG) 281 | 282 | priority = PRIORITY.FORCE 283 | 284 | # Keep a handle on the real standard output 285 | stdout = sys.stdout 286 | sys.stdout = StringIO() 287 | script.push_priority(priority) 288 | 289 | # extract data 290 | sys.stdout.seek(0) 291 | output = sys.stdout.read().strip() 292 | 293 | # return stdout back to how it was 294 | sys.stdout = stdout 295 | assert output == '%s%s=%d' % ( 296 | NZBGET_MSG_PREFIX, 297 | 'PRIORITY', 298 | priority, 299 | ) 300 | assert script.priority == priority 301 | assert script.system['PRIORITY'] == priority 302 | 303 | def test_nzbget_push_top(self): 304 | 305 | # a NZB Logger set to False uses stderr 306 | script = ScanScript(logger=False, debug=VERY_VERBOSE_DEBUG) 307 | 308 | top = not TOP 309 | 310 | # Keep a handle on the real standard output 311 | stdout = sys.stdout 312 | sys.stdout = StringIO() 313 | script.push_top(top) 314 | 315 | # extract data 316 | sys.stdout.seek(0) 317 | output = sys.stdout.read().strip() 318 | 319 | # return stdout back to how it was 320 | sys.stdout = stdout 321 | assert output == '%s%s=%d' % ( 322 | NZBGET_MSG_PREFIX, 323 | 'TOP', 324 | int(top), 325 | ) 326 | assert script.top == top 327 | assert script.system['TOP'] == top 328 | 329 | def test_nzbget_push_paused(self): 330 | 331 | # a NZB Logger set to False uses stderr 332 | script = ScanScript(logger=False, debug=VERY_VERBOSE_DEBUG) 333 | 334 | paused = not PAUSED 335 | 336 | # Keep a handle on the real standard output 337 | stdout = sys.stdout 338 | sys.stdout = StringIO() 339 | script.push_paused(paused) 340 | 341 | # extract data 342 | sys.stdout.seek(0) 343 | output = sys.stdout.read().strip() 344 | 345 | # return stdout back to how it was 346 | sys.stdout = stdout 347 | assert output == '%s%s=%d' % ( 348 | NZBGET_MSG_PREFIX, 349 | 'PAUSED', 350 | int(paused), 351 | ) 352 | assert script.paused == paused 353 | assert script.system['PAUSED'] == paused 354 | -------------------------------------------------------------------------------- /nzbget/FeedScript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A scripting wrapper for NZBGet's Feed Scripting 4 | # 5 | # Copyright (C) 2014 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | """ 18 | This class was intended to make writing NZBGet Scripts easier to manage and 19 | write by handling the common error handling and provide the most reused code 20 | in a re-usable container. It was initially written to work with NZBGet v13 21 | but provides most backwards compatibility. 22 | 23 | It was designed to be inheritied as a base class requiring you to only write 24 | the main() function which should preform the feed you are intending. 25 | 26 | It looks after fetching all of the environment variables and will parse 27 | the meta information out of the NZB-File. 28 | 29 | It allows you to set variables that other scripts can access if they need to 30 | using the set() and get() variables. This is done through a simply self 31 | maintained hash table type structure within a sqlite database. All the 32 | wrapper functions are already written. If you call 'set('MYKEY', 1') 33 | you can call get('MYKEY') in another script and continue working 34 | 35 | push() functions written to pass information back to nzbget using it's 36 | processing engine. 37 | 38 | all exceptions are now automatically handled and logging can be easily 39 | changed from stdout, to stderr or to a file. 40 | 41 | Test suite built in (using python-nose) to ensure old global variables 42 | will still work as well as make them easier to access and manipulate. 43 | 44 | Some inline documentation was based on content provided at: 45 | - http://nzbget.net/Extension_scripts 46 | 47 | 48 | ############################################################################ 49 | Feed Script Usage/Example 50 | ############################################################################ 51 | 52 | ############################################################################ 53 | ### NZBGET FEED SCRIPT ### 54 | # 55 | # Describe your Schedule Script here 56 | # Author: Chris Caron 57 | # 58 | 59 | ############################################################################ 60 | ### OPTIONS ### 61 | 62 | # 63 | # Enable NZBGet debug logging (yes, no) 64 | # Debug=no 65 | # 66 | 67 | ### NZBGET FEED SCRIPT ### 68 | ############################################################################ 69 | 70 | from nzbget import FeedScript 71 | 72 | # Now define your class while inheriting the rest 73 | class MyFeedScript(FeedScript): 74 | def main(self, *args, **kwargs): 75 | 76 | # Version Checking, Environment Variables Present, etc 77 | if not self.validate(): 78 | # No need to document a failure, validate will do that 79 | # on the reason it failed anyway 80 | return False 81 | 82 | # write all of your code here you would have otherwise put in the 83 | # script 84 | 85 | # All system environment variables (NZBOP_.*) as well as Post 86 | # Process script specific content (NZBFP_.*) 87 | # following dictionary (without the NZBOP_ or NZBFP_ prefix): 88 | print('TEMPDIR (directory is: %s' % self.get('TEMPDIR')) 89 | print('DESTDIR %s' self.get('DESTDIR')) 90 | 91 | # Set any variable you want by any key. Note that if you use 92 | # keys that were defined by the system (such as CATEGORY, DIRECTORY, 93 | # etc, you may have some undesirable results. Try to avoid reusing 94 | # system variables already defined (identified above): 95 | self.set('MY_KEY', 'MY_VALUE') 96 | 97 | # You can fetch it back; this will also set an entry in the 98 | # sqlite database for each hash references that can be pulled from 99 | # another script that simply calls self.get('MY_KEY') 100 | print(self.get('MY_KEY')) # prints MY_VALUE 101 | 102 | # You can also use push() which is similar to set() 103 | # except that it interacts with the NZBGet Server and does not use 104 | # the sqlite database. This can only be reached across other 105 | # scripts if the calling application is NZBGet itself 106 | self.push('ANOTHER_KEY', 'ANOTHER_VALUE') 107 | 108 | # You can still however locally retrieve what you set using push() 109 | # with the get() function 110 | print(self.get('ANOTHER_KEY')) # prints ANOTHER_VALUE 111 | 112 | # Your script configuration files (NZBNP_.*) are here in this 113 | # dictionary (again without the NZBNP_ prefix): 114 | # assume you defined `Debug=no` in the first 10K of your FeedScript 115 | # NZBGet translates this to `NZBNP_DEBUG` which can be retrieved 116 | # as follows: 117 | print('DEBUG %s' self.get('DEBUG')) 118 | 119 | # Returns have been made easy. Just return: 120 | # * True if everything was successful 121 | # * False if there was a problem 122 | # * None if you want to report that you've just gracefully 123 | skipped processing (this is better then False) 124 | in some circumstances. This is neither a failure or a 125 | success status. 126 | 127 | # Feel free to use the actual exit codes as well defined by 128 | # NZBGet on their website. They have also been defined here 129 | # from nzbget.ScriptBase import EXIT_CODE 130 | 131 | return True 132 | 133 | # Call your script as follows: 134 | if __name__ == "__main__": 135 | from sys import exit 136 | 137 | # Create an instance of your Script 138 | myscript = MyFeedScript() 139 | 140 | # call run() and exit() using it's returned value 141 | exit(myscript.run()) 142 | 143 | """ 144 | import re 145 | from os import environ 146 | 147 | # Relative Includes 148 | from .ScriptBase import ScriptBase 149 | from .ScriptBase import NZBGET_BOOL_FALSE 150 | from .ScriptBase import SCRIPT_MODE 151 | from .PostProcessScript import POSTPROC_ENVIRO_ID 152 | 153 | # Environment variable that prefixes all NZBGET options being passed into 154 | # scripts with respect to the NZB-File (used in Feed Scripts) 155 | FEED_ENVIRO_ID = 'NZBFP_' 156 | FEEDID_ENVIRO_ID = 'FEEDID' 157 | 158 | # Precompile Regulare Expression for Speed 159 | FEED_OPTS_RE = re.compile('^%s([A-Z0-9_]+)$' % FEED_ENVIRO_ID) 160 | 161 | 162 | class FeedScript(ScriptBase): 163 | 164 | # Default FEED ID initialization 165 | feedid = None 166 | 167 | def __init__(self, *args, **kwargs): 168 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 169 | # Multi-Script Support 170 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 171 | if not hasattr(self, 'script_dict'): 172 | # Only define once 173 | self.script_dict = {} 174 | self.script_dict[SCRIPT_MODE.FEED] = self 175 | 176 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 177 | # Initialize Parent 178 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 179 | super(FeedScript, self).__init__(*args, **kwargs) 180 | 181 | def feed_init(self, *args, **kwargs): 182 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 183 | # Fetch Script Specific Arguments 184 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 185 | feedid = kwargs.get('feedid') 186 | filename = kwargs.get('filename') 187 | 188 | # Fetch/Load Feed Script Configuration 189 | script_config = \ 190 | dict([(FEED_OPTS_RE.match(k).group(1), v.strip()) 191 | for (k, v) in environ.items() if FEED_OPTS_RE.match(k)]) 192 | 193 | if self.vvdebug: 194 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 195 | # Print Global Script Varables to help debugging process 196 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 197 | for k, v in script_config.items(): 198 | self.logger.vvdebug('%s%s=%s' % (FEED_ENVIRO_ID, k, v)) 199 | 200 | # Merge Script Configuration With System Config 201 | script_config.update(self.system) 202 | self.system = script_config 203 | 204 | # self.feedid 205 | # This is the Feed Identifier passed in from NZBGet 206 | if feedid is None: 207 | self.feedid = environ.get( 208 | '%sFEEDID' % FEED_ENVIRO_ID, 209 | ) 210 | else: 211 | self.feedid = feedid 212 | 213 | # self.filename 214 | # This is the Feed Filename passed in from NZBGet 215 | if filename is None: 216 | self.filename = environ.get( 217 | '%sFILENAME' % FEED_ENVIRO_ID, 218 | ) 219 | else: 220 | self.filename = filename 221 | 222 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 223 | # Error Handling 224 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 225 | try: 226 | self.feedid = int(self.feedid) 227 | self.logger.info('Feed ID assigned: %d' % self.feedid) 228 | except (ValueError, TypeError): 229 | # Default is 0 230 | self.feedid = 0 231 | self.logger.warning('No Feed ID was assigned') 232 | 233 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 234 | # Enforce system/global variables for script processing 235 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 236 | self.system['FEEDID'] = self.feedid 237 | if isinstance(self.feedid, int) and self.feedid > 0: 238 | environ['%sFEEDID' % FEED_ENVIRO_ID] = str(self.feedid) 239 | 240 | self.system['FILENAME'] = self.filename 241 | if self.filename: 242 | environ['%sFILENAME' % FEED_ENVIRO_ID] = self.filename 243 | 244 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 245 | # Debug Flag Check 246 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 247 | def feed_debug(self, *args, **kwargs): 248 | """Uses the environment variables to detect if debug mode is set 249 | """ 250 | return self.parse_bool( 251 | environ.get('%sDEBUG' % FEED_ENVIRO_ID, NZBGET_BOOL_FALSE), 252 | ) 253 | 254 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 255 | # Validatation 256 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 257 | def feed_validate(self, keys=None, min_version=11, *args, **kargs): 258 | """validate against environment variables 259 | """ 260 | is_okay = super(FeedScript, self)._validate( 261 | keys=keys, 262 | min_version=min_version, 263 | ) 264 | 265 | required_opts = set(( 266 | 'FEEDID', 267 | 'FILENAME', 268 | )) 269 | 270 | found_opts = set(self.system) & required_opts 271 | if found_opts != required_opts: 272 | missing_opts = list(required_opts ^ found_opts) 273 | self.logger.error( 274 | 'Validation - (v11) Directives not set: %s' % 275 | missing_opts.join(', ') 276 | ) 277 | is_okay = False 278 | 279 | return is_okay 280 | 281 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 282 | # Sanity 283 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 284 | def feed_sanity_check(self, *args, **kargs): 285 | """Sanity checking to ensure this really is a post_process script 286 | """ 287 | return ('%sDIRECTORY' % POSTPROC_ENVIRO_ID not in environ) and \ 288 | ('%sFEEDID' % FEED_ENVIRO_ID in environ) 289 | 290 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 291 | # Feeds 292 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 293 | def get_feed(self, feedid=None): 294 | """Returns a dictionary of feed details identified by the id 295 | specified. If no id is specified, then the current feed is 296 | detected and returned. 297 | """ 298 | if feedid is None: 299 | # assume default 300 | feedid = self.feedid 301 | 302 | if not isinstance(feedid, int): 303 | try: 304 | feedid = int(feedid) 305 | except (ValueError, TypeError): 306 | # can't be typecasted to an integer 307 | return {} 308 | 309 | if feedid <= 0: 310 | # No feed id defined 311 | return {} 312 | 313 | # Precompile Regulare Expression for Speed 314 | feed_re = re.compile('^%s%s%d_([A-Z0-9_]+)$' % ( 315 | FEED_ENVIRO_ID, 316 | FEEDID_ENVIRO_ID, 317 | feedid, 318 | )) 319 | 320 | self.logger.debug('Looking for %s%s%d_([A-Z0-9_]+)$' % ( 321 | FEED_ENVIRO_ID, 322 | FEEDID_ENVIRO_ID, 323 | feedid, 324 | )) 325 | 326 | # Fetch Feed related content 327 | return dict([(feed_re.match(k).group(1), v.strip()) 328 | for (k, v) in environ.items() if feed_re.match(k)]) 329 | -------------------------------------------------------------------------------- /test/test_multiscript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A Test Suite (for nose) for the MultiScripts 4 | # 5 | # Copyright (C) 2014-2019 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | import os 18 | 19 | from TestBase import TestBase 20 | from TestBase import TEMP_DIRECTORY 21 | 22 | from nzbget.PostProcessScript import PostProcessScript 23 | from nzbget.PostProcessScript import POSTPROC_ENVIRO_ID 24 | from nzbget.ScanScript import ScanScript 25 | from nzbget.ScanScript import SCAN_ENVIRO_ID 26 | from nzbget.SchedulerScript import SchedulerScript 27 | from nzbget.SchedulerScript import SCHEDULER_ENVIRO_ID 28 | from nzbget.ScriptBase import SCRIPT_MODE 29 | from nzbget.ScriptBase import EXIT_CODE 30 | from nzbget.ScriptBase import SYS_ENVIRO_ID 31 | from nzbget.Logger import VERY_VERBOSE_DEBUG 32 | 33 | 34 | class TestPostProcessScript(TestBase): 35 | def setup_method(self): 36 | """This method is run once before _each_ test method is executed""" 37 | super(TestPostProcessScript, self).setup_method() 38 | 39 | if '%sDESTDIR' % SYS_ENVIRO_ID in os.environ: 40 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 41 | if '%sDIRECTORY' % SCAN_ENVIRO_ID in os.environ: 42 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 43 | if '%sDIRECTORY' % POSTPROC_ENVIRO_ID in os.environ: 44 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 45 | 46 | def teardown_method(self): 47 | """This method is run once after _each_ test method is executed""" 48 | # Eliminate any variables defined 49 | if '%sDESTDIR' % SYS_ENVIRO_ID in os.environ: 50 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 51 | if '%sDIRECTORY' % SCAN_ENVIRO_ID in os.environ: 52 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 53 | if '%sDIRECTORY' % POSTPROC_ENVIRO_ID in os.environ: 54 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 55 | 56 | # common 57 | super(TestPostProcessScript, self).teardown_method() 58 | 59 | def test_dual_script01(self): 60 | """ 61 | Testing NZBGet Script initialization using environment variables 62 | """ 63 | class TestDualScript(PostProcessScript, SchedulerScript): 64 | pass 65 | 66 | # No environment variables make us unsure what we're testing 67 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 68 | assert script.detect_mode() == SCRIPT_MODE.NONE 69 | 70 | # Scheduler Sanity 71 | os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] = '1' 72 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 73 | assert script.detect_mode() == SCRIPT_MODE.SCHEDULER 74 | del os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] 75 | 76 | # Scan Sanity 77 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 78 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 79 | assert script.detect_mode() == SCRIPT_MODE.NONE 80 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 81 | 82 | # PostProcess Sanity 83 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 84 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 85 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 86 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 87 | 88 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 89 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 90 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 91 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 92 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 93 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 94 | 95 | # Post Process still trumps all (if all are set) 96 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 97 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 98 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 99 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 100 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 101 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 102 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 103 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 104 | 105 | def test_dual_script02(self): 106 | """ 107 | Testing NZBGet Script initialization using environment variables 108 | Reversing them from test01 alters the initialization, but 109 | should not affect the end result 110 | """ 111 | class TestDualScript(SchedulerScript, PostProcessScript): 112 | pass 113 | 114 | # No environment variables make us unsure what we're testing 115 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 116 | assert script.detect_mode() == SCRIPT_MODE.NONE 117 | 118 | # Scheduler Sanity 119 | os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] = '1' 120 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 121 | assert script.detect_mode() == SCRIPT_MODE.SCHEDULER 122 | del os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] 123 | 124 | # Scan Sanity 125 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 126 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 127 | assert script.detect_mode() == SCRIPT_MODE.NONE 128 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 129 | 130 | # PostProcess Sanity 131 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 132 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 133 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 134 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 135 | 136 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 137 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 138 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 139 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 140 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 141 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 142 | 143 | # Post Process still trumps all (if all are set) 144 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 145 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 146 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 147 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 148 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 149 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 150 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 151 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 152 | 153 | def test_dual_script03(self): 154 | """ 155 | Testing NZBGet Script initialization using environment variables 156 | Reversing them from test01 alters the initialization, but 157 | should not affect the end result 158 | """ 159 | class TestDualScript(ScanScript, PostProcessScript): 160 | pass 161 | 162 | # No environment variables make us unsure what we're testing 163 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 164 | assert script.detect_mode() == SCRIPT_MODE.NONE 165 | 166 | # Scheduler Sanity 167 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 168 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 169 | assert script.detect_mode() == SCRIPT_MODE.NONE 170 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 171 | 172 | # Scan Sanity 173 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 174 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 175 | assert script.detect_mode() == SCRIPT_MODE.SCAN 176 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 177 | 178 | # PostProcess Sanity 179 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 180 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 181 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 182 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 183 | 184 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 185 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 186 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 187 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 188 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 189 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 190 | 191 | # Post Process still trumps all (if all are set) 192 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 193 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 194 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 195 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG) 196 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 197 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 198 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 199 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 200 | 201 | def test_dual_force_script01(self): 202 | 203 | class TestDualScript(ScanScript, PostProcessScript): 204 | pass 205 | 206 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 207 | script_mode=SCRIPT_MODE.POSTPROCESSING) 208 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 209 | 210 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 211 | script_mode=SCRIPT_MODE.SCAN) 212 | assert script.detect_mode() == SCRIPT_MODE.SCAN 213 | 214 | # SchedulerScript is not part of DualScript, so therefore 215 | # it can't be foreced 216 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 217 | script_mode=SCRIPT_MODE.SCHEDULER) 218 | assert script.detect_mode() == SCRIPT_MODE.NONE 219 | 220 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 221 | script_mode=SCRIPT_MODE.NONE) 222 | assert script.detect_mode() == SCRIPT_MODE.NONE 223 | 224 | def test_dual_force_script02(self): 225 | 226 | class TestDualScript(SchedulerScript, PostProcessScript): 227 | pass 228 | 229 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 230 | script_mode=SCRIPT_MODE.POSTPROCESSING) 231 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 232 | 233 | # ScanScript is not part of DualScript, so therefore 234 | # it can't be foreced 235 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 236 | script_mode=SCRIPT_MODE.SCAN) 237 | assert script.detect_mode() == SCRIPT_MODE.NONE 238 | 239 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 240 | script_mode=SCRIPT_MODE.SCHEDULER) 241 | assert script.detect_mode() == SCRIPT_MODE.SCHEDULER 242 | 243 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 244 | script_mode=SCRIPT_MODE.NONE) 245 | assert script.detect_mode() == SCRIPT_MODE.NONE 246 | 247 | def test_dual_force_script03(self): 248 | 249 | class TestDualScript(SchedulerScript, ScanScript): 250 | pass 251 | 252 | # PostProcessingScript is not part of DualScript, so therefore 253 | # it can't be foreced 254 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 255 | script_mode=SCRIPT_MODE.POSTPROCESSING) 256 | assert script.detect_mode() == SCRIPT_MODE.NONE 257 | 258 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 259 | script_mode=SCRIPT_MODE.SCAN) 260 | assert script.detect_mode() == SCRIPT_MODE.SCAN 261 | 262 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 263 | script_mode=SCRIPT_MODE.SCHEDULER) 264 | assert script.detect_mode() == SCRIPT_MODE.SCHEDULER 265 | 266 | script = TestDualScript(logger=False, debug=VERY_VERBOSE_DEBUG, 267 | script_mode=SCRIPT_MODE.NONE) 268 | assert script.detect_mode() == SCRIPT_MODE.NONE 269 | 270 | def test_tri_script01(self): 271 | """ 272 | Testing NZBGet Script initialization using environment variables 273 | Reversing them from test01 alters the initialization, but 274 | should not affect the end result 275 | """ 276 | class TestTriScript(SchedulerScript, PostProcessScript, ScanScript): 277 | pass 278 | 279 | # No environment variables make us unsure what we're testing 280 | script = TestTriScript(logger=False, debug=VERY_VERBOSE_DEBUG) 281 | assert script.detect_mode() == SCRIPT_MODE.NONE 282 | 283 | # Scheduler Sanity 284 | os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] = '1' 285 | script = TestTriScript(logger=False, debug=VERY_VERBOSE_DEBUG) 286 | assert script.detect_mode() == SCRIPT_MODE.SCHEDULER 287 | del os.environ['%sTASKID' % SCHEDULER_ENVIRO_ID] 288 | 289 | # Scan Sanity 290 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 291 | script = TestTriScript(logger=False, debug=VERY_VERBOSE_DEBUG) 292 | assert script.detect_mode() == SCRIPT_MODE.SCAN 293 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 294 | 295 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 296 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 297 | script = TestTriScript(logger=False, debug=VERY_VERBOSE_DEBUG) 298 | assert script.detect_mode() == SCRIPT_MODE.SCAN 299 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 300 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 301 | 302 | # PostProcess Sanity 303 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 304 | script = TestTriScript(logger=False, debug=VERY_VERBOSE_DEBUG) 305 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 306 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 307 | 308 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 309 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 310 | script = TestTriScript(logger=False, debug=VERY_VERBOSE_DEBUG) 311 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 312 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 313 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 314 | 315 | # Post Process still trumps all (if all are set) 316 | os.environ['%sDESTDIR' % SYS_ENVIRO_ID] = TEMP_DIRECTORY 317 | os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = TEMP_DIRECTORY 318 | os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] = TEMP_DIRECTORY 319 | script = TestTriScript(logger=False, debug=VERY_VERBOSE_DEBUG) 320 | assert script.detect_mode() == SCRIPT_MODE.POSTPROCESSING 321 | del os.environ['%sDESTDIR' % SYS_ENVIRO_ID] 322 | del os.environ['%sDIRECTORY' % SCAN_ENVIRO_ID] 323 | del os.environ['%sDIRECTORY' % POSTPROC_ENVIRO_ID] 324 | 325 | def test_dual_run(self): 326 | class TestDualScript(SchedulerScript, PostProcessScript): 327 | def postprocess_main(self, *args, **kwargs): 328 | return None 329 | 330 | def scheduler_main(self, *args, **kwargs): 331 | return False 332 | 333 | script = TestDualScript( 334 | logger=False, debug=VERY_VERBOSE_DEBUG, 335 | script_mode=SCRIPT_MODE.POSTPROCESSING, 336 | ) 337 | assert script.run() == EXIT_CODE.NONE 338 | 339 | script = TestDualScript( 340 | logger=False, debug=VERY_VERBOSE_DEBUG, 341 | script_mode=SCRIPT_MODE.SCHEDULER, 342 | ) 343 | assert script.run() == EXIT_CODE.FAILURE 344 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/caronc/pynzbget.svg?branch=master)](https://travis-ci.org/caronc/pynzbget) 2 | [![CodeCov Status](https://codecov.io/github/caronc/pynzbget/branch/master/graph/badge.svg)](https://codecov.io/github/caronc/pynzbget) 3 | [![Paypal](https://img.shields.io/badge/paypal-donate-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MHANV39UZNQ5E)
4 | 5 | Description 6 | =========== 7 | This provides a python framework to design NZBGet scripts with. The intent 8 | was to greatly simplify the development and debugging process. It was 9 | initially designed to work with NZBGet and is backwards compatible with all previous versions (as far back to v11). However it is also compatible with SABnzbd users too! 10 | 11 | * It contains a built in meta tag parser to extract content from NZB-Files. 12 | _Note: This can only happen if lxml is installed on your system_. 13 | * It can preform very basic obfuscation support on filenames that can not be 14 | interpreted. 15 | * It creates a common SQLite database (optionally) to additionally write 16 | content passed via the set() function. This allows another script to later 17 | call get() and retrieve the data set() by another. 18 | * It prepares logging right out of the box for you, there is no setup required 19 | * All return codes have been simplified to None/True/False (you can still 20 | use the old ones if you want). 21 | * It handles all of the exception handling. By this I mean, your code can throw 22 | an except and it's traceback will be captured gracefully to logging. Then the 23 | framework will look after returning a correct failure error code to NZBGet. 24 | * It provides some very useful functions that are always being re-written 25 | inside of every other NZBGet script such as file scanning. 26 | * It greatly simplifies the handling of environment variables and interaction 27 | to and from NZBGet. 28 | * It can also be adapted to support SABnzbd scripting! Thus any script you write 29 | for NZBGet users, you can also tweak to work [with SABnzbd users too](https://github.com/caronc/pynzbget/wiki/SAB_PostProcessScripts)! 30 | 31 | Documentation 32 | ============= 33 | For the most up to date information and API, visit the wiki at 34 | * https://github.com/caronc/pynzbget/wiki 35 | 36 | The entire framework was based on the information found here: 37 | * [NZBGet](https://nzbget.net) 38 | * [NZBGet Scripting Documentation](http://nzbget.net/Extension_scripts) 39 | * [SABnzbd](http://sabnzbd.org/) 40 | * [SABnzbd Scripting Documentation](https://sabnzbd.org/wiki/scripts/post-processing-scripts) 41 | 42 | Simplified Development 43 | ====================== 44 | The following are some of the functionality that is built in for you: 45 | 46 | * __validate()__ - Handle environment checking, correct versioning as well 47 | as if the expected configuration variables you specified 48 | are present. 49 | 50 | * __health_check()__ - Checks the status of the retrieved content, currently 51 | this is only useful during Post-Processing. 52 | 53 | * __push()__ - Pushes a variables to the NZBGet server. 54 | 55 | 56 | * __set()/get()__ - Hash table get/set attributes that can be set in one script 57 | and then later retrieved from another. get() can also 58 | be used to fetch content that was previously pushed using 59 | the push() tool. You no longer need to work with environment 60 | variables. If you enable the SQLite database, set content is 61 | put here as well so that it can be retrieved by another 62 | script. 63 | 64 | * __unset()__ - This allows you to unset values set by set() and get() as well 65 | as ones set by push(). 66 | 67 | * __nzb_set()__ - Similar to the set() function identified above except it 68 | is used to build an nzb meta hash table which can be later 69 | pushed to the server using push_dnzb(). 70 | 71 | * __add_nzb()__ - Using the built in API/RPC NZBGet supports, this 72 | allows you to specify a path to an NZBFile which you want to 73 | enqueue for downloading. 74 | 75 | * __nzb_get()__ - Retieves NZB Meta information previously stored. 76 | 77 | * __nzb_unset()__ - Removes a variable previously set completely. 78 | 79 | * __get_statistics()__ - Using the built in API/RPC NZBGet supports, this 80 | retrieves and returns the statistics in an easy to ready 81 | dictionary (_PostProcessScript_ only). 82 | 83 | * __get_logs()__ - Using the built in API/RPC NZBGet supports, this 84 | retrieves and returns the latest logs. 85 | 86 | * __get_files()__ - list all files in a specified directory as well as fetching 87 | their details such as filesize, modified date, etc in an 88 | easy to reference dictionary. You can provide a ton of 89 | different filters to minimize the content returned. Filters 90 | can by a regular expression, file prefixes, and/or suffixes. 91 | 92 | * __parse_nzbfile()__ - Parse an NZB-File and extract all of its meta 93 | information from it. lxml must be installed on your 94 | system for this to work correctly. 95 | 96 | * __parse_list()__ - Takes a string (or more) as well as lists of strings as 97 | input. It then cleans it up and produces an easy to 98 | manage list by combining all of the results into 1. 99 | Hence: parse_list('.mkv, .avi') returns: 100 | [ '.mkv', '.avi' ] 101 | 102 | * __parse_path_list()__ - Very smilar to parse_list() except that it is used 103 | to handle directory paths while cleaning them up at the 104 | same time. 105 | 106 | * __parse_bool()__ - Handles all of NZBGet's configuration options such as 107 | 'on' and 'off' as well as 'yes' or 'no', or 'True' and 108 | 'False'. It greatly simplifies the checking of these 109 | variables passed in from NZBGet. 110 | 111 | 112 | * __push_guess()__ - You can push a guessit dictionary (or one of your own 113 | that can help identify your release for other scripts 114 | to use later after yours finishes. 115 | 116 | * __pull_guess()__ - Pull a previous guess pushed by another script. 117 | why redo grunt work if it's already done for you? 118 | if no previous guess content was pushed, then an 119 | empty dictionary is returned. 120 | 121 | * __push_dnzb()__ - You can push all nzb meta information onbtained to 122 | the NZBGet server as DNZB_ meta tags. 123 | 124 | * __pull_dnzb()__ - Pull all DNZB_ meta tags issued by the server and 125 | return their values in a dictionary. 126 | if no DNZB_ (NZB Meta information) was found, then an 127 | empty dictionary is returned instead. 128 | 129 | * __deobfuscate()__ - Take a filename and return it in a deobfuscated to the 130 | best of its ability. (_PostProcessScript_ only) 131 | 132 | * __is_unique_instance()__ - Allows you to ensure your instance of your script is 133 | unique. This is useful for Scheduled scripts which can be 134 | called and then run concurrently with NZBGet. 135 | How To Use 136 | ========== 137 | * Developers are only required to define a class that inherits the NZBGet class 138 | that identifies what they are attempting to write (_ScanScript_, 139 | _PostProcessScript_, etc.). 140 | 141 | * Then you write all of your code a the _main()_ you must define. 142 | 143 | Post Process Script Example 144 | =========================== 145 | ```python 146 | ############################################################################# 147 | ### NZBGET POST-PROCESSING SCRIPT ### 148 | # 149 | # Author: Your Name Goes Here 150 | # 151 | # Describe your Post-Process Script here 152 | # 153 | 154 | ############################################################################ 155 | ### OPTIONS ### 156 | 157 | # 158 | # Enable NZBGet debug logging (yes, no) 159 | # Debug=no 160 | # 161 | 162 | ### NZBGET POST-PROCESSING SCRIPT ### 163 | ############################################################################# 164 | 165 | from nzbget import PostProcessScript 166 | 167 | # Now define your class while inheriting the rest 168 | class MyPostProcessScript(PostProcessScript): 169 | def main(self, *args, **kwargs): 170 | # write all of your code here you would have otherwise put in the 171 | # script 172 | 173 | if not self.validate(): 174 | # No need to document a failure, validate will do that 175 | # on the reason it failed anyway 176 | return False 177 | 178 | # All system environment variables (NZBOP_.*) as well as Post 179 | # Process script specific content (NZBPP_.*) 180 | # following dictionary (without the NZBOP_ or NZBPP_ prefix): 181 | print 'DIRECTORY %s' self.get('DIRECTORY') 182 | print 'NZBNAME %s' self.get('NZBNAME') 183 | print 'NZBFILENAME %s' self.get('NZBFILENAME') 184 | print 'CATEGORY %s' self.get('CATEGORY') 185 | print 'TOTALSTATUS %s' self.get('TOTALSTATUS') 186 | print 'STATUS %s' self.get('STATUS') 187 | print 'SCRIPTSTATUS %s' self.get('SCRIPTSTATUS') 188 | 189 | # Set any variable you want by any key. Note that if you use 190 | # keys that were defined by the system (such as CATEGORY, DIRECTORY, 191 | # etc, you may have some undesirable results. Try to avoid reusing 192 | # system variables already defined (identified above): 193 | self.set('MY_KEY', 'MY_VALUE') 194 | 195 | # You can fetch it back; this will also set an entry in the 196 | # sqlite database for each hash references that can be pulled from 197 | # another script that simply calls self.get('MY_VAR') 198 | print self.get('MY_KEY') # prints MY_VALUE 199 | 200 | # You can also use push() which is similar to set() 201 | # except that it interacts with the NZBGet Server and does not use 202 | # the sqlite database. This can only be reached across other 203 | # scripts if the calling application is NZBGet itself 204 | self.push('ANOTHER_KEY', 'ANOTHER_VALUE') 205 | 206 | # You can still however locally retrieve what you set using push() 207 | # with the get() function 208 | print self.get('ANOTHER_KEY') # prints ANOTHER_VALUE 209 | 210 | # Your script configuration files (NZBPP_.*) are here in this 211 | # dictionary (again without the NZBPP_ prefix): 212 | # assume you defined `Debug=no` in the first 10K of your PostProcessScript 213 | # NZBGet translates this to `NZBPP_DEBUG` which can be retrieved 214 | # as follows: 215 | print 'DEBUG %s' self.get('DEBUG') 216 | 217 | # Returns have been made easy. Just return: 218 | # * True if everything was successful 219 | # * False if there was a problem 220 | # * None if you want to report that you've just gracefully 221 | skipped processing (this is better then False) 222 | in some circumstances. This is neither a failure or a 223 | success status. 224 | 225 | # Feel free to use the actual exit codes as well defined by 226 | # NZBGet on their website. They have also been defined here 227 | # from nzbget import EXIT_CODE 228 | 229 | return True 230 | 231 | # Call your script as follows: 232 | if __name__ == "__main__": 233 | from sys import exit 234 | 235 | # Create an instance of your Script 236 | ppscript = MyPostProcessScript() 237 | 238 | # call run() and exit() using it's returned value 239 | exit(ppscript.run()) 240 | ``` 241 | 242 | Scan Script Example 243 | =================== 244 | ```python 245 | ############################################################################ 246 | ### NZBGET SCAN SCRIPT ### 247 | # 248 | # Author: Your Name Goes Here 249 | # 250 | # Describe your Scan Script here 251 | # 252 | 253 | ############################################################################ 254 | ### OPTIONS ### 255 | 256 | # 257 | # Enable NZBGet debug logging (yes, no) 258 | # Debug=no 259 | # 260 | 261 | ### NZBGET SCAN SCRIPT ### 262 | ############################################################################ 263 | 264 | from nzbget import ScanScript 265 | 266 | # Now define your class while inheriting the rest 267 | class MyScanScript(ScanScript): 268 | def main(self, *args, **kwargs): 269 | # write all of your code here you would have otherwise put in the 270 | # script 271 | 272 | if not self.validate(): 273 | # No need to document a failure, validate will do that 274 | # on the reason it failed anyway 275 | return False 276 | 277 | # All system environment variables (NZBOP_.*) as well as Post 278 | # Process script specific content (NZBNP_.*) 279 | # following dictionary (without the NZBOP_ or NZBNP_ prefix): 280 | print 'DIRECTORY %s' self.get('DIRECTORY') 281 | print 'FILENAME %s' self.get('FILENAME') 282 | print 'NZBNAME %s' self.get('NZBNAME') 283 | print 'CATEGORY %s' self.get('CATEGORY') 284 | print 'PRIORITY %s' self.get('PRIORITY') 285 | print 'TOP %s' self.get('TOP') 286 | print 'PAUSED %s' self.get('PAUSED') 287 | 288 | return True 289 | 290 | # Call your script as follows: 291 | if __name__ == "__main__": 292 | from sys import exit 293 | 294 | # Create an instance of your Script 295 | scanscript = MyScanScript() 296 | 297 | # call run() and exit() using it's returned value 298 | exit(scanscript.run()) 299 | ``` 300 | 301 | Scheduler Script Example 302 | ======================= 303 | ```python 304 | ############################################################################ 305 | ### NZBGET SCHEDULER SCRIPT ### 306 | # 307 | # Describe your Schedule Script here 308 | # Author: Your Name Goes Here 309 | # 310 | 311 | ############################################################################ 312 | ### OPTIONS ### 313 | 314 | # 315 | # Enable NZBGet debug logging (yes, no) 316 | # Debug=no 317 | # 318 | 319 | ### NZBGET SCHEDULER SCRIPT ### 320 | ############################################################################ 321 | 322 | from nzbget import SchedulerScript 323 | 324 | # Now define your class while inheriting the rest 325 | class MySchedulerScript(SchedulerScript): 326 | def main(self, *args, **kwargs): 327 | 328 | # Version Checking, Environment Variables Present, etc 329 | if not self.validate(): 330 | # No need to document a failure, validate will do that 331 | # on the reason it failed anyway 332 | return False 333 | 334 | # write all of your code here you would have otherwise put in the 335 | # script 336 | 337 | # All system environment variables (NZBOP_.*) as well as Post 338 | # Process script specific content (NZBSP_.*) 339 | # following dictionary (without the NZBOP_ or NZBSP_ prefix): 340 | print 'DESTDIR %s' self.get('DESTDIR') 341 | 342 | return True 343 | # Call your script as follows: 344 | if __name__ == "__main__": 345 | from sys import exit 346 | 347 | # Create an instance of your Script 348 | myscript = MySchedulerScript() 349 | 350 | # call run() and exit() using it's returned value 351 | exit(myscript.run()) 352 | ``` 353 | 354 | MultiScript Example 355 | ======================= 356 | ```python 357 | ############################################################################ 358 | ### NZBGET POST-PROCESSING/SCHEDULER SCRIPT ### 359 | # 360 | # Describe your Multi Script here 361 | # 362 | # Author: Your Name Goes Here 363 | # 364 | 365 | ############################################################################ 366 | ### OPTIONS ### 367 | 368 | # 369 | # Enable NZBGet debug logging (yes, no) 370 | # Debug=no 371 | # 372 | 373 | ### NZBGET POST-PROCESSING/SCHEDULER SCRIPT ### 374 | ############################################################################ 375 | 376 | from nzbget import PostProcessScript 377 | from nzbget import SchedulerScript 378 | 379 | # Now define your class while inheriting the rest 380 | class MyMultiScript(PostProcessScript, SchedulerScript): 381 | 382 | def postprocess_main(self, *args, **kwargs): 383 | 384 | # Version Checking, Environment Variables Present, etc 385 | if not self.validate(): 386 | # No need to document a failure, validate will do that 387 | # on the reason it failed anyway 388 | return False 389 | 390 | # write your main function for your Post Processing 391 | 392 | return True 393 | 394 | def scheduler_main(self, *args, **kwargs): 395 | 396 | # Version Checking, Environment Variables Present, etc 397 | if not self.validate(): 398 | # No need to document a failure, validate will do that 399 | # on the reason it failed anyway 400 | return False 401 | 402 | # write your main function for your Post Processing 403 | 404 | return True 405 | 406 | # Call your script as follows: 407 | if __name__ == "__main__": 408 | from sys import exit 409 | 410 | # Create an instance of your Script 411 | myscript = MyMultiScript() 412 | 413 | # call run() and exit() using it's returned value 414 | exit(myscript.run()) 415 | ``` 416 | -------------------------------------------------------------------------------- /nzbget/ScanScript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A scripting wrapper for NZBGet's Scan Scripting 4 | # 5 | # Copyright (C) 2014 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | """ 18 | This class was intended to make writing NZBGet Scripts easier to manage and 19 | write by handling the common error handling and provide the most reused code 20 | in a re-usable container. It was initially written to work with NZBGet v13 21 | but provides most backwards compatibility. 22 | 23 | It was designed to be inheritied as a base class requiring you to only write 24 | the main() function which should preform the task you are intending. 25 | 26 | It looks after fetching all of the environment variables and will parse 27 | the meta information out of the NZB-File. 28 | 29 | It allows you to set variables that other scripts can access if they need to 30 | using the set() and get() variables. This is done through a simply self 31 | maintained hash table type structure within a sqlite database. All the 32 | wrapper functions are already written. If you call 'set('MYKEY', 1') 33 | you can call get('MYKEY') in another script and continue working 34 | 35 | push() functions written to pass information back to nzbget using it's 36 | processing engine. 37 | 38 | all exceptions are now automatically handled and logging can be easily 39 | changed from stdout, to stderr or to a file. 40 | 41 | Test suite built in (using python-nose) to ensure old global variables 42 | will still work as well as make them easier to access and manipulate. 43 | 44 | Some inline documentation was based on content provided at: 45 | - http://nzbget.net/Extension_scripts 46 | 47 | 48 | ############################################################################ 49 | Scan Script Usage/Example 50 | ############################################################################ 51 | 52 | ############################################################################ 53 | ### NZBGET SCAN SCRIPT ### 54 | # 55 | # Describe your Scan Script here 56 | # Author: Chris Caron 57 | # 58 | 59 | ############################################################################ 60 | ### OPTIONS ### 61 | 62 | # 63 | # Enable NZBGet debug logging (yes, no) 64 | # Debug=no 65 | # 66 | 67 | ### NZBGET SCAN SCRIPT ### 68 | ############################################################################ 69 | 70 | from nzbget import ScanScript 71 | 72 | # Now define your class while inheriting the rest 73 | class MyScanScript(ScanScript): 74 | def main(self, *args, **kwargs): 75 | 76 | # Version Checking, Environment Variables Present, etc 77 | if not self.validate(): 78 | # No need to document a failure, validate will do that 79 | # on the reason it failed anyway 80 | return False 81 | 82 | # write all of your code here you would have otherwise put in the 83 | # script 84 | 85 | # All system environment variables (NZBOP_.*) as well as Post 86 | # Process script specific content (NZBNP_.*) 87 | # following dictionary (without the NZBOP_ or NZBNP_ prefix): 88 | print('TEMPDIR (directory is: %s' % self.get('TEMPDIR')) 89 | print('DIRECTORY %s' self.get('DIRECTORY')) 90 | print('FILENAME %s' self.get('FILENAME')) 91 | print('NZBNAME %s' self.get('NZBNAME')) 92 | print('CATEGORY %s' self.get('CATEGORY')) 93 | print('PRIORITY %s' self.get('PRIORITY')) 94 | print('TOP %s' self.get('TOP')) 95 | print('PAUSED %s' self.get('PAUSED')) 96 | 97 | # Set any variable you want by any key. Note that if you use 98 | # keys that were defined by the system (such as CATEGORY, DIRECTORY, 99 | # etc, you may have some undesirable results. Try to avoid reusing 100 | # system variables already defined (identified above): 101 | self.set('MY_KEY', 'MY_VALUE') 102 | 103 | # You can fetch it back; this will also set an entry in the 104 | # sqlite database for each hash references that can be pulled from 105 | # another script that simply calls self.get('MY_KEY') 106 | print(self.get('MY_KEY')) # prints MY_VALUE 107 | 108 | # You can also use push() which is similar to set() 109 | # except that it interacts with the NZBGet Server and does not use 110 | # the sqlite database. This can only be reached across other 111 | # scripts if the calling application is NZBGet itself 112 | self.push('ANOTHER_KEY', 'ANOTHER_VALUE') 113 | 114 | # You can still however locally retrieve what you set using push() 115 | # with the get() function 116 | print(self.get('ANOTHER_KEY')) # prints ANOTHER_VALUE 117 | 118 | # Your script configuration files (NZBNP_.*) are here in this 119 | # dictionary (again without the NZBNP_ prefix): 120 | # assume you defined `Debug=no` in the first 10K of your ScanScript 121 | # NZBGet translates this to `NZBNP_DEBUG` which can be retrieved 122 | # as follows: 123 | print('DEBUG %s' self.get('DEBUG')) 124 | 125 | # Returns have been made easy. Just return: 126 | # * True if everything was successful 127 | # * False if there was a problem 128 | # * None if you want to report that you've just gracefully 129 | skipped processing (this is better then False) 130 | in some circumstances. This is neither a failure or a 131 | success status. 132 | 133 | # Feel free to use the actual exit codes as well defined by 134 | # NZBGet on their website. They have also been defined here 135 | # from nzbget.ScriptBase import EXIT_CODE 136 | 137 | return True 138 | 139 | # Call your script as follows: 140 | if __name__ == "__main__": 141 | from sys import exit 142 | 143 | # Create an instance of your Script 144 | myscript = MyScanScript() 145 | 146 | # call run() and exit() using it's returned value 147 | exit(myscript.run()) 148 | 149 | """ 150 | import re 151 | from os import chdir 152 | from os import environ 153 | from os.path import isdir 154 | from os.path import basename 155 | from os.path import abspath 156 | 157 | # Relative Includes 158 | from .ScriptBase import ScriptBase 159 | from .ScriptBase import SCRIPT_MODE 160 | from .ScriptBase import NZBGET_BOOL_FALSE 161 | from .ScriptBase import PRIORITY 162 | from .ScriptBase import PRIORITIES 163 | from .PostProcessScript import POSTPROC_ENVIRO_ID 164 | 165 | # Environment variable that prefixes all NZBGET options being passed into 166 | # scripts with respect to the NZB-File (used in Scan Scripts) 167 | SCAN_ENVIRO_ID = 'NZBNP_' 168 | 169 | # Precompile Regulare Expression for Speed 170 | SCAN_OPTS_RE = re.compile('^%s([A-Z0-9_]+)$' % SCAN_ENVIRO_ID) 171 | 172 | class ScanScript(ScriptBase): 173 | def __init__(self, *args, **kwargs): 174 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 175 | # Multi-Script Support 176 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 177 | if not hasattr(self, 'script_dict'): 178 | # Only define once 179 | self.script_dict = {} 180 | self.script_dict[SCRIPT_MODE.SCAN] = self 181 | 182 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 183 | # Initialize Parent 184 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 185 | super(ScanScript, self).__init__(*args, **kwargs) 186 | 187 | def scan_init(self, *args, **kwargs): 188 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 189 | # Fetch Script Specific Arguments 190 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 191 | directory = kwargs.get('directory') 192 | nzbname = kwargs.get('nzbname') 193 | filename = kwargs.get('filename') 194 | category = kwargs.get('category') 195 | priority = kwargs.get('priority') 196 | top = kwargs.get('top') 197 | paused = kwargs.get('paused') 198 | parse_nzbfile = kwargs.get('parse_nzbfile') 199 | use_database = kwargs.get('use_database') 200 | 201 | # Fetch/Load Scan Script Configuration 202 | script_config = dict([(SCAN_OPTS_RE.match(k).group(1), v.strip()) \ 203 | for (k, v) in environ.items() if SCAN_OPTS_RE.match(k)]) 204 | 205 | if self.vvdebug: 206 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 207 | # Print Global Script Varables to help debugging process 208 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 209 | for k, v in script_config.items(): 210 | self.logger.vvdebug('%s%s=%s' % (SCAN_ENVIRO_ID, k, v)) 211 | 212 | # Merge Script Configuration With System Config 213 | script_config.update(self.system) 214 | self.system = script_config 215 | 216 | # self.directory 217 | # This is the path to the destination directory for downloaded files. 218 | if directory is None: 219 | self.directory = environ.get( 220 | '%sDIRECTORY' % SCAN_ENVIRO_ID, 221 | ) 222 | else: 223 | self.directory = directory 224 | 225 | # self.nzbname 226 | # User-friendly name of processed nzb-file as it is displayed by the 227 | # program. The file path and extension are removed. If download was 228 | # renamed, this parameter reflects the new name. 229 | if nzbname is None: 230 | self.nzbname = environ.get( 231 | '%sNZBNAME' % SCAN_ENVIRO_ID, 232 | ) 233 | else: 234 | self.nzbname = nzbname 235 | 236 | # self.filename 237 | # Name of file to be processed 238 | if filename is None: 239 | self.filename = environ.get( 240 | '%sFILENAME' % SCAN_ENVIRO_ID, 241 | ) 242 | else: 243 | self.filename = filename 244 | 245 | # self.category 246 | # Category assigned to nzb-file (can be empty string). 247 | if category is None: 248 | self.category = environ.get( 249 | '%sCATEGORY' % SCAN_ENVIRO_ID, 250 | ) 251 | else: 252 | self.category = category 253 | 254 | # self.priority 255 | # The priority of the nzb file being scanned 256 | if priority is None: 257 | self.priority = environ.get( 258 | '%sPRIORITY' % SCAN_ENVIRO_ID, 259 | ) 260 | else: 261 | self.priority = priority 262 | 263 | # self.top 264 | # Flag indicating that the file will be added to the top of queue 265 | if top is None: 266 | self.top = environ.get( 267 | '%sTOP' % SCAN_ENVIRO_ID, 268 | ) 269 | else: 270 | self.top = top 271 | 272 | # self.paused 273 | # Flag indicating that the file will be added as paused 274 | if paused is None: 275 | self.paused = environ.get( 276 | '%sPAUSED' % SCAN_ENVIRO_ID, 277 | ) 278 | else: 279 | self.paused = paused 280 | 281 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 282 | # Error Handling 283 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 284 | if self.filename: 285 | # absolute path names 286 | self.filename = abspath(self.filename) 287 | 288 | if parse_nzbfile: 289 | # Initialize information fetched from NZB-File 290 | # We intentionally allow existing nzbheaders to over-ride 291 | # any found in the nzbfile 292 | self.nzbheaders = dict( 293 | self.parse_nzbfile( 294 | self.filename, check_queued=True)\ 295 | .items() + self.pull_dnzb().items(), 296 | ) 297 | 298 | if self.directory: 299 | # absolute path names 300 | self.directory = abspath(self.directory) 301 | 302 | if not (self.directory and isdir(self.directory)): 303 | self.logger.debug('Process directory is missing: %s' % \ 304 | self.directory) 305 | else: 306 | try: 307 | chdir(self.directory) 308 | except OSError: 309 | self.logger.debug('Process directory is not accessible: %s' % \ 310 | self.directory) 311 | 312 | # Priority 313 | if not isinstance(self.priority, int): 314 | try: 315 | self.priority = int(self.priority) 316 | except: 317 | self.priority = PRIORITY.NORMAL 318 | 319 | if self.priority not in PRIORITIES: 320 | self.priority = PRIORITY.NORMAL 321 | 322 | # Top 323 | try: 324 | self.top = bool(int(self.top)) 325 | except: 326 | self.top = False 327 | 328 | # Paused 329 | try: 330 | self.paused = bool(int(self.paused)) 331 | except: 332 | self.paused = False 333 | 334 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 335 | # Enforce system/global variables for script processing 336 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 337 | self.system['DIRECTORY'] = self.directory 338 | if self.directory is not None: 339 | environ['%sDIRECTORY' % SCAN_ENVIRO_ID] = self.directory 340 | 341 | self.system['NZBNAME'] = self.nzbname 342 | if self.nzbname is not None: 343 | environ['%sNZBNAME' % SCAN_ENVIRO_ID] = self.nzbname 344 | 345 | self.system['FILENAME'] = self.filename 346 | if self.filename is not None: 347 | environ['%sFILENAME' % SCAN_ENVIRO_ID] = self.filename 348 | 349 | self.system['CATEGORY'] = self.category 350 | if self.category is not None: 351 | environ['%sCATEGORY' % SCAN_ENVIRO_ID] = self.category 352 | 353 | self.system['PRIORITY'] = self.priority 354 | if self.priority is not None: 355 | environ['%sPRIORITY' % SCAN_ENVIRO_ID] = str(self.priority) 356 | 357 | self.system['TOP'] = self.top 358 | if self.top is not None: 359 | environ['%sTOP' % SCAN_ENVIRO_ID] = str(int(self.top)) 360 | 361 | self.system['PAUSED'] = self.paused 362 | if self.paused is not None: 363 | environ['%sPAUSED' % SCAN_ENVIRO_ID] = str(int(self.paused)) 364 | 365 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 366 | # Create Database for set() and get() operations 367 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 368 | if use_database: 369 | # database_key is inherited in the parent class 370 | # future calls of set() and get() will allow access 371 | # to the database now 372 | try: 373 | self.database_key = basename(self.filename) 374 | self.logger.info('Connected to SQLite Database') 375 | except AttributeError: 376 | pass 377 | 378 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 379 | # Debug Flag Check 380 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 381 | def scan_debug(self, *args, **kargs): 382 | """Uses the environment variables to detect if debug mode is set 383 | """ 384 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 385 | # Debug Handling 386 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 387 | return self.parse_bool( 388 | environ.get('%sDEBUG' % SCAN_ENVIRO_ID, NZBGET_BOOL_FALSE), 389 | ) 390 | 391 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 392 | # Sanity 393 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 394 | def scan_sanity_check(self, *args, **kargs): 395 | """Sanity checking to ensure this really is a post_process script 396 | """ 397 | return ('%sDIRECTORY' % POSTPROC_ENVIRO_ID not in environ) and \ 398 | ('%sDIRECTORY' % SCAN_ENVIRO_ID in environ) 399 | 400 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 401 | # Validatation 402 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 403 | def scan_validate(self, keys=None, min_version=11, *args, **kargs): 404 | """validate against environment variables 405 | """ 406 | is_okay = super(ScanScript, self)._validate( 407 | keys=keys, 408 | min_version=min_version, 409 | ) 410 | return is_okay 411 | 412 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 413 | # File Retrieval 414 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 415 | def scan_get_files(self, search_dir=None, regex_filter=None, 416 | prefix_filter=None, suffix_filter=None, 417 | fullstats=False, *args, **kargs): 418 | """a wrapper to the get_files() function defined in the inherited class 419 | the only difference is the search_dir automatically uses the 420 | defined download `directory` as a default (if not specified). 421 | """ 422 | if search_dir is None: 423 | search_dir = self.directory 424 | 425 | return super(ScanScript, self)._get_files( 426 | search_dir=search_dir, *args, **kargs) 427 | 428 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 429 | # Set/Control Functions (also passes data back to NZBGet) 430 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 431 | def push_nzbname(self, nzbname=None): 432 | if nzbname: 433 | # Update local nzbname 434 | self.nzbname = nzbname 435 | 436 | # Accomodate other environmental variables 437 | self.system['NZBNAME'] = self.nzbname 438 | environ['%sNZBNAME' % SCAN_ENVIRO_ID] = self.nzbname 439 | 440 | # Alert NZBGet of Change 441 | return self._push( 442 | key='NZBNAME', 443 | value=self.nzbname, 444 | ) 445 | 446 | def push_category(self, category=None): 447 | if category: 448 | # Update local category 449 | self.category = category 450 | 451 | # Accomodate other environmental variables 452 | self.system['CATEGORY'] = self.category 453 | environ['%sCATEGORY' % SCAN_ENVIRO_ID] = self.category 454 | 455 | # Alert NZBGet of Change 456 | return self._push( 457 | key='CATEGORY', 458 | value=self.category, 459 | ) 460 | 461 | def push_priority(self, priority=None): 462 | 463 | if priority is not None: 464 | if priority not in PRIORITIES: 465 | return False 466 | 467 | # Update local priority 468 | self.priority = priority 469 | 470 | # Accomodate other environmental variables 471 | self.system['PRIORITY'] = self.priority 472 | environ['%sPRIORITY' % SCAN_ENVIRO_ID] = str(self.priority) 473 | 474 | # Alert NZBGet of Change 475 | return self._push( 476 | key='PRIORITY', 477 | value=self.priority, 478 | ) 479 | 480 | def push_top(self, top=None): 481 | 482 | if top is not None: 483 | # Update local priority 484 | try: 485 | self.top = bool(int(top)) 486 | except: 487 | return False 488 | 489 | # Accomodate other environmental variables 490 | self.system['TOP'] = self.top 491 | environ['%sTOP' % SCAN_ENVIRO_ID] = str(int(self.top)) 492 | 493 | # Alert NZBGet of Change 494 | return self._push( 495 | key='TOP', 496 | # Convert bool to int for response 497 | value=int(self.top), 498 | ) 499 | 500 | def push_paused(self, paused=None): 501 | 502 | if paused is not None: 503 | # Update local priority 504 | try: 505 | self.paused = bool(int(paused)) 506 | except: 507 | return False 508 | 509 | # Accomodate other environmental variables 510 | self.system['PAUSED'] = self.paused 511 | environ['%sPAUSED' % SCAN_ENVIRO_ID] = str(int(self.paused)) 512 | 513 | # Alert NZBGet of Change 514 | return self._push( 515 | key='PAUSED', 516 | # Convert bool to int for response 517 | value=int(self.paused), 518 | ) 519 | -------------------------------------------------------------------------------- /nzbget/Database.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A simple SQLite (3) Database wrapper for shared NZBGet transactions 4 | # 5 | # Copyright (C) 2014 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | """ 18 | This script will allow a common gateway between scripts that require 19 | interaction with one another. It sets up a simple hash table like system 20 | that can be used as a method of passing variables around using the 21 | get() and set() operation. 22 | 23 | It's initialized by setting up a container object. This keeps all set() 24 | get() operations limited to what is set by the container itself. 25 | 26 | NZBScripts should not be dependant on one another. The intention 27 | is not to use this to set a variable so it can be retrieved late by another 28 | script. However setting a variable in advance that another script needs 29 | to preform the same proceedure to get to can be grealy simplified by sharing 30 | cpu power by just sharing your results in advance. 31 | """ 32 | import sqlite3 33 | import re 34 | import six 35 | from datetime import datetime 36 | from datetime import timedelta 37 | from os.path import isfile 38 | from os.path import dirname 39 | from os.path import isdir 40 | from os import unlink 41 | from os import makedirs 42 | 43 | from .Logger import init_logger 44 | from .Logger import destroy_logger 45 | from logging import Logger 46 | 47 | # This should always be set to the current database version 48 | NZBGET_DATABASE_VERSION = 1 49 | 50 | # In seconds, we identify how long to let content linger in the 51 | # database for before it's purged (no sense letting content grow) 52 | # below sets the timer for 12 hours. This database contains just transient 53 | # information; there is no need to keep content longer then this. 54 | PURGE_AGE = 60 * 60 * 12 55 | 56 | # Format required to correctly query and handle SQLite Dates 57 | SQLITE_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' 58 | 59 | NZBGET_SCHEMA = { 60 | 1: [ 61 | # Lookup just contains static content and VSPs used by the database 62 | # and it's handling of data. You should not write content here. this 63 | # is just reserved for internal operations 64 | "CREATE TABLE lookup (key TEXT PRIMARY KEY, value TEXT)", 65 | # Init base version of zero (0), the _build_schema takes care 66 | # of updating this value 67 | "INSERT INTO lookup (key, value) VALUES ('SCHEMA_VERSION', '0')", 68 | "INSERT INTO lookup (key, value) VALUES ('PURGE_AGE', '%d')" % \ 69 | PURGE_AGE, 70 | # Key Store free for use for developers of scripts 71 | # just use the set() and get() functions 72 | "CREATE TABLE keystore (" + \ 73 | "container TEXT, " + \ 74 | "category TEXT, " + \ 75 | "key TEXT, " + \ 76 | "value TEXT, " + \ 77 | "last_update DATETIME DEFAULT current_timestamp" + \ 78 | ")", 79 | "CREATE UNIQUE INDEX keystore_idx ON keystore (container, category, key)", 80 | "CREATE INDEX last_update_idx ON keystore (last_update)", 81 | ], 82 | } 83 | # This is just used for a quick reference when verifying that 84 | # all of the schema is present (during initialization) 85 | # Define just table names here that were fully declared above in 86 | # the schema 87 | NZBGET_SCHEMA_TABLES = ( 88 | u'lookup', 89 | u'keystore', 90 | ) 91 | 92 | # Categories allow us to further partition our keystore hash table 93 | # into groups for others subsections to use without stepping on 94 | # each other. 95 | class Category(object): 96 | # General Script Configuration 97 | CONFIG = u'config' 98 | # NZB/NZBD Defined Variables 99 | NZB = u'nzb' 100 | 101 | CATEGORIES = [ Category.CONFIG, Category.NZB, ] 102 | DEFAULT_CATEGORY = Category.CONFIG 103 | 104 | # keys should not be complicated... make it so they aren't 105 | VALID_KEY_RE = re.compile('[^a-zA-Z0-9_.-]') 106 | 107 | class Database(object): 108 | def __init__(self, container, database, reset=False, 109 | logger=True, debug=False): 110 | """Initializes the database if it isn't already prepared, 111 | Th en fetches an index to work with based on the key passed in. 112 | If reset is set to True, then if an existing entry is found, it is 113 | automatically reset and treated as a fresh process. 114 | """ 115 | # self.container 116 | # This acts as the index for fetching content to and from 117 | # the keystore as well as tracking what goes on. 118 | self.container = container 119 | self.database = database 120 | 121 | # A global switch that basically disables this class silently 122 | # the purpose it to prevent it from thrashing on re-occuring 123 | # failures that are a result of the users system environment 124 | self.disabled = False 125 | 126 | # Database Connection 127 | self.socket = None 128 | 129 | # logger identifier 130 | self.logger_id = self.__class__.__name__ 131 | self.logger = logger 132 | self.debug = debug 133 | 134 | if isinstance(self.logger, six.string_types): 135 | # Use Log File 136 | self.logger = init_logger( 137 | name=self.logger_id, 138 | logger=logger, 139 | debug=debug, 140 | ) 141 | 142 | elif not isinstance(self.logger, Logger): 143 | # handle all other types 144 | if logger is None: 145 | # None means don't log anything 146 | self.logger = init_logger( 147 | name=self.logger_id, 148 | logger=None, 149 | debug=debug, 150 | ) 151 | else: 152 | # Use STDOUT for now 153 | self.logger = init_logger( 154 | name=self.logger_id, 155 | logger=True, 156 | debug=debug, 157 | ) 158 | else: 159 | self.logger_id = None 160 | 161 | if reset or not isfile(self.database): 162 | # Initialize 163 | self._reset() 164 | 165 | # Connect to Database 166 | if not self.connect(): 167 | raise EnvironmentError('Could not access database.') 168 | 169 | if not self._schema_okay(): 170 | # fail-safe 171 | self._reset() 172 | if not self._schema_okay(): 173 | raise EnvironmentError('Could not prepare database.') 174 | 175 | # Keep content clean 176 | # self.prune() 177 | 178 | def __del__(self): 179 | """Gracefully close any connection to the database on 180 | destruction of this class 181 | """ 182 | self.close() 183 | if self.logger_id: 184 | destroy_logger(self.logger_id) 185 | 186 | def _reset(self, rebuild=True): 187 | """Resets the database 188 | If rebuild is set to True then the schema is re-prepared 189 | """ 190 | try: 191 | self.close() 192 | except: 193 | pass 194 | 195 | try: 196 | # Best way to reset the database is to 197 | # remove it entirely 198 | unlink(self.database) 199 | except: 200 | pass 201 | 202 | if not isdir(dirname(self.database)): 203 | try: 204 | # safely ensure the directory exists 205 | makedirs(dirname(self.database)) 206 | except: 207 | self.logger.error('Could not create directory: %s' % \ 208 | dirname(self.database)) 209 | return False 210 | 211 | if rebuild: 212 | # Connect to Database 213 | self.connect() 214 | if not self._schema_okay(): 215 | if not self._build_schema(): 216 | return False 217 | 218 | self.logger.debug('Successsfully reset database: %s' % self.database) 219 | return True 220 | 221 | def connect(self): 222 | """Establish a connection to the database 223 | """ 224 | if self.socket is not None: 225 | # Already connected 226 | return True 227 | 228 | if self.disabled: 229 | # Connections turned off 230 | return False 231 | 232 | try: 233 | self.socket = sqlite3.connect(self.database, 20) 234 | self.logger.debug( 235 | 'Opened connection to SQLite Database: %s' % \ 236 | self.database, 237 | ) 238 | 239 | except Exception as e: 240 | self.socket = None 241 | self.logger.error('Failed to connect SQLite Database') 242 | self.logger.debug( 243 | 'SQLite Database (%s) failure message: %s' % ( 244 | self.database, 245 | str(e), 246 | )) 247 | return False 248 | 249 | return True 250 | 251 | def close(self): 252 | if self.socket is not None: 253 | try: 254 | self.socket.close() 255 | except: 256 | pass 257 | 258 | self.socket = None 259 | self.logger.debug( 260 | 'Closed connection to SQLite Database %s' % \ 261 | self.database, 262 | ) 263 | return 264 | 265 | def execute(self, *args, **kwargs): 266 | 267 | if not self.socket: 268 | if not self.connect(): 269 | return None 270 | 271 | try: 272 | result = self.socket.execute(*args, **kwargs) 273 | self.logger.vdebug('DB Executing: %s' % str(args)) 274 | 275 | except sqlite3.OperationalError as e: 276 | self.logger.debug('DB Execute OpError: %s' % str(e)) 277 | return None 278 | 279 | except Exception as e: 280 | self.logger.debug('DB Execute Error: %s' % str(e)) 281 | return None 282 | 283 | return result 284 | 285 | 286 | def _build_schema(self, start_version=None): 287 | """Build the schema or upgrade it (depending on the 288 | start_version defined) 289 | """ 290 | if not self.socket: 291 | if not self.connect(): 292 | return False 293 | 294 | if not isinstance(start_version, int): 295 | start_version = self._get_version() 296 | 297 | for version in [ k for k in sorted(NZBGET_SCHEMA.keys()) \ 298 | if k > start_version]: 299 | for query in NZBGET_SCHEMA[version]: 300 | self.execute(query) 301 | self.execute( 302 | "UPDATE lookup SET value = ? " + \ 303 | "WHERE key = 'SCHEMA_VERSION'", 304 | (str(version),), 305 | ) 306 | return True 307 | 308 | def _schema_okay(self): 309 | """A simple check to see if the schema is okay 310 | """ 311 | if not self.socket: 312 | if not self.connect(): 313 | return False 314 | 315 | for table in NZBGET_SCHEMA_TABLES: 316 | try: 317 | if not bool(len(self.execute( 318 | "SELECT 1 FROM sqlite_master WHERE name = ?", (table, ) 319 | ).fetchall())): 320 | return False 321 | except AttributeError: 322 | # execute() returned None causing fetchall() not be 323 | # a valid attribute; this is the same as just being a 324 | # bad schema 325 | return False 326 | 327 | return self._get_version() == NZBGET_DATABASE_VERSION 328 | 329 | def _get_version(self): 330 | if not self.socket: 331 | if not self.connect(): 332 | return 0 333 | 334 | result = self.execute( 335 | "SELECT value FROM lookup WHERE key = ?", 336 | ('SCHEMA_VERSION', ), 337 | ) 338 | if result: 339 | try: 340 | return int(result.fetchall()[0][-1]) 341 | except: 342 | return 0 343 | return 0 344 | 345 | def prune(self, age=None, vacuum=True): 346 | """Sweep old entries out of database to keep it's size 347 | under control 348 | """ 349 | if not self.socket: 350 | if not self.connect(): 351 | return False 352 | 353 | # Default 354 | prune_age = PURGE_AGE 355 | if not isinstance(age , int): 356 | result = self.execute( 357 | "SELECT value FROM lookup WHERE key = ?", 358 | ('PURGE_AGE', ), 359 | ) 360 | if not result: 361 | # Simply put, if you remove this key, pruning will not 362 | # take place ever. 363 | return True 364 | 365 | try: 366 | prune_age = int(result.fetchall()[0][-1]) 367 | except: 368 | pass 369 | else: 370 | prune_age = age 371 | 372 | purge_ref = datetime.now() - timedelta(seconds=prune_age) 373 | purge_ref = purge_ref.strftime(SQLITE_DATE_FORMAT) 374 | 375 | self.execute( 376 | "DELETE FROM keystore WHERE last_update <= ?", 377 | (purge_ref, ), 378 | ) 379 | if vacuum: 380 | self.execute("VACUUM") 381 | 382 | return True 383 | 384 | def unset(self, key, category=None): 385 | """Remove a key from the database 386 | """ 387 | if not self.socket: 388 | if not self.connect(): 389 | return False 390 | 391 | if not category: 392 | category = DEFAULT_CATEGORY 393 | else: 394 | category = VALID_KEY_RE.sub('', category).lower() 395 | 396 | if category not in CATEGORIES: 397 | self.logger.error("Database category '%s' does not exist.") 398 | return False 399 | 400 | # clean key 401 | key = VALID_KEY_RE.sub('', key).upper() 402 | if not key: 403 | return False 404 | 405 | try: 406 | # First see if keystore already has a match 407 | if(bool(len(self.socket.execute( 408 | "SELECT value FROM keystore WHERE " + \ 409 | "container = ? AND category = ? AND key = ?", 410 | (self.container, category, key)).fetchall()))): 411 | if not self.socket.execute( 412 | "DELETE FROM keystore WHERE " +\ 413 | "container = ? AND category = ? AND key = ?", 414 | (self.container, category, key),): 415 | return False 416 | 417 | except sqlite3.OperationalError as e: 418 | # Database is corrupt or changed 419 | self.logger.debug( 420 | "Database.unset() Operational Error: %s" % str(e)) 421 | 422 | if not self._reset(): 423 | self.logger.error( 424 | "Detected damaged database; countermeasures failed.", 425 | ) 426 | self.disabled = True 427 | 428 | else: 429 | self.logger.info( 430 | "Detected damaged database; situation corrected.", 431 | ) 432 | 433 | return True 434 | 435 | 436 | def set(self, key, value, category=None): 437 | """Set a key and a value into the database for retrieval later 438 | """ 439 | if not self.socket: 440 | if not self.connect(): 441 | return False 442 | 443 | if not category: 444 | category = DEFAULT_CATEGORY 445 | else: 446 | category = VALID_KEY_RE.sub('', category).lower() 447 | 448 | if category not in CATEGORIES: 449 | self.logger.error("Database category '%s' does not exist.") 450 | return False 451 | 452 | now = datetime.now().strftime(SQLITE_DATE_FORMAT) 453 | 454 | # clean key 455 | key = VALID_KEY_RE.sub('', key).upper() 456 | if not key: 457 | return False 458 | 459 | # Get a cursor object 460 | cursor = self.socket.cursor() 461 | 462 | try: 463 | # First see if keystore already has a match 464 | if(bool(len(cursor.execute( 465 | "SELECT value FROM keystore WHERE " + \ 466 | "container = ? AND category = ? AND key = ?", 467 | (self.container, category, key)).fetchall()))): 468 | if not cursor.execute( 469 | "UPDATE keystore SET value = ?, last_update = ?" + \ 470 | " WHERE container = ? AND category = ? AND key = ?", 471 | (value, now, self.container, category, key),): 472 | return False 473 | else: 474 | if not cursor.execute( 475 | "INSERT INTO keystore " + \ 476 | "(container, category, key, value, last_update) " + \ 477 | "VALUES (?, ?, ?, ?, ?)", 478 | (self.container, category, key, value, now),): 479 | return False 480 | # commit changes 481 | self.socket.commit() 482 | 483 | except sqlite3.OperationalError as e: 484 | # Database is corrupt or changed 485 | self.logger.debug( 486 | "Database.set() Operational Error: %s" % str(e)) 487 | 488 | if not self._reset(): 489 | self.logger.error( 490 | "Detected damaged database; countermeasures failed.", 491 | ) 492 | self.disabled = True 493 | 494 | else: 495 | self.logger.info( 496 | "Detected damaged database; situation corrected.", 497 | ) 498 | return False 499 | 500 | return True 501 | 502 | def get(self, key, default=None, category=None): 503 | """Get a value after specifying a key 504 | """ 505 | 506 | if not self.socket: 507 | if not self.connect(): 508 | return default 509 | 510 | if not category: 511 | category = DEFAULT_CATEGORY 512 | else: 513 | category = VALID_KEY_RE.sub('', category).lower() 514 | 515 | if category not in CATEGORIES: 516 | self.logger.error("Database category '%s' does not exist.") 517 | return default 518 | 519 | # clean key 520 | key = VALID_KEY_RE.sub('', key).upper() 521 | 522 | try: 523 | result = self.socket.execute( 524 | "SELECT value FROM keystore " + \ 525 | "WHERE container = ? AND category = ? AND key = ?", 526 | (self.container, category, key), 527 | ) 528 | if result: 529 | try: 530 | return result.fetchall()[0][-1] 531 | except: 532 | return default 533 | 534 | except sqlite3.OperationalError as e: 535 | # Database is corrupt or changed 536 | self.logger.debug( 537 | "Database.get() Operational Error: %s" % str(e)) 538 | 539 | if not self._reset(): 540 | self.logger.error( 541 | "Detected damaged database; countermeasures failed.", 542 | ) 543 | self.disabled = True 544 | 545 | else: 546 | self.logger.info( 547 | "Detected damaged database; situation corrected.", 548 | ) 549 | 550 | return default 551 | 552 | def items(self, category=None): 553 | """Return all variables as a list of tuples (k, p) 554 | """ 555 | 556 | items = list() 557 | 558 | if not self.socket: 559 | if not self.connect(): 560 | return items 561 | 562 | if not category: 563 | category = DEFAULT_CATEGORY 564 | else: 565 | category = VALID_KEY_RE.sub('', category).lower() 566 | 567 | if category not in CATEGORIES: 568 | self.logger.error("Database category '%s' does not exist.") 569 | return items 570 | 571 | # Get a cursor object 572 | cursor = self.socket.cursor() 573 | 574 | try: 575 | cursor.execute( 576 | "SELECT key, value FROM keystore " + \ 577 | "WHERE container = ? AND category = ?", 578 | (self.container, category), 579 | ) 580 | 581 | except sqlite3.OperationalError as e: 582 | # Database is corrupt or changed 583 | self.logger.debug( 584 | "Database.items() Operational Error: %s" % str(e)) 585 | 586 | if not self._reset(): 587 | self.logger.error( 588 | "Detected damaged database; countermeasures failed.", 589 | ) 590 | self.disabled = True 591 | 592 | else: 593 | self.logger.info( 594 | "Detected damaged database; situation corrected.", 595 | ) 596 | 597 | # early return of empty list 598 | return items 599 | 600 | while True: 601 | row = cursor.fetchone() 602 | if row is None: 603 | break 604 | 605 | items.append((row[0], row[1])) 606 | 607 | return items 608 | -------------------------------------------------------------------------------- /nzbget/QueueScript.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # 3 | # A scripting wrapper for NZBGet's Queue Scripting 4 | # 5 | # Copyright (C) 2014 Chris Caron 6 | # 7 | # This program is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | """ 18 | This class was intended to make writing NZBGet Scripts easier to manage and 19 | write by handling the common error handling and provide the most reused code 20 | in a re-usable container. It was initially written to work with NZBGet v13 21 | but provides most backwards compatibility. 22 | 23 | It was designed to be inheritied as a base class requiring you to only write 24 | the main() function which should preform the task you are intending. 25 | 26 | It looks after fetching all of the environment variables and will parse 27 | the meta information out of the NZB-File. 28 | 29 | It allows you to set variables that other scripts can access if they need to 30 | using the set() and get() variables. This is done through a simply self 31 | maintained hash table type structure within a sqlite database. All the 32 | wrapper functions are already written. If you call 'set('MYKEY', 1') 33 | you can call get('MYKEY') in another script and continue working 34 | 35 | push() functions written to pass information back to nzbget using it's 36 | processing engine. 37 | 38 | all exceptions are now automatically handled and logging can be easily 39 | changed from stdout, to stderr or to a file. 40 | 41 | Test suite built in (using python-nose) to ensure old global variables 42 | will still work as well as make them easier to access and manipulate. 43 | 44 | Some inline documentation was based on content provided at: 45 | - http://nzbget.net/Extension_scripts 46 | 47 | 48 | ############################################################################ 49 | Queue Script Usage/Example 50 | ############################################################################ 51 | 52 | ############################################################################ 53 | ### NZBGET QUEUE SCRIPT ### 54 | # 55 | # Describe your Queue Script here 56 | # Author: Chris Caron 57 | # 58 | 59 | ############################################################################ 60 | ### OPTIONS ### 61 | 62 | # 63 | # Enable NZBGet debug logging (yes, no) 64 | # Debug=no 65 | # 66 | 67 | ### NZBGET QUEUE SCRIPT ### 68 | ############################################################################ 69 | 70 | from nzbget import QueueScript 71 | 72 | # Now define your class while inheriting the rest 73 | class MyQueueScript(QueueScript): 74 | def main(self, *args, **kwargs): 75 | 76 | # Version Checking, Environment Variables Present, etc 77 | if not self.validate(): 78 | # No need to document a failure, validate will do that 79 | # on the reason it failed anyway 80 | return False 81 | 82 | # write all of your code here you would have otherwise put in the 83 | # script 84 | 85 | # All system environment variables (NZBOP_.*) as well as Post 86 | # Process script specific content (NZBNP_.*) 87 | # following dictionary (without the NZBOP_ or NZBNP_ prefix): 88 | print('TEMPDIR (directory is: %s' % self.get('TEMPDIR')) 89 | print('DIRECTORY %s' self.get('DIRECTORY')) 90 | print('FILENAME %s' self.get('FILENAME')) 91 | print('NZBNAME %s' self.get('NZBNAME')) 92 | print('CATEGORY %s' self.get('CATEGORY')) 93 | print('PRIORITY %s' self.get('PRIORITY')) 94 | print('TOP %s' self.get('TOP')) 95 | print('PAUSED %s' self.get('PAUSED')) 96 | 97 | # Set any variable you want by any key. Note that if you use 98 | # keys that were defined by the system (such as CATEGORY, DIRECTORY, 99 | # etc, you may have some undesirable results. Try to avoid reusing 100 | # system variables already defined (identified above): 101 | self.set('MY_KEY', 'MY_VALUE') 102 | 103 | # You can fetch it back; this will also set an entry in the 104 | # sqlite database for each hash references that can be pulled from 105 | # another script that simply calls self.get('MY_KEY') 106 | print(self.get('MY_KEY')) # prints MY_VALUE 107 | 108 | # You can also use push() which is similar to set() 109 | # except that it interacts with the NZBGet Server and does not use 110 | # the sqlite database. This can only be reached across other 111 | # scripts if the calling application is NZBGet itself 112 | self.push('ANOTHER_KEY', 'ANOTHER_VALUE') 113 | 114 | # You can still however locally retrieve what you set using push() 115 | # with the get() function 116 | print(self.get('ANOTHER_KEY')) # prints ANOTHER_VALUE 117 | 118 | # Your script configuration files (NZBNP_.*) are here in this 119 | # dictionary (again without the NZBNP_ prefix): 120 | # assume you defined `Debug=no` in the first 10K of your QueueScript 121 | # NZBGet translates this to `NZBNP_DEBUG` which can be retrieved 122 | # as follows: 123 | print('DEBUG %s' self.get('DEBUG')) 124 | 125 | # Returns have been made easy. Just return: 126 | # * True if everything was successful 127 | # * False if there was a problem 128 | # * None if you want to report that you've just gracefully 129 | skipped processing (this is better then False) 130 | in some circumstances. This is neither a failure or a 131 | success status. 132 | 133 | # Feel free to use the actual exit codes as well defined by 134 | # NZBGet on their website. They have also been defined here 135 | # from nzbget.ScriptBase import EXIT_CODE 136 | 137 | return True 138 | 139 | # Call your script as follows: 140 | if __name__ == "__main__": 141 | from sys import exit 142 | 143 | # Create an instance of your Script 144 | myscript = MyQueueScript() 145 | 146 | # call run() and exit() using it's returned value 147 | exit(myscript.run()) 148 | 149 | """ 150 | import re 151 | from os import chdir 152 | from os import environ 153 | from os.path import isdir 154 | from os.path import basename 155 | from os.path import abspath 156 | 157 | # Relative Includes 158 | from .ScriptBase import ScriptBase 159 | from .ScriptBase import SCRIPT_MODE 160 | from .ScriptBase import PRIORITY 161 | from .ScriptBase import PRIORITIES 162 | from .ScriptBase import NZBGET_BOOL_FALSE 163 | from .PostProcessScript import POSTPROC_ENVIRO_ID 164 | 165 | # Environment variable that prefixes all NZBGET options being passed into 166 | # scripts with respect to the NZB-File (used in Queue Scripts) 167 | QUEUE_ENVIRO_ID = 'NZBNA_' 168 | 169 | class Mark(object): 170 | # A file can be marked bad 171 | BAD = 'BAD' 172 | GOOD = 'GOOD' 173 | 174 | class QueueEvent(object): 175 | # a list of all event types 176 | UNKNOWN = 'UNKNOWN' 177 | # An NZB file added to the queue 178 | NZB_ADDED = 'NZB_ADDED' 179 | # An NZB file deleted 180 | NZB_DELETED = 'NZB_DELETED' 181 | # A file Downloaded 182 | FILE_DOWNLOADED = 'FILE_DOWNLOADED' 183 | # An NZB file was Downloaded 184 | NZB_DOWNLOADED = 'NZB_DOWNLOADED' 185 | 186 | QUEUE_EVENTS = ( 187 | QueueEvent.UNKNOWN, 188 | QueueEvent.NZB_ADDED, 189 | QueueEvent.FILE_DOWNLOADED, 190 | QueueEvent.NZB_DOWNLOADED, 191 | ) 192 | 193 | # Precompile Regulare Expression for Speed 194 | QUEUE_OPTS_RE = re.compile('^%s([A-Z0-9_]+)$' % QUEUE_ENVIRO_ID) 195 | 196 | class QueueScript(ScriptBase): 197 | """QUEUE mode is called before the unpack stage 198 | """ 199 | def __init__(self, *args, **kwargs): 200 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 201 | # Multi-Script Support 202 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 203 | if not hasattr(self, 'script_dict'): 204 | # Only define once 205 | self.script_dict = {} 206 | self.script_dict[SCRIPT_MODE.QUEUE] = self 207 | 208 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 209 | # Initialize Parent 210 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 211 | super(QueueScript, self).__init__(*args, **kwargs) 212 | 213 | def queue_init(self, *args, **kwargs): 214 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 215 | # Fetch Script Specific Arguments 216 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 217 | directory = kwargs.get('directory') 218 | nzbname = kwargs.get('nzbname') 219 | filename = kwargs.get('filename') 220 | category = kwargs.get('category') 221 | priority = kwargs.get('priority') 222 | top = kwargs.get('top') 223 | paused = kwargs.get('paused') 224 | parse_nzbfile = kwargs.get('parse_nzbfile') 225 | use_database = kwargs.get('use_database') 226 | event = kwargs.get('event') 227 | 228 | # Fetch/Load Queue Script Configuration 229 | script_config = dict([(QUEUE_OPTS_RE.match(k).group(1), v.strip()) \ 230 | for (k, v) in environ.items() if QUEUE_OPTS_RE.match(k)]) 231 | 232 | if self.vvdebug: 233 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 234 | # Print Global Script Varables to help debugging process 235 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 236 | for k, v in script_config.items(): 237 | self.logger.vvdebug('%s%s=%s' % (QUEUE_ENVIRO_ID, k, v)) 238 | 239 | # Merge Script Configuration With System Config 240 | script_config.update(self.system) 241 | self.system = script_config 242 | 243 | # self.directory 244 | # This is the path to the destination directory for downloaded files. 245 | if directory is None: 246 | self.directory = environ.get( 247 | '%sDIRECTORY' % QUEUE_ENVIRO_ID, 248 | ) 249 | else: 250 | self.directory = directory 251 | 252 | # self.nzbname 253 | # User-friendly name of processed nzb-file as it is displayed by the 254 | # program. The file path and extension are removed. If download was 255 | # renamed, this parameter reflects the new name. 256 | if nzbname is None: 257 | self.nzbname = environ.get( 258 | '%sNZBNAME' % QUEUE_ENVIRO_ID, 259 | ) 260 | else: 261 | self.nzbname = nzbname 262 | 263 | # self.filename 264 | # Name of file to be processed 265 | if filename is None: 266 | self.filename = environ.get( 267 | '%sFILENAME' % QUEUE_ENVIRO_ID, 268 | ) 269 | else: 270 | self.filename = filename 271 | 272 | # self.category 273 | # Category assigned to nzb-file (can be empty string). 274 | if category is None: 275 | self.category = environ.get( 276 | '%sCATEGORY' % QUEUE_ENVIRO_ID, 277 | ) 278 | else: 279 | self.category = category 280 | 281 | # self.priority 282 | # The priority of the nzb file being scanned 283 | if priority is None: 284 | self.priority = environ.get( 285 | '%sPRIORITY' % QUEUE_ENVIRO_ID, 286 | ) 287 | else: 288 | self.priority = priority 289 | 290 | # self.top 291 | # Flag indicating that the file will be added to the top of queue 292 | if top is None: 293 | self.top = environ.get( 294 | '%sTOP' % QUEUE_ENVIRO_ID, 295 | ) 296 | else: 297 | self.top = top 298 | 299 | # self.paused 300 | # Flag indicating that the file will be added as paused 301 | if paused is None: 302 | self.paused = environ.get( 303 | '%sPAUSED' % QUEUE_ENVIRO_ID, 304 | ) 305 | else: 306 | self.paused = paused 307 | 308 | # self.event 309 | # Type of Queue Event 310 | if event is None: 311 | self.event = environ.get( 312 | '%sEVENT' % QUEUE_ENVIRO_ID, 313 | ) 314 | else: 315 | self.event = event 316 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 317 | # Error Handling 318 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 319 | if self.filename: 320 | # absolute path names 321 | self.filename = abspath(self.filename) 322 | 323 | if parse_nzbfile: 324 | # Initialize information fetched from NZB-File 325 | # We intentionally allow existing nzbheaders to over-ride 326 | # any found in the nzbfile 327 | self.nzbheaders = dict( 328 | self.filename( 329 | self.nzbfilename, check_queued=True)\ 330 | .items() + self.pull_dnzb().items(), 331 | ) 332 | 333 | if self.directory: 334 | # absolute path names 335 | self.directory = abspath(self.directory) 336 | 337 | if not (self.directory and isdir(self.directory)): 338 | self.logger.debug('Process directory is missing: %s' % \ 339 | self.directory) 340 | else: 341 | try: 342 | chdir(self.directory) 343 | except OSError: 344 | self.logger.debug('Process directory is not accessible: %s' % \ 345 | self.directory) 346 | 347 | # Priority 348 | if not isinstance(self.priority, int): 349 | try: 350 | self.priority = int(self.priority) 351 | except: 352 | self.priority = PRIORITY.NORMAL 353 | 354 | if self.priority not in PRIORITIES: 355 | self.priority = PRIORITY.NORMAL 356 | 357 | # Top 358 | try: 359 | self.top = bool(int(self.top)) 360 | except: 361 | self.top = False 362 | 363 | # Paused 364 | try: 365 | self.paused = bool(int(self.paused)) 366 | except: 367 | self.paused = False 368 | 369 | # Event 370 | if self.event not in QUEUE_EVENTS: 371 | self.event = QueueEvent.UNKNOWN 372 | 373 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 374 | # Enforce system/global variables for script processing 375 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 376 | self.system['DIRECTORY'] = self.directory 377 | if self.directory is not None: 378 | environ['%sDIRECTORY' % QUEUE_ENVIRO_ID] = self.directory 379 | 380 | self.system['NZBNAME'] = self.nzbname 381 | if self.nzbname is not None: 382 | environ['%sNZBNAME' % QUEUE_ENVIRO_ID] = self.nzbname 383 | 384 | self.system['FILENAME'] = self.filename 385 | if self.filename is not None: 386 | environ['%sFILENAME' % QUEUE_ENVIRO_ID] = self.filename 387 | 388 | self.system['CATEGORY'] = self.category 389 | if self.category is not None: 390 | environ['%sCATEGORY' % QUEUE_ENVIRO_ID] = self.category 391 | 392 | self.system['PRIORITY'] = self.priority 393 | if self.priority is not None: 394 | environ['%sPRIORITY' % QUEUE_ENVIRO_ID] = str(self.priority) 395 | 396 | self.system['TOP'] = self.top 397 | if self.top is not None: 398 | environ['%sTOP' % QUEUE_ENVIRO_ID] = str(int(self.top)) 399 | 400 | self.system['EVENT'] = self.event 401 | if self.event is not None: 402 | environ['%sEVENT' % QUEUE_ENVIRO_ID] = str(self.event).upper() 403 | 404 | self.system['PAUSED'] = self.paused 405 | if self.paused is not None: 406 | environ['%sPAUSED' % QUEUE_ENVIRO_ID] = str(int(self.paused)) 407 | 408 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 409 | # Create Database for set() and get() operations 410 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 411 | if use_database: 412 | # database_key is inherited in the parent class 413 | # future calls of set() and get() will allow access 414 | # to the database now 415 | try: 416 | self.database_key = basename(self.filename) 417 | self.logger.info('Connected to SQLite Database') 418 | except AttributeError: 419 | pass 420 | 421 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 422 | # Debug Flag Check 423 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 424 | def queue_debug(self, *args, **kargs): 425 | """Uses the environment variables to detect if debug mode is set 426 | """ 427 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 428 | # Debug Handling 429 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 430 | return self.parse_bool( 431 | environ.get('%sDEBUG' % QUEUE_ENVIRO_ID, NZBGET_BOOL_FALSE), 432 | ) 433 | 434 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 435 | # Sanity 436 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 437 | def queue_sanity_check(self, *args, **kargs): 438 | """Sanity checking to ensure this really is a post_process script 439 | """ 440 | return ('%sDIRECTORY' % POSTPROC_ENVIRO_ID not in environ) and \ 441 | ('%sDIRECTORY' % QUEUE_ENVIRO_ID in environ) 442 | 443 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 444 | # Validatation 445 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 446 | def queue_validate(self, keys=None, min_version=11, *args, **kargs): 447 | """validate against environment variables 448 | """ 449 | is_okay = super(QueueScript, self)._validate( 450 | keys=keys, 451 | min_version=min_version, 452 | ) 453 | 454 | if min_version >= 14: 455 | required_opts = set(( 456 | 'ARTICLECACHE', 457 | )) 458 | found_opts = set(self.system) & required_opts 459 | if found_opts != required_opts: 460 | missing_opts = list(required_opts ^ found_opts) 461 | self.logger.error( 462 | 'Validation - (v14) Directives not set: %s' % \ 463 | missing_opts.join(', ') 464 | ) 465 | is_okay = False 466 | 467 | return is_okay 468 | 469 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 470 | # File Retrieval 471 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 472 | def queue_get_files(self, search_dir=None, regex_filter=None, 473 | prefix_filter=None, suffix_filter=None, 474 | fullstats=False, *args, **kargs): 475 | """a wrapper to the get_files() function defined in the inherited class 476 | the only difference is the search_dir automatically uses the 477 | defined download `directory` as a default (if not specified). 478 | """ 479 | if search_dir is None: 480 | search_dir = self.directory 481 | 482 | return super(QueueScript, self)._get_files( 483 | search_dir=search_dir, 484 | regex_filter=regex_filter, 485 | prefix_filter=prefix_filter, 486 | suffix_filter=suffix_filter, 487 | fullstats=fullstats, 488 | ) 489 | 490 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 491 | # Set/Control Functions (also passes data back to NZBGet) 492 | # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 493 | def push_nzbname(self, nzbname=None): 494 | if nzbname: 495 | # Update local nzbname 496 | self.nzbname = nzbname 497 | 498 | # Accomodate other environmental variables 499 | self.system['NZBNAME'] = self.nzbname 500 | environ['%sNZBNAME' % QUEUE_ENVIRO_ID] = self.nzbname 501 | 502 | # Alert NZBGet of Change 503 | return self._push( 504 | key='NZBNAME', 505 | value=self.nzbname, 506 | ) 507 | 508 | def push_category(self, category=None): 509 | if category: 510 | # Update local category 511 | self.category = category 512 | 513 | # Accomodate other environmental variables 514 | self.system['CATEGORY'] = self.category 515 | environ['%sCATEGORY' % QUEUE_ENVIRO_ID] = self.category 516 | 517 | # Alert NZBGet of Change 518 | return self._push( 519 | key='CATEGORY', 520 | value=self.category, 521 | ) 522 | 523 | def push_priority(self, priority=None): 524 | 525 | if priority is not None: 526 | if priority not in PRIORITIES: 527 | return False 528 | 529 | # Update local priority 530 | self.priority = priority 531 | 532 | # Accomodate other environmental variables 533 | self.system['PRIORITY'] = self.priority 534 | environ['%sPRIORITY' % QUEUE_ENVIRO_ID] = str(self.priority) 535 | 536 | # Alert NZBGet of Change 537 | return self._push( 538 | key='PRIORITY', 539 | value=self.priority, 540 | ) 541 | 542 | def push_top(self, top=None): 543 | 544 | if top is not None: 545 | # Update local priority 546 | try: 547 | self.top = bool(int(top)) 548 | except: 549 | return False 550 | 551 | # Accomodate other environmental variables 552 | self.system['TOP'] = self.top 553 | environ['%sTOP' % QUEUE_ENVIRO_ID] = str(int(self.top)) 554 | 555 | # Alert NZBGet of Change 556 | return self._push( 557 | key='TOP', 558 | # Convert bool to int for response 559 | value=int(self.top), 560 | ) 561 | 562 | def push_paused(self, paused=None): 563 | 564 | if paused is not None: 565 | # Update local priority 566 | try: 567 | self.paused = bool(int(paused)) 568 | except: 569 | return False 570 | 571 | # Accomodate other environmental variables 572 | self.system['PAUSED'] = self.paused 573 | environ['%sPAUSED' % QUEUE_ENVIRO_ID] = str(int(self.paused)) 574 | 575 | # Alert NZBGet of Change 576 | return self._push( 577 | key='PAUSED', 578 | # Convert bool to int for response 579 | value=int(self.paused), 580 | ) 581 | 582 | def push_mark(self, mark=Mark.BAD): 583 | """Mark a file status 584 | """ 585 | # You can mark a file as bad 586 | return self._push( 587 | key='MARK', 588 | value=mark.upper(), 589 | ) 590 | --------------------------------------------------------------------------------