aboutsummaryrefslogtreecommitdiffstats
path: root/yt_dlp
diff options
context:
space:
mode:
Diffstat (limited to 'yt_dlp')
-rw-r--r--yt_dlp/__init__.py16
-rw-r--r--yt_dlp/options.py11
-rw-r--r--yt_dlp/postprocessor/__init__.py2
-rw-r--r--yt_dlp/postprocessor/ffmpeg.py38
-rw-r--r--yt_dlp/utils.py2
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',