605b13b1062756d3dd337fa34086766c57c42c71
[animesorter.git] / animesorter2.py
1 #!/usr/bin/env python
2
3 # Copyright: Ira W. Snyder (devel@irasnyder.com)
4 # License: GNU General Public License v2 (or at your option, any later version)
5
6 import os
7 import re
8 import sys
9 import errno
10 import shutil
11 from optparse import OptionParser
12
13 ### Default Configuration Variables ###
14 DICT_FILE = os.path.join ('~','.config','animesorter2','animesorter.dict')
15 WORK_DIR =  os.path.join ('~','downloads','usenet')
16 SORT_DIR =  os.path.join ('/','data','Anime')
17 TYPES_REGEX = '.*(avi|ogm|mkv|mp4|\d\d\d)$'
18
19
20 class AnimeSorter2:
21
22     def __init__(self, options):
23         self.options = options
24
25     def parse_dict(self):
26         """Parses a dictionary file containing the sort definitions in the form:
27         DIRECTORY = PATTERN
28
29         Returns a list of tuples of the form (compiled_regex, to_directory)"""
30
31         try:
32             f = open(self.options.dict_file, 'r', 0)
33             try:
34                 data = f.read()
35             finally:
36                 f.close()
37         except IOError:
38             self.print_dict_fail (self.options.dict_file)
39             sys.exit()
40
41         ### Get a LIST containing each line in the file
42         lines = [l for l in data.split('\n') if len(l) > 0]
43
44         ### Remove comments / blank lines (zero length lines already removed above)
45         regex = re.compile ('^\s*#.*$')
46         lines = [l for l in lines if not re.match (regex, l)]
47         regex = re.compile ('^\s*$')
48         lines = [l for l in lines if not re.match (regex, l)]
49
50         ### Split each line into a tuple, and strip each element of spaces
51         result = self.split_lines(lines)
52         result = [(re.compile(r), d) for r, d in result]
53
54         ### Give some information about the dictionary we are using
55         self.print_dict_suc (self.options.dict_file, len(result))
56
57         return tuple(result)
58
59     def split_lines(self, lines):
60
61         result = []
62
63         for l in lines:
64
65             try:
66                 r, d = l.split('=')
67                 r = r.strip()
68                 d = d.strip()
69             except ValueError:
70                 self.print_dict_bad_line (l)
71                 continue
72
73             result.append((r, d))
74
75         return result
76
77     def get_matches(self, files, pattern):
78         """get_matches(files, pattern):
79
80         files is type LIST
81         pattern is type sre.SRE_Pattern
82
83         Returns a list of the files matching the pattern as type sre.SRE_Match."""
84
85         matches = [m for m in files if pattern.search(m)]
86         return matches
87
88     def as_makedirs (self, dirname):
89         """Call os.makedirs(dirname), but check first whether we are in pretend
90            mode, or if we're running interactively."""
91
92         if not os.path.isdir (dirname):
93
94             if self.options.pretend:
95                 self.print_dir_create_pretend (dirname)
96                 return 0
97
98             if self.get_user_choice ('Make directory?: %s' % (dirname, )):
99
100                 try:
101                     os.makedirs (dirname)
102                     self.print_dir_create_suc (dirname)
103                 except:
104                     self.print_dir_create_fail (dirname)
105                     return errno.EIO
106
107         return 0
108
109     def as_move_single_file (self, f, fromdir, todir):
110         """Move the single file named $f from the directory $fromdir to the
111            directory $todir"""
112
113         srcname = os.path.join (fromdir, f)
114         dstname = os.path.join (todir, f)
115
116         if self.options.pretend:
117             self.print_move_file_pretend (f, todir)
118             return 0 # success
119
120         if self.get_user_choice ('Move file?: %s --> %s' % (srcname, dstname)):
121             try:
122                 shutil.move (srcname, dstname)
123                 self.print_move_file_suc (f, todir)
124             except:
125                 self.print_move_file_fail (f, todir)
126                 return errno.EIO
127
128         return 0
129
130     def move_files(self, files, fromdir, todir):
131         """move_files(files, fromdir, todir):
132         Move the files represented by the list FILES from FROMDIR to TODIR"""
133
134         ret = 0
135
136         ## Check for a non-default directory
137         if todir[0] != '/':
138             todir = os.path.join(self.options.output_dir, todir)
139
140         ## Create the directory if it doesn't exist
141         ret = self.as_makedirs (todir)
142
143         if ret:
144             # we cannot continue, since we can't make the directory
145             return ret
146
147         ## Try to move every file, one at a time
148         for f in files:
149             ret = self.as_move_single_file (f, fromdir, todir)
150
151             if ret:
152                 # something bad happened when moving a file
153                 break
154
155         return ret
156
157     def __dir_walker(self, dict, root, dirs, files):
158
159         ## Get all of the files in the directory that are of the correct types
160         types_re = re.compile(TYPES_REGEX, re.IGNORECASE)
161         raw_matches = [f for f in files if types_re.match(f)]
162
163         ### Loop through the dictionary and try to move everything that matches
164         for regex, todir in dict:
165             matches = self.get_matches(raw_matches, regex)
166
167             ## Move the files if we've found some
168             if len(matches) > 0:
169                 self.move_files(matches, root, todir)
170
171     def get_user_choice(self, prompt):
172
173         # If we're not in interactive mode, then always return True
174         if self.options.interactive == False:
175             return True
176
177         # Get the user's choice since we're not in interactive mode
178         done = False
179         while not done:
180             s = raw_input('%s [y/N]: ' % (prompt, )).lower()
181
182             if s == 'y' or s == 'yes':
183                 return True
184
185             if s == 'n' or s == 'no' or s == '':
186                 return False
187
188             print 'Response not understood, try again.'
189
190     def main(self):
191
192         ## Print the program's header
193         self.print_prog_header ()
194
195         ## Parse the dictionary
196         dict = self.parse_dict()
197
198         if self.options.recursive:
199             ## Start walking through directories
200             for root, dirs, files in os.walk(self.options.start_dir):
201                 self.__dir_walker(dict, root, dirs, files)
202         else:
203             self.__dir_walker(dict, self.options.start_dir,
204                     [d for d in os.listdir(self.options.start_dir) if os.path.isdir(d)],
205                     [f for f in os.listdir(self.options.start_dir) if os.path.isfile(f)])
206
207     ############################################################################
208     ### Functions for the printing system
209     ############################################################################
210
211     def print_prog_header(self):
212         if self.options.quiet:
213             return
214
215         print 'Regular Expression File Sorter (aka animesorter)'
216         print '================================================================================'
217         print 'Copyright (c) 2005,2006, Ira W. Snyder (devel@irasnyder.com)'
218         print 'All rights reserved.'
219         print 'This program is licensed under the GNU GPL v2'
220         print
221
222     def print_move_file_suc(self, f, t):
223         if self.options.quiet:
224             return
225
226         print 'Moved %s to %s' % (f, t)
227
228     def print_move_file_fail(self, f, t):
229         print 'FAILED to move %s to %s' % (f, t)
230
231     def print_move_file_pretend (self, f, t):
232         print 'Will move %s to %s' % (f, t)
233
234     def print_dir_create_suc(self, d):
235         if self.options.quiet:
236             return
237
238         print 'Created directory %s' % (d, )
239
240     def print_dir_create_fail(self, d):
241         print 'Failed to create directory %s' % (d, )
242
243     def print_dir_create_pretend (self, d):
244         print 'Will create directory %s' % (d, )
245
246     def print_dict_suc(self, dic, num):
247         if self.options.quiet:
248             return
249
250         print 'Successfully loaded %d records from %s\n' % (num, dic)
251
252     def print_dict_fail(self, dic):
253         print 'Opening dictionary: %s FAILED' % (dic, )
254
255     def print_dict_bad_line(self, dic):
256         print 'Bad line in dictionary: %s' % (dic, )
257
258
259
260 ### MAIN IS HERE ###
261 def main():
262
263     ### Get the program options
264     parser = OptionParser()
265     parser.add_option('-q', '--quiet', action='store_true', dest='quiet',
266             default=False, help="Don't print status messages to stdout")
267     parser.add_option('-d', '--dict', dest='dict_file', default=DICT_FILE,
268             help='Read dictionary from FILE', metavar='FILE')
269     parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive',
270             default=True, help='don\'t run recursively')
271     parser.add_option('-s', '--start-dir', dest='start_dir', default=WORK_DIR,
272             help='Start running at directory DIR', metavar='DIR')
273     parser.add_option('-o', '--output-dir', dest='output_dir', default=SORT_DIR,
274             help='Sort files into DIR', metavar='DIR')
275     parser.add_option('-i', '--interactive', dest='interactive', default=False,
276             help='Confirm each move', action='store_true')
277     parser.add_option('-p', '--pretend', dest='pretend', default=False,
278             help='Enable pretend mode', action='store_true')
279
280     ## Parse the options
281     (options, args) = parser.parse_args()
282
283     ## Correct directories
284     options.dict_file = os.path.abspath(os.path.expanduser(options.dict_file))
285     options.start_dir = os.path.abspath(os.path.expanduser(options.start_dir))
286     options.output_dir = os.path.abspath(os.path.expanduser(options.output_dir))
287
288     as = AnimeSorter2(options)
289     as.main()
290
291 if __name__ == '__main__':
292     main ()
293
294 # vim: set ts=4 sw=4 sts=4 expandtab: