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:
pukkandan 2021-04-11 03:48:52 +05:30
parent 56d868dbb7
commit 8fa43c73d8
No known key found for this signature in database
GPG Key ID: 0F00D95A001F4698
6 changed files with 124 additions and 60 deletions

View File

@ -639,24 +639,24 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
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 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
--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"

View File

@ -230,6 +230,9 @@ def _real_main(argv=None):
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 _real_main(argv=None):
# 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',

View File

@ -1109,10 +1109,11 @@ def parseOpts(overrideArguments=None):
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 parseOpts(overrideArguments=None):
'--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,

View File

@ -13,6 +13,7 @@ from .ffmpeg import (
FFmpegVideoConvertorPP, FFmpegVideoConvertorPP,
FFmpegVideoRemuxerPP, FFmpegVideoRemuxerPP,
FFmpegSubtitlesConvertorPP, FFmpegSubtitlesConvertorPP,
FFmpegThumbnailsConvertorPP,
FFmpegSplitChaptersPP, FFmpegSplitChaptersPP,
) )
from .xattrpp import XAttrMetadataPP from .xattrpp import XAttrMetadataPP
@ -40,6 +41,7 @@ __all__ = [
'FFmpegMetadataPP', 'FFmpegMetadataPP',
'FFmpegPostProcessor', 'FFmpegPostProcessor',
'FFmpegSubtitlesConvertorPP', 'FFmpegSubtitlesConvertorPP',
'FFmpegThumbnailsConvertorPP',
'FFmpegVideoConvertorPP', 'FFmpegVideoConvertorPP',
'FFmpegVideoRemuxerPP', 'FFmpegVideoRemuxerPP',
'MetadataFromFieldPP', 'MetadataFromFieldPP',

View File

@ -13,8 +13,10 @@ try:
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 @@ from ..utils import (
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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
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:

View File

@ -816,3 +816,73 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor):
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