[FEATURE] Add playlist thumbnail support

This commit is contained in:
Jesse Bannon 2022-09-27 23:35:41 -07:00
parent ebdd6c8aec
commit dae3a28a6b
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 FileHandler
from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.utils.logger import Logger 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.utils.thumbnail import convert_url_thumbnail
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
@ -518,6 +519,7 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
thumbnails_downloaded: Set[str], thumbnails_downloaded: Set[str],
thumbnail_list_info: CollectionThumbnailListValidator, thumbnail_list_info: CollectionThumbnailListValidator,
entry: Entry, entry: Entry,
is_last_entry: bool,
parent: EntryParent, parent: EntryParent,
) -> Set[str]: ) -> Set[str]:
""" """
@ -527,10 +529,22 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
thumbnail_name = self.overrides.apply_formatter(thumbnail_info.name, entry=entry) thumbnail_name = self.overrides.apply_formatter(thumbnail_info.name, entry=entry)
thumbnail_id = self.overrides.apply_formatter(thumbnail_info.uid) thumbnail_id = self.overrides.apply_formatter(thumbnail_info.uid)
# alread downloaded # already downloaded
if thumbnail_name in thumbnails_downloaded: if thumbnail_name in thumbnails_downloaded:
continue 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: if (thumbnail_url := parent.get_thumbnail_url(thumbnail_id=thumbnail_id)) is None:
download_logger.warning("TODO: Failed to download channel's avatar image") download_logger.warning("TODO: Failed to download channel's avatar image")
continue continue
@ -556,12 +570,15 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
""" """
thumbnails_downloaded: Set[str] = set() 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): if entry.kwargs_contains(PLAYLIST_ENTRY):
thumbnails_downloaded = self._download_parent_thumbnails( thumbnails_downloaded = self._download_parent_thumbnails(
thumbnails_downloaded=thumbnails_downloaded, thumbnails_downloaded=thumbnails_downloaded,
thumbnail_list_info=collection_url.playlist_thumbnails, thumbnail_list_info=collection_url.playlist_thumbnails,
entry=entry, entry=entry,
is_last_entry=is_last_entry,
parent=EntryParent( parent=EntryParent(
entry.kwargs(PLAYLIST_ENTRY), working_directory=self.working_directory entry.kwargs(PLAYLIST_ENTRY), working_directory=self.working_directory
), ),
@ -572,6 +589,7 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT], ABC):
thumbnails_downloaded=thumbnails_downloaded, thumbnails_downloaded=thumbnails_downloaded,
thumbnail_list_info=collection_url.source_thumbnails, thumbnail_list_info=collection_url.source_thumbnails,
entry=entry, entry=entry,
is_last_entry=is_last_entry,
parent=EntryParent( parent=EntryParent(
entry.kwargs(SOURCE_ENTRY), working_directory=self.working_directory entry.kwargs(SOURCE_ENTRY), working_directory=self.working_directory
), ),

View file

