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
15 from cStringIO
import StringIO
17 from StringIO
import StringIO
18 from tempfile
import mkdtemp
19 from threading
import BoundedSemaphore
, RLock
20 from subprocess
import Popen
, PIPE
21 from mutagen
import FileType
22 from audiomangler
.config
import Config
23 from audiomangler
.tag
import NormMetaData
24 from audiomangler
.task
import CLITask
, generator_task
25 from audiomangler
.expression
import Expr
, Format
26 from audiomangler
import util
27 from audiomangler
.util
import ClassInitMeta
28 from mutagen
import File
33 __metaclass__
= ClassInitMeta
35 has_from_wav_multi
= False
36 has_from_wav_pipe
= False
37 has_to_wav_pipe
= False
38 has_replaygain
= False
41 def __classinit__(cls
, name__
, bases__
, cls_dict
):
42 if 'type_' in cls_dict
:
43 codec_map
[cls_dict
['type_']] = cls
46 def _conv_out_filename(cls
, filename
):
47 return ''.join((filename
.rsplit('.', 1)[0], '.', cls
.ext
))
50 def from_wav_multi(cls
, indir
, infiles
, outfiles
):
51 if not hasattr('from_wav_multi_cmd'):
53 encopts
= Config
['encopts']
57 encopts
= tuple(encopts
.split())
58 args
= cls
.from_wav_multi_cmd
.evaluate({
60 'infiles':tuple(infiles
),
61 'outfiles':tuple(outfiles
),
67 return (CLITask(args
=args
, stdin
='/dev/null', stdout
='/dev/null', stderr
=sys
.stderr
, background
=True))
70 def from_wav_pipe(cls
, infile
, outfile
):
71 if not hasattr(cls
, 'from_wav_pipe_cmd'):
73 encopts
= Config
['encopts']
77 encopts
= tuple(encopts
.split())
78 outfile
= cls
._conv
_out
_filename
(infile
)
85 'encoder': cls
.encoder
87 args
= cls
.from_wav_pipe_cmd
.evaluate(env
)
89 if hasattr(cls
, '_from_wav_pipe_stdin'):
90 stdin
= cls
._from
_wav
_pipe
_stdin
.evaluate(env
)
91 return CLITask(args
=args
, stdin
=stdin
, stdout
='/dev/null', stderr
=sys
.stderr
, background
=True)
94 def to_wav_pipe(cls
, infile
, outfile
):
95 if not hasattr(cls
, 'to_wav_pipe_cmd'):
102 'decoder':cls
.decoder
104 args
= cls
.to_wav_pipe_cmd
.evaluate(env
)
106 if hasattr(cls
, 'to_wav_pipe_stdout'):
107 stdout
= cls
.to_wav_pipe_stdout
.evaluate(env
)
108 return CLITask(args
=args
, stdin
='/dev/null', stdout
=stdout
, stderr
=sys
.stderr
, background
=False)
112 def add_replaygain(cls
, files
, metas
=None):
114 'replaygain':cls
.replaygain
,
117 if metas
and hasattr(cls
, 'calc_replaygain_cmd'):
118 task
= CLITask(*cls
.calc_replaygain_cmd
.evaluate(env
))
120 tracks
, album
= cls
.calc_replaygain(output
)
122 for meta
, track
in zip(metas
, tracks
):
126 elif hasattr(cls
, 'replaygain_cmd'):
127 task
= CLITask(*cls
.replaygain_cmd
.evaluate(env
))
129 elif hasattr(cls
, 'calc_replaygain'):
130 task
= CLITask(*cls
.calc_replaygain_cmd
.evaluate(env
))
132 tracks
, album
= cls
.calc_replaygain(output
)
133 for trackfile
, trackgain
in zip(files
, tracks
):
135 m
= NormMetaData(trackgain
+ album
)
140 class MP3Codec(Codec
):
144 replaygain
= 'mp3gain'
145 has_from_wav_multi
= True
146 has_replaygain
= True
147 from_wav_multi_cmd
= Expr("(encoder, '--quiet') + encopts + ('--noreplaygain', '--nogapout', indir, '--nogaptags', '--nogap') + infiles")
148 calc_replaygain_cmd
= Expr("(replaygain, '-q', '-o', '-s', 's')+files")
151 def calc_replaygain(out
):
153 out
= [l
.split('\t')[2:4] for l
in out
.splitlines()[1:]]
156 gain
= ' '.join((i
[0], 'dB'))
157 peak
= '%.8f'% (float(i
[1]) / 32768)
158 tracks
.append((('replaygain_track_gain', gain
), ('replaygain_track_peak', peak
)))
159 gain
= ' '.join((out
[-1][0], 'dB'))
160 peak
= '%.8f'% (float(out
[-1][1]) / 32768)
161 album
= (('replaygain_album_gain', gain
), ('replaygain_album_peak', peak
))
164 class WavPackCodec(Codec
):
169 replaygain
= 'wvgain'
170 has_to_wav_pipe
= True
171 has_from_wav_pipe
= True
172 has_replaygain
= True
174 to_wav_pipe_cmd
= Expr("(decoder, '-q', '-w', infile, '-o', '-')")
175 to_wav_pipe_stdout
= Expr("outfile")
176 from_wav_pipe_cmd
= Expr("(encoder, '-q')+encopts+(infile, '-o', outfile)")
177 replaygain_cmd
= Expr("(replaygain, '-a')+files")
179 class FLACCodec(Codec
):
184 replaygain
= 'metaflac'
185 has_to_wav_pipe
= True
186 has_from_wav_pipe
= True
187 has_replaygain
= True
189 to_wav_pipe_cmd
= Expr("(decoder, '-s', '-c', '-d', infile)")
190 to_wav_pipe_stdout
= Expr("outfile")
191 from_wav_pipe_cmd
= Expr("(encoder, '-s')+encopts+(infile, )")
192 replaygain_cmd
= Expr("(replaygain, '--add-replay-gain')+files")
194 class OggVorbisCodec(Codec
):
199 replaygain
= 'vorbisgain'
200 has_to_wav_pipe
= True
201 has_from_wav_pipe
= True
202 has_replaygain
= True
203 to_wav_pipe_cmd
= Expr("(decoder, '-Q', '-o', '-', infile)")
204 to_wav_pipe_stdout
= Expr("outfile")
205 from_wav_pipe_cmd
= Expr("(encoder, '-Q')+encopts+('-o', outfile, infile)")
206 replaygain_cmd
= Expr("(replaygain, '-q', '-a')+files")
207 calc_replaygain_cmd
= Expr("(replaygain, '-a', '-n', '-d')+files")
210 def calc_replaygain(cls
, files
):
212 args
= [cls
.replaygain
, '-and']
214 p
= Popen(args
=args
, stdout
=PIPE
, stderr
=PIPE
)
215 (out
, err
) = p
.communicate()
217 for match
in re
.finditer('^\s*(\S+ dB)\s*\|\s*([0-9]+)\s*\|', out
,
219 gain
= match
.group(1)
220 peak
= float(match
.group(2)) / 32768
221 apeak
= max(apeak
, peak
)
223 tracks
.append((('replaygain_track_gain', gain
), ('replaygain_track_peak', peak
)))
224 again
= re
.search('^Recommended Album Gain:\s*(\S+ dB)', err
, re
.M
)
226 album
= (('replaygain_album_gain', again
.group(1)), ('replaygain_album_peak', "%.8f" % apeak
))
228 album
= (('replaygain_album_peak', apeak
),)
231 def transcode_track(dtask
, etask
, sem
):
238 def check_and_copy_cover(fileset
, targetfiles
):
239 cover_sizes
= Config
['cover_sizes']
242 cover_out_filename
= Config
['cover_out_filename']
243 if not cover_out_filename
:
245 cover_out_filename
= Format(cover_out_filename
)
246 cover_sizes
= cover_sizes
.split(',')
250 cover_filenames
= Config
['cover_filenames']
252 cover_filenames
= cover_filenames
.split(',')
255 cover_out_filenames
= [cover_out_filename
.evaluate({'size':s
}) for s
in cover_sizes
]
256 for (infile
, targetfile
) in zip(fileset
, targetfiles
):
257 outdir
= os
.path
.split(targetfile
)[0]
258 if outdir
in outdirs
: continue
259 if all(os
.path
.isfile(os
.path
.join(outdir
, filename
) for filename
in cover_out_filenames
)):
263 for filename
in (os
.path
.join(infile
.meta
['dir'], file_
) for file_
in cover_filenames
):
265 d
= open(filename
).read()
266 i
= Image
.open(StringIO(d
))
272 tags
= [(value
.type, value
) for key
, value
in infile
.tags
.items()
273 if key
.startswith('APIC') and hasattr(value
, 'type')
274 and value
.type in (0, 3)]
275 tags
.sort(None, None, True)
276 for t
, value
in tags
:
280 i
= Image
.open(StringIO(d
))
286 for s
in cover_sizes
:
295 iw
= i
.resize((w
, h
), Image
.ADAPTIVE
)
296 filename
= os
.path
.join(
297 outdir
, cover_out_filename
.evaluate({'size':s
})
299 print "save cover %s" % filename
303 rg_keys
= 'replaygain_track_gain', 'replaygain_track_peak', 'replaygain_album_gain', 'replaygain_album_peak'
304 def transcode_set(targetcodec
, fileset
, targetfiles
, alsem
, trsem
, workdirs
, workdirs_l
):
310 workdir
, pipefiles
= workdirs
.pop()
312 outfiles
= map(targetcodec
._conv
_out
_filename
, pipefiles
[:len(fileset
)])
313 if targetcodec
._from
_wav
_pipe
:
314 for i
, p
, o
in zip(fileset
, pipefiles
, outfiles
):
316 dtask
= get_codec(i
).to_wav_pipe(i
.meta
['path'], p
)
317 etask
= targetcodec
.from_wav_pipe(p
, o
)
319 #ttask = FuncTask(background=True, target=transcode_track,
320 #args=(dtask, etask, trsem)
324 bgprocs
.add(ttask
.run())
329 elif targetcodec
._from
_wav
_multi
:
330 etask
= targetcodec
.from_wav_multi(
331 workdir
, pipefiles
[:len(fileset
)], outfiles
334 for i
, o
in zip(fileset
, pipefiles
):
335 task
= get_codec(i
).to_wav_pipe(i
.meta
['path'], o
)
340 newreplaygain
= False
341 for i
, o
in zip(fileset
, outfiles
):
343 if not (i
.lossless
and targetcodec
.lossless
):
348 if not newreplaygain
:
354 if newreplaygain
and targetcodec
._replaygain
:
355 targetcodec
.add_replaygain(outfiles
, metas
)
356 for i
, m
, o
, t
in zip(fileset
, metas
, outfiles
, targetfiles
):
360 targetdir
= os
.path
.split(t
)[0]
361 if targetdir
not in dirs
:
363 if not os
.path
.isdir(targetdir
):
364 os
.makedirs(targetdir
)
365 print "%s -> %s" %(i
.filename
, t
)
366 util
.move(o
.filename
, t
)
367 check_and_copy_cover(fileset
, targetfiles
)
371 workdirs
.add((workdir
, pipefiles
))
376 def sync_sets(sets
=[], targettids
=()):
378 semct
= int(Config
['jobs'])
379 except (ValueError, TypeError):
382 targetcodec
= Config
['type']
383 if ',' in targetcodec
:
384 allowedcodecs
= targetcodec
.split(',')
385 targetcodec
= allowedcodecs
[0]
386 allowedcodecs
= set(allowedcodecs
)
388 allowedcodecs
= set((targetcodec
,))
389 targetcodec
= get_codec(targetcodec
)
390 workdir
= Config
['workdir'] or Config
['base']
391 workdir
= mkdtemp(dir=workdir
, prefix
='audiomangler_work_')
392 if targetcodec
._from
_wav
_pipe
:
393 if len(sets
) > semct
* 2:
394 alsem
= BoundedSemaphore(semct
)
397 trsem
= BoundedSemaphore(semct
)
399 elif targetcodec
._from
_wav
_multi
:
401 alsem
= BoundedSemaphore(semct
)
402 numpipes
= max(len(s
) for s
in sets
)
405 for n
in range(semct
):
406 w
= os
.path
.join(workdir
, "%02d" % n
)
409 for m
in range(numpipes
):
410 pipes
.append(os
.path
.join(w
, "%02d.wav"%m
))
413 workdirs
.add((w
, pipes
))
415 if all(file_
.type_
in allowedcodecs
for file_
in fileset
):
416 targetfiles
= [f
.format() for f
in fileset
]
417 if not all(file_
.tid
in targettids
for file_
in fileset
):
418 print "copying files"
422 targetdir
= os
.path
.split(t
)[0]
423 if targetdir
not in dirs
:
425 if not os
.path
.isdir(targetdir
):
426 os
.makedirs(targetdir
)
427 print "%s -> %s" % (i
.filename
, t
)
428 util
.copy(i
.filename
, t
)
429 codecs
= set((get_codec(f
) for f
in fileset
))
431 if codec
and not codecs
and codec
._replaygain
:
432 codec
.add_replaygain(targetfiles
)
433 check_and_copy_cover(fileset
, targetfiles
)
435 postadd
= {'type':targetcodec
.type_
, 'ext':targetcodec
.ext
}
436 targetfiles
= [f
.format(postadd
=postadd
)for f
in fileset
]
437 if all(file_
.tid
in targettids
for file_
in fileset
):
438 check_and_copy_cover(fileset
, targetfiles
)
442 for task
in list(bgtasks
):
445 # FuncTask use needs rewrite
447 #background=True, target=transcode_set, args=(
448 #targetcodec, fileset, targetfiles, alsem, trsem, workdirs,
452 bgtasks
.add(task
.run())
457 for w
, ps
in workdirs
:
464 if isinstance(item
, FileType
):
465 item
= getattr(item
, 'type_')
466 return codec_map
[item
]
468 __all__
= ['sync_sets', 'get_codec']