Merge branch 'dev'

This commit is contained in:
clinton-hall 2019-12-08 14:44:16 +13:00
commit 2e7d4a5863
18 changed files with 552 additions and 226 deletions

View file

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 12.1.01 current_version = 12.1.02
commit = True commit = True
tag = False tag = False

View file

@ -79,7 +79,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
if section is None: #Check for user_scripts for 'ALL' and 'UNCAT' if section is None: #Check for user_scripts for 'ALL' and 'UNCAT'
if usercat in core.CATEGORIES: if usercat in core.CATEGORIES:
section = core.CFG.findsection('ALL').isenabled() section = core.CFG.findsection('ALL').isenabled()
usercat = 'ALL' usercat = 'ALL'
else: else:
section = core.CFG.findsection('UNCAT').isenabled() section = core.CFG.findsection('UNCAT').isenabled()
usercat = 'UNCAT' usercat = 'UNCAT'
@ -213,7 +213,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
core.flatten(output_destination) core.flatten(output_destination)
# Now check if video files exist in destination: # Now check if video files exist in destination:
if section_name in ['SickBeard', 'NzbDrone', 'Sonarr', 'CouchPotato', 'Radarr']: if section_name in ['SickBeard', 'NzbDrone', 'Sonarr', 'CouchPotato', 'Radarr', 'Watcher3']:
num_videos = len( num_videos = len(
core.list_media_files(output_destination, media=True, audio=False, meta=False, archives=False)) core.list_media_files(output_destination, media=True, audio=False, meta=False, archives=False))
if num_videos > 0: if num_videos > 0:
@ -227,7 +227,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
# Only these sections can handling failed downloads # Only these sections can handling failed downloads
# so make sure everything else gets through without the check for failed # so make sure everything else gets through without the check for failed
if section_name not in ['CouchPotato', 'Radarr', 'SickBeard', 'NzbDrone', 'Sonarr']: if section_name not in ['CouchPotato', 'Radarr', 'SickBeard', 'NzbDrone', 'Sonarr', 'Watcher3']:
status = 0 status = 0
logger.info('Calling {0}:{1} to post-process:{2}'.format(section_name, usercat, input_name)) logger.info('Calling {0}:{1} to post-process:{2}'.format(section_name, usercat, input_name))
@ -241,7 +241,7 @@ def process_torrent(input_directory, input_name, input_category, input_hash, inp
) )
if section_name == 'UserScript': if section_name == 'UserScript':
result = external_script(output_destination, input_name, input_category, section) result = external_script(output_destination, input_name, input_category, section)
elif section_name in ['CouchPotato', 'Radarr']: elif section_name in ['CouchPotato', 'Radarr', 'Watcher3']:
result = movies.process(section_name, output_destination, input_name, status, client_agent, input_hash, input_category) result = movies.process(section_name, output_destination, input_name, status, client_agent, input_hash, input_category)
elif section_name in ['SickBeard', 'NzbDrone', 'Sonarr']: elif section_name in ['SickBeard', 'NzbDrone', 'Sonarr']:
if input_hash: if input_hash:

View file

@ -12,7 +12,7 @@
git_user = git_user =
# GitHUB branch for repo # GitHUB branch for repo
git_branch = git_branch =
# Enable/Disable forceful cleaning of leftover files following postprocess # Enable/Disable forceful cleaning of leftover files following postprocess
force_clean = 0 force_clean = 0
# Enable/Disable logging debug messages to nzbtomedia.log # Enable/Disable logging debug messages to nzbtomedia.log
log_debug = 0 log_debug = 0
@ -36,7 +36,7 @@
[Posix] [Posix]
### Process priority setting for External commands (Extractor and Transcoder) on Posix (Unix/Linux/OSX) systems. ### Process priority setting for External commands (Extractor and Transcoder) on Posix (Unix/Linux/OSX) systems.
# Set the Niceness value for the nice command. These range from -20 (most favorable to the process) to 19 (least favorable to the process). # Set the Niceness value for the nice command. These range from -20 (most favorable to the process) to 19 (least favorable to the process).
# If entering an integer e.g 'niceness = 4', this is added to the nice command and passed as 'nice -n4' (Default). # If entering an integer e.g 'niceness = 4', this is added to the nice command and passed as 'nice -n4' (Default).
# If entering a comma separated list e.g. 'niceness = nice,4' this will be passed as 'nice 4' (Safer). # If entering a comma separated list e.g. 'niceness = nice,4' this will be passed as 'nice 4' (Safer).
niceness = nice,-n0 niceness = nice,-n0
# Set the ionice scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle. # Set the ionice scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
@ -111,6 +111,36 @@
##### Set to define import behavior Move or Copy ##### Set to define import behavior Move or Copy
importMode = Copy importMode = Copy
[Watcher3]
#### autoProcessing for Movies
#### movie - category that gets called for post-processing with CPS
[[movie]]
enabled = 0
apikey =
host = localhost
port = 9090
###### ADVANCED USE - ONLY EDIT IF YOU KNOW WHAT YOU'RE DOING ######
ssl = 0
web_root =
# api key for www.omdbapi.com (used as alternative to imdb)
omdbapikey =
# Enable/Disable linking for Torrents
Torrent_NoLink = 0
keep_archive = 1
delete_failed = 0
wait_for = 0
extract = 1
# Set this to minimum required size to consider a media file valid (in MB)
minSize = 0
# Enable/Disable deleting ignored files (samples and invalid media files)
delete_ignored = 0
##### Enable if Watcher3 is on a remote server for this category
remote_path = 0
##### Set to path where download client places completed downloads locally for this category
watch_dir =
##### Set the recursive directory permissions to the following (0 to disable)
chmodDirectory = 0
[SickBeard] [SickBeard]
#### autoProcessing for TV Series #### autoProcessing for TV Series
#### tv - category that gets called for post-processing with SB #### tv - category that gets called for post-processing with SB
@ -266,7 +296,7 @@
apikey = apikey =
host = localhost host = localhost
port = 8085 port = 8085
###### ######
library = Set to path where you want the processed games to be moved to. library = Set to path where you want the processed games to be moved to.
###### ADVANCED USE - ONLY EDIT IF YOU KNOW WHAT YOU'RE DOING ###### ###### ADVANCED USE - ONLY EDIT IF YOU KNOW WHAT YOU'RE DOING ######
ssl = 0 ssl = 0
@ -312,7 +342,7 @@
[Network] [Network]
# Enter Mount points as LocalPath,RemotePath and separate each pair with '|' # Enter Mount points as LocalPath,RemotePath and separate each pair with '|'
# e.g. MountPoints = /volume1/Public/,E:\|/volume2/share/,\\NAS\ # e.g. MountPoints = /volume1/Public/,E:\|/volume2/share/,\\NAS\
mount_points = mount_points =
[Nzb] [Nzb]
###### clientAgent - Supported clients: sabnzbd, nzbget ###### clientAgent - Supported clients: sabnzbd, nzbget
@ -331,7 +361,7 @@
useLink = hard useLink = hard
###### outputDirectory - Default output directory (categories will be appended as sub directory to outputDirectory) ###### outputDirectory - Default output directory (categories will be appended as sub directory to outputDirectory)
outputDirectory = /abs/path/to/complete/ outputDirectory = /abs/path/to/complete/
###### Enter the default path to your default download directory (non-category downloads). this directory is protected by safe_mode. ###### Enter the default path to your default download directory (non-category downloads). this directory is protected by safe_mode.
default_downloadDirectory = default_downloadDirectory =
###### Other categories/labels defined for your downloader. Does not include CouchPotato, SickBeard, HeadPhones, Mylar categories. ###### Other categories/labels defined for your downloader. Does not include CouchPotato, SickBeard, HeadPhones, Mylar categories.
categories = music_videos,pictures,software,manual categories = music_videos,pictures,software,manual
@ -374,15 +404,15 @@
plex_host = localhost plex_host = localhost
plex_port = 32400 plex_port = 32400
plex_token = plex_token =
plex_ssl = 0 plex_ssl = 0
# Enter Plex category to section mapping as Category,section and separate each pair with '|' # Enter Plex category to section mapping as Category,section and separate each pair with '|'
# e.g. plex_sections = movie,3|tv,4 # e.g. plex_sections = movie,3|tv,4
plex_sections = plex_sections =
[Transcoder] [Transcoder]
# getsubs. enable to download subtitles. # getsubs. enable to download subtitles.
getSubs = 0 getSubs = 0
# subLanguages. create a list of languages in the order you want them in your subtitles. # subLanguages. create a list of languages in the order you want them in your subtitles.
subLanguages = eng,spa,fra subLanguages = eng,spa,fra
# transcode. enable to use transcoder # transcode. enable to use transcoder
transcode = 0 transcode = 0
@ -397,7 +427,7 @@
# outputQualityPercent. used as -q:a value. 0 will disable this from being used. # outputQualityPercent. used as -q:a value. 0 will disable this from being used.
outputQualityPercent = 0 outputQualityPercent = 0
# outputVideoPath. Set path you want transcoded videos moved to. Leave blank to disable. # outputVideoPath. Set path you want transcoded videos moved to. Leave blank to disable.
outputVideoPath = outputVideoPath =
# processOutput. 1 will send the outputVideoPath to SickBeard/CouchPotato. 0 will send original files. # processOutput. 1 will send the outputVideoPath to SickBeard/CouchPotato. 0 will send original files.
processOutput = 0 processOutput = 0
# audioLanguage. set the 3 letter language code you want as your primary audio track. # audioLanguage. set the 3 letter language code you want as your primary audio track.
@ -427,7 +457,7 @@
#### Define custom settings below. #### Define custom settings below.
outputVideoExtension = .mp4 outputVideoExtension = .mp4
outputVideoCodec = libx264 outputVideoCodec = libx264
VideoCodecAllow = VideoCodecAllow =
outputVideoPreset = medium outputVideoPreset = medium
outputVideoResolution = 1920:1080 outputVideoResolution = 1920:1080
outputVideoFramerate = 24 outputVideoFramerate = 24
@ -435,15 +465,15 @@
outputVideoCRF = 19 outputVideoCRF = 19
outputVideoLevel = 3.1 outputVideoLevel = 3.1
outputAudioCodec = ac3 outputAudioCodec = ac3
AudioCodecAllow = AudioCodecAllow =
outputAudioChannels = 6 outputAudioChannels = 6
outputAudioBitrate = 640k outputAudioBitrate = 640k
outputAudioTrack2Codec = libfaac outputAudioTrack2Codec = libfaac
AudioCodec2Allow = AudioCodec2Allow =
outputAudioTrack2Channels = 2 outputAudioTrack2Channels = 2
outputAudioTrack2Bitrate = 128000 outputAudioTrack2Bitrate = 128000
outputAudioOtherCodec = libmp3lame outputAudioOtherCodec = libmp3lame
AudioOtherCodecAllow = AudioOtherCodecAllow =
outputAudioOtherChannels = outputAudioOtherChannels =
outputAudioOtherBitrate = 128000 outputAudioOtherBitrate = 128000
outputSubtitleCodec = outputSubtitleCodec =
@ -500,4 +530,4 @@
# enter a list (comma separated) of Group Tags you want removed from filenames to help with subtitle matching. # enter a list (comma separated) of Group Tags you want removed from filenames to help with subtitle matching.
# e.g remove_group = [rarbag],-NZBgeek # e.g remove_group = [rarbag],-NZBgeek
# be careful if your "group" is a common "real" word. Please report if you have any group replacements that would fall in this category. # be careful if your "group" is a common "real" word. Please report if you have any group replacements that would fall in this category.
remove_group = remove_group =

