From a24eac69ae8f7b3ca171e66d160ec6a0bbf46689 Mon Sep 17 00:00:00 2001 From: Nic Wolfe <nic@wolfeden.ca> Date: Mon, 19 Mar 2012 23:23:24 -0600 Subject: [PATCH] Did some minor code cleanup and added a bunch of comments. --- sickbeard/notifiers/boxcar.py | 68 ++++++++++++++++----- sickbeard/notifiers/nmj.py | 38 +++++++++++- sickbeard/notifiers/notifo.py | 44 +++++++++++++- sickbeard/notifiers/trakt.py | 46 +++++++++++++-- sickbeard/postProcessor.py | 108 ++++++++++++++++++++++++++++++++-- sickbeard/processTV.py | 7 +++ sickbeard/sab.py | 74 +++++++++++++++-------- 7 files changed, 334 insertions(+), 51 deletions(-) diff --git a/sickbeard/notifiers/boxcar.py b/sickbeard/notifiers/boxcar.py index 3e1ac37e5..35a86942d 100644 --- a/sickbeard/notifiers/boxcar.py +++ b/sickbeard/notifiers/boxcar.py @@ -34,38 +34,66 @@ class BoxcarNotifier: return self._sendBoxcar("This is a test notification from SickBeard", title, email) def _sendBoxcar(self, msg, title, email, subscribe=False): + """ + Sends a boxcar notification to the address provided + + msg: The message to send (unicode) + title: The title of the message + email: The email address to send the message to (or to subscribe with) + subscribe: If true then instead of sending a message this function will send a subscription notification (optional, default is False) + + returns: True if the message succeeded, False otherwise + """ + + # build up the URL and parameters msg = msg.strip() curUrl = API_URL - data = urllib.urlencode({ + + # if this is a subscription notification then act accordingly + if subscribe: + data = urllib.urlencode({'email': email}) + curUrl = curUrl + "/subscribe" + + # for normal requests we need all these parameters + else: + data = urllib.urlencode({ 'email': email, 'notification[from_screen_name]': title, 'notification[message]': msg.encode('utf-8'), 'notification[from_remote_service_id]': int(time.time()) }) - if subscribe: # subscription notification - data = urllib.urlencode({'email': email}) - curUrl = curUrl + "/subscribe" - req = urllib2.Request(curUrl) + + # send the request to boxcar try: + req = urllib2.Request(curUrl) handle = urllib2.urlopen(req, data) handle.close() + except urllib2.URLError, e: + # if we get an error back that doesn't have an error code then who knows what's really happening if not hasattr(e, 'code'): logger.log("Boxcar notification failed." + ex(e), logger.ERROR) return False else: logger.log("Boxcar notification failed. Error code: " + str(e.code), logger.WARNING) - if e.code == 404: #HTTP status 404 if the provided email address isn't a Boxcar user. + # HTTP status 404 if the provided email address isn't a Boxcar user. + if e.code == 404: logger.log("Username is wrong/not a boxcar email. Boxcar will send an email to it", logger.WARNING) return False - elif e.code == 401: #For HTTP status code 401's, it is because you are passing in either an invalid token, or the user has not added your service. - if subscribe: #If the user has already added your service, we'll return an HTTP status code of 401. + + # For HTTP status code 401's, it is because you are passing in either an invalid token, or the user has not added your service. + elif e.code == 401: + + # If the user has already added your service, we'll return an HTTP status code of 401. + if subscribe: logger.log("Already subscribed to service", logger.ERROR) # i dont know if this is true or false ... its neither but i also dont know how we got here in the first place return False - else: #HTTP status 401 if the user doesn't have the service added + + #HTTP status 401 if the user doesn't have the service added + else: subscribeNote = self._sendBoxcar(msg, title, email, True) if subscribeNote: logger.log("Subscription send", logger.DEBUG) @@ -73,12 +101,14 @@ class BoxcarNotifier: else: logger.log("Subscription could not be send", logger.ERROR) return False - elif e.code == 400: #If you receive an HTTP status code of 400, it is because you failed to send the proper parameters + + # If you receive an HTTP status code of 400, it is because you failed to send the proper parameters + elif e.code == 400: logger.log("Wrong data send to boxcar", logger.ERROR) return False - else:# 200 - logger.log("Boxcar notification successful.", logger.DEBUG) - return True + + logger.log("Boxcar notification successful.", logger.DEBUG) + return True def notify_snatch(self, ep_name, title=notifyStrings[NOTIFY_SNATCH]): if sickbeard.BOXCAR_NOTIFY_ONSNATCH: @@ -89,11 +119,21 @@ class BoxcarNotifier: if sickbeard.BOXCAR_NOTIFY_ONDOWNLOAD: self._notifyBoxcar(title, ep_name) - def _notifyBoxcar(self, title, message=None, username=None, force=False): + def _notifyBoxcar(self, title, message, username=None, force=False): + """ + Sends a boxcar notification based on the provided info or SB config + + title: The title of the notification to send + message: The message string to send + username: The username to send the notification to (optional, defaults to the username in the config) + force: If True then the notification will be sent even if Boxcar is disabled in the config + """ + if not sickbeard.USE_BOXCAR and not force: logger.log("Notification for Boxcar not enabled, skipping this notification", logger.DEBUG) return False + # if no username was given then use the one from the config if not username: username = sickbeard.BOXCAR_USERNAME diff --git a/sickbeard/notifiers/nmj.py b/sickbeard/notifiers/nmj.py index de074f048..b84ae816c 100644 --- a/sickbeard/notifiers/nmj.py +++ b/sickbeard/notifiers/nmj.py @@ -31,6 +31,15 @@ except ImportError: class NMJNotifier: def notify_settings(self, host): + """ + Retrieves the settings from a NMJ/Popcorn hour + + host: The hostname/IP of the Popcorn Hour server + + Returns: True if the settings were retrieved successfully, False otherwise + """ + + # establish a terminal session to the PC terminal = False try: terminal = telnetlib.Telnet(host) @@ -38,6 +47,7 @@ class NMJNotifier: logger.log(u"Warning: unable to get a telnet session to %s" % (host), logger.ERROR) return False + # tell the terminal to output the necessary info to the screen so we can search it later logger.log(u"Connected to %s via telnet" % (host), logger.DEBUG) terminal.read_until("sh-3.00# ") terminal.write("cat /tmp/source\n") @@ -49,6 +59,7 @@ class NMJNotifier: device = "" match = re.search(r"(.+\.db)\r\n?(.+)(?=sh-3.00# cat /tmp/netshare)", tnoutput) + # if we found the database in the terminal output then save that database to the config if match: database = match.group(1) device = match.group(2) @@ -57,7 +68,8 @@ class NMJNotifier: else: logger.log(u"Could not get current NMJ database on %s, NMJ is probably not running!" % (host), logger.ERROR) return False - + + # if the device is a remote host then try to parse the mounting URL and save it to the config if device.startswith("NETWORK_SHARE/"): match = re.search(".*(?=\r\n?%s)" % (re.escape(device[14:])), tnoutput) @@ -82,6 +94,17 @@ class NMJNotifier: return self._sendNMJ(host, database, mount) def _sendNMJ(self, host, database, mount=None): + """ + Sends a NMJ update command to the specified machine + + host: The hostname/IP to send the request to (no port) + database: The database to send the requst to + mount: The mount URL to use (optional) + + Returns: True if the request succeeded, False otherwise + """ + + # if a mount URL is provided then attempt to open a handle to that URL if mount: try: req = urllib2.Request(mount) @@ -91,6 +114,7 @@ class NMJNotifier: logger.log(u"Warning: Couldn't contact popcorn hour on host %s: %s" % (host, e)) return False + # build up the request URL and parameters UPDATE_URL = "http://%(host)s:8008/metadata_database?%(params)s" params = { "arg0": "scanner_start", @@ -100,6 +124,7 @@ class NMJNotifier: params = urllib.urlencode(params) updateUrl = UPDATE_URL % {"host": host, "params": params} + # send the request to the server try: req = urllib2.Request(updateUrl) logger.log(u"Sending NMJ scan update command via url: %s" % (updateUrl), logger.DEBUG) @@ -109,6 +134,7 @@ class NMJNotifier: logger.log(u"Warning: Couldn't contact Popcorn Hour on host %s: %s" % (host, e)) return False + # try to parse the resulting XML try: et = etree.fromstring(response) result = et.findtext("returnValue") @@ -116,6 +142,7 @@ class NMJNotifier: logger.log(u"Unable to parse XML returned from the Popcorn Hour: %s" % (e), logger.ERROR) return False + # if the result was a number then consider that an error if int(result) > 0: logger.log(u"Popcorn Hour returned an errorcode: %s" % (result)) return False @@ -124,10 +151,19 @@ class NMJNotifier: return True def _notifyNMJ(self, host=None, database=None, mount=None, force=False): + """ + Sends a NMJ update command based on the SB config settings + + host: The host to send the command to (optional, defaults to the host in the config) + database: The database to use (optional, defaults to the database in the config) + mount: The mount URL (optional, defaults to the mount URL in the config) + force: If True then the notification will be sent even if NMJ is disabled in the config + """ if not sickbeard.USE_NMJ and not force: logger.log("Notification for NMJ scan update not enabled, skipping this notification", logger.DEBUG) return False + # fill in omitted parameters if not host: host = sickbeard.NMJ_HOST if not database: diff --git a/sickbeard/notifiers/notifo.py b/sickbeard/notifiers/notifo.py index 59c0d1ee2..11337e070 100644 --- a/sickbeard/notifiers/notifo.py +++ b/sickbeard/notifiers/notifo.py @@ -37,7 +37,22 @@ class NotifoNotifier: return self._sendNotifo("This is a test notification from SickBeard", title, username, apisecret) def _sendNotifo(self, msg, title, username, apisecret, label="SickBeard"): + """ + Sends a message to notify using the given authentication information + + msg: The string to send to notifo + title: The title of the message + username: The username to send it to + apisecret: The API key for the username + label: The label to use for the message (optional) + + Returns: True if the message was delivered, False otherwise + """ + + # tidy up the message msg = msg.strip() + + # build up the URL and parameters apiurl = API_URL % {"username": username, "secret": apisecret} data = urllib.urlencode({ "title": title, @@ -45,18 +60,22 @@ class NotifoNotifier: "msg": msg.encode(sickbeard.SYS_ENCODING) }) + # send the request to notifo try: data = urllib.urlopen(apiurl, data) result = json.load(data) + except ValueError, e: logger.log(u"Unable to decode JSON: "+data, logger.ERROR) return False + except IOError, e: logger.log(u"Error trying to communicate with notifo: "+ex(e), logger.ERROR) return False data.close() + # see if it worked if result["status"] != "success" or result["response_message"] != "OK": return False else: @@ -64,14 +83,37 @@ class NotifoNotifier: def notify_snatch(self, ep_name, title="Snatched:"): + """ + Send a notification that an episode was snatched + + ep_name: The name of the episode that was snatched + title: The title of the notification (optional) + """ if sickbeard.NOTIFO_NOTIFY_ONSNATCH: self._notifyNotifo(title, ep_name) def notify_download(self, ep_name, title="Completed:"): + """ + Send a notification that an episode was downloaded + + ep_name: The name of the episode that was downloaded + title: The title of the notification (optional) + """ if sickbeard.NOTIFO_NOTIFY_ONDOWNLOAD: self._notifyNotifo(title, ep_name) - def _notifyNotifo(self, title, message=None, username=None, apisecret=None, force=False): + def _notifyNotifo(self, title, message, username=None, apisecret=None, force=False): + """ + Send a notifo notification based on the SB settings. + + title: The title to send + message: The message to send + username: The username to send it to (optional, default to the username in the config) + apisecret: The API key to use (optional, defaults to the api key in the config) + force: If true then the notification will be sent even if it is disabled in the config (optional) + + Returns: True if the message succeeded, false otherwise + """ if not sickbeard.USE_NOTIFO and not force: logger.log("Notification for Notifo not enabled, skipping this notification", logger.DEBUG) return False diff --git a/sickbeard/notifiers/trakt.py b/sickbeard/notifiers/trakt.py index aaea6116c..d5bcb6515 100644 --- a/sickbeard/notifiers/trakt.py +++ b/sickbeard/notifiers/trakt.py @@ -30,12 +30,10 @@ import sickbeard from sickbeard import logger -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree #@UnusedImport - class TraktNotifier: + """ + A "notifier" for trakt.tv which keeps track of what has and hasn't been added to your library. + """ def notify_snatch(self, ep_name): pass @@ -44,10 +42,17 @@ class TraktNotifier: pass def update_library(self, ep_obj): + """ + Sends a request to trakt indicating that the given episode is part of our library. + + ep_obj: The TVEpisode object to add to trakt + """ + if sickbeard.USE_TRAKT: method = "show/episode/library/" method += "%API%" + # URL parameters data = { 'tvdb_id': ep_obj.show.tvdbid, 'title': ep_obj.show.name, @@ -62,6 +67,17 @@ class TraktNotifier: self._notifyTrakt(method, None, None, None, data) def test_notify(self, api, username, password): + """ + Sends a test notification to trakt with the given authentication info and returns a boolean + representing success. + + api: The api string to use + username: The username to use + password: The password to use + + Returns: True if the request succeeded, False otherwise + """ + method = "account/test/" method += "%API%" return self._notifyTrakt(method, api, username, password, {}) @@ -79,23 +95,42 @@ class TraktNotifier: return sickbeard.USE_TRAKT def _notifyTrakt(self, method, api, username, password, data = {}): + """ + A generic method for communicating with trakt. Uses the method and data provided along + with the auth info to send the command. + + method: The URL to use at trakt, relative, no leading slash. + api: The API string to provide to trakt + username: The username to use when logging in + password: The unencrypted password to use when logging in + + Returns: A boolean representing success + """ logger.log("trakt_notifier: Call method " + method, logger.DEBUG) + # if the API isn't given then use the config API if not api: api = self._api() + + # if the username isn't given then use the config username if not username: username = self._username() + + # if the password isn't given then use the config password if not password: password = self._password() password = sha1(password).hexdigest() + # replace the API string with what we found method = method.replace("%API%", api) data["username"] = username data["password"] = password + # take the URL params and make a json object out of them encoded_data = json.dumps(data); + # request the URL from trakt and parse the result as json try: logger.log("trakt_notifier: Calling method http://api.trakt.tv/" + method + ", with data" + encoded_data, logger.DEBUG) stream = urllib2.urlopen("http://api.trakt.tv/" + method, encoded_data) @@ -105,6 +140,7 @@ class TraktNotifier: if ("error" in resp): raise Exception(resp["error"]) + except (IOError): logger.log("trakt_notifier: Failed calling method", logger.ERROR) return False diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index 0d7c8c1f3..7539779df 100755 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -45,6 +45,9 @@ from sickbeard.name_parser.parser import NameParser, InvalidNameException from lib.tvdb_api import tvdb_api, tvdb_exceptions class PostProcessor(object): + """ + A class which will process a media file according to the post processing settings in the config. + """ EXISTS_LARGER = 1 EXISTS_SAME = 2 @@ -52,6 +55,12 @@ class PostProcessor(object): DOESNT_EXIST = 4 def __init__(self, file_path, nzb_name = None): + """ + Creates a new post processor with the given file path and optionally an NZB name. + + file_path: The path to the file to be processed + nzb_name: The name of the NZB which resulted in this file being downloaded (optional) + """ # absolute path to the folder that is being processed self.folder_path = ek.ek(os.path.dirname, ek.ek(os.path.abspath, file_path)) @@ -74,10 +83,28 @@ class PostProcessor(object): self.log = '' def _log(self, message, level=logger.MESSAGE): + """ + A wrapper for the internal logger which also keeps track of messages and saves them to a string for later. + + message: The string to log (unicode) + level: The log level to use (optional) + """ logger.log(message, level) self.log += message + '\n' def _checkForExistingFile(self, existing_file): + """ + Checks if a file exists already and if it does whether it's bigger or smaller than + the file we are post processing + + existing_file: The file to compare to + + Returns: + DOESNT_EXIST if the file doesn't exist + EXISTS_LARGER if the file exists and is larger than the file we are post processing + EXISTS_SMALLER if the file exists and is smaller than the file we are post processing + EXISTS_SAME if the file exists and is the same size as the file we are post processing + """ if not existing_file: self._log(u"There is no existing file so there's no worries about replacing it", logger.DEBUG) @@ -104,6 +131,13 @@ class PostProcessor(object): return PostProcessor.DOESNT_EXIST def _list_associated_files(self, file_path): + """ + For a given file path searches for files with the same name but different extension and returns them. + + file_path: The file to check for associated files + + Returns: A list containing all files which are associated to the given file + """ if not file_path: return [] @@ -125,10 +159,17 @@ class PostProcessor(object): return file_path_list def _delete(self, file_path, associated_files=False): + """ + Deletes the file and optionall all associated files. + + file_path: The file to delete + associated_files: True to delete all files which differ only by extension, False to leave them + """ if not file_path: return + # figure out which files we want to delete if associated_files: file_list = self._list_associated_files(file_path) else: @@ -138,6 +179,7 @@ class PostProcessor(object): self._log(u"There were no files associated with "+file_path+", not deleting anything", logger.DEBUG) return + # delete the file and any other files which we want to delete for cur_file in file_list: self._log(u"Deleting file "+cur_file, logger.DEBUG) if ek.ek(os.path.isfile, cur_file): @@ -145,8 +187,11 @@ class PostProcessor(object): def _combined_file_operation (self, file_path, new_path, new_base_name, associated_files=False, action=None): """ - file_path: The full path of the media file to copy - new_path: Destination path where we want to copy the file to + Performs a generic operation (move or copy) on a file. Can rename the file as well as change its location, + and optionally move associated files too. + + file_path: The full path of the media file to act on + new_path: Destination path where we want to move/copy the file to new_base_name: The base filename (no extension) to use during the copy. Use None to keep the same name. associated_files: Boolean, whether we should copy similarly-named files too action: function that takes an old path and new path and does an operation with them (move/copy) @@ -228,6 +273,14 @@ class PostProcessor(object): self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_copy) def _find_ep_destination_folder(self, ep_obj): + """ + Finds the final folder where the episode should go. If season folders are enabled + and an existing season folder can be found then it is used, otherwise a new one + is created in accordance with the config settings. If season folders aren't enabled + then this function should simply return the show dir. + + ep_obj: The TVEpisode object to figure out the location for + """ # if we're supposed to put it in a season folder then figure out what folder to use season_folder = '' @@ -271,10 +324,12 @@ class PostProcessor(object): to_return = (None, None, []) + # if we don't have either of these then there's nothing to use to search the history for anyway if not self.nzb_name and not self.folder_name: self.in_history = False return to_return + # make a list of possible names to use in the search names = [] if self.nzb_name: names.append(self.nzb_name) @@ -285,6 +340,7 @@ class PostProcessor(object): myDB = db.DBConnection() + # search the database for a possible match and return immediately if we find one for curName in names: sql_results = myDB.select("SELECT * FROM history WHERE resource LIKE ?", [re.sub("[\.\-\ ]", "_", curName)]) @@ -306,6 +362,8 @@ class PostProcessor(object): """ Takes a name and tries to figure out a show, season, and episode from it. + name: A string which we want to analyze to determine show info from (unicode) + Returns a (tvdb_id, season, [episodes]) tuple. The first two may be None and episodes may be [] if none were found. """ @@ -486,6 +544,16 @@ class PostProcessor(object): return (tvdb_id, season, episodes) def _get_ep_obj(self, tvdb_id, season, episodes): + """ + Retrieve the TVEpisode object requested. + + tvdb_id: The TVDBID of the show (int) + season: The season of the episode (int) + episodes: A list of episodes to find (list of ints) + + If the episode(s) can be found then a TVEpisode object with the correct related eps will + be instantiated and returned. If the episode can't be found then None will be returned. + """ show_obj = None @@ -496,6 +564,7 @@ class PostProcessor(object): except exceptions.MultipleShowObjectsException: raise #TODO: later I'll just log this, for now I want to know about it ASAP + # if we can't find the show then there's nothing we can really do if not show_obj: self._log(u"This show isn't in your list, you need to add it to SB before post-processing an episode", logger.ERROR) raise exceptions.PostProcessingFailed() @@ -513,6 +582,7 @@ class PostProcessor(object): self._log(u"Unable to create episode: "+ex(e), logger.DEBUG) raise exceptions.PostProcessingFailed() + # associate all the episodes together under a single root episode if root_ep == None: root_ep = curEp root_ep.relatedEps = [] @@ -522,22 +592,34 @@ class PostProcessor(object): return root_ep def _get_quality(self, ep_obj): + """ + Determines the quality of the file that is being post processed, first by checking if it is directly + available in the TVEpisode's status or otherwise by parsing through the data available. + + ep_obj: The TVEpisode object related to the file we are post processing + + Returns: A quality value found in common.Quality + """ ep_quality = common.Quality.UNKNOWN - # make sure the quality is set right before we continue + # if there is a quality available in the status then we don't need to bother guessing from the filename if ep_obj.status in common.Quality.SNATCHED + common.Quality.SNATCHED_PROPER: oldStatus, ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) #@UnusedVariable if ep_quality != common.Quality.UNKNOWN: self._log(u"The old status had a quality in it, using that: "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) return ep_quality + # nzb name is the most reliable if it exists, followed by folder name and lastly file name name_list = [self.nzb_name, self.folder_name, self.file_name] # search all possible names for our new quality, in case the file or dir doesn't have it for cur_name in name_list: + + # some stuff might be None at this point still if not cur_name: continue + ep_quality = common.Quality.nameQuality(cur_name) self._log(u"Looking up quality for name "+cur_name+u", got "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) @@ -556,8 +638,17 @@ class PostProcessor(object): return ep_quality def _run_extra_scripts(self, ep_obj): + """ + Executes any extra scripts defined in the config. + + ep_obj: The object to use when calling the extra script + """ for curScriptName in sickbeard.EXTRA_SCRIPTS: + + # generate a safe command line string to execute the script and provide all the parameters script_cmd = shlex.split(curScriptName) + [ep_obj.location, self.file_path, str(ep_obj.show.tvdbid), str(ep_obj.season), str(ep_obj.episode), str(ep_obj.airdate)] + + # use subprocess to run the command and capture output self._log(u"Executing command "+str(script_cmd)) self._log(u"Absolute path to script: "+ek.ek(os.path.abspath, script_cmd[0]), logger.DEBUG) try: @@ -568,6 +659,15 @@ class PostProcessor(object): self._log(u"Unable to run extra_script: "+ex(e)) def _is_priority(self, ep_obj, new_ep_quality): + """ + Determines if the episode is a priority download or not (if it is expected). Episodes which are expected + (snatched) or larger than the existing episode are priority, others are not. + + ep_obj: The TVEpisode object in question + new_ep_quality: The quality of the episode that is being processed + + Returns: True if the episode is priority, False otherwise. + """ # if SB downloaded this on purpose then this is a priority download if self.in_history or ep_obj.status in common.Quality.SNATCHED + common.Quality.SNATCHED_PROPER: @@ -726,5 +826,3 @@ class PostProcessor(object): self._run_extra_scripts(ep_obj) return True - - # e diff --git a/sickbeard/processTV.py b/sickbeard/processTV.py index 95425f2c6..57c41569d 100644 --- a/sickbeard/processTV.py +++ b/sickbeard/processTV.py @@ -35,6 +35,13 @@ def logHelper (logMessage, logLevel=logger.MESSAGE): return logMessage + u"\n" def processDir (dirName, nzbName=None, recurse=False): + """ + Scans through the files in dirName and processes whatever media files it finds + + dirName: The folder name to look in + nzbName: The NZB name which resulted in this folder being downloaded + recurse: Boolean for whether we should descend into subfolders or not + """ returnStr = '' diff --git a/sickbeard/sab.py b/sickbeard/sab.py index 252d739cd..880e222d3 100644 --- a/sickbeard/sab.py +++ b/sickbeard/sab.py @@ -35,9 +35,14 @@ from sickbeard import logger from sickbeard.exceptions import ex def sendNZB(nzb): + """ + Sends an NZB to SABnzbd via the API. + + nzb: The NZBSearchResult object to send to SAB + """ + # set up a dict with the URL params in it params = {} - if sickbeard.SAB_USERNAME != None: params['ma_username'] = sickbeard.SAB_USERNAME if sickbeard.SAB_PASSWORD != None: @@ -58,7 +63,7 @@ def sendNZB(nzb): if nzb.provider.getID() == 'newzbin': id = nzb.provider.getIDFromURL(nzb.url) if not id: - logger.log("Unable to send NZB to sab, can't find ID in URL "+str(nzb.url), logger.ERROR) + logger.log("Unable to send NZB to sab, can't find ID in URL " + str(nzb.url), logger.ERROR) return False params['mode'] = 'addid' params['name'] = id @@ -69,23 +74,23 @@ def sendNZB(nzb): # if we get a raw data result we want to upload it to SAB elif nzb.resultType == "nzbdata": params['mode'] = 'addfile' - multiPartParams = {"nzbfile": (nzb.name+".nzb", nzb.extraInfo[0])} + multiPartParams = {"nzbfile": (nzb.name + ".nzb", nzb.extraInfo[0])} url = sickbeard.SAB_HOST + "api?" + urllib.urlencode(params) logger.log(u"Sending NZB to SABnzbd") - logger.log(u"URL: " + url, logger.DEBUG) try: - + # if we have the URL to an NZB then we've built up the SAB API URL already so just call it if nzb.resultType == "nzb": - f = urllib.urlopen(url) + f = urllib.urlopen(url) + + # if we are uploading the NZB data to SAB then we need to build a little POST form and send it elif nzb.resultType == "nzbdata": cookies = cookielib.CookieJar() opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler.MultipartPostHandler) - req = urllib2.Request(url, multiPartParams, headers={'User-Agent': USER_AGENT}) @@ -93,31 +98,36 @@ def sendNZB(nzb): f = opener.open(req) except (EOFError, IOError), e: - logger.log(u"Unable to connect to SAB: "+ex(e), logger.ERROR) + logger.log(u"Unable to connect to SAB: " + ex(e), logger.ERROR) return False except httplib.InvalidURL, e: - logger.log(u"Invalid SAB host, check your config: "+ex(e), logger.ERROR) + logger.log(u"Invalid SAB host, check your config: " + ex(e), logger.ERROR) return False + # this means we couldn't open the connection or something just as bad if f == None: logger.log(u"No data returned from SABnzbd, NZB not sent", logger.ERROR) return False + # if we opened the URL connection then read the result from SAB try: result = f.readlines() except Exception, e: logger.log(u"Error trying to get result from SAB, NZB not sent: " + ex(e), logger.ERROR) return False + # SAB shouldn't return a blank result, this most likely (but not always) means that it timed out and didn't recieve the NZB if len(result) == 0: logger.log(u"No data returned from SABnzbd, NZB not sent", logger.ERROR) return False + # massage the result a little bit sabText = result[0].strip() logger.log(u"Result text from SAB: " + sabText, logger.DEBUG) + # do some crude parsing of the result text to determine what SAB said if sabText == "ok": logger.log(u"NZB sent to SAB successfully", logger.DEBUG) return True @@ -133,11 +143,11 @@ def _checkSabResponse(f): result = f.readlines() except Exception, e: logger.log(u"Error trying to get result from SAB" + ex(e), logger.ERROR) - return False,"Error from SAB" + return False, "Error from SAB" if len(result) == 0: logger.log(u"No data returned from SABnzbd, NZB not sent", logger.ERROR) - return False,"No data from SAB" + return False, "No data from SAB" sabText = result[0].strip() sabJson = {} @@ -148,22 +158,22 @@ def _checkSabResponse(f): if sabText == "Missing authentication": logger.log(u"Incorrect username/password sent to SAB", logger.ERROR) - return False,"Incorrect username/password sent to SAB" + return False, "Incorrect username/password sent to SAB" elif 'error' in sabJson: logger.log(sabJson['error'], logger.ERROR) - return False,sabJson['error'] + return False, sabJson['error'] else: - return True,sabText + return True, sabText def _sabURLOpenSimple(url): try: f = urllib.urlopen(url) except (EOFError, IOError), e: - logger.log(u"Unable to connect to SAB: "+ex(e), logger.ERROR) - return False,"Unable to connect" + logger.log(u"Unable to connect to SAB: " + ex(e), logger.ERROR) + return False, "Unable to connect" except httplib.InvalidURL, e: - logger.log(u"Invalid SAB host, check your config: "+ex(e), logger.ERROR) - return False,"Invalid SAB host" + logger.log(u"Invalid SAB host, check your config: " + ex(e), logger.ERROR) + return False, "Invalid SAB host" if f == None: logger.log(u"No data returned from SABnzbd", logger.ERROR) return False, "No data returned from SABnzbd" @@ -175,31 +185,45 @@ def getSabAccesMethod(host=None, username=None, password=None, apikey=None): result, f = _sabURLOpenSimple(url) if not result: - return False,f + return False, f result, sabText = _checkSabResponse(f) if not result: - return False,sabText + return False, sabText - return True,sabText + return True, sabText def testAuthentication(host=None, username=None, password=None, apikey=None): + """ + Sends a simple API request to SAB to determine if the given connection information is connect + + host: The host where SAB is running (incl port) + username: The username to use for the HTTP request + password: The password to use for the HTTP request + apikey: The API key to provide to SAB + + Returns: A tuple containing the success boolean and a message + """ + + # build up the URL parameters params = {} params['mode'] = 'queue' - params['output'] ='json' + params['output'] = 'json' params['ma_username'] = username params['ma_password'] = password params['apikey'] = apikey url = host + "api?" + urllib.urlencode(params) + # send the test request logger.log(u"SABnzbd test URL: " + url, logger.DEBUG) result, f = _sabURLOpenSimple(url) if not result: - return False,f + return False, f + # check the result and determine if it's good or not result, sabText = _checkSabResponse(f) if not result: - return False,sabText + return False, sabText - return True,"Success" + return True, "Success" -- GitLab