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
266 # Ready to go
267 self._queue = tuple(tasks)
268 self._state = READY
269 return
270
271 def tasks(self):
272 return self._queue
273
274 def getfirst(self):
275 return self._queue[0]
276
277 def __call__(self):
278 return self._state
279
280 def __del__(self):
281 self.close()
282
283 def close(self):
284 for child in self._queue:
285 child._parent = None
286 self._queue = ()
287
288