Merge remote-tracking branch 'Huarong/master'
[youtube-dl] / youtube_dl / PostProcessor.py
1 import os
2 import subprocess
3 import sys
4 import time
5
6 from .utils import *
7
8
9 class PostProcessor(object):
10     """Post Processor class.
11
12     PostProcessor objects can be added to downloaders with their
13     add_post_processor() method. When the downloader has finished a
14     successful download, it will take its internal chain of PostProcessors
15     and start calling the run() method on each one of them, first with
16     an initial argument and then with the returned value of the previous
17     PostProcessor.
18
19     The chain will be stopped if one of them ever returns None or the end
20     of the chain is reached.
21
22     PostProcessor objects follow a "mutual registration" process similar
23     to InfoExtractor objects.
24     """
25
26     _downloader = None
27
28     def __init__(self, downloader=None):
29         self._downloader = downloader
30
31     def set_downloader(self, downloader):
32         """Sets the downloader for this PP."""
33         self._downloader = downloader
34
35     def run(self, information):
36         """Run the PostProcessor.
37
38         The "information" argument is a dictionary like the ones
39         composed by InfoExtractors. The only difference is that this
40         one has an extra field called "filepath" that points to the
41         downloaded file.
42
43         This method returns a tuple, the first element of which describes
44         whether the original file should be kept (i.e. not deleted - None for
45         no preference), and the second of which is the updated information.
46
47         In addition, this method may raise a PostProcessingError
48         exception if post processing fails.
49         """
50         return None, information # by default, keep file and do nothing
51
52 class FFmpegPostProcessorError(PostProcessingError):
53     pass
54
55 class AudioConversionError(PostProcessingError):
56     pass
57
58 class FFmpegPostProcessor(PostProcessor):
59     def __init__(self,downloader=None):
60         PostProcessor.__init__(self, downloader)
61         self._exes = self.detect_executables()
62
63     @staticmethod
64     def detect_executables():
65         def executable(exe):
66             try:
67                 subprocess.Popen([exe, '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
68             except OSError:
69                 return False
70             return exe
71         programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
72         return dict((program, executable(program)) for program in programs)
73
74     def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
75         if not self._exes['ffmpeg'] and not self._exes['avconv']:
76             raise FFmpegPostProcessorError(u'ffmpeg or avconv not found. Please install one.')
77
78         files_cmd = []
79         for path in input_paths:
80             files_cmd.extend(['-i', encodeFilename(path)])
81         cmd = ([self._exes['avconv'] or self._exes['ffmpeg'], '-y'] + files_cmd
82                + opts +
83                [encodeFilename(self._ffmpeg_filename_argument(out_path))])
84
85         p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
86         stdout,stderr = p.communicate()
87         if p.returncode != 0:
88             stderr = stderr.decode('utf-8', 'replace')
89             msg = stderr.strip().split('\n')[-1]
90             raise FFmpegPostProcessorError(msg)
91
92     def run_ffmpeg(self, path, out_path, opts):
93         self.run_ffmpeg_multiple_files([path], out_path, opts)
94
95     def _ffmpeg_filename_argument(self, fn):
96         # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
97         if fn.startswith(u'-'):
98             return u'./' + fn
99         return fn
100
101 class FFmpegExtractAudioPP(FFmpegPostProcessor):
102     def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
103         FFmpegPostProcessor.__init__(self, downloader)
104         if preferredcodec is None:
105             preferredcodec = 'best'
106         self._preferredcodec = preferredcodec
107         self._preferredquality = preferredquality
108         self._nopostoverwrites = nopostoverwrites
109
110     def get_audio_codec(self, path):
111         if not self._exes['ffprobe'] and not self._exes['avprobe']:
112             raise PostProcessingError(u'ffprobe or avprobe not found. Please install one.')
113         try:
114             cmd = [self._exes['avprobe'] or self._exes['ffprobe'], '-show_streams', encodeFilename(self._ffmpeg_filename_argument(path))]
115             handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE)
116             output = handle.communicate()[0]
117             if handle.wait() != 0:
118                 return None
119         except (IOError, OSError):
120             return None
121         audio_codec = None
122         for line in output.decode('ascii', 'ignore').split('\n'):
123             if line.startswith('codec_name='):
124                 audio_codec = line.split('=')[1].strip()
125             elif line.strip() == 'codec_type=audio' and audio_codec is not None:
126                 return audio_codec
127         return None
128
129     def run_ffmpeg(self, path, out_path, codec, more_opts):
130         if not self._exes['ffmpeg'] and not self._exes['avconv']:
131             raise AudioConversionError('ffmpeg or avconv not found. Please install one.')
132         if codec is None:
133             acodec_opts = []
134         else:
135             acodec_opts = ['-acodec', codec]
136         opts = ['-vn'] + acodec_opts + more_opts
137         try:
138             FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
139         except FFmpegPostProcessorError as err:
140             raise AudioConversionError(err.message)
141
142     def run(self, information):
143         path = information['filepath']
144
145         filecodec = self.get_audio_codec(path)
146         if filecodec is None:
147             raise PostProcessingError(u'WARNING: unable to obtain file audio codec with ffprobe')
148
149         more_opts = []
150         if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
151             if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
152                 # Lossless, but in another container
153                 acodec = 'copy'
154                 extension = 'm4a'
155                 more_opts = [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
156             elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']:
157                 # Lossless if possible
158                 acodec = 'copy'
159                 extension = filecodec
160                 if filecodec == 'aac':
161                     more_opts = ['-f', 'adts']
162                 if filecodec == 'vorbis':
163                     extension = 'ogg'
164             else:
165                 # MP3 otherwise.
166                 acodec = 'libmp3lame'
167                 extension = 'mp3'
168                 more_opts = []
169                 if self._preferredquality is not None:
170                     if int(self._preferredquality) < 10:
171                         more_opts += [self._exes['avconv'] and '-q:a' or '-aq', self._preferredquality]
172                     else:
173                         more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
174         else:
175             # We convert the audio (lossy)
176             acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec]
177             extension = self._preferredcodec
178             more_opts = []
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]
182                 else:
183                     more_opts += [self._exes['avconv'] and '-b:a' or '-ab', self._preferredquality + 'k']
184             if self._preferredcodec == 'aac':
185                 more_opts += ['-f', 'adts']
186             if self._preferredcodec == 'm4a':
187                 more_opts += [self._exes['avconv'] and '-bsf:a' or '-absf', 'aac_adtstoasc']
188             if self._preferredcodec == 'vorbis':
189                 extension = 'ogg'
190             if self._preferredcodec == 'wav':
191                 extension = 'wav'
192                 more_opts += ['-f', 'wav']
193
194         prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
195         new_path = prefix + sep + extension
196
197         # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
198         if new_path == path:
199             self._nopostoverwrites = True
200
201         try:
202             if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
203                 self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
204             else:
205                 self._downloader.to_screen(u'[' + (self._exes['avconv'] and 'avconv' or 'ffmpeg') + '] Destination: ' + new_path)
206                 self.run_ffmpeg(path, new_path, acodec, more_opts)
207         except:
208             etype,e,tb = sys.exc_info()
209             if isinstance(e, AudioConversionError):
210                 msg = u'audio conversion failed: ' + e.message
211             else:
212                 msg = u'error running ' + (self._exes['avconv'] and 'avconv' or 'ffmpeg')
213             raise PostProcessingError(msg)
214
215         # Try to update the date time for extracted audio file.
216         if information.get('filetime') is not None:
217             try:
218                 os.utime(encodeFilename(new_path), (time.time(), information['filetime']))
219             except:
220                 self._downloader.report_warning(u'Cannot update utime of audio file')
221
222         information['filepath'] = new_path
223         return self._nopostoverwrites,information
224
225 class FFmpegVideoConvertor(FFmpegPostProcessor):
226     def __init__(self, downloader=None,preferedformat=None):
227         super(FFmpegVideoConvertor, self).__init__(downloader)
228         self._preferedformat=preferedformat
229
230     def run(self, information):
231         path = information['filepath']
232         prefix, sep, ext = path.rpartition(u'.')
233         outpath = prefix + sep + self._preferedformat
234         if information['ext'] == self._preferedformat:
235             self._downloader.to_screen(u'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
236             return True,information
237         self._downloader.to_screen(u'['+'ffmpeg'+'] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) +outpath)
238         self.run_ffmpeg(path, outpath, [])
239         information['filepath'] = outpath
240         information['format'] = self._preferedformat
241         information['ext'] = self._preferedformat
242         return False,information
243
244
245 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
246     # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
247     _lang_map = {
248         'aa': 'aar',
249         'ab': 'abk',
250         'ae': 'ave',
251         'af': 'afr',
252         'ak': 'aka',
253         'am': 'amh',
254         'an': 'arg',
255         'ar': 'ara',
256         'as': 'asm',
257         'av': 'ava',
258         'ay': 'aym',
259         'az': 'aze',
260         'ba': 'bak',
261         'be': 'bel',
262         'bg': 'bul',
263         'bh': 'bih',
264         'bi': 'bis',
265         'bm': 'bam',
266         'bn': 'ben',
267         'bo': 'bod',
268         'br': 'bre',
269         'bs': 'bos',
270         'ca': 'cat',
271         'ce': 'che',
272         'ch': 'cha',
273         'co': 'cos',
274         'cr': 'cre',
275         'cs': 'ces',
276         'cu': 'chu',
277         'cv': 'chv',
278         'cy': 'cym',
279         'da': 'dan',
280         'de': 'deu',
281         'dv': 'div',
282         'dz': 'dzo',
283         'ee': 'ewe',
284         'el': 'ell',
285         'en': 'eng',
286         'eo': 'epo',
287         'es': 'spa',
288         'et': 'est',
289         'eu': 'eus',
290         'fa': 'fas',
291         'ff': 'ful',
292         'fi': 'fin',
293         'fj': 'fij',
294         'fo': 'fao',
295         'fr': 'fra',
296         'fy': 'fry',
297         'ga': 'gle',
298         'gd': 'gla',
299         'gl': 'glg',
300         'gn': 'grn',
301         'gu': 'guj',
302         'gv': 'glv',
303         'ha': 'hau',
304         'he': 'heb',
305         'hi': 'hin',
306         'ho': 'hmo',
307         'hr': 'hrv',
308         'ht': 'hat',
309         'hu': 'hun',
310         'hy': 'hye',
311         'hz': 'her',
312         'ia': 'ina',
313         'id': 'ind',
314         'ie': 'ile',
315         'ig': 'ibo',
316         'ii': 'iii',
317         'ik': 'ipk',
318         'io': 'ido',
319         'is': 'isl',
320         'it': 'ita',
321         'iu': 'iku',
322         'ja': 'jpn',
323         'jv': 'jav',
324         'ka': 'kat',
325         'kg': 'kon',
326         'ki': 'kik',
327         'kj': 'kua',
328         'kk': 'kaz',
329         'kl': 'kal',
330         'km': 'khm',
331         'kn': 'kan',
332         'ko': 'kor',
333         'kr': 'kau',
334         'ks': 'kas',
335         'ku': 'kur',
336         'kv': 'kom',
337         'kw': 'cor',
338         'ky': 'kir',
339         'la': 'lat',
340         'lb': 'ltz',
341         'lg': 'lug',
342         'li': 'lim',
343         'ln': 'lin',
344         'lo': 'lao',
345         'lt': 'lit',
346         'lu': 'lub',
347         'lv': 'lav',
348         'mg': 'mlg',
349         'mh': 'mah',
350         'mi': 'mri',
351         'mk': 'mkd',
352         'ml': 'mal',
353         'mn': 'mon',
354         'mr': 'mar',
355         'ms': 'msa',
356         'mt': 'mlt',
357         'my': 'mya',
358         'na': 'nau',
359         'nb': 'nob',
360         'nd': 'nde',
361         'ne': 'nep',
362         'ng': 'ndo',
363         'nl': 'nld',
364         'nn': 'nno',
365         'no': 'nor',
366         'nr': 'nbl',
367         'nv': 'nav',
368         'ny': 'nya',
369         'oc': 'oci',
370         'oj': 'oji',
371         'om': 'orm',
372         'or': 'ori',
373         'os': 'oss',
374         'pa': 'pan',
375         'pi': 'pli',
376         'pl': 'pol',
377         'ps': 'pus',
378         'pt': 'por',
379         'qu': 'que',
380         'rm': 'roh',
381         'rn': 'run',
382         'ro': 'ron',
383         'ru': 'rus',
384         'rw': 'kin',
385         'sa': 'san',
386         'sc': 'srd',
387         'sd': 'snd',
388         'se': 'sme',
389         'sg': 'sag',
390         'si': 'sin',
391         'sk': 'slk',
392         'sl': 'slv',
393         'sm': 'smo',
394         'sn': 'sna',
395         'so': 'som',
396         'sq': 'sqi',
397         'sr': 'srp',
398         'ss': 'ssw',
399         'st': 'sot',
400         'su': 'sun',
401         'sv': 'swe',
402         'sw': 'swa',
403         'ta': 'tam',
404         'te': 'tel',
405         'tg': 'tgk',
406         'th': 'tha',
407         'ti': 'tir',
408         'tk': 'tuk',
409         'tl': 'tgl',
410         'tn': 'tsn',
411         'to': 'ton',
412         'tr': 'tur',
413         'ts': 'tso',
414         'tt': 'tat',
415         'tw': 'twi',
416         'ty': 'tah',
417         'ug': 'uig',
418         'uk': 'ukr',
419         'ur': 'urd',
420         'uz': 'uzb',
421         've': 'ven',
422         'vi': 'vie',
423         'vo': 'vol',
424         'wa': 'wln',
425         'wo': 'wol',
426         'xh': 'xho',
427         'yi': 'yid',
428         'yo': 'yor',
429         'za': 'zha',
430         'zh': 'zho',
431         'zu': 'zul',
432     }
433
434     def __init__(self, downloader=None, subtitlesformat='srt'):
435         super(FFmpegEmbedSubtitlePP, self).__init__(downloader)
436         self._subformat = subtitlesformat
437
438     @classmethod
439     def _conver_lang_code(cls, code):
440         """Convert language code from ISO 639-1 to ISO 639-2/T"""
441         return cls._lang_map.get(code[:2])
442
443     def run(self, information):
444         if information['ext'] != u'mp4':
445             self._downloader.to_screen(u'[ffmpeg] Subtitles can only be embedded in mp4 files')
446             return True, information
447         sub_langs = [key for key in information['subtitles']]
448
449         filename = information['filepath']
450         input_files = [filename] + [subtitles_filename(filename, lang, self._subformat) for lang in sub_langs]
451
452         opts = ['-map', '0:0', '-map', '0:1', '-c:v', 'copy', '-c:a', 'copy']
453         for (i, lang) in enumerate(sub_langs):
454             opts.extend(['-map', '%d:0' % (i+1), '-c:s:%d' % i, 'mov_text'])
455             lang_code = self._conver_lang_code(lang)
456             if lang_code is not None:
457                 opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
458         opts.extend(['-f', 'mp4'])
459
460         temp_filename = filename + u'.temp'
461         self._downloader.to_screen(u'[ffmpeg] Embedding subtitles in \'%s\'' % filename)
462         self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
463         os.remove(encodeFilename(filename))
464         os.rename(encodeFilename(temp_filename), encodeFilename(filename))
465
466         return True, information