Guest User

Untitled

a guest
Nov 3rd, 2015
216
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.20 KB | None | 0 0
  1. #!/usr/bin/python
  2. """
  3. This program automatically renames movie files based on its best guess at
  4. their title and year, based on IMDB.
  5. In order to make interactive mode nicer (and save precious time in
  6. non-interactive mode), this program uses two threads to do the work. The first
  7. thread is the producer, and it fills a shared queue with operations to
  8. manipulate the filesystem (e.g., copy a file, or create a directory, etc.). The
  9. second thread is the consumer, whose job it is to take filesystem operations
  10. from the queue and perform them. This design makes it possible for interactive
  11. mode to progress at its own pace without having to wait for each file to be
  12. copied, since that can take minutes for large files.
  13. The producer waits around for the consumer thread to finish its work (after
  14. letting it know that it's finished producing). It also catches any Ctrl-C
  15. signal (SIGINT) and exits all threads.
  16. """
  17.  
  18. import argparse
  19. from glob import iglob
  20. import os
  21. import Queue
  22. import re
  23. import shutil
  24. import signal # always deliver signals to the main thread
  25. import sys
  26. from threading import Thread
  27.  
  28. try:
  29.     from imdbpie import Imdb
  30. except ImportError:
  31.     print "Please install the imdbpie library: pip install imdbpie"
  32.     sys.exit(1)
  33.  
  34. class Consumer(Thread):
  35.     """
  36.    Consumer to perform filesystem operations. Each operation is a closure.
  37.    """
  38.     def __init__(self, producer, args, fs_op_queue):
  39.         super(Consumer, self).__init__()
  40.         self.producer = producer
  41.         self.args = args
  42.         self.fs_op_queue = fs_op_queue
  43.  
  44.     def run(self):
  45.         while self.producer.is_working():
  46.             self.consume()
  47.         while not self.fs_op_queue.empty():
  48.             self.consume()
  49.  
  50.     def consume(self):
  51.         try:
  52.             fs_op = self.fs_op_queue.get(timeout=1)
  53.             fs_op()
  54.         except Queue.Empty:
  55.             pass
  56.  
  57. class Producer():
  58.     """
  59.    Class to do most of the processing, and produce filesystem operations to be
  60.    performed by the Consumer. Each operation is passed as a closure.
  61.    """
  62.     def get_imdb_title_from_guess(self, title_guess):
  63.         """
  64.        Use the IMDB API to get the year from a title. I'm assuming that the
  65.        first result is the best match for the search, but I don't know if this
  66.        is documented anywhere.
  67.        """
  68.         title_format = "%s (%s)"
  69.         results_list = self.imdb.search_for_title(title_guess)
  70.         if len(results_list):
  71.             result = results_list[0]
  72.             imdb_title = title_format % (result["title"], result["year"])
  73.             self.log_guess("Using \"%s\" as best result for guess \"%s\"" %
  74.                            (imdb_title, title_guess))
  75.             return (True, imdb_title)
  76.         else:
  77.             print "ERROR: could not get year for \"%s\"" % title_guess
  78.             return (False, "")
  79.  
  80.     def get_best_guess(self, filename):
  81.         """
  82.        Given a filename and args, compute the best guess at the title.
  83.        """
  84.         guess = filename
  85.  
  86.         for manipulate in self.guess_manipulations:
  87.             guess = manipulate(guess)
  88.  
  89.         self.log_guess("Computed best guess of \"%s\" for \"%s\"" %
  90.                        (guess, filename))
  91.         return guess
  92.  
  93.     def log_guess(self, string):
  94.         if self.args.verbose or self.args.guess_only:
  95.             print string
  96.  
  97.     def create_container(self, container_dir):
  98.         """
  99.        Produce the file operation to create a particular container directory,
  100.        if necessary. Does not actually perform the file operation.
  101.        """
  102.         if os.path.exists(container_dir):
  103.             if os.path.isdir(container_dir):
  104.                 # path exists, and is directory; this is fine
  105.                 return True
  106.  
  107.             # not a directory:
  108.             self.log_fs_problem("\"%s\" already exists but is not a directory" %
  109.                                 container_dir)
  110.  
  111.             if not self.args.force:
  112.                 return False
  113.             else:
  114.                 # operation should be forced
  115.                 self.log_fs_operation("Remove: \"%s\"" % container_dir)
  116.                 def fs_op():
  117.                     os.remove(container_dir)
  118.                 self.do_fs_operation(fs_op)
  119.  
  120.         self.log_fs_operation("Create: container \"%s\"" % container_dir)
  121.         def fs_op():
  122.             # if args.output_dir doesn't exist, this will create it
  123.             os.makedirs(container_dir)
  124.         self.do_fs_operation(fs_op)
  125.         return True
  126.  
  127.     def transfer_file(self, src_file, dest_file):
  128.         """
  129.        Produce the fs_ops to move/copy src_file to dest_file.
  130.        """
  131.         if os.path.exists(dest_file):
  132.             self.log_fs_problem("File already exists: \"%s\"" % dest_file)
  133.  
  134.             if not self.args.force:
  135.                 return False
  136.             else:
  137.                 # operation should be forced
  138.                 self.log_fs_operation("Remove: \"%s\"" % dest_file)
  139.                 def fs_op():
  140.                     if os.path.isdir(dest_file):
  141.                         shutil.rmtree(dest_file)
  142.                     else:
  143.                         os.remove(dest_file)
  144.                 self.do_fs_operation(fs_op)
  145.  
  146.         self.log_fs_operation("%s: \"%s\" -> \"%s\"" %
  147.                               ("Copy" if self.args.copy else "Move",
  148.                                src_file,
  149.                                dest_file))
  150.         def fs_op():
  151.             if self.args.copy:
  152.                 shutil.copy(src_file, dest_file)
  153.             else:
  154.                 shutil.move(src_file, dest_file)
  155.         self.do_fs_operation(fs_op)
  156.         return True
  157.  
  158.     def do_fs_operation(self, fs_op):
  159.         if not self.args.dry_run:
  160.             if not self.fs_op_queue:
  161.                 fs_op()
  162.             else:
  163.                 self.fs_op_queue.put(fs_op)
  164.  
  165.     def log_fs_operation(self, string):
  166.         if self.args.verbose or self.args.dry_run:
  167.             print string
  168.  
  169.     def log_fs_problem(self, string):
  170.         if self.args.verbose or self.args.dry_run or not self.args.force:
  171.             print string
  172.  
  173.     def get_dest_dir(self, pretty_name):
  174.         dest_dir = None
  175.         if self.args.bare_file:
  176.             dest_dir = self.output_dir
  177.         else:
  178.             dest_dir = os.path.join(self.output_dir, pretty_name)
  179.         return dest_dir
  180.  
  181.     def process_file(self, filepath):
  182.         """
  183.        Process a given movie file.
  184.        """
  185.         if not os.path.isfile(filepath):
  186.             print "Not a file: \"%s\"" % filepath
  187.             return False
  188.         filename_with_ext = os.path.basename(filepath)
  189.         (filename, ext) = os.path.splitext(filename_with_ext)
  190.         guess = self.get_best_guess(filename)
  191.         (b_success, pretty_name) = self.get_imdb_title_from_guess(guess)
  192.         for c in "\/:*?\"<>|":
  193.             # clean disallowed chars from windows filenames
  194.             pretty_name = pretty_name.replace(c, " ")
  195.         if self.args.guess_only or not b_success:
  196.             return b_success
  197.  
  198.         dest_dir = self.get_dest_dir(pretty_name)
  199.         dest_file = os.path.join(dest_dir, pretty_name + ext)
  200.         if self.args.keep_going and os.path.isfile(dest_file):
  201.             return True
  202.         if self.args.interactive:
  203.             # only continue if the user says to
  204.             action = "copy" if self.args.copy else "move"
  205.             print ("\nWill %s \"%s\"\n\t-> \"%s\"." %
  206.                    (action, filepath, dest_file)),
  207.             cont = raw_input("\nContinue? [Y/n] ")
  208.             if not re.match("^(y(es)?)$|^$", cont, re.IGNORECASE):
  209.                 return False
  210.         # take advantage of short-circuiting operators to chain together the
  211.         # next few calls, stop at any failure, and return the final success or
  212.         # failure value
  213.         return (self.create_container(dest_dir) and
  214.                 self.transfer_file(filepath, dest_file))
  215.  
  216.     def run(self):
  217.         """
  218.        Move/copy all the movie files into containing directories in the
  219.        output_dir.
  220.        """
  221.         # process the file args
  222.         total_renamed = 0
  223.         total_skipped = 0
  224.         for movie_file in self.files:
  225.             b_success = self.process_file(movie_file)
  226.             if self.args.totals:
  227.                 if b_success:
  228.                     total_renamed += 1
  229.                 else:
  230.                     total_skipped += 1
  231.  
  232.         if self.args.totals:
  233.             total_processed = total_renamed + total_skipped
  234.             print ("%d file%s processed: %d renamed, %d skipped" %
  235.                    (total_processed,
  236.                     "s" if total_processed > 1 else "",
  237.                     total_renamed,
  238.                     total_skipped))
  239.  
  240.         self.b_is_working = False
  241.  
  242.         if self.consumer:
  243.             if self.consumer.is_alive():
  244.                 print "Do NOT exit, filesystem thread is still processing files!"
  245.             while self.consumer.is_alive():
  246.                 self.consumer.join(0.5)
  247.  
  248.     def build_regex_manipulation(self, regex_pattern, sub_string):
  249.         """
  250.        Given a tuple of a pattern and a string to substitute it with, return a
  251.        function that takes a guess and substitutes all instances of the
  252.        pattern. This avoids recompiling regular expressions unnecessarily,
  253.        since it compiles them each once during initialization, then all the
  254.        closures are shared between iterations of the main processing loop.
  255.        """
  256.         re_obj = re.compile(regex_pattern, re.IGNORECASE)
  257.         def manipulate(guess):
  258.             return re_obj.sub(sub_string, guess)
  259.         return manipulate
  260.  
  261.     def is_working(self):
  262.         return self.b_is_working
  263.  
  264.     def __init__(self, args):
  265.         self.args = args
  266.         self.imdb = Imdb()
  267.  
  268.         # only set up Tkinter if we're going to use it
  269.         if not self.args.files or not self.args.output_dir:
  270.             try:
  271.                 from Tkinter import Tk
  272.                 from tkFileDialog import askopenfilename, askdirectory
  273.             except ImportError:
  274.                 print "Please install the Tkinter library for graphical file and directory choosers."
  275.                 sys.exit(1)
  276.             root = Tk()
  277.             root.withdraw() # keep the root window from appearing
  278.  
  279.         # get movie files to process
  280.         self.files = []
  281.         if not self.args.files:
  282.             self.files = askopenfilename(
  283.                             multiple=True,
  284.                             title="What movies do you want to rename?")
  285.             if not self.files:
  286.                 sys.exit(0)
  287.         else:
  288.             # use glob to expand wildcards, since Windows sucks and doesn't do
  289.             # it for us
  290.             self.files = [filename for file_list in map(iglob, self.args.files)
  291.                     for filename in file_list]
  292.  
  293.         # get output directory
  294.         if not self.args.output_dir:
  295.             self.output_dir = askdirectory(title="Choose the output directory")
  296.             if not self.output_dir:
  297.                 sys.exit(0)
  298.         else:
  299.             self.output_dir = self.args.output_dir
  300.  
  301.         # set up guess manipulations
  302.         self.guess_manipulations = []
  303.         if self.args.remove:
  304.             # remove user-specified patterns
  305.             self.guess_manipulations.extend(map(
  306.                 lambda pattern: self.build_regex_manipulation(pattern, ""),
  307.                 self.args.remove))
  308.         # remove some common junk
  309.         self.guess_manipulations.extend(map(
  310.             lambda (pat, sub): self.build_regex_manipulation(pat, sub),
  311.             [("[^A-Za-z0-9']+", " "), # remove non-alphanumeric (replace with space)
  312.              ("(\\d){3,4}[ip]", ""), # remove 1080p, 720i, etc.
  313.              ("(dvd)|(bluray)", ""), # remove DVD, bluray
  314.              ("(19\\d{2})|(2\\d{3})", ""), # remove year, if already in filename
  315.              ("\\s+", " "), # collapse whitespace
  316.             ]))
  317.         self.guess_manipulations.append(lambda string: string.strip()) # trim leading and trailing whitespace
  318.  
  319.         self.b_is_working = True
  320.         # set up shared fs_op queue
  321.         self.fs_op_queue = None
  322.         if not self.args.dry_run and not self.args.synchronous:
  323.             self.fs_op_queue = Queue.Queue(maxsize=0) # to be shared by producer, consumer
  324.  
  325.         # set up consumer
  326.         self.consumer = None
  327.         if self.fs_op_queue:
  328.             self.consumer = Consumer(self, args, self.fs_op_queue)
  329.             self.consumer.daemon = True
  330.             self.consumer.start()
  331.  
  332. def get_argparser():
  333.     """
  334.    Set up the argument parser.
  335.    """
  336.     argparser = argparse.ArgumentParser(description="""
  337.        Move and rename movie files. This program tries to be intelligent about
  338.        using filenames to determine the real title of a movie, and uses the
  339.        real title to get the year from IMDB. It then moves the files to
  340.        OUTPUT_DIR, under the name "Film Title (####)" where #### is the year.
  341.        Default behavior is to move files, but they can be copied instead.
  342.        Default behavior is to place each renamed file in its own container
  343.        directory inside OUTPUT_DIR, but this can be disabled as well.
  344.        Filesystem operations (copying, moving, deleting, creating) are
  345.        asynchronous by default, but can be made synchronous with the -s flag.
  346.        """)
  347.     argparser.add_argument("-b", "--bare-file", action="store_true", help="""
  348.        Don't place movies in container directories inside OUTPUT_DIR.
  349.        """)
  350.     argparser.add_argument("-c", "--copy", action="store_true", help="""
  351.        Copy files instead of moving them.
  352.        """)
  353.     argparser.add_argument("-f", "--force", action="store_true", help="""
  354.        Forcibly overwrite any existing files and container directories. Note
  355.        that this is not affected by -n; it can pretend to forcibly overwrite
  356.        things.
  357.        """)
  358.     argparser.add_argument("-g", "--guess-only", action="store_true", help="""
  359.        Compute the best guess and query IMDB, but don't move the file(s).
  360.        """)
  361.     argparser.add_argument("-i", "--interactive", action="store_true", help="""
  362.        Display the guess and ask before renaming/moving each file.
  363.        """)
  364.     argparser.add_argument("-k", "--keep-going", action="store_true", help="""
  365.        If this program finds that the file it would create (and containing
  366.        directory) already exist, it should continue without asking the user.
  367.        This is useful for running on a large directory without having to
  368.        filter out the files that have already been processed.
  369.        """)
  370.     argparser.add_argument("-n", "--dry-run", action="store_true",
  371.         help="""
  372.        Run the program and show what would be done, but don't actually do
  373.        anything.
  374.        """)
  375.     argparser.add_argument("-r", "--remove", action="append", metavar="regex",
  376.         help="""
  377.        Case-insensitive regular expressions to be removed from filename
  378.        guesses when trying to find the title. (This flag may be used more than
  379.        once.) This is useful if the filenames have a lot of junk in them.
  380.        """)
  381.     argparser.add_argument("-s", "--synchronous", action="store_true", help="""
  382.        Do filesystem operations in a synchronous manner, rather than
  383.        asynchronous (default is asynchronous).
  384.        """)
  385.     argparser.add_argument("-t", "--totals", action="store_true", help="""
  386.        After execution is finished, print the total number of files
  387.        moved/renamed and the number of files skipped.
  388.        """)
  389.     argparser.add_argument("-v", "--verbose", action="store_true", help="""
  390.        Print verbose messages during program execution.
  391.        """)
  392.     argparser.add_argument("-o", "--output-dir", action="store", help="""
  393.        The directory in which this program should place the files it
  394.        processes.
  395.        """)
  396.     argparser.add_argument("files", action="store", metavar="movieFile",
  397.         type=str, nargs="*", help="""
  398.        The movie files to be processed.
  399.        """)
  400.     return argparser
  401.  
  402. def handler(signum, frame):
  403.     sys.exit(0)
  404.  
  405. def main():
  406.     args = get_argparser().parse_args()
  407.     signal.signal(signal.SIGINT, handler) # install signal handler
  408.     Producer(args).run()
  409.  
  410. if __name__ == "__main__":
  411.     main()
Advertisement
Add Comment
Please, Sign In to add comment