From 701358b880ade6f2a601875618d4e442249bbb4b Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Wed, 18 Jun 2025 21:49:25 -0700 Subject: [PATCH] [FEATURE] `square_thumbnail` plugin --- src/ytdl_sub/config/plugin/plugin_mapping.py | 3 + src/ytdl_sub/plugins/square_thumbnail.py | 77 ++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/ytdl_sub/plugins/square_thumbnail.py diff --git a/src/ytdl_sub/config/plugin/plugin_mapping.py b/src/ytdl_sub/config/plugin/plugin_mapping.py index b52bb776..ac1b851c 100644 --- a/src/ytdl_sub/config/plugin/plugin_mapping.py +++ b/src/ytdl_sub/config/plugin/plugin_mapping.py @@ -24,6 +24,7 @@ from ytdl_sub.plugins.music_tags import MusicTagsPlugin from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin from ytdl_sub.plugins.split_by_chapters import SplitByChaptersPlugin +from ytdl_sub.plugins.square_thumbnail import SquareThumbnailPlugin from ytdl_sub.plugins.static_nfo_tags import StaticNfoTagsPlugin from ytdl_sub.plugins.subtitles import SubtitlesPlugin from ytdl_sub.plugins.throttle_protection import ThrottleProtectionPlugin @@ -40,6 +41,7 @@ class PluginMapping: "audio_extract": AudioExtractPlugin, "date_range": DateRangePlugin, "embed_thumbnail": EmbedThumbnailPlugin, + "square_thumbnail": SquareThumbnailPlugin, "file_convert": FileConvertPlugin, "format": FormatPlugin, "match_filters": MatchFiltersPlugin, @@ -86,6 +88,7 @@ class PluginMapping: VideoTagsPlugin, NfoTagsPlugin, StaticNfoTagsPlugin, + SquareThumbnailPlugin, EmbedThumbnailPlugin, ] diff --git a/src/ytdl_sub/plugins/square_thumbnail.py b/src/ytdl_sub/plugins/square_thumbnail.py new file mode 100644 index 00000000..5a34c32c --- /dev/null +++ b/src/ytdl_sub/plugins/square_thumbnail.py @@ -0,0 +1,77 @@ +from typing import List +from typing import Optional + +from ytdl_sub.config.plugin.plugin import Plugin +from ytdl_sub.config.validators.options import OptionsValidator +from ytdl_sub.entries.entry import Entry +from ytdl_sub.utils.ffmpeg import FFMPEG +from ytdl_sub.utils.file_handler import FileHandler +from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.logger import Logger +from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator + +logger = Logger.get("square-thumbnail") + + +class SquareThumbnailOptions(OverridesBooleanFormatterValidator, OptionsValidator): + """ + Whether to make thumbnails square. Supports both file and embedded-based thumbnails. Ideal + for representing audio albums. + + :Usage: + + .. code-block:: yaml + + square_thumbnail: True + """ + + +class SquareThumbnailPlugin(Plugin[SquareThumbnailOptions]): + plugin_options_type = SquareThumbnailOptions + + @property + def _square_thumbnail(self) -> bool: + return self.overrides.evaluate_boolean(self.plugin_options) + + @classmethod + def _convert_to_square_thumbnail(cls, entry: Entry) -> None: + thumbnail_path = entry.get_download_thumbnail_path() + tmp_file_path = FFMPEG.tmp_file_path(thumbnail_path) + try: + ffmpeg_args: List[str] = [ + "-i", + thumbnail_path, + "-c:v" + "mjpeg" + "-qmin" + "1" + "-qscale:v" + "1" + "-vf" + "crop=min(iw\\,ih):min(iw\\,ih)" + "-bitexact", # for reproducibility + tmp_file_path, + ] + FFMPEG.run(ffmpeg_args) + FileHandler.move(tmp_file_path, thumbnail_path) + finally: + FileHandler.delete(tmp_file_path) + + def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]: + """ + Maybe make the thumbnail square + """ + if not self._square_thumbnail: + return None + + if not self.is_dry_run: + if not entry.is_thumbnail_downloaded(): + logger.warning( + "Cannot make a square thumbnail for '%s' because it is not available", + entry.title, + ) + return None + + self._convert_to_square_thumbnail(entry) + + return FileMetadata("Square thumbnail")