Fixes for Python 2.6
[animesorter.git] / animesorter2.py
1 #!/usr/bin/env python
2 # vim: set ts=4 sts=4 sw=4 textwidth=92 expandtab:
3
4 """
5 The animesorter program
6
7 This will sort arbitrary files into directories by regular expression.
8 """
9
10 __author__    = "Ira W. Snyder (devel@irasnyder.com)"
11 __copyright__ = "Copyright (c) 2006,2007 Ira W. Snyder (devel@irasnyder.com)"
12 __license__   = "GNU GPL v2 (or, at your option, any later version)"
13
14 #    animesorter2.py -- a regular expression file-sorting utility
15 #
16 #    Copyright (C) 2006,2007  Ira W. Snyder (devel@irasnyder.com)
17 #
18 #    This program is free software; you can redistribute it and/or modify
19 #    it under the terms of the GNU General Public License as published by
20 #    the Free Software Foundation; either version 2 of the License, or
21 #    (at your option) any later version.
22 #
23 #    This program is distributed in the hope that it will be useful,
24 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
25 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26 #    GNU General Public License for more details.
27 #
28 #    You should have received a copy of the GNU General Public License
29 #    along with this program; if not, write to the Free Software
30 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
31
32
33 import os
34 import re
35 import sys
36 import errno
37 import shutil
38 import logging
39 import optparse
40
41 PROGRAM_NAME = 'animesorter2'
42 PROGRAM_VERSION = '2.1.0'
43
44 ### Default Configuration Variables ###
45 DICT_FILE = os.path.join ('~','.config','animesorter2','animesorter.dict')
46 WORK_DIR =  os.path.join ('~','downloads','usenet')
47 SORT_DIR =  os.path.join ('/','data','Anime')
48
49 class AnimeSorter2:
50
51     def __init__(self, options):
52         self.options = options
53         self.dict = None
54
55     def __valid_dict_line (self, line):
56         if len(line) <= 0:
57             return False
58
59         if '=' not in line:
60             return False
61
62         # Comment lines are not really valid
63         if re.match ('^(\s*#.*|\s*)$', line):
64             return False
65
66         # Make sure that there is a definition and it is valid
67         try:
68             (regex, directory) = line.split('=')
69             regex = regex.strip()
70             directory = directory.strip()
71         except:
72             return False
73
74         # Make sure they have length
75         if len(regex) <= 0 or len(directory) <= 0:
76             return False
77
78         # I guess that it's valid now
79         return True
80
81     def parse_dict(self):
82         """Parses a dictionary file containing the sort definitions in the form:
83         REGEX_PATTERN = DIRECTORY
84
85         Returns a list of tuples of the form (compiled_regex, to_directory)"""
86
87         try:
88             f = open(self.options.dict_file, 'r', 0)
89             try:
90                 raw_lines = f.readlines()
91             finally:
92                 f.close()
93         except IOError:
94             logging.critical ('Opening dictionary: %s FAILED' % self.options.dict_file)
95             sys.exit(1)
96
97         ### Find all of the valid lines in the file
98         valid_lines = [l for l in raw_lines if self.__valid_dict_line (l)]
99
100         # Set up variable for result
101         result = []
102
103         ### Split each line into a tuple, and strip each element of spaces
104         for l in valid_lines:
105             (regex, directory) = l.split('=')
106             regex = regex.strip()
107             directory = directory.strip()
108
109             # Fix up the directory if necessary
110             if directory[0] != '/':
111                 directory = os.path.join (self.options.output_dir, directory)
112
113             # Fix up the regex
114             if regex[0] != '^':
115                 regex = '^' + regex
116
117             if regex[-1] != '$':
118                 regex += '$'
119
120             # Store the result
121             result.append ( (re.compile (regex), directory) )
122
123         ### Give some information about the dictionary we are using
124         logging.info ('Successfully loaded %d records from %s\n' % \
125                 (len(result), self.options.dict_file))
126
127         return tuple (result)
128
129     def as_makedirs (self, dirname):
130         """Call os.makedirs(dirname), but check first whether we are in pretend
131            mode, or if we're running interactively."""
132
133         if not os.path.isdir (dirname):
134
135             if self.options.pretend:
136                 logging.info ('Will create directory %s' % dirname)
137                 return 0
138
139             if self.get_user_choice ('Make directory?: %s' % (dirname, )):
140
141                 try:
142                     os.makedirs (dirname)
143                     logging.info ('Created directory %s' % dirname)
144                 except:
145                     logging.critical ('Failed to create directory %s' % dirname)
146                     return errno.EIO
147
148         return 0
149
150     def as_move_single_file (self, f, fromdir, todir):
151         """Move the single file named $f from the directory $fromdir to the
152            directory $todir"""
153
154         srcname = os.path.join (fromdir, f)
155         dstname = os.path.join (todir, f)
156
157         if self.options.pretend:
158             logging.info ('Will move %s to %s' % (f, todir))
159             return 0 # success
160
161         if self.get_user_choice ('Move file?: %s --> %s' % (srcname, dstname)):
162             try:
163                 shutil.move (srcname, dstname)
164                 logging.info ('Moved %s to %s' % (f, todir))
165             except:
166                 logging.critical ('FAILED to move %s to %s' % (f, todir))
167                 return errno.EIO
168
169         return 0
170
171     def move_files(self, files, fromdir, todir):
172         """move_files(files, fromdir, todir):
173         Move the files represented by the list FILES from FROMDIR to TODIR"""
174
175         ret = 0
176
177         # Leave immediately if we have nothing to do
178         if len(files) <= 0:
179             return ret
180
181         ## Create the directory if it doesn't exist
182         ret = self.as_makedirs (todir)
183
184         if ret:
185             # we cannot continue, since we can't make the directory
186             return ret
187
188         ## Try to move every file, one at a time
189         for f in files:
190             ret = self.as_move_single_file (f, fromdir, todir)
191
192             if ret:
193                 # something bad happened when moving a file
194                 break
195
196         return ret
197
198     def __dir_walker(self, rootdir, files):
199
200         for (r,d) in self.dict:
201             matches = [f for f in files if r.match(f)]
202             self.move_files (matches, rootdir, d)
203
204     def get_user_choice(self, prompt):
205
206         # If we're not in interactive mode, then always return True
207         if self.options.interactive == False:
208             return True
209
210         # Get the user's choice since we're not in interactive mode
211         done = False
212         while not done:
213             s = raw_input('%s [y/N]: ' % (prompt, )).lower()
214
215             if s == 'y' or s == 'yes':
216                 return True
217
218             if s == 'n' or s == 'no' or s == '':
219                 return False
220
221             print 'Response not understood, try again.'
222
223     def main(self):
224
225         ## Print the program's header
226         logging.info ('Regular Expression File Sorter (aka animesorter)')
227         logging.info ('=' * 80)
228         logging.info ('Copyright (c) 2005-2007, Ira W. Snyder (devel@irasnyder.com)')
229         logging.info ('This program is licensed under the GNU GPL v2')
230         logging.info ('')
231
232         ## Parse the dictionary
233         self.dict = self.parse_dict()
234
235         if self.options.recursive:
236             ## Start walking through directories
237             for root, dirs, files in os.walk(self.options.start_dir):
238                 self.__dir_walker(root, files)
239         else:
240             self.__dir_walker(self.options.start_dir, [f for f in
241                     os.listdir(self.options.start_dir) if os.path.isfile(f)])
242
243
244 ### MAIN IS HERE ###
245 def main():
246
247     # Set up the logger
248     logging.basicConfig (level=logging.INFO, format='%(message)s')
249
250     ### Get the program options
251     parser = optparse.OptionParser()
252     parser.add_option('-q', '--quiet', action='store_true', dest='quiet',
253             default=False, help="Don't print status messages to stdout")
254     parser.add_option('-d', '--dict', dest='dict_file', default=DICT_FILE,
255             help='Read dictionary from FILE', metavar='FILE')
256     parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive',
257             default=True, help='don\'t run recursively')
258     parser.add_option('-s', '--start-dir', dest='start_dir', default=WORK_DIR,
259             help='Start running at directory DIR', metavar='DIR')
260     parser.add_option('-o', '--output-dir', dest='output_dir', default=SORT_DIR,
261             help='Sort files into DIR', metavar='DIR')
262     parser.add_option('-i', '--interactive', dest='interactive', default=False,
263             help='Confirm each move', action='store_true')
264     parser.add_option('-p', '--pretend', dest='pretend', default=False,
265             help='Enable pretend mode', action='store_true')
266     parser.add_option('-e', '--editor', dest='run_editor', default=False,
267             help='Run editor on dictionary', action='store_true')
268     parser.add_option('-V', '--version', dest='version', default=False,
269             help='Show version and exit', action='store_true')
270
271     ## Parse the options
272     (options, args) = parser.parse_args()
273
274     ## Correct directories
275     options.dict_file = os.path.abspath(os.path.expanduser(options.dict_file))
276     options.start_dir = os.path.abspath(os.path.expanduser(options.start_dir))
277     options.output_dir = os.path.abspath(os.path.expanduser(options.output_dir))
278
279     ## Show version if necessary
280     if options.version:
281         print '%s - %s' % (PROGRAM_NAME, PROGRAM_VERSION)
282         print
283         print 'Copyright (c) 2005-2007 Ira W. Snyder (devel@irasnyder.com)'
284         print 'This program comes with ABSOLUTELY NO WARRANTY.'
285         print 'This is free software, and you are welcome to redistribute it'
286         print 'under certain conditions. See the file COPYING for details.'
287
288         sys.exit (0)
289
290     ## Run editor if necessary
291     if options.run_editor:
292         editor = os.getenv ('EDITOR')
293
294         if editor != None:
295             os.system ('%s %s' % (editor, options.dict_file))
296         else:
297             logging.critical ('Default editor could not be found!')
298             sys.exit (1)
299
300         sys.exit (0) # successful, but exit anyway
301
302     # Change the loglevel if we're running in quiet mode
303     if options.quiet:
304         logging.getLogger().setLevel (logging.CRITICAL)
305
306     sorter = AnimeSorter2(options)
307     sorter.main()
308
309 if __name__ == '__main__':
310     main ()
311