ffmpeg.py (26138B)
1 from __future__ import unicode_literals 2 3 import io 4 import os 5 import subprocess 6 import time 7 import re 8 9 10 from .common import AudioConversionError, PostProcessor 11 12 from ..utils import ( 13 encodeArgument, 14 encodeFilename, 15 get_exe_version, 16 is_outdated_version, 17 PostProcessingError, 18 prepend_extension, 19 shell_quote, 20 subtitles_filename, 21 dfxp2srt, 22 ISO639Utils, 23 replace_extension, 24 ) 25 26 27 EXT_TO_OUT_FORMATS = { 28 'aac': 'adts', 29 'flac': 'flac', 30 'm4a': 'ipod', 31 'mka': 'matroska', 32 'mkv': 'matroska', 33 'mpg': 'mpeg', 34 'ogv': 'ogg', 35 'ts': 'mpegts', 36 'wma': 'asf', 37 'wmv': 'asf', 38 } 39 ACODECS = { 40 'mp3': 'libmp3lame', 41 'aac': 'aac', 42 'flac': 'flac', 43 'm4a': 'aac', 44 'opus': 'libopus', 45 'vorbis': 'libvorbis', 46 'wav': None, 47 } 48 49 50 class FFmpegPostProcessorError(PostProcessingError): 51 pass 52 53 54 class FFmpegPostProcessor(PostProcessor): 55 def __init__(self, downloader=None): 56 PostProcessor.__init__(self, downloader) 57 self._determine_executables() 58 59 def check_version(self): 60 if not self.available: 61 raise FFmpegPostProcessorError('ffmpeg or avconv not found. Please install one.') 62 63 required_version = '10-0' if self.basename == 'avconv' else '1.0' 64 if is_outdated_version( 65 self._versions[self.basename], required_version): 66 warning = 'Your copy of %s is outdated, update %s to version %s or newer if you encounter any errors.' % ( 67 self.basename, self.basename, required_version) 68 if self._downloader: 69 self._downloader.report_warning(warning) 70 71 @staticmethod 72 def get_versions(downloader=None): 73 return FFmpegPostProcessor(downloader)._versions 74 75 def _determine_executables(self): 76 programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] 77 prefer_ffmpeg = True 78 79 def get_ffmpeg_version(path): 80 ver = get_exe_version(path, args=['-version']) 81 if ver: 82 regexs = [ 83 r'(?:\d+:)?([0-9.]+)-[0-9]+ubuntu[0-9.]+$', # Ubuntu, see [1] 84 r'n([0-9.]+)$', # Arch Linux 85 # 1. http://www.ducea.com/2006/06/17/ubuntu-package-version-naming-explanation/ 86 ] 87 for regex in regexs: 88 mobj = re.match(regex, ver) 89 if mobj: 90 ver = mobj.group(1) 91 return ver 92 93 self.basename = None 94 self.probe_basename = None 95 96 self._paths = None 97 self._versions = None 98 if self._downloader: 99 prefer_ffmpeg = self._downloader.params.get('prefer_ffmpeg', True) 100 location = self._downloader.params.get('ffmpeg_location') 101 if location is not None: 102 if not os.path.exists(location): 103 self._downloader.report_warning( 104 'ffmpeg-location %s does not exist! ' 105 'Continuing without avconv/ffmpeg.' % (location)) 106 self._versions = {} 107 return 108 elif not os.path.isdir(location): 109 basename = os.path.splitext(os.path.basename(location))[0] 110 if basename not in programs: 111 self._downloader.report_warning( 112 'Cannot identify executable %s, its basename should be one of %s. ' 113 'Continuing without avconv/ffmpeg.' % 114 (location, ', '.join(programs))) 115 self._versions = {} 116 return None 117 location = os.path.dirname(os.path.abspath(location)) 118 if basename in ('ffmpeg', 'ffprobe'): 119 prefer_ffmpeg = True 120 121 self._paths = dict( 122 (p, os.path.join(location, p)) for p in programs) 123 self._versions = dict( 124 (p, get_ffmpeg_version(self._paths[p])) for p in programs) 125 if self._versions is None: 126 self._versions = dict( 127 (p, get_ffmpeg_version(p)) for p in programs) 128 self._paths = dict((p, p) for p in programs) 129 130 if prefer_ffmpeg is False: 131 prefs = ('avconv', 'ffmpeg') 132 else: 133 prefs = ('ffmpeg', 'avconv') 134 for p in prefs: 135 if self._versions[p]: 136 self.basename = p 137 break 138 139 if prefer_ffmpeg is False: 140 prefs = ('avprobe', 'ffprobe') 141 else: 142 prefs = ('ffprobe', 'avprobe') 143 for p in prefs: 144 if self._versions[p]: 145 self.probe_basename = p 146 break 147 148 @property 149 def available(self): 150 return self.basename is not None 151 152 @property 153 def executable(self): 154 return self._paths[self.basename] 155 156 @property 157 def probe_available(self): 158 return self.probe_basename is not None 159 160 @property 161 def probe_executable(self): 162 return self._paths[self.probe_basename] 163 164 def get_audio_codec(self, path): 165 if not self.probe_available and not self.available: 166 raise PostProcessingError('ffprobe/avprobe and ffmpeg/avconv not found. Please install one.') 167 try: 168 if self.probe_available: 169 cmd = [ 170 encodeFilename(self.probe_executable, True), 171 encodeArgument('-show_streams')] 172 else: 173 cmd = [ 174 encodeFilename(self.executable, True), 175 encodeArgument('-i')] 176 cmd.append(encodeFilename(self._ffmpeg_filename_argument(path), True)) 177 if self._downloader.params.get('verbose', False): 178 self._downloader.to_screen( 179 '[debug] %s command line: %s' % (self.basename, shell_quote(cmd))) 180 handle = subprocess.Popen( 181 cmd, stderr=subprocess.PIPE, 182 stdout=subprocess.PIPE, stdin=subprocess.PIPE) 183 stdout_data, stderr_data = handle.communicate() 184 expected_ret = 0 if self.probe_available else 1 185 if handle.wait() != expected_ret: 186 return None 187 except (IOError, OSError): 188 return None 189 output = (stdout_data if self.probe_available else stderr_data).decode('ascii', 'ignore') 190 if self.probe_available: 191 audio_codec = None 192 for line in output.split('\n'): 193 if line.startswith('codec_name='): 194 audio_codec = line.split('=')[1].strip() 195 elif line.strip() == 'codec_type=audio' and audio_codec is not None: 196 return audio_codec 197 else: 198 # Stream #FILE_INDEX:STREAM_INDEX[STREAM_ID](LANGUAGE): CODEC_TYPE: CODEC_NAME 199 mobj = re.search( 200 r'Stream\s*#\d+:\d+(?:\[0x[0-9a-f]+\])?(?:\([a-z]{3}\))?:\s*Audio:\s*([0-9a-z]+)', 201 output) 202 if mobj: 203 return mobj.group(1) 204 return None 205 206 def run_ffmpeg_multiple_files(self, input_paths, out_path, opts): 207 self.check_version() 208 209 oldest_mtime = min( 210 os.stat(encodeFilename(path)).st_mtime for path in input_paths) 211 212 opts += self._configuration_args() 213 214 files_cmd = [] 215 for path in input_paths: 216 files_cmd.extend([ 217 encodeArgument('-i'), 218 encodeFilename(self._ffmpeg_filename_argument(path), True) 219 ]) 220 cmd = [encodeFilename(self.executable, True), encodeArgument('-y')] 221 # avconv does not have repeat option 222 if self.basename == 'ffmpeg': 223 cmd += [encodeArgument('-loglevel'), encodeArgument('repeat+info')] 224 cmd += (files_cmd 225 + [encodeArgument(o) for o in opts] 226 + [encodeFilename(self._ffmpeg_filename_argument(out_path), True)]) 227 228 if self._downloader.params.get('verbose', False): 229 self._downloader.to_screen('[debug] ffmpeg command line: %s' % shell_quote(cmd)) 230 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) 231 stdout, stderr = p.communicate() 232 if p.returncode != 0: 233 stderr = stderr.decode('utf-8', 'replace') 234 msgs = stderr.strip().split('\n') 235 msg = msgs[-1] 236 if self._downloader.params.get('verbose', False): 237 self._downloader.to_screen('[debug] ' + '\n'.join(msgs[:-1])) 238 raise FFmpegPostProcessorError(msg) 239 self.try_utime(out_path, oldest_mtime, oldest_mtime) 240 241 def run_ffmpeg(self, path, out_path, opts): 242 self.run_ffmpeg_multiple_files([path], out_path, opts) 243 244 def _ffmpeg_filename_argument(self, fn): 245 # Always use 'file:' because the filename may contain ':' (ffmpeg 246 # interprets that as a protocol) or can start with '-' (-- is broken in 247 # ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details) 248 # Also leave '-' intact in order not to break streaming to stdout. 249 return 'file:' + fn if fn != '-' else fn 250 251 252 class FFmpegExtractAudioPP(FFmpegPostProcessor): 253 def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False): 254 FFmpegPostProcessor.__init__(self, downloader) 255 if preferredcodec is None: 256 preferredcodec = 'best' 257 self._preferredcodec = preferredcodec 258 self._preferredquality = preferredquality 259 self._nopostoverwrites = nopostoverwrites 260 261 def run_ffmpeg(self, path, out_path, codec, more_opts): 262 if codec is None: 263 acodec_opts = [] 264 else: 265 acodec_opts = ['-acodec', codec] 266 opts = ['-vn'] + acodec_opts + more_opts 267 try: 268 FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts) 269 except FFmpegPostProcessorError as err: 270 raise AudioConversionError(err.msg) 271 272 def run(self, information): 273 path = information['filepath'] 274 275 filecodec = self.get_audio_codec(path) 276 if filecodec is None: 277 raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe') 278 279 more_opts = [] 280 if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'): 281 if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']: 282 # Lossless, but in another container 283 acodec = 'copy' 284 extension = 'm4a' 285 more_opts = ['-bsf:a', 'aac_adtstoasc'] 286 elif filecodec in ['aac', 'flac', 'mp3', 'vorbis', 'opus']: 287 # Lossless if possible 288 acodec = 'copy' 289 extension = filecodec 290 if filecodec == 'aac': 291 more_opts = ['-f', 'adts'] 292 if filecodec == 'vorbis': 293 extension = 'ogg' 294 else: 295 # MP3 otherwise. 296 acodec = 'libmp3lame' 297 extension = 'mp3' 298 more_opts = [] 299 if self._preferredquality is not None: 300 if int(self._preferredquality) < 10: 301 more_opts += ['-q:a', self._preferredquality] 302 else: 303 more_opts += ['-b:a', self._preferredquality + 'k'] 304 else: 305 # We convert the audio (lossy if codec is lossy) 306 acodec = ACODECS[self._preferredcodec] 307 extension = self._preferredcodec 308 more_opts = [] 309 if self._preferredquality is not None: 310 # The opus codec doesn't support the -aq option 311 if int(self._preferredquality) < 10 and extension != 'opus': 312 more_opts += ['-q:a', self._preferredquality] 313 else: 314 more_opts += ['-b:a', self._preferredquality + 'k'] 315 if self._preferredcodec == 'aac': 316 more_opts += ['-f', 'adts'] 317 if self._preferredcodec == 'm4a': 318 more_opts += ['-bsf:a', 'aac_adtstoasc'] 319 if self._preferredcodec == 'vorbis': 320 extension = 'ogg' 321 if self._preferredcodec == 'wav': 322 extension = 'wav' 323 more_opts += ['-f', 'wav'] 324 325 prefix, sep, ext = path.rpartition('.') # not os.path.splitext, since the latter does not work on unicode in all setups 326 new_path = prefix + sep + extension 327 328 information['filepath'] = new_path 329 information['ext'] = extension 330 331 # If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly. 332 if (new_path == path 333 or (self._nopostoverwrites and os.path.exists(encodeFilename(new_path)))): 334 self._downloader.to_screen('[ffmpeg] Post-process file %s exists, skipping' % new_path) 335 return [], information 336 337 try: 338 self._downloader.to_screen('[ffmpeg] Destination: ' + new_path) 339 self.run_ffmpeg(path, new_path, acodec, more_opts) 340 except AudioConversionError as e: 341 raise PostProcessingError( 342 'audio conversion failed: ' + e.msg) 343 except Exception: 344 raise PostProcessingError('error running ' + self.basename) 345 346 # Try to update the date time for extracted audio file. 347 if information.get('filetime') is not None: 348 self.try_utime( 349 new_path, time.time(), information['filetime'], 350 errnote='Cannot update utime of audio file') 351 352 return [path], information 353 354 355 class FFmpegVideoConvertorPP(FFmpegPostProcessor): 356 def __init__(self, downloader=None, preferedformat=None): 357 super(FFmpegVideoConvertorPP, self).__init__(downloader) 358 self._preferedformat = preferedformat 359 360 def run(self, information): 361 path = information['filepath'] 362 if information['ext'] == self._preferedformat: 363 self._downloader.to_screen('[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat)) 364 return [], information 365 options = [] 366 if self._preferedformat == 'avi': 367 options.extend(['-c:v', 'libxvid', '-vtag', 'XVID']) 368 prefix, sep, ext = path.rpartition('.') 369 outpath = prefix + sep + self._preferedformat 370 self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath) 371 self.run_ffmpeg(path, outpath, options) 372 information['filepath'] = outpath 373 information['format'] = self._preferedformat 374 information['ext'] = self._preferedformat 375 return [path], information 376 377 378 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor): 379 def run(self, information): 380 if information['ext'] not in ('mp4', 'webm', 'mkv'): 381 self._downloader.to_screen('[ffmpeg] Subtitles can only be embedded in mp4, webm or mkv files') 382 return [], information 383 subtitles = information.get('requested_subtitles') 384 if not subtitles: 385 self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to embed') 386 return [], information 387 388 filename = information['filepath'] 389 390 ext = information['ext'] 391 sub_langs = [] 392 sub_filenames = [] 393 webm_vtt_warn = False 394 395 for lang, sub_info in subtitles.items(): 396 sub_ext = sub_info['ext'] 397 if ext != 'webm' or ext == 'webm' and sub_ext == 'vtt': 398 sub_langs.append(lang) 399 sub_filenames.append(subtitles_filename(filename, lang, sub_ext, ext)) 400 else: 401 if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt': 402 webm_vtt_warn = True 403 self._downloader.to_screen('[ffmpeg] Only WebVTT subtitles can be embedded in webm files') 404 405 if not sub_langs: 406 return [], information 407 408 input_files = [filename] + sub_filenames 409 410 opts = [ 411 '-map', '0', 412 '-c', 'copy', 413 # Don't copy the existing subtitles, we may be running the 414 # postprocessor a second time 415 '-map', '-0:s', 416 # Don't copy Apple TV chapters track, bin_data (see #19042, #19024, 417 # https://trac.ffmpeg.org/ticket/6016) 418 '-map', '-0:d', 419 ] 420 if information['ext'] == 'mp4': 421 opts += ['-c:s', 'mov_text'] 422 for (i, lang) in enumerate(sub_langs): 423 opts.extend(['-map', '%d:0' % (i + 1)]) 424 lang_code = ISO639Utils.short2long(lang) or lang 425 opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code]) 426 427 temp_filename = prepend_extension(filename, 'temp') 428 self._downloader.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename) 429 self.run_ffmpeg_multiple_files(input_files, temp_filename, opts) 430 os.remove(encodeFilename(filename)) 431 os.rename(encodeFilename(temp_filename), encodeFilename(filename)) 432 433 return sub_filenames, information 434 435 436 class FFmpegMetadataPP(FFmpegPostProcessor): 437 def run(self, info): 438 metadata = {} 439 440 def add(meta_list, info_list=None): 441 if not info_list: 442 info_list = meta_list 443 if not isinstance(meta_list, (list, tuple)): 444 meta_list = (meta_list,) 445 if not isinstance(info_list, (list, tuple)): 446 info_list = (info_list,) 447 for info_f in info_list: 448 if info.get(info_f) is not None: 449 for meta_f in meta_list: 450 metadata[meta_f] = info[info_f] 451 break 452 453 # See [1-4] for some info on media metadata/metadata supported 454 # by ffmpeg. 455 # 1. https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/ 456 # 2. https://wiki.multimedia.cx/index.php/FFmpeg_Metadata 457 # 3. https://kodi.wiki/view/Video_file_tagging 458 # 4. http://atomicparsley.sourceforge.net/mpeg-4files.html 459 460 add('title', ('track', 'title')) 461 add('date', 'upload_date') 462 add(('description', 'comment'), 'description') 463 add('purl', 'webpage_url') 464 add('track', 'track_number') 465 add('artist', ('artist', 'creator', 'uploader', 'uploader_id')) 466 add('genre') 467 add('album') 468 add('album_artist') 469 add('disc', 'disc_number') 470 add('show', 'series') 471 add('season_number') 472 add('episode_id', ('episode', 'episode_id')) 473 add('episode_sort', 'episode_number') 474 475 if not metadata: 476 self._downloader.to_screen('[ffmpeg] There isn\'t any metadata to add') 477 return [], info 478 479 filename = info['filepath'] 480 temp_filename = prepend_extension(filename, 'temp') 481 in_filenames = [filename] 482 options = [] 483 484 if info['ext'] == 'm4a': 485 options.extend(['-vn', '-acodec', 'copy']) 486 else: 487 options.extend(['-c', 'copy']) 488 489 for (name, value) in metadata.items(): 490 options.extend(['-metadata', '%s=%s' % (name, value)]) 491 492 chapters = info.get('chapters', []) 493 if chapters: 494 metadata_filename = replace_extension(filename, 'meta') 495 with io.open(metadata_filename, 'wt', encoding='utf-8') as f: 496 def ffmpeg_escape(text): 497 return re.sub(r'(=|;|#|\\|\n)', r'\\\1', text) 498 499 metadata_file_content = ';FFMETADATA1\n' 500 for chapter in chapters: 501 metadata_file_content += '[CHAPTER]\nTIMEBASE=1/1000\n' 502 metadata_file_content += 'START=%d\n' % (chapter['start_time'] * 1000) 503 metadata_file_content += 'END=%d\n' % (chapter['end_time'] * 1000) 504 chapter_title = chapter.get('title') 505 if chapter_title: 506 metadata_file_content += 'title=%s\n' % ffmpeg_escape(chapter_title) 507 f.write(metadata_file_content) 508 in_filenames.append(metadata_filename) 509 options.extend(['-map_metadata', '1']) 510 511 self._downloader.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename) 512 self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options) 513 if chapters: 514 os.remove(metadata_filename) 515 os.remove(encodeFilename(filename)) 516 os.rename(encodeFilename(temp_filename), encodeFilename(filename)) 517 return [], info 518 519 520 class FFmpegMergerPP(FFmpegPostProcessor): 521 def run(self, info): 522 filename = info['filepath'] 523 temp_filename = prepend_extension(filename, 'temp') 524 args = ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0'] 525 self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename) 526 self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args) 527 os.rename(encodeFilename(temp_filename), encodeFilename(filename)) 528 return info['__files_to_merge'], info 529 530 def can_merge(self): 531 # TODO: figure out merge-capable ffmpeg version 532 if self.basename != 'avconv': 533 return True 534 535 required_version = '10-0' 536 if is_outdated_version( 537 self._versions[self.basename], required_version): 538 warning = ('Your copy of %s is outdated and unable to properly mux separate video and audio files, ' 539 'youtube-dl will download single file media. ' 540 'Update %s to version %s or newer to fix this.') % ( 541 self.basename, self.basename, required_version) 542 if self._downloader: 543 self._downloader.report_warning(warning) 544 return False 545 return True 546 547 548 class FFmpegFixupStretchedPP(FFmpegPostProcessor): 549 def run(self, info): 550 stretched_ratio = info.get('stretched_ratio') 551 if stretched_ratio is None or stretched_ratio == 1: 552 return [], info 553 554 filename = info['filepath'] 555 temp_filename = prepend_extension(filename, 'temp') 556 557 options = ['-c', 'copy', '-aspect', '%f' % stretched_ratio] 558 self._downloader.to_screen('[ffmpeg] Fixing aspect ratio in "%s"' % filename) 559 self.run_ffmpeg(filename, temp_filename, options) 560 561 os.remove(encodeFilename(filename)) 562 os.rename(encodeFilename(temp_filename), encodeFilename(filename)) 563 564 return [], info 565 566 567 class FFmpegFixupM4aPP(FFmpegPostProcessor): 568 def run(self, info): 569 if info.get('container') != 'm4a_dash': 570 return [], info 571 572 filename = info['filepath'] 573 temp_filename = prepend_extension(filename, 'temp') 574 575 options = ['-c', 'copy', '-f', 'mp4'] 576 self._downloader.to_screen('[ffmpeg] Correcting container in "%s"' % filename) 577 self.run_ffmpeg(filename, temp_filename, options) 578 579 os.remove(encodeFilename(filename)) 580 os.rename(encodeFilename(temp_filename), encodeFilename(filename)) 581 582 return [], info 583 584 585 class FFmpegFixupM3u8PP(FFmpegPostProcessor): 586 def run(self, info): 587 filename = info['filepath'] 588 if self.get_audio_codec(filename) == 'aac': 589 temp_filename = prepend_extension(filename, 'temp') 590 591 options = ['-c', 'copy', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc'] 592 self._downloader.to_screen('[ffmpeg] Fixing malformed AAC bitstream in "%s"' % filename) 593 self.run_ffmpeg(filename, temp_filename, options) 594 595 os.remove(encodeFilename(filename)) 596 os.rename(encodeFilename(temp_filename), encodeFilename(filename)) 597 return [], info 598 599 600 class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor): 601 def __init__(self, downloader=None, format=None): 602 super(FFmpegSubtitlesConvertorPP, self).__init__(downloader) 603 self.format = format 604 605 def run(self, info): 606 subs = info.get('requested_subtitles') 607 filename = info['filepath'] 608 new_ext = self.format 609 new_format = new_ext 610 if new_format == 'vtt': 611 new_format = 'webvtt' 612 if subs is None: 613 self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to convert') 614 return [], info 615 self._downloader.to_screen('[ffmpeg] Converting subtitles') 616 sub_filenames = [] 617 for lang, sub in subs.items(): 618 ext = sub['ext'] 619 if ext == new_ext: 620 self._downloader.to_screen( 621 '[ffmpeg] Subtitle file for %s is already in the requested format' % new_ext) 622 continue 623 old_file = subtitles_filename(filename, lang, ext, info.get('ext')) 624 sub_filenames.append(old_file) 625 new_file = subtitles_filename(filename, lang, new_ext, info.get('ext')) 626 627 if ext in ('dfxp', 'ttml', 'tt'): 628 self._downloader.report_warning( 629 'You have requested to convert dfxp (TTML) subtitles into another format, ' 630 'which results in style information loss') 631 632 dfxp_file = old_file 633 srt_file = subtitles_filename(filename, lang, 'srt', info.get('ext')) 634 635 with open(dfxp_file, 'rb') as f: 636 srt_data = dfxp2srt(f.read()) 637 638 with io.open(srt_file, 'wt', encoding='utf-8') as f: 639 f.write(srt_data) 640 old_file = srt_file 641 642 subs[lang] = { 643 'ext': 'srt', 644 'data': srt_data 645 } 646 647 if new_ext == 'srt': 648 continue 649 else: 650 sub_filenames.append(srt_file) 651 652 self.run_ffmpeg(old_file, new_file, ['-f', new_format]) 653 654 with io.open(new_file, 'rt', encoding='utf-8') as f: 655 subs[lang] = { 656 'ext': new_ext, 657 'data': f.read(), 658 } 659 660 return sub_filenames, info