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