Add option --concat-playlist

Closes #1855, related: #382
This commit is contained in:
pukkandan 2022-01-13 16:31:08 +05:30
parent 5df1ac92bd
commit 3b603dbdf1
No known key found for this signature in database
GPG key ID: 0F00D95A001F4698
7 changed files with 87 additions and 12 deletions

View file

@ -893,6 +893,15 @@ You can also fork the project on github and run your fork's [build workflow](.gi
multiple times multiple times
--xattrs Write metadata to the video file's xattrs --xattrs Write metadata to the video file's xattrs
(using dublin core and xdg standards) (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 --fixup POLICY Automatically correct known faults of the
file. One of never (do nothing), warn (only file. One of never (do nothing), warn (only
emit a warning), detect_or_warn (the emit a warning), detect_or_warn (the
@ -1106,7 +1115,7 @@ To summarize, the general syntax for a field is:
%(name[.keys][addition][>strf][,alternate][&replacement][|default])[flags][width][.precision][length]type %(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: The available fields are:

View file

@ -1596,6 +1596,19 @@ class YoutubeDL(object):
def _ensure_dir_exists(self, path): def _ensure_dir_exists(self, path):
return make_dir(path, self.report_error) 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): def __process_playlist(self, ie_result, download):
# We process each entry in the playlist # We process each entry in the playlist
playlist = ie_result.get('title') or ie_result.get('id') playlist = ie_result.get('title') or ie_result.get('id')
@ -1695,17 +1708,7 @@ class YoutubeDL(object):
_infojson_written = False _infojson_written = False
if not self.params.get('simulate') and self.params.get('allow_playlist_files', True): if not self.params.get('simulate') and self.params.get('allow_playlist_files', True):
ie_copy = { ie_copy = self._playlist_infodict(ie_result, n_entries=n_entries)
'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))
_infojson_written = self._write_info_json( _infojson_written = self._write_info_json(
'playlist', ie_result, self.prepare_filename(ie_copy, 'pl_infojson')) 'playlist', ie_result, self.prepare_filename(ie_copy, 'pl_infojson'))
if _infojson_written is None: if _infojson_written is None:

View file

@ -591,6 +591,12 @@ def _real_main(argv=None):
# XAttrMetadataPP should be run after post-processors that may change file contents # XAttrMetadataPP should be run after post-processors that may change file contents
if opts.xattrs: if opts.xattrs:
postprocessors.append({'key': 'XAttrMetadata'}) 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 # Exec must be the last PP of each category
if opts.exec_before_dl_cmd: if opts.exec_before_dl_cmd:
opts.exec_cmd.setdefault('before_dl', opts.exec_before_dl_cmd) opts.exec_cmd.setdefault('before_dl', opts.exec_before_dl_cmd)

View file

@ -1397,6 +1397,16 @@ def create_parser():
'--xattrs', '--xattrs',
action='store_true', dest='xattrs', default=False, action='store_true', dest='xattrs', default=False,
help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)') 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( postproc.add_option(
'--fixup', '--fixup',
metavar='POLICY', dest='fixup', default=None, metavar='POLICY', dest='fixup', default=None,

View file

@ -7,6 +7,7 @@ from .embedthumbnail import EmbedThumbnailPP
from .exec import ExecPP, ExecAfterDownloadPP from .exec import ExecPP, ExecAfterDownloadPP
from .ffmpeg import ( from .ffmpeg import (
FFmpegPostProcessor, FFmpegPostProcessor,
FFmpegConcatPP,
FFmpegEmbedSubtitlePP, FFmpegEmbedSubtitlePP,
FFmpegExtractAudioPP, FFmpegExtractAudioPP,
FFmpegFixupDuplicateMoovPP, FFmpegFixupDuplicateMoovPP,

View file

@ -1123,3 +1123,48 @@ class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor):
if not has_thumbnail: if not has_thumbnail:
self.to_screen('There aren\'t any thumbnails to convert') self.to_screen('There aren\'t any thumbnails to convert')
return files_to_delete, info 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

View file

@ -4695,6 +4695,7 @@ OUTTMPL_TYPES = {
'annotation': 'annotations.xml', 'annotation': 'annotations.xml',
'infojson': 'info.json', 'infojson': 'info.json',
'link': None, 'link': None,
'pl_video': None,
'pl_thumbnail': None, 'pl_thumbnail': None,
'pl_description': 'description', 'pl_description': 'description',
'pl_infojson': 'info.json', 'pl_infojson': 'info.json',