[FEATURE] Add playlist thumbnail support (#250)
This commit is contained in:
parent
a970fb9d06
commit
839192059b
9 changed files with 86 additions and 10 deletions
|
|
@ -39,6 +39,7 @@ from ytdl_sub.utils.exceptions import FileNotDownloadedException
|
|||
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 ThumbnailTypes
|
||||
from ytdl_sub.utils.thumbnail import convert_url_thumbnail
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
|
||||
|
|
@ -518,6 +519,7 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
|
|||
thumbnails_downloaded: Set[str],
|
||||
thumbnail_list_info: CollectionThumbnailListValidator,
|
||||
entry: Entry,
|
||||
is_last_entry: bool,
|
||||
parent: EntryParent,
|
||||
) -> Set[str]:
|
||||
"""
|
||||
|
|
@ -527,10 +529,22 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
|
|||
thumbnail_name = self.overrides.apply_formatter(thumbnail_info.name, entry=entry)
|
||||
thumbnail_id = self.overrides.apply_formatter(thumbnail_info.uid)
|
||||
|
||||
# alread downloaded
|
||||
# already downloaded
|
||||
if thumbnail_name in thumbnails_downloaded:
|
||||
continue
|
||||
|
||||
if thumbnail_id == ThumbnailTypes.LATEST_ENTRY:
|
||||
# always save in dry-run even if it doesn't exist...
|
||||
if is_last_entry and (
|
||||
self.is_dry_run or os.path.isfile(entry.get_download_thumbnail_path())
|
||||
):
|
||||
self.save_file(
|
||||
file_name=entry.get_download_thumbnail_name(),
|
||||
output_file_name=thumbnail_name,
|
||||
)
|
||||
thumbnails_downloaded.add(thumbnail_name)
|
||||
continue
|
||||
|
||||
if (thumbnail_url := parent.get_thumbnail_url(thumbnail_id=thumbnail_id)) is None:
|
||||
download_logger.warning("TODO: Failed to download channel's avatar image")
|
||||
continue
|
||||
|
|
@ -556,12 +570,15 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
|
|||
"""
|
||||
thumbnails_downloaded: Set[str] = set()
|
||||
|
||||
for entry in entries:
|
||||
for idx, entry in enumerate(entries):
|
||||
is_last_entry = idx == len(entries) - 1
|
||||
|
||||
if entry.kwargs_contains(PLAYLIST_ENTRY):
|
||||
thumbnails_downloaded = self._download_parent_thumbnails(
|
||||
thumbnails_downloaded=thumbnails_downloaded,
|
||||
thumbnail_list_info=collection_url.playlist_thumbnails,
|
||||
entry=entry,
|
||||
is_last_entry=is_last_entry,
|
||||
parent=EntryParent(
|
||||
entry.kwargs(PLAYLIST_ENTRY), working_directory=self.working_directory
|
||||
),
|
||||
|
|
@ -572,6 +589,7 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
|
|||
thumbnails_downloaded=thumbnails_downloaded,
|
||||
thumbnail_list_info=collection_url.source_thumbnails,
|
||||
entry=entry,
|
||||
is_last_entry=is_last_entry,
|
||||
parent=EntryParent(
|
||||
entry.kwargs(SOURCE_ENTRY), working_directory=self.working_directory
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.downloaders.downloader import Downloader
|
||||
from ytdl_sub.downloaders.downloader import DownloaderValidator
|
||||
from ytdl_sub.downloaders.generic.collection_validator import CollectionValidator
|
||||
from ytdl_sub.utils.thumbnail import ThumbnailTypes
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.url_validator import YoutubePlaylistUrlValidator
|
||||
|
||||
|
||||
|
|
@ -23,21 +27,38 @@ class YoutubePlaylistDownloaderOptions(DownloaderValidator):
|
|||
"""
|
||||
|
||||
_required_keys = {"playlist_url"}
|
||||
_optional_keys = {"playlist_thumbnail_name"}
|
||||
|
||||
def __init__(self, name, value):
|
||||
super().__init__(name, value)
|
||||
self._playlist_url = self._validate_key(
|
||||
"playlist_url", YoutubePlaylistUrlValidator
|
||||
).playlist_url
|
||||
self._playlist_thumbnail_name = self._validate_key_if_present(
|
||||
"playlist_thumbnail_name", OverridesStringFormatterValidator
|
||||
)
|
||||
|
||||
@property
|
||||
def collection_validator(self) -> CollectionValidator:
|
||||
"""Downloads the playlist url"""
|
||||
playlist_thumbnails: List[Dict] = []
|
||||
if self.playlist_thumbnail_path:
|
||||
playlist_thumbnails.append(
|
||||
{
|
||||
"name": self.playlist_thumbnail_path.format_string,
|
||||
"uid": ThumbnailTypes.LATEST_ENTRY,
|
||||
}
|
||||
)
|
||||
|
||||
return CollectionValidator(
|
||||
name=self._name,
|
||||
value={
|
||||
"urls": [
|
||||
{"url": self.playlist_url, "variables": {"playlist_size": "{playlist_count}"}}
|
||||
{
|
||||
"url": self.playlist_url,
|
||||
"playlist_thumbnails": playlist_thumbnails,
|
||||
"variables": {"playlist_size": "{playlist_count}"},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
|
@ -50,6 +71,13 @@ class YoutubePlaylistDownloaderOptions(DownloaderValidator):
|
|||
"""
|
||||
return self._playlist_url
|
||||
|
||||
@property
|
||||
def playlist_thumbnail_path(self) -> Optional[OverridesStringFormatterValidator]:
|
||||
"""
|
||||
Optional. Path to store the playlist's thumbnail
|
||||
"""
|
||||
return self._playlist_thumbnail_name
|
||||
|
||||
|
||||
class YoutubePlaylistDownloader(Downloader[YoutubePlaylistDownloaderOptions]):
|
||||
downloader_options_type = YoutubePlaylistDownloaderOptions
|
||||
|
|
|
|||
|
|
@ -71,10 +71,12 @@ class SubscriptionDownload(BaseSubscription, ABC):
|
|||
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(),
|
||||
output_file_name=output_thumbnail_name,
|
||||
entry=entry,
|
||||
copy_file=True,
|
||||
)
|
||||
|
||||
if self.output_options.info_json_name:
|
||||
|
|
|
|||
|
|
@ -329,7 +329,11 @@ class FileHandler:
|
|||
os.remove(file_path)
|
||||
|
||||
def move_file_to_output_directory(
|
||||
self, file_name: str, output_file_name: str, file_metadata: Optional[FileMetadata] = None
|
||||
self,
|
||||
file_name: str,
|
||||
output_file_name: str,
|
||||
file_metadata: Optional[FileMetadata] = None,
|
||||
copy_file: bool = False,
|
||||
):
|
||||
"""
|
||||
Copies a file from the working directory to the output directory.
|
||||
|
|
@ -344,6 +348,8 @@ class FileHandler:
|
|||
Desired output file name in the output_directory
|
||||
file_metadata
|
||||
Optional. Metadata to record to the transaction log for this file
|
||||
copy_file
|
||||
Optional. If True, copy the file. Move otherwise
|
||||
"""
|
||||
source_file_path = Path(self.working_directory) / file_name
|
||||
output_file_path = Path(self.output_directory) / output_file_name
|
||||
|
|
@ -365,7 +371,10 @@ class FileHandler:
|
|||
|
||||
if not self.dry_run:
|
||||
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
|
||||
self.move(src_file_path=source_file_path, dst_file_path=output_file_path)
|
||||
if copy_file:
|
||||
self.copy(src_file_path=source_file_path, dst_file_path=output_file_path)
|
||||
else:
|
||||
self.move(src_file_path=source_file_path, dst_file_path=output_file_path)
|
||||
|
||||
def delete_file_from_output_directory(self, file_name: str):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ from ytdl_sub.utils.ffmpeg import FFMPEG
|
|||
from ytdl_sub.utils.retry import retry
|
||||
|
||||
|
||||
class ThumbnailTypes:
|
||||
LATEST_ENTRY = "latest_entry"
|
||||
|
||||
|
||||
def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> None:
|
||||
"""
|
||||
Converts an entry's downloaded thumbnail into jpg format
|
||||
|
|
|
|||
|
|
@ -541,6 +541,7 @@ class EnhancedDownloadArchive:
|
|||
file_metadata: Optional[FileMetadata] = None,
|
||||
output_file_name: Optional[str] = None,
|
||||
entry: Optional[Entry] = None,
|
||||
copy_file: bool = False,
|
||||
):
|
||||
"""
|
||||
Saves a file from the working directory to the output directory and record it in the
|
||||
|
|
@ -557,6 +558,8 @@ class EnhancedDownloadArchive:
|
|||
directory path). If None, use the same working_directory file_name
|
||||
entry
|
||||
Optional. Entry that this file belongs to
|
||||
copy_file
|
||||
Optional. If True, copy the file. Move otherwise
|
||||
"""
|
||||
if output_file_name is None:
|
||||
output_file_name = file_name
|
||||
|
|
@ -565,7 +568,10 @@ class EnhancedDownloadArchive:
|
|||
self.mapping.add_entry(entry=entry, entry_file_path=output_file_name)
|
||||
|
||||
self._file_handler.move_file_to_output_directory(
|
||||
file_name=file_name, file_metadata=file_metadata, output_file_name=output_file_name
|
||||
file_name=file_name,
|
||||
file_metadata=file_metadata,
|
||||
output_file_name=output_file_name,
|
||||
copy_file=copy_file,
|
||||
)
|
||||
|
||||
def get_file_handler_transaction_log(self) -> FileHandlerTransactionLog:
|
||||
|
|
@ -611,6 +617,7 @@ class DownloadArchiver:
|
|||
file_metadata: Optional[FileMetadata] = None,
|
||||
output_file_name: Optional[str] = None,
|
||||
entry: Optional[Entry] = None,
|
||||
copy_file: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Saves a file in the working directory to the output directory.
|
||||
|
|
@ -626,10 +633,13 @@ class DownloadArchiver:
|
|||
directory path). If None, use the same working_directory file_name
|
||||
entry
|
||||
Optional. Entry that the file belongs to
|
||||
copy_file
|
||||
Optional. If True, copy the file. Move otherwise
|
||||
"""
|
||||
self._enhanced_download_archive.save_file_to_output_directory(
|
||||
file_name=file_name,
|
||||
file_metadata=file_metadata,
|
||||
output_file_name=output_file_name,
|
||||
entry=entry,
|
||||
copy_file=copy_file,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
{
|
||||
".ytdl-sub-music_video_playlist_test-download-archive.json": "25b8e44961343116436584e341c7fe9b",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "cbd6a34138c3044e37158ee1a3c89eea",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "614f0db46765b30c21473c2b42efaea8",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "e66287b9832277b6a4d1554e29d9fdcc",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "de6f9a17a5ebf92aa02b14f7342b8d36",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "b707a8d1741db62dd8b8470e7a2f9368",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "156ac62ef15a958fe9fbac6c6a58ed87",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.27].mp4": "04ab5cb3cc12325d0c96a7cd04a8b91d",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Feb.27].nfo": "9a4a57bb91f1ac97b588fdf66bac4c9b",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg": "e7830aa8a64b0cde65ba3f7e5fc56530",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Mar.21].info.json": "509eebbb687bab378e36dac08b5cf989",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Mar.21].info.json": "4e2186286a679dd091308dabf28ed43c",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Mar.21].mp4": "025de6099a5c98e6397153c7a62d517d",
|
||||
"JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo": "b98f1fa3e76b4592aeeaed4f6e220809",
|
||||
"poster.jpg": "e7830aa8a64b0cde65ba3f7e5fc56530",
|
||||
"tvshow.nfo": "e4123860532466ed5e0ebf2c9e44eb18"
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo
|
|||
playlist_index: 1
|
||||
title: Jesse's Minecraft Server [Trailer - Mar.21]
|
||||
year: 2011
|
||||
poster.jpg
|
||||
tvshow.nfo
|
||||
NFO tags:
|
||||
test:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ from ytdl_sub.subscriptions.subscription import Subscription
|
|||
def playlist_preset_dict(output_directory):
|
||||
return {
|
||||
"preset": "yt_music_video_playlist",
|
||||
"youtube": {"playlist_url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35"},
|
||||
"youtube": {
|
||||
"playlist_url": "https://youtube.com/playlist?list=PL5BC0FC26BECA5A35",
|
||||
"playlist_thumbnail_name": "poster.jpg",
|
||||
},
|
||||
# override the output directory with our fixture-generated dir
|
||||
"output_options": {"output_directory": output_directory},
|
||||
# download the worst format so it is fast
|
||||
|
|
|
|||
Loading…
Reference in a new issue