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 ###########################################################################
14 from cStringIO
import StringIO
16 from StringIO
import StringIO
17 from tempfile
import mkdtemp
18 from threading
import BoundedSemaphore
, RLock
19 from subprocess
import Popen
, PIPE
20 from mutagen
import FileType
21 from audiomangler
.config
import Config
, from_config
22 from audiomangler
.tag
import NormMetaData
23 from audiomangler
.task
import CLIPipelineTask
, CLITask
, generator_task
24 from audiomangler
.expression
import Expr
, Format
, FileFormat
25 from audiomangler
import util
26 from audiomangler
.util
import ClassInitMeta
27 from mutagen
import File
28 from functools
import wraps
33 __metaclass__
= ClassInitMeta
35 _from_wav_multi
= False
36 _from_wav_pipe
= False
40 def __classinit__(cls
, name
, bases
, cls_dict
):
41 if 'type_' in cls_dict
:
42 codec_map
[cls_dict
['type_']] = cls
45 def _conv_out_filename(cls
, filename
):
46 return ''.join((filename
.rsplit('.',1)[0],'.',cls
.ext
))
49 def from_wav_multi(cls
,indir
,infiles
,outfiles
):
50 if not getattr(cls
,'_from_wav_multi_cmd',None):
52 encopts
= Config
['encopts']
56 encopts
= tuple(encopts
.split())
57 args
= cls
._from
_wav
_multi
_cmd
.evaluate({
59 'infiles':tuple(infiles
),
60 'outfiles':tuple(outfiles
),
66 return (CLITask(args
=args
,stdin
='/dev/null',stdout
='/dev/null',stderr
=sys
.stderr
,background
=True))
69 def from_wav_pipe(cls
, infile
, outfile
):
70 if not getattr(cls
,'_from_wav_pipe_cmd',None):
72 encopts
= Config
['encopts']
76 encopts
= tuple(encopts
.split())
77 outfile
= cls
._conv
_out
_filename
(infile
)
84 'encoder': cls
.encoder
86 args
= cls
._from
_wav
_pipe
_cmd
.evaluate(env
)
88 if hasattr(cls
,'_from_wav_pipe_stdin'):
89 stdin
= cls
._from
_wav
_pipe
_stdin
.evaluate(env
)
90 return CLITask(args
=args
,stdin
=stdin
,stdout
='/dev/null',stderr
=sys
.stderr
,background
=True)
93 def to_wav_pipe(cls
, infile
, outfile
):
94 if not getattr(cls
,'_to_wav_pipe_cmd',None):
101 'decoder':cls
.decoder
103 args
= cls
._to
_wav
_pipe
_cmd
.evaluate(env
)
105 if hasattr(cls
,'_to_wav_pipe_stdout'):
106 stdout
= cls
._to
_wav
_pipe
_stdout
.evaluate(env
)
107 return CLITask(args
=args
,stdin
='/dev/null',stdout
=stdout
,stderr
=sys
.stderr
,background
=False)
111 def add_replaygain(cls
,files
,metas
=None):
113 'replaygain':cls
.replaygain
,
116 if metas
and hasattr(cls
, '_calc_replaygain_cmd'):
117 task
= CLITask(*cls
._calc
_replaygain
_cmd
.evaluate(env
))
119 tracks
, album
= cls
.calc_replaygain(output
)
121 for meta
,track
in zip(metas
,tracks
):
125 elif hasattr(cls
,'_replaygain_cmd'):
126 task
= CLITask(*cls
._replaygain
_cmd
.evaluate(env
))
128 elif hasattr(cls
,'calc_replaygain'):
129 task
= CLITask(*cls
._calc
_replaygain
_cmd
.evaluate(env
))
131 tracks
, album
= cls
.calc_replaygain(output
)
132 for trackfile
,trackgain
in zip(files
,tracks
):
134 m
= NormMetaData(trackgain
+ album
)
139 class MP3Codec(Codec
):
143 replaygain
= 'mp3gain'
144 _from_wav_multi
= True
146 _from_wav_multi_cmd
= Expr("(encoder,'--quiet')+encopts+('--noreplaygain','--nogapout',indir,'--nogaptags','--nogap')+infiles")
147 _calc_replaygain_cmd
= Expr("(replaygain,'-q','-o','-s','s')+files")
150 def calc_replaygain(out
):
152 out
= [l
.split('\t')[2:4] for l
in out
.splitlines()[1:]]
155 gain
= ' '.join((i
[0],'dB'))
156 peak
= '%.8f'% (float(i
[1]) / 32768)
157 tracks
.append((('replaygain_track_gain',gain
),('replaygain_track_peak',peak
)))
158 gain
= ' '.join((out
[-1][0],'dB'))
159 peak
= '%.8f'% (float(out
[-1][1]) / 32768)
160 album
= (('replaygain_album_gain',gain
),('replaygain_album_peak',peak
))
163 class WavPackCodec(Codec
):
168 replaygain
= 'wvgain'
170 _from_wav_pipe
= True
173 _to_wav_pipe_cmd
= Expr("(decoder,'-q','-w',infile,'-o','-')")
174 _to_wav_pipe_stdout
= Expr("outfile")
175 _from_wav_pipe_cmd
= Expr("(encoder,'-q')+encopts+(infile,'-o',outfile)")
176 _replaygain_cmd
= Expr("(replaygain,'-a')+files")
178 class FLACCodec(Codec
):
183 replaygain
= 'metaflac'
185 _from_wav_pipe
= True
188 _to_wav_pipe_cmd
= Expr("(decoder,'-s','-c','-d',infile)")
189 _to_wav_pipe_stdout
= Expr("outfile")
190 _from_wav_pipe_cmd
= Expr("(encoder,'-s')+encopts+(infile,)")
191 _replaygain_cmd
= Expr("(replaygain,'--add-replay-gain')+files")
193 class OggVorbisCodec(Codec
):
198 replaygain
= 'vorbisgain'
200 _from_wav_pipe
= True
202 _to_wav_pipe_cmd
= Expr("(decoder,'-Q','-o','-',infile)")
203 _to_wav_pipe_stdout
= Expr("outfile")
204 _from_wav_pipe_cmd
= Expr("(encoder,'-Q')+encopts+('-o',outfile,infile)")
205 _replaygain_cmd
= Expr("(replaygain,'-q','-a')+files")
208 def calc_replaygain(cls
,files
):
210 args
= [cls
.replaygain
,'-and']
212 p
= Popen(args
=args
, stdout
=PIPE
, stderr
=PIPE
)
213 (out
, err
) = p
.communicate()
215 for match
in re
.finditer('^\s*(\S+ dB)\s*\|\s*([0-9]+)\s*\|',out
,re
.M
):
216 gain
= match
.group(1)
217 peak
= float(match
.group(2)) / 32768
218 apeak
= max(apeak
,peak
)
220 tracks
.append((('replaygain_track_gain',gain
),('replaygain_track_peak',peak
)))
221 again
= re
.search('^Recommended Album Gain:\s*(\S+ dB)',err
,re
.M
)
223 album
= (('replaygain_album_gain',again
.group(1)),('replaygain_album_peak',"%.8f" % apeak
))
225 album
= (('replaygain_album_peak',apeak
),)
228 def transcode_track(dtask
, etask
, sem
):
235 def check_and_copy_cover(fileset
,targetfiles
):
236 cover_sizes
= Config
['cover_sizes']
239 cover_out_filename
= Config
['cover_out_filename']
240 if not cover_out_filename
:
242 cover_out_filename
= Format(cover_out_filename
)
243 cover_sizes
= cover_sizes
.split(',')
247 cover_filenames
= Config
['cover_filenames']
249 cover_filenames
= cover_filenames
.split(',')
252 cover_out_filenames
= [cover_out_filename
.evaluate({'size':s
}) for s
in cover_sizes
]
253 for (infile
,targetfile
) in zip(fileset
,targetfiles
):
254 outdir
= os
.path
.split(targetfile
)[0]
255 if outdir
in outdirs
: continue
256 if reduce(lambda x
,y
: x
and os
.path
.isfile(os
.path
.join(outdir
,y
)), cover_out_filenames
, True):
260 for filename
in (os
.path
.join(infile
.meta
['dir'],f
) for f
in cover_filenames
):
262 d
= open(filename
).read()
263 i
= Image
.open(StringIO(d
))
269 tags
= [(value
.type,value
) for key
,value
in infile
.tags
.items()
270 if key
.startswith('APIC') and hasattr(value
,'type')
271 and value
.type in (0,3)]
272 tags
.sort(None,None,True)
277 i
= Image
.open(StringIO(d
))
283 for s
in cover_sizes
:
292 iw
= i
.resize((w
,h
),Image
.ADAPTIVE
)
293 filename
= os
.path
.join(
294 outdir
,cover_out_filename
.evaluate({'size':s
})
296 print "save cover %s" % filename
300 rg_keys
= ('replaygain_track_gain','replaygain_track_peak','replaygain_album_gain','replaygain_album_peak')
301 def transcode_set(targetcodec
,fileset
,targetfiles
,alsem
,trsem
,workdirs
,workdirs_l
):
307 workdir
, pipefiles
= workdirs
.pop()
309 outfiles
= map(targetcodec
._conv
_out
_filename
,pipefiles
[:len(fileset
)])
310 if targetcodec
._from
_wav
_pipe
:
311 for i
,p
,o
in zip(fileset
,pipefiles
,outfiles
):
313 dtask
= get_codec(i
).to_wav_pipe(i
.meta
['path'],p
)
314 etask
= targetcodec
.from_wav_pipe(p
,o
)
315 ttask
= FuncTask(background
=True,target
=transcode_track
,
316 args
=(dtask
,etask
,trsem
)
320 bgprocs
.add(ttask
.run())
325 elif targetcodec
._from
_wav
_multi
:
326 etask
= targetcodec
.from_wav_multi(
327 workdir
,pipefiles
[:len(fileset
)],outfiles
330 for i
,o
in zip(fileset
,pipefiles
):
331 task
= get_codec(i
).to_wav_pipe(i
.meta
['path'],o
)
336 newreplaygain
= False
337 for i
,o
in zip(fileset
,outfiles
):
339 if not (i
.lossless
and targetcodec
.lossless
):
344 if not newreplaygain
:
350 if newreplaygain
and targetcodec
._replaygain
:
351 targetcodec
.add_replaygain(outfiles
,metas
)
352 for i
,m
,o
,t
in zip(fileset
,metas
,outfiles
,targetfiles
):
356 targetdir
= os
.path
.split(t
)[0]
357 if targetdir
not in dirs
:
359 if not os
.path
.isdir(targetdir
):
360 os
.makedirs(targetdir
)
361 print "%s -> %s" %(i
.filename
,t
)
362 util
.move(o
.filename
,t
)
363 check_and_copy_cover(fileset
,targetfiles
)
367 workdirs
.add((workdir
,pipefiles
))
372 def sync_sets(sets
=[],targettids
=()):
374 semct
= int(Config
['jobs'])
375 except (ValueError,TypeError):
378 targetcodec
= Config
['type']
379 if ',' in targetcodec
:
380 allowedcodecs
= targetcodec
.split(',')
381 targetcodec
= allowedcodecs
[0]
382 allowedcodecs
= set(allowedcodecs
)
384 allowedcodecs
= set((targetcodec
,))
385 targetcodec
= get_codec(targetcodec
)
386 workdir
= Config
['workdir'] or Config
['base']
387 workdir
= mkdtemp(dir=workdir
,prefix
='audiomangler_work_')
388 if targetcodec
._from
_wav
_pipe
:
389 if len(sets
) > semct
* 2:
390 alsem
= BoundedSemaphore(semct
)
393 trsem
= BoundedSemaphore(semct
)
395 elif targetcodec
._from
_wav
_multi
:
397 alsem
= BoundedSemaphore(semct
)
398 numpipes
= max(len(s
) for s
in sets
)
401 for n
in range(semct
):
402 w
= os
.path
.join(workdir
,"%02d" % n
)
405 for m
in range(numpipes
):
406 pipes
.append(os
.path
.join(w
,"%02d.wav"%m
))
409 workdirs
.add((w
,pipes
))
411 if reduce(lambda x
,y
: x
and (y
.type_
in allowedcodecs
), fileset
, True):
412 targetfiles
= [f
.format() for f
in fileset
]
413 if not reduce(lambda x
,y
: x
and (y
.tid
in targettids
),
415 print "copying files"
419 targetdir
= os
.path
.split(t
)[0]
420 if targetdir
not in dirs
:
422 if not os
.path
.isdir(targetdir
):
423 os
.makedirs(targetdir
)
424 print "%s -> %s" % (i
.filename
,t
)
425 util
.copy(i
.filename
,t
)
426 codecs
= set((get_codec(f
) for f
in fileset
))
428 if codec
and not codecs
and codec
._replaygain
:
429 codec
.add_replaygain(targetfiles
)
430 check_and_copy_cover(fileset
,targetfiles
)
432 postadd
= {'type':targetcodec
.type_
,'ext':targetcodec
.ext
}
433 targetfiles
= [f
.format(postadd
=postadd
)for f
in fileset
]
434 if reduce(lambda x
,y
: x
and y
.tid
in targettids
, fileset
, True):
435 check_and_copy_cover(fileset
,targetfiles
)
439 for task
in list(bgtasks
):
443 background
=True,target
=transcode_set
,args
=(
444 targetcodec
,fileset
,targetfiles
,alsem
,trsem
,workdirs
,workdirs_l
447 bgtasks
.add(task
.run())
452 for w
,ps
in workdirs
:
459 if isinstance(item
, FileType
):
460 item
= getattr(item
,'type_')
461 return codec_map
[item
]
463 __all__
= ['sync_sets','get_codec']