[FEATURE] Add playlist thumbnail support (#250)

This commit is contained in:
Jesse Bannon 2022-09-27 23:48:18 -07:00 committed by GitHub
parent a970fb9d06
commit 839192059b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 86 additions and 10 deletions

View file

@ -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
),

View file

@ -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

View file

@ -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:

View file

@ -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):
"""

View file

@ -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

View file

@ -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,
)

View 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"
}

View file

@ -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:

View file

@ -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