10 class PostProcessor(object):
11 """Post Processor class.
13 PostProcessor objects can be added to downloaders with their
14 add_post_processor() method. When the downloader has finished a
15 successful download, it will take its internal chain of PostProcessors
16 and start calling the run() method on each one of them, first with
17 an initial argument and then with the returned value of the previous
20 The chain will be stopped if one of them ever returns None or the end
21 of the chain is reached.
23 PostProcessor objects follow a "mutual registration" process similar
24 to InfoExtractor objects.
29 def __init__(self, downloader=None):
30 self._downloader = downloader
32 def set_downloader(self, downloader):
33 """Sets the downloader for this PP."""
34 self._downloader = downloader
36 def run(self, information):
37 """Run the PostProcessor.
39 The "information" argument is a dictionary like the ones
40 composed by InfoExtractors. The only difference is that this
41 one has an extra field called "filepath" that points to the
44 This method returns a tuple, the first element of which describes
45 whether the original file should be kept (i.e. not deleted - None for
46 no preference), and the second of which is the updated information.
48 In addition, this method may raise a PostProcessingError
49 exception if post processing fails.
51 return None, information # by default, keep file and do nothing
53 class FFmpegPostProcessorError(PostProcessingError):
56 class AudioConversionError(PostProcessingError):
59 class FFmpegPostProcessor(PostProcessor):
60 def __init__(self,downloader=None):
61 PostProcessor.__init__(self, downloader)
62 self._exes = self.detect_executables()
65 def detect_executables():
68 subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
72 programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
73 return dict((program, executable(program)) for program in programs)
75 def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
76 if not self._exes['ffmpeg'] and not self._exes['avconv']:
77 raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.')
80 for path in input_paths:
81 files_cmd.extend(['-i', encodeFilename(path)])
82 cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y'] + files_cmd
84 [encodeFilename(self._ffmpeg_filename_argument(out_path))])
86 if self._downloader.params.get('verbose', False):
87 self._downloader.to_screen(u'[debug] ffmpeg command line: %s' % shell_quote(cmd))
88 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
89 stdout,stderr = p.communicate()
91 stderr = stderr.decode('utf-8', 'replace')
92 msg = stderr.strip().split('\n')[-1]
93 raise FFmpegPostProcessorError(msg)
95 def run_ffmpeg(self, path, out_path, opts):
96 self.run_ffmpeg_multiple_files([path], out_path, opts)
98 def _ffmpeg_filename_argument(self, fn):
99 # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
100 if fn.startswith(u'-'):
104 class FFmpegExtractAudioPP(FFmpegPostProcessor):
105 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
106 FFmpegPostProcessor.__init__(self, downloader)
107 if preferredcodec is None:
108 preferredcodec = 'best'
109 self._preferredcodec = preferredcodec
110 self._preferredquality = preferredquality
111 self._nopostoverwrites = nopostoverwrites
113 def get_audio_codec(self, path):
114 if not self._exes['ffprobe'] and not self._exes['avprobe']:
115 raise PostProcessingError(u'ffprobe or avprobe not found. Please install one.')
117 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
118 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
119 output = handle.communicate()[0]
120 if handle.wait() != 0:
122 except (IOError, OSError):
125 for line in output.decode('ascii', 'ignore').split('\n'):
126 if line.startswith('codec_name='):
127 audio_codec = line.split('=')[1].strip()
128 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
132 def run_ffmpeg(self, path, out_path, codec, more_opts):
133 if not self._exes['ffmpeg'] and not self._exes['avconv']:
134 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
138 acodec_opts = ['-acodec', codec]
139 opts = ['-vn'] + acodec_opts + more_opts
141 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
142 except FFmpegPostProcessorError as err:
143 raise AudioConversionError(err.msg)
145 def run(self, information):
146 path = information['filepath']
148 filecodec = self.get_audio_codec(path)
149 if filecodec is None:
150 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
153 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
154 if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
155 # Lossless, but in another container
158 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
159 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
160 # Lossless if possible
162 extension = filecodec
163 if filecodec == 'aac':
164 more_opts = ['-f', 'adts']
165 if filecodec == 'vorbis':
169 acodec = 'libmp3lame'
172 if self._preferredquality is not None:
173 if int(self._preferredquality) < 10:
174 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
176 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
178 # We convert the audio (lossy)
179 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
180 extension = self._preferredcodec
182 if self._preferredquality is not None:
183 # The opus codec doesn't support the -aq option
184 if int(self._preferredquality) < 10 and extension != 'opus':
185 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
187 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
188 if self._preferredcodec == 'aac':
189 more_opts += ['-f', 'adts']
190 if self._preferredcodec == 'm4a':
191 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
192 if self._preferredcodec == 'vorbis':
194 if self._preferredcodec == 'wav':
196 more_opts += ['-f', 'wav']
198 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
199 new_path = prefix + sep + extension
201 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
203 self._nopostoverwrites = True
206 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
207 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
209 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
210 self.run_ffmpeg(path, new_path, acodec, more_opts)
212 etype,e,tb = sys.exc_info()
213 if isinstance(e, AudioConversionError):
214 msg = u'audio conversion failed: ' + e.msg
216 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
217 raise PostProcessingError(msg)
219 # Try to update the date time for extracted audio file.
220 if information.get('filetime') is not None:
222 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
224 self._downloader.report_warning(u'Cannot update utime of audio file')
226 information['filepath'] = new_path
227 return self._nopostoverwrites,information
229 class FFmpegVideoConvertor(FFmpegPostProcessor):
230 def __init__(self, downloader=None,preferedformat=None):
231 super(FFmpegVideoConvertor, self).__init__(downloader)
232 self._preferedformat=preferedformat
234 def run(self, information):
235 path = information['filepath']
236 prefix, sep, ext = path.rpartition(u'.')
237 outpath = prefix + sep + self._preferedformat
238 if information['ext'] == self._preferedformat:
239 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
240 return True,information
241 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
242 self.run_ffmpeg(path, outpath, [])
243 information['filepath'] = outpath
244 information['format'] = self._preferedformat
245 information['ext'] = self._preferedformat
246 return False,information
249 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
250 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
438 def __init__(self, downloader=None, subtitlesformat='srt'):
439 super(FFmpegEmbedSubtitlePP, self).__init__(downloader)
440 self._subformat = subtitlesformat
443 def _conver_lang_code(cls, code):
444 """Convert language code from ISO 639-1 to ISO 639-2/T"""
445 return cls._lang_map.get(code[:2])
447 def run(self, information):
448 if information['ext'] != u'mp4':
449 self._downloader.to_screen(u'[ffmpeg] Subtitles can only be embedded in mp4 files')
450 return True, information
451 if not information.get('subtitles'):
452 self._downloader.to_screen(u'[ffmpeg] There aren\'t any subtitles to embed')
453 return True, information
455 sub_langs = [key for key in information['subtitles']]
456 filename = information['filepath']
457 input_files = [filename] + [subtitles_filename(filename, lang, self._subformat) for lang in sub_langs]
459 opts = ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy']
460 for (i, lang) in enumerate(sub_langs):
461 opts.extend(['-map', '%d:0' % (i+1), '-c:s:%d' % i, 'mov_text'])
462 lang_code = self._conver_lang_code(lang)
463 if lang_code is not None:
464 opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
465 opts.extend(['-f', 'mp4'])
467 temp_filename = filename + u'.temp'
468 self._downloader.to_screen(u'[ffmpeg] Embedding subtitles in \'%s\'' % filename)
469 self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
470 os.remove(encodeFilename(filename))
471 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
473 return True, information
476 class FFmpegMetadataPP(FFmpegPostProcessor):
479 if info.get('title') is not None:
480 metadata['title'] = info['title']
481 if info.get('upload_date') is not None:
482 metadata['date'] = info['upload_date']
483 if info.get('uploader') is not None:
484 metadata['artist'] = info['uploader']
485 elif info.get('uploader_id') is not None:
486 metadata['artist'] = info['uploader_id']
489 self._downloader.to_screen(u'[ffmpeg] There isn\'t any metadata to add')
492 filename = info['filepath']
493 ext = os.path.splitext(filename)[1][1:]
494 temp_filename = filename + u'.temp'
496 options = ['-c', 'copy']
497 for (name, value) in metadata.items():
498 options.extend(['-metadata', '%s="%s"' % (name, value)])
499 options.extend(['-f', ext])
501 self._downloader.to_screen(u'[ffmpeg] Adding metadata to \'%s\'' % filename)
502 self.run_ffmpeg(filename, temp_filename, options)
503 os.remove(encodeFilename(filename))
504 os.rename(encodeFilename(temp_filename), encodeFilename(filename))