]> jfr.im git - yt-dlp.git/commitdiff
Add option `--concat-playlist`
authorpukkandan <redacted>
Thu, 13 Jan 2022 11:01:08 +0000 (16:31 +0530)
committerpukkandan <redacted>
Thu, 13 Jan 2022 11:02:23 +0000 (16:32 +0530)
Closes #1855, related: #382

README.md
yt_dlp/YoutubeDL.py
yt_dlp/__init__.py
yt_dlp/options.py
yt_dlp/postprocessor/__init__.py
yt_dlp/postprocessor/ffmpeg.py
yt_dlp/utils.py

index 6ba9163bba976fee32dd806bac74e418350e7043..54b565e593582c851c74d3c1ac172ba4bbd0f336 100644 (file)
--- a/README.md
+++ b/README.md
@@ -893,6 +893,15 @@ ## Post-Processing Options:
                                      multiple times
     --xattrs                         Write metadata to the video file's xattrs
                                      (using dublin core and xdg standards)
+    --concat-playlist POLICY         Concatenate videos in a playlist. One of
+                                     "never" (default), "always", or
+                                     "multi_video" (only when the videos form a
+                                     single show). All the video files must have
+                                     same codecs and number of streams to be
+                                     concatable. The "pl_video:" prefix can be
+                                     used with "--paths" and "--output" to set
+                                     the output filename for the split files.
+                                     See "OUTPUT TEMPLATE" for details
     --fixup POLICY                   Automatically correct known faults of the
                                      file. One of never (do nothing), warn (only
                                      emit a warning), detect_or_warn (the
@@ -1106,7 +1115,7 @@ # OUTPUT TEMPLATE
 %(name[.keys][addition][>strf][,alternate][&replacement][|default])[flags][width][.precision][length]type
 ```
 
-Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`. For example, `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"`  will put the thumbnails in a folder with the same name as the video. If any of the templates (except default) is empty, that type of file will not be written. Eg: `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
+Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`, `pl_video`. For example, `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"`  will put the thumbnails in a folder with the same name as the video. If any of the templates (except default) is empty, that type of file will not be written. Eg: `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
 
 The available fields are:
 
index 71369bc44c3a3558ef24c1f8b9de59328c1a7a15..dfca76bb00a8aae024d2452db7c144035455da5e 100644 (file)
@@ -1596,6 +1596,19 @@ def _fixup(r):
     def _ensure_dir_exists(self, path):
         return make_dir(path, self.report_error)
 
+    @staticmethod
+    def _playlist_infodict(ie_result, **kwargs):
+        return {
+            **ie_result,
+            'playlist': ie_result.get('title') or ie_result.get('id'),
+            'playlist_id': ie_result.get('id'),
+            'playlist_title': ie_result.get('title'),
+            'playlist_uploader': ie_result.get('uploader'),
+            'playlist_uploader_id': ie_result.get('uploader_id'),
+            'playlist_index': 0,
+            **kwargs,
+        }
+
     def __process_playlist(self, ie_result, download):
         # We process each entry in the playlist
         playlist = ie_result.get('title') or ie_result.get('id')
@@ -1695,17 +1708,7 @@ def get_entry(i):
 
         _infojson_written = False
         if not self.params.get('simulate') and self.params.get('allow_playlist_files', True):
-            ie_copy = {
-                'playlist': playlist,
-                'playlist_id': ie_result.get('id'),
-                'playlist_title': ie_result.get('title'),
-                'playlist_uploader': ie_result.get('uploader'),
-                'playlist_uploader_id': ie_result.get('uploader_id'),
-                'playlist_index': 0,
-                'n_entries': n_entries,
-            }
-            ie_copy.update(dict(ie_result))
-
+            ie_copy = self._playlist_infodict(ie_result, n_entries=n_entries)
             _infojson_written = self._write_info_json(
                 'playlist', ie_result, self.prepare_filename(ie_copy, 'pl_infojson'))
             if _infojson_written is None:
index 85f000df4f5f0816005481a55a1b5923e4503310..f3faf0ce49213f7bd3cdd2608ba0c4f5021d71b7 100644 (file)
@@ -591,6 +591,12 @@ def report_unplayable_conflict(opt_name, arg, default=False, allowed=None):
     # XAttrMetadataPP should be run after post-processors that may change file contents
     if opts.xattrs:
         postprocessors.append({'key': 'XAttrMetadata'})
+    if opts.concat_playlist != 'never':
+        postprocessors.append({
+            'key': 'FFmpegConcat',
+            'only_multi_video': opts.concat_playlist != 'always',
+            'when': 'playlist',
+        })
     # Exec must be the last PP of each category
     if opts.exec_before_dl_cmd:
         opts.exec_cmd.setdefault('before_dl', opts.exec_before_dl_cmd)
index cc0a933be5c66cd691c9d49d053055fadeac2a8a..cb6f01d4d36eb2e0cb8b3cadd8a4ffae786fb2d9 100644 (file)
@@ -1397,6 +1397,16 @@ def _dict_from_options_callback(
         '--xattrs',
         action='store_true', dest='xattrs', default=False,
         help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)')
+    postproc.add_option(
+        '--concat-playlist',
+        metavar='POLICY', dest='concat_playlist', default='multi_video',
+        choices=('never', 'always', 'multi_video'),
+        help=(
+            'Concatenate videos in a playlist. One of "never" (default), "always", or '
+            '"multi_video" (only when the videos form a single show). '
+            'All the video files must have same codecs and number of streams to be concatable. '
+            'The "pl_video:" prefix can be used with "--paths" and "--output" to '
+            'set the output filename for the split files. See "OUTPUT TEMPLATE" for details'))
     postproc.add_option(
         '--fixup',
         metavar='POLICY', dest='fixup', default=None,
index 7f8adb3686d854b528722cb8fdafa1d13d2297cd..e411cc145c4119460db3409fe15820145dcf752a 100644 (file)
@@ -7,6 +7,7 @@
 from .exec import ExecPP, ExecAfterDownloadPP
 from .ffmpeg import (
     FFmpegPostProcessor,
+    FFmpegConcatPP,
     FFmpegEmbedSubtitlePP,
     FFmpegExtractAudioPP,
     FFmpegFixupDuplicateMoovPP,
index 43c1b276d07edf4d0a3d8fef5e2b8fe6ff6f3298..213de0ecf3397bd759a8b5dfecc1b960ec3474d6 100644 (file)
@@ -1123,3 +1123,48 @@ 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:
+            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')
+        super().concat_files(in_files, out_file)
+
+    @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')
+
+        self.concat_files(in_files, out_file)
+
+        info['requested_downloads'] = [{
+            'filepath': out_file,
+            'ext': ie_copy['ext'],
+        }]
+        return in_files, info
index 9b7f658546ba1ac07296c5116bb498c6c8031c17..b7e718028b513d1fe1b8a56ecdddf5d2d1ee3d6f 100644 (file)
@@ -4695,6 +4695,7 @@ def q(qid):
     'annotation': 'annotations.xml',
     'infojson': 'info.json',
     'link': None,
+    'pl_video': None,
     'pl_thumbnail': None,
     'pl_description': 'description',
     'pl_infojson': 'info.json',