diff --git a/TorrentToMedia.py b/TorrentToMedia.py index 9fdbd587..1bd92b28 100755 --- a/TorrentToMedia.py +++ b/TorrentToMedia.py @@ -132,7 +132,7 @@ def processTorrent(inputDirectory, inputName, inputCategory, inputHash, inputID, logger.debug("Scanning files in directory: {0}".format(inputDirectory)) - if sectionName == 'HeadPhones': + if sectionName in ['HeadPhones', 'Lidarr']: core.NOFLATTEN.extend( inputCategory) # Make sure we preserve folder structure for HeadPhones. @@ -239,7 +239,7 @@ def processTorrent(inputDirectory, inputName, inputCategory, inputHash, inputID, inputHash = inputHash.upper() result = core.autoProcessTV().processEpisode(sectionName, outputDestination, inputName, status, clientAgent, inputHash, inputCategory) - elif sectionName == 'HeadPhones': + elif sectionName in ['HeadPhones', 'Lidarr']: result = core.autoProcessMusic().process(sectionName, outputDestination, inputName, status, clientAgent, inputCategory) elif sectionName == 'Mylar': diff --git a/autoProcessMedia.cfg.spec b/autoProcessMedia.cfg.spec index 90a736a0..62aef675 100644 --- a/autoProcessMedia.cfg.spec +++ b/autoProcessMedia.cfg.spec @@ -192,6 +192,33 @@ ##### Set to path where download client places completed downloads locally for this category watch_dir = +[Lidarr] + #### autoProcessing for Movies + #### raCategory - category that gets called for post-processing with Radarr + [[music]] + enabled = 0 + apikey = + host = localhost + port = 8686 + ###### ADVANCED USE - ONLY EDIT IF YOU KNOW WHAT YOU'RE DOING ###### + web_root = + ssl = 0 + delete_failed = 0 + # Enable/Disable linking for Torrents + Torrent_NoLink = 0 + keep_archive = 1 + extract = 1 + nzbExtractionBy = Downloader + wait_for = 6 + # 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 NzbDrone 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 = + [Mylar] #### autoProcessing for Comics #### comics - category that gets called for post-processing with Mylar diff --git a/core/autoProcess/autoProcessMusic.py b/core/autoProcess/autoProcessMusic.py index 125b3569..f9ff1057 100644 --- a/core/autoProcess/autoProcessMusic.py +++ b/core/autoProcess/autoProcessMusic.py @@ -4,6 +4,7 @@ import os import time import requests import core +import json from core.nzbToMediaUtil import convert_to_ascii, remoteDir, listMediaFiles, server_responding from core.nzbToMediaSceneExceptions import process_all_exceptions @@ -13,6 +14,39 @@ requests.packages.urllib3.disable_warnings() class autoProcessMusic(object): + def command_complete(self, url, params, headers, section): + try: + r = requests.get(url, params=params, headers=headers, stream=True, verify=False, timeout=(30, 60)) + except requests.ConnectionError: + logger.error("Unable to open URL: {0}".format(url), section) + return None + if r.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]: + logger.error("Server returned status {0}".format(r.status_code), section) + return None + else: + try: + return r.json()['state'] + except (ValueError, KeyError): + # ValueError catches simplejson's JSONDecodeError and json's ValueError + logger.error("{0} did not return expected json data.".format(section), section) + return None + + def CDH(self, url2, headers, section="MAIN"): + try: + r = requests.get(url2, params={}, headers=headers, stream=True, verify=False, timeout=(30, 60)) + except requests.ConnectionError: + logger.error("Unable to open URL: {0}".format(url2), section) + return False + if r.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]: + logger.error("Server returned status {0}".format(r.status_code), section) + return False + else: + try: + return r.json().get("enableCompletedDownloadHandling", False) + except ValueError: + # ValueError catches simplejson's JSONDecodeError and json's ValueError + return False + def get_status(self, url, apikey, dirName): logger.debug("Attempting to get current status for release:{0}".format(os.path.basename(dirName))) @@ -96,7 +130,10 @@ class autoProcessMusic(object): else: extract = int(cfg.get("extract", 0)) - url = "{0}{1}:{2}{3}/api".format(protocol, host, port, web_root) + if section == "Lidarr": + url = "{0}{1}:{2}{3}/api/v1".format(protocol, host, port, web_root) + else: + url = "{0}{1}:{2}{3}/api".format(protocol, host, port, web_root) if not server_responding(url): logger.error("Server did not respond. Exiting", section) return [1, "{0}: Failed to post-process - {1} did not respond.".format(section, section)] @@ -123,7 +160,7 @@ class autoProcessMusic(object): # logger.info("Status shown as failed from Downloader, but valid video files found. Setting as successful.", section) # status = 0 - if status == 0: + if status == 0 and section == "HeadPhones": params = { 'apikey': apikey, @@ -149,6 +186,74 @@ class autoProcessMusic(object): logger.warning("The music album does not appear to have changed status after {0} minutes. Please check your Logs".format(wait_for), section) return [1, "{0}: Failed to post-process - No change in wanted status".format(section)] + elif status == 0 and section == "Lidarr": + url = "{0}{1}:{2}{3}/api/v1/command".format(protocol, host, port, web_root) + url2 = "{0}{1}:{2}{3}/api/v1/config/downloadClient".format(protocol, host, port, web_root) + headers = {"X-Api-Key": apikey} + if remote_path: + logger.debug("remote_path: {0}".format(remoteDir(dirName)), section) + data = {"name": "DownloadedAlbumScan", "path": remoteDir(dirName), "downloadClientId": download_id, "importMode": "Move"} + else: + logger.debug("path: {0}".format(dirName), section) + data = {"name": "DownloadedAlbumScan", "path": dirName, "downloadClientId": download_id, "importMode": "Move"} + if not download_id: + data.pop("downloadClientId") + data = json.dumps(data) + try: + logger.debug("Opening URL: {0} with data: {1}".format(url, data), section) + r = requests.post(url, data=data, headers=headers, stream=True, verify=False, timeout=(30, 1800)) + except requests.ConnectionError: + logger.error("Unable to open URL: {0}".format(url), section) + return [1, "{0}: Failed to post-process - Unable to connect to {1}".format(section, section)] + + Success = False + Queued = False + Started = False + try: + res = json.loads(r.content) + scan_id = int(res['id']) + logger.debug("Scan started with id: {0}".format(scan_id), section) + Started = True + except Exception as e: + logger.warning("No scan id was returned due to: {0}".format(e), section) + scan_id = None + Started = False + return [1, "{0}: Failed to post-process - Unable to start scan".format(section)] + + n = 0 + params = {} + url = "{0}/{1}".format(url, scan_id) + while n < 6: # set up wait_for minutes to see if command completes.. + time.sleep(10 * wait_for) + command_status = self.command_complete(url, params, headers, section) + if command_status and command_status in ['completed', 'failed']: + break + n += 1 + if command_status: + logger.debug("The Scan command return status: {0}".format(command_status), section) + if not os.path.exists(dirName): + logger.debug("The directory {0} has been removed. Renaming was successful.".format(dirName), section) + return [0, "{0}: Successfully post-processed {1}".format(section, inputName)] + elif command_status and command_status in ['completed']: + logger.debug("The Scan command has completed successfully. Renaming was successful.", section) + return [0, "{0}: Successfully post-processed {1}".format(section, inputName)] + elif command_status and command_status in ['failed']: + logger.debug("The Scan command has failed. Renaming was not successful.", section) + # return [1, "%s: Failed to post-process %s" % (section, inputName) ] + if self.CDH(url2, headers, section=section): + logger.debug("The Scan command did not return status completed, but complete Download Handling is enabled. Passing back to {0}.".format(section), section) + return [status, "{0}: Complete DownLoad Handling is enabled. Passing back to {1}".format(section, section)] + else: + logger.warning("The Scan command did not return a valid status. Renaming was not successful.", section) + return [1, "{0}: Failed to post-process {1}".format(section, inputName)] + else: - logger.warning("FAILED DOWNLOAD DETECTED", section) - return [1, "{0}: Failed to post-process. {1} does not support failed downloads".format(section, section)] \ No newline at end of file + if section == "Lidarr": + logger.postprocess("FAILED: The download failed. Sending failed download to {0} for CDH processing".format(section), section) + return [1, "{0}: Download Failed. Sending back to {1}".format(section, section)] # Return as failed to flag this in the downloader. + else: + logger.warning("FAILED DOWNLOAD DETECTED", section) + if delete_failed and os.path.isdir(dirName) and not os.path.dirname(dirName) == dirName: + logger.postprocess("Deleting failed files and folder {0}".format(dirName), section) + rmDir(dirName) + return [1, "{0}: Failed to post-process. {1} does not support failed downloads".format(section, section)] # Return as failed to flag this in the downloader. \ No newline at end of file diff --git a/core/nzbToMediaConfig.py b/core/nzbToMediaConfig.py index 0b2222d3..369bc45c 100644 --- a/core/nzbToMediaConfig.py +++ b/core/nzbToMediaConfig.py @@ -263,6 +263,11 @@ class ConfigObj(configobj.ConfigObj, Section): logger.warning("{x} category is set for CouchPotato and Radarr. " "Please check your config in NZBGet".format (x=os.environ['NZBPO_RACATEGORY'])) + if 'NZBPO_LICATEGORY' in os.environ and 'NZBPO_HPCATEGORY' in os.environ: + if os.environ['NZBPO_LICATEGORY'] == os.environ['NZBPO_HPCATEGORY']: + logger.warning("{x} category is set for HeadPhones and Lidarr. " + "Please check your config in NZBGet".format + (x=os.environ['NZBPO_LICATEGORY'])) section = "Nzb" key = 'NZBOP_DESTDIR' if key in os.environ: @@ -413,6 +418,25 @@ class ConfigObj(configobj.ConfigObj, Section): if os.environ[envCatKey] in CFG_NEW['CouchPotato'].sections: CFG_NEW['CouchPotato'][envCatKey]['enabled'] = 0 + section = "Lidarr" + envCatKey = 'NZBPO_LICATEGORY' + envKeys = ['ENABLED', 'HOST', 'APIKEY', 'PORT', 'SSL', 'WEB_ROOT', 'WATCH_DIR', 'FORK', 'DELETE_FAILED', + 'TORRENT_NOLINK', 'NZBEXTRACTIONBY', 'WAIT_FOR', 'DELETE_FAILED', 'REMOTE_PATH'] + cfgKeys = ['enabled', 'host', 'apikey', 'port', 'ssl', 'web_root', 'watch_dir', 'fork', 'delete_failed', + 'Torrent_NoLink', 'nzbExtractionBy', 'wait_for', 'delete_failed', 'remote_path'] + if envCatKey in os.environ: + for index in range(len(envKeys)): + key = 'NZBPO_RA{index}'.format(index=envKeys[index]) + if key in os.environ: + option = cfgKeys[index] + value = os.environ[key] + if os.environ[envCatKey] not in CFG_NEW[section].sections: + CFG_NEW[section][os.environ[envCatKey]] = {} + CFG_NEW[section][os.environ[envCatKey]][option] = value + CFG_NEW[section][os.environ[envCatKey]]['enabled'] = 1 + if os.environ[envCatKey] in CFG_NEW['CouchPotato'].sections: + CFG_NEW['CouchPotato'][envCatKey]['enabled'] = 0 + section = "Extensions" envKeys = ['COMPRESSEDEXTENSIONS', 'MEDIAEXTENSIONS', 'METAEXTENSIONS'] cfgKeys = ['compressedExtensions', 'mediaExtensions', 'metaExtensions'] diff --git a/nzbToLidarr.py b/nzbToLidarr.py new file mode 100755 index 00000000..a11a5eca --- /dev/null +++ b/nzbToLidarr.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python2 +# coding=utf-8 +# +############################################################################## +### NZBGET POST-PROCESSING SCRIPT ### + +# Post-Process to Lidarr. +# +# 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 + +## Lidarr + +# Lidarr script category. +# +# category that gets called for post-processing with NzbDrone. +#liCategory=music2 + +# Lidarr host. +# +# The ipaddress for your Lidarr server. e.g For the Same system use localhost or 127.0.0.1 +#lihost=localhost + +# Lidarr port. +#liport=8686 + +# Lidarr API key. +#liapikey= + +# Lidarr uses ssl (0, 1). +# +# Set to 1 if using ssl, else set to 0. +#lissl=0 + +# Lidarr web_root +# +# set this if using a reverse proxy. +#liweb_root= + +# Lidarr wait_for +# +# Set the number of minutes to wait after calling the renamer, to check the episode has changed status. +#liwait_for=6 + +# Lidarr Delete Failed Downloads (0, 1). +# +# set to 1 to delete failed, or 0 to leave files in place. +#lidelete_failed=0 + +# Lidarr and NZBGet are a different system (0, 1). +# +# Enable to replace local path with the path as per the mountPoints below. +#liremote_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). +#niceness=10 + +# 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, 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=libmp3lame +#AudioCodecAllow = +#outputAudioBitrate=128k +#outputQualityPercent = 0 +#outputAudioTrack2Codec = libfaac +#AudioCodec2Allow = +#outputAudioTrack2Bitrate = 128k +#outputAudioOtherCodec = libmp3lame +#AudioOtherCodecAllow = +#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 ### +############################################################################## + +import sys +import nzbToMedia + +section = "Lidarr" +result = nzbToMedia.main(sys.argv, section) +sys.exit(result) diff --git a/nzbToMedia.py b/nzbToMedia.py index eadec7ee..b7717475 100755 --- a/nzbToMedia.py +++ b/nzbToMedia.py @@ -273,6 +273,49 @@ # Enable to replace local path with the path as per the mountPoints below. #hpremote_path=0 +## Lidarr + +# Lidarr script category. +# +# category that gets called for post-processing with NzbDrone. +#liCategory=music2 + +# Lidarr host. +# +# The ipaddress for your Lidarr server. e.g For the Same system use localhost or 127.0.0.1 +#lihost=localhost + +# Lidarr port. +#liport=8686 + +# Lidarr API key. +#liapikey= + +# Lidarr uses ssl (0, 1). +# +# Set to 1 if using ssl, else set to 0. +#lissl=0 + +# Lidarr web_root +# +# set this if using a reverse proxy. +#liweb_root= + +# Lidarr wait_for +# +# Set the number of minutes to wait after calling the renamer, to check the episode has changed status. +#liwait_for=6 + +# Lidarr Delete Failed Downloads (0, 1). +# +# set to 1 to delete failed, or 0 to leave files in place. +#lidelete_failed=0 + +# Lidarr and NZBGet are a different system (0, 1). +# +# Enable to replace local path with the path as per the mountPoints below. +#liremote_path=0 + ## Mylar # Mylar script category. @@ -672,7 +715,7 @@ def process(inputDirectory, inputName=None, status=0, clientAgent='manual', down elif sectionName in ["SickBeard", "NzbDrone", "Sonarr"]: result = autoProcessTV().processEpisode(sectionName, inputDirectory, inputName, status, clientAgent, download_id, inputCategory, failureLink) - elif sectionName == "HeadPhones": + elif sectionName in ["HeadPhones", "Lidarr"]: result = autoProcessMusic().process(sectionName, inputDirectory, inputName, status, clientAgent, inputCategory) elif sectionName == "Mylar": result = autoProcessComics().processEpisode(sectionName, inputDirectory, inputName, status, clientAgent, @@ -690,7 +733,7 @@ def process(inputDirectory, inputName=None, status=0, clientAgent='manual', down if clientAgent != 'manual': # update download status in our DB update_downloadInfoStatus(inputName, 1) - if sectionName not in ['UserScript', 'NzbDrone', 'Sonarr', 'Radarr']: + if sectionName not in ['UserScript', 'NzbDrone', 'Sonarr', 'Radarr', 'Lidarr']: # cleanup our processing folders of any misc unwanted files and empty directories cleanDir(inputDirectory, sectionName, inputCategory) @@ -763,6 +806,8 @@ def main(args, section=None): download_id = os.environ['NZBPR_SONARR'] elif 'NZBPR_RADARR' in os.environ: download_id = os.environ['NZBPR_RADARR'] + elif 'NZBPR_LIDARR' in os.environ: + download_id = os.environ['NZBPR_LIDARR'] if 'NZBPR__DNZB_FAILURE' in os.environ: failureLink = os.environ['NZBPR__DNZB_FAILURE']