separate preset options

This commit is contained in:
jbannon 2022-04-22 20:52:15 +00:00
parent f2ec156138
commit 24fac00064
8 changed files with 158 additions and 161 deletions

View file

@ -56,7 +56,6 @@ presets:
convert_thumbnail: "jpeg" convert_thumbnail: "jpeg"
thumbnail_name: "{output_file_name}.jpg" thumbnail_name: "{output_file_name}.jpg"
maintain_download_archive: True maintain_download_archive: True
maintain_stale_file_deletion: True
convert_thumbnail: convert_thumbnail:
to: "jpg" to: "jpg"
nfo_tags: nfo_tags:

View file

@ -1,4 +1,3 @@
from abc import ABC
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
@ -6,25 +5,21 @@ from typing import Optional
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from yt_dlp.utils import sanitize_filename
from ytdl_subscribe.config.preset_class_mappings import DownloadStrategyMapping from ytdl_subscribe.config.preset_class_mappings import DownloadStrategyMapping
from ytdl_subscribe.config.preset_class_mappings import PluginMapping from ytdl_subscribe.config.preset_class_mappings import PluginMapping
from ytdl_subscribe.config.preset_options import DownloadStrategyValidator
from ytdl_subscribe.config.preset_options import OutputOptions
from ytdl_subscribe.config.preset_options import Overrides
from ytdl_subscribe.config.preset_options import YTDLOptions
from ytdl_subscribe.downloaders.downloader import Downloader from ytdl_subscribe.downloaders.downloader import Downloader
from ytdl_subscribe.downloaders.downloader import DownloaderValidator from ytdl_subscribe.downloaders.downloader import DownloaderValidator
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginOptions from ytdl_subscribe.plugins.plugin import PluginOptions
from ytdl_subscribe.utils.exceptions import StringFormattingVariableNotFoundException from ytdl_subscribe.utils.exceptions import StringFormattingVariableNotFoundException
from ytdl_subscribe.utils.exceptions import ValidationException from ytdl_subscribe.utils.exceptions import ValidationException
from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_subscribe.validators.string_formatter_validators import OverridesStringFormatterValidator
from ytdl_subscribe.validators.string_formatter_validators import StringFormatterValidator
from ytdl_subscribe.validators.validators import BoolValidator
from ytdl_subscribe.validators.validators import DictValidator from ytdl_subscribe.validators.validators import DictValidator
from ytdl_subscribe.validators.validators import LiteralDictValidator
from ytdl_subscribe.validators.validators import StringValidator
from ytdl_subscribe.validators.validators import Validator from ytdl_subscribe.validators.validators import Validator
PRESET_REQUIRED_KEYS = {"output_options"} PRESET_REQUIRED_KEYS = {"output_options"}
@ -36,100 +31,7 @@ PRESET_OPTIONAL_KEYS = {
} }
class YTDLOptions(LiteralDictValidator): class Preset(StrictDictValidator):
"""Ensures `ytdl_options` is a dict"""
class Overrides(DictFormatterValidator):
"""Ensures `overrides` is a dict"""
def __init__(self, name, value):
super().__init__(name, value)
for key in self._keys:
sanitized_key_name = f"sanitized_{key}"
# First, sanitize the format string
self._value[sanitized_key_name] = sanitize_filename(self._value[key].format_string)
# Then, convert it into a StringFormatterValidator
self._value[sanitized_key_name] = StringFormatterValidator(
name="__should_never_fail__",
value=self._value[sanitized_key_name],
)
def apply_formatter(
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
) -> str:
"""
Returns the format_string after .format has been called on it using entry (if provided) and
override values
"""
variable_dict = self.dict_with_format_strings
if entry:
variable_dict = dict(entry.to_dict(), **variable_dict)
return formatter.apply_formatter(variable_dict)
class OutputOptions(StrictDictValidator):
"""Where to output the final files and thumbnails"""
_required_keys = {"output_directory", "file_name"}
_optional_keys = {
"thumbnail_name",
"maintain_download_archive",
"maintain_stale_file_deletion",
}
def __init__(self, name, value):
super().__init__(name, value)
# Output directory should resolve without any entry variables.
# This is to check the directory for any download-archives before any downloads begin
self.output_directory: OverridesStringFormatterValidator = self._validate_key(
key="output_directory", validator=OverridesStringFormatterValidator
)
# file name and thumbnails however can use entry variables
self.file_name: StringFormatterValidator = self._validate_key(
key="file_name", validator=StringFormatterValidator
)
self.thumbnail_name = self._validate_key_if_present(
key="thumbnail_name", validator=StringFormatterValidator
)
self.maintain_download_archive = self._validate_key_if_present(
key="maintain_download_archive", validator=BoolValidator, default=False
)
self.maintain_stale_file_deletion = self._validate_key_if_present(
key="maintain_stale_file_deletion", validator=BoolValidator, default=False
)
if self.maintain_stale_file_deletion.value and not self.maintain_download_archive.value:
raise self._validation_exception(
"maintain_stale_file_deletion requires maintain_download_archive set to True"
)
class DownloadStrategyValidator(StrictDictValidator, ABC):
"""
Ensures a download strategy exists for a source. Does not validate any more than that.
The respective Downloader's option validator will do that.
"""
# All media sources must define a download strategy
_required_keys = {"download_strategy"}
# Extra fields will be strict-validated using other StictDictValidators
_allow_extra_keys = True
def __init__(self, name: str, value: Any):
super().__init__(name=name, value=value)
self.name = self._validate_key(
key="download_strategy",
validator=StringValidator,
).value
class PresetValidator(StrictDictValidator):
_required_keys = PRESET_REQUIRED_KEYS _required_keys = PRESET_REQUIRED_KEYS
_optional_keys = PRESET_OPTIONAL_KEYS _optional_keys = PRESET_OPTIONAL_KEYS

