]> jfr.im git - yt-dlp.git/blobdiff - yt_dlp/postprocessor/ffmpeg.py
[cleanup] Minor fixes
[yt-dlp.git] / yt_dlp / postprocessor / ffmpeg.py
index 97f04d11663564e22f165c576a0c676c19cd6cf2..5b98c7d9764e996816106007732af6e8e6d50d42 100644 (file)
@@ -13,6 +13,7 @@
 
 from ..compat import compat_str
 from ..utils import (
+    determine_ext,
     dfxp2srt,
     encodeArgument,
     encodeFilename,
@@ -191,6 +192,18 @@ def probe_available(self):
     def probe_executable(self):
         return self._paths[self.probe_basename]
 
+    @staticmethod
+    def stream_copy_opts(copy=True, *, ext=None):
+        yield from ('-map', '0')
+        # Don't copy Apple TV chapters track, bin_data
+        # See https://github.com/yt-dlp/yt-dlp/issues/2, #19042, #19024, https://trac.ffmpeg.org/ticket/6016
+        yield from ('-dn', '-ignore_unknown')
+        if copy:
+            yield from ('-c', 'copy')
+        # For some reason, '-c copy -map 0' is not enough to copy subtitles
+        if ext in ('mp4', 'mov'):
+            yield from ('-c:s', 'mov_text')
+
     def get_audio_codec(self, path):
         if not self.probe_available and not self.available:
             raise PostProcessingError('ffprobe and ffmpeg not found. Please install or provide the path using --ffmpeg-location')
@@ -352,8 +365,9 @@ def force_keyframes(self, filename, timestamps):
             timestamps = timestamps[1:]
         keyframe_file = prepend_extension(filename, 'keyframes.temp')
         self.to_screen(f'Re-encoding "{filename}" with appropriate keyframes')
-        self.run_ffmpeg(filename, keyframe_file, ['-force_key_frames', ','.join(
-            f'{t:.6f}' for t in timestamps)])
+        self.run_ffmpeg(filename, keyframe_file, [
+            *self.stream_copy_opts(False, ext=determine_ext(filename)),
+            '-force_key_frames', ','.join(f'{t:.6f}' for t in timestamps)])
         return keyframe_file
 
     def concat_files(self, in_files, out_file, concat_opts=None):
@@ -368,10 +382,7 @@ def concat_files(self, in_files, out_file, concat_opts=None):
         with open(concat_file, 'wt', encoding='utf-8') as f:
             f.writelines(self._concat_spec(in_files, concat_opts))
 
-        out_flags = ['-c', 'copy']
-        if out_file.rpartition('.')[-1] in ('mp4', 'mov'):
-            # For some reason, '-c copy' is not enough to copy subtitles
-            out_flags.extend(['-c:s', 'mov_text'])
+        out_flags = list(self.stream_copy_opts(ext=determine_ext(out_file)))
 
         try:
             self.real_run_ffmpeg(
@@ -557,7 +568,7 @@ def run(self, info):
             else f'already is in target format {source_ext}' if source_ext == target_ext
             else None)
         if _skip_msg:
-            self.to_screen(f'Not {self._ACTION} media file {filename!r}; {_skip_msg}')
+            self.to_screen(f'Not {self._ACTION} media file "{filename}"; {_skip_msg}')
             return [], info
 
         outpath = replace_extension(filename, target_ext, source_ext)
@@ -574,7 +585,7 @@ class FFmpegVideoRemuxerPP(FFmpegVideoConvertorPP):
 
     @staticmethod
     def _options(target_ext):
-        return ['-c', 'copy', '-map', '0', '-dn']
+        return FFmpegPostProcessor.stream_copy_opts()
 
 
 class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
@@ -634,16 +645,11 @@ def run(self, info):
         input_files = [filename] + sub_filenames
 
         opts = [
-            '-c', 'copy', '-map', '0', '-dn',
+            *self.stream_copy_opts(ext=info['ext']),
             # Don't copy the existing subtitles, we may be running the
             # postprocessor a second time
             '-map', '-0:s',
-            # Don't copy Apple TV chapters track, bin_data (see #19042, #19024,
-            # https://trac.ffmpeg.org/ticket/6016)
-            '-map', '-0:d',
         ]
-        if info['ext'] == 'mp4':
-            opts += ['-c:s', 'mov_text']
         for i, (lang, name) in enumerate(zip(sub_langs, sub_names)):
             opts.extend(['-map', '%d:0' % (i + 1)])
             lang_code = ISO639Utils.short2long(lang) or lang
@@ -671,11 +677,10 @@ def __init__(self, downloader, add_metadata=True, add_chapters=True, add_infojso
 
     @staticmethod
     def _options(target_ext):
-        yield from ('-map', '0', '-dn')
-        if target_ext == 'm4a':
+        audio_only = target_ext == 'm4a'
+        yield from FFmpegPostProcessor.stream_copy_opts(not audio_only)
+        if audio_only:
             yield from ('-vn', '-acodec', 'copy')
-        else:
-            yield from ('-c', 'copy')
 
     @PostProcessor._restrict_to(images=False)
     def run(self, info):
@@ -775,7 +780,7 @@ def add(meta_list, info_list=None):
         stream_idx = 0
         for fmt in info.get('requested_formats') or []:
             stream_count = 2 if 'none' not in (fmt.get('vcodec'), fmt.get('acodec')) else 1
-            lang = ISO639Utils.short2long(fmt['language']) or fmt.get('language')
+            lang = ISO639Utils.short2long(fmt.get('language') or '') or fmt.get('language')
             for i in range(stream_idx, stream_idx + stream_count):
                 if lang:
                     metadata[str(i)].setdefault('language', lang)
@@ -859,7 +864,7 @@ def run(self, info):
         stretched_ratio = info.get('stretched_ratio')
         if stretched_ratio not in (None, 1):
             self._fixup('Fixing aspect ratio', info['filepath'], [
-                '-c', 'copy', '-map', '0', '-dn', '-aspect', '%f' % stretched_ratio])
+                *self.stream_copy_opts(), '-aspect', '%f' % stretched_ratio])
         return [], info
 
 
