diff options
Diffstat (limited to 'yt_dlp')
-rw-r--r-- | yt_dlp/__init__.py | 16 | ||||
-rw-r--r-- | yt_dlp/options.py | 11 | ||||
-rw-r--r-- | yt_dlp/postprocessor/__init__.py | 2 | ||||
-rw-r--r-- | yt_dlp/postprocessor/ffmpeg.py | 38 | ||||
-rw-r--r-- | yt_dlp/utils.py | 2 |
5 files changed, 64 insertions, 5 deletions
diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 55b962be1..15a006d50 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -279,9 +279,14 @@ def _real_main(argv=None): def report_conflict(arg1, arg2): write_string('WARNING: %s is ignored since %s was given\n' % (arg2, arg1), out=sys.stderr) + if opts.remuxvideo and opts.recodevideo: report_conflict('--recode-video', '--remux-video') opts.remuxvideo = False + if opts.sponskrub_cut and opts.split_chapters and opts.sponskrub is not False: + report_conflict('--split-chapter', '--sponskrub-cut') + opts.sponskrub_cut = False + if opts.allow_unplayable_formats: if opts.extractaudio: report_conflict('--allow-unplayable-formats', '--extract-audio') @@ -371,11 +376,7 @@ def _real_main(argv=None): }) if not already_have_thumbnail: opts.writethumbnail = True - # XAttrMetadataPP should be run after post-processors that may change file - # contents - if opts.xattrs: - postprocessors.append({'key': 'XAttrMetadata'}) - # This should be below all ffmpeg PP because it may cut parts out from the video + # This should be below most ffmpeg PP because it may cut parts out from the video # If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found if opts.sponskrub is not False: postprocessors.append({ @@ -386,6 +387,11 @@ def _real_main(argv=None): 'force': opts.sponskrub_force, 'ignoreerror': opts.sponskrub is None, }) + if opts.split_chapters: + postprocessors.append({'key': 'FFmpegSplitChapters'}) + # XAttrMetadataPP should be run after post-processors that may change file contents + if opts.xattrs: + postprocessors.append({'key': 'XAttrMetadata'}) # ExecAfterDownload must be the last PP if opts.exec_cmd: postprocessors.append({ diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 1e995b490..99b7db184 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -1183,6 +1183,17 @@ def parseOpts(overrideArguments=None): '--convert-subs', '--convert-subtitles', metavar='FORMAT', dest='convertsubtitles', default=None, help='Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)') + postproc.add_option( + '--split-chapters', '--split-tracks', + dest='split_chapters', action='store_true', default=False, + help=( + 'Split video into multiple files based on internal chapters. ' + 'The "chapter:" 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( + '--no-split-chapters', '--no-split-tracks', + dest='split_chapters', action='store_false', + help='Do not split video based on chapters (default)') sponskrub = optparse.OptionGroup(parser, 'SponSkrub (SponsorBlock) Options', description=( 'SponSkrub (https://github.com/yt-dlp/SponSkrub) is a utility to mark/remove sponsor segments ' diff --git a/yt_dlp/postprocessor/__init__.py b/yt_dlp/postprocessor/__init__.py index c5aa925c6..5c0679815 100644 --- a/yt_dlp/postprocessor/__init__.py +++ b/yt_dlp/postprocessor/__init__.py @@ -13,6 +13,7 @@ from .ffmpeg import ( FFmpegVideoConvertorPP, FFmpegVideoRemuxerPP, FFmpegSubtitlesConvertorPP, + FFmpegSplitChaptersPP, ) from .xattrpp import XAttrMetadataPP from .execafterdownload import ExecAfterDownloadPP @@ -31,6 +32,7 @@ __all__ = [ 'ExecAfterDownloadPP', 'FFmpegEmbedSubtitlePP', 'FFmpegExtractAudioPP', + 'FFmpegSplitChaptersPP', 'FFmpegFixupM3u8PP', 'FFmpegFixupM4aPP', 'FFmpegFixupStretchedPP', diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index a8635c1d1..7d0452dbc 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -10,6 +10,7 @@ import json from .common import AudioConversionError, PostProcessor +from ..compat import compat_str from ..utils import ( encodeArgument, encodeFilename, @@ -769,3 +770,40 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor): } return sub_filenames, info + + +class FFmpegSplitChaptersPP(FFmpegPostProcessor): + + def _prepare_filename(self, number, chapter, info): + info = info.copy() + info.update({ + 'section_number': number, + 'section_title': chapter.get('title'), + 'section_start': chapter.get('start_time'), + 'section_end': chapter.get('end_time'), + }) + return self._downloader.prepare_filename(info, 'chapter') + + def _ffmpeg_args_for_chapter(self, number, chapter, info): + destination = self._prepare_filename(number, chapter, info) + if not self._downloader._ensure_dir_exists(encodeFilename(destination)): + return + + chapter['_filename'] = destination + self.to_screen('Chapter %03d; Destination: %s' % (number, destination)) + return ( + destination, + ['-ss', compat_str(chapter['start_time']), + '-to', compat_str(chapter['end_time'])]) + + def run(self, info): + chapters = info.get('chapters') or [] + if not chapters: + self.report_warning('There are no tracks to extract') + return [], 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([(info['filepath'], opts)], [(destination, ['-c', 'copy'])]) + return [], info diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 77f8c0f4d..a913b9814 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -4182,8 +4182,10 @@ def qualities(quality_ids): DEFAULT_OUTTMPL = { 'default': '%(title)s [%(id)s].%(ext)s', + 'chapter': '%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s', } OUTTMPL_TYPES = { + 'chapter': None, 'subtitle': None, 'thumbnail': None, 'description': 'description', |