View file

@ -0,0 +1,108 @@
from abc import ABC
from typing import Any
from typing import Optional
from yt_dlp.utils import sanitize_filename
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.date_range_validator import DateRangeValidator
from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.string_formatter_validators import OverridesStringFormatterValidator
from ytdl_subscribe.validators.string_formatter_validators import StringFormatterValidator
from ytdl_subscribe.validators.validators import BoolValidator
from ytdl_subscribe.validators.validators import LiteralDictValidator
from ytdl_subscribe.validators.validators import StringValidator
class YTDLOptions(LiteralDictValidator):
"""Ensures `ytdl_options` is a dict"""
class Overrides(DictFormatterValidator):
"""Ensures `overrides` is a dict"""
def __init__(self, name, value):
super().__init__(name, value)
for key in self._keys:
sanitized_key_name = f"sanitized_{key}"
# First, sanitize the format string
self._value[sanitized_key_name] = sanitize_filename(self._value[key].format_string)
# Then, convert it into a StringFormatterValidator
self._value[sanitized_key_name] = StringFormatterValidator(
name="__should_never_fail__",
value=self._value[sanitized_key_name],
)
def apply_formatter(
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
) -> str:
"""
Returns the format_string after .format has been called on it using entry (if provided) and
override values
"""
variable_dict = self.dict_with_format_strings
if entry:
variable_dict = dict(entry.to_dict(), **variable_dict)
return formatter.apply_formatter(variable_dict)
class OutputOptions(StrictDictValidator):
"""Where to output the final files and thumbnails"""
_required_keys = {"output_directory", "file_name"}
_optional_keys = {
"thumbnail_name",
"maintain_download_archive",
"maintain_stale_file_deletion",
}
def __init__(self, name, value):
super().__init__(name, value)
# Output directory should resolve without any entry variables.
# This is to check the directory for any download-archives before any downloads begin
self.output_directory: OverridesStringFormatterValidator = self._validate_key(
key="output_directory", validator=OverridesStringFormatterValidator
)
# file name and thumbnails however can use entry variables
self.file_name: StringFormatterValidator = self._validate_key(
key="file_name", validator=StringFormatterValidator
)
self.thumbnail_name = self._validate_key_if_present(
key="thumbnail_name", validator=StringFormatterValidator
)
self.maintain_download_archive = self._validate_key_if_present(
key="maintain_download_archive", validator=BoolValidator, default=False
)
self.maintain_stale_file_deletion = self._validate_key_if_present(
key="maintain_stale_file_deletion", validator=DateRangeValidator, default=False
)
if self.maintain_stale_file_deletion and not self.maintain_download_archive:
raise self._validation_exception(
"maintain_stale_file_deletion requires maintain_download_archive set to True"
)
class DownloadStrategyValidator(StrictDictValidator, ABC):
"""
Ensures a download strategy exists for a source. Does not validate any more than that.
The respective Downloader's option validator will do that.
"""
# All media sources must define a download strategy
_required_keys = {"download_strategy"}
# Extra fields will be strict-validated using other StictDictValidators
_allow_extra_keys = True
def __init__(self, name: str, value: Any):
super().__init__(name=name, value=value)
self.name = self._validate_key(
key="download_strategy",
validator=StringValidator,
).value