View file

@ -21,45 +21,14 @@ jobs:
python.version: '3.6' python.version: '3.6'
Python37: Python37:
python.version: '3.7' python.version: '3.7'
maxParallel: 4 Python38:
python.version: '3.8'
maxParallel: 5
steps: steps:
#- script: | - script: sudo apt-get install ffmpeg
# Make sure all packages are pulled from latest displayName: 'Install ffmpeg'
#sudo apt-get update
# Fail out if any setups fail
#set -e
# Delete old Pythons
#rm -rf $AGENT_TOOLSDIRECTORY/Python/2.7.16
#rm -rf $AGENT_TOOLSDIRECTORY/Python/3.5.7
#rm -rf $AGENT_TOOLSDIRECTORY/Python/3.7.3
# Download new Pythons
#azcopy --recursive \
#--source https://vstsagenttools.blob.core.windows.net/tools/hostedtoolcache/linux/Python/2.7.15 \
#--destination $AGENT_TOOLSDIRECTORY/Python/2.7.15
#azcopy --recursive \
#--source https://vstsagenttools.blob.core.windows.net/tools/hostedtoolcache/linux/Python/3.5.5 \
#--destination $AGENT_TOOLSDIRECTORY/Python/3.5.5
#azcopy --recursive \
#--source https://vstsagenttools.blob.core.windows.net/tools/hostedtoolcache/linux/Python/3.7.2 \
#--destination $AGENT_TOOLSDIRECTORY/Python/3.7.2
# Install new Pythons
#original_directory=$PWD
#setups=$(find $AGENT_TOOLSDIRECTORY/Python -name setup.sh)
#for setup in $setups; do
#chmod +x $setup;
#cd $(dirname $setup);
#./$(basename $setup);
#cd $original_directory;
#done;
#displayName: 'Workaround: update apt and roll back Python versions'
- task: UsePythonVersion@0 - task: UsePythonVersion@0
inputs: inputs:
versionSpec: '$(python.version)' versionSpec: '$(python.version)'
@ -68,9 +37,6 @@ jobs:
- script: python -m pip install --upgrade pip - script: python -m pip install --upgrade pip
displayName: 'Install dependencies' displayName: 'Install dependencies'
- script: sudo apt-get install ffmpeg
displayName: 'Install ffmpeg'
- script: | - script: |
pip install pytest pip install pytest
pytest tests --doctest-modules --junitxml=junit/test-results.xml pytest tests --doctest-modules --junitxml=junit/test-results.xml

View file

@ -12,6 +12,8 @@ import subprocess
import sys import sys
import shutil import shutil
sys.dont_write_bytecode = True
FOLDER_STRUCTURE = { FOLDER_STRUCTURE = {
'libs': [ 'libs': [
'common', 'common',

View file

@ -83,7 +83,7 @@ from core.utils import (
wake_up, wake_up,
) )
__version__ = '12.1.01' __version__ = '12.1.02'
# Client Agents # Client Agents
NZB_CLIENTS = ['sabnzbd', 'nzbget', 'manual'] NZB_CLIENTS = ['sabnzbd', 'nzbget', 'manual']

View file

@ -72,6 +72,8 @@ def process(section, dir_name, input_name=None, status=0, client_agent='manual',
base_url = '{0}{1}:{2}{3}/api/command'.format(protocol, host, port, web_root) base_url = '{0}{1}:{2}{3}/api/command'.format(protocol, host, port, web_root)
url2 = '{0}{1}:{2}{3}/api/config/downloadClient'.format(protocol, host, port, web_root) url2 = '{0}{1}:{2}{3}/api/config/downloadClient'.format(protocol, host, port, web_root)
headers = {'X-Api-Key': apikey} headers = {'X-Api-Key': apikey}
if section == 'Watcher3':
base_url = '{0}{1}:{2}{3}/postprocessing'.format(protocol, host, port, web_root)
if not apikey: if not apikey:
logger.info('No CouchPotato or Radarr apikey entered. Performing transcoder functions only') logger.info('No CouchPotato or Radarr apikey entered. Performing transcoder functions only')
release = None release = None
@ -178,7 +180,7 @@ def process(section, dir_name, input_name=None, status=0, client_agent='manual',
os.rename(video, video2) os.rename(video, video2)
if not apikey: # If only using Transcoder functions, exit here. if not apikey: # If only using Transcoder functions, exit here.
logger.info('No CouchPotato or Radarr apikey entered. Processing completed.') logger.info('No CouchPotato or Radarr or Watcher3 apikey entered. Processing completed.')
return ProcessResult( return ProcessResult(
message='{0}: Successfully post-processed {1}'.format(section, input_name), message='{0}: Successfully post-processed {1}'.format(section, input_name),
status_code=0, status_code=0,
@ -210,9 +212,20 @@ def process(section, dir_name, input_name=None, status=0, client_agent='manual',
logger.debug('Opening URL: {0} with PARAMS: {1}'.format(base_url, payload), section) logger.debug('Opening URL: {0} with PARAMS: {1}'.format(base_url, payload), section)
logger.postprocess('Starting DownloadedMoviesScan scan for {0}'.format(input_name), section) logger.postprocess('Starting DownloadedMoviesScan scan for {0}'.format(input_name), section)
if section == 'Watcher3':
if input_name and os.path.isfile(os.path.join(dir_name, input_name)):
params['media_folder'] = os.path.join(params['media_folder'], input_name)
payload = {'apikey': apikey, 'path': params['media_folder'], 'guid': download_id, 'mode': 'complete'}
if not download_id:
payload.pop('guid')
logger.debug('Opening URL: {0} with PARAMS: {1}'.format(base_url, payload), section)
logger.postprocess('Starting postprocessing scan for {0}'.format(input_name), section)
try: try:
if section == 'CouchPotato': if section == 'CouchPotato':
r = requests.get(url, params=params, verify=False, timeout=(30, 1800)) r = requests.get(url, params=params, verify=False, timeout=(30, 1800))
elif section == 'Watcher3':
r = requests.post(base_url, data=payload, verify=False, timeout=(30, 1800))
else: else:
r = requests.post(base_url, data=json.dumps(payload), headers=headers, stream=True, verify=False, timeout=(30, 1800)) r = requests.post(base_url, data=json.dumps(payload), headers=headers, stream=True, verify=False, timeout=(30, 1800))
except requests.ConnectionError: except requests.ConnectionError:
@ -239,12 +252,23 @@ def process(section, dir_name, input_name=None, status=0, client_agent='manual',
elif section == 'Radarr': elif section == 'Radarr':
logger.postprocess('Radarr response: {0}'.format(result['state'])) logger.postprocess('Radarr response: {0}'.format(result['state']))
try: try:
res = json.loads(r.content) scan_id = int(result['id'])
scan_id = int(res['id'])
logger.debug('Scan started with id: {0}'.format(scan_id), section) logger.debug('Scan started with id: {0}'.format(scan_id), section)
except Exception as e: except Exception as e:
logger.warning('No scan id was returned due to: {0}'.format(e), section) logger.warning('No scan id was returned due to: {0}'.format(e), section)
scan_id = None scan_id = None
elif section == 'Watcher3' and result['status'] == 'finished':
logger.postprocess('Watcher3 updated status to {0}'.format(result['tasks']['update_movie_status']))
if result['tasks']['update_movie_status'] == 'Finished':
return ProcessResult(
message='{0}: Successfully post-processed {1}'.format(section, input_name),
status_code=status,
)
else:
return ProcessResult(
message='{0}: Failed to post-process - changed status to {1}'.format(section, result['tasks']['update_movie_status']),
status_code=1,
)
else: else:
logger.error('FAILED: {0} scan was unable to finish for folder {1}. exiting!'.format(method, dir_name), logger.error('FAILED: {0} scan was unable to finish for folder {1}. exiting!'.format(method, dir_name),
section) section)
@ -264,6 +288,20 @@ def process(section, dir_name, input_name=None, status=0, client_agent='manual',
message='{0}: Sending failed download back to {0}'.format(section), message='{0}: Sending failed download back to {0}'.format(section),
status_code=1, # Return as failed to flag this in the downloader. status_code=1, # Return as failed to flag this in the downloader.
) # Return failed flag, but log the event as successful. ) # Return failed flag, but log the event as successful.
elif section == 'Watcher3':
logger.postprocess('Sending failed download to {0} for CDH processing'.format(section), section)
path = remote_dir(dir_name) if remote_path else dir_name
if input_name and os.path.isfile(os.path.join(dir_name, input_name)):
path = os.path.join(path, input_name)
payload = {'apikey': apikey, 'path': path, 'guid': download_id, 'mode': 'failed'}
r = requests.post(base_url, data=payload, verify=False, timeout=(30, 1800))
result = r.json()
logger.postprocess('Watcher3 response: {0}'.format(result))
if result['status'] == 'finished':
return ProcessResult(
message='{0}: Sending failed download back to {0}'.format(section),
status_code=1, # Return as failed to flag this in the downloader.
) # Return failed flag, but log the event as successful.
if delete_failed and os.path.isdir(dir_name) and not os.path.dirname(dir_name) == dir_name: if delete_failed and os.path.isdir(dir_name) and not os.path.dirname(dir_name) == dir_name:
logger.postprocess('Deleting failed files and folder {0}'.format(dir_name), section) logger.postprocess('Deleting failed files and folder {0}'.format(dir_name), section)

View file

@ -150,7 +150,7 @@ class ConfigObj(configobj.ConfigObj, Section):
if CFG_OLD[section].sections: if CFG_OLD[section].sections:
subsections.update({section: CFG_OLD[section].sections}) subsections.update({section: CFG_OLD[section].sections})
for option, value in CFG_OLD[section].items(): for option, value in CFG_OLD[section].items():
if option in ['category', 'cpsCategory', 'sbCategory', 'hpCategory', 'mlCategory', 'gzCategory', 'raCategory', 'ndCategory']: if option in ['category', 'cpsCategory', 'sbCategory', 'hpCategory', 'mlCategory', 'gzCategory', 'raCategory', 'ndCategory', 'W3Category']:
if not isinstance(value, list): if not isinstance(value, list):
value = [value] value = [value]
@ -271,6 +271,16 @@ class ConfigObj(configobj.ConfigObj, Section):
logger.warning('{x} category is set for CouchPotato and Radarr. ' logger.warning('{x} category is set for CouchPotato and Radarr. '
'Please check your config in NZBGet'.format 'Please check your config in NZBGet'.format
(x=os.environ['NZBPO_RACATEGORY'])) (x=os.environ['NZBPO_RACATEGORY']))
if 'NZBPO_RACATEGORY' in os.environ and 'NZBPO_W3CATEGORY' in os.environ:
if os.environ['NZBPO_RACATEGORY'] == os.environ['NZBPO_W3CATEGORY']:
logger.warning('{x} category is set for Watcher3 and Radarr. '
'Please check your config in NZBGet'.format
(x=os.environ['NZBPO_RACATEGORY']))
if 'NZBPO_W3CATEGORY' in os.environ and 'NZBPO_CPSCATEGORY' in os.environ:
if os.environ['NZBPO_W3CATEGORY'] == os.environ['NZBPO_CPSCATEGORY']:
logger.warning('{x} category is set for CouchPotato and Watcher3. '
'Please check your config in NZBGet'.format
(x=os.environ['NZBPO_W3CATEGORY']))
if 'NZBPO_LICATEGORY' in os.environ and 'NZBPO_HPCATEGORY' in os.environ: if 'NZBPO_LICATEGORY' in os.environ and 'NZBPO_HPCATEGORY' in os.environ:
if os.environ['NZBPO_LICATEGORY'] == os.environ['NZBPO_HPCATEGORY']: if os.environ['NZBPO_LICATEGORY'] == os.environ['NZBPO_HPCATEGORY']:
logger.warning('{x} category is set for HeadPhones and Lidarr. ' logger.warning('{x} category is set for HeadPhones and Lidarr. '
@ -321,6 +331,29 @@ class ConfigObj(configobj.ConfigObj, Section):
cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1 cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1
if os.environ[env_cat_key] in cfg_new['Radarr'].sections: if os.environ[env_cat_key] in cfg_new['Radarr'].sections:
cfg_new['Radarr'][env_cat_key]['enabled'] = 0 cfg_new['Radarr'][env_cat_key]['enabled'] = 0
if os.environ[env_cat_key] in cfg_new['Watcher3'].sections:
cfg_new['Watcher3'][env_cat_key]['enabled'] = 0
section = 'Watcher3'
env_cat_key = 'NZBPO_W3CATEGORY'
env_keys = ['ENABLED', 'APIKEY', 'HOST', 'PORT', 'SSL', 'WEB_ROOT', 'METHOD', 'DELETE_FAILED', 'REMOTE_PATH',
'WAIT_FOR', 'WATCH_DIR', 'OMDBAPIKEY']
cfg_keys = ['enabled', 'apikey', 'host', 'port', 'ssl', 'web_root', 'method', 'delete_failed', 'remote_path',
'wait_for', 'watch_dir', 'omdbapikey']
if env_cat_key in os.environ:
for index in range(len(env_keys)):
key = 'NZBPO_W3{index}'.format(index=env_keys[index])
if key in os.environ:
option = cfg_keys[index]
value = os.environ[key]
if os.environ[env_cat_key] not in cfg_new[section].sections:
cfg_new[section][os.environ[env_cat_key]] = {}
cfg_new[section][os.environ[env_cat_key]][option] = value
cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1
if os.environ[env_cat_key] in cfg_new['Radarr'].sections:
cfg_new['Radarr'][env_cat_key]['enabled'] = 0
if os.environ[env_cat_key] in cfg_new['CouchPotato'].sections:
cfg_new['CouchPotato'][env_cat_key]['enabled'] = 0
section = 'SickBeard' section = 'SickBeard'
env_cat_key = 'NZBPO_SBCATEGORY' env_cat_key = 'NZBPO_SBCATEGORY'
@ -444,6 +477,8 @@ class ConfigObj(configobj.ConfigObj, Section):
cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1 cfg_new[section][os.environ[env_cat_key]]['enabled'] = 1
if os.environ[env_cat_key] in cfg_new['CouchPotato'].sections: if os.environ[env_cat_key] in cfg_new['CouchPotato'].sections:
cfg_new['CouchPotato'][env_cat_key]['enabled'] = 0 cfg_new['CouchPotato'][env_cat_key]['enabled'] = 0
if os.environ[env_cat_key] in cfg_new['Wacther3'].sections:
cfg_new['Watcher3'][env_cat_key]['enabled'] = 0
section = 'Lidarr' section = 'Lidarr'
env_cat_key = 'NZBPO_LICATEGORY' env_cat_key = 'NZBPO_LICATEGORY'

View file

@ -168,7 +168,8 @@ def auto_fork(section, input_category):
else: else:
logger.info('{section}:{category} fork auto-detection failed'.format logger.info('{section}:{category} fork auto-detection failed'.format
(section=section, category=input_category)) (section=section, category=input_category))
fork = core.FORKS.items()[core.FORKS.keys().index(core.FORK_DEFAULT)] fork = list(core.FORKS.items())[list(core.FORKS.keys()).index(core.FORK_DEFAULT)]
logger.info('{section}:{category} fork set to {fork}'.format logger.info('{section}:{category} fork set to {fork}'.format
(section=section, category=input_category, fork=fork[0])) (section=section, category=input_category, fork=fork[0]))

View file

@ -11,6 +11,10 @@ import subliminal
import core import core
from core import logger from core import logger
for provider in subliminal.provider_manager.internal_extensions:
if provider not in [str(x) for x in subliminal.provider_manager.list_entry_points()]:
subliminal.provider_manager.register(str(provider))
def import_subs(filename): def import_subs(filename):
if not core.GETSUBS: if not core.GETSUBS:

View file

@ -53,10 +53,11 @@ def move_file(mediafile, path, link):
title = os.path.splitext(os.path.basename(mediafile))[0] title = os.path.splitext(os.path.basename(mediafile))[0]
new_path = os.path.join(path, sanitize_name(title)) new_path = os.path.join(path, sanitize_name(title))
try: # Removed as encoding of directory no-longer required
new_path = new_path.encode(core.SYS_ENCODING) #try:
except Exception: # new_path = new_path.encode(core.SYS_ENCODING)
pass #except Exception:
# pass
# Just fail-safe incase we already have afile with this clean-name (was actually a bug from earlier code, but let's be safe). # Just fail-safe incase we already have afile with this clean-name (was actually a bug from earlier code, but let's be safe).
if os.path.isfile(new_path): if os.path.isfile(new_path):

View file

@ -53,7 +53,7 @@ class CheckVersion(object):
'source': running from source without git 'source': running from source without git
""" """
# check if we're a windows build # check if we're a windows build
if os.path.isdir(os.path.join(core.APP_ROOT, u'.git')): if os.path.exists(os.path.join(core.APP_ROOT, u'.git')):
install_type = 'git' install_type = 'git'
else: else:
install_type = 'source' install_type = 'source'

1
eol.py
View file

@ -28,6 +28,7 @@ def date(string, fmt='%Y-%m-%d'):
# https://devguide.python.org/ # https://devguide.python.org/
# https://devguide.python.org/devcycle/#devcycle # https://devguide.python.org/devcycle/#devcycle
PYTHON_EOL = { PYTHON_EOL = {
(3, 8): date('2024-10-14'),
(3, 7): date('2023-06-27'), (3, 7): date('2023-06-27'),
(3, 6): date('2021-12-23'), (3, 6): date('2021-12-23'),
(3, 5): date('2020-09-13'), (3, 5): date('2020-09-13'),

View file

@ -1,7 +1,6 @@
import requests import requests
import json import json
class LoginRequired(Exception): class LoginRequired(Exception):
def __str__(self): def __str__(self):
return 'Please login first.' return 'Please login first.'
@ -15,7 +14,7 @@ class Client(object):
self.url = url self.url = url
session = requests.Session() session = requests.Session()
check_prefs = session.get(url+'query/preferences') check_prefs = session.get(url+'api/v2/app/preferences')
if check_prefs.status_code == 200: if check_prefs.status_code == 200:
self._is_authenticated = True self._is_authenticated = True
@ -24,9 +23,9 @@ class Client(object):
elif check_prefs.status_code == 404: elif check_prefs.status_code == 404:
self._is_authenticated = False self._is_authenticated = False
raise RuntimeError(""" raise RuntimeError("""
This wrapper only supports qBittorrent applications This wrapper only supports qBittorrent applications with
with version higher than 3.1.x. version higher than 4.1.0 (which implemented Web API v2.0).
Please use the latest qBittorrent release. Please use the latest qBittorrent release.
""") """)
else: else:
@ -35,10 +34,8 @@ class Client(object):
def _get(self, endpoint, **kwargs): def _get(self, endpoint, **kwargs):
""" """
Method to perform GET request on the API. Method to perform GET request on the API.
:param endpoint: Endpoint of the API. :param endpoint: Endpoint of the API.
:param kwargs: Other keyword arguments for requests. :param kwargs: Other keyword arguments for requests.
:return: Response of the GET request. :return: Response of the GET request.
""" """
return self._request(endpoint, 'get', **kwargs) return self._request(endpoint, 'get', **kwargs)
@ -46,11 +43,9 @@ class Client(object):
def _post(self, endpoint, data, **kwargs): def _post(self, endpoint, data, **kwargs):
""" """
Method to perform POST request on the API. Method to perform POST request on the API.
:param endpoint: Endpoint of the API. :param endpoint: Endpoint of the API.
:param data: POST DATA for the request. :param data: POST DATA for the request.
:param kwargs: Other keyword arguments for requests. :param kwargs: Other keyword arguments for requests.
:return: Response of the POST request. :return: Response of the POST request.
""" """
return self._request(endpoint, 'post', data, **kwargs) return self._request(endpoint, 'post', data, **kwargs)
@ -58,12 +53,10 @@ class Client(object):
def _request(self, endpoint, method, data=None, **kwargs): def _request(self, endpoint, method, data=None, **kwargs):
""" """
Method to hanle both GET and POST requests. Method to hanle both GET and POST requests.
:param endpoint: Endpoint of the API. :param endpoint: Endpoint of the API.
:param method: Method of HTTP request. :param method: Method of HTTP request.
:param data: POST DATA for the request. :param data: POST DATA for the request.
:param kwargs: Other keyword arguments. :param kwargs: Other keyword arguments.
:return: Response for the request. :return: Response for the request.
""" """
final_url = self.url + endpoint final_url = self.url + endpoint
@ -93,18 +86,15 @@ class Client(object):
def login(self, username='admin', password='admin'): def login(self, username='admin', password='admin'):
""" """
Method to authenticate the qBittorrent Client. Method to authenticate the qBittorrent Client.
Declares a class attribute named ``session`` which Declares a class attribute named ``session`` which
stores the authenticated session if the login is correct. stores the authenticated session if the login is correct.
Else, shows the login error. Else, shows the login error.
:param username: Username. :param username: Username.
:param password: Password. :param password: Password.
:return: Response to login request to the API. :return: Response to login request to the API.
""" """
self.session = requests.Session() self.session = requests.Session()
login = self.session.post(self.url+'login', login = self.session.post(self.url+'api/v2/auth/login',
data={'username': username, data={'username': username,
'password': password}) 'password': password})
if login.text == 'Ok.': if login.text == 'Ok.':
@ -116,7 +106,7 @@ class Client(object):
""" """
Logout the current session. Logout the current session.
""" """
response = self._get('logout') response = self._get('api/v2/auth/logout')
self._is_authenticated = False self._is_authenticated = False
return response return response
@ -125,39 +115,31 @@ class Client(object):
""" """
Get qBittorrent version. Get qBittorrent version.
""" """
return self._get('version/qbittorrent') return self._get('api/v2/app/version')
@property @property
def api_version(self): def api_version(self):
""" """
Get WEB API version. Get WEB API version.
""" """
return self._get('version/api') return self._get('api/v2/app/webapiVersion')
@property
def api_min_version(self):
"""
Get minimum WEB API version.
"""
return self._get('version/api_min')
def shutdown(self): def shutdown(self):
""" """
Shutdown qBittorrent. Shutdown qBittorrent.
""" """
return self._get('command/shutdown') return self._get('api/v2/app/shutdown')
def torrents(self, **filters): def torrents(self, **filters):
""" """
Returns a list of torrents matching the supplied filters. Returns a list of torrents matching the supplied filters.
:param filter: Current status of the torrents. :param filter: Current status of the torrents.
:param category: Fetch all torrents with the supplied label. :param category: Fetch all torrents with the supplied label.
:param sort: Sort torrents by. :param sort: Sort torrents by.
:param reverse: Enable reverse sorting. :param reverse: Enable reverse sorting.
:param limit: Limit the number of torrents returned. :param limit: Limit the number of torrents returned.
:param offset: Set offset (if less than 0, offset from end). :param offset: Set offset (if less than 0, offset from end).
:param hashes: Filter by hashes. Can contain multiple hashes separated by |.
:return: list() of torrent with matching filter. :return: list() of torrent with matching filter.
""" """
params = {} params = {}
@ -166,46 +148,42 @@ class Client(object):
name = 'filter' if name == 'status' else name name = 'filter' if name == 'status' else name
params[name] = value params[name] = value
return self._get('query/torrents', params=params) return self._get('api/v2/torrents/info', params=params)
def get_torrent(self, infohash): def get_torrent(self, infohash):
""" """
Get details of the torrent. Get details of the torrent.
:param infohash: INFO HASH of the torrent. :param infohash: INFO HASH of the torrent.
""" """
return self._get('query/propertiesGeneral/' + infohash.lower()) return self._get('api/v2/torrents/properties', params={'hash': infohash.lower()})
def get_torrent_trackers(self, infohash): def get_torrent_trackers(self, infohash):
""" """
Get trackers for the torrent. Get trackers for the torrent.
:param infohash: INFO HASH of the torrent. :param infohash: INFO HASH of the torrent.
""" """
return self._get('query/propertiesTrackers/' + infohash.lower()) return self._get('api/v2/torrents/trackers', params={'hash': infohash.lower()})
def get_torrent_webseeds(self, infohash): def get_torrent_webseeds(self, infohash):
""" """
Get webseeds for the torrent. Get webseeds for the torrent.
:param infohash: INFO HASH of the torrent. :param infohash: INFO HASH of the torrent.
""" """
return self._get('query/propertiesWebSeeds/' + infohash.lower()) return self._get('api/v2/torrents/webseeds', params={'hash': infohash.lower()})
def get_torrent_files(self, infohash): def get_torrent_files(self, infohash):
""" """
Get list of files for the torrent. Get list of files for the torrent.
:param infohash: INFO HASH of the torrent. :param infohash: INFO HASH of the torrent.
""" """
return self._get('query/propertiesFiles/' + infohash.lower()) return self._get('api/v2/torrents/files', params={'hash': infohash.lower()})
@property @property
def global_transfer_info(self): def global_transfer_info(self):
""" """
Get JSON data of the global transfer info of qBittorrent. Get JSON data of the global transfer info of qBittorrent.
""" """
return self._get('query/transferInfo') return self._get('api/v2/transfer/info')
@property @property
def preferences(self): def preferences(self):
@ -214,39 +192,27 @@ class Client(object):
Can also be used to assign individual preferences. Can also be used to assign individual preferences.
For setting multiple preferences at once, For setting multiple preferences at once,
see ``set_preferences`` method. see ``set_preferences`` method.
Note: Even if this is a ``property``, Note: Even if this is a ``property``,
to fetch the current preferences dict, you are required to fetch the current preferences dict, you are required
to call it like a bound method. to call it like a bound method.
Wrong:: Wrong::
qb.preferences qb.preferences
Right:: Right::
qb.preferences() qb.preferences()
""" """
prefs = self._get('query/preferences') prefs = self._get('api/v2/app/preferences')
class Proxy(Client): class Proxy(Client):
""" """
Proxy class to to allow assignment of individual preferences. Proxy class to to allow assignment of individual preferences.
this class overrides some methods to ease things. this class overrides some methods to ease things.
Because of this, settings can be assigned like:: Because of this, settings can be assigned like::
In [5]: prefs = qb.preferences() In [5]: prefs = qb.preferences()
In [6]: prefs['autorun_enabled'] In [6]: prefs['autorun_enabled']
Out[6]: True Out[6]: True
In [7]: prefs['autorun_enabled'] = False In [7]: prefs['autorun_enabled'] = False
In [8]: prefs['autorun_enabled'] In [8]: prefs['autorun_enabled']
Out[8]: False Out[8]: False
""" """
def __init__(self, url, prefs, auth, session): def __init__(self, url, prefs, auth, session):
@ -270,78 +236,74 @@ class Client(object):
def sync(self, rid=0): def sync(self, rid=0):
""" """
Sync the torrents by supplied LAST RESPONSE ID. Sync the torrents by supplied LAST RESPONSE ID.
Read more @ http://git.io/vEgXr Read more @ https://git.io/fxgB8
:param rid: Response ID of last request. :param rid: Response ID of last request.
""" """
return self._get('sync/maindata', params={'rid': rid}) return self._get('api/v2/sync/maindata', params={'rid': rid})
def download_from_link(self, link, **kwargs): def download_from_link(self, link, **kwargs):
""" """
Download torrent using a link. Download torrent using a link.
:param link: URL Link or list of. :param link: URL Link or list of.
:param savepath: Path to download the torrent. :param savepath: Path to download the torrent.
:param category: Label or Category of the torrent(s). :param category: Label or Category of the torrent(s).
:return: Empty JSON data. :return: Empty JSON data.
""" """
# old:new format # qBittorrent requires adds to be done with multipath/form-data
old_arg_map = {'save_path': 'savepath'} # , 'label': 'category'} # POST requests for both URLs and .torrent files. Info on this
# can be found here, and here:
# convert old option names to new option names # http://docs.python-requests.org/en/master/user/quickstart/#post-a-multipart-encoded-file
options = kwargs.copy() # http://docs.python-requests.org/en/master/user/advanced/#post-multiple-multipart-encoded-files
for old_arg, new_arg in old_arg_map.items(): if isinstance(link, list):
if options.get(old_arg) and not options.get(new_arg): links = '\n'.join(link)
options[new_arg] = options[old_arg] else:
links = link
options['urls'] = link torrent_data = {}
torrent_data['urls'] = (None, links)
# workaround to send multipart/formdata request for k, v in kwargs.iteritems():
# http://stackoverflow.com/a/23131823/4726598 torrent_data[k] = (None, v)
dummy_file = {'_dummy': (None, '_dummy')} return self._post('api/v2/torrents/add', data=None, files=torrent_data)
return self._post('command/download', data=options, files=dummy_file)
def download_from_file(self, file_buffer, **kwargs): def download_from_file(self, file_buffer, **kwargs):
""" """
Download torrent using a file. Download torrent using a file.
:param file_buffer: Single file() buffer or list of. :param file_buffer: Single file() buffer or list of.
:param save_path: Path to download the torrent. :param save_path: Path to download the torrent.
:param label: Label of the torrent(s). :param label: Label of the torrent(s).
:return: Empty JSON data. :return: Empty JSON data.
""" """
# qBittorrent requires adds to be done with multipath/form-data
# POST requests for both URLs and .torrent files. Info on this
# can be found here, and here:
# http://docs.python-requests.org/en/master/user/quickstart/#post-a-multipart-encoded-file
# http://docs.python-requests.org/en/master/user/advanced/#post-multiple-multipart-encoded-files
if isinstance(file_buffer, list): if isinstance(file_buffer, list):
torrent_files = {} torrent_data = []
for i, f in enumerate(file_buffer): for f in file_buffer:
torrent_files.update({'torrents%s' % i: f}) fname = f.name
torrent_data.append(('torrents', (fname, f)))
else: else:
torrent_files = {'torrents': file_buffer} fname = file_buffer.name
torrent_data = [('torrents', (fname, file_buffer))]
for k, v in kwargs.iteritems():
torrent_data.append((k, (None, v)))
data = kwargs.copy() return self._post('api/v2/torrents/add', data=None, files=torrent_data)
if data.get('save_path'):
data.update({'savepath': data['save_path']})
return self._post('command/upload', data=data, files=torrent_files)
def add_trackers(self, infohash, trackers): def add_trackers(self, infohash, trackers):
""" """
Add trackers to a torrent. Add trackers to a torrent.
:param infohash: INFO HASH of torrent. :param infohash: INFO HASH of torrent.
:param trackers: Trackers. :param trackers: Trackers.
""" """
data = {'hash': infohash.lower(), data = {'hash': infohash.lower(),
'urls': trackers} 'urls': trackers}
return self._post('command/addTrackers', data=data) return self._post('api/v2/torrents/addTrackers', data=data)
@staticmethod @staticmethod
def _process_infohash_list(infohash_list): def _process_infohash_list(infohash_list):
""" """
Method to convert the infohash_list to qBittorrent API friendly values. Method to convert the infohash_list to qBittorrent API friendly values.
:param infohash_list: List of infohash. :param infohash_list: List of infohash.
""" """
if isinstance(infohash_list, list): if isinstance(infohash_list, list):
@ -353,142 +315,122 @@ class Client(object):
def pause(self, infohash): def pause(self, infohash):
""" """
Pause a torrent. Pause a torrent.
:param infohash: INFO HASH of torrent. :param infohash: INFO HASH of torrent.
""" """
return self._post('command/pause', data={'hash': infohash.lower()}) return self._post('api/v2/torrents/pause', data={'hashes': infohash.lower()})
def pause_all(self): def pause_all(self):
""" """
Pause all torrents. Pause all torrents.
""" """
return self._get('command/pauseAll') return self._post('api/v2/torrents/pause', data={'hashes': 'all'})
def pause_multiple(self, infohash_list): def pause_multiple(self, infohash_list):
""" """
Pause multiple torrents. Pause multiple torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/pauseAll', data=data) return self._post('api/v2/torrents/pause', data=data)
def set_label(self, infohash_list, label):
"""
Set the label on multiple torrents.
IMPORTANT: OLD API method, kept as it is to avoid breaking stuffs.
:param infohash_list: Single or list() of infohashes.
"""
data = self._process_infohash_list(infohash_list)
data['label'] = label
return self._post('command/setLabel', data=data)
def set_category(self, infohash_list, category): def set_category(self, infohash_list, category):
""" """
Set the category on multiple torrents. Set the category on multiple torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
data['category'] = category data['category'] = category
return self._post('command/setCategory', data=data) return self._post('api/v2/torrents/setCategory', data=data)
def resume(self, infohash): def resume(self, infohash):
""" """
Resume a paused torrent. Resume a paused torrent.
:param infohash: INFO HASH of torrent. :param infohash: INFO HASH of torrent.
""" """
return self._post('command/resume', data={'hash': infohash.lower()}) return self._post('api/v2/torrents/resume', data={'hashes': infohash.lower()})
def resume_all(self): def resume_all(self):
""" """
Resume all torrents. Resume all torrents.
""" """
return self._get('command/resumeAll') return self._get('api/v2/torrents/resume', data={'hashes': 'all'})
def resume_multiple(self, infohash_list): def resume_multiple(self, infohash_list):
""" """
Resume multiple paused torrents. Resume multiple paused torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/resumeAll', data=data) return self._post('api/v2/torrents/resume', data=data)
def delete(self, infohash_list): def delete(self, infohash_list):
""" """
Delete torrents. Delete torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/delete', data=data) data['deleteFiles'] = 'false'
return self._post('api/v2/torrents/delete', data=data)
def delete_permanently(self, infohash_list): def delete_permanently(self, infohash_list):
""" """
Permanently delete torrents. Permanently delete torrents.
*** WARNING : This will instruct qBittorrent to delete files
*** from your hard disk. Use with caution.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/deletePerm', data=data) data['deleteFiles'] = 'true'
return self._post('api/v2/torrents/delete', data=data)
def recheck(self, infohash_list): def recheck(self, infohash_list):
""" """
Recheck torrents. Recheck torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/recheck', data=data) return self._post('api/v2/torrents/recheck', data=data)
def increase_priority(self, infohash_list): def increase_priority(self, infohash_list):
""" """
Increase priority of torrents. Increase priority of torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/increasePrio', data=data) return self._post('api/v2/torrents/increasePrio', data=data)
def decrease_priority(self, infohash_list): def decrease_priority(self, infohash_list):
""" """
Decrease priority of torrents. Decrease priority of torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/decreasePrio', data=data) return self._post('api/v2/torrents/decreasePrio', data=data)
def set_max_priority(self, infohash_list): def set_max_priority(self, infohash_list):
""" """
Set torrents to maximum priority level. Set torrents to maximum priority level.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/topPrio', data=data) return self._post('api/v2/torrents/topPrio', data=data)
def set_min_priority(self, infohash_list): def set_min_priority(self, infohash_list):
""" """
Set torrents to minimum priority level. Set torrents to minimum priority level.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/bottomPrio', data=data) return self._post('api/v2/torrents/bottomPrio', data=data)
def set_file_priority(self, infohash, file_id, priority): def set_file_priority(self, infohash, file_id, priority):
""" """
Set file of a torrent to a supplied priority level. Set file of a torrent to a supplied priority level.
:param infohash: INFO HASH of torrent. :param infohash: INFO HASH of torrent.
:param file_id: ID of the file to set priority. :param file_id: ID of the file to set priority.
:param priority: Priority level of the file. :param priority: Priority level of the file.
""" """
if priority not in [0, 1, 2, 7]: if priority not in [0, 1, 6, 7]:
raise ValueError("Invalid priority, refer WEB-UI docs for info.") raise ValueError("Invalid priority, refer WEB-UI docs for info.")
elif not isinstance(file_id, int): elif not isinstance(file_id, int):
raise TypeError("File ID must be an int") raise TypeError("File ID must be an int")
@ -497,7 +439,7 @@ class Client(object):
'id': file_id, 'id': file_id,
'priority': priority} 'priority': priority}
return self._post('command/setFilePrio', data=data) return self._post('api/v2/torrents/filePrio', data=data)
# Get-set global download and upload speed limits. # Get-set global download and upload speed limits.
@ -505,15 +447,14 @@ class Client(object):
""" """
Get global download speed limit. Get global download speed limit.
""" """
return self._get('command/getGlobalDlLimit') return self._get('api/v2/transfer/downloadLimit')
def set_global_download_limit(self, limit): def set_global_download_limit(self, limit):
""" """
Set global download speed limit. Set global download speed limit.
:param limit: Speed limit in bytes. :param limit: Speed limit in bytes.
""" """
return self._post('command/setGlobalDlLimit', data={'limit': limit}) return self._post('api/v2/transfer/setDownloadLimit', data={'limit': limit})
global_download_limit = property(get_global_download_limit, global_download_limit = property(get_global_download_limit,
set_global_download_limit) set_global_download_limit)
@ -522,15 +463,14 @@ class Client(object):
""" """
Get global upload speed limit. Get global upload speed limit.
""" """
return self._get('command/getGlobalUpLimit') return self._get('api/v2/transfer/uploadLimit')
def set_global_upload_limit(self, limit): def set_global_upload_limit(self, limit):
""" """
Set global upload speed limit. Set global upload speed limit.
:param limit: Speed limit in bytes. :param limit: Speed limit in bytes.
""" """
return self._post('command/setGlobalUpLimit', data={'limit': limit}) return self._post('api/v2/transfer/setUploadLimit', data={'limit': limit})
global_upload_limit = property(get_global_upload_limit, global_upload_limit = property(get_global_upload_limit,
set_global_upload_limit) set_global_upload_limit)
@ -539,61 +479,56 @@ class Client(object):
def get_torrent_download_limit(self, infohash_list): def get_torrent_download_limit(self, infohash_list):
""" """
Get download speed limit of the supplied torrents. Get download speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/getTorrentsDlLimit', data=data) return self._post('api/v2/torrents/downloadLimit', data=data)
def set_torrent_download_limit(self, infohash_list, limit): def set_torrent_download_limit(self, infohash_list, limit):
""" """
Set download speed limit of the supplied torrents. Set download speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
:param limit: Speed limit in bytes. :param limit: Speed limit in bytes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
data.update({'limit': limit}) data.update({'limit': limit})
return self._post('command/setTorrentsDlLimit', data=data) return self._post('api/v2/torrents/setDownloadLimit', data=data)
def get_torrent_upload_limit(self, infohash_list): def get_torrent_upload_limit(self, infohash_list):
""" """
Get upoload speed limit of the supplied torrents. Get upoload speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/getTorrentsUpLimit', data=data) return self._post('api/v2/torrents/uploadLimit', data=data)
def set_torrent_upload_limit(self, infohash_list, limit): def set_torrent_upload_limit(self, infohash_list, limit):
""" """
Set upload speed limit of the supplied torrents. Set upload speed limit of the supplied torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
:param limit: Speed limit in bytes. :param limit: Speed limit in bytes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
data.update({'limit': limit}) data.update({'limit': limit})
return self._post('command/setTorrentsUpLimit', data=data) return self._post('api/v2/torrents/setUploadLimit', data=data)
# setting preferences # setting preferences
def set_preferences(self, **kwargs): def set_preferences(self, **kwargs):
""" """
Set preferences of qBittorrent. Set preferences of qBittorrent.
Read all possible preferences @ http://git.io/vEgDQ Read all possible preferences @ https://git.io/fx2Y9
:param kwargs: set preferences in kwargs form. :param kwargs: set preferences in kwargs form.
""" """
json_data = "json={}".format(json.dumps(kwargs)) json_data = "json={}".format(json.dumps(kwargs))
headers = {'content-type': 'application/x-www-form-urlencoded'} headers = {'content-type': 'application/x-www-form-urlencoded'}
return self._post('command/setPreferences', data=json_data, return self._post('api/v2/app/setPreferences', data=json_data,
headers=headers) headers=headers)
def get_alternative_speed_status(self): def get_alternative_speed_status(self):
""" """
Get Alternative speed limits. (1/0) Get Alternative speed limits. (1/0)
""" """
return self._get('command/alternativeSpeedLimitsEnabled') return self._get('api/v2/transfer/speedLimitsMode')
alternative_speed_status = property(get_alternative_speed_status) alternative_speed_status = property(get_alternative_speed_status)
@ -601,33 +536,30 @@ class Client(object):
""" """
Toggle alternative speed limits. Toggle alternative speed limits.
""" """
return self._get('command/toggleAlternativeSpeedLimits') return self._get('api/v2/transfer/toggleSpeedLimitsMode')
def toggle_sequential_download(self, infohash_list): def toggle_sequential_download(self, infohash_list):
""" """
Toggle sequential download in supplied torrents. Toggle sequential download in supplied torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/toggleSequentialDownload', data=data) return self._post('api/v2/torrents/toggleSequentialDownload', data=data)
def toggle_first_last_piece_priority(self, infohash_list): def toggle_first_last_piece_priority(self, infohash_list):
""" """
Toggle first/last piece priority of supplied torrents. Toggle first/last piece priority of supplied torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
return self._post('command/toggleFirstLastPiecePrio', data=data) return self._post('api/v2/torrents/toggleFirstLastPiecePrio', data=data)
def force_start(self, infohash_list, value=True): def force_start(self, infohash_list, value=True):
""" """
Force start selected torrents. Force start selected torrents.
:param infohash_list: Single or list() of infohashes. :param infohash_list: Single or list() of infohashes.
:param value: Force start value (bool) :param value: Force start value (bool)
""" """
data = self._process_infohash_list(infohash_list) data = self._process_infohash_list(infohash_list)
data.update({'value': json.dumps(value)}) data.update({'value': json.dumps(value)})
return self._post('command/setForceStart', data=data) return self._post('api/v2/torrents/setForceStart', data=data)

View file

@ -5,7 +5,7 @@
### NZBGET POST-PROCESSING SCRIPT ### ### NZBGET POST-PROCESSING SCRIPT ###
# Post-Process to CouchPotato, SickBeard, Sonarr, Mylar, Gamez, HeadPhones, # Post-Process to CouchPotato, SickBeard, Sonarr, Mylar, Gamez, HeadPhones,
# LazyLibrarian, Radarr, Lidarr # LazyLibrarian, Radarr, Lidarr, Watcher3
# #
# This script sends the download to your automated media management servers. # This script sends the download to your automated media management servers.
# #
@ -142,6 +142,54 @@
# Enable to replace local path with the path as per the mountPoints below. # Enable to replace local path with the path as per the mountPoints below.
#raremote_path=0 #raremote_path=0
## Watcher3
# Wather3 script category.
#
# category that gets called for post-processing with Watcher3.
#W3Category=movie
# Watcher3 api key.
#W3apikey=
# Watcher3 host.
#
# The ipaddress for your Watcher3 server. e.g For the Same system use localhost or 127.0.0.1
#W3host=localhost
# Watcher3 port.
#W3port=9090
# Watcher3 uses ssl (0, 1).
#
# Set to 1 if using ssl, else set to 0.
#W3ssl=0
# Watcher3 URL_Base
#
# set this if using a reverse proxy.
#W3web_root=
# OMDB API Key.
#
# api key for www.omdbapi.com (used as alternative to imdb to assist with movie identification).
#W3omdbapikey=
# Wacther3 Delete Failed Downloads (0, 1).
#
# set to 1 to delete failed, or 0 to leave files in place.
#W3delete_failed=0
# Wacther3 wait_for
#
# Set the number of minutes to wait after calling the renamer, to check the movie has changed status.
#W3wait_for=2
# Watcher3 and NZBGet are a different system (0, 1).
#
# Enable to replace local path with the path as per the mountPoints below.
#W3remote_path=0
## SickBeard ## SickBeard
# SickBeard script category. # SickBeard script category.
@ -799,7 +847,7 @@ def process(input_directory, input_name=None, status=0, client_agent='manual', d
logger.info('Calling {0}:{1} to post-process:{2}'.format(section_name, input_category, input_name)) logger.info('Calling {0}:{1} to post-process:{2}'.format(section_name, input_category, input_name))
if section_name in ['CouchPotato', 'Radarr']: if section_name in ['CouchPotato', 'Radarr', 'Watcher3']:
result = movies.process(section_name, input_directory, input_name, status, client_agent, download_id, input_category, failure_link) result = movies.process(section_name, input_directory, input_name, status, client_agent, download_id, input_category, failure_link)
elif section_name in ['SickBeard', 'NzbDrone', 'Sonarr']: elif section_name in ['SickBeard', 'NzbDrone', 'Sonarr']:
result = tv.process(section_name, input_directory, input_name, status, client_agent, download_id, input_category, failure_link) result = tv.process(section_name, input_directory, input_name, status, client_agent, download_id, input_category, failure_link)

268
nzbToWatcher3.py Normal file
View file

@ -0,0 +1,268 @@
#!/usr/bin/env python
# coding=utf-8
#
##############################################################################
### NZBGET POST-PROCESSING SCRIPT ###
# Post-Process to Watcher3
#
# This script sends the download to your automated media management servers.
#
# NOTE: This script requires Python to be installed on your system.
##############################################################################
### OPTIONS ###
## General
# Auto Update nzbToMedia (0, 1).
#
# Set to 1 if you want nzbToMedia to automatically check for and update to the latest version
#auto_update=0
# Check Media for corruption (0, 1).
#
# Enable/Disable media file checking using ffprobe.
#check_media=1
# Safe Mode protection of DestDir (0, 1).
#
# Enable/Disable a safety check to ensure we don't process all downloads in the default_downloadDirectory by mistake.
#safe_mode=1
# Disable additional extraction checks for failed (0, 1).
#
# Turn this on to disable additional extraction attempts for failed downloads. Default = 0 this will attempt to extract and verify if media is present.
#no_extract_failed = 0
## Watcher3
# Watcher3 script category.
#
# category that gets called for post-processing with Watcher3.
#W3Category=movie
# Watcher3 api key.
#W3apikey=
# Watcher3 host.
#
# The ipaddress for your Watcher3 server. e.g For the Same system use localhost or 127.0.0.1
#W3host=localhost
# Watcher3 port.
#W3port=5050
# Watcher3 uses ssl (0, 1).
#
# Set to 1 if using ssl, else set to 0.
#W3ssl=0
# Watcher3 URL_Base
#
# set this if using a reverse proxy.
#W3web_root=
# Watcher3 watch directory.
#
# set this to where your Watcher3 completed downloads are.
#W3watch_dir=
# OMDB API Key.
#
# api key for www.omdbapi.com (used as alternative to imdb to assist with movie identification).
#W3omdbapikey=
# Watcher3 Delete Failed Downloads (0, 1).
#
# set to 1 to delete failed, or 0 to leave files in place.
#W3delete_failed=0
# Watcher3 wait_for
#
# Set the number of minutes to wait after calling the renamer, to check the movie has changed status.
#W3wait_for=2
# Watcher3 and NZBGet are a different system (0, 1).
#
# Enable to replace local path with the path as per the mountPoints below.
#W3remote_path=0
## Network
# Network Mount Points (Needed for remote path above)
#
# Enter Mount points as LocalPath,RemotePath and separate each pair with '|'
# e.g. mountPoints=/volume1/Public/,E:\|/volume2/share/,\\NAS\
#mountPoints=
## Extensions
# Media Extensions
#
# This is a list of media extensions that are used to verify that the download does contain valid media.
#mediaExtensions=.mkv,.avi,.divx,.xvid,.mov,.wmv,.mp4,.mpg,.mpeg,.vob,.iso,.ts
## Posix
# Niceness for external tasks Extractor and Transcoder.
#
# Set the Niceness value for the nice command. These range from -20 (most favorable to the process) to 19 (least favorable to the process).
# If entering an integer e.g 'niceness=4', this is added to the nice command and passed as 'nice -n4' (Default).
# If entering a comma separated list e.g. 'niceness=nice,4' this will be passed as 'nice 4' (Safer).
#niceness=nice,-n0
# ionice scheduling class (0, 1, 2, 3).
#
# Set the ionice scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
#ionice_class=2
# ionice scheduling class data.
#
# Set the ionice scheduling class data. This defines the class data, if the class accepts an argument. For real time and best-effort, 0-7 is valid data.
#ionice_classdata=4
## Transcoder
# getSubs (0, 1).
#
# set to 1 to download subtitles.
#getSubs=0
# subLanguages.
#
# subLanguages. create a list of languages in the order you want them in your subtitles.
#subLanguages=eng,spa,fra
# Transcode (0, 1).
#
# set to 1 to transcode, otherwise set to 0.
#transcode=0
# create a duplicate, or replace the original (0, 1).
#
# set to 1 to cretae a new file or 0 to replace the original
#duplicate=1
# ignore extensions.
#
# list of extensions that won't be transcoded.
#ignoreExtensions=.avi,.mkv
# outputFastStart (0,1).
#
# outputFastStart. 1 will use -movflags + faststart. 0 will disable this from being used.
#outputFastStart=0
# outputVideoPath.
#
# outputVideoPath. Set path you want transcoded videos moved to. Leave blank to disable.
#outputVideoPath=
# processOutput (0,1).
#
# processOutput. 1 will send the outputVideoPath to SickBeard/CouchPotato. 0 will send original files.
#processOutput=0
# audioLanguage.
#
# audioLanguage. set the 3 letter language code you want as your primary audio track.
#audioLanguage=eng
# allAudioLanguages (0,1).
#
# allAudioLanguages. 1 will keep all audio tracks (uses AudioCodec3) where available.
#allAudioLanguages=0
# allSubLanguages (0,1).
#
# allSubLanguages. 1 will keep all exisiting sub languages. 0 will discare those not in your list above.
#allSubLanguages=0
# embedSubs (0,1).
#
# embedSubs. 1 will embded external sub/srt subs into your video if this is supported.
#embedSubs=1
# burnInSubtitle (0,1).
#
# burnInSubtitle. burns the default sub language into your video (needed for players that don't support subs)
#burnInSubtitle=0
# extractSubs (0,1).
#
# extractSubs. 1 will extract subs from the video file and save these as external srt files.
#extractSubs=0
# externalSubDir.
#
# externalSubDir. set the directory where subs should be saved (if not the same directory as the video)
#externalSubDir=
# outputDefault (None, iPad, iPad-1080p, iPad-720p, Apple-TV2, iPod, iPhone, PS3, xbox, Roku-1080p, Roku-720p, Roku-480p, mkv, mkv-bluray, mp4-scene-release, MKV-SD).
#
# outputDefault. Loads default configs for the selected device. The remaining options below are ignored.
# If you want to use your own profile, set None and set the remaining options below.
#outputDefault=None
# hwAccel (0,1).
#
# hwAccel. 1 will set ffmpeg to enable hardware acceleration (this requires a recent ffmpeg).
#hwAccel=0
# ffmpeg output settings.
#outputVideoExtension=.mp4
#outputVideoCodec=libx264
#VideoCodecAllow=
#outputVideoResolution=720:-1
#outputVideoPreset=medium
#outputVideoFramerate=24
#outputVideoBitrate=800k
#outputAudioCodec=ac3
#AudioCodecAllow=
#outputAudioChannels=6
#outputAudioBitrate=640k
#outputQualityPercent=
#outputAudioTrack2Codec=libfaac
#AudioCodec2Allow=
#outputAudioTrack2Channels=2
#outputAudioTrack2Bitrate=160k
#outputAudioOtherCodec=libmp3lame
#AudioOtherCodecAllow=
#outputAudioOtherChannels=2
#outputAudioOtherBitrate=128k
#outputSubtitleCodec=
## WakeOnLan
# use WOL (0, 1).
#
# set to 1 to send WOL broadcast to the mac and test the server (e.g. xbmc) on the host and port specified.
#wolwake=0
# WOL MAC
#
# enter the mac address of the system to be woken.
#wolmac=00:01:2e:2D:64:e1
# Set the Host and Port of a server to verify system has woken.
#wolhost=192.168.1.37
#wolport=80
### NZBGET POST-PROCESSING SCRIPT ###
##############################################################################
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
import sys
import nzbToMedia
section = 'Watcher3'
result = nzbToMedia.main(sys.argv, section)
sys.exit(result)

View file

@ -23,7 +23,7 @@ def read(*names, **kwargs):
setup( setup(
name='nzbToMedia', name='nzbToMedia',
version='12.1.01', version='12.1.02',
license='GPLv3', license='GPLv3',
description='Efficient on demand post processing', description='Efficient on demand post processing',
long_description=""" long_description="""

View file

@ -11,4 +11,4 @@ from core import transcoder
def test_transcoder_check(): def test_transcoder_check():
assert transcoder.is_video_good(core.TEST_FILE, 0) is True assert transcoder.is_video_good(core.TEST_FILE, 1) is True