Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #! /usr/bin/env python2.5
- # pylint: disable-msg=I0011
- """
- Organize TV episode files into folders.
- Notes:
- - you need to have Python >= 2.5 installed (aptitude install python-minimal)
- - save as "tv-organizer" and make it executable
- - should work on any *nix and Windows, but only tested on Ubuntu
- - use as a cron or rtorrent completion job like this:
- tv-organizer ~/rtorrent/completed ~/Videos/TV
- - to add folders for new shows, first add "-cn" to the above
- command line and call it manually, then if the proposed
- show titles check out OK, drop the "n"
- """
- #
- # Configuration, change to fit your needs
- #
- # Alias names for shows (keys must be lower case!)
- SHOW_ALIASES = {
- "csi": "CSI Las Vegas",
- "csi ny": "CSI New York",
- "csi new york": "CSI New York",
- "csi miami": "CSI Miami",
- "doctor who 2005": "Doctor Who",
- "ncis la": "NCIS Los Angeles",
- "sanctuary us": "Sanctuary",
- "snl": "Saturday Night Live",
- }
- # Fine tune mapping sensitivity from file name to folder names
- MATCH_THRESHOLD = 0.6
- # You probably never need to touch this!
- TV_TRAIL = (
- r"(?:\.(?P<release_tags>PREAIR|READNFO))?"
- r"(?:[._](?P<release>REPACK|PROPER|REALPROPER|INTERNAL))?"
- r"(?:[._](?P<aspect>WS))?"
- r"[._](?P<format>HDTV|PDTV|DSR|DVDSCR|720p)"
- r"[._][Xx][Vv2][Ii6][Dd4]-(?P<group>.+?)(?P<extension>\.avi|\.mkv|\.m4v)?$"
- )
- TV_PATTERNS = [
- ( # Normal TV Episodes
- r"^(?P<show>.+?)\.[sS]?(?P<season>\d{1,2})[xeE](?P<episode>\d{2}(?:eE\d{2})?)"
- r"(?:\.(?P<title>.+[a-z]{1,2}.+?))?"
- + TV_TRAIL
- ),
- ( # Shows
- r"^(?P<show>.+?)\.(?P<date>\d{4}\.\d{2}\.\d{2})"
- r"(?:\.(?P<title>.+[a-z]{1,2}.+?))?"
- + TV_TRAIL
- ),
- ( # Mini Series
- r"^(?P<show>.+?)\.(?P<date>\d{4})"
- r"(?:\.Part(?P<part>\d+?))?"
- r"(?:\.(?P<title>.+[a-z]{1,2}.+?))?"
- + TV_TRAIL
- ),
- ]
- #
- # HERE BE DRAGONS!
- #
- VERSION = "1.0"
- INDENT = 9
- EDITOR = 'gedit "%s" &'
- import os
- import re
- import sys
- import time
- import difflib
- import logging
- from optparse import OptionParser
- def normalizeTitle(show_title):
- """ Normalize show title.
- """
- show_title = ' '.join([i.strip().capitalize()
- for i in show_title.replace('.', ' ').replace('_', ' ').split()
- ])
- show_title = SHOW_ALIASES.get(show_title.lower(), show_title)
- return show_title
- class TvOrganizer(object):
- """ The TvOrganizer command line interface.
- """
- def __init__(self):
- """ Initialize command line handler.
- """
- self.startup = time.time()
- self.args = None
- self.options = None
- self.indent = '\n' + ' ' * INDENT
- self.patterns = [re.compile(i, re.I) for i in TV_PATTERNS]
- logging.basicConfig(level=logging.INFO,
- format="%%(levelname)-%ds%%(message)s" % INDENT)
- self.log = logging.getLogger(self.__class__.__name__)
- self.parser = OptionParser(
- "%prog [options] <episode folder> [<target folder>]",
- version="%prog " + VERSION)
- self.parser.add_option("-q", "--quiet", dest="quiet",
- action="store_true", default=False,
- help="omit informational logging")
- self.parser.add_option("-v", "--verbose", dest="verbose",
- action="store_true", default=False,
- help="increase informational logging")
- self.parser.add_option("-n", "--dry-run", dest="dry_run",
- action="store_true", default=False,
- help="don't do anything, only show what would've been done")
- self.parser.add_option("--fix", dest="fix",
- action="store_true", default=False,
- help="edit this script")
- self.parser.add_option("-c", "--create-folders", dest="create",
- action="store_true", default=False,
- help="create target folders for unknown shows")
- def getOptions(self):
- """ Get program options.
- """
- self.options, self.args = self.parser.parse_args()
- if self.options.verbose:
- logging.getLogger().setLevel(logging.DEBUG)
- if self.options.quiet:
- logging.getLogger().setLevel(logging.WARNING)
- self.log.debug("Parsed command line options: %r" % self.options)
- if self.options.verbose and self.options.quiet:
- self.parser.error("Quiet verbosity? Reconsider your options!")
- def fail(self, msg):
- """ Failure.
- """
- self.log.error(msg)
- sys.exit(1)
- def run(self):
- """ The main loop.
- """
- # Parse command line arguments
- self.getOptions()
- if self.options.fix:
- os.system(EDITOR % sys.argv[0])
- sys.exit(0)
- if not(1 <= len(self.args) <= 2):
- self.parser.error("You must provide a source and optional target folder path!")
- # Initialize statistics
- skip_count = 0
- # Get episode and show lists
- source_dir, target_dir = (self.args*2)[:2]
- self.log.debug("Organizing %r into %r" % (source_dir, target_dir))
- episodes = sorted([n for n in os.listdir(source_dir) if os.path.isfile(os.path.join(source_dir, n))])
- self.log.debug("Episodes: %r" % (episodes))
- shows = [n for n in os.listdir(target_dir) if os.path.isdir(os.path.join(target_dir, n))]
- self.log.debug("Shows %r" % (shows))
- # Move all files that look like TV shows
- for episode in episodes:
- # Check to see if it's a show
- for pattern in self.patterns:
- matched = pattern.match(episode)
- if matched:
- show_data = matched.groupdict()
- self.log.debug("Parsed %r to %r" % (episode, show_data))
- show_title = show_data["show"]
- break
- else:
- # Not a show (none of our regex matched)
- self.log.debug("Not a TV show: %r" % (episode))
- skip_count += 1
- continue
- # Now match the file's show title against the show list
- show_title = normalizeTitle(show_title)
- matches = difflib.get_close_matches(show_title, shows, 10, MATCH_THRESHOLD)
- self.log.debug("Matched %r to %r out of %r" % (show_title, matches, shows))
- # Create folders for new unknown shows?
- if self.options.create and not matches:
- # Add it
- self.log.info("%r +++ Creating target folder" % (show_title, ))
- shows.append(show_title)
- matches = [show_title]
- if not self.options.dry_run:
- # Create it on disk
- try:
- os.mkdir(os.path.join(target_dir, show_title))
- except OSError, exc:
- self.log.error("Failed to create %r (%s)" % (show_title, exc))
- # Did we find a unique match?
- if len(matches) == 1:
- # Move the episode to its show folder
- show_title, = matches
- self.log.info("%r <-- %r" % (show_title, episode,))
- if not self.options.dry_run:
- try:
- os.rename(
- os.path.join(source_dir, episode),
- os.path.join(target_dir, show_title, episode)
- )
- except OSError, exc:
- self.log.error("Failed to move %r (%s)" % (episode, exc))
- elif matches:
- # Match is not unique
- self.log.warning("Found no unique target folder for %r (%s) amongst %r" % (
- episode, show_title, matches
- ))
- else:
- # No match at all
- self.log.warning("Found no target folder for %r (%s)" % (episode, show_title))
- # Dump statistics
- if skip_count:
- self.log.warning("Skipped %d out of %d files." % (skip_count, len(episodes)))
- if __name__ == "__main__":
- # Run main loop
- TvOrganizer().run()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement