diff --git a/README.md b/README.md index 18817d30..c77b2f6c 100644 --- a/README.md +++ b/README.md @@ -94,9 +94,6 @@ presets: thumbnail_name: "{episode_name}.jpg" maintain_download_archive: True - convert_thumbnail: - to: "jpg" - nfo_tags: nfo_name: "{episode_name}.nfo" nfo_root: "episodedetails" diff --git a/config.yaml b/config.yaml index f67a4975..4cd4de9f 100644 --- a/config.yaml +++ b/config.yaml @@ -14,8 +14,6 @@ presets: file_name: "[{album_year}] {album_sanitized}/{track_number_padded} - {title_sanitized}.{ext}" thumbnail_name: "[{album_year}] {album_sanitized}/folder.jpg" maintain_download_archive: True - convert_thumbnail: - to: "jpg" music_tags: tags: artist: "{artist}" @@ -47,11 +45,6 @@ presets: file_name: "{music_video_name}.{ext}" thumbnail_name: "{music_video_name}.jpg" - # Always convert the video thumbnail to jpg - # TODO: always convert all thumbnails to jpg in code - convert_thumbnail: - to: "jpg" - # For each video downloaded, add a music video NFO file for it. Populate it # with tags that Kodi will read and use to display it in the music or music # videos section. @@ -101,8 +94,6 @@ presets: output_options: file_name: "{output_file_name}.{ext}" thumbnail_name: "{output_file_name}.jpg" - convert_thumbnail: - to: "jpg" nfo_tags: nfo_name: "{output_file_name}.nfo" nfo_root: "episodedetails" diff --git a/docker/root/defaults/config.yaml b/docker/root/defaults/config.yaml index b4681772..e7b4c6b0 100644 --- a/docker/root/defaults/config.yaml +++ b/docker/root/defaults/config.yaml @@ -21,9 +21,6 @@ presets: file_name: "{music_video_name}.{ext}" thumbnail_name: "{music_video_name}.jpg" - convert_thumbnail: - to: "jpg" - nfo_tags: nfo_name: "{music_video_name}.nfo" nfo_root: "musicvideo" diff --git a/examples/kodi_music_videos_config.yaml b/examples/kodi_music_videos_config.yaml index 521fdf76..589d3290 100644 --- a/examples/kodi_music_videos_config.yaml +++ b/examples/kodi_music_videos_config.yaml @@ -37,11 +37,6 @@ presets: file_name: "{music_video_name}.{ext}" thumbnail_name: "{music_video_name}.jpg" - # Always convert the video thumbnail to jpg - # TODO: always convert all thumbnails to jpg in code - convert_thumbnail: - to: "jpg" - # For each video downloaded, add a music video NFO file for it. Populate it # with tags that Kodi will read and use to display it in the music or music # videos section. diff --git a/examples/kodi_tv_shows_config.yaml b/examples/kodi_tv_shows_config.yaml index d0092009..6bcb4db8 100644 --- a/examples/kodi_tv_shows_config.yaml +++ b/examples/kodi_tv_shows_config.yaml @@ -55,11 +55,6 @@ presets: thumbnail_name: "{episode_name}.jpg" maintain_download_archive: True - # Always convert the video thumbnail to jpg - # TODO: always convert all thumbnails to jpg in code - convert_thumbnail: - to: "jpg" - # For each video downloaded, add an episode NFO file for it. We give it # the same date-like episode name using our {episode_name} variable, and # populate the NFO with available video metadata that Kodi will recognize. diff --git a/examples/soundcloud_discography_config.yaml b/examples/soundcloud_discography_config.yaml index dfdb6704..1bc061fb 100644 --- a/examples/soundcloud_discography_config.yaml +++ b/examples/soundcloud_discography_config.yaml @@ -48,11 +48,6 @@ presets: thumbnail_name: "{album_directory_name}/folder.jpg" maintain_download_archive: True - # Always convert the video thumbnail to jpg - # TODO: always convert all thumbnails to jpg in code - convert_thumbnail: - to: "jpg" - # For each song downloaded, populate the audio file with music tags. # Tagging should work with most audio file formats. See # https://ytdl-sub.readthedocs.io/en/latest/config.html#music-tags diff --git a/src/ytdl_sub/config/preset_class_mappings.py b/src/ytdl_sub/config/preset_class_mappings.py index 958fa957..04606b36 100644 --- a/src/ytdl_sub/config/preset_class_mappings.py +++ b/src/ytdl_sub/config/preset_class_mappings.py @@ -7,7 +7,6 @@ from ytdl_sub.downloaders.soundcloud_downloader import SoundcloudAlbumsAndSingle from ytdl_sub.downloaders.youtube_downloader import YoutubeChannelDownloader from ytdl_sub.downloaders.youtube_downloader import YoutubePlaylistDownloader from ytdl_sub.downloaders.youtube_downloader import YoutubeVideoDownloader -from ytdl_sub.plugins.convert_thumbnail import ConvertThumbnailPlugin 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 @@ -101,7 +100,6 @@ class PluginMapping: """ _MAPPING: Dict[str, Type[Plugin]] = { - "convert_thumbnail": ConvertThumbnailPlugin, "music_tags": MusicTagsPlugin, "nfo_tags": NfoTagsPlugin, "output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin, diff --git a/src/ytdl_sub/downloaders/youtube_downloader.py b/src/ytdl_sub/downloaders/youtube_downloader.py index 694f20f5..267a7e67 100644 --- a/src/ytdl_sub/downloaders/youtube_downloader.py +++ b/src/ytdl_sub/downloaders/youtube_downloader.py @@ -5,10 +5,6 @@ from typing import Generic from typing import List from typing import Optional from typing import TypeVar -from urllib.request import urlopen - -from PIL.Image import Image -from PIL.Image import open as pil_open from ytdl_sub.config.preset_options import Overrides from ytdl_sub.downloaders.downloader import Downloader @@ -18,6 +14,7 @@ from ytdl_sub.entries.youtube import YoutubeChannel from ytdl_sub.entries.youtube import YoutubePlaylistVideo from ytdl_sub.entries.youtube import YoutubeVideo from ytdl_sub.utils.logger import Logger +from ytdl_sub.utils.thumbnail import convert_url_thumbnail from ytdl_sub.validators.date_range_validator import DateRangeValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.validators import StringValidator @@ -312,10 +309,9 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions logger.warning("Could not find a thumbnail for %s", self.channel.uid) return - with urlopen(thumbnail_url) as file: - image: Image = pil_open(file).convert("RGB") - - image.save(fp=output_thumbnail_path, format="jpeg") + convert_url_thumbnail( + thumbnail_url=thumbnail_url, output_thumbnail_path=output_thumbnail_path + ) def post_download(self, overrides: Overrides, output_directory: str): """ diff --git a/src/ytdl_sub/entries/variables/entry_variables.py b/src/ytdl_sub/entries/variables/entry_variables.py index d9999d95..c2f19df4 100644 --- a/src/ytdl_sub/entries/variables/entry_variables.py +++ b/src/ytdl_sub/entries/variables/entry_variables.py @@ -82,10 +82,10 @@ class EntryVariables(SourceVariables): """ Returns ------- - The download entry's thumbnail extension + The download entry's thumbnail extension. Will always return 'jpg'. Until there is a need + to support other image types, we always convert to jpg. """ - # TODO: Always return jpg - return self.kwargs("thumbnail").split(".")[-1] + return "jpg" @property def upload_date(self: BaseEntry) -> str: diff --git a/src/ytdl_sub/entries/youtube.py b/src/ytdl_sub/entries/youtube.py index 8f6b65d9..b7e9a86c 100644 --- a/src/ytdl_sub/entries/youtube.py +++ b/src/ytdl_sub/entries/youtube.py @@ -1,5 +1,3 @@ -import os.path -from pathlib import Path from typing import Optional from ytdl_sub.entries.entry import Entry @@ -8,31 +6,9 @@ from ytdl_sub.entries.variables.youtube_variables import YoutubeVideoVariables class YoutubeVideo(YoutubeVideoVariables, Entry): """ - Entry object to represent a Youtube video. + Entry object to represent a Youtube video. Reserved for shared Youtube entry logic. """ - def get_download_thumbnail_path(self) -> str: - """ - Returns - ------- - The entry's thumbnail's file path to where it was downloaded. Ytdl has a bug where the - thumbnail entry is not the actual thumbnail it downloaded. So check all the possible - extensions that it may have downloaded, and see if that thumbnail's extension exists. - - TODO: always make thumbnails jpg - """ - thumbnails = self.kwargs("thumbnails") - possible_thumbnail_exts = set() - for thumbnail in thumbnails: - possible_thumbnail_exts.add(thumbnail["url"].split(".")[-1]) - - for ext in possible_thumbnail_exts: - possible_thumbnail_path = str(Path(self.working_directory()) / f"{self.uid}.{ext}") - if os.path.isfile(possible_thumbnail_path): - return possible_thumbnail_path - - return super().get_download_thumbnail_path() - class YoutubePlaylistVideo(YoutubeVideo): @property diff --git a/src/ytdl_sub/plugins/convert_thumbnail.py b/src/ytdl_sub/plugins/convert_thumbnail.py deleted file mode 100644 index 288902ee..00000000 --- a/src/ytdl_sub/plugins/convert_thumbnail.py +++ /dev/null @@ -1,44 +0,0 @@ -import os - -from PIL.Image import Image -from PIL.Image import open as pil_open - -from ytdl_sub.entries.entry import Entry -from ytdl_sub.plugins.plugin import Plugin -from ytdl_sub.plugins.plugin import PluginOptions -from ytdl_sub.validators.string_select_validator import StringSelectValidator - - -class ThumbnailTypes(StringSelectValidator): - """Valid image types that thumbnails can be converted to""" - - _select_values = {"jpg"} - - -class ConvertThumbnailOptions(PluginOptions): - _required_keys = {"to"} - - def __init__(self, name, value): - super().__init__(name, value) - self.convert_to = self._validate_key(key="to", validator=ThumbnailTypes) - - -class ConvertThumbnailPlugin(Plugin[ConvertThumbnailOptions]): - plugin_options_type = ConvertThumbnailOptions - - def post_process_entry(self, entry: Entry): - """ - Convert the source thumbnail to the desired type. Leave the original file name and - extension intact to let output_options deal with renaming it. - """ - if not os.path.isfile(entry.get_download_thumbnail_path()): - raise ValueError("Thumbnail not found") - - image: Image = pil_open(entry.get_download_thumbnail_path()).convert("RGB") - - # Pillow likes the formal 'jpeg' name and not 'jpg' - thumbnail_format = self.plugin_options.convert_to.value - if thumbnail_format == "jpg": - thumbnail_format = "jpeg" - - image.save(fp=entry.get_download_thumbnail_path(), format=thumbnail_format) diff --git a/src/ytdl_sub/subscriptions/subscription.py b/src/ytdl_sub/subscriptions/subscription.py index e461c89a..f7a0157e 100644 --- a/src/ytdl_sub/subscriptions/subscription.py +++ b/src/ytdl_sub/subscriptions/subscription.py @@ -18,6 +18,7 @@ from ytdl_sub.downloaders.downloader import DownloaderValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.utils.thumbnail import convert_download_thumbnail from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive @@ -178,6 +179,10 @@ class Subscription: 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 + convert_download_thumbnail(entry=entry) + self._copy_file_to_output_directory( entry=entry, source_file_path=entry.get_download_thumbnail_path(), diff --git a/src/ytdl_sub/utils/thumbnail.py b/src/ytdl_sub/utils/thumbnail.py new file mode 100644 index 00000000..7ac7cb14 --- /dev/null +++ b/src/ytdl_sub/utils/thumbnail.py @@ -0,0 +1,66 @@ +import os +from pathlib import Path +from typing import Optional +from urllib.request import urlopen + +from PIL.Image import Image +from PIL.Image import open as pil_open + +from ytdl_sub.entries.entry import Entry + + +def _get_downloaded_thumbnail_path(entry: Entry) -> Optional[str]: + thumbnails = entry.kwargs("thumbnails") + possible_thumbnail_exts = set() + + # The source `thumbnail` value and the actual downloaded thumbnail extension sometimes do + # not match. Find all possible extensions by checking all available thumbnails. + for thumbnail in thumbnails: + possible_thumbnail_exts.add(thumbnail["url"].split(".")[-1]) + + for ext in possible_thumbnail_exts: + possible_thumbnail_path = str(Path(entry.working_directory()) / f"{entry.uid}.{ext}") + if os.path.isfile(possible_thumbnail_path): + return possible_thumbnail_path + + return None + + +def convert_download_thumbnail(entry: Entry): + """ + Converts an entry's downloaded thumbnail into jpg format + + Parameters + ---------- + entry + Entry with the thumbnail + + Raises + ------ + ValueError + Entry thumbnail file not found + """ + download_thumbnail_path = _get_downloaded_thumbnail_path(entry=entry) + download_thumbnail_path_as_jpg = entry.get_download_thumbnail_path() + if not download_thumbnail_path: + raise ValueError("Thumbnail not found") + + image: Image = pil_open(download_thumbnail_path).convert("RGB") + image.save(fp=download_thumbnail_path_as_jpg, format="jpeg") + + +def convert_url_thumbnail(thumbnail_url: str, output_thumbnail_path: str): + """ + Downloads and converts a thumbnail from a url into a jpg + + Parameters + ---------- + thumbnail_url + URL of the thumbnail + output_thumbnail_path + Thumbnail file destination after its converted to jpg + """ + with urlopen(thumbnail_url) as file: + image: Image = pil_open(file).convert("RGB") + + image.save(fp=output_thumbnail_path, format="jpeg") diff --git a/tests/unit/entries/conftest.py b/tests/unit/entries/conftest.py index e24a9aef..31e1a0d8 100644 --- a/tests/unit/entries/conftest.py +++ b/tests/unit/entries/conftest.py @@ -57,7 +57,7 @@ def ext(): @pytest.fixture def thumbnail_ext(): - return "yall" + return "jpg" @pytest.fixture