diff --git a/autoProcessMedia.cfg.sample b/autoProcessMedia.cfg.sample index e761ddbe..ad21db01 100644 --- a/autoProcessMedia.cfg.sample +++ b/autoProcessMedia.cfg.sample @@ -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 diff --git a/nzbToMedia.py b/nzbToMedia.py index bf2a9de9..c17e2304 100755 --- a/nzbToMedia.py +++ b/nzbToMedia.py @@ -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() diff --git a/nzbToNzbDrone.py b/nzbToNzbDrone.py new file mode 100755 index 00000000..dddcb2bc --- /dev/null +++ b/nzbToNzbDrone.py @@ -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) diff --git a/nzbtomedia/autoProcess/autoProcessTVND.py b/nzbtomedia/autoProcess/autoProcessTVND.py new file mode 100644 index 00000000..0a7dc3b8 --- /dev/null +++ b/nzbtomedia/autoProcess/autoProcessTVND.py @@ -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 diff --git a/nzbtomedia/migratecfg.py b/nzbtomedia/migratecfg.py index 4aafbe3e..210f652b 100644 --- a/nzbtomedia/migratecfg.py +++ b/nzbtomedia/migratecfg.py @@ -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']