@@ -867,8 +872,7 @@ class FFmpegFixupM4aPP(FFmpegFixupPostProcessor):
     @PostProcessor._restrict_to(images=False, video=False)
     def run(self, info):
         if info.get('container') == 'm4a_dash':
-            self._fixup('Correcting container', info['filepath'], [
-                '-c', 'copy', '-map', '0', '-dn', '-f', 'mp4'])
+            self._fixup('Correcting container', info['filepath'], [*self.stream_copy_opts(), '-f', 'mp4'])
         return [], info
 
 
@@ -888,7 +892,7 @@ def _needs_fixup(self, info):
     def run(self, info):
         if all(self._needs_fixup(info)):
             self._fixup('Fixing MPEG-TS in MP4 container', info['filepath'], [
-                '-c', 'copy', '-map', '0', '-dn', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc'])
+                *self.stream_copy_opts(), '-f', 'mp4', '-bsf:a', 'aac_adtstoasc'])
         return [], info
 
 
@@ -909,24 +913,24 @@ def run(self, info):
             opts = ['-vf', 'setpts=PTS-STARTPTS']
         else:
             opts = ['-c', 'copy', '-bsf', 'setts=ts=TS-STARTPTS']
-        self._fixup('Fixing frame timestamp', info['filepath'], opts + ['-map', '0', '-dn', '-ss', self.trim])
+        self._fixup('Fixing frame timestamp', info['filepath'], opts + [*self.stream_copy_opts(False), '-ss', self.trim])
         return [], info
 
 
-class FFmpegCopyStreamPostProcessor(FFmpegFixupPostProcessor):
+class FFmpegCopyStreamPP(FFmpegFixupPostProcessor):
     MESSAGE = 'Copying stream'
 
     @PostProcessor._restrict_to(images=False)
     def run(self, info):
