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