diff --git a/src/ytdl_sub/downloaders/url/downloader.py b/src/ytdl_sub/downloaders/url/downloader.py index d66749af..49b64f7d 100644 --- a/src/ytdl_sub/downloaders/url/downloader.py +++ b/src/ytdl_sub/downloaders/url/downloader.py @@ -34,8 +34,8 @@ from ytdl_sub.entries.variables.kwargs import YTDL_SUB_MATCH_FILTER_REJECT from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.thumbnail import ThumbnailTypes -from ytdl_sub.utils.thumbnail import convert_download_thumbnail from ytdl_sub.utils.thumbnail import download_and_convert_url_thumbnail +from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive download_logger = Logger.get(name="downloader") @@ -82,8 +82,6 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension): # If latest entry, always update the thumbnail on each entry if thumbnail_id == ThumbnailTypes.LATEST_ENTRY: - # Make sure the entry's thumbnail is converted to jpg - convert_download_thumbnail(entry, error_if_not_found=False) # always save in dry-run even if it doesn't exist... if self.is_dry_run or os.path.isfile(entry.get_download_thumbnail_path()): @@ -138,8 +136,14 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension): def modify_entry(self, entry: Entry) -> Optional[Entry]: """ - Use the entry to download thumbnails (or move if LATEST_ENTRY) + Use the entry to download thumbnails (or move if LATEST_ENTRY). + In addition, convert the entry thumbnail to jpg """ + # We always convert entry thumbnails to jpgs, and is performed here to be done + # as early as possible in the plugin pipeline (downstream plugins depend on it being jpg) + if not self.is_dry_run: + try_convert_download_thumbnail(entry=entry) + if entry.kwargs_get(COLLECTION_URL) in self._collection_url_mapping: self._download_url_thumbnails( collection_url=self._collection_url_mapping[entry.kwargs(COLLECTION_URL)], diff --git a/src/ytdl_sub/downloaders/ytdlp.py b/src/ytdl_sub/downloaders/ytdlp.py index d66f0c80..a7912232 100644 --- a/src/ytdl_sub/downloaders/ytdlp.py +++ b/src/ytdl_sub/downloaders/ytdlp.py @@ -86,10 +86,12 @@ class YTDLP: If the entry fails to download """ num_tries = 0 - entry_files_exist = False copied_ytdl_options_overrides = copy.deepcopy(ytdl_options_overrides) - while not entry_files_exist and num_tries < cls._EXTRACT_ENTRY_NUM_RETRIES: + is_downloaded = False + entry_dict: Optional[Dict] = None + + while num_tries < cls._EXTRACT_ENTRY_NUM_RETRIES: entry_dict = cls.extract_info( ytdl_options_overrides=copied_ytdl_options_overrides, **kwargs ) @@ -115,7 +117,7 @@ class YTDLP: time.sleep(cls._EXTRACT_ENTRY_RETRY_WAIT_SEC) num_tries += 1 - # Remove the download archive so it can retry without thinking its already downloaded, + # Remove the download archive to retry without thinking its already downloaded, # even though it is not if "download_archive" in copied_ytdl_options_overrides: del copied_ytdl_options_overrides["download_archive"] @@ -127,6 +129,10 @@ class YTDLP: cls._EXTRACT_ENTRY_NUM_RETRIES, ) + # Still return if the media file downloaded (thumbnail could be missing) + if is_downloaded and entry_dict is not None: + return entry_dict + error_dict = {"ytdl_options": ytdl_options_overrides, "kwargs": kwargs} raise FileNotDownloadedException( f"yt-dlp failed to download an entry with these arguments: {error_dict}" diff --git a/src/ytdl_sub/entries/entry.py b/src/ytdl_sub/entries/entry.py index 08d2285d..2d19f2fc 100644 --- a/src/ytdl_sub/entries/entry.py +++ b/src/ytdl_sub/entries/entry.py @@ -92,6 +92,15 @@ class Entry(EntryVariables, BaseEntry): """ return self.get_ytdlp_download_thumbnail_path() is not None + @final + def is_thumbnail_available(self) -> bool: + """ + Returns + ------- + True if the thumbnail file exists and is its proper format. False otherwise. + """ + return os.path.isfile(self.get_download_thumbnail_path()) + @final def is_downloaded(self) -> bool: """ diff --git a/src/ytdl_sub/plugins/embed_thumbnail.py b/src/ytdl_sub/plugins/embed_thumbnail.py index 45dbd776..512f7790 100644 --- a/src/ytdl_sub/plugins/embed_thumbnail.py +++ b/src/ytdl_sub/plugins/embed_thumbnail.py @@ -11,7 +11,6 @@ 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.utils.thumbnail import convert_download_thumbnail from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS from ytdl_sub.validators.validators import BoolValidator @@ -90,10 +89,12 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]): logger.warning("webm does not support embedded thumbnails, skipping") return None - if not self.is_dry_run: - # convert the entry thumbnail so it is embedded as jpg - convert_download_thumbnail(entry=entry) + if not entry.is_thumbnail_downloaded(): + logger.warning( + "Cannot embed thumbnail for '%s' because it is not available", entry.title + ) + if not self.is_dry_run: if entry.ext in AUDIO_CODEC_EXTS: self._embed_audio_file(entry) else: diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py index 7dcbfdaa..955591b6 100644 --- a/src/ytdl_sub/plugins/music_tags.py +++ b/src/ytdl_sub/plugins/music_tags.py @@ -11,7 +11,6 @@ from ytdl_sub.config.preset_options import OptionsDictValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.thumbnail import convert_download_thumbnail from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator @@ -162,11 +161,7 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]): ) setattr(audio_file, tag_name, tag_value[0]) - if self.plugin_options.embed_thumbnail: - - # convert the entry thumbnail so it is embedded as jpg - convert_download_thumbnail(entry=entry) - + if self.plugin_options.embed_thumbnail and entry.is_thumbnail_available(): with open(entry.get_download_thumbnail_path(), "rb") as thumb: mediafile_img = mediafile.Image( data=thumb.read(), desc="cover", type=mediafile.ImageType.front diff --git a/src/ytdl_sub/plugins/split_by_chapters.py b/src/ytdl_sub/plugins/split_by_chapters.py index f684f921..f6eef699 100644 --- a/src/ytdl_sub/plugins/split_by_chapters.py +++ b/src/ytdl_sub/plugins/split_by_chapters.py @@ -1,5 +1,4 @@ import copy -import os.path from pathlib import Path from typing import Any from typing import List @@ -21,7 +20,6 @@ from ytdl_sub.utils.exceptions import ValidationException 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.thumbnail import convert_download_thumbnail from ytdl_sub.validators.string_select_validator import StringSelectValidator @@ -184,11 +182,6 @@ class SplitByChaptersPlugin(SplitPlugin[SplitByChaptersOptions]): f"Tried to split '{entry.title}' by chapters but it has no chapters" ) - # convert the entry thumbnail early so we do not have to guess the thumbnail extension - # when copying it. Do not error if it's not found, in case thumbnail_name is not set - if not self.is_dry_run: - convert_download_thumbnail(entry=entry, error_if_not_found=False) - for idx, title in enumerate(chapters.titles): new_uid = _split_video_uid(source_uid=entry.uid, idx=idx) @@ -209,7 +202,7 @@ class SplitByChaptersPlugin(SplitPlugin[SplitByChaptersOptions]): # Copy the original vid thumbnail to the working directory with the new uid. This so # downstream logic thinks this split video has its own thumbnail - if os.path.isfile(entry.get_download_thumbnail_path()): + if entry.is_thumbnail_available(): FileHandler.copy( src_file_path=entry.get_download_thumbnail_path(), dst_file_path=Path(self.working_directory) diff --git a/src/ytdl_sub/subscriptions/subscription_download.py b/src/ytdl_sub/subscriptions/subscription_download.py index b0de508d..97a96019 100644 --- a/src/ytdl_sub/subscriptions/subscription_download.py +++ b/src/ytdl_sub/subscriptions/subscription_download.py @@ -21,7 +21,6 @@ from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandlerTransactionLog from ytdl_sub.utils.file_handler import FileMetadata -from ytdl_sub.utils.thumbnail import convert_download_thumbnail def _get_split_plugin(plugins: List[Plugin]) -> Optional[SplitPlugin]: @@ -69,16 +68,11 @@ class SubscriptionDownload(BaseSubscription, ABC): entry=entry, ) - # TODO: see if entry even has a thumbnail - if self.output_options.thumbnail_name: + if self.output_options.thumbnail_name and entry.is_thumbnail_available(): output_thumbnail_name = self.overrides.apply_formatter( formatter=self.output_options.thumbnail_name, entry=entry ) - # We always convert entry thumbnails to jpgs, and is performed here - if not dry_run: - convert_download_thumbnail(entry=entry) - # Copy the thumbnails since they could be used later for other things self._enhanced_download_archive.save_file_to_output_directory( file_name=entry.get_download_thumbnail_name(), diff --git a/src/ytdl_sub/utils/thumbnail.py b/src/ytdl_sub/utils/thumbnail.py index 73168f32..a9aa5243 100644 --- a/src/ytdl_sub/utils/thumbnail.py +++ b/src/ytdl_sub/utils/thumbnail.py @@ -1,11 +1,14 @@ +import logging import os import tempfile +from subprocess import CalledProcessError from typing import Optional from urllib.request import urlopen 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.logger import Logger from ytdl_sub.utils.retry import retry @@ -13,21 +16,18 @@ class ThumbnailTypes: LATEST_ENTRY = "latest_entry" -def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> None: +logger: logging.Logger = Logger.get("thumbnail") + + +def try_convert_download_thumbnail(entry: Entry) -> None: """ - Converts an entry's downloaded thumbnail into jpg format + Converts an entry's downloaded thumbnail into jpg format. + Log with a warning if the thumbnail is not found or fails to convert Parameters ---------- entry Entry with the thumbnail - error_if_not_found - If the thumbnail is not found, error if True. - - Raises - ------ - ValueError - Entry thumbnail file not found """ download_thumbnail_path = entry.get_ytdlp_download_thumbnail_path() download_thumbnail_path_as_jpg = entry.get_download_thumbnail_path() @@ -37,15 +37,17 @@ def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> return if not download_thumbnail_path: - if error_if_not_found: - raise ValueError("Thumbnail not found") - return + logger.warning("Thumbnail for '%s' was not downloaded", entry.title) if not download_thumbnail_path == download_thumbnail_path_as_jpg: - FFMPEG.run( - ["-y", "-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg] - ) - FileHandler.delete(download_thumbnail_path) + try: + FFMPEG.run( + ["-y", "-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg] + ) + except CalledProcessError: + logger.warning("Failed to convert thumbnail for '%s' to jpg", entry.title) + finally: + FileHandler.delete(download_thumbnail_path) @retry(times=3, exceptions=(Exception,))