View file

@ -9,8 +9,8 @@ from mergedeep import mergedeep
from ytdl_subscribe.config.config_file import ConfigFile from ytdl_subscribe.config.config_file import ConfigFile
from ytdl_subscribe.config.preset import PRESET_OPTIONAL_KEYS from ytdl_subscribe.config.preset import PRESET_OPTIONAL_KEYS
from ytdl_subscribe.config.preset import PRESET_REQUIRED_KEYS from ytdl_subscribe.config.preset import PRESET_REQUIRED_KEYS
from ytdl_subscribe.config.preset import Overrides from ytdl_subscribe.config.preset import Preset
from ytdl_subscribe.config.preset import PresetValidator from ytdl_subscribe.config.preset_options import Overrides
from ytdl_subscribe.subscriptions.subscription import Subscription from ytdl_subscribe.subscriptions.subscription import Subscription
from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.validators import StringValidator from ytdl_subscribe.validators.validators import StringValidator
@ -52,7 +52,7 @@ class SubscriptionValidator(StrictDictValidator):
preset_dict = mergedeep.merge(preset_dict, self._dict, strategy=mergedeep.Strategy.REPLACE) preset_dict = mergedeep.merge(preset_dict, self._dict, strategy=mergedeep.Strategy.REPLACE)
del preset_dict["preset"] del preset_dict["preset"]
self.preset = PresetValidator( self.preset = Preset(
name=f"{self._name}.{preset_name}", name=f"{self._name}.{preset_name}",
value=preset_dict, value=preset_dict,
) )

View file

