]>
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, |
39672624 | 24 | ISO639Utils, |
496c1923 PH |
25 | ) |
26 | ||
27 | ||
496c1923 PH |
28 | class FFmpegPostProcessorError(PostProcessingError): |
29 | pass | |
30 | ||
d799b47b | 31 | |
496c1923 | 32 | class FFmpegPostProcessor(PostProcessor): |
d47aeb22 | 33 | def __init__(self, downloader=None): |
496c1923 | 34 | PostProcessor.__init__(self, downloader) |
73fac4e9 | 35 | self._determine_executables() |
496c1923 | 36 | |
48844745 | 37 | def check_version(self): |
f740fae2 | 38 | if not self.available: |
3aa578ca | 39 | raise FFmpegPostProcessorError('ffmpeg or avconv not found. Please install one.') |
48844745 | 40 | |
65bf37ef | 41 | required_version = '10-0' if self.basename == 'avconv' else '1.0' |
48844745 | 42 | if is_outdated_version( |
73fac4e9 | 43 | self._versions[self.basename], required_version): |
3aa578ca | 44 | warning = 'Your copy of %s is outdated, update %s to version %s or newer if you encounter any errors.' % ( |
73fac4e9 | 45 | self.basename, self.basename, required_version) |
6194bb14 PH |
46 | if self._downloader: |
47 | self._downloader.report_warning(warning) | |
48844745 | 48 | |
496c1923 | 49 | @staticmethod |
73fac4e9 PH |
50 | def get_versions(downloader=None): |
51 | return FFmpegPostProcessor(downloader)._versions | |
6271f1ca | 52 | |
73fac4e9 PH |
53 | def _determine_executables(self): |
54 | programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] | |
374c761e | 55 | prefer_ffmpeg = False |
73fac4e9 PH |
56 | |
57 | self.basename = None | |
58 | self.probe_basename = None | |
59 | ||
60 | self._paths = None | |
61 | self._versions = None | |
62 | if self._downloader: | |
374c761e | 63 | prefer_ffmpeg = self._downloader.params.get('prefer_ffmpeg', False) |
73fac4e9 PH |
64 | location = self._downloader.params.get('ffmpeg_location') |
65 | if location is not None: | |
66 | if not os.path.exists(location): | |
67 | self._downloader.report_warning( | |
68 | 'ffmpeg-location %s does not exist! ' | |
69 | 'Continuing without avconv/ffmpeg.' % (location)) | |
70 | self._versions = {} | |
71 | return | |
72 | elif not os.path.isdir(location): | |
73 | basename = os.path.splitext(os.path.basename(location))[0] | |
74 | if basename not in programs: | |
75 | self._downloader.report_warning( | |
76 | 'Cannot identify executable %s, its basename should be one of %s. ' | |
77 | 'Continuing without avconv/ffmpeg.' % | |
78 | (location, ', '.join(programs))) | |
79 | self._versions = {} | |
80 | return None | |
81 | location = os.path.dirname(os.path.abspath(location)) | |
82 | if basename in ('ffmpeg', 'ffprobe'): | |
83 | prefer_ffmpeg = True | |
84 | ||
85 | self._paths = dict( | |
86 | (p, os.path.join(location, p)) for p in programs) | |
87 | self._versions = dict( | |
88 | (p, get_exe_version(self._paths[p], args=['-version'])) | |
89 | for p in programs) | |
90 | if self._versions is None: | |
91 | self._versions = dict( | |
92 | (p, get_exe_version(p, args=['-version'])) for p in programs) | |
93 | self._paths = dict((p, p) for p in programs) | |
94 | ||
95 | if prefer_ffmpeg: | |
d28b5171 | 96 | prefs = ('ffmpeg', 'avconv') |
76b1bd67 | 97 | else: |
d28b5171 PH |
98 | prefs = ('avconv', 'ffmpeg') |
99 | for p in prefs: | |
100 | if self._versions[p]: | |
73fac4e9 PH |
101 | self.basename = p |
102 | break | |
76b1bd67 | 103 | |
73fac4e9 | 104 | if prefer_ffmpeg: |
50b51830 | 105 | prefs = ('ffprobe', 'avprobe') |
1a253e13 PH |
106 | else: |
107 | prefs = ('avprobe', 'ffprobe') | |
108 | for p in prefs: | |
109 | if self._versions[p]: | |
73fac4e9 PH |
110 | self.probe_basename = p |
111 | break | |
112 | ||
f740fae2 | 113 | @property |
73fac4e9 PH |
114 | def available(self): |
115 | return self.basename is not None | |
1a253e13 | 116 | |
73fac4e9 PH |
117 | @property |
118 | def executable(self): | |
119 | return self._paths[self.basename] | |
120 | ||
3da4b313 JMF |
121 | @property |
122 | def probe_available(self): | |
123 | return self.probe_basename is not None | |
124 | ||
73fac4e9 PH |
125 | @property |
126 | def probe_executable(self): | |
127 | return self._paths[self.probe_basename] | |
76b1bd67 | 128 | |
496c1923 | 129 | def run_ffmpeg_multiple_files(self, input_paths, out_path, opts): |
48844745 | 130 | self.check_version() |
496c1923 | 131 | |
52afb2ac PH |
132 | oldest_mtime = min( |
133 | os.stat(encodeFilename(path)).st_mtime for path in input_paths) | |
43bc8890 | 134 | |
15006fed S |
135 | opts += self._configuration_args() |
136 | ||
496c1923 PH |
137 | files_cmd = [] |
138 | for path in input_paths: | |
8a7bbd16 JMF |
139 | files_cmd.extend([ |
140 | encodeArgument('-i'), | |
141 | encodeFilename(self._ffmpeg_filename_argument(path), True) | |
142 | ]) | |
73fac4e9 | 143 | cmd = ([encodeFilename(self.executable, True), encodeArgument('-y')] + |
43bc8890 PH |
144 | files_cmd + |
145 | [encodeArgument(o) for o in opts] + | |
496c1923 PH |
146 | [encodeFilename(self._ffmpeg_filename_argument(out_path), True)]) |
147 | ||
148 | if self._downloader.params.get('verbose', False): | |
3aa578ca | 149 | self._downloader.to_screen('[debug] ffmpeg command line: %s' % shell_quote(cmd)) |
cffcbc02 | 150 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) |
62fec3b2 | 151 | stdout, stderr = p.communicate() |
496c1923 PH |
152 | if p.returncode != 0: |
153 | stderr = stderr.decode('utf-8', 'replace') | |
154 | msg = stderr.strip().split('\n')[-1] | |
155 | raise FFmpegPostProcessorError(msg) | |
dd29eb7f | 156 | self.try_utime(out_path, oldest_mtime, oldest_mtime) |
cc55d088 | 157 | |
496c1923 PH |
158 | def run_ffmpeg(self, path, out_path, opts): |
159 | self.run_ffmpeg_multiple_files([path], out_path, opts) | |
160 | ||
161 | def _ffmpeg_filename_argument(self, fn): | |
8a7bbd16 JMF |
162 | # Always use 'file:' because the filename may contain ':' (ffmpeg |
163 | # interprets that as a protocol) or can start with '-' (-- is broken in | |
164 | # ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details) | |
165 | return 'file:' + fn | |
496c1923 PH |
166 | |
167 | ||
168 | class FFmpegExtractAudioPP(FFmpegPostProcessor): | |
169 | def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False): | |
170 | FFmpegPostProcessor.__init__(self, downloader) | |
171 | if preferredcodec is None: | |
172 | preferredcodec = 'best' | |
173 | self._preferredcodec = preferredcodec | |
174 | self._preferredquality = preferredquality | |
175 | self._nopostoverwrites = nopostoverwrites | |
176 | ||
177 | def get_audio_codec(self, path): | |
1a253e13 | 178 | |
3da4b313 | 179 | if not self.probe_available: |
3aa578ca | 180 | raise PostProcessingError('ffprobe or avprobe not found. Please install one.') |
496c1923 PH |
181 | try: |
182 | cmd = [ | |
73fac4e9 | 183 | encodeFilename(self.probe_executable, True), |
b0e87c31 | 184 | encodeArgument('-show_streams'), |
496c1923 | 185 | encodeFilename(self._ffmpeg_filename_argument(path), True)] |
73fac4e9 | 186 | if self._downloader.params.get('verbose', False): |
5bfd430f | 187 | self._downloader.to_screen('[debug] %s command line: %s' % (self.basename, shell_quote(cmd))) |
cffcbc02 | 188 | handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE, stdin=subprocess.PIPE) |
496c1923 PH |
189 | output = handle.communicate()[0] |
190 | if handle.wait() != 0: | |
191 | return None | |
192 | except (IOError, OSError): | |
193 | return None | |
194 | audio_codec = None | |
195 | for line in output.decode('ascii', 'ignore').split('\n'): | |
196 | if line.startswith('codec_name='): | |
197 | audio_codec = line.split('=')[1].strip() | |
198 | elif line.strip() == 'codec_type=audio' and audio_codec is not None: | |
199 | return audio_codec | |
200 | return None | |
201 | ||
202 | def run_ffmpeg(self, path, out_path, codec, more_opts): | |
496c1923 PH |
203 | if codec is None: |
204 | acodec_opts = [] | |
205 | else: | |
206 | acodec_opts = ['-acodec', codec] | |
207 | opts = ['-vn'] + acodec_opts + more_opts | |
208 | try: | |
209 | FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts) | |
210 | except FFmpegPostProcessorError as err: | |
211 | raise AudioConversionError(err.msg) | |
212 | ||
213 | def run(self, information): | |
214 | path = information['filepath'] | |
215 | ||
216 | filecodec = self.get_audio_codec(path) | |
217 | if filecodec is None: | |
3aa578ca | 218 | raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe') |
496c1923 PH |
219 | |
220 | more_opts = [] | |
221 | if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'): | |
222 | if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']: | |
223 | # Lossless, but in another container | |
224 | acodec = 'copy' | |
225 | extension = 'm4a' | |
467d3c9a | 226 | more_opts = ['-bsf:a', 'aac_adtstoasc'] |
496c1923 PH |
227 | elif filecodec in ['aac', 'mp3', 'vorbis', 'opus']: |
228 | # Lossless if possible | |
229 | acodec = 'copy' | |
230 | extension = filecodec | |
231 | if filecodec == 'aac': | |
232 | more_opts = ['-f', 'adts'] | |
233 | if filecodec == 'vorbis': | |
234 | extension = 'ogg' | |
235 | else: | |
236 | # MP3 otherwise. | |
237 | acodec = 'libmp3lame' | |
238 | extension = 'mp3' | |
239 | more_opts = [] | |
240 | if self._preferredquality is not None: | |
241 | if int(self._preferredquality) < 10: | |
467d3c9a | 242 | more_opts += ['-q:a', self._preferredquality] |
496c1923 | 243 | else: |
467d3c9a | 244 | more_opts += ['-b:a', self._preferredquality + 'k'] |
496c1923 PH |
245 | else: |
246 | # We convert the audio (lossy) | |
247 | acodec = {'mp3': 'libmp3lame', 'aac': 'aac', 'm4a': 'aac', 'opus': 'opus', 'vorbis': 'libvorbis', 'wav': None}[self._preferredcodec] | |
248 | extension = self._preferredcodec | |
249 | more_opts = [] | |
250 | if self._preferredquality is not None: | |
251 | # The opus codec doesn't support the -aq option | |
252 | if int(self._preferredquality) < 10 and extension != 'opus': | |
467d3c9a | 253 | more_opts += ['-q:a', self._preferredquality] |
496c1923 | 254 | else: |
467d3c9a | 255 | more_opts += ['-b:a', self._preferredquality + 'k'] |
496c1923 PH |
256 | if self._preferredcodec == 'aac': |
257 | more_opts += ['-f', 'adts'] | |
258 | if self._preferredcodec == 'm4a': | |
467d3c9a | 259 | more_opts += ['-bsf:a', 'aac_adtstoasc'] |
496c1923 PH |
260 | if self._preferredcodec == 'vorbis': |
261 | extension = 'ogg' | |
262 | if self._preferredcodec == 'wav': | |
263 | extension = 'wav' | |
264 | more_opts += ['-f', 'wav'] | |
265 | ||
3aa578ca | 266 | prefix, sep, ext = path.rpartition('.') # not os.path.splitext, since the latter does not work on unicode in all setups |
496c1923 PH |
267 | new_path = prefix + sep + extension |
268 | ||
269 | # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly. | |
ce81b141 JMF |
270 | if (new_path == path or |
271 | (self._nopostoverwrites and os.path.exists(encodeFilename(new_path)))): | |
9750e7d7 | 272 | self._downloader.to_screen('[ffmpeg] Post-process file %s exists, skipping' % new_path) |
592e97e8 | 273 | return [], information |
496c1923 PH |
274 | |
275 | try: | |
deb85c32 | 276 | self._downloader.to_screen('[ffmpeg] Destination: ' + new_path) |
ce81b141 | 277 | self.run_ffmpeg(path, new_path, acodec, more_opts) |
70a1165b JMF |
278 | except AudioConversionError as e: |
279 | raise PostProcessingError( | |
280 | 'audio conversion failed: ' + e.msg) | |
281 | except Exception: | |
282 | raise PostProcessingError('error running ' + self.basename) | |
496c1923 PH |
283 | |
284 | # Try to update the date time for extracted audio file. | |
285 | if information.get('filetime') is not None: | |
dd29eb7f S |
286 | self.try_utime( |
287 | new_path, time.time(), information['filetime'], | |
288 | errnote='Cannot update utime of audio file') | |
496c1923 PH |
289 | |
290 | information['filepath'] = new_path | |
ddbed364 | 291 | information['ext'] = extension |
292 | ||
592e97e8 | 293 | return [path], information |
496c1923 PH |
294 | |
295 | ||
4f026faf | 296 | class FFmpegVideoConvertorPP(FFmpegPostProcessor): |
5f6a1245 | 297 | def __init__(self, downloader=None, preferedformat=None): |
4f026faf | 298 | super(FFmpegVideoConvertorPP, self).__init__(downloader) |
5f6a1245 | 299 | self._preferedformat = preferedformat |
496c1923 PH |
300 | |
301 | def run(self, information): | |
302 | path = information['filepath'] | |
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 |
15006fed S |
306 | options = [] |
307 | if self._preferedformat == 'avi': | |
308 | options.extend(['-c:v', 'libxvid', '-vtag', 'XVID']) | |
309 | prefix, sep, ext = path.rpartition('.') | |
310 | outpath = prefix + sep + self._preferedformat | |
3aa578ca | 311 | self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath) |
d84f1d14 | 312 | self.run_ffmpeg(path, outpath, options) |
496c1923 PH |
313 | information['filepath'] = outpath |
314 | information['format'] = self._preferedformat | |
f72b0a60 | 315 | information['ext'] = self._preferedformat |
592e97e8 | 316 | return [path], information |
496c1923 PH |
317 | |
318 | ||
319 | class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): | |
496c1923 | 320 | def run(self, information): |
083c1bb9 N |
321 | if information['ext'] not in ['mp4', 'mkv']: |
322 | self._downloader.to_screen('[ffmpeg] Subtitles can only be embedded in mp4 or mkv files') | |
592e97e8 | 323 | return [], information |
c84dd8a9 JMF |
324 | subtitles = information.get('requested_subtitles') |
325 | if not subtitles: | |
3aa578ca | 326 | self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to embed') |
592e97e8 | 327 | return [], information |
496c1923 | 328 | |
c84dd8a9 | 329 | sub_langs = list(subtitles.keys()) |
496c1923 | 330 | filename = information['filepath'] |
14523ed9 JMF |
331 | sub_filenames = [subtitles_filename(filename, lang, sub_info['ext']) for lang, sub_info in subtitles.items()] |
332 | input_files = [filename] + sub_filenames | |
496c1923 | 333 | |
e205db3b JMF |
334 | opts = [ |
335 | '-map', '0', | |
336 | '-c', 'copy', | |
337 | # Don't copy the existing subtitles, we may be running the | |
338 | # postprocessor a second time | |
339 | '-map', '-0:s', | |
340 | ] | |
083c1bb9 N |
341 | if information['ext'] == 'mp4': |
342 | opts += ['-c:s', 'mov_text'] | |
496c1923 | 343 | for (i, lang) in enumerate(sub_langs): |
2875cf01 | 344 | opts.extend(['-map', '%d:0' % (i + 1)]) |
39672624 | 345 | lang_code = ISO639Utils.short2long(lang) |
496c1923 PH |
346 | if lang_code is not None: |
347 | opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code]) | |
496c1923 | 348 | |
2875cf01 | 349 | temp_filename = prepend_extension(filename, 'temp') |
3aa578ca | 350 | self._downloader.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename) |
496c1923 PH |
351 | self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) |
352 | os.remove(encodeFilename(filename)) | |
353 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
354 | ||
14523ed9 | 355 | return sub_filenames, information |
496c1923 PH |
356 | |
357 | ||
358 | class FFmpegMetadataPP(FFmpegPostProcessor): | |
359 | def run(self, info): | |
360 | metadata = {} | |
88cf6fb3 | 361 | if info.get('title') is not None: |
496c1923 PH |
362 | metadata['title'] = info['title'] |
363 | if info.get('upload_date') is not None: | |
364 | metadata['date'] = info['upload_date'] | |
e7db87f7 | 365 | if info.get('artist') is not None: |
366 | metadata['artist'] = info['artist'] | |
367 | elif info.get('uploader') is not None: | |
496c1923 PH |
368 | metadata['artist'] = info['uploader'] |
369 | elif info.get('uploader_id') is not None: | |
370 | metadata['artist'] = info['uploader_id'] | |
bd3cbe07 DP |
371 | if info.get('description') is not None: |
372 | metadata['description'] = info['description'] | |
2cf0ecac | 373 | metadata['comment'] = info['description'] |
bd3cbe07 | 374 | if info.get('webpage_url') is not None: |
2cf0ecac | 375 | metadata['purl'] = info['webpage_url'] |
e7db87f7 | 376 | if info.get('album') is not None: |
377 | metadata['album'] = info['album'] | |
496c1923 PH |
378 | |
379 | if not metadata: | |
3aa578ca | 380 | self._downloader.to_screen('[ffmpeg] There isn\'t any metadata to add') |
592e97e8 | 381 | return [], info |
496c1923 PH |
382 | |
383 | filename = info['filepath'] | |
384 | temp_filename = prepend_extension(filename, 'temp') | |
385 | ||
3aa578ca | 386 | if info['ext'] == 'm4a': |
39c68260 | 387 | options = ['-vn', '-acodec', 'copy'] |
388 | else: | |
389 | options = ['-c', 'copy'] | |
390 | ||
496c1923 PH |
391 | for (name, value) in metadata.items(): |
392 | options.extend(['-metadata', '%s=%s' % (name, value)]) | |
393 | ||
cf57433b | 394 | # https://github.com/rg3/youtube-dl/issues/8350 |
7d3a035e | 395 | if info.get('protocol') == 'm3u8_native' or info.get('protocol') == 'm3u8' and self._downloader.params.get('hls_prefer_native', False): |
cf57433b | 396 | options.extend(['-bsf:a', 'aac_adtstoasc']) |
397 | ||
3aa578ca | 398 | self._downloader.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename) |
496c1923 PH |
399 | self.run_ffmpeg(filename, temp_filename, options) |
400 | os.remove(encodeFilename(filename)) | |
401 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
592e97e8 | 402 | return [], info |
496c1923 PH |
403 | |
404 | ||
405 | class FFmpegMergerPP(FFmpegPostProcessor): | |
406 | def run(self, info): | |
407 | filename = info['filepath'] | |
5b5fbc08 | 408 | temp_filename = prepend_extension(filename, 'temp') |
bc3e582f | 409 | args = ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0'] |
3aa578ca | 410 | self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename) |
5b5fbc08 JMF |
411 | self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args) |
412 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
d47aeb22 | 413 | return info['__files_to_merge'], info |
496c1923 | 414 | |
13763ce5 S |
415 | def can_merge(self): |
416 | # TODO: figure out merge-capable ffmpeg version | |
417 | if self.basename != 'avconv': | |
418 | return True | |
419 | ||
420 | required_version = '10-0' | |
421 | if is_outdated_version( | |
422 | self._versions[self.basename], required_version): | |
423 | warning = ('Your copy of %s is outdated and unable to properly mux separate video and audio files, ' | |
424 | 'youtube-dl will download single file media. ' | |
425 | 'Update %s to version %s or newer to fix this.') % ( | |
426 | self.basename, self.basename, required_version) | |
427 | if self._downloader: | |
428 | self._downloader.report_warning(warning) | |
429 | return False | |
430 | return True | |
431 | ||
0c14e2fb | 432 | |
6271f1ca PH |
433 | class FFmpegFixupStretchedPP(FFmpegPostProcessor): |
434 | def run(self, info): | |
435 | stretched_ratio = info.get('stretched_ratio') | |
436 | if stretched_ratio is None or stretched_ratio == 1: | |
592e97e8 | 437 | return [], info |
6271f1ca PH |
438 | |
439 | filename = info['filepath'] | |
440 | temp_filename = prepend_extension(filename, 'temp') | |
441 | ||
442 | options = ['-c', 'copy', '-aspect', '%f' % stretched_ratio] | |
443 | self._downloader.to_screen('[ffmpeg] Fixing aspect ratio in "%s"' % filename) | |
444 | self.run_ffmpeg(filename, temp_filename, options) | |
445 | ||
446 | os.remove(encodeFilename(filename)) | |
447 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
448 | ||
592e97e8 | 449 | return [], info |
62cd676c PH |
450 | |
451 | ||
452 | class FFmpegFixupM4aPP(FFmpegPostProcessor): | |
453 | def run(self, info): | |
454 | if info.get('container') != 'm4a_dash': | |
592e97e8 | 455 | return [], info |
62cd676c PH |
456 | |
457 | filename = info['filepath'] | |
458 | temp_filename = prepend_extension(filename, 'temp') | |
459 | ||
460 | options = ['-c', 'copy', '-f', 'mp4'] | |
461 | self._downloader.to_screen('[ffmpeg] Correcting container in "%s"' % filename) | |
462 | self.run_ffmpeg(filename, temp_filename, options) | |
463 | ||
464 | os.remove(encodeFilename(filename)) | |
465 | os.rename(encodeFilename(temp_filename), encodeFilename(filename)) | |
466 | ||
592e97e8 | 467 | return [], info |
e9fade72 JMF |
468 | |
469 | ||
470 | class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor): | |
471 | def __init__(self, downloader=None, format=None): | |
472 | super(FFmpegSubtitlesConvertorPP, self).__init__(downloader) | |
473 | self.format = format | |
474 | ||
475 | def run(self, info): | |
476 | subs = info.get('requested_subtitles') | |
477 | filename = info['filepath'] | |
478 | new_ext = self.format | |
479 | new_format = new_ext | |
480 | if new_format == 'vtt': | |
481 | new_format = 'webvtt' | |
482 | if subs is None: | |
483 | self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to convert') | |
592e97e8 | 484 | return [], info |
e9fade72 | 485 | self._downloader.to_screen('[ffmpeg] Converting subtitles') |
e04398e3 | 486 | sub_filenames = [] |
e9fade72 JMF |
487 | for lang, sub in subs.items(): |
488 | ext = sub['ext'] | |
489 | if ext == new_ext: | |
490 | self._downloader.to_screen( | |
491 | '[ffmpeg] Subtitle file for %s is already in the requested' | |
492 | 'format' % new_ext) | |
493 | continue | |
e04398e3 JMF |
494 | old_file = subtitles_filename(filename, lang, ext) |
495 | sub_filenames.append(old_file) | |
e9fade72 | 496 | new_file = subtitles_filename(filename, lang, new_ext) |
bf6427d2 YCH |
497 | |
498 | if ext == 'dfxp' or ext == 'ttml': | |
499 | self._downloader.report_warning( | |
500 | 'You have requested to convert dfxp (TTML) subtitles into another format, ' | |
501 | 'which results in style information loss') | |
502 | ||
e04398e3 | 503 | dfxp_file = old_file |
bf6427d2 YCH |
504 | srt_file = subtitles_filename(filename, lang, 'srt') |
505 | ||
506 | with io.open(dfxp_file, 'rt', encoding='utf-8') as f: | |
507 | srt_data = dfxp2srt(f.read()) | |
508 | ||
509 | with io.open(srt_file, 'wt', encoding='utf-8') as f: | |
510 | f.write(srt_data) | |
7e62c2eb | 511 | old_file = srt_file |
bf6427d2 YCH |
512 | |
513 | ext = 'srt' | |
514 | subs[lang] = { | |
515 | 'ext': 'srt', | |
516 | 'data': srt_data | |
517 | } | |
518 | ||
519 | if new_ext == 'srt': | |
520 | continue | |
521 | ||
e04398e3 | 522 | self.run_ffmpeg(old_file, new_file, ['-f', new_format]) |
e9fade72 JMF |
523 | |
524 | with io.open(new_file, 'rt', encoding='utf-8') as f: | |
525 | subs[lang] = { | |
526 | 'ext': ext, | |
527 | 'data': f.read(), | |
528 | } | |
529 | ||
e04398e3 | 530 | return sub_filenames, info |