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 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
87 stdout,stderr = p.communicate()
89 stderr = stderr.decode('utf-8', 'replace')
90 msg = stderr.strip().split('\n')[-1]
91 raise FFmpegPostProcessorError(msg)
93 def run_ffmpeg(self, path, out_path, opts):
94 self.run_ffmpeg_multiple_files([path], out_path, opts)
96 def _ffmpeg_filename_argument(self, fn):
97 # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
98 if fn.startswith(u'-'):
102 class FFmpegExtractAudioPP(FFmpegPostProcessor):
103 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
104 FFmpegPostProcessor.__init__(self, downloader)
105 if preferredcodec is None:
106 preferredcodec = 'best'
107 self._preferredcodec = preferredcodec
108 self._preferredquality = preferredquality
109 self._nopostoverwrites = nopostoverwrites
111 def get_audio_codec(self, path):
112 if not self._exes['ffprobe'] and not self._exes['avprobe']:
113 raise PostProcessingError(u'ffprobe or avprobe not found. Please install one.')
115 cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
116 handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
117 output = handle.communicate()[0]
118 if handle.wait() != 0:
120 except (IOError, OSError):
123 for line in output.decode('ascii', 'ignore').split('\n'):
124 if line.startswith('codec_name='):
125 audio_codec = line.split('=')[1].strip()
126 elif line.strip() == 'codec_type=audio' and audio_codec is not None:
130 def run_ffmpeg(self, path, out_path, codec, more_opts):
131 if not self._exes['ffmpeg'] and not self._exes['avconv']:
132 raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
136 acodec_opts = ['-acodec', codec]
137 opts = ['-vn'] + acodec_opts + more_opts
139 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
140 except FFmpegPostProcessorError as err:
141 raise AudioConversionError(err.msg)
143 def run(self, information):
144 path = information['filepath']
146 filecodec = self.get_audio_codec(path)
147 if filecodec is None:
148 raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
151 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
152 if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
153 # Lossless, but in another container
156 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
157 elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
158 # Lossless if possible
160 extension = filecodec
161 if filecodec == 'aac':
162 more_opts = ['-f', 'adts']
163 if filecodec == 'vorbis':
167 acodec = 'libmp3lame'
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']
176 # We convert the audio (lossy)
177 acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
178 extension = self._preferredcodec
180 if self._preferredquality is not None:
181 if int(self._preferredquality) < 10:
182 more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
184 more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
185 if self._preferredcodec == 'aac':
186 more_opts += ['-f', 'adts']
187 if self._preferredcodec == 'm4a':
188 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
189 if self._preferredcodec == 'vorbis':
191 if self._preferredcodec == 'wav':
193 more_opts += ['-f', 'wav']
195 prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
196 new_path = prefix + sep + extension
198 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
200 self._nopostoverwrites = True
203 if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
204 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
206 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
207 self.run_ffmpeg(path, new_path, acodec, more_opts)
209 etype,e,tb = sys.exc_info()
210 if isinstance(e, AudioConversionError):
211 msg = u'audio conversion failed: ' + e.msg
213 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
214 raise PostProcessingError(msg)
216 # Try to update the date time for extracted audio file.
217 if information.get('filetime') is not None:
219 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
221 self._downloader.report_warning(u'Cannot update utime of audio file')
223 information['filepath'] = new_path
224 return self._nopostoverwrites,information
226 class FFmpegVideoConvertor(FFmpegPostProcessor):
227 def __init__(self, downloader=None,preferedformat=None):
228 super(FFmpegVideoConvertor, self).__init__(downloader)
229 self._preferedformat=preferedformat
231 def run(self, information):
232 path = information['filepath']
233 prefix, sep, ext = path.rpartition(u'.')
234 outpath = prefix + sep + self._preferedformat
235 if information['ext'] == self._preferedformat:
236 self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
237 return True,information
238 self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
239 self.run_ffmpeg(path, outpath, [])
240 information['filepath'] = outpath
241 information['format'] = self._preferedformat
242 information['ext'] = self._preferedformat
243 return False,information
246 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
247 # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
435 def __init__(self, downloader=None, subtitlesformat='srt'):
436 super(FFmpegEmbedSubtitlePP, self).__init__(downloader)
437 self._subformat = subtitlesformat
440 def _conver_lang_code(cls, code):
441 """Convert language code from ISO 639-1 to ISO 639-2/T"""
442 return cls._lang_map.get(code[:2])
444 def run(self, information):
445 if information['ext'] != u'mp4':
446 self._downloader.to_screen(u'[ffmpeg] Subtitles can only be embedded in mp4 files')
447 return True, information
448 if not information.get('subtitles'):
449 self._downloader.to_screen(u'[ffmpeg] There aren\'t any subtitles to embed')
450 return True, information
452 sub_langs = [key for key in information['subtitles']]
453 filename = information['filepath']
454 input_files = [filename] + [subtitles_filename(filename, lang, self._subformat) for lang in sub_langs]
456 opts = ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy']
457 for (i, lang) in enumerate(sub_langs):
458 opts.extend(['-map', '%d:0' % (i+1), '-c:s:%d' % i, 'mov_text'])
459 lang_code = self._conver_lang_code(lang)
460 if lang_code is not None:
461 opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
462 opts.extend(['-f', 'mp4'])
464 temp_filename = filename + u'.temp'
465 self._downloader.to_screen(u'[ffmpeg] Embedding subtitles in \'%s\'' % filename)
466 self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
467 os.remove(encodeFilename(filename))
468 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
470 return True, information
473 class FFmpegMetadataPP(FFmpegPostProcessor):
476 if info.get('title') is not None:
477 metadata['title'] = info['title']
478 if info.get('upload_date') is not None:
479 metadata['date'] = info['upload_date']
480 if info.get('uploader') is not None:
481 metadata['artist'] = info['uploader']
482 elif info.get('uploader_id') is not None:
483 metadata['artist'] = info['uploader_id']
486 self._downloader.to_screen(u'[ffmpeg] There isn\'t any metadata to add')
489 filename = info['filepath']
490 ext = os.path.splitext(filename)[1][1:]
491 temp_filename = filename + u'.temp'
493 options = ['-c', 'copy']
494 for (name, value) in metadata.items():
495 options.extend(['-metadata', '%s="%s"' % (name, value)])
496 options.extend(['-f', ext])
498 self._downloader.to_screen(u'[ffmpeg] Adding metadata to \'%s\'' % filename)
499 self.run_ffmpeg(filename, temp_filename, options)
500 os.remove(encodeFilename(filename))
501 os.rename(encodeFilename(temp_filename), encodeFilename(filename))