Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python
- """
- This program automatically renames movie files based on its best guess at
- their title and year, based on IMDB.
- In order to make interactive mode nicer (and save precious time in
- non-interactive mode), this program uses two threads to do the work. The first
- thread is the producer, and it fills a shared queue with operations to
- manipulate the filesystem (e.g., copy a file, or create a directory, etc.). The
- second thread is the consumer, whose job it is to take filesystem operations
- from the queue and perform them. This design makes it possible for interactive
- mode to progress at its own pace without having to wait for each file to be
- copied, since that can take minutes for large files.
- The producer waits around for the consumer thread to finish its work (after
- letting it know that it's finished producing). It also catches any Ctrl-C
- signal (SIGINT) and exits all threads.
- """
- import argparse
- from glob import iglob
- import os
- import Queue
- import re
- import shutil
- import signal # always deliver signals to the main thread
- import sys
- from threading import Thread
- try:
- from imdbpie import Imdb
- except ImportError:
- print "Please install the imdbpie library: pip install imdbpie"
- sys.exit(1)
- class Consumer(Thread):
- """
- Consumer to perform filesystem operations. Each operation is a closure.
- """
- def __init__(self, producer, args, fs_op_queue):
- super(Consumer, self).__init__()
- self.producer = producer
- self.args = args
- self.fs_op_queue = fs_op_queue
- def run(self):
- while self.producer.is_working():
- self.consume()
- while not self.fs_op_queue.empty():
- self.consume()
- def consume(self):
- try:
- fs_op = self.fs_op_queue.get(timeout=1)
- fs_op()
- except Queue.Empty:
- pass
- class Producer():
- """
- Class to do most of the processing, and produce filesystem operations to be
- performed by the Consumer. Each operation is passed as a closure.
- """
- def get_imdb_title_from_guess(self, title_guess):
- """
- Use the IMDB API to get the year from a title. I'm assuming that the
- first result is the best match for the search, but I don't know if this
- is documented anywhere.
- """
- title_format = "%s (%s)"
- results_list = self.imdb.search_for_title(title_guess)
- if len(results_list):
- result = results_list[0]
- imdb_title = title_format % (result["title"], result["year"])
- self.log_guess("Using \"%s\" as best result for guess \"%s\"" %
- (imdb_title, title_guess))
- return (True, imdb_title)
- else:
- print "ERROR: could not get year for \"%s\"" % title_guess
- return (False, "")
- def get_best_guess(self, filename):
- """
- Given a filename and args, compute the best guess at the title.
- """
- guess = filename
- for manipulate in self.guess_manipulations:
- guess = manipulate(guess)
- self.log_guess("Computed best guess of \"%s\" for \"%s\"" %
- (guess, filename))
- return guess
- def log_guess(self, string):
- if self.args.verbose or self.args.guess_only:
- print string
- def create_container(self, container_dir):
- """
- Produce the file operation to create a particular container directory,
- if necessary. Does not actually perform the file operation.
- """
- if os.path.exists(container_dir):
- if os.path.isdir(container_dir):
- # path exists, and is directory; this is fine
- return True
- # not a directory:
- self.log_fs_problem("\"%s\" already exists but is not a directory" %
- container_dir)
- if not self.args.force:
- return False
- else:
- # operation should be forced
- self.log_fs_operation("Remove: \"%s\"" % container_dir)
- def fs_op():
- os.remove(container_dir)
- self.do_fs_operation(fs_op)
- self.log_fs_operation("Create: container \"%s\"" % container_dir)
- def fs_op():
- # if args.output_dir doesn't exist, this will create it
- os.makedirs(container_dir)
- self.do_fs_operation(fs_op)
- return True
- def transfer_file(self, src_file, dest_file):
- """
- Produce the fs_ops to move/copy src_file to dest_file.
- """
- if os.path.exists(dest_file):
- self.log_fs_problem("File already exists: \"%s\"" % dest_file)
- if not self.args.force:
- return False
- else:
- # operation should be forced
- self.log_fs_operation("Remove: \"%s\"" % dest_file)
- def fs_op():
- if os.path.isdir(dest_file):
- shutil.rmtree(dest_file)
- else:
- os.remove(dest_file)
- self.do_fs_operation(fs_op)
- self.log_fs_operation("%s: \"%s\" -> \"%s\"" %
- ("Copy" if self.args.copy else "Move",
- src_file,
- dest_file))
- def fs_op():
- if self.args.copy:
- shutil.copy(src_file, dest_file)
- else:
- shutil.move(src_file, dest_file)
- self.do_fs_operation(fs_op)
- return True
- def do_fs_operation(self, fs_op):
- if not self.args.dry_run:
- if not self.fs_op_queue:
- fs_op()
- else:
- self.fs_op_queue.put(fs_op)
- def log_fs_operation(self, string):
- if self.args.verbose or self.args.dry_run:
- print string
- def log_fs_problem(self, string):
- if self.args.verbose or self.args.dry_run or not self.args.force:
- print string
- def get_dest_dir(self, pretty_name):
- dest_dir = None
- if self.args.bare_file:
- dest_dir = self.output_dir
- else:
- dest_dir = os.path.join(self.output_dir, pretty_name)
- return dest_dir
- def process_file(self, filepath):
- """
- Process a given movie file.
- """
- if not os.path.isfile(filepath):
- print "Not a file: \"%s\"" % filepath
- return False
- filename_with_ext = os.path.basename(filepath)
- (filename, ext) = os.path.splitext(filename_with_ext)
- guess = self.get_best_guess(filename)
- (b_success, pretty_name) = self.get_imdb_title_from_guess(guess)
- for c in "\/:*?\"<>|":
- # clean disallowed chars from windows filenames
- pretty_name = pretty_name.replace(c, " ")
- if self.args.guess_only or not b_success:
- return b_success
- dest_dir = self.get_dest_dir(pretty_name)
- dest_file = os.path.join(dest_dir, pretty_name + ext)
- if self.args.keep_going and os.path.isfile(dest_file):
- return True
- if self.args.interactive:
- # only continue if the user says to
- action = "copy" if self.args.copy else "move"
- print ("\nWill %s \"%s\"\n\t-> \"%s\"." %
- (action, filepath, dest_file)),
- cont = raw_input("\nContinue? [Y/n] ")
- if not re.match("^(y(es)?)$|^$", cont, re.IGNORECASE):
- return False
- # take advantage of short-circuiting operators to chain together the
- # next few calls, stop at any failure, and return the final success or
- # failure value
- return (self.create_container(dest_dir) and
- self.transfer_file(filepath, dest_file))
- def run(self):
- """
- Move/copy all the movie files into containing directories in the
- output_dir.
- """
- # process the file args
- total_renamed = 0
- total_skipped = 0
- for movie_file in self.files:
- b_success = self.process_file(movie_file)
- if self.args.totals:
- if b_success:
- total_renamed += 1
- else:
- total_skipped += 1
- if self.args.totals:
- total_processed = total_renamed + total_skipped
- print ("%d file%s processed: %d renamed, %d skipped" %
- (total_processed,
- "s" if total_processed > 1 else "",
- total_renamed,
- total_skipped))
- self.b_is_working = False
- if self.consumer:
- if self.consumer.is_alive():
- print "Do NOT exit, filesystem thread is still processing files!"
- while self.consumer.is_alive():
- self.consumer.join(0.5)
- def build_regex_manipulation(self, regex_pattern, sub_string):
- """
- Given a tuple of a pattern and a string to substitute it with, return a
- function that takes a guess and substitutes all instances of the
- pattern. This avoids recompiling regular expressions unnecessarily,
- since it compiles them each once during initialization, then all the
- closures are shared between iterations of the main processing loop.
- """
- re_obj = re.compile(regex_pattern, re.IGNORECASE)
- def manipulate(guess):
- return re_obj.sub(sub_string, guess)
- return manipulate
- def is_working(self):
- return self.b_is_working
- def __init__(self, args):
- self.args = args
- self.imdb = Imdb()
- # only set up Tkinter if we're going to use it
- if not self.args.files or not self.args.output_dir:
- try:
- from Tkinter import Tk
- from tkFileDialog import askopenfilename, askdirectory
- except ImportError:
- print "Please install the Tkinter library for graphical file and directory choosers."
- sys.exit(1)
- root = Tk()
- root.withdraw() # keep the root window from appearing
- # get movie files to process
- self.files = []
- if not self.args.files:
- self.files = askopenfilename(
- multiple=True,
- title="What movies do you want to rename?")
- if not self.files:
- sys.exit(0)
- else:
- # use glob to expand wildcards, since Windows sucks and doesn't do
- # it for us
- self.files = [filename for file_list in map(iglob, self.args.files)
- for filename in file_list]
- # get output directory
- if not self.args.output_dir:
- self.output_dir = askdirectory(title="Choose the output directory")
- if not self.output_dir:
- sys.exit(0)
- else:
- self.output_dir = self.args.output_dir
- # set up guess manipulations
- self.guess_manipulations = []
- if self.args.remove:
- # remove user-specified patterns
- self.guess_manipulations.extend(map(
- lambda pattern: self.build_regex_manipulation(pattern, ""),
- self.args.remove))
- # remove some common junk
- self.guess_manipulations.extend(map(
- lambda (pat, sub): self.build_regex_manipulation(pat, sub),
- [("[^A-Za-z0-9']+", " "), # remove non-alphanumeric (replace with space)
- ("(\\d){3,4}[ip]", ""), # remove 1080p, 720i, etc.
- ("(dvd)|(bluray)", ""), # remove DVD, bluray
- ("(19\\d{2})|(2\\d{3})", ""), # remove year, if already in filename
- ("\\s+", " "), # collapse whitespace
- ]))
- self.guess_manipulations.append(lambda string: string.strip()) # trim leading and trailing whitespace
- self.b_is_working = True
- # set up shared fs_op queue
- self.fs_op_queue = None
- if not self.args.dry_run and not self.args.synchronous:
- self.fs_op_queue = Queue.Queue(maxsize=0) # to be shared by producer, consumer
- # set up consumer
- self.consumer = None
- if self.fs_op_queue:
- self.consumer = Consumer(self, args, self.fs_op_queue)
- self.consumer.daemon = True
- self.consumer.start()
- def get_argparser():
- """
- Set up the argument parser.
- """
- argparser = argparse.ArgumentParser(description="""
- Move and rename movie files. This program tries to be intelligent about
- using filenames to determine the real title of a movie, and uses the
- real title to get the year from IMDB. It then moves the files to
- OUTPUT_DIR, under the name "Film Title (####)" where #### is the year.
- Default behavior is to move files, but they can be copied instead.
- Default behavior is to place each renamed file in its own container
- directory inside OUTPUT_DIR, but this can be disabled as well.
- Filesystem operations (copying, moving, deleting, creating) are
- asynchronous by default, but can be made synchronous with the -s flag.
- """)
- argparser.add_argument("-b", "--bare-file", action="store_true", help="""
- Don't place movies in container directories inside OUTPUT_DIR.
- """)
- argparser.add_argument("-c", "--copy", action="store_true", help="""
- Copy files instead of moving them.
- """)
- argparser.add_argument("-f", "--force", action="store_true", help="""
- Forcibly overwrite any existing files and container directories. Note
- that this is not affected by -n; it can pretend to forcibly overwrite
- things.
- """)
- argparser.add_argument("-g", "--guess-only", action="store_true", help="""
- Compute the best guess and query IMDB, but don't move the file(s).
- """)
- argparser.add_argument("-i", "--interactive", action="store_true", help="""
- Display the guess and ask before renaming/moving each file.
- """)
- argparser.add_argument("-k", "--keep-going", action="store_true", help="""
- If this program finds that the file it would create (and containing
- directory) already exist, it should continue without asking the user.
- This is useful for running on a large directory without having to
- filter out the files that have already been processed.
- """)
- argparser.add_argument("-n", "--dry-run", action="store_true",
- help="""
- Run the program and show what would be done, but don't actually do
- anything.
- """)
- argparser.add_argument("-r", "--remove", action="append", metavar="regex",
- help="""
- Case-insensitive regular expressions to be removed from filename
- guesses when trying to find the title. (This flag may be used more than
- once.) This is useful if the filenames have a lot of junk in them.
- """)
- argparser.add_argument("-s", "--synchronous", action="store_true", help="""
- Do filesystem operations in a synchronous manner, rather than
- asynchronous (default is asynchronous).
- """)
- argparser.add_argument("-t", "--totals", action="store_true", help="""
- After execution is finished, print the total number of files
- moved/renamed and the number of files skipped.
- """)
- argparser.add_argument("-v", "--verbose", action="store_true", help="""
- Print verbose messages during program execution.
- """)
- argparser.add_argument("-o", "--output-dir", action="store", help="""
- The directory in which this program should place the files it
- processes.
- """)
- argparser.add_argument("files", action="store", metavar="movieFile",
- type=str, nargs="*", help="""
- The movie files to be processed.
- """)
- return argparser
- def handler(signum, frame):
- sys.exit(0)
- def main():
- args = get_argparser().parse_args()
- signal.signal(signal.SIGINT, handler) # install signal handler
- Producer(args).run()
- if __name__ == "__main__":
- main()
Advertisement
Add Comment
Please, Sign In to add comment