From a7a4476a72ecbbc13cf35ed168403a8c9a74fecd Mon Sep 17 00:00:00 2001 From: "Ira W. Snyder" Date: Tue, 9 Jan 2007 13:43:14 -0800 Subject: [PATCH 1/1] Initial commit of the animesorter project. Signed-off-by: Ira W. Snyder --- animesorter.dict | 19 +++ animesorter2.py | 294 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 animesorter.dict create mode 100755 animesorter2.py diff --git a/animesorter.dict b/animesorter.dict new file mode 100644 index 0000000..5cb8954 --- /dev/null +++ b/animesorter.dict @@ -0,0 +1,19 @@ +# Sample dictionary file for animesorter. +# Available from http://irasnyder.com/svn/programming/projects/animesorter +# Please copy this file to ~/.config/animesorter2/animesorter.dict + +# You will probably want to edit this file and fill it in with your +# own values. +# +# All lines beginning with a hash (#) are comments, and are ignored. +# Everything else will be attempted to be parsed, and the program +# will warn you if it fails to parse a record. + +# Shows use of a relative path. +# The $SORT_DIR gets appended to the front of this. +\[Something\]_some_file_.+_\[.+\].avi = somefolder + +# Shows use of an absolute path. +# The files matching the pattern are deposited in the +# folder to the right of the equal sign. +\[Something\]_some_file_.+_\[.+\].avi = /absolute/path/instead diff --git a/animesorter2.py b/animesorter2.py new file mode 100755 index 0000000..9dbfea0 --- /dev/null +++ b/animesorter2.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python + +# Copyright: Ira W. Snyder (devel@irasnyder.com) +# License: GNU General Public License v2 (or at your option, any later version) + +import os +import re +import sys +import errno +import shutil +from optparse import OptionParser + +### Default Configuration Variables ### +DICT_FILE = '~/.config/animesorter2/animesorter.dict' +WORK_DIR = '~/downloads/usenet' +SORT_DIR = '/data/Anime' +TYPES_REGEX = '.*(avi|ogm|mkv|mp4|\d\d\d)$' + + +class AnimeSorter2: + + def __init__(self, options): + self.options = options + + def parse_dict(self): + """Parses a dictionary file containing the sort definitions in the form: + DIRECTORY = PATTERN + + Returns a list of tuples of the form (compiled_regex, to_directory)""" + + try: + f = open(self.options.dict_file, 'r', 0) + try: + data = f.read() + finally: + f.close() + except IOError: + self.print_dict_fail (self.options.dict_file) + sys.exit() + + ### Get a LIST containing each line in the file + lines = [l for l in data.split('\n') if len(l) > 0] + + ### Remove comments / blank lines (zero length lines already removed above) + regex = re.compile ('^\s*#.*$') + lines = [l for l in lines if not re.match (regex, l)] + regex = re.compile ('^\s*$') + lines = [l for l in lines if not re.match (regex, l)] + + ### Split each line into a tuple, and strip each element of spaces + result = self.split_lines(lines) + result = [(re.compile(r), d) for r, d in result] + + ### Give some information about the dictionary we are using + self.print_dict_suc (self.options.dict_file, len(result)) + + return tuple(result) + + def split_lines(self, lines): + + result = [] + + for l in lines: + + try: + r, d = l.split('=') + r = r.strip() + d = d.strip() + except ValueError: + self.print_dict_bad_line (l) + continue + + result.append((r, d)) + + return result + + def get_matches(self, files, pattern): + """get_matches(files, pattern): + + files is type LIST + pattern is type sre.SRE_Pattern + + Returns a list of the files matching the pattern as type sre.SRE_Match.""" + + matches = [m for m in files if pattern.search(m)] + return matches + + def as_makedirs (self, dirname): + """Call os.makedirs(dirname), but check first whether we are in pretend + mode, or if we're running interactively.""" + + if not os.path.isdir (dirname): + + if self.options.pretend: + self.print_dir_create_pretend (dirname) + return 0 + + if self.get_user_choice ('Make directory?: %s' % (dirname, )): + + try: + os.makedirs (dirname) + self.print_dir_create_suc (dirname) + except: + self.print_dir_create_fail (dirname) + return errno.EIO + + return 0 + + def as_move_single_file (self, f, fromdir, todir): + """Move the single file named $f from the directory $fromdir to the + directory $todir""" + + srcname = os.path.join (fromdir, f) + dstname = os.path.join (todir, f) + + if self.options.pretend: + self.print_move_file_pretend (f, todir) + return 0 # success + + if self.get_user_choice ('Move file?: %s --> %s' % (srcname, dstname)): + try: + shutil.move (srcname, dstname) + self.print_move_file_suc (f, todir) + except: + self.print_move_file_fail (f, todir) + return errno.EIO + + return 0 + + def move_files(self, files, fromdir, todir): + """move_files(files, fromdir, todir): + Move the files represented by the list FILES from FROMDIR to TODIR""" + + ret = 0 + + ## Check for a non-default directory + if todir[0] != '/': + todir = os.path.join(self.options.output_dir, todir) + + ## Create the directory if it doesn't exist + ret = self.as_makedirs (todir) + + if ret: + # we cannot continue, since we can't make the directory + return ret + + ## Try to move every file, one at a time + for f in files: + ret = self.as_move_single_file (f, fromdir, todir) + + if ret: + # something bad happened when moving a file + break + + return ret + + def __dir_walker(self, dict, root, dirs, files): + + ## Get all of the files in the directory that are of the correct types + types_re = re.compile(TYPES_REGEX, re.IGNORECASE) + raw_matches = [f for f in files if types_re.match(f)] + + ### Loop through the dictionary and try to move everything that matches + for regex, todir in dict: + matches = self.get_matches(raw_matches, regex) + + ## Move the files if we've found some + if len(matches) > 0: + self.move_files(matches, root, todir) + + def get_user_choice(self, prompt): + + # If we're not in interactive mode, then always return True + if self.options.interactive == False: + return True + + # Get the user's choice since we're not in interactive mode + done = False + while not done: + s = raw_input('%s [y/N]: ' % (prompt, )).lower() + + if s == 'y' or s == 'yes': + return True + + if s == 'n' or s == 'no' or s == '': + return False + + print 'Response not understood, try again.' + + def main(self): + + ## Print the program's header + self.print_prog_header () + + ## Parse the dictionary + dict = self.parse_dict() + + if self.options.recursive: + ## Start walking through directories + for root, dirs, files in os.walk(self.options.start_dir): + self.__dir_walker(dict, root, dirs, files) + else: + self.__dir_walker(dict, self.options.start_dir, + [d for d in os.listdir(self.options.start_dir) if os.path.isdir(d)], + [f for f in os.listdir(self.options.start_dir) if os.path.isfile(f)]) + + ############################################################################ + ### Functions for the printing system + ############################################################################ + + def print_prog_header(self): + if self.options.quiet: + return + + print 'Regular Expression File Sorter (aka animesorter)' + print '================================================================================' + print 'Copyright (c) 2005,2006, Ira W. Snyder (devel@irasnyder.com)' + print 'All rights reserved.' + print 'This program is licensed under the GNU GPL v2' + print + + def print_move_file_suc(self, f, t): + if self.options.quiet: + return + + print 'Moved %s to %s' % (f, t) + + def print_move_file_fail(self, f, t): + print 'FAILED to move %s to %s' % (f, t) + + def print_move_file_pretend (self, f, t): + print 'Will move %s to %s' % (f, t) + + def print_dir_create_suc(self, d): + if self.options.quiet: + return + + print 'Created directory %s' % (d, ) + + def print_dir_create_fail(self, d): + print 'Failed to create directory %s' % (d, ) + + def print_dir_create_pretend (self, d): + print 'Will create directory %s' % (d, ) + + def print_dict_suc(self, dic, num): + if self.options.quiet: + return + + print 'Successfully loaded %d records from %s\n' % (num, dic) + + def print_dict_fail(self, dic): + print 'Opening dictionary: %s FAILED' % (dic, ) + + def print_dict_bad_line(self, dic): + print 'Bad line in dictionary: %s' % (dic, ) + + + +### MAIN IS HERE ### +def main(): + + ### Get the program options + parser = OptionParser() + parser.add_option('-q', '--quiet', action='store_true', dest='quiet', + default=False, help="Don't print status messages to stdout") + parser.add_option('-d', '--dict', dest='dict_file', default=DICT_FILE, + help='Read dictionary from FILE', metavar='FILE') + parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive', + default=True, help='don\'t run recursively') + parser.add_option('-s', '--start-dir', dest='start_dir', default=WORK_DIR, + help='Start running at directory DIR', metavar='DIR') + parser.add_option('-o', '--output-dir', dest='output_dir', default=SORT_DIR, + help='Sort files into DIR', metavar='DIR') + parser.add_option('-i', '--interactive', dest='interactive', default=False, + help='Confirm each move', action='store_true') + parser.add_option('-p', '--pretend', dest='pretend', default=False, + help='Enable pretend mode', action='store_true') + + ## Parse the options + (options, args) = parser.parse_args() + + ## Correct directories + options.dict_file = os.path.abspath(os.path.expanduser(options.dict_file)) + options.start_dir = os.path.abspath(os.path.expanduser(options.start_dir)) + options.output_dir = os.path.abspath(os.path.expanduser(options.output_dir)) + + as = AnimeSorter2(options) + as.main() + +if __name__ == '__main__': + main () + +# vim: set ts=4 sw=4 sts=4 expandtab: -- 2.25.1