116 |
ira |
1 |
#!/usr/bin/python
|
|
|
2 |
|
121 |
ira |
3 |
import os, re, shutil, sys
|
|
|
4 |
from optparse import OptionParser
|
116 |
ira |
5 |
|
121 |
ira |
6 |
### DEFAULT CONFIGURATION VARIABLES ###
|
175 |
ira |
7 |
DICT_FILE = os.path.expanduser('~/.config/animesorter/animesorter.dict')
|
121 |
ira |
8 |
WORK_DIR = os.path.expanduser('~/downloads/usenet')
|
|
|
9 |
SORT_DIR = os.path.expanduser('/data/Anime')
|
116 |
ira |
10 |
|
181 |
ira |
11 |
class AnimeSorter:
|
121 |
ira |
12 |
|
181 |
ira |
13 |
def __init__(self):
|
|
|
14 |
self.options = {}
|
|
|
15 |
self.move_status = []
|
121 |
ira |
16 |
|
181 |
ira |
17 |
def parse_dict(self):
|
|
|
18 |
"""Parses a dictionary file containing the sort definitions in the form:
|
|
|
19 |
DIRECTORY = PATTERN
|
116 |
ira |
20 |
|
181 |
ira |
21 |
Returns a list of tuples of the form (compiled_regex, to_directory)"""
|
|
|
22 |
|
116 |
ira |
23 |
try:
|
181 |
ira |
24 |
f = open(self.options.dict_file, 'r', 0)
|
|
|
25 |
try:
|
|
|
26 |
data = f.read()
|
|
|
27 |
finally:
|
|
|
28 |
f.close()
|
|
|
29 |
except IOError:
|
|
|
30 |
print 'Error opening %s ... exiting!' % options.dict_file
|
|
|
31 |
sys.exit()
|
116 |
ira |
32 |
|
181 |
ira |
33 |
### Get a LIST containing each line in the file
|
|
|
34 |
lines = [l for l in data.split('\n') if len(l) > 0]
|
116 |
ira |
35 |
|
181 |
ira |
36 |
### Split each line into a tuple, and strip each element of spaces
|
|
|
37 |
result = [(r, d) for r, d in [l.split('=') for l in lines]]
|
|
|
38 |
result = [(r.strip() ,d.strip()) for r, d in result]
|
|
|
39 |
result = [(re.compile(r), d) for r, d in result]
|
116 |
ira |
40 |
|
181 |
ira |
41 |
return tuple(result)
|
116 |
ira |
42 |
|
181 |
ira |
43 |
def get_types_re(self):
|
|
|
44 |
"""Returns a compiled regular expression which will find objects of the
|
|
|
45 |
types specified in this function's definition."""
|
116 |
ira |
46 |
|
181 |
ira |
47 |
types = ('avi', 'ogm', 'mkv', 'mp4')
|
|
|
48 |
types_regex = ''
|
116 |
ira |
49 |
|
181 |
ira |
50 |
for i in types:
|
|
|
51 |
types_regex += '%s|' % i
|
116 |
ira |
52 |
|
181 |
ira |
53 |
types_regex = '.*(%s)$' % types_regex[:-1]
|
116 |
ira |
54 |
|
181 |
ira |
55 |
return re.compile(types_regex, re.IGNORECASE)
|
116 |
ira |
56 |
|
181 |
ira |
57 |
def get_matches(self, files, pattern):
|
|
|
58 |
"""get_matches(files, pattern):
|
116 |
ira |
59 |
|
181 |
ira |
60 |
files is type LIST
|
|
|
61 |
pattern is type sre.SRE_Pattern
|
116 |
ira |
62 |
|
181 |
ira |
63 |
Returns a list of the files matching the pattern as type sre.SRE_Match."""
|
116 |
ira |
64 |
|
181 |
ira |
65 |
matches = [m for m in files if pattern.search(m)]
|
|
|
66 |
return matches
|
116 |
ira |
67 |
|
181 |
ira |
68 |
def move_files(self, files, fromdir, todir):
|
|
|
69 |
"""move_files(files, fromdir, todir):
|
|
|
70 |
Move the files represented by the list FILES from FROMDIR to TODIR"""
|
|
|
71 |
## Check for a non-default directory
|
|
|
72 |
if todir[0] != '/':
|
|
|
73 |
todir = os.path.join(self.options.output_dir, todir)
|
116 |
ira |
74 |
|
181 |
ira |
75 |
## Create the directory if it doesn't exist
|
|
|
76 |
if not os.path.isdir(todir):
|
|
|
77 |
try:
|
|
|
78 |
os.makedirs(todir)
|
|
|
79 |
except:
|
|
|
80 |
self.move_status.append('FAILED to create directory: %s' % todir)
|
116 |
ira |
81 |
|
181 |
ira |
82 |
## Try to move every file, one at a time
|
|
|
83 |
for f in files:
|
|
|
84 |
srcname = os.path.join(fromdir, f)
|
|
|
85 |
dstname = os.path.join(todir, f)
|
116 |
ira |
86 |
|
181 |
ira |
87 |
try:
|
|
|
88 |
shutil.move(srcname, dstname)
|
|
|
89 |
except:
|
|
|
90 |
self.move_status.append('FAILED to move %s to %s' % (f, todir))
|
|
|
91 |
return
|
116 |
ira |
92 |
|
181 |
ira |
93 |
self.move_status.append('Moved %s to %s' % (f, todir))
|
116 |
ira |
94 |
|
181 |
ira |
95 |
def print_prog_header(self):
|
|
|
96 |
if self.options.verbose:
|
|
|
97 |
print 'Regular Expression File Sorter (aka animesorter)'
|
|
|
98 |
print '================================================================================'
|
|
|
99 |
print 'Copyright (c) 2005, Ira W. Snyder (devel@irasnyder.com)'
|
|
|
100 |
print 'All rights reserved.'
|
|
|
101 |
print 'This program is licensed under the GNU GPL v2'
|
|
|
102 |
print
|
116 |
ira |
103 |
|
181 |
ira |
104 |
def print_dir_header(self, dir):
|
|
|
105 |
if self.options.verbose:
|
|
|
106 |
print 'Working in directory: %s' % dir
|
|
|
107 |
print '================================================================================'
|
116 |
ira |
108 |
|
181 |
ira |
109 |
def print_move_status(self):
|
|
|
110 |
if self.options.verbose:
|
|
|
111 |
for i in self.move_status:
|
|
|
112 |
print i
|
116 |
ira |
113 |
|
181 |
ira |
114 |
print
|
116 |
ira |
115 |
|
181 |
ira |
116 |
def get_parsed_options(self):
|
|
|
117 |
parser = OptionParser()
|
|
|
118 |
parser.add_option('-q', '--quiet', action='store_false', dest='verbose',
|
|
|
119 |
default=True, help='don\'t print status messages to stdout')
|
|
|
120 |
parser.add_option('-d', '--dict', dest='dict_file', default=DICT_FILE,
|
|
|
121 |
help='read dictionary from FILE', metavar='FILE')
|
|
|
122 |
#parser.add_option('-n', '--not-recursive', action='store_false', dest='recursive',
|
|
|
123 |
# default=True, help='don\'t run recursively')
|
|
|
124 |
parser.add_option('-s', '--start-dir', dest='start_dir', default=WORK_DIR,
|
|
|
125 |
help='start running at directory DIR', metavar='DIR')
|
|
|
126 |
parser.add_option('-o', '--output-dir', dest='output_dir', default=SORT_DIR,
|
|
|
127 |
help='sort files into DIR', metavar='DIR')
|
116 |
ira |
128 |
|
181 |
ira |
129 |
## Parse the options
|
|
|
130 |
(options, args) = parser.parse_args()
|
116 |
ira |
131 |
|
181 |
ira |
132 |
## Correct directories
|
|
|
133 |
options.dict_file = os.path.abspath(options.dict_file)
|
|
|
134 |
options.start_dir = os.path.abspath(options.start_dir)
|
|
|
135 |
options.output_dir = os.path.abspath(options.output_dir)
|
116 |
ira |
136 |
|
181 |
ira |
137 |
return options
|
116 |
ira |
138 |
|
181 |
ira |
139 |
def main(self):
|
116 |
ira |
140 |
|
181 |
ira |
141 |
## Get Options
|
|
|
142 |
self.options = self.get_parsed_options()
|
116 |
ira |
143 |
|
181 |
ira |
144 |
## Print the program's header
|
|
|
145 |
self.print_prog_header()
|
116 |
ira |
146 |
|
181 |
ira |
147 |
## Parse the dictionary
|
|
|
148 |
dict = self.parse_dict()
|
116 |
ira |
149 |
|
181 |
ira |
150 |
## Start walking through directories
|
|
|
151 |
for root, dirs, files in os.walk(self.options.start_dir):
|
116 |
ira |
152 |
|
181 |
ira |
153 |
## Blank the status variable
|
|
|
154 |
self.move_status = []
|
116 |
ira |
155 |
|
181 |
ira |
156 |
## Get all of the files in the directory that are of the correct types
|
|
|
157 |
types_re = self.get_types_re()
|
|
|
158 |
raw_matches = [f for f in files if types_re.match(f)]
|
116 |
ira |
159 |
|
181 |
ira |
160 |
### Loop through the dictionary and try to move everything that matches
|
|
|
161 |
for regex, todir in dict:
|
|
|
162 |
matches = self.get_matches(raw_matches, regex)
|
121 |
ira |
163 |
|
181 |
ira |
164 |
## Move the files if we've found some
|
|
|
165 |
if len(matches) > 0:
|
|
|
166 |
self.move_files(matches, root, todir)
|
175 |
ira |
167 |
|
181 |
ira |
168 |
if len(self.move_status) > 0:
|
|
|
169 |
## print header
|
|
|
170 |
self.print_dir_header(root)
|
|
|
171 |
|
|
|
172 |
## print status
|
|
|
173 |
self.print_move_status()
|
|
|
174 |
|
175 |
ira |
175 |
### MAIN IS HERE ###
|
|
|
176 |
if __name__ == '__main__':
|
181 |
ira |
177 |
as = AnimeSorter()
|
|
|
178 |
as.main()
|
175 |
ira |
179 |
|