1 # -*- coding: utf-8 -*-
2 ###########################################################################
3 # Copyright (C) 2008 by Andrew Mahone
4 # <andrew.mahone@gmail.com>
6 # Copyright: See COPYING file that comes with this distribution
8 ###########################################################################
9 # pylint: disable=E1101
16 from cStringIO
import StringIO
18 from StringIO
import StringIO
19 from tempfile
import mkdtemp
20 from shutil
import rmtree
21 from threading
import BoundedSemaphore
, RLock
22 from subprocess
import Popen
, PIPE
23 from mutagen
import FileType
24 from audiomangler
.config
import Config
25 from audiomangler
.tag
import NormMetaData
26 from audiomangler
.task
import CLITask
, generator_task
27 from audiomangler
.expression
import Expr
, Format
28 from audiomangler
import util
29 from audiomangler
.util
import ClassInitMeta
30 from mutagen
import File
35 __metaclass__
= ClassInitMeta
37 has_from_wav_multi
= False
38 has_from_wav_pipe
= False
39 has_to_wav_pipe
= False
40 has_replaygain
= False
43 def __classinit__(cls
, name__
, bases__
, cls_dict
):
44 if 'type_' in cls_dict
:
45 codec_map
[cls_dict
['type_']] = cls
48 def _conv_out_filename(cls
, filename
):
49 return ''.join((filename
.rsplit('.', 1)[0], '.', cls
.ext
))
52 def from_wav_multi(cls
, indir
, infiles
, outfiles
):
53 if not hasattr('from_wav_multi_cmd'):
55 encopts
= Config
['encopts']
59 encopts
= tuple(encopts
.split())
60 args
= cls
.from_wav_multi_cmd
.evaluate({
62 'infiles':tuple(infiles
),
63 'outfiles':tuple(outfiles
),
69 return CLITask(args
=args
, stderr
=sys
.stderr
, background
=True)
72 def from_wav_pipe(cls
, infile
, outfile
):
73 if not hasattr(cls
, 'from_wav_pipe_cmd'):
75 encopts
= Config
['encopts']
79 encopts
= tuple(encopts
.split())
80 outfile
= cls
._conv
_out
_filename
(infile
)
87 'encoder': cls
.encoder
89 args
= cls
.from_wav_pipe_cmd
.evaluate(env
)
92 if hasattr(cls
, '_from_wav_pipe_stdin'):
93 stdin
= cls
._from
_wav
_pipe
_stdin
.evaluate(env
)
95 return CLITask(*args
, stdin
=stdin
)
98 def to_wav_pipe(cls
, infile
, outfile
):
99 if not hasattr(cls
, 'to_wav_pipe_cmd'):
106 'decoder':cls
.decoder
108 args
= cls
.to_wav_pipe_cmd
.evaluate(env
)
110 if hasattr(cls
, 'to_wav_pipe_stdout'):
111 stdout
= cls
.to_wav_pipe_stdout
.evaluate(env
)
112 return CLITask(args
=args
, stdin
='/dev/null', stdout
=stdout
, stderr
=sys
.stderr
, background
=False)
116 def add_replaygain(cls
, files
, metas
=None):
118 'replaygain':cls
.replaygain
,
121 if metas
and hasattr(cls
, 'calc_replaygain_cmd'):
122 task
= CLITask(*cls
.calc_replaygain_cmd
.evaluate(env
))
124 tracks
, album
= cls
.calc_replaygain(output
)
126 for meta
, track
in zip(metas
, tracks
):
130 elif hasattr(cls
, 'replaygain_cmd'):
131 task
= CLITask(*cls
.replaygain_cmd
.evaluate(env
))
133 elif hasattr(cls
, 'calc_replaygain'):
134 task
= CLITask(*cls
.calc_replaygain_cmd
.evaluate(env
))
136 tracks
, album
= cls
.calc_replaygain(output
)
137 for trackfile
, trackgain
in zip(files
, tracks
):
139 m
= NormMetaData(trackgain
+ album
)
144 class MP3Codec(Codec
):
148 replaygain
= 'mp3gain'
149 has_from_wav_multi
= True
150 has_replaygain
= True
151 from_wav_multi_cmd
= Expr("(encoder, '--quiet') + encopts + ('--noreplaygain', '--nogapout', indir, '--nogaptags', '--nogap') + infiles")
152 calc_replaygain_cmd
= Expr("(replaygain, '-q', '-o', '-s', 's')+files")
155 def calc_replaygain(out
):
157 out
= [l
.split('\t')[2:4] for l
in out
.splitlines()[1:]]
160 gain
= ' '.join((i
[0], 'dB'))
161 peak
= '%.8f'% (float(i
[1]) / 32768)
162 tracks
.append((('replaygain_track_gain', gain
), ('replaygain_track_peak', peak
)))
163 gain
= ' '.join((out
[-1][0], 'dB'))
164 peak
= '%.8f'% (float(out
[-1][1]) / 32768)
165 album
= (('replaygain_album_gain', gain
), ('replaygain_album_peak', peak
))
168 class WavPackCodec(Codec
):
173 replaygain
= 'wvgain'
174 has_to_wav_pipe
= True
175 has_from_wav_pipe
= True
176 has_replaygain
= True
178 to_wav_pipe_cmd
= Expr("(decoder, '-q', '-w', infile, '-o', '-')")
179 to_wav_pipe_stdout
= Expr("outfile")
180 from_wav_pipe_cmd
= Expr("(encoder, '-q')+encopts+(infile, '-o', outfile)")
181 replaygain_cmd
= Expr("(replaygain, '-a')+files")
183 class FLACCodec(Codec
):
188 replaygain
= 'metaflac'
189 has_to_wav_pipe
= True
190 has_from_wav_pipe
= True
191 has_replaygain
= True
193 to_wav_pipe_cmd
= Expr("(decoder, '-s', '-c', '-d', infile)")
194 to_wav_pipe_stdout
= Expr("outfile")
195 from_wav_pipe_cmd
= Expr("(encoder, '-s')+encopts+(infile, )")
196 replaygain_cmd
= Expr("(replaygain, '--add-replay-gain')+files")
198 class OggVorbisCodec(Codec
):
203 replaygain
= 'vorbisgain'
204 has_to_wav_pipe
= True
205 has_from_wav_pipe
= True
206 has_replaygain
= True
207 to_wav_pipe_cmd
= Expr("(decoder, '-Q', '-o', '-', infile)")
208 to_wav_pipe_stdout
= Expr("outfile")
209 from_wav_pipe_cmd
= Expr("(encoder, '-Q')+encopts+('-o', outfile, infile)")
210 replaygain_cmd
= Expr("(replaygain, '-q', '-a')+files")
211 calc_replaygain_cmd
= Expr("(replaygain, '-a', '-n', '-d')+files")
214 def calc_replaygain(cls
, files
):
216 args
= [cls
.replaygain
, '-and']
218 p
= Popen(args
=args
, stdout
=PIPE
, stderr
=PIPE
)
219 (out
, err
) = p
.communicate()
221 for match
in re
.finditer('^\s*(\S+ dB)\s*\|\s*([0-9]+)\s*\|', out
,
223 gain
= match
.group(1)
224 peak
= float(match
.group(2)) / 32768
225 apeak
= max(apeak
, peak
)
227 tracks
.append((('replaygain_track_gain', gain
), ('replaygain_track_peak', peak
)))
228 again
= re
.search('^Recommended Album Gain:\s*(\S+ dB)', err
, re
.M
)
230 album
= (('replaygain_album_gain', again
.group(1)), ('replaygain_album_peak', "%.8f" % apeak
))
232 album
= (('replaygain_album_peak', apeak
),)
235 class PipeManager(object):
236 __slots__
= 'pipes', 'pipedir', 'count', 'prefix', 'suffix'
237 def __init__(self
, base
=None, prefix
='', suffix
=''):
244 def create_dir(self
):
245 if self
.pipedir
is None:
246 self
.pipedir
= os
.path
.abspath(Config
['workdir'] or Config
['base'])
247 self
.pipedir
= mkdtemp(prefix
='audiomangler_work_', dir=self
.pipedir
)
248 atexit
.register(self
.cleanup
)
250 def get_pipes(self
, count
=1):
256 for idx
in xrange(count
):
258 newpath
= os
.path
.join(self
.pipedir
,'%s%08x%s' % (prefix
, self
.count
, suffix
))
261 self
.pipes
.add(newpath
)
262 result
.append(self
.pipes
.pop())
266 def free_pipes(self
, pipes
):
267 if isinstance(pipes
, basestring
):
269 self
.pipes
.update(pipes
)
272 rmtree(self
.pipedir
, ignore_errors
=True)
274 class FileManager(object):
275 __slots__
= 'files', 'filedir', 'count', 'prefix', 'suffix'
276 def __init__(self
, base
=None, prefix
='', suffix
=''):
283 def create_dir(self
):
284 if self
.filedir
is None:
285 workdir
= Config
['workdir']
287 workdir
= os
.path
.abspath(workdir
)
288 basedir
= Config
['base']
290 basedir
= os
.path
.abspath(basedir
)
291 print "%r, %r" % (workdir
, basedir
)
292 if workdir
is None or workdir
== basedir
:
294 pipe_manager
.create_dir()
295 self
.filedir
= pipe_manager
.pipedir
299 self
.filedir
= workdir
300 self
.filedir
= mkdtemp(prefix
='audiomangler_work_', dir=self
.filedir
)
301 atexit
.register(self
.cleanup
)
303 def get_files(self
, count
=1):
309 for idx
in xrange(count
):
311 newpath
= os
.path
.join(self
.pipedir
,'%s%08x%s' % (prefix
, self
.count
, suffix
))
313 self
.files
.add(newpath
)
314 result
.append(self
.files
.pop())
318 def free_files(self
, files
):
319 if isinstance(files
, basestring
):
321 self
.files
.update(files
)
324 rmtree(self
.filedir
, ignore_errors
=True)
326 pipe_manager
= PipeManager(prefix
='pipe', suffix
='.wav')
327 file_manager
= FileManager(prefix
='out')
329 def transcode_track(dtask
, etask
, sem
):
336 def check_and_copy_cover(fileset
, targetfiles
):
337 cover_sizes
= Config
['cover_sizes']
340 cover_out_filename
= Config
['cover_out_filename']
341 if not cover_out_filename
:
343 cover_out_filename
= Format(cover_out_filename
)
344 cover_sizes
= cover_sizes
.split(',')
348 cover_filenames
= Config
['cover_filenames']
350 cover_filenames
= cover_filenames
.split(',')
353 cover_out_filenames
= [cover_out_filename
.evaluate({'size':s
}) for s
in cover_sizes
]
354 for (infile
, targetfile
) in zip(fileset
, targetfiles
):
355 outdir
= os
.path
.split(targetfile
)[0]
356 if outdir
in outdirs
: continue
357 if all(os
.path
.isfile(os
.path
.join(outdir
, filename
) for filename
in cover_out_filenames
)):
361 for filename
in (os
.path
.join(infile
.meta
['dir'], file_
) for file_
in cover_filenames
):
363 d
= open(filename
).read()
364 i
= Image
.open(StringIO(d
))
370 tags
= [(value
.type, value
) for key
, value
in infile
.tags
.items()
371 if key
.startswith('APIC') and hasattr(value
, 'type')
372 and value
.type in (0, 3)]
373 tags
.sort(None, None, True)
374 for t
, value
in tags
:
378 i
= Image
.open(StringIO(d
))
384 for s
in cover_sizes
:
393 iw
= i
.resize((w
, h
), Image
.ADAPTIVE
)
394 filename
= os
.path
.join(
395 outdir
, cover_out_filename
.evaluate({'size':s
})
397 print "save cover %s" % filename
401 rg_keys
= 'replaygain_track_gain', 'replaygain_track_peak', 'replaygain_album_gain', 'replaygain_album_peak'
402 def transcode_set(targetcodec
, fileset
, targetfiles
, alsem
, trsem
, workdirs
, workdirs_l
):
408 workdir
, pipefiles
= workdirs
.pop()
410 outfiles
= map(targetcodec
._conv
_out
_filename
, pipefiles
[:len(fileset
)])
411 if targetcodec
._from
_wav
_pipe
:
412 for i
, p
, o
in zip(fileset
, pipefiles
, outfiles
):
414 dtask
= get_codec(i
).to_wav_pipe(i
.meta
['path'], p
)
415 etask
= targetcodec
.from_wav_pipe(p
, o
)
417 #ttask = FuncTask(background=True, target=transcode_track,
418 #args=(dtask, etask, trsem)
422 bgprocs
.add(ttask
.run())
427 elif targetcodec
._from
_wav
_multi
:
428 etask
= targetcodec
.from_wav_multi(
429 workdir
, pipefiles
[:len(fileset
)], outfiles
432 for i
, o
in zip(fileset
, pipefiles
):
433 task
= get_codec(i
).to_wav_pipe(i
.meta
['path'], o
)
438 newreplaygain
= False
439 for i
, o
in zip(fileset
, outfiles
):
441 if not (i
.lossless
and targetcodec
.lossless
):
446 if not newreplaygain
:
452 if newreplaygain
and targetcodec
._replaygain
:
453 targetcodec
.add_replaygain(outfiles
, metas
)
454 for i
, m
, o
, t
in zip(fileset
, metas
, outfiles
, targetfiles
):
458 targetdir
= os
.path
.split(t
)[0]
459 if targetdir
not in dirs
:
461 if not os
.path
.isdir(targetdir
):
462 os
.makedirs(targetdir
)
463 print "%s -> %s" %(i
.filename
, t
)
464 util
.move(o
.filename
, t
)
465 check_and_copy_cover(fileset
, targetfiles
)
469 workdirs
.add((workdir
, pipefiles
))
474 def sync_sets(sets
=[], targettids
=()):
476 semct
= int(Config
['jobs'])
477 except (ValueError, TypeError):
480 targetcodec
= Config
['type']
481 if ',' in targetcodec
:
482 allowedcodecs
= targetcodec
.split(',')
483 targetcodec
= allowedcodecs
[0]
484 allowedcodecs
= set(allowedcodecs
)
486 allowedcodecs
= set((targetcodec
,))
487 targetcodec
= get_codec(targetcodec
)
488 workdir
= Config
['workdir'] or Config
['base']
489 workdir
= mkdtemp(dir=workdir
, prefix
='audiomangler_work_')
490 if targetcodec
._from
_wav
_pipe
:
491 if len(sets
) > semct
* 2:
492 alsem
= BoundedSemaphore(semct
)
495 trsem
= BoundedSemaphore(semct
)
497 elif targetcodec
._from
_wav
_multi
:
499 alsem
= BoundedSemaphore(semct
)
500 numpipes
= max(len(s
) for s
in sets
)
503 for n
in range(semct
):
504 w
= os
.path
.join(workdir
, "%02d" % n
)
507 for m
in range(numpipes
):
508 pipes
.append(os
.path
.join(w
, "%02d.wav"%m
))
511 workdirs
.add((w
, pipes
))
513 if all(file_
.type_
in allowedcodecs
for file_
in fileset
):
514 targetfiles
= [f
.format() for f
in fileset
]
515 if not all(file_
.tid
in targettids
for file_
in fileset
):
516 print "copying files"
520 targetdir
= os
.path
.split(t
)[0]
521 if targetdir
not in dirs
:
523 if not os
.path
.isdir(targetdir
):
524 os
.makedirs(targetdir
)
525 print "%s -> %s" % (i
.filename
, t
)
526 util
.copy(i
.filename
, t
)
527 codecs
= set((get_codec(f
) for f
in fileset
))
529 if codec
and not codecs
and codec
._replaygain
:
530 codec
.add_replaygain(targetfiles
)
531 check_and_copy_cover(fileset
, targetfiles
)
533 postadd
= {'type':targetcodec
.type_
, 'ext':targetcodec
.ext
}
534 targetfiles
= [f
.format(postadd
=postadd
)for f
in fileset
]
535 if all(file_
.tid
in targettids
for file_
in fileset
):
536 check_and_copy_cover(fileset
, targetfiles
)
540 for task
in list(bgtasks
):
543 # FuncTask use needs rewrite
545 #background=True, target=transcode_set, args=(
546 #targetcodec, fileset, targetfiles, alsem, trsem, workdirs,
550 bgtasks
.add(task
.run())
555 for w
, ps
in workdirs
:
562 if isinstance(item
, FileType
):
563 item
= getattr(item
, 'type_')
564 return codec_map
[item
]
566 __all__
= ['sync_sets', 'get_codec']