8 compat_subprocess_get_DEVNULL,
17 class PostProcessor(object):
18 """Post Processor class.
20 PostProcessor objects can be added to downloaders with their
21 add_post_processor() method. When the downloader has finished a
22 successful download, it will take its internal chain of PostProcessors
23 and start calling the run() method on each one of them, first with
24 an initial argument and then with the returned value of the previous
27 The chain will be stopped if one of them ever returns None or the end
28 of the chain is reached.
30 PostProcessor objects follow a "mutual registration" process similar
31 to InfoExtractor objects.
36 def __init__(self, downloader=None):
37 self._downloader = downloader
39 def set_downloader(self, downloader):
40 """Sets the downloader for this PP."""
41 self._downloader = downloader
43 def run(self, information):
44 """Run the PostProcessor.
46 The "information" argument is a dictionary like the ones
47 composed by InfoExtractors. The only difference is that this
48 one has an extra field called "filepath" that points to the
51 This method returns a tuple, the first element of which describes
52 whether the original file should be kept (i.e. not deleted - None for
53 no preference), and the second of which is the updated information.
55 In addition, this method may raise a PostProcessingError
56 exception if post processing fails.
58 return None, information # by default, keep file and do nothing
60 class FFmpegPostProcessorError(PostProcessingError):
63 class AudioConversionError(PostProcessingError):
66 class FFmpegPostProcessor(PostProcessor):
67 def __init__(self,downloader=None):
68 PostProcessor.__init__(self, downloader)
69 self._exes = self.detect_executables()
72 def detect_executables():
75 subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
79 programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
80 return dict((program, executable(program)) for program in programs)
82 def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
83 if not self._exes['ffmpeg'] and not self._exes['avconv']:
84 raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.')
87 for path in input_paths:
88 files_cmd.extend(['-i', encodeFilename(path, True)])
89 cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y'] + files_cmd
91 [encodeFilename(self._ffmpeg_filename_argument(out_path), True)])
93 if self._downloader.params.get('verbose', False):
94 self._downloader.to_screen(u'[debug] ffmpeg command line: %s' % shell_quote(cmd))
95 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
96 stdout,stderr = p.communicate()
98 stderr = stderr.decode('utf-8', 'replace')
99 msg = stderr.strip().split('\n')[-1]
100 raise FFmpegPostProcessorError(msg)
102 def run_ffmpeg(self, path, out_path, opts):
103 self.run_ffmpeg_multiple_files([path], out_path, opts)
105 def _ffmpeg_filename_argument(self, fn):
106 # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
107 if fn.startswith(u'-'):
111 class FFmpegExtractAudioPP(FFmpegPostProcessor):
112 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
113 FFmpegPostProcessor.__init__(self, downloader)
114 if preferredcodec is None:
115 preferredcodec = 'best'
116 self._preferredcodec = preferredcodec
117 self._preferredquality = preferredquality
118 self._nopostoverwrites = nopostoverwrites
120 def get_audio_codec(self, path):
121 if not self._exes['ffprobe'] and not self._exes['avprobe']:
122 raise PostProcessingError(u'ffprobe or avprobe not found. Please install one.')
125 self._exes['avprobe'] or self._exes['ffprobe'],
127 encodeFilename(self._ffmpeg_filename_argument(path), True)]
128 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
129 output = handle.communicate()[0]
130 if handle.wait() != 0:
132 except (IOError, OSError):
135 for line in output.decode('ascii', 'ignore').split('\n'):
136 if line.startswith('codec_name='):
137 audio_codec = line.split('=')[1].strip()
138 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
142 def run_ffmpeg(self, path, out_path, codec, more_opts):
143 if not self._exes['ffmpeg'] and not self._exes['avconv']:
144 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
148 acodec_opts = ['-acodec', codec]
149 opts = ['-vn'] + acodec_opts + more_opts
151 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
152 except FFmpegPostProcessorError as err:
153 raise AudioConversionError(err.msg)
155 def run(self, information):
156 path = information['filepath']
158 filecodec = self.get_audio_codec(path)
159 if filecodec is None:
160 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
163 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
164 if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
165 # Lossless, but in another container
168 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
169 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
170 # Lossless if possible
172 extension = filecodec
173 if filecodec == 'aac':
174 more_opts = ['-f', 'adts']
175 if filecodec == 'vorbis':
179 acodec = 'libmp3lame'
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']
188 # We convert the audio (lossy)
189 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
190 extension = self._preferredcodec
192 if self._preferredquality is not None:
193 # The opus codec doesn't support the -aq option
194 if int(self._preferredquality) < 10 and extension != 'opus':
195 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
197 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
198 if self._preferredcodec == 'aac':
199 more_opts += ['-f', 'adts']
200 if self._preferredcodec == 'm4a':
201 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
202 if self._preferredcodec == 'vorbis':
204 if self._preferredcodec == 'wav':
206 more_opts += ['-f', 'wav']
208 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
209 new_path = prefix + sep + extension
211 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
213 self._nopostoverwrites = True
216 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
217 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
219 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
220 self.run_ffmpeg(path, new_path, acodec, more_opts)
222 etype,e,tb = sys.exc_info()
223 if isinstance(e, AudioConversionError):
224 msg = u'audio conversion failed: ' + e.msg
226 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
227 raise PostProcessingError(msg)
229 # Try to update the date time for extracted audio file.
230 if information.get('filetime') is not None:
232 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
234 self._downloader.report_warning(u'Cannot update utime of audio file')
236 information['filepath'] = new_path
237 return self._nopostoverwrites,information
239 class FFmpegVideoConvertor(FFmpegPostProcessor):
240 def __init__(self, downloader=None,preferedformat=None):
241 super(FFmpegVideoConvertor, self).__init__(downloader)
242 self._preferedformat=preferedformat
244 def run(self, information):
245 path = information['filepath']
246 prefix, sep, ext = path.rpartition(u'.')
247 outpath = prefix + sep + self._preferedformat
248 if information['ext'] == self._preferedformat:
249 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
250 return True,information
251 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
252 self.run_ffmpeg(path, outpath, [])
253 information['filepath'] = outpath
254 information['format'] = self._preferedformat
255 information['ext'] = self._preferedformat
256 return False,information
259 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
260 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
448 def __init__(self, downloader=None, subtitlesformat='srt'):
449 super(FFmpegEmbedSubtitlePP, self).__init__(downloader)
450 self._subformat = subtitlesformat
453 def _conver_lang_code(cls, code):
454 """Convert language code from ISO 639-1 to ISO 639-2/T"""
455 return cls._lang_map.get(code[:2])
457 def run(self, information):
458 if information['ext'] != u'mp4':
459 self._downloader.to_screen(u'[ffmpeg] Subtitles can only be embedded in mp4 files')
460 return True, information
461 if not information.get('subtitles'):
462 self._downloader.to_screen(u'[ffmpeg] There aren\'t any subtitles to embed')
463 return True, information
465 sub_langs = [key for key in information['subtitles']]
466 filename = information['filepath']
467 input_files = [filename] + [subtitles_filename(filename, lang, self._subformat) for lang in sub_langs]
469 opts = ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy']
470 for (i, lang) in enumerate(sub_langs):
471 opts.extend(['-map', '%d:0' % (i+1), '-c:s:%d' % i, 'mov_text'])
472 lang_code = self._conver_lang_code(lang)
473 if lang_code is not None:
474 opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
475 opts.extend(['-f', 'mp4'])
477 temp_filename = filename + u'.temp'
478 self._downloader.to_screen(u'[ffmpeg] Embedding subtitles in \'%s\'' % filename)
479 self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
480 os.remove(encodeFilename(filename))
481 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
483 return True, information
486 class FFmpegMetadataPP(FFmpegPostProcessor):
489 if info.get('title') is not None:
490 metadata['title'] = info['title']
491 if info.get('upload_date') is not None:
492 metadata['date'] = info['upload_date']
493 if info.get('uploader') is not None:
494 metadata['artist'] = info['uploader']
495 elif info.get('uploader_id') is not None:
496 metadata['artist'] = info['uploader_id']
499 self._downloader.to_screen(u'[ffmpeg] There isn\'t any metadata to add')
502 filename = info['filepath']
503 temp_filename = prepend_extension(filename, 'temp')
505 options = ['-c', 'copy']
506 for (name, value) in metadata.items():
507 options.extend(['-metadata', '%s=%s' % (name, value)])
509 self._downloader.to_screen(u'[ffmpeg] Adding metadata to \'%s\'' % filename)
510 self.run_ffmpeg(filename, temp_filename, options)
511 os.remove(encodeFilename(filename))
512 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
516 class FFmpegMergerPP(FFmpegPostProcessor):
518 filename = info['filepath']
519 args = ['-c', 'copy']
520 self.run_ffmpeg_multiple_files(info['__files_to_merge'], filename, args)