@ -13,7 +13,7 @@ from yt_dlp.utils import RejectedVideoReached
from ytdl_subscribe.downloaders.downloader import Downloader from ytdl_subscribe.downloaders.downloader import Downloader
from ytdl_subscribe.downloaders.downloader import DownloaderValidator from ytdl_subscribe.downloaders.downloader import DownloaderValidator
from ytdl_subscribe.entries.youtube import YoutubeVideo from ytdl_subscribe.entries.youtube import YoutubeVideo
from ytdl_subscribe.validators.date_range_validator import DownloadDateRangeSource from ytdl_subscribe.validators.date_range_validator import DateRangeValidator
from ytdl_subscribe.validators.validators import StringValidator from ytdl_subscribe.validators.validators import StringValidator
############################################################################### ###############################################################################
@ -54,14 +54,19 @@ class YoutubeDownloader(
"""Returns full channel url""" """Returns full channel url"""
return f"https://youtube.com/channel/{channel_id}" return f"https://youtube.com/channel/{channel_id}"
def _download_with_metadata( def _download_using_metadata(
self, url: str, ytdl_options_overrides: Optional[Dict] = None self,
) -> None: url: str,
ignore_prefix: str,
ytdl_options_overrides: Optional[Dict] = None,
) -> List[YoutubeVideo]:
""" """
Do not get entries from the extract info, let it write to the info.json file and load Do not get entries from the extract info, let it write to the info.json file and load
that instead. This is because if the video is already downloaded in a playlist, it will that instead. This is because if the video is already downloaded in a playlist, it will
not fetch the metadata (maybe there is a way??) not fetch the metadata (maybe there is a way??)
""" """
entries: List[YoutubeVideo] = []
ytdl_overrides = { ytdl_overrides = {
"writeinfojson": True, "writeinfojson": True,
} }
@ -73,6 +78,14 @@ class YoutubeDownloader(
except RejectedVideoReached: except RejectedVideoReached:
pass pass
# Load the entries from info.json, ignore the playlist entry
for file_name in os.listdir(self.working_directory):
if file_name.endswith(".info.json") and not file_name.startswith(ignore_prefix):
with open(Path(self.working_directory) / file_name, "r", encoding="utf-8") as file:
entries.append(YoutubeVideo(**json.load(file)))
return entries
def download_video(self, video_id: str) -> YoutubeVideo: def download_video(self, video_id: str) -> YoutubeVideo:
"""Download a single Youtube video""" """Download a single Youtube video"""
entry = self.extract_info(url=self.video_url(video_id)) entry = self.extract_info(url=self.video_url(video_id))
@ -82,20 +95,9 @@ class YoutubeDownloader(
""" """
Downloads all videos in a Youtube playlist Downloads all videos in a Youtube playlist
""" """
playlist_url = self.playlist_url(playlist_id=playlist_id) return self._download_using_metadata(
url=self.playlist_url(playlist_id=playlist_id), ignore_prefix=playlist_id
self._download_with_metadata(url=playlist_url) )
# Load the entries from info.json, ignore the playlist entry
entries: List[YoutubeVideo] = []
# Load the entries from info.json, ignore the playlist entry
for file_name in os.listdir(self.working_directory):
if file_name.endswith(".info.json") and not file_name.startswith(playlist_id):
with open(Path(self.working_directory) / file_name, "r", encoding="utf-8") as file:
entries.append(YoutubeVideo(**json.load(file)))
return entries
def download_channel( def download_channel(
self, channel_id: str, ytdl_options_overrides: Optional[Dict] = None self, channel_id: str, ytdl_options_overrides: Optional[Dict] = None
@ -103,22 +105,12 @@ class YoutubeDownloader(
""" """
Downloads all videos from a channel Downloads all videos from a channel
""" """
self._download_with_metadata( return self._download_using_metadata(
url=self.channel_url(channel_id), ytdl_options_overrides=ytdl_options_overrides url=self.channel_url(channel_id),
ignore_prefix=channel_id,
ytdl_options_overrides=ytdl_options_overrides,
) )
# Load the entries from info.json
entries: List[YoutubeVideo] = []
# Load the entries from info.json
# TODO dupe code between this and playlist
for file_name in os.listdir(self.working_directory):
if file_name.endswith(".info.json") and not file_name.startswith(channel_id):
with open(Path(self.working_directory) / file_name, "r", encoding="utf-8") as file:
entries.append(YoutubeVideo(**json.load(file)))
return entries
############################################################################### ###############################################################################
# Youtube single video downloader + options # Youtube single video downloader + options
@ -163,13 +155,13 @@ class YoutubePlaylistDownloader(YoutubeDownloader[YoutubePlaylistDownloaderOptio
# Youtube channel downloader + options # Youtube channel downloader + options
class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DownloadDateRangeSource): class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidator):
_required_keys = {"channel_id"} _required_keys = {"channel_id"}
_optional_keys = {"before", "after"} _optional_keys = {"before", "after"}
def __init__(self, name, value): def __init__(self, name, value):
YoutubeDownloaderOptions.__init__(self, name, value) YoutubeDownloaderOptions.__init__(self, name, value)
DownloadDateRangeSource.__init__(self, name, value) DateRangeValidator.__init__(self, name, value)
self.channel_id = self._validate_key("channel_id", StringValidator) self.channel_id = self._validate_key("channel_id", StringValidator)

View file

@ -4,7 +4,7 @@ from typing import Type
from typing import TypeVar from typing import TypeVar
from typing import final from typing import final
from ytdl_subscribe.config.preset import Overrides from ytdl_subscribe.config.preset_options import Overrides
from ytdl_subscribe.entries.entry import Entry from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_subscribe.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive

View file

@ -3,23 +3,20 @@ import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from shutil import copyfile from shutil import copyfile
from typing import Dict
from typing import List from typing import List
from typing import Optional
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from ytdl_subscribe.config.config_file import ConfigOptions from ytdl_subscribe.config.config_file import ConfigOptions
from ytdl_subscribe.config.preset import OutputOptions from ytdl_subscribe.config.preset import Preset
from ytdl_subscribe.config.preset import Overrides from ytdl_subscribe.config.preset_options import OutputOptions
from ytdl_subscribe.config.preset import PresetValidator from ytdl_subscribe.config.preset_options import Overrides
from ytdl_subscribe.config.preset import YTDLOptions from ytdl_subscribe.config.preset_options import YTDLOptions
from ytdl_subscribe.downloaders.downloader import Downloader from ytdl_subscribe.downloaders.downloader import Downloader
from ytdl_subscribe.downloaders.downloader import DownloaderValidator from ytdl_subscribe.downloaders.downloader import DownloaderValidator
from ytdl_subscribe.entries.entry import Entry from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginOptions from ytdl_subscribe.plugins.plugin import PluginOptions
from ytdl_subscribe.validators.date_range_validator import DownloadDateRangeSource
from ytdl_subscribe.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_subscribe.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
@ -41,7 +38,7 @@ class Subscription:
self, self,
name: str, name: str,
config_options: ConfigOptions, config_options: ConfigOptions,
preset_options: PresetValidator, preset_options: Preset,
): ):
""" """
Parameters Parameters
@ -49,7 +46,7 @@ class Subscription:
name: str name: str
Name of the subscription Name of the subscription
config_options: ConfigOptions config_options: ConfigOptions
preset_options: PresetValidator preset_options: Preset
""" """
self.name = name self.name = name
self.__config_options = config_options self.__config_options = config_options
@ -143,15 +140,14 @@ class Subscription:
yield yield
# TODO: add date range in output options # If output options maintains stale file deletion, perform the delete here prior to saving
# date_range = None # the download archive
# if isinstance(self.source_options, DownloadDateRangeSource): if self.output_options.maintain_stale_file_deletion:
# date_range = self.source_options.get_date_range() self._enhanced_download_archive.remove_stale_files(
# date_range=self.output_options.maintain_stale_file_deletion.get_date_range()
# if date_range and self.output_options.maintain_stale_file_deletion: )
# self._enhanced_download_archive.remove_stale_files(date_range=date_range)
# self._enhanced_download_archive.save_download_archive()
# self._enhanced_download_archive.save_download_archive()
def _initialize_plugins(self) -> List[Plugin]: def _initialize_plugins(self) -> List[Plugin]:
plugins: List[Plugin] = [] plugins: List[Plugin] = []

View file

@ -7,7 +7,7 @@ from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.string_datetime import StringDatetimeValidator from ytdl_subscribe.validators.string_datetime import StringDatetimeValidator
class DownloadDateRangeSource(StrictDictValidator, ABC): class DateRangeValidator(StrictDictValidator, ABC):
optional_keys = {"before", "after"} optional_keys = {"before", "after"}
def __init__(self, name, value): def __init__(self, name, value):