1 ###########################################################################
2 # Copyright (C) 2008 by Andrew Mahone
3 # <andrew.mahone@gmail.com>
5 # Copyright: See COPYING file that comes with this distribution
7 ###########################################################################
13 from cStringIO
import StringIO
15 from StringIO
import StringIO
16 from tempfile
import mkdtemp
17 from threading
import BoundedSemaphore
, RLock
18 from subprocess
import Popen
, PIPE
19 from mutagen
import FileType
20 from audiomangler
import Config
, from_config
, FuncTask
, CLITask
, TaskSet
, Expr
, Format
, File
, util
, NormMetaData
22 class CodecMeta(type):
23 def __new__(cls
, name
, bases
, cls_dict
):
24 class_init
= cls_dict
.get('__classinit__',None)
26 cls_dict
['__classinit__'] = staticmethod(class_init
)
27 return super(CodecMeta
,cls
).__new
__(cls
, name
, bases
, cls_dict
)
29 def __init__(self
, name
, bases
, cls_dict
):
30 if callable(getattr(self
,'__classinit__',None)):
31 self
.__classinit
__(self
, name
, bases
, cls_dict
)
36 __metaclass__
= CodecMeta
38 _from_wav_multi
= False
39 _from_wav_pipe
= 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 getattr(cls
,'_from_wav_multi_cmd',None):
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
,stdin
='/dev/null',stdout
='/dev/null',stderr
=sys
.stderr
,background
=True))
72 def from_wav_pipe(cls
, infile
, outfile
):
73 if not getattr(cls
,'_from_wav_pipe_cmd',None):
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
)
91 if hasattr(cls
,'_from_wav_pipe_stdin'):
92 stdin
= cls
._from
_wav
_pipe
_stdin
.evaluate(env
)
93 return CLITask(args
=args
,stdin
=stdin
,stdout
='/dev/null',stderr
=sys
.stderr
,background
=True)
96 def to_wav_pipe(cls
, infile
, outfile
):
97 if not getattr(cls
,'_to_wav_pipe_cmd',None):
104 'decoder':cls
.decoder
106 args
= cls
._to
_wav
_pipe
_cmd
.evaluate(env
)
108 if hasattr(cls
,'_to_wav_pipe_stdout'):
109 stdout
= cls
._to
_wav
_pipe
_stdout
.evaluate(env
)
110 return CLITask(args
=args
,stdin
='/dev/null',stdout
=stdout
,stderr
=sys
.stderr
,background
=False)
113 def add_replaygain(cls
,files
,metas
=None):
114 if metas
and hasattr(cls
, 'calc_replaygain'):
115 tracks
, album
= cls
.calc_replaygain(files
)
117 for meta
,track
in zip(metas
,tracks
):
121 elif hasattr(cls
,'_replaygain_cmd'):
123 'replaygain':cls
.replaygain
,
126 args
= cls
._replaygain
_cmd
.evaluate(env
)
127 CLITask(args
=args
,stdin
='/dev/null',stdout
='/dev/null',stderr
='/dev/null',background
=False).run()
128 elif hasattr(cls
,'calc_replaygain'):
129 tracks
, album
= cls
.calc_replaygain(files
)
130 for trackfile
,trackgain
in zip(files
,tracks
):
132 m
= NormMetaData(trackgain
+ album
)
138 class MP3Codec(Codec
):
142 replaygain
= 'mp3gain'
143 _from_wav_multi
= True
145 _from_wav_multi_cmd
= Expr("(encoder,'--quiet')+encopts+('--noreplaygain','--nogapout',indir,'--nogaptags','--nogap')+infiles")
148 def calc_replaygain(cls
,files
):
150 args
= [cls
.replaygain
,'-qo','-s','s']
152 p
= Popen(args
=args
, stdout
=PIPE
, stderr
=PIPE
)
153 (out
, err
) = p
.communicate()
154 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'
171 _from_wav_pipe
= 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,'-qa')+files")
179 class FLACCodec(Codec
):
184 replaygain
= 'metaflac'
186 _from_wav_pipe
= 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'
201 _from_wav_pipe
= 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)")
209 def calc_replaygain(cls
,files
):
211 args
= [cls
.replaygain
,'-and']
213 p
= Popen(args
=args
, stdout
=PIPE
, stderr
=PIPE
)
214 (out
, err
) = p
.communicate()
216 for match
in re
.finditer('^\s*(\S+ dB)\s*\|\s*([0-9]+)\s*\|',out
,re
.M
):
217 gain
= match
.group(1)
218 peak
= float(match
.group(2)) / 32768
219 apeak
= max(apeak
,peak
)
221 tracks
.append((('replaygain_track_gain',gain
),('replaygain_track_peak',peak
)))
222 again
= re
.search('^Recommended Album Gain:\s*(\S+ dB)',err
,re
.M
)
224 album
= (('replaygain_album_gain',again
.group(1)),('replaygain_album_peak',"%.8f" % apeak
))
226 album
= (('replaygain_album_peak',apeak
),)
229 def transcode_track(dtask
, etask
, sem
):
236 def check_and_copy_cover(fileset
,targetfiles
):
237 cover_sizes
= Config
['cover_sizes']
240 cover_out_filename
= Config
['cover_out_filename']
241 if not cover_out_filename
:
243 cover_out_filename
= Format(cover_out_filename
)
244 cover_sizes
= cover_sizes
.split(',')
248 cover_filenames
= Config
['cover_filenames']
250 cover_filenames
= cover_filenames
.split(',')
253 cover_out_filenames
= [cover_out_filename
.evaluate({'size':s
}) for s
in cover_sizes
]
254 for (infile
,targetfile
) in zip(fileset
,targetfiles
):
255 outdir
= os
.path
.split(targetfile
)[0]
256 if outdir
in outdirs
: continue
257 if reduce(lambda x
,y
: x
and os
.path
.isfile(os
.path
.join(outdir
,y
)), cover_out_filenames
, True):
261 for filename
in (os
.path
.join(infile
.meta
['dir'],f
) for f
in cover_filenames
):
263 d
= open(filename
).read()
264 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
})
299 rg_keys
= ('replaygain_track_gain','replaygain_track_peak','replaygain_album_gain','replaygain_album_peak')
300 def transcode_set(targetcodec
,fileset
,targetfiles
,alsem
,trsem
,workdirs
,workdirs_l
):
306 workdir
, pipefiles
= workdirs
.pop()
308 outfiles
= map(targetcodec
._conv
_out
_filename
,pipefiles
[:len(fileset
)])
309 if targetcodec
._from
_wav
_pipe
:
310 for i
,p
,o
in zip(fileset
,pipefiles
,outfiles
):
312 dtask
= get_codec(i
).to_wav_pipe(i
.meta
['path'],p
)
313 etask
= targetcodec
.from_wav_pipe(p
,o
)
314 ttask
= FuncTask(background
=True,target
=transcode_track
,
315 args
=(dtask
,etask
,trsem
)
319 bgprocs
.add(ttask
.run())
324 elif targetcodec
._from
_wav
_multi
:
325 etask
= targetcodec
.from_wav_multi(
326 workdir
,pipefiles
[:len(fileset
)],outfiles
329 for i
,o
in zip(fileset
,pipefiles
):
330 task
= get_codec(i
).to_wav_pipe(i
.meta
['path'],o
)
335 newreplaygain
= False
336 for i
,o
in zip(fileset
,outfiles
):
338 if not (i
.lossless
and targetcodec
.lossless
):
343 if not newreplaygain
:
349 if newreplaygain
and targetcodec
._replaygain
:
350 targetcodec
.add_replaygain(outfiles
,metas
)
351 for m
,o
,t
in zip(metas
,outfiles
,targetfiles
):
355 targetdir
= os
.path
.split(t
)[0]
356 if targetdir
not in dirs
:
358 if not os
.path
.isdir(targetdir
):
359 os
.makedirs(targetdir
)
360 print "%s -> %s" %(i
.filename
,t
)
361 util
.move(o
.filename
,t
)
362 check_and_copy_cover(fileset
,targetfiles
)
366 workdirs
.add((workdir
,pipefiles
))
371 def sync_sets(sets
=[],targettids
=()):
373 semct
= int(Config
['jobs'])
374 except (ValueError,TypeError):
377 targetcodec
= Config
['type']
378 if ',' in targetcodec
:
379 allowedcodecs
= targetcodec
.split(',')
380 targetcodec
= allowedcodecs
[0]
381 allowedcodecs
= set(allowedcodecs
)
383 allowedcodecs
= set((targetcodec
,))
384 targetcodec
= get_codec(targetcodec
)
385 workdir
= Config
['workdir'] or Config
['base']
386 workdir
= mkdtemp(dir=workdir
,prefix
='audiomangler_work_')
387 if targetcodec
._from
_wav
_pipe
:
388 if len(sets
) > semct
* 2:
389 alsem
= BoundedSemaphore(semct
)
392 trsem
= BoundedSemaphore(semct
)
394 elif targetcodec
._from
_wav
_multi
:
396 alsem
= BoundedSemaphore(semct
)
397 numpipes
= max(len(s
) for s
in sets
)
400 for n
in range(semct
):
401 w
= os
.path
.join(workdir
,"%02d" % n
)
404 for m
in range(numpipes
):
405 pipes
.append(os
.path
.join(w
,"%02d.wav"%m
))
408 workdirs
.add((w
,pipes
))
410 if reduce(lambda x
,y
: x
and (y
.type_
in allowedcodecs
), fileset
, True):
411 targetfiles
= [f
.format() for f
in fileset
]
412 if not reduce(lambda x
,y
: x
and (y
.tid
in targettids
),
414 print "copying files"
418 targetdir
= os
.path
.split(t
)[0]
419 if targetdir
not in dirs
:
421 if not os
.path
.isdir(targetdir
):
422 os
.makedirs(targetdir
)
423 print "%s -> %s" % (i
.filename
,t
)
424 util
.copy(i
.filename
,t
)
428 for task
in list(bgtasks
):
431 postadd
= {'type':targetcodec
.type_
,'ext':targetcodec
.ext
}
432 targetfiles
= [f
.format(postadd
=postadd
)for f
in fileset
]
434 background
=True,target
=transcode_set
,args
=(
435 targetcodec
,fileset
,targetfiles
,alsem
,trsem
,workdirs
,workdirs_l
438 bgtasks
.add(task
.run())
443 for w
,ps
in workdirs
:
450 if isinstance(item
, FileType
):
451 item
= getattr(item
,'type_')
452 return codec_map
[item
]
454 __all__
= ['sync_sets','get_codec']