Added --update patch from Mark J. Hewitt
mp3togo/track.py
1 # - track.py -
2 # This file is part of mp3togo
3
4 # Convert audio files to play on a mp3 player
5 # Manage the transform of a single file
6 #
7 # (c) Simeon Veldstra 2006 <reallifesim@gmail.com>
8 #
9 # This software is free.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You may redistribute this program under the terms of the
17 # GNU General Public Licence version 2
18 # Available in this package or at http://www.fsf.org
19
20
21 import os
22 import sys
23 import threading
24
25 import mp3togo.conf as conf
26 import mp3togo.tags as tags
27 import mp3togo.task as task
28 import mp3togo.cache as cache
29 from mp3togo.helpers import helpers
30
31 #helpers = mp3togo.helpers.helpers
32
33
34 READY = "ready"
35 RUNNING = "running"
36 DONE = 0
37 FAILED = None
38
39 DOGGFACTOR = 10
40 DMP3FACTOR = 10
41 DFLACFACTOR = 3
42
43
44 class Track:
45 """Encapsulate the transformation of a file."""
46
47 def __init__(self, filename, opts, cooked=None):
48
49 self.bytes = 0
50 self._filename = filename
51 self._queue = ()
52 self._state = FAILED
53 self._cache = cooked
54 self._hash = False
55
56 def undo_decode():
57 if os.path.exists(self._wavname):
58 os.unlink(self._wavname)
59 return True
60
61 def undo_encode():
62 if os.path.exists(self._outname):
63 os.unlink(self._outname)
64 return True
65
66 # Sentinel tasks
67 def finish():
68 if os.path.exists(self._outname):
69 self.bytes = os.stat(self._outname).st_size
70 self._state = DONE
71 return DONE
72 def start():
73 self.bytes = 0
74 self._state = RUNNING
75 return RUNNING
76 def abort():
77 self._state = FAILED
78 return FAILED
79 job = task.SimpleTask(self, start, None, abort, name="Start")
80 tasks = [job]
81 del job
82
83 # Read the tags
84 # Do this early so that 'treestructure' can access them
85 self.tags = tags.Tags(filename, opts)
86 self.tags.read()
87
88 # Names
89 filetype = opts.getfiletype(filename)
90 dir, base = os.path.split(filename)
91 base = os.path.splitext(base)[0]
92 base = base + opts['brwarning']
93 base = opts.cleanfilename(base)
94 if opts['treestructure'] != '':
95 # Standard format string
96 # See tags.Tags.format.__doc__
97 fmt = opts['treestructure']
98 dest = self.tags.format(fmt)
99 dest = opts.cleanfilename(dest)
100
101 self._outdir = os.path.dirname(dest)
102 self._outdir = os.path.join(opts['playerdir'], self._outdir)
103 self._outname = os.path.join(opts['playerdir'], dest)
104 else:
105 if opts['treedepth']:
106 head, dir = os.path.split(dir)
107 for i in range(opts['treedepth'] -1):
108 head, tail = os.path.split(head)
109 dir = os.path.join(tail, dir)
110 dir = opts.cleanfilename(dir)
111 else:
112 dir = ''
113 self._outdir = os.path.join(opts['playerdir'], dir)
114 self._outname = os.path.join(self._outdir, base)
115 self._outname += '.' + helpers[opts['encoder']]['type']
116 self._wavname = os.path.join(opts['tempdir'], base) + '.wav'
117
118 def make_dirs():
119 if not os.path.isdir(self._outdir):
120 os.makedirs(self._outdir)
121 return True
122
123 def rm_dirs():
124 if opts['treedepth']:
125 tail = self._outdir
126 for i in range(opts['treedepth']):
127 try:
128 os.rmdir(tail)
129 except OSError:
130 return True
131 tail, head = os.path.split(tail)
132 return True
133
134 job = task.SimpleTask(self, make_dirs, None,
135 rm_dirs, name="Creating dirs")
136 tasks.append(job)
137 del job
138
139 # Check the cache - if there is one
140 if self._cache:
141 self._hash = self._cache.search(self._filename)
142 if self._hash:
143 def recover_cache():
144 return self._cache.recover(self._hash, self._outname)
145 def undo_recover_cache():
146 if os.path.exists(self._outname):
147 os.unlink(self._outname)
148 return True
149 outreq = os.stat(self._cache.file(self._hash)).st_size
150 job = task.SimpleTask(self, recover_cache, None,
151 undo_recover_cache,
152 outsize=outreq, name="Hitting Cache")
153 tasks.append(job)
154 del job
155
156 # Decode
157 if not self._hash:
158 prog = conf.find_helper(filetype, 'decode')
159 tmpreq = opts.est_decoded_size(self._filename)
160 jobname = "Decoding %s" % filetype
161 if callable(helpers[prog]['cmd']):
162 # SimpleTask
163 func = helpers[prog]['cmd'](self._filename, self._wavname)
164 job = task.SimpleTask(self, func, None, undo_decode,
165 tmpsize=tmpreq, name=jobname)
166 else:
167 # Task
168 args = conf.make_args(prog, self._filename,
169 self._wavname)
170 job = task.Task(self, args, helpers[prog]['parser'],
171 undo_decode, tmpsize=tmpreq, name=jobname)
172 tasks.append(job)
173 del job
174
175 # Normalize
176 if not self._hash:
177 if opts.bin['normalize-audio']:
178 ncmd = [opts.bin['normalize-audio'], self._wavname]
179 job = task.Task(self, ncmd, lambda: '',
180 lambda: True, name="Normalizing")
181 tasks.append(job)
182 del job
183 else:
184 if not opts['nonormal']:
185 opts.log(2, "'normalize-audio' binary not found, skipping.")
186
187 # Encode
188 if not self._hash:
189 encoder = opts['encoder']
190 prog = helpers[encoder]
191 outreq = tmpreq / opts['compfactor']
192 jobname = "Encoding %s" % prog['type']
193 filter = prog['parser']
194 if callable(prog['cmd']):
195 func = prog['cmd'](self._wavname, self._outname)
196 job = task.SimpleTask(self, func, None, undo_encode,
197 outsize=outreq, name=jobname)
198 else:
199 # Task
200 args = conf.make_args(encoder, self._wavname,
201 self._outname, opts['encopts'])
202 job = task.Task(self, args, filter,
203 undo_encode, outsize=outreq, name=jobname)
204 tasks.append(job)
205 del job
206
207 # Clean up wav
208 if not self._hash:
209 job = task.SimpleTask(self, undo_decode, None, undo_decode,
210 tmpsize=-tmpreq,
211 name='Cleaning')
212 tasks.append(job)
213 del job
214
215 # Write index file
216 if opts['index']:
217 indexname = os.path.join(self._outdir, '2go.index')
218 def undo_index():
219 tags.remove_from_index(self._outname, indexname)
220 return True
221 def write_index():
222 self.tags.writeindex(self._outname, indexname)
223 return True
224 job = task.SimpleTask(self, write_index, None,
225 undo_index, name='Writing index')
226 tasks.append(job)
227 del job
228
229 # Tag
230 # tag files from the cache as well, brwarning may have changed
231 if not opts['notags']:
232 def tag_output():
233 self.tags.write(self._outname)
234 return True
235 job = task.SimpleTask(self, tag_output, None,
236 lambda: True, name="Tagging")
237 tasks.append(job)
238 del job
239
240 # Cache the output if we had to go to the trouble of producing it
241 if not self._hash and self._cache:
242 self._hash = cache.checksum(self._filename)
243 def cache_final():
244 self._cache.stash(self._outname, self._hash)
245 return True
246 job = task.SimpleTask(self, cache_final, name="Caching result")
247 tasks.append(job)
248 del job
249
250 # Completion sentinel
251 job = task.SimpleTask(self, finish, None, start, name="Done")
252 tasks.append(job)
253 del job
254
255 ## Consider the track done if the output file exists:
256 #if os.path.exists(self._outname) and not opts['force']:
257 # opts.log(1, "Skipping existing file: %s\n" % self._outname)
258 # self._queue = tuple(tasks)
259 # if self._hash and self._cache:
260 # self._cache.release(self._hash)
261 # for tsk in self._queue:
262 # tsk._status = task.DONE
263 # self._state = DONE
264 # return
265 if not opts['force']:
266 if os.path.exists(self._outname):
267 # In update mode, consider the track done if an existing file is older than the source
268 if opts['update']:
269 sourceinfo = os.stat(self._filename)
270 targetinfo = os.stat(self._outname)
271 if targetinfo.st_mtime >= sourceinfo.st_mtime:
272 opts.log(1, "Skipping up to date file: %s\n" % self._outname)
273 self._queue = tuple(tasks)
274 if self._hash and self._cache:
275 self._cache.release(self._hash)
276 for tsk in self._queue:
277 tsk._status = task.DONE
278 self._state = DONE
279 return
280 else:
281 opts.log(1, "Replacing out of date file: %s\n" % self._outname)
282 else: # Consider the track done if the output file exists:
283 opts.log(1, "Skipping existing file: %s\n" % self._outname)
284 self._queue = tuple(tasks)
285 if self._hash and self._cache:
286 self._cache.release(self._hash)
287 for tsk in self._queue:
288 tsk._status = task.DONE
289 self._state = DONE
290 return
291
292 # Ready to go
293 self._queue = tuple(tasks)
294 self._state = READY
295 return
296
297 def tasks(self):
298 return self._queue
299
300 def getfirst(self):
301 return self._queue[0]
302
303 def __call__(self):
304 return self._state
305
306 def __del__(self):
307 self.close()
308
309 def close(self):
310 for child in self._queue:
311 child._parent = None
312 self._queue = ()
313