Change the directory walker to a more sane implementation
[animesorter.git] / animesorter2.py
index 605b13b..04073be 100755 (executable)
@@ -1,13 +1,41 @@
 #!/usr/bin/env python
 #!/usr/bin/env python
+# vim: set ts=4 sts=4 sw=4 textwidth=92 expandtab:
+
+"""
+The animesorter program
+
+This will sort arbitrary files into directories by regular expression.
+"""
+
+__author__    = "Ira W. Snyder (devel@irasnyder.com)"
+__copyright__ = "Copyright (c) 2006,2007 Ira W. Snyder (devel@irasnyder.com)"
+__license__   = "GNU GPL v2 (or, at your option, any later version)"
+
+#    animesorter2.py -- a regular expression file-sorting utility
+#
+#    Copyright (C) 2006,2007  Ira W. Snyder (devel@irasnyder.com)
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
-# 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
 
 import os
 import re
 import sys
 import errno
 import shutil
+import logging
 from optparse import OptionParser
 
 ### Default Configuration Variables ###
 from optparse import OptionParser
 
 ### Default Configuration Variables ###
@@ -21,69 +49,81 @@ class AnimeSorter2:
 
     def __init__(self, options):
         self.options = options
 
     def __init__(self, options):
         self.options = options
+        self.dict = None
+
+    def __valid_dict_line (self, line):
+        if len(line) <= 0:
+            return False
+
+        if '=' not in line:
+            return False
+
+        # Comment lines are not really valid
+        if re.match ('^(\s*#.*|\s*)$', line):
+            return False
+
+        # Make sure that there is a definition and it is valid
+        try:
+            (regex, directory) = line.split('=')
+            regex = regex.strip()
+            directory = directory.strip()
+        except:
+            return False
+
+        # Make sure they have length
+        if len(regex) <= 0 or len(directory) <= 0:
+            return False
+
+        # I guess that it's valid now
+        return True
 
     def parse_dict(self):
         """Parses a dictionary file containing the sort definitions in the form:
 
     def parse_dict(self):
         """Parses a dictionary file containing the sort definitions in the form:
