Advertisement
Guest User

Untitled

a guest
Dec 28th, 2009
134
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.22 KB | None | 0 0
  1. #! /usr/bin/env python2.5
  2. # pylint: disable-msg=I0011
  3. """
  4. Organize TV episode files into folders.
  5.  
  6. Notes:
  7.  - you need to have Python >= 2.5 installed (aptitude install python-minimal)
  8.  - save as "tv-organizer" and make it executable
  9.  - should work on any *nix and Windows, but only tested on Ubuntu
  10.  - use as a cron or rtorrent completion job like this:
  11.        tv-organizer ~/rtorrent/completed ~/Videos/TV
  12.  - to add folders for new shows, first add "-cn" to the above
  13.    command line and call it manually, then if the proposed
  14.    show titles check out OK, drop the "n"
  15.  
  16. """
  17.  
  18. #
  19. # Configuration, change to fit your needs
  20. #
  21.  
  22. # Alias names for shows (keys must be lower case!)
  23. SHOW_ALIASES = {
  24.     "csi": "CSI Las Vegas",
  25.     "csi ny": "CSI New York",
  26.     "csi new york": "CSI New York",
  27.     "csi miami": "CSI Miami",
  28.     "doctor who 2005": "Doctor Who",
  29.     "ncis la": "NCIS Los Angeles",
  30.     "sanctuary us": "Sanctuary",
  31.     "snl": "Saturday Night Live",
  32. }
  33.  
  34. # Fine tune mapping sensitivity from file name to folder names
  35. MATCH_THRESHOLD = 0.6
  36.  
  37. # You probably never need to touch this!
  38. TV_TRAIL = (
  39.         r"(?:\.(?P<release_tags>PREAIR|READNFO))?"
  40.         r"(?:[._](?P<release>REPACK|PROPER|REALPROPER|INTERNAL))?"
  41.         r"(?:[._](?P<aspect>WS))?"
  42.         r"[._](?P<format>HDTV|PDTV|DSR|DVDSCR|720p)"
  43.         r"[._][Xx][Vv2][Ii6][Dd4]-(?P<group>.+?)(?P<extension>\.avi|\.mkv|\.m4v)?$"
  44. )
  45. TV_PATTERNS = [
  46.     ( # Normal TV Episodes
  47.         r"^(?P<show>.+?)\.[sS]?(?P<season>\d{1,2})[xeE](?P<episode>\d{2}(?:eE\d{2})?)"
  48.         r"(?:\.(?P<title>.+[a-z]{1,2}.+?))?"
  49.         + TV_TRAIL
  50.     ),
  51.     ( # Shows
  52.         r"^(?P<show>.+?)\.(?P<date>\d{4}\.\d{2}\.\d{2})"
  53.         r"(?:\.(?P<title>.+[a-z]{1,2}.+?))?"
  54.         + TV_TRAIL
  55.     ),
  56.     ( # Mini Series
  57.         r"^(?P<show>.+?)\.(?P<date>\d{4})"
  58.         r"(?:\.Part(?P<part>\d+?))?"
  59.         r"(?:\.(?P<title>.+[a-z]{1,2}.+?))?"
  60.         + TV_TRAIL
  61.     ),
  62. ]
  63.  
  64.  
  65. #
  66. # HERE BE DRAGONS!
  67. #
  68. VERSION = "1.0"
  69. INDENT = 9
  70. EDITOR = 'gedit "%s" &'
  71.  
  72. import os
  73. import re
  74. import sys
  75. import time
  76. import difflib
  77. import logging
  78.  
  79. from optparse import OptionParser
  80.  
  81.  
  82. def normalizeTitle(show_title):
  83.     """ Normalize show title.
  84.    """
  85.     show_title = ' '.join([i.strip().capitalize()
  86.         for i in show_title.replace('.', ' ').replace('_', ' ').split()
  87.     ])
  88.     show_title = SHOW_ALIASES.get(show_title.lower(), show_title)
  89.     return show_title
  90.  
  91.  
  92. class TvOrganizer(object):
  93.     """ The TvOrganizer command line interface.
  94.    """
  95.  
  96.     def __init__(self):
  97.         """ Initialize command line handler.
  98.        """
  99.         self.startup = time.time()
  100.         self.args = None
  101.         self.options = None
  102.         self.indent = '\n' + ' ' * INDENT
  103.         self.patterns = [re.compile(i, re.I) for i in TV_PATTERNS]
  104.  
  105.         logging.basicConfig(level=logging.INFO,
  106.             format="%%(levelname)-%ds%%(message)s" % INDENT)
  107.         self.log = logging.getLogger(self.__class__.__name__)
  108.  
  109.         self.parser = OptionParser(
  110.             "%prog [options] <episode folder> [<target folder>]",
  111.             version="%prog " + VERSION)
  112.  
  113.         self.parser.add_option("-q", "--quiet", dest="quiet",
  114.             action="store_true", default=False,
  115.             help="omit informational logging")
  116.         self.parser.add_option("-v", "--verbose", dest="verbose",
  117.             action="store_true", default=False,
  118.             help="increase informational logging")
  119.         self.parser.add_option("-n", "--dry-run", dest="dry_run",
  120.             action="store_true", default=False,
  121.             help="don't do anything, only show what would've been done")
  122.         self.parser.add_option("--fix", dest="fix",
  123.             action="store_true", default=False,
  124.             help="edit this script")
  125.         self.parser.add_option("-c", "--create-folders", dest="create",
  126.             action="store_true", default=False,
  127.             help="create target folders for unknown shows")
  128.  
  129.  
  130.     def getOptions(self):
  131.         """ Get program options.
  132.        """
  133.         self.options, self.args = self.parser.parse_args()
  134.  
  135.         if self.options.verbose:
  136.             logging.getLogger().setLevel(logging.DEBUG)
  137.         if self.options.quiet:
  138.             logging.getLogger().setLevel(logging.WARNING)
  139.  
  140.         self.log.debug("Parsed command line options: %r" % self.options)
  141.  
  142.         if self.options.verbose and self.options.quiet:
  143.             self.parser.error("Quiet verbosity? Reconsider your options!")
  144.  
  145.  
  146.     def fail(self, msg):
  147.         """ Failure.
  148.        """
  149.         self.log.error(msg)
  150.         sys.exit(1)
  151.  
  152.  
  153.     def run(self):
  154.         """ The main loop.
  155.        """
  156.         # Parse command line arguments
  157.         self.getOptions()
  158.  
  159.         if self.options.fix:
  160.             os.system(EDITOR % sys.argv[0])
  161.             sys.exit(0)
  162.  
  163.         if not(1 <= len(self.args) <= 2):
  164.             self.parser.error("You must provide a source and optional target folder path!")
  165.  
  166.         # Initialize statistics
  167.         skip_count = 0
  168.  
  169.         # Get episode and show lists
  170.         source_dir, target_dir = (self.args*2)[:2]
  171.         self.log.debug("Organizing %r into %r" % (source_dir, target_dir))
  172.  
  173.         episodes = sorted([n for n in os.listdir(source_dir) if os.path.isfile(os.path.join(source_dir, n))])
  174.         self.log.debug("Episodes: %r" % (episodes))
  175.        
  176.         shows = [n for n in os.listdir(target_dir) if os.path.isdir(os.path.join(target_dir, n))]
  177.         self.log.debug("Shows %r" % (shows))
  178.  
  179.         # Move all files that look like TV shows
  180.         for episode in episodes:
  181.             # Check to see if it's a show
  182.             for pattern in self.patterns:
  183.                 matched = pattern.match(episode)
  184.                 if matched:
  185.                     show_data = matched.groupdict()
  186.                     self.log.debug("Parsed %r to %r" % (episode, show_data))
  187.                     show_title = show_data["show"]
  188.                     break
  189.             else:
  190.                 # Not a show (none of our regex matched)
  191.                 self.log.debug("Not a TV show: %r" % (episode))
  192.                 skip_count += 1
  193.                 continue
  194.  
  195.             # Now match the file's show title against the show list
  196.             show_title = normalizeTitle(show_title)
  197.             matches = difflib.get_close_matches(show_title, shows, 10, MATCH_THRESHOLD)
  198.             self.log.debug("Matched %r to %r out of %r" % (show_title, matches, shows))
  199.  
  200.             # Create folders for new unknown shows?
  201.             if self.options.create and not matches:
  202.                 # Add it
  203.                 self.log.info("%r +++ Creating target folder" % (show_title, ))
  204.                 shows.append(show_title)
  205.                 matches = [show_title]
  206.                 if not self.options.dry_run:
  207.                     # Create it on disk
  208.                     try:
  209.                         os.mkdir(os.path.join(target_dir, show_title))
  210.                     except OSError, exc:
  211.                         self.log.error("Failed to create %r (%s)" % (show_title, exc))
  212.            
  213.             # Did we find a unique match?
  214.             if len(matches) == 1:
  215.                 # Move the episode to its show folder
  216.                 show_title, = matches
  217.                 self.log.info("%r <-- %r" % (show_title, episode,))
  218.                 if not self.options.dry_run:
  219.                     try:
  220.                         os.rename(
  221.                             os.path.join(source_dir, episode),
  222.                             os.path.join(target_dir, show_title, episode)
  223.                         )
  224.                     except OSError, exc:
  225.                         self.log.error("Failed to move %r (%s)" % (episode, exc))
  226.             elif matches:
  227.                 # Match is not unique
  228.                 self.log.warning("Found no unique target folder for %r (%s) amongst %r" % (
  229.                     episode, show_title, matches
  230.                 ))
  231.             else:
  232.                 # No match at all
  233.                 self.log.warning("Found no target folder for %r (%s)" % (episode, show_title))
  234.  
  235.         # Dump statistics
  236.         if skip_count:
  237.             self.log.warning("Skipped %d out of %d files." % (skip_count, len(episodes)))
  238.  
  239.  
  240. if __name__ == "__main__":
  241.     # Run main loop
  242.     TvOrganizer().run()
  243.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement