Initial commit of the animesorter project.
authorIra W. Snyder <devel@irasnyder.com>
Tue, 9 Jan 2007 21:43:14 +0000 (13:43 -0800)
committerIra W. Snyder <devel@irasnyder.com>
Tue, 9 Jan 2007 21:43:14 +0000 (13:43 -0800)
Signed-off-by: Ira W. Snyder <devel@irasnyder.com>
animesorter.dict [new file with mode: 0644]
animesorter2.py [new file with mode: 0755]

diff --git a/animesorter.dict b/animesorter.dict
new file mode 100644 (file)
index 0000000..5cb8954
--- /dev/null
@@ -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 (executable)
index 0000000..9dbfea0
--- /dev/null
@@ -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: