mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-11-30 11:11:30 -05:00
Add option --convert-thumbnails
Closes: https://github.com/yt-dlp/yt-dlp/issues/99 https://github.com/yt-dlp/yt-dlp/issues/102
This commit is contained in:
parent
56d868dbb7
commit
8fa43c73d8
6 changed files with 124 additions and 60 deletions
38
README.md
38
README.md
|
@ -639,24 +639,24 @@ ## Post-Processing Options:
|
||||||
Specify the postprocessor/executable name
|
Specify the postprocessor/executable name
|
||||||
and the arguments separated by a colon ":"
|
and the arguments separated by a colon ":"
|
||||||
to give the argument to the specified
|
to give the argument to the specified
|
||||||
postprocessor/executable. Supported
|
postprocessor/executable. Supported PP are:
|
||||||
postprocessors are: SponSkrub,
|
Merger, ExtractAudio, SplitChapters,
|
||||||
ExtractAudio, VideoRemuxer, VideoConvertor,
|
Metadata, EmbedSubtitle, EmbedThumbnail,
|
||||||
EmbedSubtitle, Metadata, Merger,
|
SubtitlesConvertor, ThumbnailsConvertor,
|
||||||
FixupStretched, FixupM4a, FixupM3u8,
|
VideoRemuxer, VideoConvertor, SponSkrub,
|
||||||
SubtitlesConvertor, EmbedThumbnail and
|
FixupStretched, FixupM4a and FixupM3u8. The
|
||||||
SplitChapters. The supported executables
|
supported executables are: AtomicParsley,
|
||||||
are: SponSkrub, FFmpeg, FFprobe, and
|
FFmpeg, FFprobe, and SponSkrub. You can
|
||||||
AtomicParsley. You can also specify
|
also specify "PP+EXE:ARGS" to give the
|
||||||
"PP+EXE:ARGS" to give the arguments to the
|
arguments to the specified executable only
|
||||||
specified executable only when being used
|
when being used by the specified
|
||||||
by the specified postprocessor.
|
postprocessor. Additionally, for
|
||||||
Additionally, for ffmpeg/ffprobe, "_i"/"_o"
|
ffmpeg/ffprobe, "_i"/"_o" can be appended
|
||||||
can be appended to the prefix optionally
|
to the prefix optionally followed by a
|
||||||
followed by a number to pass the argument
|
number to pass the argument before the
|
||||||
before the specified input/output file. Eg:
|
specified input/output file. Eg: --ppa
|
||||||
--ppa "Merger+ffmpeg_i1:-v quiet". You can
|
"Merger+ffmpeg_i1:-v quiet". You can use
|
||||||
use this option multiple times to give
|
this option multiple times to give
|
||||||
different arguments to different
|
different arguments to different
|
||||||
postprocessors. (Alias: --ppa)
|
postprocessors. (Alias: --ppa)
|
||||||
-k, --keep-video Keep the intermediate video file on disk
|
-k, --keep-video Keep the intermediate video file on disk
|
||||||
|
@ -697,6 +697,8 @@ ## Post-Processing Options:
|
||||||
--convert-subs FORMAT Convert the subtitles to another format
|
--convert-subs FORMAT Convert the subtitles to another format
|
||||||
(currently supported: srt|ass|vtt|lrc)
|
(currently supported: srt|ass|vtt|lrc)
|
||||||
(Alias: --convert-subtitles)
|
(Alias: --convert-subtitles)
|
||||||
|
--convert-thumbnails FORMAT Convert the thumbnails to another format
|
||||||
|
(currently supported: jpg)
|
||||||
--split-chapters Split video into multiple files based on
|
--split-chapters Split video into multiple files based on
|
||||||
internal chapters. The "chapter:" prefix
|
internal chapters. The "chapter:" prefix
|
||||||
can be used with "--paths" and "--output"
|
can be used with "--paths" and "--output"
|
||||||
|
|
|
@ -230,6 +230,9 @@ def parse_retries(retries, name=''):
|
||||||
if opts.convertsubtitles is not None:
|
if opts.convertsubtitles is not None:
|
||||||
if opts.convertsubtitles not in ('srt', 'vtt', 'ass', 'lrc'):
|
if opts.convertsubtitles not in ('srt', 'vtt', 'ass', 'lrc'):
|
||||||
parser.error('invalid subtitle format specified')
|
parser.error('invalid subtitle format specified')
|
||||||
|
if opts.convertthumbnails is not None:
|
||||||
|
if opts.convertthumbnails not in ('jpg', ):
|
||||||
|
parser.error('invalid thumbnail format specified')
|
||||||
|
|
||||||
if opts.date is not None:
|
if opts.date is not None:
|
||||||
date = DateRange.day(opts.date)
|
date = DateRange.day(opts.date)
|
||||||
|
@ -332,6 +335,13 @@ def report_conflict(arg1, arg2):
|
||||||
# Run this before the actual video download
|
# Run this before the actual video download
|
||||||
'when': 'before_dl'
|
'when': 'before_dl'
|
||||||
})
|
})
|
||||||
|
if opts.convertthumbnails:
|
||||||
|
postprocessors.append({
|
||||||
|
'key': 'FFmpegThumbnailsConvertor',
|
||||||
|
'format': opts.convertthumbnails,
|
||||||
|
# Run this before the actual video download
|
||||||
|
'when': 'before_dl'
|
||||||
|
})
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'FFmpegExtractAudio',
|
'key': 'FFmpegExtractAudio',
|
||||||
|
|
|
@ -1109,10 +1109,11 @@ def _dict_from_multiple_values_options_callback(
|
||||||
help=(
|
help=(
|
||||||
'Give these arguments to the postprocessors. '
|
'Give these arguments to the postprocessors. '
|
||||||
'Specify the postprocessor/executable name and the arguments separated by a colon ":" '
|
'Specify the postprocessor/executable name and the arguments separated by a colon ":" '
|
||||||
'to give the argument to the specified postprocessor/executable. Supported postprocessors are: '
|
'to give the argument to the specified postprocessor/executable. Supported PP are: '
|
||||||
'SponSkrub, ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, '
|
'Merger, ExtractAudio, SplitChapters, Metadata, EmbedSubtitle, EmbedThumbnail, '
|
||||||
'FixupStretched, FixupM4a, FixupM3u8, SubtitlesConvertor, EmbedThumbnail and SplitChapters. '
|
'SubtitlesConvertor, ThumbnailsConvertor, VideoRemuxer, VideoConvertor, '
|
||||||
'The supported executables are: SponSkrub, FFmpeg, FFprobe, and AtomicParsley. '
|
'SponSkrub, FixupStretched, FixupM4a and FixupM3u8. '
|
||||||
|
'The supported executables are: AtomicParsley, FFmpeg, FFprobe, and SponSkrub. '
|
||||||
'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable '
|
'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable '
|
||||||
'only when being used by the specified postprocessor. Additionally, for ffmpeg/ffprobe, '
|
'only when being used by the specified postprocessor. Additionally, for ffmpeg/ffprobe, '
|
||||||
'"_i"/"_o" can be appended to the prefix optionally followed by a number to pass the argument '
|
'"_i"/"_o" can be appended to the prefix optionally followed by a number to pass the argument '
|
||||||
|
@ -1204,6 +1205,10 @@ def _dict_from_multiple_values_options_callback(
|
||||||
'--convert-subs', '--convert-sub', '--convert-subtitles',
|
'--convert-subs', '--convert-sub', '--convert-subtitles',
|
||||||
metavar='FORMAT', dest='convertsubtitles', default=None,
|
metavar='FORMAT', dest='convertsubtitles', default=None,
|
||||||
help='Convert the subtitles to another format (currently supported: srt|ass|vtt|lrc) (Alias: --convert-subtitles)')
|
help='Convert the subtitles to another format (currently supported: srt|ass|vtt|lrc) (Alias: --convert-subtitles)')
|
||||||
|
postproc.add_option(
|
||||||
|
'--convert-thumbnails',
|
||||||
|
metavar='FORMAT', dest='convertthumbnails', default=None,
|
||||||
|
help='Convert the thumbnails to another format (currently supported: jpg)')
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--split-chapters', '--split-tracks',
|
'--split-chapters', '--split-tracks',
|
||||||
dest='split_chapters', action='store_true', default=False,
|
dest='split_chapters', action='store_true', default=False,
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
FFmpegVideoConvertorPP,
|
FFmpegVideoConvertorPP,
|
||||||
FFmpegVideoRemuxerPP,
|
FFmpegVideoRemuxerPP,
|
||||||
FFmpegSubtitlesConvertorPP,
|
FFmpegSubtitlesConvertorPP,
|
||||||
|
FFmpegThumbnailsConvertorPP,
|
||||||
FFmpegSplitChaptersPP,
|
FFmpegSplitChaptersPP,
|
||||||
)
|
)
|
||||||
from .xattrpp import XAttrMetadataPP
|
from .xattrpp import XAttrMetadataPP
|
||||||
|
@ -40,6 +41,7 @@ def get_postprocessor(key):
|
||||||
'FFmpegMetadataPP',
|
'FFmpegMetadataPP',
|
||||||
'FFmpegPostProcessor',
|
'FFmpegPostProcessor',
|
||||||
'FFmpegSubtitlesConvertorPP',
|
'FFmpegSubtitlesConvertorPP',
|
||||||
|
'FFmpegThumbnailsConvertorPP',
|
||||||
'FFmpegVideoConvertorPP',
|
'FFmpegVideoConvertorPP',
|
||||||
'FFmpegVideoRemuxerPP',
|
'FFmpegVideoRemuxerPP',
|
||||||
'MetadataFromFieldPP',
|
'MetadataFromFieldPP',
|
||||||
|
|
|
@ -13,8 +13,10 @@
|
||||||
except ImportError:
|
except ImportError:
|
||||||
has_mutagen = False
|
has_mutagen = False
|
||||||
|
|
||||||
from .ffmpeg import FFmpegPostProcessor
|
from .ffmpeg import (
|
||||||
|
FFmpegPostProcessor,
|
||||||
|
FFmpegThumbnailsConvertorPP,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
check_executable,
|
check_executable,
|
||||||
encodeArgument,
|
encodeArgument,
|
||||||
|
@ -23,7 +25,6 @@
|
||||||
PostProcessingError,
|
PostProcessingError,
|
||||||
prepend_extension,
|
prepend_extension,
|
||||||
process_communicate_or_kill,
|
process_communicate_or_kill,
|
||||||
replace_extension,
|
|
||||||
shell_quote,
|
shell_quote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class EmbedThumbnailPPError(PostProcessingError):
|
||||||
class EmbedThumbnailPP(FFmpegPostProcessor):
|
class EmbedThumbnailPP(FFmpegPostProcessor):
|
||||||
|
|
||||||
def __init__(self, downloader=None, already_have_thumbnail=False):
|
def __init__(self, downloader=None, already_have_thumbnail=False):
|
||||||
super(EmbedThumbnailPP, self).__init__(downloader)
|
FFmpegPostProcessor.__init__(self, downloader)
|
||||||
self._already_have_thumbnail = already_have_thumbnail
|
self._already_have_thumbnail = already_have_thumbnail
|
||||||
|
|
||||||
def run(self, info):
|
def run(self, info):
|
||||||
|
@ -46,44 +47,21 @@ def run(self, info):
|
||||||
self.to_screen('There aren\'t any thumbnails to embed')
|
self.to_screen('There aren\'t any thumbnails to embed')
|
||||||
return [], info
|
return [], info
|
||||||
|
|
||||||
initial_thumbnail = original_thumbnail = thumbnail_filename = info['thumbnails'][-1]['filepath']
|
thumbnail_filename = info['thumbnails'][-1]['filepath']
|
||||||
|
|
||||||
if not os.path.exists(encodeFilename(thumbnail_filename)):
|
if not os.path.exists(encodeFilename(thumbnail_filename)):
|
||||||
self.report_warning('Skipping embedding the thumbnail because the file is missing.')
|
self.report_warning('Skipping embedding the thumbnail because the file is missing.')
|
||||||
return [], info
|
return [], info
|
||||||
|
|
||||||
def is_webp(path):
|
|
||||||
with open(encodeFilename(path), 'rb') as f:
|
|
||||||
b = f.read(12)
|
|
||||||
return b[0:4] == b'RIFF' and b[8:] == b'WEBP'
|
|
||||||
|
|
||||||
# Correct extension for WebP file with wrong extension (see #25687, #25717)
|
# Correct extension for WebP file with wrong extension (see #25687, #25717)
|
||||||
_, thumbnail_ext = os.path.splitext(thumbnail_filename)
|
convertor = FFmpegThumbnailsConvertorPP(self._downloader)
|
||||||
if thumbnail_ext:
|
convertor.fixup_webp(info, -1)
|
||||||
thumbnail_ext = thumbnail_ext[1:].lower()
|
|
||||||
if thumbnail_ext != 'webp' and is_webp(thumbnail_filename):
|
original_thumbnail = thumbnail_filename = info['thumbnails'][-1]['filepath']
|
||||||
self.to_screen('Correcting extension to webp and escaping path for thumbnail "%s"' % thumbnail_filename)
|
|
||||||
thumbnail_webp_filename = replace_extension(thumbnail_filename, 'webp')
|
|
||||||
if os.path.exists(thumbnail_webp_filename):
|
|
||||||
os.remove(thumbnail_webp_filename)
|
|
||||||
os.rename(encodeFilename(thumbnail_filename), encodeFilename(thumbnail_webp_filename))
|
|
||||||
original_thumbnail = thumbnail_filename = thumbnail_webp_filename
|
|
||||||
thumbnail_ext = 'webp'
|
|
||||||
|
|
||||||
# Convert unsupported thumbnail formats to JPEG (see #25687, #25717)
|
# Convert unsupported thumbnail formats to JPEG (see #25687, #25717)
|
||||||
if thumbnail_ext not in ['jpg', 'png']:
|
_, thumbnail_ext = os.path.splitext(thumbnail_filename)
|
||||||
# NB: % is supposed to be escaped with %% but this does not work
|
if thumbnail_ext not in ('jpg', 'png'):
|
||||||
# for input files so working around with standard substitution
|
thumbnail_filename = convertor.convert_thumbnail(thumbnail_filename, 'jpg')
|
||||||
escaped_thumbnail_filename = thumbnail_filename.replace('%', '#')
|
|
||||||
os.rename(encodeFilename(thumbnail_filename), encodeFilename(escaped_thumbnail_filename))
|
|
||||||
escaped_thumbnail_jpg_filename = replace_extension(escaped_thumbnail_filename, 'jpg')
|
|
||||||
self.to_screen('Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename)
|
|
||||||
self.run_ffmpeg(escaped_thumbnail_filename, escaped_thumbnail_jpg_filename, ['-bsf:v', 'mjpeg2jpeg'])
|
|
||||||
thumbnail_jpg_filename = replace_extension(thumbnail_filename, 'jpg')
|
|
||||||
# Rename back to unescaped for further processing
|
|
||||||
os.rename(encodeFilename(escaped_thumbnail_filename), encodeFilename(thumbnail_filename))
|
|
||||||
os.rename(encodeFilename(escaped_thumbnail_jpg_filename), encodeFilename(thumbnail_jpg_filename))
|
|
||||||
thumbnail_filename = thumbnail_jpg_filename
|
|
||||||
thumbnail_ext = 'jpg'
|
thumbnail_ext = 'jpg'
|
||||||
|
|
||||||
mtime = os.stat(encodeFilename(filename)).st_mtime
|
mtime = os.stat(encodeFilename(filename)).st_mtime
|
||||||
|
@ -194,9 +172,6 @@ def is_webp(path):
|
||||||
|
|
||||||
files_to_delete = [thumbnail_filename]
|
files_to_delete = [thumbnail_filename]
|
||||||
if self._already_have_thumbnail:
|
if self._already_have_thumbnail:
|
||||||
info['__files_to_move'][original_thumbnail] = replace_extension(
|
|
||||||
info['__files_to_move'][initial_thumbnail],
|
|
||||||
os.path.splitext(original_thumbnail)[1][1:])
|
|
||||||
if original_thumbnail == thumbnail_filename:
|
if original_thumbnail == thumbnail_filename:
|
||||||
files_to_delete = []
|
files_to_delete = []
|
||||||
elif original_thumbnail != thumbnail_filename:
|
elif original_thumbnail != thumbnail_filename:
|
||||||
|
|
|
@ -816,3 +816,73 @@ def run(self, info):
|
||||||
destination, opts = self._ffmpeg_args_for_chapter(idx + 1, chapter, info)
|
destination, opts = self._ffmpeg_args_for_chapter(idx + 1, chapter, info)
|
||||||
self.real_run_ffmpeg([(info['filepath'], opts)], [(destination, ['-c', 'copy'])])
|
self.real_run_ffmpeg([(info['filepath'], opts)], [(destination, ['-c', 'copy'])])
|
||||||
return [], info
|
return [], info
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor):
|
||||||
|
def __init__(self, downloader=None, format=None):
|
||||||
|
super(FFmpegThumbnailsConvertorPP, self).__init__(downloader)
|
||||||
|
self.format = format
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_webp(path):
|
||||||
|
with open(encodeFilename(path), 'rb') as f:
|
||||||
|
b = f.read(12)
|
||||||
|
return b[0:4] == b'RIFF' and b[8:] == b'WEBP'
|
||||||
|
|
||||||
|
def fixup_webp(self, info, idx=-1):
|
||||||
|
thumbnail_filename = info['thumbnails'][idx]['filepath']
|
||||||
|
_, thumbnail_ext = os.path.splitext(thumbnail_filename)
|
||||||
|
if thumbnail_ext:
|
||||||
|
thumbnail_ext = thumbnail_ext[1:].lower()
|
||||||
|
if thumbnail_ext != 'webp' and self.is_webp(thumbnail_filename):
|
||||||
|
self.to_screen('Correcting thumbnail "%s" extension to webp' % thumbnail_filename)
|
||||||
|
webp_filename = replace_extension(thumbnail_filename, 'webp')
|
||||||
|
if os.path.exists(webp_filename):
|
||||||
|
os.remove(webp_filename)
|
||||||
|
os.rename(encodeFilename(thumbnail_filename), encodeFilename(webp_filename))
|
||||||
|
info['thumbnails'][idx]['filepath'] = webp_filename
|
||||||
|
info['__files_to_move'][webp_filename] = replace_extension(
|
||||||
|
info['__files_to_move'].pop(thumbnail_filename), 'webp')
|
||||||
|
|
||||||
|
def convert_thumbnail(self, thumbnail_filename, ext):
|
||||||
|
if ext != 'jpg':
|
||||||
|
raise FFmpegPostProcessorError('Only conversion to jpg is currently supported')
|
||||||
|
# NB: % is supposed to be escaped with %% but this does not work
|
||||||
|
# for input files so working around with standard substitution
|
||||||
|
escaped_thumbnail_filename = thumbnail_filename.replace('%', '#')
|
||||||
|
os.rename(encodeFilename(thumbnail_filename), encodeFilename(escaped_thumbnail_filename))
|
||||||
|
escaped_thumbnail_jpg_filename = replace_extension(escaped_thumbnail_filename, 'jpg')
|
||||||
|
self.to_screen('Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename)
|
||||||
|
self.run_ffmpeg(escaped_thumbnail_filename, escaped_thumbnail_jpg_filename, ['-bsf:v', 'mjpeg2jpeg'])
|
||||||
|
thumbnail_jpg_filename = replace_extension(thumbnail_filename, 'jpg')
|
||||||
|
# Rename back to unescaped
|
||||||
|
os.rename(encodeFilename(escaped_thumbnail_filename), encodeFilename(thumbnail_filename))
|
||||||
|
os.rename(encodeFilename(escaped_thumbnail_jpg_filename), encodeFilename(thumbnail_jpg_filename))
|
||||||
|
return thumbnail_jpg_filename
|
||||||
|
|
||||||
|
def run(self, info):
|
||||||
|
if self.format != 'jpg':
|
||||||
|
raise FFmpegPostProcessorError('Only conversion to jpg is currently supported')
|
||||||
|
files_to_delete = []
|
||||||
|
has_thumbnail = False
|
||||||
|
|
||||||
|
for idx, thumbnail_dict in enumerate(info['thumbnails']):
|
||||||
|
if 'filepath' not in thumbnail_dict:
|
||||||
|
continue
|
||||||
|
has_thumbnail = True
|
||||||
|
self.fixup_webp(info, idx)
|
||||||
|
original_thumbnail = thumbnail_dict['filepath']
|
||||||
|
_, thumbnail_ext = os.path.splitext(original_thumbnail)
|
||||||
|
if thumbnail_ext:
|
||||||
|
thumbnail_ext = thumbnail_ext[1:].lower()
|
||||||
|
if thumbnail_ext == self.format:
|
||||||
|
self.to_screen('Thumbnail "%s" is already in the requested format' % original_thumbnail)
|
||||||
|
continue
|
||||||
|
thumbnail_dict['filepath'] = self.convert_thumbnail(original_thumbnail, self.format)
|
||||||
|
files_to_delete.append(original_thumbnail)
|
||||||
|
info['__files_to_move'][thumbnail_dict['filepath']] = replace_extension(
|
||||||
|
info['__files_to_move'][original_thumbnail], self.format)
|
||||||
|
|
||||||
|
if not has_thumbnail:
|
||||||
|
self.to_screen('There aren\'t any thumbnails to convert')
|
||||||
|
return files_to_delete, info
|
||||||
|
|
Loading…
Reference in a new issue