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)])
89 cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y'] + files_cmd
91 [encodeFilename(self._ffmpeg_filename_argument(out_path))])
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.')
124 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
125 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
126 output = handle.communicate()[0]
127 if handle.wait() != 0:
129 except (IOError, OSError):
132 for line in output.decode('ascii', 'ignore').split('\n'):
133 if line.startswith('codec_name='):
134 audio_codec = line.split('=')[1].strip()
135 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
139 def run_ffmpeg(self, path, out_path, codec, more_opts):
140 if not self._exes['ffmpeg'] and not self._exes['avconv']:
141 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
145 acodec_opts = ['-acodec', codec]
146 opts = ['-vn'] + acodec_opts + more_opts
148 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
149 except FFmpegPostProcessorError as err:
150 raise AudioConversionError(err.msg)
152 def run(self, information):
153 path = information['filepath']
155 filecodec = self.get_audio_codec(path)
156 if filecodec is None:
157 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
160 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
161 if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
162 # Lossless, but in another container
165 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
166 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
167 # Lossless if possible
169 extension = filecodec
170 if filecodec == 'aac':
171 more_opts = ['-f', 'adts']
172 if filecodec == 'vorbis':
176 acodec = 'libmp3lame'
179 if self._preferredquality is not None:
180 if int(self._preferredquality) < 10:
181 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
183 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
185 # We convert the audio (lossy)
186 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
187 extension = self._preferredcodec
189 if self._preferredquality is not None:
190 # The opus codec doesn't support the -aq option
191 if int(self._preferredquality) < 10 and extension != 'opus':
192 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
194 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
195 if self._preferredcodec == 'aac':
196 more_opts += ['-f', 'adts']
197 if self._preferredcodec == 'm4a':
198 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
199 if self._preferredcodec == 'vorbis':
201 if self._preferredcodec == 'wav':
203 more_opts += ['-f', 'wav']
205 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
206 new_path = prefix + sep + extension
208 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
210 self._nopostoverwrites = True
213 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
214 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
216 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
217 self.run_ffmpeg(path, new_path, acodec, more_opts)
219 etype,e,tb = sys.exc_info()
220 if isinstance(e, AudioConversionError):
221 msg = u'audio conversion failed: ' + e.msg
223 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
224 raise PostProcessingError(msg)
226 # Try to update the date time for extracted audio file.
227 if information.get('filetime') is not None:
229 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
231 self._downloader.report_warning(u'Cannot update utime of audio file')
233 information['filepath'] = new_path
234 return self._nopostoverwrites,information
236 class FFmpegVideoConvertor(FFmpegPostProcessor):
237 def __init__(self, downloader=None,preferedformat=None):
238 super(FFmpegVideoConvertor, self).__init__(downloader)
239 self._preferedformat=preferedformat
241 def run(self, information):
242 path = information['filepath']
243 prefix, sep, ext = path.rpartition(u'.')
244 outpath = prefix + sep + self._preferedformat
245 if information['ext'] == self._preferedformat:
246 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
247 return True,information
248 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
249 self.run_ffmpeg(path, outpath, [])
250 information['filepath'] = outpath
251 information['format'] = self._preferedformat
252 information['ext'] = self._preferedformat
253 return False,information
256 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
257 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
445 def __init__(self, downloader=None, subtitlesformat='srt'):
446 super(FFmpegEmbedSubtitlePP, self).__init__(downloader)
447 self._subformat = subtitlesformat
450 def _conver_lang_code(cls, code):
451 """Convert language code from ISO 639-1 to ISO 639-2/T"""
452 return cls._lang_map.get(code[:2])
454 def run(self, information):
455 if information['ext'] != u'mp4':
456 self._downloader.to_screen(u'[ffmpeg] Subtitles can only be embedded in mp4 files')
457 return True, information
458 if not information.get('subtitles'):
459 self._downloader.to_screen(u'[ffmpeg] There aren\'t any subtitles to embed')
460 return True, information
462 sub_langs = [key for key in information['subtitles']]
463 filename = information['filepath']
464 input_files = [filename] + [subtitles_filename(filename, lang, self._subformat) for lang in sub_langs]
466 opts = ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy']
467 for (i, lang) in enumerate(sub_langs):
468 opts.extend(['-map', '%d:0' % (i+1), '-c:s:%d' % i, 'mov_text'])
469 lang_code = self._conver_lang_code(lang)
470 if lang_code is not None:
471 opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
472 opts.extend(['-f', 'mp4'])
474 temp_filename = filename + u'.temp'
475 self._downloader.to_screen(u'[ffmpeg] Embedding subtitles in \'%s\'' % filename)
476 self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
477 os.remove(encodeFilename(filename))
478 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
480 return True, information
483 class FFmpegMetadataPP(FFmpegPostProcessor):
486 if info.get('title') is not None:
487 metadata['title'] = info['title']
488 if info.get('upload_date') is not None:
489 metadata['date'] = info['upload_date']
490 if info.get('uploader') is not None:
491 metadata['artist'] = info['uploader']
492 elif info.get('uploader_id') is not None:
493 metadata['artist'] = info['uploader_id']
496 self._downloader.to_screen(u'[ffmpeg] There isn\'t any metadata to add')
499 filename = info['filepath']
500 temp_filename = prepend_extension(filename, 'temp')
502 options = ['-c', 'copy']
503 for (name, value) in metadata.items():
504 options.extend(['-metadata', '%s=%s' % (name, value)])
506 self._downloader.to_screen(u'[ffmpeg] Adding metadata to \'%s\'' % filename)
507 self.run_ffmpeg(filename, temp_filename, options)
508 os.remove(encodeFilename(filename))
509 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
513 class FFmpegMergerPP(FFmpegPostProcessor):
515 filename = info['filepath']
516 args = ['-c', 'copy']
517 self.run_ffmpeg_multiple_files(info['__files_to_merge'], filename, args)