Merge with bugfix-0.5
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, intags=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 if intags:
86 self.tags = intags
87 else:
88 self.tags = tags.Tags(filename, opts)
89 self.tags.read()
90
91 # Names
92 filetype = opts.getfiletype(filename)
93 dir, base = os.path.split(filename)
94 base = os.path.splitext(base)[0]
95 base = base + opts['brwarning']
96 base = opts.cleanfilename(base)
97 if opts['treestructure'] != '':
98 # Standard format string
99 # See tags.Tags.format.__doc__
100 fmt = opts['treestructure']
101 dest = self.tags.format(fmt)
102 dest = opts.cleanfilename(dest)
103
104 self._outdir = os.path.dirname(dest)
105 self._outdir = os.path.join(opts['playerdir'], self._outdir)
106 self._outname = os.path.join(opts['playerdir'], dest)
107 else:
108 if opts['treedepth']:
109 head, dir = os.path.split(dir)
110 for i in range(opts['treedepth'] -1):
111 head, tail = os.path.split(head)
112 dir = os.path.join(tail, dir)
113 dir = opts.cleanfilename(dir)
114 else:
115 dir = ''
116 self._outdir = os.path.join(opts['playerdir'], dir)
117 self._outname = os.path.join(self._outdir, base)
118 self._outname += '.' + helpers[opts['encoder']]['type']
119 self._wavname = os.path.join(opts['tempdir'], base) + '.wav'
120
121 def make_dirs():
122 if not os.path.isdir(self._outdir):
123 os.makedirs(self._outdir)
124 return True
125
126 def rm_dirs():
127 if opts['treedepth']:
128 tail = self._outdir
129 for i in range(opts['treedepth']):
130 try:
131 os.rmdir(tail)
132 except OSError:
133 return True
134 tail, head = os.path.split(tail)
135 return True
136
137 job = task.SimpleTask(self, make_dirs, None,
138 rm_dirs, name="Creating dirs")
139 tasks.append(job)
140 del job
141
142 # Check the cache - if there is one
143 if self._cache:
144 self._hash = self._cache.search(self._filename)
145 if self._hash:
146 def recover_cache():
147 return self._cache.recover(self._hash, self._outname)
148 def undo_recover_cache():
149 if os.path.exists(self._outname):
150 os.unlink(self._outname)
151 return True
152 outreq = os.stat(self._cache.file(self._hash)).st_size
153 job = task.SimpleTask(self, recover_cache, None,
154 undo_recover_cache,
155 outsize=outreq, name="Hitting Cache")
156 tasks.append(job)
157 del job
158
159 # Decode
160 if not self._hash:
161 prog = conf.find_helper(filetype, 'decode')
162 tmpreq = opts.est_decoded_size(self._filename)
163 jobname = "Decoding %s" % filetype
164 if callable(helpers[prog]['cmd']):
165 # SimpleTask
166 func = helpers[prog]['cmd'](self._filename, self._wavname)
167 job = task.SimpleTask(self, func, None, undo_decode,
168 tmpsize=tmpreq, name=jobname)
169 else:
170 # Task
171 args = conf.make_args(prog, self._filename,
172 self._wavname)
173 job = task.Task(self, args, helpers[prog]['parser'],
174 undo_decode, tmpsize=tmpreq, name=jobname)
175 tasks.append(job)
176 del job
177
178 # Normalize
179 if not self._hash:
180 if opts.bin['normalize-audio']:
181 ncmd = [opts.bin['normalize-audio'], self._wavname]
182 job = task.Task(self, ncmd, lambda: '',
183 lambda: True, name="Normalizing")
184 tasks.append(job)
185 del job
186 else:
187 if not opts['nonormal']:
188 opts.log(2, "'normalize-audio' binary not found, skipping.")
189
190 # Encode
191 if not self._hash:
192 encoder = opts['encoder']
193 prog = helpers[encoder]
194 outreq = tmpreq / opts['compfactor']
195 jobname = "Encoding %s" % prog['type']
196 filter = prog['parser']
197 if callable(prog['cmd']):
198 func = prog['cmd'](self._wavname, self._outname)
199 job = task.SimpleTask(self, func, None, undo_encode,
200 outsize=outreq, name=jobname)
201 else:
202 # Task
203 args = conf.make_args(encoder, self._wavname,
204 self._outname, opts['encopts'])
205 job = task.Task(self, args, filter,
206 undo_encode, outsize=outreq, name=jobname)
207 tasks.append(job)
208 del job
209
210 # Clean up wav
211 if not self._hash:
212 job = task.SimpleTask(self, undo_decode, None, undo_decode,
213 tmpsize=-tmpreq,
214 name='Cleaning')
215 tasks.append(job)
216 del job
217
218 # Write index file
219 if opts['index']:
220 indexname = os.path.join(self._outdir, '2go.index')
221 def undo_index():
222 tags.remove_from_index(self._outname, indexname)
223 return True
224 def write_index():
225 self.tags.writeindex(self._outname, indexname)
226 return True
227 job = task.SimpleTask(self, write_index, None,
228 undo_index, name='Writing index')
229 tasks.append(job)
230 del job
231
232 # Tag
233 # tag files from the cache as well, brwarning may have changed
234 if not opts['notags']:
235 def tag_output():
236 self.tags.write(self._outname)
237 return True
238 job = task.SimpleTask(self, tag_output, None,
239 lambda: True, name="Tagging")
240 tasks.append(job)
241 del job
242
243 # Cache the output if we had to go to the trouble of producing it
244 if not self._hash and self._cache:
245 self._hash = cache.checksum(self._filename)
246 def cache_final():
247 self._cache.stash(self._outname, self._hash)
248 return True
249 job = task.SimpleTask(self, cache_final, name="Caching result")
250 tasks.append(job)
251 del job
252
253 # Completion sentinel
254 job = task.SimpleTask(self, finish, None, start, name="Done")
255 tasks.append(job)
256 del job
257
258 ## Consider the track done if the output file exists:
259 #if os.path.exists(self._outname) and not opts['force']:
260 # opts.log(1, "Skipping existing file: %s\n" % self._outname)
261 # self._queue = tuple(tasks)
262 # if self._hash and self._cache:
263 # self._cache.release(self._hash)
264 # for tsk in self._queue:
265 # tsk._status = task.DONE
266 # self._state = DONE
267 # return
268 if not opts['force']:
269 if os.path.exists(self._outname):
270 # In update mode, consider the track done if an existing file is older than the source
271 if opts['update']:
272 sourceinfo = os.stat(self._filename)
273 targetinfo = os.stat(self._outname)
274 if targetinfo.st_mtime >= sourceinfo.st_mtime:
275 opts.log(1, "Skipping up to date file: %s\n" % self._outname)
276 self._queue = tuple(tasks)
277 if self._hash and self._cache:
278 self._cache.release(self._hash)
279 for tsk in self._queue:
280 tsk._status = task.DONE
281 self._state = DONE
282 return
283 else:
284 opts.log(1, "Replacing out of date file: %s\n" % self._outname)
285 else: # Consider the track done if the output file exists:
286 opts.log(1, "Skipping existing file: %s\n" % self._outname)
287 self._queue = tuple(tasks)
288 if self._hash and self._cache:
289 self._cache.release(self._hash)
290 for tsk in self._queue:
291 tsk._status = task.DONE
292 self._state = DONE
293 return
294
295 # Ready to go
296 self._queue = tuple(tasks)
297 self._state = READY
298 return
299
300 def tasks(self):
301 return self._queue
302
303 def getfirst(self):
304 return self._queue[0]
305
306 def __call__(self):
307 return self._state
308
309 def __del__(self):
310 self.close()
311
312 def close(self):
313 for child in self._queue:
314 child._parent = None
315 self._queue = ()
316