@ -1,8 +1,12 @@
from typing import Dict 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 Downloader
from ytdl_sub.downloaders.downloader import DownloaderValidator from ytdl_sub.downloaders.downloader import DownloaderValidator
from ytdl_sub.downloaders.generic.collection_validator import CollectionValidator 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 from ytdl_sub.validators.url_validator import YoutubePlaylistUrlValidator
@ -23,21 +27,38 @@ class YoutubePlaylistDownloaderOptions(DownloaderValidator):
""" """
_required_keys = {"playlist_url"} _required_keys = {"playlist_url"}
_optional_keys = {"playlist_thumbnail_name"}
def __init__(self, name, value): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
self._playlist_url = self._validate_key( self._playlist_url = self._validate_key(
"playlist_url", YoutubePlaylistUrlValidator "playlist_url", YoutubePlaylistUrlValidator
).playlist_url ).playlist_url
self._playlist_thumbnail_name = self._validate_key_if_present(
"playlist_thumbnail_name", OverridesStringFormatterValidator
)
@property @property
def collection_validator(self) -> CollectionValidator: def collection_validator(self) -> CollectionValidator:
"""Downloads the playlist url""" """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( return CollectionValidator(
name=self._name, name=self._name,
value={ value={
"urls": [ "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 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]): class YoutubePlaylistDownloader(Downloader[YoutubePlaylistDownloaderOptions]):
downloader_options_type = YoutubePlaylistDownloaderOptions downloader_options_type = YoutubePlaylistDownloaderOptions

View file

@ -71,10 +71,12 @@ class SubscriptionDownload(BaseSubscription, ABC):
if not dry_run: if not dry_run:
convert_download_thumbnail(entry=entry) 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( self._enhanced_download_archive.save_file_to_output_directory(
file_name=entry.get_download_thumbnail_name(), file_name=entry.get_download_thumbnail_name(),
output_file_name=output_thumbnail_name, output_file_name=output_thumbnail_name,
entry=entry, entry=entry,
copy_file=True,
) )
if self.output_options.info_json_name: if self.output_options.info_json_name:

View file

@ -329,7 +329,11 @@ class FileHandler:
os.remove(file_path) os.remove(file_path)
def move_file_to_output_directory( 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. 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 Desired output file name in the output_directory
file_metadata file_metadata
Optional. Metadata to record to the transaction log for this file 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 source_file_path = Path(self.working_directory) / file_name
output_file_path = Path(self.output_directory) / output_file_name output_file_path = Path(self.output_directory) / output_file_name
@ -365,7 +371,10 @@ class FileHandler:
if not self.dry_run: if not self.dry_run:
os.makedirs(os.path.dirname(output_file_path), exist_ok=True) 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): 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 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: def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> None:
""" """
Converts an entry's downloaded thumbnail into jpg format Converts an entry's downloaded thumbnail into jpg format

View file

@ -541,6 +541,7 @@ class EnhancedDownloadArchive:
file_metadata: Optional[FileMetadata] = None, file_metadata: Optional[FileMetadata] = None,
output_file_name: Optional[str] = None, output_file_name: Optional[str] = None,
entry: Optional[Entry] = 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 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 directory path). If None, use the same working_directory file_name
entry entry
Optional. Entry that this file belongs to Optional. Entry that this file belongs to
copy_file
Optional. If True, copy the file. Move otherwise
""" """
if output_file_name is None: if output_file_name is None:
output_file_name = file_name output_file_name = file_name
@ -565,7 +568,10 @@ class EnhancedDownloadArchive:
self.mapping.add_entry(entry=entry, entry_file_path=output_file_name) self.mapping.add_entry(entry=entry, entry_file_path=output_file_name)
self._file_handler.move_file_to_output_directory( 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: def get_file_handler_transaction_log(self) -> FileHandlerTransactionLog:
@ -611,6 +617,7 @@ class DownloadArchiver:
file_metadata: Optional[FileMetadata] = None, file_metadata: Optional[FileMetadata] = None,
output_file_name: Optional[str] = None, output_file_name: Optional[str] = None,
entry: Optional[Entry] = None, entry: Optional[Entry] = None,
copy_file: bool = False,
) -> None: ) -> None:
""" """
Saves a file in the working directory to the output directory. 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 directory path). If None, use the same working_directory file_name
entry entry
Optional. Entry that the file belongs to 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( self._enhanced_download_archive.save_file_to_output_directory(
file_name=file_name, file_name=file_name,
file_metadata=file_metadata, file_metadata=file_metadata,
output_file_name=output_file_name, output_file_name=output_file_name,
entry=entry, entry=entry,
copy_file=copy_file,
) )

View file

@ -1,16 +1,17 @@
{ {
".ytdl-sub-music_video_playlist_test-download-archive.json": "25b8e44961343116436584e341c7fe9b", ".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]-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].mp4": "e66287b9832277b6a4d1554e29d9fdcc",
"JMC - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "de6f9a17a5ebf92aa02b14f7342b8d36", "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]-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].mp4": "04ab5cb3cc12325d0c96a7cd04a8b91d",
"JMC - Jesse's Minecraft Server [Trailer - Feb.27].nfo": "9a4a57bb91f1ac97b588fdf66bac4c9b", "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]-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].mp4": "025de6099a5c98e6397153c7a62d517d",
"JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo": "b98f1fa3e76b4592aeeaed4f6e220809", "JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo": "b98f1fa3e76b4592aeeaed4f6e220809",
"poster.jpg": "e7830aa8a64b0cde65ba3f7e5fc56530",
"tvshow.nfo": "e4123860532466ed5e0ebf2c9e44eb18" "tvshow.nfo": "e4123860532466ed5e0ebf2c9e44eb18"
} }

View file

@ -37,6 +37,7 @@ JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo
playlist_index: 1 playlist_index: 1
title: Jesse's Minecraft Server [Trailer - Mar.21] title: Jesse's Minecraft Server [Trailer - Mar.21]
year: 2011 year: 2011
poster.jpg
tvshow.nfo tvshow.nfo
NFO tags: NFO tags:
test: test:

View file

@ -12,7 +12,10 @@ from ytdl_sub.subscriptions.subscription import Subscription
def playlist_preset_dict(output_directory): def playlist_preset_dict(output_directory):
return { return {
"preset": "yt_music_video_playlist", "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 # override the output directory with our fixture-generated dir
"output_options": {"output_directory": output_directory}, "output_options": {"output_directory": output_directory},
# download the worst format so it is fast # download the worst format so it is fast