2 # vim: set ts=4 sts=4 sw=4 textwidth=92 expandtab:
5 The animesorter program
7 This will sort arbitrary files into directories by regular expression.
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)"
14 # animesorter2.py -- a regular expression file-sorting utility
16 # Copyright (C) 2006,2007 Ira W. Snyder (devel@irasnyder.com)
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.
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.
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
41 PROGRAM_NAME = 'animesorter2'
42 PROGRAM_VERSION = '2.1.0'
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')
51 def __init__(self, options):
52 self.options = options
55 def __valid_dict_line (self, line):
62 # Comment lines are not really valid
63 if re.match ('^(\s*#.*|\s*)$', line):
66 # Make sure that there is a definition and it is valid
68 (regex, directory) = line.split('=')
70 directory = directory.strip()
74 # Make sure they have length
75 if len(regex) <= 0 or len(directory) <= 0:
78 # I guess that it's valid now
82 """Parses a dictionary file containing the sort definitions in the form:
83 REGEX_PATTERN = DIRECTORY
85 Returns a list of tuples of the form (compiled_regex, to_directory)"""
88 f = open(self.options.dict_file, 'r', 0)
90 raw_lines = f.readlines()
94 logging.critical ('Opening dictionary: %s FAILED' % self.options.dict_file)
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)]
100 # Set up variable for result
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()
109 # Fix up the directory if necessary
110 if directory[0] != '/':
111 directory = os.path.join (self.options.output_dir, directory)
121 result.append ( (re.compile (regex), directory) )
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))
127 return tuple (result)
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."""
133 if not os.path.isdir (dirname):
135 if self.options.pretend:
136 logging.info ('Will create directory %s' % dirname)
139 if self.get_user_choice ('Make directory?: %s' % (dirname, )):
142 os.makedirs (dirname)
143 logging.info ('Created directory %s' % dirname)
145 logging.critical ('Failed to create directory %s' % dirname)
150 def as_move_single_file (self, f, fromdir, todir):
151 """Move the single file named $f from the directory $fromdir to the
154 srcname = os.path.join (fromdir, f)
155 dstname = os.path.join (todir, f)
157 if self.options.pretend:
158 logging.info ('Will move %s to %s' % (f, todir))
161 if self.get_user_choice ('Move file?: %s --> %s' % (srcname, dstname)):
163 shutil.move (srcname, dstname)
164 logging.info ('Moved %s to %s' % (f, todir))
166 logging.critical ('FAILED to move %s to %s' % (f, todir))
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"""
177 # Leave immediately if we have nothing to do
181 ## Create the directory if it doesn't exist
182 ret = self.as_makedirs (todir)
185 # we cannot continue, since we can't make the directory
188 ## Try to move every file, one at a time
190 ret = self.as_move_single_file (f, fromdir, todir)
193 # something bad happened when moving a file
198 def __dir_walker(self, rootdir, files):
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)
204 def get_user_choice(self, prompt):
206 # If we're not in interactive mode, then always return True
207 if self.options.interactive == False:
210 # Get the user's choice since we're not in interactive mode
213 s = raw_input('%s [y/N]: ' % (prompt, )).lower()
215 if s == 'y' or s == 'yes':
218 if s == 'n' or s == 'no' or s == '':
221 print 'Response not understood, try again.'
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')
232 ## Parse the dictionary
233 self.dict = self.parse_dict()
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)
240 self.__dir_walker(self.options.start_dir, [f for f in
241 os.listdir(self.options.start_dir) if os.path.isfile(f)])
248 logging.basicConfig (level=logging.INFO, format='%(message)s')
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')
272 (options, args) = parser.parse_args()
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))
279 ## Show version if necessary
281 print '%s - %s' % (PROGRAM_NAME, PROGRAM_VERSION)
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.'
290 ## Run editor if necessary
291 if options.run_editor:
292 editor = os.getenv ('EDITOR')
295 os.system ('%s %s' % (editor, options.dict_file))
297 logging.critical ('Default editor could not be found!')
300 sys.exit (0) # successful, but exit anyway
302 # Change the loglevel if we're running in quiet mode
304 logging.getLogger().setLevel (logging.CRITICAL)
306 as = AnimeSorter2(options)
309 if __name__ == '__main__':