9 class PostProcessor(object):
10 """Post Processor class.
12 PostProcessor objects can be added to downloaders with their
13 add_post_processor() method. When the downloader has finished a
14 successful download, it will take its internal chain of PostProcessors
15 and start calling the run() method on each one of them, first with
16 an initial argument and then with the returned value of the previous
19 The chain will be stopped if one of them ever returns None or the end
20 of the chain is reached.
22 PostProcessor objects follow a "mutual registration" process similar
23 to InfoExtractor objects.
28 def __init__(self, downloader=None):
29 self._downloader = downloader
31 def set_downloader(self, downloader):
32 """Sets the downloader for this PP."""
33 self._downloader = downloader
35 def run(self, information):
36 """Run the PostProcessor.
38 The "information" argument is a dictionary like the ones
39 composed by InfoExtractors. The only difference is that this
40 one has an extra field called "filepath" that points to the
43 This method returns a tuple, the first element of which describes
44 whether the original file should be kept (i.e. not deleted - None for
45 no preference), and the second of which is the updated information.
47 In addition, this method may raise a PostProcessingError
48 exception if post processing fails.
50 return None, information # by default, keep file and do nothing
52 class FFmpegPostProcessorError(PostProcessingError):
55 class AudioConversionError(PostProcessingError):
58 class FFmpegPostProcessor(PostProcessor):
59 def __init__(self,downloader=None):
60 PostProcessor.__init__(self, downloader)
61 self._exes = self.detect_executables()
64 def detect_executables():
67 subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
71 programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
72 return dict((program, executable(program)) for program in programs)
74 def run_ffmpeg(self, path, out_path, opts):
75 if not self._exes['ffmpeg'] and not self._exes['avconv']:
76 raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.')
77 cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y', '-i', encodeFilename(path)]
79 [encodeFilename(self._ffmpeg_filename_argument(out_path))])
80 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
81 stdout,stderr = p.communicate()
83 stderr = stderr.decode('utf-8', 'replace')
84 msg = stderr.strip().split('\n')[-1]
85 raise FFmpegPostProcessorError(msg)
87 def _ffmpeg_filename_argument(self, fn):
88 # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
89 if fn.startswith(u'-'):
93 class FFmpegExtractAudioPP(FFmpegPostProcessor):
94 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
95 FFmpegPostProcessor.__init__(self, downloader)
96 if preferredcodec is None:
97 preferredcodec = 'best'
98 self._preferredcodec = preferredcodec
99 self._preferredquality = preferredquality
100 self._nopostoverwrites = nopostoverwrites
102 def get_audio_codec(self, path):
103 if not self._exes['ffprobe'] and not self._exes['avprobe']: return None
105 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
106 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
107 output = handle.communicate()[0]
108 if handle.wait() != 0:
110 except (IOError, OSError):
113 for line in output.decode('ascii', 'ignore').split('\n'):
114 if line.startswith('codec_name='):
115 audio_codec = line.split('=')[1].strip()
116 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
120 def run_ffmpeg(self, path, out_path, codec, more_opts):
121 if not self._exes['ffmpeg'] and not self._exes['avconv']:
122 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
126 acodec_opts = ['-acodec', codec]
127 opts = ['-vn'] + acodec_opts + more_opts
129 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
130 except FFmpegPostProcessorError as err:
131 raise AudioConversionError(err.message)
133 def run(self, information):
134 path = information['filepath']
136 filecodec = self.get_audio_codec(path)
137 if filecodec is None:
138 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
141 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
142 if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
143 # Lossless, but in another container
146 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
147 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
148 # Lossless if possible
150 extension = filecodec
151 if filecodec == 'aac':
152 more_opts = ['-f', 'adts']
153 if filecodec == 'vorbis':
157 acodec = 'libmp3lame'
160 if self._preferredquality is not None:
161 if int(self._preferredquality) < 10:
162 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
164 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
166 # We convert the audio (lossy)
167 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
168 extension = self._preferredcodec
170 if self._preferredquality is not None:
171 if int(self._preferredquality) < 10:
172 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
174 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
175 if self._preferredcodec == 'aac':
176 more_opts += ['-f', 'adts']
177 if self._preferredcodec == 'm4a':
178 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
179 if self._preferredcodec == 'vorbis':
181 if self._preferredcodec == 'wav':
183 more_opts += ['-f', 'wav']
185 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
186 new_path = prefix + sep + extension
188 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
190 self._nopostoverwrites = True
193 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
194 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
196 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
197 self.run_ffmpeg(path, new_path, acodec, more_opts)
199 etype,e,tb = sys.exc_info()
200 if isinstance(e, AudioConversionError):
201 msg = u'audio conversion failed: ' + e.message
203 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
204 raise PostProcessingError(msg)
206 # Try to update the date time for extracted audio file.
207 if information.get('filetime') is not None:
209 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
211 self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
213 information['filepath'] = new_path
214 return self._nopostoverwrites,information
216 class FFmpegVideoConvertor(FFmpegPostProcessor):
217 def __init__(self, downloader=None,preferedformat=None):
218 super(FFmpegVideoConvertor, self).__init__(downloader)
219 self._preferedformat=preferedformat
221 def run(self, information):
222 path = information['filepath']
223 prefix, sep, ext = path.rpartition(u'.')
224 outpath = prefix + sep + self._preferedformat
225 if information['ext'] == self._preferedformat:
226 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
227 return True,information
228 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
229 self.run_ffmpeg(path, outpath, [])
230 information['filepath'] = outpath
231 information['format'] = self._preferedformat
232 information['ext'] = self._preferedformat
233 return False,information