2 # -*- coding: utf-8 -*-
4 from __future__ import absolute_import
14 class PostProcessor(object):
15 """Post Processor class.
17 PostProcessor objects can be added to downloaders with their
18 add_post_processor() method. When the downloader has finished a
19 successful download, it will take its internal chain of PostProcessors
20 and start calling the run() method on each one of them, first with
21 an initial argument and then with the returned value of the previous
24 The chain will be stopped if one of them ever returns None or the end
25 of the chain is reached.
27 PostProcessor objects follow a "mutual registration" process similar
28 to InfoExtractor objects.
33 def __init__(self, downloader=None):
34 self._downloader = downloader
36 def set_downloader(self, downloader):
37 """Sets the downloader for this PP."""
38 self._downloader = downloader
40 def run(self, information):
41 """Run the PostProcessor.
43 The "information" argument is a dictionary like the ones
44 composed by InfoExtractors. The only difference is that this
45 one has an extra field called "filepath" that points to the
48 When this method returns None, the postprocessing chain is
49 stopped. However, this method may return an information
50 dictionary that will be passed to the next postprocessing
51 object in the chain. It can be the one it received after
54 In addition, this method may raise a PostProcessingError
55 exception that will be taken into account by the downloader
58 return information # by default, do nothing
60 class AudioConversionError(BaseException):
61 def __init__(self, message):
62 self.message = message
64 class FFmpegExtractAudioPP(PostProcessor):
65 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, keepvideo=False, nopostoverwrites=False):
66 PostProcessor.__init__(self, downloader)
67 if preferredcodec is None:
68 preferredcodec = 'best'
69 self._preferredcodec = preferredcodec
70 self._preferredquality = preferredquality
71 self._keepvideo = keepvideo
72 self._nopostoverwrites = nopostoverwrites
73 self._exes = self.detect_executables()
76 def detect_executables():
79 subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
83 programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
84 return dict((program, executable(program)) for program in programs)
86 def get_audio_codec(self, path):
87 if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
89 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', '--', encodeFilename(path)]
90 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
91 output = handle.communicate()[0]
92 if handle.wait() != 0:
94 except (IOError, OSError):
97 for line in output.decode('ascii', 'ignore').split('\n'):
98 if line.startswith('codec_name='):
99 audio_codec = line.split('=')[1].strip()
100 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
104 def run_ffmpeg(self, path, out_path, codec, more_opts):
105 if not self._exes['ffmpeg'] and not self._exes['avconv']:
106 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
110 acodec_opts = ['-acodec', codec]
111 if self._nopostoverwrites:
112 overwrite_opts = '-n'
114 overwrite_opts = '-y'
115 cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], overwrite_opts, '-i', encodeFilename(path), '-vn']
116 + acodec_opts + more_opts +
117 ['--', encodeFilename(out_path)])
118 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
119 stdout,stderr = p.communicate()
120 if p.returncode != 0:
121 msg = stderr.strip().split('\n')[-1]
122 raise AudioConversionError(msg)
124 def run(self, information):
125 path = information['filepath']
127 filecodec = self.get_audio_codec(path)
128 if filecodec is None:
129 self._downloader.to_stderr(u'WARNING: unable to obtain file audio codec with ffprobe')
133 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
134 if self._preferredcodec == 'm4a' and filecodec == 'aac':
135 # Lossless, but in another container
137 extension = self._preferredcodec
138 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
139 elif filecodec in ['aac', 'mp3', 'vorbis']:
140 # Lossless if possible
142 extension = filecodec
143 if filecodec == 'aac':
144 more_opts = ['-f', 'adts']
145 if filecodec == 'vorbis':
149 acodec = 'libmp3lame'
152 if self._preferredquality is not None:
153 if int(self._preferredquality) < 10:
154 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
156 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
158 # We convert the audio (lossy)
159 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
160 extension = self._preferredcodec
162 if self._preferredquality is not None:
163 if int(self._preferredquality) < 10:
164 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
166 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
167 if self._preferredcodec == 'aac':
168 more_opts += ['-f', 'adts']
169 if self._preferredcodec == 'm4a':
170 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
171 if self._preferredcodec == 'vorbis':
173 if self._preferredcodec == 'wav':
175 more_opts += ['-f', 'wav']
177 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
178 new_path = prefix + sep + extension
179 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
181 self.run_ffmpeg(path, new_path, acodec, more_opts)
183 etype,e,tb = sys.exc_info()
184 if isinstance(e, AudioConversionError):
185 self._downloader.to_stderr(u'ERROR: audio conversion failed: ' + e.message)
187 self._downloader.to_stderr(u'ERROR: error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg'))
190 # Try to update the date time for extracted audio file.
191 if information.get('filetime') is not None:
193 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
195 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
197 if not self._keepvideo:
199 os.remove(encodeFilename(path))
200 except (IOError, OSError):
201 self._downloader.to_stderr(u'WARNING: Unable to remove downloaded video file')
204 information['filepath'] = new_path