Always convert thumbnails to jpg (#51)
* always use jpg * fix unit test * update readme and configs
This commit is contained in:
parent
a6cf9c3a11
commit
8b336e6fb0
14 changed files with 80 additions and 113 deletions
|
|
@ -94,9 +94,6 @@ presets:
|
||||||
thumbnail_name: "{episode_name}.jpg"
|
thumbnail_name: "{episode_name}.jpg"
|
||||||
maintain_download_archive: True
|
maintain_download_archive: True
|
||||||
|
|
||||||
convert_thumbnail:
|
|
||||||
to: "jpg"
|
|
||||||
|
|
||||||
nfo_tags:
|
nfo_tags:
|
||||||
nfo_name: "{episode_name}.nfo"
|
nfo_name: "{episode_name}.nfo"
|
||||||
nfo_root: "episodedetails"
|
nfo_root: "episodedetails"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ presets:
|
||||||
file_name: "[{album_year}] {album_sanitized}/{track_number_padded} - {title_sanitized}.{ext}"
|
file_name: "[{album_year}] {album_sanitized}/{track_number_padded} - {title_sanitized}.{ext}"
|
||||||
thumbnail_name: "[{album_year}] {album_sanitized}/folder.jpg"
|
thumbnail_name: "[{album_year}] {album_sanitized}/folder.jpg"
|
||||||
maintain_download_archive: True
|
maintain_download_archive: True
|
||||||
convert_thumbnail:
|
|
||||||
to: "jpg"
|
|
||||||
music_tags:
|
music_tags:
|
||||||
tags:
|
tags:
|
||||||
artist: "{artist}"
|
artist: "{artist}"
|
||||||
|
|
@ -47,11 +45,6 @@ presets:
|
||||||
file_name: "{music_video_name}.{ext}"
|
file_name: "{music_video_name}.{ext}"
|
||||||
thumbnail_name: "{music_video_name}.jpg"
|
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
|
# 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
|
# with tags that Kodi will read and use to display it in the music or music
|
||||||
# videos section.
|
# videos section.
|
||||||
|
|
@ -101,8 +94,6 @@ presets:
|
||||||
output_options:
|
output_options:
|
||||||
file_name: "{output_file_name}.{ext}"
|
file_name: "{output_file_name}.{ext}"
|
||||||
thumbnail_name: "{output_file_name}.jpg"
|
thumbnail_name: "{output_file_name}.jpg"
|
||||||
convert_thumbnail:
|
|
||||||
to: "jpg"
|
|
||||||
nfo_tags:
|
nfo_tags:
|
||||||
nfo_name: "{output_file_name}.nfo"
|
nfo_name: "{output_file_name}.nfo"
|
||||||
nfo_root: "episodedetails"
|
nfo_root: "episodedetails"
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,6 @@ presets:
|
||||||
file_name: "{music_video_name}.{ext}"
|
file_name: "{music_video_name}.{ext}"
|
||||||
thumbnail_name: "{music_video_name}.jpg"
|
thumbnail_name: "{music_video_name}.jpg"
|
||||||
|
|
||||||
convert_thumbnail:
|
|
||||||
to: "jpg"
|
|
||||||
|
|
||||||
nfo_tags:
|
nfo_tags:
|
||||||
nfo_name: "{music_video_name}.nfo"
|
nfo_name: "{music_video_name}.nfo"
|
||||||
nfo_root: "musicvideo"
|
nfo_root: "musicvideo"
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,6 @@ presets:
|
||||||
file_name: "{music_video_name}.{ext}"
|
file_name: "{music_video_name}.{ext}"
|
||||||
thumbnail_name: "{music_video_name}.jpg"
|
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
|
# 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
|
# with tags that Kodi will read and use to display it in the music or music
|
||||||
# videos section.
|
# videos section.
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,6 @@ presets:
|
||||||
thumbnail_name: "{episode_name}.jpg"
|
thumbnail_name: "{episode_name}.jpg"
|
||||||
maintain_download_archive: True
|
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
|
# 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
|
# the same date-like episode name using our {episode_name} variable, and
|
||||||
# populate the NFO with available video metadata that Kodi will recognize.
|
# populate the NFO with available video metadata that Kodi will recognize.
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,6 @@ presets:
|
||||||
thumbnail_name: "{album_directory_name}/folder.jpg"
|
thumbnail_name: "{album_directory_name}/folder.jpg"
|
||||||
maintain_download_archive: True
|
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.
|
# For each song downloaded, populate the audio file with music tags.
|
||||||
# Tagging should work with most audio file formats. See
|
# Tagging should work with most audio file formats. See
|
||||||
# https://ytdl-sub.readthedocs.io/en/latest/config.html#music-tags
|
# https://ytdl-sub.readthedocs.io/en/latest/config.html#music-tags
|
||||||
|
|
|
||||||
|
|
@ -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 YoutubeChannelDownloader
|
||||||
from ytdl_sub.downloaders.youtube_downloader import YoutubePlaylistDownloader
|
from ytdl_sub.downloaders.youtube_downloader import YoutubePlaylistDownloader
|
||||||
from ytdl_sub.downloaders.youtube_downloader import YoutubeVideoDownloader
|
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.music_tags import MusicTagsPlugin
|
||||||
from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin
|
from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin
|
||||||
from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin
|
from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin
|
||||||
|
|
@ -101,7 +100,6 @@ class PluginMapping:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_MAPPING: Dict[str, Type[Plugin]] = {
|
_MAPPING: Dict[str, Type[Plugin]] = {
|
||||||
"convert_thumbnail": ConvertThumbnailPlugin,
|
|
||||||
"music_tags": MusicTagsPlugin,
|
"music_tags": MusicTagsPlugin,
|
||||||
"nfo_tags": NfoTagsPlugin,
|
"nfo_tags": NfoTagsPlugin,
|
||||||
"output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin,
|
"output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,6 @@ from typing import Generic
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TypeVar
|
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.config.preset_options import Overrides
|
||||||
from ytdl_sub.downloaders.downloader import Downloader
|
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 YoutubePlaylistVideo
|
||||||
from ytdl_sub.entries.youtube import YoutubeVideo
|
from ytdl_sub.entries.youtube import YoutubeVideo
|
||||||
from ytdl_sub.utils.logger import Logger
|
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.date_range_validator import DateRangeValidator
|
||||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||||
from ytdl_sub.validators.validators import StringValidator
|
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)
|
logger.warning("Could not find a thumbnail for %s", self.channel.uid)
|
||||||
return
|
return
|
||||||
|
|
||||||
with urlopen(thumbnail_url) as file:
|
convert_url_thumbnail(
|
||||||
image: Image = pil_open(file).convert("RGB")
|
thumbnail_url=thumbnail_url, output_thumbnail_path=output_thumbnail_path
|
||||||
|
)
|
||||||
image.save(fp=output_thumbnail_path, format="jpeg")
|
|
||||||
|
|
||||||
def post_download(self, overrides: Overrides, output_directory: str):
|
def post_download(self, overrides: Overrides, output_directory: str):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,10 @@ class EntryVariables(SourceVariables):
|
||||||
"""
|
"""
|
||||||
Returns
|
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 "jpg"
|
||||||
return self.kwargs("thumbnail").split(".")[-1]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def upload_date(self: BaseEntry) -> str:
|
def upload_date(self: BaseEntry) -> str:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import os.path
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
|
|
@ -8,31 +6,9 @@ from ytdl_sub.entries.variables.youtube_variables import YoutubeVideoVariables
|
||||||
|
|
||||||
class YoutubeVideo(YoutubeVideoVariables, Entry):
|
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):
|
class YoutubePlaylistVideo(YoutubeVideo):
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -18,6 +18,7 @@ from ytdl_sub.downloaders.downloader import DownloaderValidator
|
||||||
from ytdl_sub.entries.entry import Entry
|
from ytdl_sub.entries.entry import Entry
|
||||||
from ytdl_sub.plugins.plugin import Plugin
|
from ytdl_sub.plugins.plugin import Plugin
|
||||||
from ytdl_sub.plugins.plugin import PluginOptions
|
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
|
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -178,6 +179,10 @@ class Subscription:
|
||||||
output_thumbnail_name = self.overrides.apply_formatter(
|
output_thumbnail_name = self.overrides.apply_formatter(
|
||||||
formatter=self.output_options.thumbnail_name, entry=entry
|
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(
|
self._copy_file_to_output_directory(
|
||||||
entry=entry,
|
entry=entry,
|
||||||
source_file_path=entry.get_download_thumbnail_path(),
|
source_file_path=entry.get_download_thumbnail_path(),
|
||||||
|
|
|
||||||
66
src/ytdl_sub/utils/thumbnail.py
Normal file
66
src/ytdl_sub/utils/thumbnail.py
Normal file
|
|
@ -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")
|
||||||
|
|
@ -57,7 +57,7 @@ def ext():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def thumbnail_ext():
|
def thumbnail_ext():
|
||||||
return "yall"
|
return "jpg"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue