text: #!/usr/bin/python3 -OO
  # Copyright 2007-2019 The SABnzbd-Team <team@sabnzbd.org>
  #
  # This program is free software; you can redistribute it and/or
  # modify it under the terms of the GNU General Public License
  # as published by the Free Software Foundation; either version 2
  # of the License, or (at your option) any later version.
  #
  # This program is distributed in the hope that it will be useful,
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  # GNU General Public License for more details.
  #
  # You should have received a copy of the GNU General Public License
  # along with this program; if not, write to the Free Software
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  
  """
  sabnzbd.sorting - Sorting Functions
  Series Sorting - Sorting downloads into seasons & episodes
  Date Sorting - Sorting downloads by a custom date matching
  Generic Sorting - Sorting large files by a custom matching
  """
  
  import os
  import logging
  import re
  
  import sabnzbd
  from sabnzbd.filesystem import move_to_path, cleanup_empty_directories, get_unique_path, \
      get_unique_filename, get_ext, renamer, sanitize_foldername, clip_path
  from sabnzbd.constants import series_match, date_match, year_match, sample_match
  import sabnzbd.cfg as cfg
  
  RE_SAMPLE = re.compile(sample_match, re.I)
  # Do not rename .vob files as they are usually DVD's
  EXCLUDED_FILE_EXTS = ('.vob', '.bin')
  
  LOWERCASE = ('the', 'of', 'and', 'at', 'vs', 'a', 'an', 'but', 'nor', 'for', 'on',
                           'so', 'yet', 'with')
  UPPERCASE = ('III', 'II', 'IV')
  
  REPLACE_AFTER = {
      '()': '',
      '..': '.',
      '__': '_',
      '  ': ' ',
      ' .%ext': '.%ext'
  }
  
  # Title() function messes up country names, so need to replace them instead
  COUNTRY_REP = ('(US)', '(UK)', '(EU)', '(CA)', '(YU)', '(VE)', '(TR)', '(CH)',
                 '(SE)', '(ES)', '(KR)', '(ZA)', '(SK)', '(SG)', '(RU)', '(RO)',
                 '(PR)', '(PT)', '(PL)', '(PH)', '(PK)', '(NO)', '(NG)', '(NZ)',
                 '(NL)', '(MX)', '(MY)', '(MK)', '(KZ)', '(JP)', '(JM)', '(IT)',
                 '(IL)', '(IE)', '(IN)', '(IS)', '(HU)', '(HK)', '(HN)', '(GR)',
                 '(GH)', '(DE)', '(FR)', '(FI)', '(DK)', '(CZ)', '(HR)', '(CR)',
                 '(CO)', '(CN)', '(CL)', '(BG)', '(BR)', '(BE)', '(AT)', '(AU)',
                 '(AW)', '(AR)', '(AL)', '(AF)')
  
  
  def ends_in_file(path):
      """ Return True when path ends with '.%ext' or '%fn' """
      _RE_ENDEXT = re.compile(r'\.%ext[{}]*$', re.I)
      _RE_ENDFN = re.compile(r'%fn[{}]*$', re.I)
      return bool(_RE_ENDEXT.search(path) or _RE_ENDFN.search(path))
  
  
  def move_to_parent_folder(workdir):
      """ Move all in 'workdir' into 'workdir/..' """
      # Determine 'folder'/..
      workdir = os.path.abspath(os.path.normpath(workdir))
      dest = os.path.abspath(os.path.normpath(os.path.join(workdir, '..')))
  
      # Check for DVD folders and stop if found
      for item in os.listdir(workdir):
          if item.lower() in ('video_ts', 'audio_ts', 'bdmv'):
              return workdir, True
  
      for root, dirs, files in os.walk(workdir):
          for _file in files:
              path = os.path.join(root, _file)
              new_path = path.replace(workdir, dest)
              ok, new_path = move_to_path(path, new_path)
              if not ok:
                  return dest, False
  
      cleanup_empty_directories(workdir)
      return dest, True
  
  
  class Sorter:
      """ Generic Sorter class """
  
      def __init__(self, nzo, cat):
          self.sorter = None
          self.type = None
          self.sort_file = False
          self.nzo = nzo
          self.cat = cat
          self.ext = ''
  
      def detect(self, job_name, complete_dir):
          """ Detect which kind of sort applies """
          self.sorter = SeriesSorter(self.nzo, job_name, complete_dir, self.cat)
          if self.sorter.matched:
              complete_dir = self.sorter.get_final_path()
              self.type = 'tv'
              self.sort_file = True
              return complete_dir
  
          self.sorter = DateSorter(self.nzo, job_name, complete_dir, self.cat)
          if self.sorter.matched:
              complete_dir = self.sorter.get_final_path()
              self.type = 'date'
              self.sort_file = True
              return complete_dir
  
          self.sorter = MovieSorter(self.nzo, job_name, complete_dir, self.cat)
          if self.sorter.matched:
              complete_dir = self.sorter.get_final_path()
              self.type = 'movie'
              self.sort_file = True
              return complete_dir
  
          self.sort_file = False
          return complete_dir
  
      def rename(self, newfiles, workdir_complete):
          """ Rename files of the job """
          if self.sorter.rename_or_not:
              self.sorter.rename(newfiles, workdir_complete)
  
      def rename_with_ext(self, workdir_complete):
          """ Special renamer for %ext """
          if self.sorter.rename_or_not and '%ext' in workdir_complete and self.ext:
              # Replace %ext with extension
              newpath = workdir_complete.replace('%ext', self.ext)
              try:
                  renamer(workdir_complete, newpath)
              except:
                  return newpath, False
              return newpath, True
          else:
              return workdir_complete, True
  
      def move(self, workdir_complete):
          ok = True
          if self.type == 'movie':
              move_to_parent = True
              # check if we should leave the files inside an extra folder
              if cfg.movie_extra_folders():
                  # if there is a folder in the download, leave it in an extra folder
                  move_to_parent = not check_for_folder(workdir_complete)
              if move_to_parent:
                  workdir_complete, ok = move_to_parent_folder(workdir_complete)
          else:
              workdir_complete, ok = move_to_parent_folder(workdir_complete)
          if not ok:
              return workdir_complete, False
  
          path, part = os.path.split(workdir_complete)
          if '%fn' in part and self.sorter.fname:
              old = workdir_complete
              workdir_complete = os.path.join(path, part.replace('%fn', self.sorter.fname))
              workdir_complete = get_unique_path(workdir_complete, create_dir=False)
              try:
                  renamer(old, workdir_complete)
              except:
                  logging.error(T('Cannot create directory %s'), clip_path(workdir_complete))
                  workdir_complete = old
                  ok = False
          return workdir_complete, ok
  
  
  class SeriesSorter:
      """ Methods for Series Sorting """
  
      def __init__(self, nzo, job_name, path, cat):
          self.matched = False
  
          self.original_job_name = job_name
          self.original_path = path
          self.nzo = nzo
          self.cat = cat
          self.sort_string = cfg.tv_sort_string()
          self.cats = cfg.tv_categories()
          self.filename_set = ''
          self.fname = ''  # Value for %fn substitution in folders
          self.final_path = ''
  
          self.match_obj = None
          self.extras = None
  
          self.rename_or_not = False
  
          self.show_info = {}
  
          # Check if it is a TV show on init()
          self.match()
  
      def match(self, force=False):
          """ Checks the regex for a match, if so set self.match to true """
          if force or (cfg.enable_tv_sorting() and cfg.tv_sort_string()):
              if force or (not self.cats) or (self.cat and self.cat.lower() in self.cats) or (not self.cat and 'None' in self.cats):
                  # First check if the show matches TV episode regular expressions. Returns regex match object
                  self.match_obj, self.extras = check_regexs(self.original_job_name, series_match)
                  if self.match_obj:
                      logging.debug("Found TV Show (%s)", self.original_job_name)
                      self.matched = True
  
      def is_match(self):
          """ Returns whether there was a match or not """
          return self.matched
  
      def get_final_path(self):
          """ Collect and construct all the variables such as episode name, show names """
          if self.get_values():
              # Get the final path
              path = self.construct_path()
              self.final_path = os.path.join(self.original_path, path)
              return self.final_path
          else:
              # Error Sorting
              return os.path.join(self.original_path, self.original_job_name)
  
      def get_multi_ep_naming(self, one, two, extras):
          """ Returns a list of unique values joined into a string and separated by - (ex:01-02-03-04) """
          extra_list = [one]
          extra2_list = [two]
          for extra in extras:
              if extra not in (extra_list, extra2_list):
                  ep_no2 = extra.rjust(2, '0')
                  extra_list.append(extra)
                  extra2_list.append(ep_no2)
  
          one = '-'.join(extra_list)
          two = '-'.join(extra2_list)
          return one, two
  
      def get_shownames(self):
          """ Get the show name from the match object and format it """
          # Get the formatted title and alternate title formats
          self.show_info['show_tname'], self.show_info['show_tname_two'], self.show_info['show_tname_three'] = get_titles(self.nzo, self.match_obj, self.original_job_name, True)
          self.show_info['show_name'], self.show_info['show_name_two'], self.show_info['show_name_three'] = get_titles(self.nzo, self.match_obj, self.original_job_name)
  
      def get_seasons(self):
          """ Get the season number from the match object and format it """
          try:
              season = self.match_obj.group(1).strip('_')  # season number
          except AttributeError:
              season = '1'
  
          # Provide alternative formatting (0 padding)
          if season.lower() == 's':
              season2 = season
          else:
              try:
                  season = str(int(season))
              except:
                  pass
              season2 = season.rjust(2, '0')
  
          self.show_info['season_num'] = season
          self.show_info['season_num_alt'] = season2
  
      def get_episodes(self):
          """ Get the episode numbers from the match object, format and join them """
          try:
              ep_no = self.match_obj.group(2)  # episode number
          except AttributeError:
              ep_no = '1'
          # Store the original episode number
  
          # Provide alternative formatting (0 padding)
          ep_no2 = ep_no.rjust(2, '0')
          try:
              ep_no = str(int(ep_no))
          except:
              pass
  
          # Dual episode support
          if self.extras:
              ep_no, ep_no2 = self.get_multi_ep_naming(ep_no, ep_no2, self.extras)
  
          self.show_info['episode_num'] = ep_no
          self.show_info['episode_num_alt'] = ep_no2
  
      def get_showdescriptions(self):
          """ Get the show descriptions from the match object and format them """
          self.show_info['ep_name'], self.show_info['ep_name_two'], self.show_info['ep_name_three'] = get_descriptions(self.nzo, self.match_obj, self.original_job_name)
  
      def get_values(self):
          """ Collect and construct all the values needed for path replacement """
          try:
              # - Show Name
              self.get_shownames()
  
              # - Season
              self.get_seasons()
  
              # - Episode Number
              self.get_episodes()
  
              # - Episode Name
              self.get_showdescriptions()
  
              return True
  
          except:
              logging.error(T('Error getting TV info (%s)'), clip_path(self.original_job_name))
              logging.info("Traceback: ", exc_info=True)
              return False
  
      def construct_path(self):
          """ Replaces the sort string with real values such as Show Name and Episode Number """
  
          sorter = self.sort_string.replace('\\', '/')
          mapping = []
  
          if ends_in_file(sorter):
              extension = True
              sorter = sorter.replace('.%ext', '')
          else:
              extension = False
  
          # Replace Show name
          mapping.append(('%sn', self.show_info['show_tname']))
          mapping.append(('%s.n', self.show_info['show_tname_two']))
          mapping.append(('%s_n', self.show_info['show_tname_three']))
          mapping.append(('%sN', self.show_info['show_name']))
          mapping.append(('%s.N', self.show_info['show_name_two']))
          mapping.append(('%s_N', self.show_info['show_name_three']))
  
          # Replace season number
          mapping.append(('%s', self.show_info['season_num']))
          mapping.append(('%0s', self.show_info['season_num_alt']))
  
          # Original dir name
          mapping.append(('%dn', self.original_job_name))
  
          # Replace episode names
          if self.show_info['ep_name']:
              mapping.append(('%en', self.show_info['ep_name']))
              mapping.append(('%e.n', self.show_info['ep_name_two']))
              mapping.append(('%e_n', self.show_info['ep_name_three']))
          else:
              mapping.append(('%en', ''))
              mapping.append(('%e.n', ''))
              mapping.append(('%e_n', ''))
  
          # Replace episode number
          mapping.append(('%e', self.show_info['episode_num']))
          mapping.append(('%0e', self.show_info['episode_num_alt']))
  
          # Make sure unsupported %desc is removed
          mapping.append(('%desc', ''))
  
          # Replace elements
          path = path_subst(sorter, mapping)
  
          for key, name in REPLACE_AFTER.items():
              path = path.replace(key, name)
  
          # Lowercase all characters wrapped in {}
          path = to_lowercase(path)
  
          # Strip any extra ' ' '.' or '_' around foldernames
          path = strip_folders(path)
  
          # Split the last part of the path up for the renamer
          if extension:
              head, tail = os.path.split(path)
              self.filename_set = tail
              self.rename_or_not = True
          else:
              head = path
  
          if head:
              return os.path.normpath(head)
          else:
              # The normpath function translates "" to "."
              # which results in wrong path.join later on
              return head
  
      def rename(self, files, current_path):
          """ Rename for Series """
          logging.debug("Renaming Series")
          largest = (None, None, 0)
  
          def to_filepath(f, current_path):
              if is_full_path(f):
                  filepath = os.path.normpath(f)
              else:
                  filepath = os.path.normpath(os.path.join(current_path, f))
              return filepath
  
          # Create a generator of filepaths, ignore sample files and excluded files (vobs ect)
          filepaths = ((file, to_filepath(file, current_path)) for file in files if not RE_SAMPLE.search(file)
                       and get_ext(file) not in EXCLUDED_FILE_EXTS)
  
          # Find the largest existing file
          for file, fp in filepaths:
              # If for some reason the file no longer exists, skip
              if not os.path.exists(fp):
                  continue
  
              size = os.stat(fp).st_size
              f_file, f_fp, f_size = largest
              if size > f_size:
                  largest = (file, fp, size)
  
          file, filepath, size = largest
          # >20MB
          if filepath and size > 20971520:
              self.fname, self.ext = os.path.splitext(os.path.split(file)[1])
              newname = "%s%s" % (self.filename_set, self.ext)
              # Replace %fn with the original filename
              newname = newname.replace('%fn', self.fname)
              newpath = os.path.join(current_path, newname)
              # Replace %ext with extension
              newpath = newpath.replace('%ext', self.ext)
              try:
                  logging.debug("Rename: %s to %s", filepath, newpath)
                  renamer(filepath, newpath)
              except:
                  logging.error(T('Failed to rename: %s to %s'), clip_path(current_path), clip_path(newpath))
                  logging.info("Traceback: ", exc_info=True)
              rename_similar(current_path, self.ext, self.filename_set, ())
          else:
              logging.debug('Nothing to rename, %s', files)
  
  
  _RE_MULTIPLE = (
      re.compile(r'cd\W?(\d+)\W?', re.I),        # .cd1.mkv
      re.compile(r'\w\W?([\w\d])[{}]*$', re.I),  # blah1.mkv blaha.mkv
      re.compile(r'\w\W([\w\d])\W', re.I)        # blah-1-ok.mkv blah-a-ok.mkv
  )
  
  
  def check_for_multiple(files):
      """ Return list of files that looks like a multi-part post """
      for regex in _RE_MULTIPLE:
          matched_files = check_for_sequence(regex, files)
          if matched_files:
              return matched_files
      return ''
  
  
  def check_for_sequence(regex, files):
      """ Return list of files that looks like a sequence, using 'regex' """
      matches = {}
      prefix = None
      # Build up a dictionary of matches
      # The key is based off the match, ie {1:'blah-part1.mkv'}
      for _file in files:
          name, ext = os.path.splitext(_file)
          match1 = regex.search(name)
          if match1:
              if not prefix or prefix == name[:match1.start()]:
                  matches[match1.group(1)] = name + ext
                  prefix = name[:match1.start()]
  
      # Don't do anything if only one or no files matched
      if len(list(matches.keys())) < 2:
          return {}
  
      key_prev = 0
      passed = True
      alphabet = 'abcdefghijklmnopqrstuvwxyz'
  
      # Check the dictionary to see if the keys are in a numeric or alphabetic sequence
      for akey in sorted(matches.keys()):
          if akey.isdigit():
              key = int(akey)
          elif akey in alphabet:
              key = alphabet.find(akey) + 1
          else:
              passed = False
  
          if passed:
              if not key_prev:
                  key_prev = key
              else:
                  if key_prev + 1 == key:
                      key_prev = key
                  else:
                      passed = False
          if passed:
              # convert {'b':'filename-b.mkv'} to {'2', 'filename-b.mkv'}
              item = matches.pop(akey)
              matches[str(key)] = item
  
      if passed:
          return matches
      else:
          return {}
  
  
  class MovieSorter:
      """ Methods for Generic Sorting """
  
      def __init__(self, nzo, job_name, path, cat):
          self.matched = False
  
          self.original_job_name = job_name
          self.original_path = path
          self.sort_string = cfg.movie_sort_string()
          self.extra = cfg.movie_sort_extra()
          self.cats = cfg.movie_categories()
          self.cat = cat
          self.nzo = nzo
          self.filename_set = ''
          self.fname = ''  # Value for %fn substitution in folders
          self.final_path = ''
  
          self.match_obj = None
  
          self.rename_or_not = False
  
          self.movie_info = {}
  
          # Check if we match the category in init()
          self.match()
  
      def match(self, force=False):
          """ Checks the category for a match, if so set self.match to true """
          if force or (cfg.enable_movie_sorting() and self.sort_string):
              # First check if the show matches TV episode regular expressions. Returns regex match object
              if force or (self.cat and self.cat.lower() in self.cats) or (not self.cat and 'None' in self.cats):
                  logging.debug("Found Movie (%s)", self.original_job_name)
                  self.matched = True
  
      def get_final_path(self):
          """ Collect and construct all the variables such as episode name, show names """
          if self.get_values():
              # Get the final path
              path = self.construct_path()
              self.final_path = os.path.join(self.original_path, path)
              return self.final_path
          else:
              # Error Sorting
              return os.path.join(self.original_path, self.original_job_name)
  
      def get_values(self):
          """ Collect and construct all the values needed for path replacement """
          # - Get Year
          if self.nzo:
              year = self.nzo.nzo_info.get('year')
          else:
              year = ''
          if year:
              year_m = None
          else:
              job_name = self.original_job_name.replace('_', ' ')
              RE_YEAR = re.compile(year_match, re.I)
              year_m = RE_YEAR.search(job_name)
              if year_m:
                  # Find the last matched date
                  # Keep year_m to use in get_titles
                  year = RE_YEAR.findall(job_name)[-1][0]
              else:
                  year = ''
          self.movie_info['year'] = year
  
          # - Get Decades
          self.movie_info['decade'], self.movie_info['decade_two'] = get_decades(year)
  
          # - Get Title
          self.movie_info['ttitle'], self.movie_info['ttitle_two'], self.movie_info['ttitle_three'] = get_titles(self.nzo, year_m, self.original_job_name, True)
          self.movie_info['title'], self.movie_info['title_two'], self.movie_info['title_three'] = get_titles(self.nzo, year_m, self.original_job_name)
  
          return True
  
      def construct_path(self):
          """ Return path reconstructed from original and sort expression """
          sorter = self.sort_string.replace('\\', '/')
          mapping = []
  
          if ends_in_file(sorter):
              extension = True
              sorter = sorter.replace(".%ext", '')
          else:
              extension = False
  
          # Replace title
          mapping.append(('%title', self.movie_info['title']))
          mapping.append(('%.title', self.movie_info['title_two']))
          mapping.append(('%_title', self.movie_info['title_three']))
  
          # Replace title (short forms)
          mapping.append(('%t', self.movie_info['title']))
          mapping.append(('%.t', self.movie_info['title_two']))
          mapping.append(('%_t', self.movie_info['title_three']))
  
          mapping.append(('%sn', self.movie_info['title']))
          mapping.append(('%s.n', self.movie_info['title_two']))
          mapping.append(('%s_n', self.movie_info['title_three']))
  
          mapping.append(('%sN', self.movie_info['ttitle']))
          mapping.append(('%s.N', self.movie_info['ttitle_two']))
          mapping.append(('%s_N', self.movie_info['ttitle_three']))
  
          # Replace year
          mapping.append(('%y', self.movie_info['year']))
  
          # Replace decades
          mapping.append(('%decade', self.movie_info['decade']))
          mapping.append(('%0decade', self.movie_info['decade_two']))
  
          # Original dir name
          mapping.append(('%dn', self.original_job_name))
  
          path = path_subst(sorter, mapping)
  
          for key, name in REPLACE_AFTER.items():
              path = path.replace(key, name)
  
          # Lowercase all characters wrapped in {}
          path = to_lowercase(path)
  
          # Strip any extra ' ' '.' or '_' around foldernames
          path = strip_folders(path)
  
          # Split the last part of the path up for the renamer
          if extension:
              head, tail = os.path.split(path)
              self.filename_set = tail
              self.rename_or_not = True
          else:
              head = path
  
          if head:
              return os.path.normpath(head)
          else:
              # The normpath function translates "" to "."
              # which results in wrong path.join later on
              return head
  
      def rename(self, _files, current_path):
          """ Rename for Generic files """
          logging.debug("Renaming Generic file")
  
          def filter_files(_file, current_path):
              if is_full_path(_file):
                  filepath = os.path.normpath(_file)
              else:
                  filepath = os.path.normpath(os.path.join(current_path, _file))
              if os.path.exists(filepath):
                  size = os.stat(filepath).st_size
                  if size >= cfg.movie_rename_limit.get_int() and not RE_SAMPLE.search(_file) \
                     and get_ext(_file) not in EXCLUDED_FILE_EXTS:
                      return True
              return False
  
          # remove any files below the limit from this list
          files = [_file for _file in _files if filter_files(_file, current_path)]
  
          length = len(files)
          # Single File Handling
          if length == 1:
              file = files[0]
              if is_full_path(file):
                  filepath = os.path.normpath(file)
              else:
                  filepath = os.path.normpath(os.path.join(current_path, file))
              if os.path.exists(filepath):
                  self.fname, ext = os.path.splitext(os.path.split(file)[1])
                  newname = "%s%s" % (self.filename_set, ext)
                  newname = newname.replace('%fn', self.fname)
                  newpath = os.path.join(current_path, newname)
                  try:
                      logging.debug("Rename: %s to %s", filepath, newpath)
                      renamer(filepath, newpath)
                  except:
                      logging.error(T('Failed to rename: %s to %s'), clip_path(filepath), clip_path(newpath))
                      logging.info("Traceback: ", exc_info=True)
                  rename_similar(current_path, ext, self.filename_set, ())
  
          # Sequence File Handling
          # if there is more than one extracted file check for CD1/1/A in the title
          elif self.extra:
              matched_files = check_for_multiple(files)
              # rename files marked as in a set
              if matched_files:
                  logging.debug("Renaming a series of generic files (%s)", matched_files)
                  renamed = list(matched_files.values())
                  for index, file in matched_files.items():
                      filepath = os.path.join(current_path, file)
                      renamed.append(filepath)
                      self.fname, ext = os.path.splitext(os.path.split(file)[1])
                      name = '%s%s' % (self.filename_set, self.extra)
                      name = name.replace('%1', str(index)).replace('%fn', self.fname)
                      name = name + ext
                      newpath = os.path.join(current_path, name)
                      try:
                          logging.debug("Rename: %s to %s", filepath, newpath)
                          renamer(filepath, newpath)
                      except:
                          logging.error(T('Failed to rename: %s to %s'), clip_path(filepath), clip_path(newpath))
                          logging.info("Traceback: ", exc_info=True)
                  rename_similar(current_path, ext, self.filename_set, renamed)
              else:
                  logging.debug("Movie files not in sequence %s", _files)
  
  
  class DateSorter:
      """ Methods for Date Sorting """
  
      def __init__(self, nzo, job_name, path, cat):
          self.matched = False
  
          self.original_job_name = job_name
          self.original_path = path
          self.sort_string = cfg.date_sort_string()
          self.cats = cfg.date_categories()
          self.cat = cat
          self.nzo = nzo
          self.filename_set = ''
          self.fname = ''  # Value for %fn substitution in folders
  
          self.match_obj = None
  
          self.rename_or_not = False
          self.date_type = None
  
          self.date_info = {}
          self.final_path = ''
  
          # Check if we match the category in init()
          self.match()
  
      def match(self, force=False):
          """ Checks the category for a match, if so set self.matched to true """
          if force or (cfg.enable_date_sorting() and self.sort_string):
              # First check if the show matches TV episode regular expressions. Returns regex match object
              if force or (self.cat and self.cat.lower() in self.cats) or (not self.cat and 'None' in self.cats):
                  self.match_obj, self.date_type = check_for_date(self.original_job_name, date_match)
                  if self.match_obj:
                      logging.debug("Found date for sorting (%s)", self.original_job_name)
                      self.matched = True
  
      def is_match(self):
          """ Returns whether there was a match or not """
          return self.matched
  
      def get_final_path(self):
          """ Collect and construct all the variables such as episode name, show names """
          if self.get_values():
              # Get the final path
              path = self.construct_path()
              self.final_path = os.path.join(self.original_path, path)
              return self.final_path
          else:
              # Error Sorting
              return os.path.join(self.original_path, self.original_job_name)
  
      def get_values(self):
          """ Collect and construct all the values needed for path replacement """
  
          # 2008-10-16
          if self.date_type == 1:
              self.date_info['year'] = self.match_obj.group(1)
              self.date_info['month'] = self.match_obj.group(2)
              self.date_info['date'] = self.match_obj.group(3)
          # 10.16.2008
          else:
              self.date_info['year'] = self.match_obj.group(3)
              self.date_info['month'] = self.match_obj.group(1)
              self.date_info['date'] = self.match_obj.group(2)
  
          self.date_info['month_two'] = self.date_info['month'].rjust(2, '0')
          self.date_info['date_two'] = self.date_info['date'].rjust(2, '0')
  
          # - Get Decades
          self.date_info['decade'], self.date_info['decade_two'] = get_decades(self.date_info['year'])
  
          # - Get Title
          self.date_info['ttitle'], self.date_info['ttitle_two'], self.date_info['ttitle_three'] = get_titles(self.nzo, self.match_obj, self.original_job_name, True)
          self.date_info['title'], self.date_info['title_two'], self.date_info['title_three'] = get_titles(self.nzo, self.match_obj, self.original_job_name)
  
          self.date_info['ep_name'], self.date_info['ep_name_two'], self.date_info['ep_name_three'] = get_descriptions(self.nzo, self.match_obj, self.original_job_name)
  
          return True
  
      def construct_path(self):
          """ Return path reconstructed from original and sort expression """
          sorter = self.sort_string.replace('\\', '/')
          mapping = []
  
          if ends_in_file(sorter):
              extension = True
              sorter = sorter.replace(".%ext", '')
          else:
              extension = False
  
          # Replace title
          mapping.append(('%title', self.date_info['title']))
          mapping.append(('%.title', self.date_info['title_two']))
          mapping.append(('%_title', self.date_info['title_three']))
  
          mapping.append(('%t', self.date_info['title']))
          mapping.append(('%.t', self.date_info['title_two']))
          mapping.append(('%_t', self.date_info['title_three']))
  
          mapping.append(('%sn', self.date_info['ttitle']))
          mapping.append(('%s.n', self.date_info['ttitle_two']))
          mapping.append(('%s_n', self.date_info['ttitle_three']))
          mapping.append(('%sN', self.date_info['title']))
          mapping.append(('%s.N', self.date_info['title_two']))
          mapping.append(('%s_N', self.date_info['title_three']))
  
          # Replace year
          mapping.append(('%year', self.date_info['year']))
          mapping.append(('%y', self.date_info['year']))
  
          if self.date_info['ep_name']:
              mapping.append(('%desc', self.date_info['ep_name']))
              mapping.append(('%.desc', self.date_info['ep_name_two']))
              mapping.append(('%_desc', self.date_info['ep_name_three']))
          else:
              mapping.append(('%desc', ''))
              mapping.append(('%.desc', ''))
              mapping.append(('%_desc', ''))
  
          # Replace dir-name before replacing %d for month
          mapping.append(('%dn', self.original_job_name))
  
          # Replace decades
          mapping.append(('%decade', self.date_info['decade']))
          mapping.append(('%0decade', self.date_info['decade_two']))
  
          # Replace month
          mapping.append(('%m', self.date_info['month']))
          mapping.append(('%0m', self.date_info['month_two']))
  
          # Replace date
          mapping.append(('%d', self.date_info['date']))
          mapping.append(('%0d', self.date_info['date_two']))
  
          path = path_subst(sorter, mapping)
  
          for key, name in REPLACE_AFTER.items():
              path = path.replace(key, name)
  
          # Lowercase all characters wrapped in {}
          path = to_lowercase(path)
  
          # Strip any extra ' ' '.' or '_' around foldernames
          path = strip_folders(path)
  
          # Split the last part of the path up for the renamer
          if extension:
              head, tail = os.path.split(path)
              self.filename_set = tail
              self.rename_or_not = True
          else:
              head = path
  
          if head:
              return os.path.normpath(head)
          else:
              # The normpath function translates "" to "."
              # which results in wrong path.join later on
              return head
  
      def rename(self, files, current_path):
          """ Renaming Date file """
          logging.debug("Renaming Date file")
          # find the master file to rename
          for file in files:
              if is_full_path(file):
                  filepath = os.path.normpath(file)
              else:
                  filepath = os.path.normpath(os.path.join(current_path, file))
  
              if os.path.exists(filepath):
                  size = os.stat(filepath).st_size
                  if size > cfg.movie_rename_limit.get_int():
                      if 'sample' not in file:
                          self.fname, ext = os.path.splitext(os.path.split(file)[1])
                          newname = "%s%s" % (self.filename_set, ext)
                          newname = newname.replace('%fn', self.fname)
                          newpath = os.path.join(current_path, newname)
                          if not os.path.exists(newpath):
                              try:
                                  logging.debug("Rename: %s to %s", filepath, newpath)
                                  renamer(filepath, newpath)
                              except:
                                  logging.error(T('Failed to rename: %s to %s'), clip_path(current_path), clip_path(newpath))
                                  logging.info("Traceback: ", exc_info=True)
                              rename_similar(current_path, ext, self.filename_set, ())
                              break
  
  
  def path_subst(path, mapping):
      """ Replace the sort sting elements by real values.
          Non-elements are copied literally.
          path = the sort string
          mapping = array of tuples that maps all elements to their values
      """
      # Added ugly hack to prevent %ext from being masked by %e
      newpath = []
      plen = len(path)
      n = 0
      while n < plen:
          result = path[n]
          if result == '%':
              for key, value in mapping:
                  if path.startswith(key, n) and not path.startswith('%ext', n):
                      n += len(key) - 1
                      result = value
                      break
          newpath.append(result)
          n += 1
      return ''.join(newpath)
  
  
  def get_titles(nzo, match, name, titleing=False):
      """ The title will be the part before the match
          Clean it up and title() it
  
          ''.title() isn't very good under python so this contains
          a lot of little hacks to make it better and for more control
      """
      if nzo:
          title = nzo.nzo_info.get('propername')
      else:
          title = ''
      if not title:
          if match:
              name = name[:match.start()]
  
          # Replace .US. with (US)
          if cfg.tv_sort_countries() == 1:
              for rep in COUNTRY_REP:
                  # (us) > (US)
                  name = replace_word(name, rep.lower(), rep)
                  # (Us) > (US)
                  name = replace_word(name, rep.title(), rep)
                  # .US. > (US)
                  dotted_country = '.%s.' % (rep.strip('()'))
                  name = replace_word(name, dotted_country, rep)
          # Remove .US. and (US)
          elif cfg.tv_sort_countries() == 2:
              for rep in COUNTRY_REP:
                  # Remove (US)
                  name = replace_word(name, rep, '')
                  dotted_country = '.%s.' % (rep.strip('()'))
                  # Remove .US.
                  name = replace_word(name, dotted_country, '.')
  
          title = name.replace('.', ' ').replace('_', ' ')
          title = title.strip().strip('(').strip('_').strip('-').strip().strip('_')
  
          if titleing:
              title = title.title()  # title the show name so it is in a consistent letter case
  
              # title applied uppercase to 's Python bug?
              title = title.replace("'S", "'s")
  
              # Replace titled country names, (Us) with (US) and so on
              if cfg.tv_sort_countries() == 1:
                  for rep in COUNTRY_REP:
                      title = title.replace(rep.title(), rep)
              # Remove country names, ie (Us)
              elif cfg.tv_sort_countries() == 2:
                  for rep in COUNTRY_REP:
                      title = title.replace(rep.title(), '').strip()
  
              # Make sure some words such as 'and' or 'of' stay lowercased.
              for x in LOWERCASE:
                  xtitled = x.title()
                  title = replace_word(title, xtitled, x)
  
              # Make sure some words such as 'III' or 'IV' stay uppercased.
              for x in UPPERCASE:
                  xtitled = x.title()
                  title = replace_word(title, xtitled, x)
  
              # Make sure the first letter of the title is always uppercase
              if title:
                  title = title[0].title() + title[1:]
  
      # The title with spaces replaced by dots
      dots = title.replace(" - ", "-").replace(' ', '.').replace('_', '.')
      dots = dots.replace('(', '.').replace(')', '.').replace('..', '.').rstrip('.')
  
      # The title with spaces replaced by underscores
      underscores = title.replace(' ', '_').replace('.', '_').replace('__', '_').rstrip('_')
  
      return title, dots, underscores
  
  
  def replace_word(word_input, one, two):
      """ Regex replace on just words """
      regex = re.compile(r'\W(%s)(\W|$)' % one, re.I)
      matches = regex.findall(word_input)
      if matches:
          for unused in matches:
              word_input = word_input.replace(one, two)
      return word_input
  
  
  def get_descriptions(nzo, match, name):
      """ If present, get a description from the nzb name.
          A description has to be after the matched item, separated either
          like ' - Description' or '_-_Description'
      """
      if nzo:
          ep_name = nzo.nzo_info.get('episodename')
      else:
          ep_name = ''
      if not ep_name:
          if match:
              ep_name = name[match.end():]  # Need to improve for multi-ep support
          else:
              ep_name = name
          ep_name = ep_name.strip(' _.')
          if ep_name.startswith('-'):
              ep_name = ep_name.strip('- _.')
          if '.' in ep_name and ' ' not in ep_name:
              ep_name = ep_name.replace('.', ' ')
          ep_name = ep_name.replace('_', ' ')
      ep_name2 = ep_name.replace(" - ", "-").replace(" ", ".")
      ep_name3 = ep_name.replace(" ", "_")
      return ep_name, ep_name2, ep_name3
  
  
  def get_decades(year):
      """ Return 4 digit and 2 digit decades given 'year' """
      if year:
          try:
              decade = year[2:3] + '0'
              decade2 = year[:3] + '0'
          except:
              decade = ''
              decade2 = ''
      else:
          decade = ''
          decade2 = ''
      return decade, decade2
  
  
  def check_for_folder(path):
      """ Return True if any folder is found in the tree at 'path' """
      for _root, dirs, _files in os.walk(path):
          if dirs:
              return True
      return False
  
  
  def to_lowercase(path):
      """ Lowercases any characters enclosed in {} """
      _RE_LOWERCASE = re.compile(r'{([^{]*)}')
      while True:
          m = _RE_LOWERCASE.search(path)
          if not m:
              break
          path = path[:m.start()] + m.group(1).lower() + path[m.end():]
  
      # just in case
      path = path.replace('{', '')
      path = path.replace('}', '')
      return path
  
  
  def strip_folders(path):
      """ Return 'path' without leading and trailing spaces and underscores in each element
          For Windows, also remove leading and trailing dots
      """
      unc = sabnzbd.WIN32 and (path.startswith('//') or path.startswith('\\\\'))
      f = path.strip('/').split('/')
  
      # For path beginning with a slash, insert empty element to prevent loss
      if path.strip()[0] in '/\\':
          f.insert(0, '')
  
      def strip_all(x):
          """ Strip all leading/trailing underscores also dots for Windows """
          x = x.strip().strip('_')
          if sabnzbd.WIN32:
              # OSX and Linux should keep dots, because leading dots are significant
              # while Windows cannot handle trailing dots
              x = x.strip('.')
          x = x.strip()
          return x
  
      path = os.path.normpath('/'.join([strip_all(x) for x in f]))
      if unc:
          return '\\' + path
      else:
          return path
  
  
  def rename_similar(folder, skip_ext, name, skipped_files):
      """ Rename all other files in the 'folder' hierarchy after 'name'
          and move them to the root of 'folder'.
          Files having extension 'skip_ext' will be moved, but not renamed.
          Don't touch files in list `skipped_files`
      """
      logging.debug('Give files in set "%s" matching names.', name)
      folder = os.path.normpath(folder)
      skip_ext = skip_ext.lower()
  
      for root, dirs, files in os.walk(folder):
          for f in files:
              path = os.path.join(root, f)
              if path in skipped_files:
                  continue
              org, ext = os.path.splitext(f)
              if ext.lower() == skip_ext:
                  # Move file, but do not rename
                  newpath = os.path.join(folder, f)
              else:
                  # Move file and rename
                  newname = "%s%s" % (name, ext)
                  newname = newname.replace('%fn', org)
                  newpath = os.path.join(folder, newname)
              if path != newpath:
                  newpath = get_unique_filename(newpath)
                  try:
                      logging.debug("Rename: %s to %s", path, newpath)
                      renamer(path, newpath)
                  except:
                      logging.error(T('Failed to rename similar file: %s to %s'), clip_path(path), clip_path(newpath))
                      logging.info("Traceback: ", exc_info=True)
      cleanup_empty_directories(folder)
  
  
  def check_regexs(filename, matchers):
      """ Regular Expression match for a list of regexes
          Returns the MatchObject if a match is made
          This version checks for an additional match
      """
      extras = []
      for expressions in matchers:
          expression, extramatchers = expressions
          match1 = expression.search(filename)
          if match1:
              for m in extramatchers:
                  match2 = m.findall(filename, match1.end())
                  if match2:
                      for match in match2:
                          if type(match) == type(()) and len(match) > 1:
                              extras.append(match[1])
                          else:
                              extras.append(match)
                      break
              return match1, extras
      return None, None
  
  
  def check_for_date(filename, matcher):
      """ Regular Expression match for date based files
          Returns the MatchObject if a match is made
      """
      x = 0
      if matcher:
          for expression in matcher:
              regex = re.compile(expression)
              match1 = regex.search(filename)
              x += 1
              if match1:
                  return match1, x
      return None, 0
  
  
  def is_full_path(file):
      """ Return True if path is absolute """
      if file.startswith('\\') or file.startswith('/'):
          return True
      try:
          if file[1:3] == ':\\':
              return True
      except:
          pass
      return False
  
  
  def eval_sort(sorttype, expression, name=None, multipart=''):
      """ Preview a sort expression, to be used by API """
      from sabnzbd.api import Ttemplate
      path = ''
      name = sanitize_foldername(name)
      if sorttype == 'series':
          name = name or ('%s S01E05 - %s [DTS]' % (Ttemplate('show-name'), Ttemplate('ep-name')))
          sorter = SeriesSorter(None, name, path, 'tv')
      elif sorttype == 'movie':
          name = name or (Ttemplate('movie-sp-name') + ' (2009)')
          sorter = MovieSorter(None, name, path, 'tv')
      elif sorttype == 'date':
          name = name or (Ttemplate('show-name') + ' 2009-01-02')
          sorter = DateSorter(None, name, path, 'tv')
      else:
          return None
      sorter.sort_string = expression
      sorter.match(force=True)
      path = sorter.get_final_path()
      path = os.path.normpath(os.path.join(path, sorter.filename_set))
      fname = Ttemplate('orgFilename')
      fpath = path
      if sorttype == 'movie' and '%1' in multipart:
          fname = fname + multipart.replace('%1', '1')
          fpath = fpath + multipart.replace('%1', '1')
      if '%fn' in path:
          path = path.replace('%fn', fname + '.mkv')
      else:
          if sorter.rename_or_not:
              path = fpath + '.mkv'
          else:
              if sabnzbd.WIN32:
                  path += '\\'
              else:
                  path += '/'
      return path
  