Fixed ommitted tags, bug report Konstantin Pastbin.
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 try:
227 self.tags.write(self._outname)
228 except:
229 if opts['verbosity'] > 3:
230 import sys, traceback
231 traceback.print_exc(file=sys.stdout)
232 raise
233 return True
234 job = task.SimpleTask(self, tag_output, None,
235 lambda: True, name="Tagging")
236 tasks.append(job)
237 del job
238
239 # Cache the output if we had to go to the trouble of producing it
240 if not self._hash and self._cache:
241 self._hash = cache.checksum(self._filename)
242 def cache_final():
243 self._cache.stash(self._outname, self._hash)
244 return True
245 job = task.SimpleTask(self, cache_final, name="Caching result")
246 tasks.append(job)
247 del job
248
249 # Completion sentinel
250 job = task.SimpleTask(self, finish, None, start, name="Done")
251 tasks.append(job)
252 del job
253
254 if not opts['force']:
255 if os.path.exists(self._outname):
256 # In update mode, consider the track done if an existing file is older than the source
257 if opts['update']:
258 sourceinfo = os.stat(self._filename)
259 targetinfo = os.stat(self._outname)
260 if targetinfo.st_mtime >= sourceinfo.st_mtime:
261 opts.log(1, "Skipping up to date file: %s\n" % self._outname)
262 self._queue = tuple(tasks)
263 if self._hash and self._cache:
264 self._cache.release(self._hash)
265 for tsk in self._queue:
266 tsk._status = task.DONE
267 self.bytes = os.stat(self._outname).st_size
268 self._state = DONE
269 return
270 else:
271 opts.log(1, "Replacing out of date file: %s\n" % self._outname)
272 else: # Consider the track done if the output file exists:
273 opts.log(1, "Skipping existing file: %s\n" % self._outname)
274 self._queue = tuple(tasks)
275 if self._hash and self._cache:
276 self._cache.release(self._hash)
277 for tsk in self._queue:
278 tsk._status = task.DONE
279 self.bytes = os.stat(self._outname).st_size
280 self._state = DONE
281 return
282
283 # Ready to go
284 self._queue = tuple(tasks)
285 self._state = READY
286 return
287
288 def tasks(self):
289 return self._queue
290
291 def getfirst(self):
292 return self._queue[0]
293
294 def __call__(self):
295 return self._state
296
297 def __del__(self):
298 self.close()
299
300 def close(self):
301 for child in self._queue:
302 child._parent = None
303 self._queue = ()
304