-        DIRECTORY = PATTERN
+        REGEX_PATTERN = DIRECTORY
 
         Returns a list of tuples of the form (compiled_regex, to_directory)"""
 
         try:
             f = open(self.options.dict_file, 'r', 0)
             try:
 
         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()
+                raw_lines = f.readlines()
             finally:
                 f.close()
         except IOError:
             finally:
                 f.close()
         except IOError:
-            self.print_dict_fail (self.options.dict_file)
+            logging.critical ('Opening dictionary: %s FAILED' % self.options.dict_file)
             sys.exit()
 
             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):
+        ### Find all of the valid lines in the file
+        valid_lines = [l for l in raw_lines if self.__valid_dict_line (l)]
 
 
+        # Set up variable for result
         result = []
 
         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
+        ### Split each line into a tuple, and strip each element of spaces
+        for l in valid_lines:
+            (regex, directory) = l.split('=')
+            regex = regex.strip()
+            directory = directory.strip()
 
 
-            result.append((r, d))
+            # Fix up the directory if necessary
+            if directory[0] != '/':
+                directory = os.path.join (self.options.output_dir, directory)
 
 
-        return result
+            # Fix up the regex
+            if regex[0] != '^':
+                regex = '^' + regex
 
 
-    def get_matches(self, files, pattern):
-        """get_matches(files, pattern):
+            if regex[-1] != '$':
+                regex += '$'
 
 
-        files is type LIST
-        pattern is type sre.SRE_Pattern
+            # Store the result
+            result.append ( (re.compile (regex), directory) )
 
 
-        Returns a list of the files matching the pattern as type sre.SRE_Match."""
+        ### Give some information about the dictionary we are using
+        logging.info ('Successfully loaded %d records from %s\n' % \
+                (len(result), self.options.dict_file))
 
 
-        matches = [m for m in files if pattern.search(m)]
-        return matches
+        return tuple (result)
 
     def as_makedirs (self, dirname):
         """Call os.makedirs(dirname), but check first whether we are in pretend
 
     def as_makedirs (self, dirname):
         """Call os.makedirs(dirname), but check first whether we are in pretend
@@ -92,16 +132,16 @@ class AnimeSorter2:
         if not os.path.isdir (dirname):
 
             if self.options.pretend:
         if not os.path.isdir (dirname):
 
             if self.options.pretend:
-                self.print_dir_create_pretend (dirname)
+                logging.info ('Will create directory %s' % dirname)
                 return 0
 
             if self.get_user_choice ('Make directory?: %s' % (dirname, )):
 
                 try:
                     os.makedirs (dirname)
                 return 0
 
             if self.get_user_choice ('Make directory?: %s' % (dirname, )):
 
                 try:
                     os.makedirs (dirname)
-                    self.print_dir_create_suc (dirname)
+                    logging.info ('Created directory %s' % dirname)
                 except:
                 except:
-                    self.print_dir_create_fail (dirname)
+                    logging.critical ('Failed to create directory %s' % dirname)
                     return errno.EIO
 
         return 0
                     return errno.EIO
 
         return 0
@@ -114,15 +154,15 @@ class AnimeSorter2:
         dstname = os.path.join (todir, f)
 
         if self.options.pretend:
         dstname = os.path.join (todir, f)
 
         if self.options.pretend:
-            self.print_move_file_pretend (f, todir)
+            logging.info ('Will move %s to %s' % (f, todir))
             return 0 # success
 
         if self.get_user_choice ('Move file?: %s --> %s' % (srcname, dstname)):
             try:
                 shutil.move (srcname, dstname)
             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)
+                logging.info ('Moved %s to %s' % (f, todir))
             except:
             except:
-                self.print_move_file_fail (f, todir)
+                logging.critical ('FAILED to move %s to %s' % (f, todir))
                 return errno.EIO
 
         return 0
                 return errno.EIO
 
         return 0
@@ -133,9 +173,9 @@ class AnimeSorter2:
 
         ret = 0
 
 
         ret = 0
 
-        ## Check for a non-default directory
-        if todir[0] != '/':
-            todir = os.path.join(self.options.output_dir, todir)
+        # Leave immediately if we have nothing to do
+        if len(files) <= 0:
+            return ret
 
         ## Create the directory if it doesn't exist
         ret = self.as_makedirs (todir)
 
         ## Create the directory if it doesn't exist
         ret = self.as_makedirs (todir)
@@ -154,19 +194,11 @@ class AnimeSorter2:
 
         return ret
 
 
         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)]
+    def __dir_walker(self, rootdir, files):
 
 
-        ### 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)
+        for (r,d) in self.dict:
+            matches = [f for f in files if r.match(f)]
+            self.move_files (matches, rootdir, d)
 
     def get_user_choice(self, prompt):
 
 
     def get_user_choice(self, prompt):
 
@@ -190,76 +222,30 @@ class AnimeSorter2:
     def main(self):
 
         ## Print the program's header
     def main(self):
 
         ## Print the program's header
-        self.print_prog_header ()
+        logging.info ('Regular Expression File Sorter (aka animesorter)')
+        logging.info ('=' * 80)
+        logging.info ('Copyright (c) 2005-2007, Ira W. Snyder (devel@irasnyder.com)')
+        logging.info ('This program is licensed under the GNU GPL v2')
+        logging.info ('')
 
         ## Parse the dictionary
 
         ## Parse the dictionary
-        dict = self.parse_dict()
+        self.dict = self.parse_dict()
 
         if self.options.recursive:
             ## Start walking through directories
             for root, dirs, files in os.walk(self.options.start_dir):
 
         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)
+                self.__dir_walker(root, files)
         else:
         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, )
-
+            self.__dir_walker(self.options.start_dir, [f for f in
+                    os.listdir(self.options.start_dir) if os.path.isfile(f)])
 
 
 ### MAIN IS HERE ###
 def main():
 
 
 
 ### MAIN IS HERE ###
 def main():
 
+    # Set up the logger
+    logging.basicConfig (level=logging.INFO, format='%(message)s')
+
     ### Get the program options
     parser = OptionParser()
     parser.add_option('-q', '--quiet', action='store_true', dest='quiet',
     ### Get the program options
     parser = OptionParser()
     parser.add_option('-q', '--quiet', action='store_true', dest='quiet',
@@ -285,10 +271,13 @@ def main():
     options.start_dir = os.path.abspath(os.path.expanduser(options.start_dir))
     options.output_dir = os.path.abspath(os.path.expanduser(options.output_dir))
 
     options.start_dir = os.path.abspath(os.path.expanduser(options.start_dir))
     options.output_dir = os.path.abspath(os.path.expanduser(options.output_dir))
 
+    # Change the loglevel if we're running in quiet mode
+    if options.quiet:
+        logging.getLogger().setLevel (logging.CRITICAL)
+
     as = AnimeSorter2(options)
     as.main()
 
 if __name__ == '__main__':
     main ()
 
     as = AnimeSorter2(options)
     as.main()
 
 if __name__ == '__main__':
     main ()
 
-# vim: set ts=4 sw=4 sts=4 expandtab: