NzbDrone support

Although NzbDrone says that a post processing script isn't strictly needed, I've had it attempt to move downloads before they've finished unpacking. So I decided to add NzbDrone support to nzbToMedia!

This has been tested by myself, and seems to work just fine. I have a pretty simple setup though, so any other testing would be appreciated.

 - Added nzbToNzbDrone script, as well as added NzbDrone support to nzbToMedia.
 - Rather than adding to autoProcessTV, I created another script, autoProcessTVND (I know, original name...). This does lead to some code duplication, and fixes for one will have to be added to the other too, but I thought this was cleaner than throwing a bunch of conditionals into autoProcessTV. If that's wrong, let me know and I'll make the changes...
 - Added default config to the main .cfg.sample file and migrate code for NZBGet.
 - By default, the same category as SB is used, but SB is preferred as it is checked first. An option also exists to prefer NzbDrone if there's a clash.
 - NzbDrone needs an API key; basic auth is being removed soon.
 - I didn't include the delay options that SB uses, as NzbDrone returns immediately from the API call; it doesn't wait until the end, so there's no need to include a timeout or delay. It might be possible to check the status of the scan, but I don't think that functionality exists yet.
This commit is contained in:
Smenus 2014-04-06 04:29:19 +01:00
commit 50676fae3c
5 changed files with 415 additions and 4 deletions

View file

@ -40,6 +40,18 @@
Torrent_NoLink = 0
process_method =
[NzbDrone]
#### autoProcessing for TV Series
#### ndCategory - category that gets called for post-processing with NzbDrone
[[tv]]
Host = localhost
Port = 8989
APIKey =
###### ADVANCED USE - ONLY EDIT IF YOU KNOW WHAT YOU'RE DOING ######
WebRoot =
SSL = 0
Prefer = 0
[HeadPhones]
#### autoProcessing for Music
#### music - category that gets called for post-processing with HP

View file

