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 if int(self._preferredquality) < 10:
184 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
186 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
187 if self._preferredcodec == 'aac':
188 more_opts += ['-f', 'adts']
189 if self._preferredcodec == 'm4a':
190 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
191 if self._preferredcodec == 'vorbis':
193 if self._preferredcodec == 'wav':
195 more_opts += ['-f', 'wav']
197 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
198 new_path = prefix + sep + extension
200 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
202 self._nopostoverwrites = True
205 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
206 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
208 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
209 self.run_ffmpeg(path, new_path, acodec, more_opts)
211 etype,e,tb = sys.exc_info()
212 if isinstance(e, AudioConversionError):
213 msg = u'audio conversion failed: ' + e.msg
215 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
216 raise PostProcessingError(msg)
218 # Try to update the date time for extracted audio file.
219 if information.get('filetime') is not None:
221 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
223 self._downloader.report_warning(u'Cannot update utime of audio file')
225 information['filepath'] = new_path
226 return self._nopostoverwrites,information
228 class FFmpegVideoConvertor(FFmpegPostProcessor):
229 def __init__(self, downloader=None,preferedformat=None):
230 super(FFmpegVideoConvertor, self).__init__(downloader)
231 self._preferedformat=preferedformat
233 def run(self, information):
234 path = information['filepath']
235 prefix, sep, ext = path.rpartition(u'.')
236 outpath = prefix + sep + self._preferedformat
237 if information['ext'] == self._preferedformat:
238 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
239 return True,information
240 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
241 self.run_ffmpeg(path, outpath, [])
242 information['filepath'] = outpath
243 information['format'] = self._preferedformat
244 information['ext'] = self._preferedformat
245 return False,information
248 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
249 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
437 def __init__(self, downloader=None, subtitlesformat='srt'):
438 super(FFmpegEmbedSubtitlePP, self).__init__(downloader)
439 self._subformat = subtitlesformat
442 def _conver_lang_code(cls, code):
443 """Convert language code from ISO 639-1 to ISO 639-2/T"""
444 return cls._lang_map.get(code[:2])
446 def run(self, information):
447 if information['ext'] != u'mp4':
448 self._downloader.to_screen(u'[ffmpeg] Subtitles can only be embedded in mp4 files')
449 return True, information
450 if not information.get('subtitles'):
451 self._downloader.to_screen(u'[ffmpeg] There aren\'t any subtitles to embed')
452 return True, information
454 sub_langs = [key for key in information['subtitles']]
455 filename = information['filepath']
456 input_files = [filename] + [subtitles_filename(filename, lang, self._subformat) for lang in sub_langs]
458 opts = ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy']
459 for (i, lang) in enumerate(sub_langs):
460 opts.extend(['-map', '%d:0' % (i+1), '-c:s:%d' % i, 'mov_text'])
461 lang_code = self._conver_lang_code(lang)
462 if lang_code is not None:
463 opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
464 opts.extend(['-f', 'mp4'])
466 temp_filename = filename + u'.temp'
467 self._downloader.to_screen(u'[ffmpeg] Embedding subtitles in \'%s\'' % filename)
468 self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
469 os.remove(encodeFilename(filename))
470 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
472 return True, information
475 class FFmpegMetadataPP(FFmpegPostProcessor):
478 if info.get('title') is not None:
479 metadata['title'] = info['title']
480 if info.get('upload_date') is not None:
481 metadata['date'] = info['upload_date']
482 if info.get('uploader') is not None:
483 metadata['artist'] = info['uploader']
484 elif info.get('uploader_id') is not None:
485 metadata['artist'] = info['uploader_id']
488 self._downloader.to_screen(u'[ffmpeg] There isn\'t any metadata to add')
491 filename = info['filepath']
492 ext = os.path.splitext(filename)[1][1:]
493 temp_filename = filename + u'.temp'
495 options = ['-c', 'copy']
496 for (name, value) in metadata.items():
497 options.extend(['-metadata', '%s="%s"' % (name, value)])
498 options.extend(['-f', ext])
500 self._downloader.to_screen(u'[ffmpeg] Adding metadata to \'%s\'' % filename)
501 self.run_ffmpeg(filename, temp_filename, options)
502 os.remove(encodeFilename(filename))
503 os.rename(encodeFilename(temp_filename), encodeFilename(filename))