-        self._fixup(self.MESSAGE, info['filepath'], ['-c', 'copy', '-map', '0', '-dn'])
+        self._fixup(self.MESSAGE, info['filepath'], self.stream_copy_opts())
         return [], info
 
 
-class FFmpegFixupDurationPP(FFmpegCopyStreamPostProcessor):
+class FFmpegFixupDurationPP(FFmpegCopyStreamPP):
     MESSAGE = 'Fixing video duration'
 
 
-class FFmpegFixupDuplicateMoovPP(FFmpegCopyStreamPostProcessor):
+class FFmpegFixupDuplicateMoovPP(FFmpegCopyStreamPP):
     MESSAGE = 'Fixing duplicate MOOV atoms'
 
 
@@ -1046,7 +1050,7 @@ def run(self, info):
         self.to_screen('Splitting video by chapters; %d chapters found' % len(chapters))
         for idx, chapter in enumerate(chapters):
             destination, opts = self._ffmpeg_args_for_chapter(idx + 1, chapter, info)
-            self.real_run_ffmpeg([(in_file, opts)], [(destination, ['-c', 'copy'])])
+            self.real_run_ffmpeg([(in_file, opts)], [(destination, self.stream_copy_opts())])
         if in_file != info['filepath']:
             os.remove(in_file)
         return [], info
@@ -1119,3 +1123,53 @@ def run(self, info):
         if not has_thumbnail:
             self.to_screen('There aren\'t any thumbnails to convert')
         return files_to_delete, info
+
+
+class FFmpegConcatPP(FFmpegPostProcessor):
+    def __init__(self, downloader, only_multi_video=False):
+        self._only_multi_video = only_multi_video
+        super().__init__(downloader)
+
+    def concat_files(self, in_files, out_file):
+        if len(in_files) == 1:
+            if os.path.realpath(in_files[0]) != os.path.realpath(out_file):
+                self.to_screen(f'Moving "{in_files[0]}" to "{out_file}"')
+            os.replace(in_files[0], out_file)
+            return []
+
+        codecs = [traverse_obj(self.get_metadata_object(file), ('streams', ..., 'codec_name')) for file in in_files]
+        if len(set(map(tuple, codecs))) > 1:
+            raise PostProcessingError(
+                'The files have different streams/codecs and cannot be concatenated. '
+                'Either select different formats or --recode-video them to a common format')
+
+        self.to_screen(f'Concatenating {len(in_files)} files; Destination: {out_file}')
+        super().concat_files(in_files, out_file)
+        return in_files
+
+    @PostProcessor._restrict_to(images=False)
+    def run(self, info):
+        if not info.get('entries') or self._only_multi_video and info['_type'] != 'multi_video':
+            return [], info
+        elif None in info['entries']:
+            raise PostProcessingError('Aborting concatenation because some downloads failed')
+        elif any(len(entry) > 1 for entry in traverse_obj(info, ('entries', ..., 'requested_downloads')) or []):
+            raise PostProcessingError('Concatenation is not supported when downloading multiple separate formats')
+
+        in_files = traverse_obj(info, ('entries', ..., 'requested_downloads', 0, 'filepath'))
+        if not in_files:
+            self.to_screen('There are no files to concatenate')
+            return [], info
+
+        ie_copy = self._downloader._playlist_infodict(info)
+        exts = [traverse_obj(entry, ('requested_downloads', 0, 'ext'), 'ext') for entry in info['entries']]
+        ie_copy['ext'] = exts[0] if len(set(exts)) == 1 else 'mkv'
+        out_file = self._downloader.prepare_filename(ie_copy, 'pl_video')
+
+        files_to_delete = self.concat_files(in_files, out_file)
+
+        info['requested_downloads'] = [{
+            'filepath': out_file,
+            'ext': ie_copy['ext'],
+        }]
+        return files_to_delete, info