From 00bbc5f17710367adc7508062e155547b35edd20 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Sun, 5 Jun 2022 19:58:56 +0530 Subject: [PATCH] [ThumbnailsConvertor] Allow conditional conversion Closes #3970 --- README.md | 4 ++- yt_dlp/__init__.py | 14 +++++----- yt_dlp/options.py | 3 +- yt_dlp/postprocessor/ffmpeg.py | 51 ++++++++++++++++++++-------------- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 87986e4c3..86f172a64 100644 --- a/README.md +++ b/README.md @@ -985,7 +985,9 @@ You can also fork the project on github and run your fork's [build workflow](.gi (currently supported: srt, vtt, ass, lrc) (Alias: --convert-subtitles) --convert-thumbnails FORMAT Convert the thumbnails to another format - (currently supported: jpg, png, webp) + (currently supported: jpg, png, webp). You + can specify multiple rules using similar + syntax as --remux-video --split-chapters Split video into multiple files based on internal chapters. The "chapter:" prefix can be used with "--paths" and "--output" to set diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index d0abc395a..d1b78303e 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -215,13 +215,13 @@ def validate_options(opts): # Postprocessor formats validate_in('audio format', opts.audioformat, ['best'] + list(FFmpegExtractAudioPP.SUPPORTED_EXTS)) validate_in('subtitle format', opts.convertsubtitles, FFmpegSubtitlesConvertorPP.SUPPORTED_EXTS) - validate_in('thumbnail format', opts.convertthumbnails, FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS) - if opts.recodevideo is not None: - opts.recodevideo = opts.recodevideo.replace(' ', '') - validate_regex('video recode format', opts.recodevideo, FFmpegVideoConvertorPP.FORMAT_RE) - if opts.remuxvideo is not None: - opts.remuxvideo = opts.remuxvideo.replace(' ', '') - validate_regex('video remux format', opts.remuxvideo, FFmpegVideoRemuxerPP.FORMAT_RE) + for name, value, pp in ( + ('thumbnail format', opts.convertthumbnails, FFmpegThumbnailsConvertorPP), + ('recode video format', opts.recodevideo, FFmpegVideoConvertorPP), + ('remux video format', opts.remuxvideo, FFmpegVideoRemuxerPP), + ): + if value is not None: + validate_regex(name, value.replace(' ', ''), pp.FORMAT_RE) if opts.audioquality: opts.audioquality = opts.audioquality.strip('k').strip('K') # int_or_none prevents inf, nan diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 7cffcecfa..b326e885f 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -1610,7 +1610,8 @@ def create_parser(): metavar='FORMAT', dest='convertthumbnails', default=None, help=( 'Convert the thumbnails to another format ' - '(currently supported: %s) ' % ', '.join(FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS))) + f'(currently supported: {", ".join(FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS)}). ' + 'You can specify multiple rules using similar syntax as --remux-video')) postproc.add_option( '--split-chapters', '--split-tracks', dest='split_chapters', action='store_true', default=False, diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index dad8b7f8f..3777703eb 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -56,6 +56,25 @@ ACODECS = { } +def create_mapping_re(supported): + return re.compile(r'{0}(?:/{0})*$'.format(r'(?:\w+>)?(?:%s)' % '|'.join(supported))) + + +def resolve_mapping(source, mapping): + """ + Get corresponding item from a mapping string like 'A>B/C>D/E' + @returns (target, error_message) + """ + for pair in mapping.lower().split('/'): + kv = pair.split('>', 1) + if len(kv) == 1 or kv[0].strip() == source: + target = kv[-1].strip() + if target == source: + return target, f'already is in target format {source}' + return target, None + return None, f'could not find a mapping for {source}' + + class FFmpegPostProcessorError(PostProcessingError): pass @@ -542,18 +561,12 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor): class FFmpegVideoConvertorPP(FFmpegPostProcessor): SUPPORTED_EXTS = ('mp4', 'mkv', 'flv', 'webm', 'mov', 'avi', 'mka', 'ogg', *FFmpegExtractAudioPP.SUPPORTED_EXTS) - FORMAT_RE = re.compile(r'{0}(?:/{0})*$'.format(r'(?:\w+>)?(?:%s)' % '|'.join(SUPPORTED_EXTS))) + FORMAT_RE = create_mapping_re(SUPPORTED_EXTS) _ACTION = 'converting' def __init__(self, downloader=None, preferedformat=None): super().__init__(downloader) - self._preferedformats = preferedformat.lower().split('/') - - def _target_ext(self, source_ext): - for pair in self._preferedformats: - kv = pair.split('>') - if len(kv) == 1 or kv[0].strip() == source_ext: - return kv[-1].strip() + self.mapping = preferedformat @staticmethod def _options(target_ext): @@ -564,11 +577,7 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor): @PostProcessor._restrict_to(images=False) def run(self, info): filename, source_ext = info['filepath'], info['ext'].lower() - target_ext = self._target_ext(source_ext) - _skip_msg = ( - f'could not find a mapping for {source_ext}' if not target_ext - else f'already is in target format {source_ext}' if source_ext == target_ext - else None) + target_ext, _skip_msg = resolve_mapping(source_ext, self.mapping) if _skip_msg: self.to_screen(f'Not {self._ACTION} media file "{filename}"; {_skip_msg}') return [], info @@ -1068,10 +1077,11 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor): class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor): SUPPORTED_EXTS = ('jpg', 'png', 'webp') + FORMAT_RE = create_mapping_re(SUPPORTED_EXTS) def __init__(self, downloader=None, format=None): super().__init__(downloader) - self.format = format + self.mapping = format @classmethod def is_webp(cls, path): @@ -1115,18 +1125,17 @@ class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor): continue has_thumbnail = True self.fixup_webp(info, idx) - _, thumbnail_ext = os.path.splitext(original_thumbnail) - if thumbnail_ext: - thumbnail_ext = thumbnail_ext[1:].lower() + thumbnail_ext = os.path.splitext(original_thumbnail)[1][1:].lower() if thumbnail_ext == 'jpeg': thumbnail_ext = 'jpg' - if thumbnail_ext == self.format: - self.to_screen('Thumbnail "%s" is already in the requested format' % original_thumbnail) + target_ext, _skip_msg = resolve_mapping(thumbnail_ext, self.mapping) + if _skip_msg: + self.to_screen(f'Not converting thumbnail "{original_thumbnail}"; {_skip_msg}') continue - thumbnail_dict['filepath'] = self.convert_thumbnail(original_thumbnail, self.format) + thumbnail_dict['filepath'] = self.convert_thumbnail(original_thumbnail, target_ext) files_to_delete.append(original_thumbnail) info['__files_to_move'][thumbnail_dict['filepath']] = replace_extension( - info['__files_to_move'][original_thumbnail], self.format) + info['__files_to_move'][original_thumbnail], target_ext) if not has_thumbnail: self.to_screen('There aren\'t any thumbnails to convert')