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