]>
Commit | Line | Data |
---|---|---|
3aa578ca PH |
1 | from __future__ import unicode_literals |
2 | ||
e9fade72 | 3 | import io |
496c1923 PH |
4 | import os |
5 | import subprocess | |
496c1923 PH |
6 | import time |
7 | ||
8 | ||
9 | from .common import AudioConversionError, PostProcessor | |
10 | ||
8c25f81b | 11 | from ..compat import ( |
496c1923 | 12 | compat_subprocess_get_DEVNULL, |
8c25f81b PH |
13 | ) |
14 | from ..utils import ( | |
f07b74fc | 15 | encodeArgument, |
496c1923 | 16 | encodeFilename, |
95807118 | 17 | get_exe_version, |
48844745 | 18 | is_outdated_version, |
496c1923 PH |
19 | PostProcessingError, |
20 | prepend_extension, | |
21 | shell_quote, | |
22 | subtitles_filename, | |
bf6427d2 | 23 | dfxp2srt, |
496c1923 PH |
24 | ) |
25 | ||
26 | ||
496c1923 PH |
27 | class FFmpegPostProcessorError(PostProcessingError): |
28 | pass | |
29 | ||
d799b47b | 30 | |
496c1923 | 31 | class FFmpegPostProcessor(PostProcessor): |
1866432d AH |
32 | def __init__(self, downloader=None, extra_cmd_args=None): |
33 | PostProcessor.__init__(self, downloader, extra_cmd_args) | |
73fac4e9 | 34 | self._determine_executables() |
496c1923 | 35 | |
48844745 | 36 | def check_version(self): |
f740fae2 | 37 | if not self.available: |
3aa578ca | 38 | raise FFmpegPostProcessorError('ffmpeg or avconv not found. Please install one.') |
48844745 | 39 | |
65bf37ef | 40 | required_version = '10-0' if self.basename == 'avconv' else '1.0' |
48844745 | 41 | if is_outdated_version( |
73fac4e9 | 42 | self._versions[self.basename], required_version): |
3aa578ca | 43 | warning = 'Your copy of %s is outdated, update %s to version %s or newer if you encounter any errors.' % ( |
73fac4e9 | 44 | self.basename, self.basename, required_version) |
6194bb14 PH |
45 | if self._downloader: |
46 | self._downloader.report_warning(warning) | |
48844745 | 47 | |
496c1923 | 48 | @staticmethod |
73fac4e9 PH |
49 | def get_versions(downloader=None): |
50 | return FFmpegPostProcessor(downloader)._versions | |
6271f1ca | 51 | |
73fac4e9 PH |
52 | def _determine_executables(self): |
53 | programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] | |
54 | prefer_ffmpeg = self._downloader.params.get('prefer_ffmpeg', False) | |
55 | ||
56 | self.basename = None | |
57 | self.probe_basename = None | |
58 | ||
59 | self._paths = None | |
60 | self._versions = None | |
61 | if self._downloader: | |
62 | location = self._downloader.params.get('ffmpeg_location') | |
63 | if location is not None: | |
64 | if not os.path.exists(location): | |
65 | self._downloader.report_warning( | |
66 | 'ffmpeg-location %s does not exist! ' | |
67 | 'Continuing without avconv/ffmpeg.' % (location)) | |
68 | self._versions = {} | |
69 | return | |
70 | elif not os.path.isdir(location): | |
71 | basename = os.path.splitext(os.path.basename(location))[0] | |
72 | if basename not in programs: | |
73 | self._downloader.report_warning( | |
74 | 'Cannot identify executable %s, its basename should be one of %s. ' | |
75 | 'Continuing without avconv/ffmpeg.' % | |
76 | (location, ', '.join(programs))) | |
77 | self._versions = {} | |
78 | return None | |
79 | location = os.path.dirname(os.path.abspath(location)) | |
80 | if basename in ('ffmpeg', 'ffprobe'): | |
81 | prefer_ffmpeg = True | |
82 | ||
83 | self._paths = dict( | |
84 | (p, os.path.join(location, p)) for p in programs) | |
85 | self._versions = dict( | |
86 | (p, get_exe_version(self._paths[p], args=['-version'])) | |
87 | for p in programs) | |
88 | if self._versions is None: | |
89 | self._versions = dict( | |
90 | (p, get_exe_version(p, args=['-version'])) for p in programs) | |
91 | self._paths = dict((p, p) for p in programs) | |
92 | ||
93 | if prefer_ffmpeg: | |
d28b5171 | 94 | prefs = ('ffmpeg', 'avconv') |
76b1bd67 | 95 | else: |
d28b5171 PH |
96 | prefs = ('avconv', 'ffmpeg') |
97 | for p in prefs: | |
98 | if self._versions[p]: | |
73fac4e9 PH |
99 | self.basename = p |
100 | break | |
76b1bd67 | 101 | |
73fac4e9 | 102 | if prefer_ffmpeg: |
50b51830 | 103 | prefs = ('ffprobe', 'avprobe') |
1a253e13 PH |
104 | else: |
105 | prefs = ('avprobe', 'ffprobe') | |
106 | for p in prefs: | |
107 | if self._versions[p]: | |
73fac4e9 PH |
108 | self.probe_basename = p |
109 | break | |
110 | ||
f740fae2 | 111 | @property |
73fac4e9 PH |
112 | def available(self): |
113 | return self.basename is not None | |
1a253e13 | 114 | |
73fac4e9 PH |
115 | @property |
116 | def executable(self): | |
117 | return self._paths[self.basename] | |
118 | ||
3da4b313 JMF |
119 | @property |
120 | def probe_available(self): | |
121 | return self.probe_basename is not None | |
122 | ||
73fac4e9 PH |
123 | @property |
124 | def probe_executable(self): | |
125 | return self._paths[self.probe_basename] | |
76b1bd67 | 126 | |
496c1923 | 127 | def run_ffmpeg_multiple_files(self, input_paths, out_path, opts): |
48844745 | 128 | self.check_version() |
496c1923 | 129 | |
52afb2ac PH |
130 | oldest_mtime = min( |
131 | os.stat(encodeFilename(path)).st_mtime for path in input_paths) | |
43bc8890 | 132 | |
496c1923 PH |
133 | files_cmd = [] |
134 | for path in input_paths: | |
b0e87c31 | 135 | files_cmd.extend([encodeArgument('-i'), encodeFilename(path, True)]) |
73fac4e9 | 136 | cmd = ([encodeFilename(self.executable, True), encodeArgument('-y')] + |
43bc8890 PH |
137 | files_cmd + |
138 | [encodeArgument(o) for o in opts] + | |
496c1923 PH |
139 | [encodeFilename(self._ffmpeg_filename_argument(out_path), True)]) |
140 | ||
141 | if self._downloader.params.get('verbose', False): | |
3aa578ca | 142 | self._downloader.to_screen('[debug] ffmpeg command line: %s' % shell_quote(cmd)) |
cffcbc02 | 143 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) |
62fec3b2 | 144 | stdout, stderr = p.communicate() |
496c1923 PH |
145 | if p.returncode != 0: |
146 | stderr = stderr.decode('utf-8', 'replace') | |
147 | msg = stderr.strip().split('\n')[-1] | |
148 | raise FFmpegPostProcessorError(msg) | |
dd29eb7f | 149 | self.try_utime(out_path, oldest_mtime, oldest_mtime) |
cc55d088 | 150 | |
496c1923 PH |
151 | def run_ffmpeg(self, path, out_path, opts): |
152 | self.run_ffmpeg_multiple_files([path], out_path, opts) | |
153 | ||
154 | def _ffmpeg_filename_argument(self, fn): | |
155 | # ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details | |
3aa578ca PH |
156 | if fn.startswith('-'): |
157 | return './' + fn | |
496c1923 PH |
158 | return fn |
159 | ||
160 | ||
161 | class FFmpegExtractAudioPP(FFmpegPostProcessor): | |
162 | def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False): | |
163 | FFmpegPostProcessor.__init__(self, downloader) | |
164 | if preferredcodec is None: | |
165 | preferredcodec = 'best' | |
166 | self._preferredcodec = preferredcodec | |
167 | self._preferredquality = preferredquality | |
168 | self._nopostoverwrites = nopostoverwrites | |
169 | ||
170 | def get_audio_codec(self, path): | |
1a253e13 | 171 | |
3da4b313 | 172 | if not self.probe_available: |
3aa578ca | 173 | raise PostProcessingError('ffprobe or avprobe not found. Please install one.') |
496c1923 PH |
174 | try: |
175 | cmd = [ | |
73fac4e9 | 176 | encodeFilename(self.probe_executable, True), |
b0e87c31 | 177 | encodeArgument('-show_streams'), |
496c1923 | 178 | encodeFilename(self._ffmpeg_filename_argument(path), True)] |
73fac4e9 | 179 | if self._downloader.params.get('verbose', False): |
5bfd430f | 180 | self._downloader.to_screen('[debug] %s command line: %s' % (self.basename, shell_quote(cmd))) |
cffcbc02 | 181 | handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE, stdin=subprocess.PIPE) |
496c1923 PH |
182 | output = handle.communicate()[0] |
183 | if handle.wait() != 0: | |
184 | return None | |
185 | except (IOError, OSError): | |
186 | return None | |
187 | audio_codec = None | |
188 | for line in output.decode('ascii', 'ignore').split('\n'): | |
189 | if line.startswith('codec_name='): | |
190 | audio_codec = line.split('=')[1].strip() | |
191 | elif line.strip() == 'codec_type=audio' and audio_codec is not None: | |
192 | return audio_codec | |
193 | return None | |
194 | ||
195 | def run_ffmpeg(self, path, out_path, codec, more_opts): | |
496c1923 PH |
196 | if codec is None: |
197 | acodec_opts = [] | |
198 | else: | |
199 | acodec_opts = ['-acodec', codec] | |
200 | opts = ['-vn'] + acodec_opts + more_opts | |
201 | try: | |
202 | FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts) | |
203 | except FFmpegPostProcessorError as err: | |
204 | raise AudioConversionError(err.msg) | |
205 | ||
206 | def run(self, information): | |
207 | path = information['filepath'] | |
208 | ||
209 | filecodec = self.get_audio_codec(path) | |
210 | if filecodec is None: | |
3aa578ca | 211 | raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe') |
496c1923 PH |
212 | |
213 | more_opts = [] | |
214 | if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'): | |
215 | if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']: | |
216 | # Lossless, but in another container | |
217 | acodec = 'copy' | |
218 | extension = 'm4a' | |
467d3c9a | 219 | more_opts = ['-bsf:a', 'aac_adtstoasc'] |
496c1923 PH |
220 | elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']: |
221 | # Lossless if possible | |
222 | acodec = 'copy' | |
223 | extension = filecodec | |
224 | if filecodec == 'aac': | |
225 | more_opts = ['-f', 'adts'] | |
226 | if filecodec == 'vorbis': | |
227 | extension = 'ogg' | |
228 | else: | |
229 | # MP3 otherwise. | |
230 | acodec = 'libmp3lame' | |
231 | extension = 'mp3' | |
232 | more_opts = [] | |
233 | if self._preferredquality is not None: | |
234 | if int(self._preferredquality) < 10: | |
467d3c9a | 235 | more_opts += ['-q:a', self._preferredquality] |
496c1923 | 236 | else: |
467d3c9a | 237 | more_opts += ['-b:a', self._preferredquality + 'k'] |
496c1923 PH |
238 | else: |
239 | # We convert the audio (lossy) | |
240 | acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec] | |
241 | extension = self._preferredcodec | |
242 | more_opts = [] | |
243 | if self._preferredquality is not None: | |
244 | # The opus codec doesn't support the -aq option | |
245 | if int(self._preferredquality) < 10 and extension != 'opus': | |
467d3c9a | 246 | more_opts += ['-q:a', self._preferredquality] |
496c1923 | 247 | else: |
467d3c9a | 248 | more_opts += ['-b:a', self._preferredquality + 'k'] |
496c1923 PH |
249 | if self._preferredcodec == 'aac': |
250 | more_opts += ['-f', 'adts'] | |
251 | if self._preferredcodec == 'm4a': | |
467d3c9a | 252 | more_opts += ['-bsf:a', 'aac_adtstoasc'] |
496c1923 PH |
253 | if self._preferredcodec == 'vorbis': |
254 | extension = 'ogg' | |
255 | if self._preferredcodec == 'wav': | |
256 | extension = 'wav' | |
257 | more_opts += ['-f', 'wav'] | |
258 | ||
3aa578ca | 259 | prefix, sep, ext = path.rpartition('.') # not os.path.splitext, since the latter does not work on unicode in all setups |
496c1923 PH |
260 | new_path = prefix + sep + extension |
261 | ||
262 | # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly. | |
ce81b141 JMF |
263 | if (new_path == path or |
264 | (self._nopostoverwrites and os.path.exists(encodeFilename(new_path)))): | |
265 | self._downloader.to_screen('[youtube] Post-process file %s exists, skipping' % new_path) | |
592e97e8 | 266 | return [], information |
496c1923 PH |
267 | |
268 | try: | |
ce81b141 JMF |
269 | self._downloader.to_screen('[' + self.basename + '] Destination: ' + new_path) |
270 | self.run_ffmpeg(path, new_path, acodec, more_opts) | |
70a1165b JMF |
271 | except AudioConversionError as e: |
272 | raise PostProcessingError( | |
273 | 'audio conversion failed: ' + e.msg) | |
274 | except Exception: | |
275 | raise PostProcessingError('error running ' + self.basename) | |
496c1923 PH |
276 | |
277 | # Try to update the date time for extracted audio file. | |
278 | if information.get('filetime') is not None: | |
dd29eb7f S |
279 | self.try_utime( |
280 | new_path, time.time(), information['filetime'], | |
281 | errnote='Cannot update utime of audio file') | |
496c1923 PH |
282 | |
283 | information['filepath'] = new_path | |
ddbed364 | 284 | information['ext'] = extension |
285 | ||
592e97e8 | 286 | return [path], information |
496c1923 PH |
287 | |
288 | ||
4f026faf | 289 | class FFmpegVideoConvertorPP(FFmpegPostProcessor): |
1866432d AH |
290 | def __init__(self, downloader=None, preferedformat=None, extra_cmd_args=None): |
291 | super(FFmpegVideoConvertorPP, self).__init__(downloader, extra_cmd_args) | |
5f6a1245 | 292 | self._preferedformat = preferedformat |
496c1923 PH |
293 | |
294 | def run(self, information): | |
295 | path = information['filepath'] | |
3aa578ca | 296 | prefix, sep, ext = path.rpartition('.') |
d84f1d14 | 297 | ext = self._preferedformat |
1866432d | 298 | options = self._extra_cmd_args |
d84f1d14 AH |
299 | if self._preferedformat == 'xvid': |
300 | ext = 'avi' | |
301 | options.extend(['-c:v', 'libxvid', '-vtag', 'XVID']) | |
302 | outpath = prefix + sep + ext | |
496c1923 | 303 | if information['ext'] == self._preferedformat: |
3aa578ca | 304 | self._downloader.to_screen('[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat)) |
592e97e8 | 305 | return [], information |
3aa578ca | 306 | self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath) |
d84f1d14 | 307 | self.run_ffmpeg(path, outpath, options) |
496c1923 PH |
308 | information['filepath'] = outpath |
309 | information['format'] = self._preferedformat | |
d84f1d14 | 310 | information['ext'] = ext |
592e97e8 | 311 | return [path], information |
496c1923 PH |
312 | |
313 | ||
314 | class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): | |
315 | # See http://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt | |
316 | _lang_map = { | |
317 | 'aa': 'aar', | |
318 | 'ab': 'abk', | |
319 | 'ae': 'ave', | |
320 | 'af': 'afr', | |
321 | 'ak': 'aka', | |
322 | 'am': 'amh', | |
323 | 'an': 'arg', | |
324 | 'ar': 'ara', | |
325 | 'as': 'asm', | |
326 | 'av': 'ava', | |
327 | 'ay': 'aym', | |
328 | 'az': 'aze', | |
329 | 'ba': 'bak', | |
330 | 'be': 'bel', | |
331 | 'bg': 'bul', | |
332 | 'bh': 'bih', | |
333 | 'bi': 'bis', | |
334 | 'bm': 'bam', | |
335 | 'bn': 'ben', | |
336 | 'bo': 'bod', | |
337 | 'br': 'bre', | |
338 | 'bs': 'bos', | |
339 | 'ca': 'cat', | |
340 | 'ce': 'che', | |
341 | 'ch': 'cha', | |
342 | 'co': 'cos', | |
343 | 'cr': 'cre', | |
344 | 'cs': 'ces', | |
345 | 'cu': 'chu', | |
346 | 'cv': 'chv', | |
347 | 'cy': 'cym', | |
348 | 'da': 'dan', | |
349 | 'de': 'deu', | |
350 | 'dv': 'div', | |
351 | 'dz': 'dzo', | |
352 | 'ee': 'ewe', | |
353 | 'el': 'ell', | |
354 | 'en': 'eng', | |
355 | 'eo': 'epo', | |
356 | 'es': 'spa', | |
357 | 'et': 'est', | |
358 | 'eu': 'eus', | |
359 | 'fa': 'fas', | |
360 | 'ff': 'ful', | |
361 | 'fi': 'fin', | |
362 | 'fj': 'fij', | |
363 | 'fo': 'fao', | |
364 | 'fr': 'fra', | |
365 | 'fy': 'fry', | |
366 | 'ga': 'gle', | |
367 | 'gd': 'gla', | |
368 | 'gl': 'glg', | |
369 | 'gn': 'grn', | |
370 | 'gu': 'guj', | |
371 | 'gv': 'glv', | |
372 | 'ha': 'hau', | |
373 | 'he': 'heb', | |
374 | 'hi': 'hin', | |
375 | 'ho': 'hmo', | |
376 | 'hr': 'hrv', | |
377 | 'ht': 'hat', | |
378 | 'hu': 'hun', | |
379 | 'hy': 'hye', | |
380 | 'hz': 'her', | |
381 | 'ia': 'ina', | |
382 | 'id': 'ind', | |
383 | 'ie': 'ile', | |
384 | 'ig': 'ibo', | |
385 | 'ii': 'iii', | |
386 | 'ik': 'ipk', | |
387 | 'io': 'ido', | |
388 | 'is': 'isl', | |
389 | 'it': 'ita', | |
390 | 'iu': 'iku', | |
391 | 'ja': 'jpn', | |
392 | 'jv': 'jav', | |
393 | 'ka': 'kat', | |
394 | 'kg': 'kon', | |
395 | 'ki': 'kik', | |
396 | 'kj': 'kua', | |
397 | 'kk': 'kaz', | |
398 | 'kl': 'kal', | |
399 | 'km': 'khm', | |
400 | 'kn': 'kan', | |
401 | 'ko': 'kor', | |
402 | 'kr': 'kau', | |
403 | 'ks': 'kas', | |
404 | 'ku': 'kur', | |
405 | 'kv': 'kom', | |
406 | 'kw': 'cor', | |
407 | 'ky': 'kir', | |
408 | 'la': 'lat', | |
409 | 'lb': 'ltz', | |
410 | 'lg': 'lug', | |
411 | 'li': 'lim', | |
412 | 'ln': 'lin', | |
413 | 'lo': 'lao', | |
414 | 'lt': 'lit', | |
415 | 'lu': 'lub', | |
416 | 'lv': 'lav', | |
417 | 'mg': 'mlg', | |
418 | 'mh': 'mah', | |
419 | 'mi': 'mri', | |
420 | 'mk': 'mkd', | |
421 | 'ml': 'mal', | |
422 | 'mn': 'mon', | |
423 | 'mr': 'mar', | |
424 | 'ms': 'msa', | |
425 | 'mt': 'mlt', | |
426 | 'my': 'mya', | |
427 | 'na': 'nau', | |
428 | 'nb': 'nob', | |
429 | 'nd': 'nde', | |
430 | 'ne': 'nep', | |
431 | 'ng': 'ndo', | |
432 | 'nl': 'nld', | |
433 | 'nn': 'nno', | |
434 | 'no': 'nor', | |
435 | 'nr': 'nbl', | |
436 | 'nv': 'nav', | |
437 | 'ny': 'nya', | |
438 | 'oc': 'oci', | |
439 | 'oj': 'oji', | |
440 | 'om': 'orm', | |
441 | 'or': 'ori', | |
442 | 'os': 'oss', | |
443 | 'pa': 'pan', | |
444 | 'pi': 'pli', | |
445 | 'pl': 'pol', | |
446 | 'ps': 'pus', | |
447 | 'pt': 'por', | |
448 | 'qu': 'que', | |
449 | 'rm': 'roh', | |
450 | 'rn': 'run', | |
451 | 'ro': 'ron', | |
452 | 'ru': 'rus', | |
453 | 'rw': 'kin', | |
454 | 'sa': 'san', | |
455 | 'sc': 'srd', | |
456 | 'sd': 'snd', | |
457 | 'se': 'sme', | |
458 | 'sg': 'sag', | |
459 | 'si': 'sin', | |
460 | 'sk': 'slk', | |
461 | 'sl': 'slv', | |
462 | 'sm': 'smo', | |
463 | 'sn': 'sna', | |
464 | 'so': 'som', | |
465 | 'sq': 'sqi', | |
466 | 'sr': 'srp', | |
467 | 'ss': 'ssw', | |
468 | 'st': 'sot', | |
469 | 'su': 'sun', | |
470 | 'sv': 'swe', | |
471 | 'sw': 'swa', | |
472 | 'ta': 'tam', | |
473 | 'te': 'tel', | |
474 | 'tg': 'tgk', | |
475 | 'th': 'tha', | |
476 | 'ti': 'tir', | |
477 | 'tk': 'tuk', | |
478 | 'tl': 'tgl', | |
479 | 'tn': 'tsn', | |
480 | 'to': 'ton', | |
481 | 'tr': 'tur', | |
482 | 'ts': 'tso', | |
483 | 'tt': 'tat', | |
484 | 'tw': 'twi', | |
485 | 'ty': 'tah', | |
486 | 'ug': 'uig', | |
487 | 'uk': 'ukr', | |
488 | 'ur': 'urd', | |
489 | 'uz': 'uzb', | |
490 | 've': 'ven', | |
491 | 'vi': 'vie', | |
492 | 'vo': 'vol', | |
493 | 'wa': 'wln', | |
494 | 'wo': 'wol', | |
495 | 'xh': 'xho', | |
496 | 'yi': 'yid', | |
497 | 'yo': 'yor', | |
498 | 'za': 'zha', | |
499 | 'zh': 'zho', | |
500 | 'zu': 'zul', | |
501 | } | |
502 | ||
496c1923 PH |
503 | @classmethod |
504 | def _conver_lang_code(cls, code): | |
505 | """Convert language code from ISO 639-1 to ISO 639-2/T""" | |
506 | return cls._lang_map.get(code[:2]) | |
507 | ||
508 | def run(self, information): | |
083c1bb9 N |
509 | if information['ext'] not in ['mp4', 'mkv']: |
510 | self._downloader.to_screen('[ffmpeg] Subtitles can only be embedded in mp4 or mkv files') | |
592e97e8 | 511 | return [], information |
c84dd8a9 JMF |
512 | subtitles = information.get('requested_subtitles') |
513 | if not subtitles: | |
3aa578ca | 514 | self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to embed') |
592e97e8 | 515 | return [], information |
496c1923 | 516 | |
c84dd8a9 | 517 | sub_langs = list(subtitles.keys()) |
496c1923 | 518 | filename = information['filepath'] |
14523ed9 JMF |
519 | sub_filenames = [subtitles_filename(filename, lang, sub_info['ext']) for lang, sub_info in subtitles.items()] |
520 | input_files = [filename] + sub_filenames | |
496c1923 | 521 | |
e205db3b JMF |
522 | opts = [ |
523 | '-map', '0', | |
524 | '-c', 'copy', | |
525 | # Don't copy the existing subtitles, we may be running the | |
526 | # postprocessor a second time | |
527 | '-map', '-0:s', | |
528 | ] | |
083c1bb9 N |
529 | if information['ext'] == 'mp4': |
530 | opts += ['-c:s', 'mov_text'] | |
496c1923 | 531 | for (i, lang) in enumerate(sub_langs): |
2875cf01 | 532 | opts.extend(['-map', '%d:0' % (i + 1)]) |
496c1923 PH |
533 | lang_code = self._conver_lang_code(lang) |
534 | if lang_code is not None: | |
535 | opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code]) | |
496c1923 | 536 | |
2875cf01 | 537 | temp_filename = prepend_extension(filename, 'temp') |
3aa578ca | 538 | self._downloader.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename) |
496c1923 PH |
539 | self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) |
540 | os.remove(encodeFilename(filename)) | |
541 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
542 | ||
14523ed9 | 543 | return sub_filenames, information |
496c1923 PH |
544 | |
545 | ||
546 | class FFmpegMetadataPP(FFmpegPostProcessor): | |
547 | def run(self, info): | |
548 | metadata = {} | |
88cf6fb3 | 549 | if info.get('title') is not None: |
496c1923 PH |
550 | metadata['title'] = info['title'] |
551 | if info.get('upload_date') is not None: | |
552 | metadata['date'] = info['upload_date'] | |
e7db87f7 | 553 | if info.get('artist') is not None: |
554 | metadata['artist'] = info['artist'] | |
555 | elif info.get('uploader') is not None: | |
496c1923 PH |
556 | metadata['artist'] = info['uploader'] |
557 | elif info.get('uploader_id') is not None: | |
558 | metadata['artist'] = info['uploader_id'] | |
bd3cbe07 DP |
559 | if info.get('description') is not None: |
560 | metadata['description'] = info['description'] | |
2cf0ecac | 561 | metadata['comment'] = info['description'] |
bd3cbe07 | 562 | if info.get('webpage_url') is not None: |
2cf0ecac | 563 | metadata['purl'] = info['webpage_url'] |
e7db87f7 | 564 | if info.get('album') is not None: |
565 | metadata['album'] = info['album'] | |
496c1923 PH |
566 | |
567 | if not metadata: | |
3aa578ca | 568 | self._downloader.to_screen('[ffmpeg] There isn\'t any metadata to add') |
592e97e8 | 569 | return [], info |
496c1923 PH |
570 | |
571 | filename = info['filepath'] | |
572 | temp_filename = prepend_extension(filename, 'temp') | |
573 | ||
3aa578ca | 574 | if info['ext'] == 'm4a': |
39c68260 | 575 | options = ['-vn', '-acodec', 'copy'] |
576 | else: | |
577 | options = ['-c', 'copy'] | |
578 | ||
496c1923 PH |
579 | for (name, value) in metadata.items(): |
580 | options.extend(['-metadata', '%s=%s' % (name, value)]) | |
581 | ||
3aa578ca | 582 | self._downloader.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename) |
496c1923 PH |
583 | self.run_ffmpeg(filename, temp_filename, options) |
584 | os.remove(encodeFilename(filename)) | |
585 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
592e97e8 | 586 | return [], info |
496c1923 PH |
587 | |
588 | ||
589 | class FFmpegMergerPP(FFmpegPostProcessor): | |
590 | def run(self, info): | |
591 | filename = info['filepath'] | |
5b5fbc08 | 592 | temp_filename = prepend_extension(filename, 'temp') |
bc3e582f | 593 | args = ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0'] |
3aa578ca | 594 | self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename) |
5b5fbc08 JMF |
595 | self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args) |
596 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
d47aeb22 | 597 | return info['__files_to_merge'], info |
496c1923 | 598 | |
13763ce5 S |
599 | def can_merge(self): |
600 | # TODO: figure out merge-capable ffmpeg version | |
601 | if self.basename != 'avconv': | |
602 | return True | |
603 | ||
604 | required_version = '10-0' | |
605 | if is_outdated_version( | |
606 | self._versions[self.basename], required_version): | |
607 | warning = ('Your copy of %s is outdated and unable to properly mux separate video and audio files, ' | |
608 | 'youtube-dl will download single file media. ' | |
609 | 'Update %s to version %s or newer to fix this.') % ( | |
610 | self.basename, self.basename, required_version) | |
611 | if self._downloader: | |
612 | self._downloader.report_warning(warning) | |
613 | return False | |
614 | return True | |
615 | ||
0c14e2fb | 616 | |
6271f1ca PH |
617 | class FFmpegFixupStretchedPP(FFmpegPostProcessor): |
618 | def run(self, info): | |
619 | stretched_ratio = info.get('stretched_ratio') | |
620 | if stretched_ratio is None or stretched_ratio == 1: | |
592e97e8 | 621 | return [], info |
6271f1ca PH |
622 | |
623 | filename = info['filepath'] | |
624 | temp_filename = prepend_extension(filename, 'temp') | |
625 | ||
626 | options = ['-c', 'copy', '-aspect', '%f' % stretched_ratio] | |
627 | self._downloader.to_screen('[ffmpeg] Fixing aspect ratio in "%s"' % filename) | |
628 | self.run_ffmpeg(filename, temp_filename, options) | |
629 | ||
630 | os.remove(encodeFilename(filename)) | |
631 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
632 | ||
592e97e8 | 633 | return [], info |
62cd676c PH |
634 | |
635 | ||
636 | class FFmpegFixupM4aPP(FFmpegPostProcessor): | |
637 | def run(self, info): | |
638 | if info.get('container') != 'm4a_dash': | |
592e97e8 | 639 | return [], info |
62cd676c PH |
640 | |
641 | filename = info['filepath'] | |
642 | temp_filename = prepend_extension(filename, 'temp') | |
643 | ||
644 | options = ['-c', 'copy', '-f', 'mp4'] | |
645 | self._downloader.to_screen('[ffmpeg] Correcting container in "%s"' % filename) | |
646 | self.run_ffmpeg(filename, temp_filename, options) | |
647 | ||
648 | os.remove(encodeFilename(filename)) | |
649 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
650 | ||
592e97e8 | 651 | return [], info |
e9fade72 JMF |
652 | |
653 | ||
654 | class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor): | |
655 | def __init__(self, downloader=None, format=None): | |
656 | super(FFmpegSubtitlesConvertorPP, self).__init__(downloader) | |
657 | self.format = format | |
658 | ||
659 | def run(self, info): | |
660 | subs = info.get('requested_subtitles') | |
661 | filename = info['filepath'] | |
662 | new_ext = self.format | |
663 | new_format = new_ext | |
664 | if new_format == 'vtt': | |
665 | new_format = 'webvtt' | |
666 | if subs is None: | |
667 | self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to convert') | |
592e97e8 | 668 | return [], info |
e9fade72 JMF |
669 | self._downloader.to_screen('[ffmpeg] Converting subtitles') |
670 | for lang, sub in subs.items(): | |
671 | ext = sub['ext'] | |
672 | if ext == new_ext: | |
673 | self._downloader.to_screen( | |
674 | '[ffmpeg] Subtitle file for %s is already in the requested' | |
675 | 'format' % new_ext) | |
676 | continue | |
677 | new_file = subtitles_filename(filename, lang, new_ext) | |
bf6427d2 YCH |
678 | |
679 | if ext == 'dfxp' or ext == 'ttml': | |
680 | self._downloader.report_warning( | |
681 | 'You have requested to convert dfxp (TTML) subtitles into another format, ' | |
682 | 'which results in style information loss') | |
683 | ||
684 | dfxp_file = subtitles_filename(filename, lang, ext) | |
685 | srt_file = subtitles_filename(filename, lang, 'srt') | |
686 | ||
687 | with io.open(dfxp_file, 'rt', encoding='utf-8') as f: | |
688 | srt_data = dfxp2srt(f.read()) | |
689 | ||
690 | with io.open(srt_file, 'wt', encoding='utf-8') as f: | |
691 | f.write(srt_data) | |
692 | ||
693 | ext = 'srt' | |
694 | subs[lang] = { | |
695 | 'ext': 'srt', | |
696 | 'data': srt_data | |
697 | } | |
698 | ||
699 | if new_ext == 'srt': | |
700 | continue | |
701 | ||
e9fade72 JMF |
702 | self.run_ffmpeg( |
703 | subtitles_filename(filename, lang, ext), | |
704 | new_file, ['-f', new_format]) | |
705 | ||
706 | with io.open(new_file, 'rt', encoding='utf-8') as f: | |
707 | subs[lang] = { | |
708 | 'ext': ext, | |
709 | 'data': f.read(), | |
710 | } | |
711 | ||
592e97e8 | 712 | return [], info |