@ -9,7 +9,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib'
##############################################################################
### NZBGET POST-PROCESSING SCRIPT ###
# Post-Process to CouchPotato, SickBeard, Mylar, Gamez, HeadPhones.
# Post-Process to CouchPotato, SickBeard, NzbDrone, Mylar, Gamez, HeadPhones.
#
# This script sends the download to your automated media management servers.
#
@ -133,6 +133,35 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib'
# set this to move, copy, hardlin, symlink as appropriate if you want to over-ride SB defaults. Leave blank to use SB default.
#sbprocess_method=
## NzbDrone
# NzbDrone script category.
#
# category that gets called for post-processing with NzbDrone.
#ndCategory=tv
# NzbDrone host.
#ndHost=localhost
# NzbDrone port.
#ndPort=8989
# NzbDrone API key.
#ndAPIKey=
# NzbDrone uses SSL (0, 1).
#
# Set to 1 if using SSL, else set to 0.
#ndSSL=0
# NzbDrone web root.
#
# set this if using a reverse proxy.
#ndWebRoot=
# Prefer NzbDrone if categories clash (0, 1).
#ndPrefer=0
## HeadPhones
# HeadPhones script category.
@ -282,6 +311,7 @@ from nzbtomedia.autoProcess.autoProcessGames import autoProcessGames
from nzbtomedia.autoProcess.autoProcessMovie import autoProcessMovie
from nzbtomedia.autoProcess.autoProcessMusic import autoProcessMusic
from nzbtomedia.autoProcess.autoProcessTV import autoProcessTV
from nzbtomedia.autoProcess.autoProcessTVND import autoProcessTVND
from nzbtomedia.migratecfg import migratecfg
from nzbtomedia.nzbToMediaConfig import config
from nzbtomedia.nzbToMediaUtil import nzbtomedia_configure_logging, WakeUp, get_dirnames
@ -298,7 +328,7 @@ def process(nzbDir, inputName=None, status=0, clientAgent='manual', download_id=
else:
Logger.info("MAIN: Calling CouchPotatoServer to post-process: %s", inputName)
return autoProcessMovie().process(nzbDir, inputName, status, clientAgent, download_id, inputCategory)
elif inputCategory in sections["SickBeard"]:
elif inputCategory in sections["SickBeard"] and (inputCategory not in sections["NzbDrone"] or int(config()["NzbDrone"][inputCategory]["Prefer"]) == 0):
if isinstance(nzbDir, list):
for dirName in nzbDir:
Logger.info("MAIN: Calling Sick-Beard to post-process: %s", inputName)
@ -308,6 +338,16 @@ def process(nzbDir, inputName=None, status=0, clientAgent='manual', download_id=
else:
Logger.info("MAIN: Calling Sick-Beard to post-process: %s", inputName)
return autoProcessTV().processEpisode(nzbDir, inputName, status, clientAgent, inputCategory)
elif inputCategory in sections["NzbDrone"]:
if isinstance(nzbDir, list):
for dirName in nzbDir:
Logger.info("MAIN: Calling NzbDrone to post-process: %s", inputName)
result = autoProcessTVND().processEpisode(dirName, dirName, status, clientAgent, inputCategory)
if result !=0:
return result
else:
Logger.info("MAIN: Calling Sick-Beard to post-process: %s", inputName)
return autoProcessTV().processEpisode(nzbDir, inputName, status, clientAgent, inputCategory)
elif inputCategory in sections["HeadPhones"]:
if isinstance(nzbDir, list):
for dirName in nzbDir:
@ -362,7 +402,7 @@ else:
sys.exit(-1)
# setup sections and categories
sections = config.get_categories(["CouchPotato","SickBeard","HeadPhones","Mylar","Gamez"])
sections = config.get_categories(["CouchPotato","SickBeard","NzbDrone","HeadPhones","Mylar","Gamez"])
WakeUp()

221
nzbToNzbDrone.py Executable file
View file

@ -0,0 +1,221 @@
#!/usr/bin/env python
# adds lib directory to system path
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
#
##############################################################################
### NZBGET POST-PROCESSING SCRIPT ###
# Post-Process to NzbDrone.
#
# This script sends the download to your automated media management servers.
#
# NOTE: This script requires Python to be installed on your system.
##############################################################################
### OPTIONS ###
## NzbDrone
# NzbDrone script category.
#
# category that gets called for post-processing with NzbDrone.
#ndCategory=tv
# NzbDrone host.
#ndHost=localhost
# NzbDrone port.
#ndPort=8989
# NzbDrone API key.
#ndAPIKey=
# NzbDrone uses SSL (0, 1).
#
# Set to 1 if using SSL, else set to 0.
#ndSSL=0
# NzbDrone web root.
#
# set this if using a reverse proxy.
#ndWebRoot=
## 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
## Transcoder
# 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
# ffmpeg output settings.
#outputVideoExtension=.mp4
#outputVideoCodec=libx264
#outputVideoPreset=medium
#outputVideoFramerate=24
#outputVideoBitrate=800k
#outputAudioCodec=libmp3lame
#outputAudioBitrate=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 ###
##############################################################################
import logging
from nzbtomedia.autoProcess.autoProcessTVND import autoProcessTVND
from nzbtomedia.migratecfg import migratecfg
from nzbtomedia.nzbToMediaConfig import config
from nzbtomedia.nzbToMediaUtil import nzbtomedia_configure_logging, WakeUp, get_dirnames
# run migrate to convert old cfg to new style cfg plus fix any cfg missing values/options.
if migratecfg().migrate():
# check to write settings from nzbGet UI to autoProcessMedia.cfg.
if os.environ.has_key('NZBOP_SCRIPTDIR'):
migratecfg().addnzbget()
nzbtomedia_configure_logging(config.LOG_FILE)
Logger = logging.getLogger(__name__)
Logger.info("====================") # Seperate old from new log
Logger.info("nzbToNzbDrone %s", config.NZBTOMEDIA_VERSION)
Logger.info("MAIN: Loading config from %s", config.CONFIG_FILE)
else:
sys.exit(-1)
# NzbDrone category
categories = config.get_categories(["NzbDrone"])
WakeUp()
# NZBGet V11+
# Check if the script is called from nzbget 11.0 or later
if os.environ.has_key('NZBOP_SCRIPTDIR') and not os.environ['NZBOP_VERSION'][0:5] < '11.0':
Logger.info("MAIN: Script triggered from NZBGet (11.0 or later).")
# Check nzbget.conf options
status = 0
if os.environ['NZBOP_UNPACK'] != 'yes':
Logger.error("MAIN: Please enable option \"Unpack\" in nzbget configuration file, exiting")
sys.exit(config.NZBGET_POSTPROCESS_ERROR)
# Check par status
if os.environ['NZBPP_PARSTATUS'] == '3':
Logger.warning("MAIN: Par-check successful, but Par-repair disabled, exiting")
Logger.info("MAIN: Please check your Par-repair settings for future downloads.")
sys.exit(config.NZBGET_POSTPROCESS_NONE)
if os.environ['NZBPP_PARSTATUS'] == '1' or os.environ['NZBPP_PARSTATUS'] == '4':
Logger.warning("MAIN: Par-repair failed, setting status \"failed\"")
status = 1
# Check unpack status
if os.environ['NZBPP_UNPACKSTATUS'] == '1':
Logger.warning("MAIN: Unpack failed, setting status \"failed\"")
status = 1
if os.environ['NZBPP_UNPACKSTATUS'] == '0' and os.environ['NZBPP_PARSTATUS'] == '0':
# Unpack was skipped due to nzb-file properties or due to errors during par-check
if os.environ['NZBPP_HEALTH'] < 1000:
Logger.warning("MAIN: Download health is compromised and Par-check/repair disabled or no .par2 files found. Setting status \"failed\"")
Logger.info("MAIN: Please check your Par-check/repair settings for future downloads.")
status = 1
else:
Logger.info("MAIN: Par-check/repair disabled or no .par2 files found, and Unpack not required. Health is ok so handle as though download successful")
Logger.info("MAIN: Please check your Par-check/repair settings for future downloads.")
# Check if destination directory exists (important for reprocessing of history items)
if not os.path.isdir(os.environ['NZBPP_DIRECTORY']):
Logger.error("MAIN: Nothing to post-process: destination directory %s doesn't exist. Setting status \"failed\"", os.environ['NZBPP_DIRECTORY'])
status = 1
# All checks done, now launching the script.
Logger.info("MAIN: Script triggered from NZBGet, starting autoProcessTVND...")
clientAgent = "nzbget"
result = autoProcessTVND().processEpisode(os.environ['NZBPP_DIRECTORY'], os.environ['NZBPP_NZBFILENAME'], status, clientAgent, os.environ['NZBPP_CATEGORY'])
# SABnzbd Pre 0.7.17
elif len(sys.argv) == config.SABNZB_NO_OF_ARGUMENTS:
# SABnzbd argv:
# 1 The final directory of the job (full path)
# 2 The original name of the NZB file
# 3 Clean version of the job name (no path info and ".nzb" removed)
# 4 Indexer's report number (if supported)
# 5 User-defined category
# 6 Group that the NZB was posted in e.g. alt.binaries.x
# 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
Logger.info("MAIN: Script triggered from SABnzbd, starting autoProcessTVND...")
clientAgent = "sabnzbd"
result = autoProcessTVND().processEpisode(sys.argv[1], sys.argv[2], sys.argv[7], clientAgent, sys.argv[5])
# SABnzbd 0.7.17+
elif len(sys.argv) >= config.SABNZB_0717_NO_OF_ARGUMENTS:
# SABnzbd argv:
# 1 The final directory of the job (full path)
# 2 The original name of the NZB file
# 3 Clean version of the job name (no path info and ".nzb" removed)
# 4 Indexer's report number (if supported)
# 5 User-defined category
# 6 Group that the NZB was posted in e.g. alt.binaries.x
# 7 Status of post processing. 0 = OK, 1=failed verification, 2=failed unpack, 3=1+2
# 8 Failure URL
Logger.info("MAIN: Script triggered from SABnzbd 0.7.17+, starting autoProcessTVND...")
clientAgent = "sabnzbd"
result = autoProcessTVND().processEpisode(sys.argv[1], sys.argv[2], sys.argv[7], clientAgent, sys.argv[5])
else:
result = 0
Logger.debug("MAIN: Invalid number of arguments received from client.")
Logger.info("MAIN: Running autoProcessTVND as a manual run...")
for section, category in categories.items():
for dirName in get_dirnames(section, category):
Logger.info("MAIN: Calling " + section + ":" + category + " to post-process: %s", dirName)
results = autoProcessTVND().processEpisode(dirName, dirName, 0)
if results != 0:
result = 1
Logger.info("MAIN: A problem was reported in the autoProcessTVND script.")
if result == 0:
Logger.info("MAIN: The autoProcessTVND script completed successfully.")
if os.environ.has_key('NZBOP_SCRIPTDIR'): # return code for nzbget v11
sys.exit(config.NZBGET_POSTPROCESS_SUCCESS)
else:
Logger.info("MAIN: A problem was reported in the autoProcessTVND script.")
if os.environ.has_key('NZBOP_SCRIPTDIR'): # return code for nzbget v11
sys.exit(config.NZBGET_POSTPROCESS_ERROR)

View file

@ -0,0 +1,120 @@
import json
import logging
import os
import socket
from lib import requests
from nzbtomedia.Transcoder import Transcoder
from nzbtomedia.nzbToMediaConfig import config
from nzbtomedia.nzbToMediaSceneExceptions import process_all_exceptions
from nzbtomedia.nzbToMediaUtil import convert_to_ascii, is_sample, flatten
Logger = logging.getLogger()
class autoProcessTVND:
def processEpisode(self, dirName, nzbName=None, failed=False, clientAgent = "manual", inputCategory=None):
if dirName is None:
Logger.error("No directory was given!")
return 1 # failure
socket.setdefaulttimeout(int(config.NZBTOMEDIA_TIMEOUT)) #initialize socket timeout.
Logger.info("Loading config from %s", config.CONFIG_FILE)
status = int(failed)
section = "NzbDrone"
host = config()[section][inputCategory]["Host"]
port = config()[section][inputCategory]["Port"]
api_key = config()[section][inputCategory]["APIKey"]
try:
ssl = int(config()[section][inputCategory]["SSL"])
except:
ssl = 0
try:
web_root = config()[section][inputCategory]["WebRoot"]
except:
web_root = ""
try:
transcode = int(config()["Transcoder"]["transcode"])
except:
transcode = 0
try:
SampleIDs = (config()["Extensions"]["SampleIDs"])
except:
SampleIDs = ['sample','-s.']
mediaContainer = (config()["Extensions"]["mediaExtensions"])
minSampleSize = int(config()["Extensions"]["minSampleSize"])
if not os.path.isdir(dirName) and os.path.isfile(dirName): # If the input directory is a file, assume single file download and split dir/name.
dirName = os.path.split(os.path.normpath(dirName))[0]
SpecificPath = os.path.join(dirName, str(nzbName))
cleanName = os.path.splitext(SpecificPath)
if cleanName[1] == ".nzb":
SpecificPath = cleanName[0]
if os.path.isdir(SpecificPath):
dirName = SpecificPath
# Confirm that the path contains videos and clean up.
if nzbName:
process_all_exceptions(nzbName.lower(), dirName)
nzbName, dirName = convert_to_ascii(nzbName, dirName)
# Now check if tv files exist in destination.
video = int(0)
for dirpath, dirnames, filenames in os.walk(dirName):
for file in filenames:
filePath = os.path.join(dirpath, file)
fileExtension = os.path.splitext(file)[1]
if fileExtension in mediaContainer: # If the file is a video file
if is_sample(filePath, nzbName, minSampleSize, SampleIDs):
Logger.debug("Removing sample file: %s", filePath)
os.unlink(filePath) # remove samples
else:
video = video + 1
if video > 0: # Check that a video exists. if not, assume failed.
flatten(dirName) # to make sure NzbDrone can find the video (not in sub-folder)
elif clientAgent == "manual":
Logger.warning("No media files found in directory %s to manually process.", dirName)
return 0 # Success (as far as this script is concerned)
else:
Logger.warning("No media files found in directory %s. Processing this as a failed download", dirName)
status = int(1)
failed = True
if status == 0 and transcode == 1: # only transcode successful downlaods
result = Transcoder().Transcode_directory(dirName)
if result == 0:
Logger.debug("Transcoding succeeded for files in %s", dirName)
else:
Logger.warning("Transcoding failed for files in %s", dirName)
if status == 0:
Logger.info("The download succeeded. Sending process request to NzbDrone")
else:
Logger.info("The download failed. Sending 'failed' process request to NzbDrone")
if ssl:
protocol = "https://"
else:
protocol = "http://"
url = protocol + host + ":" + port + web_root + "/api/command"
data = json.dumps({"name": "DownloadedEpisodesScan", "path": dirName})
headers = {"X-Api-Key": api_key}
Logger.debug("Opening URL: %s, with data %s and headers %s", url, data, headers)
try:
r = requests.post(url, data=data, headers=headers)
except requests.ConnectionError:
Logger.exception("Unable to open URL")
return 1 # failure
for line in r.iter_lines():
if line: Logger.info("%s", line)
return 0 # Success

View file

@ -35,7 +35,7 @@ class migratecfg:
if section == "SickBeard":
if option == "category": # change this old format
option = "sbCategory"
if option in ["cpsCategory","sbCategory","hpCategory","mlCategory","gzCategory"]:
if option in ["cpsCategory","sbCategory","ndCategory","hpCategory","mlCategory","gzCategory"]:
if not isinstance(value, list):
value = [value]
@ -81,6 +81,10 @@ class migratecfg:
if option in ["category", "sbCategory"]:
continue
if section == "NzbDrone":
if option == "ndCategory":
continue
if section == "HeadPhones":
if option in ["username", "password" ]:
continue
@ -157,6 +161,20 @@ class migratecfg:
confignew[section][os.environ[envCatKey]] = {}
confignew[section][os.environ[envCatKey]][option] = value
section = "NzbDrone"
envCatKey = 'NZBPO_NDCATEGORY'
envKeys = ['HOST', 'PORT', 'APIKEY', 'SSL', 'WEBROOT', 'PREFER']
cfgKeys = ['Host', 'Port', 'APIKey', 'SSL', 'WebRoot', 'Prefer']
if os.environ.has_key(envCatKey):
for index in range(len(envKeys)):
key = 'NZBPO_ND' + envKeys[index]
if os.environ.has_key(key):
option = cfgKeys[index]
value = os.environ[key]
if os.environ[envCatKey] not in confignew[section].sections:
confignew[section][os.environ[envCatKey]] = {}
confignew[section][os.environ[envCatKey]][option] = value
section = "HeadPhones"
envCatKey = 'NZBPO_HPCATEGORY'
envKeys = ['APIKEY', 'HOST', 'PORT', 'SSL', 'WEB_ROOT', 'DELAY', 'TIMEPERGIB']