diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index b3bcc53e..7ae03041 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -1,7 +1,6 @@ from typing import Dict from typing import Optional -from yt_dlp.utils import DateRange from yt_dlp.utils import sanitize_filename from ytdl_sub.entries.entry import Entry @@ -226,16 +225,3 @@ class OutputOptions(StrictDictValidator): files after ``19000101``, which implies all files. """ return self._keep_files_after - - def get_upload_date_range_to_keep(self) -> Optional[DateRange]: - """ - Returns - ------- - Date range if the 'before' or 'after' is defined. None otherwise. - """ - if self.keep_files_before or self.keep_files_after: - return DateRange( - start=self.keep_files_after.datetime_str if self.keep_files_after else None, - end=self.keep_files_before.datetime_str if self.keep_files_before else None, - ) - return None diff --git a/src/ytdl_sub/downloaders/youtube/channel.py b/src/ytdl_sub/downloaders/youtube/channel.py index f3f93e9e..e248f02e 100644 --- a/src/ytdl_sub/downloaders/youtube/channel.py +++ b/src/ytdl_sub/downloaders/youtube/channel.py @@ -12,14 +12,15 @@ from ytdl_sub.downloaders.youtube.abc import YoutubeDownloaderOptions from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder from ytdl_sub.entries.youtube import YoutubeChannel from ytdl_sub.entries.youtube import YoutubeVideo +from ytdl_sub.utils.datetime import to_date_range_hack from ytdl_sub.utils.thumbnail import convert_url_thumbnail -from ytdl_sub.validators.date_range_validator import DateRangeValidator +from ytdl_sub.validators.string_datetime import StringDatetimeValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.url_validator import YoutubeChannelUrlValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive -class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidator): +class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions): """ Downloads all videos from a youtube channel. @@ -49,8 +50,7 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidat } def __init__(self, name, value): - YoutubeDownloaderOptions.__init__(self, name, value) - DateRangeValidator.__init__(self, name, value) + super().__init__(name, value) self._channel_url = self._validate_key( "channel_url", YoutubeChannelUrlValidator ).channel_url @@ -60,6 +60,8 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidat self._channel_banner_path = self._validate_key_if_present( "channel_banner_path", OverridesStringFormatterValidator ) + self._before = self._validate_key_if_present("before", StringDatetimeValidator) + self._after = self._validate_key_if_present("after", StringDatetimeValidator) @property def channel_url(self) -> str: @@ -84,6 +86,22 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DateRangeValidat """ return self._channel_banner_path + @property + def before(self) -> Optional[StringDatetimeValidator]: + """ + DEPRECATED: use the `date_range` plugin instead. Will be removed in version 0.5.0 + Optional. Only download videos before this datetime. + """ + return self._before + + @property + def after(self) -> Optional[StringDatetimeValidator]: + """ + DEPRECATED: use the `date_range` plugin instead. Will be removed in version 0.5.0 + Optional. Only download videos after this datetime. + """ + return self._after + class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions, YoutubeVideo]): downloader_options_type = YoutubeChannelDownloaderOptions @@ -138,7 +156,9 @@ class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions ytdl_options_overrides = {} # If a date range is specified when download a YT channel, add it into the ytdl options - source_date_range = self.download_options.get_date_range() + source_date_range = to_date_range_hack( + before=self.download_options.before, after=self.download_options.after + ) if source_date_range: ytdl_options_overrides["daterange"] = source_date_range diff --git a/src/ytdl_sub/plugins/date_range.py b/src/ytdl_sub/plugins/date_range.py new file mode 100644 index 00000000..8859cc26 --- /dev/null +++ b/src/ytdl_sub/plugins/date_range.py @@ -0,0 +1,67 @@ +from typing import Dict +from typing import Optional + +from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder +from ytdl_sub.plugins.plugin import Plugin +from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.utils.datetime import to_date_range +from ytdl_sub.validators.string_datetime import StringDatetimeValidator + + +class DateRangeOptions(PluginOptions): + """ + Only download media within the specified date range + + Usage: + + .. code-block:: yaml + + presets: + my_example_preset: + date_range: + before: "now" + after: "today-2weeks" + """ + + _optional_keys = {"before", "after"} + + def __init__(self, name, value): + super().__init__(name, value) + self._before = self._validate_key_if_present("before", StringDatetimeValidator) + self._after = self._validate_key_if_present("after", StringDatetimeValidator) + + @property + def before(self) -> Optional[StringDatetimeValidator]: + """ + Optional. Only download videos before this datetime. + """ + return self._before + + @property + def after(self) -> Optional[StringDatetimeValidator]: + """ + Optional. Only download videos after this datetime. + """ + return self._after + + +class DateRangePlugin(Plugin[DateRangeOptions]): + plugin_options_type = DateRangeOptions + + def ytdl_options(self) -> Optional[Dict]: + """ + Returns + ------- + YTDL options for extracting audio + """ + ytdl_options_builder = YTDLOptionsBuilder() + + source_date_range = to_date_range( + before=self.plugin_options.before, + after=self.plugin_options.after, + overrides=self.overrides, + ) + if source_date_range: + ytdl_options_builder.add({"daterange": source_date_range}) + + return ytdl_options_builder.to_dict() diff --git a/src/ytdl_sub/subscriptions/subscription.py b/src/ytdl_sub/subscriptions/subscription.py index 5683cab1..251d4caf 100644 --- a/src/ytdl_sub/subscriptions/subscription.py +++ b/src/ytdl_sub/subscriptions/subscription.py @@ -19,6 +19,7 @@ from ytdl_sub.downloaders.downloader import DownloaderValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.subscriptions.subscription_ytdl_options import SubscriptionYTDLOptions +from ytdl_sub.utils.datetime import to_date_range from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandlerTransactionLog from ytdl_sub.utils.file_handler import FileMetadata @@ -230,7 +231,11 @@ class Subscription: # If output options maintains stale file deletion, perform the delete here prior to saving # the download archive if self.maintain_download_archive: - date_range_to_keep = self.output_options.get_upload_date_range_to_keep() + date_range_to_keep = to_date_range( + before=self.output_options.keep_files_before, + after=self.output_options.keep_files_after, + overrides=self.overrides, + ) if date_range_to_keep: self._enhanced_download_archive.remove_stale_files(date_range=date_range_to_keep) diff --git a/src/ytdl_sub/utils/datetime.py b/src/ytdl_sub/utils/datetime.py new file mode 100644 index 00000000..3231461a --- /dev/null +++ b/src/ytdl_sub/utils/datetime.py @@ -0,0 +1,56 @@ +from typing import Optional + +from yt_dlp import DateRange + +from ytdl_sub.config.preset_options import Overrides +from ytdl_sub.validators.string_datetime import StringDatetimeValidator + + +def to_date_range_hack( + before: Optional[StringDatetimeValidator], after: Optional[StringDatetimeValidator] +) -> Optional[DateRange]: + """ + Workaround for channel before/after support. + + Returns + ------- + Date range if the 'before' or 'after' is defined. None otherwise. + """ + start: Optional[str] = None + end: Optional[str] = None + + if after: + start = after.apply_formatter(variable_dict={}) + + if before: + end = before.apply_formatter(variable_dict={}) + + if start or end: + return DateRange(start=start, end=end) + + return None + + +def to_date_range( + before: Optional[StringDatetimeValidator], + after: Optional[StringDatetimeValidator], + overrides: Overrides, +) -> Optional[DateRange]: + """ + Returns + ------- + Date range if the 'before' or 'after' is defined. None otherwise. + """ + start: Optional[str] = None + end: Optional[str] = None + + if after: + start = overrides.apply_formatter(formatter=after) + + if before: + end = overrides.apply_formatter(formatter=before) + + if start or end: + return DateRange(start=start, end=end) + + return None diff --git a/src/ytdl_sub/validators/date_range_validator.py b/src/ytdl_sub/validators/date_range_validator.py deleted file mode 100644 index aa0268bd..00000000 --- a/src/ytdl_sub/validators/date_range_validator.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Optional - -from yt_dlp import DateRange - -from ytdl_sub.validators.strict_dict_validator import StrictDictValidator -from ytdl_sub.validators.string_datetime import StringDatetimeValidator - - -class DateRangeValidator(StrictDictValidator): - _optional_keys = {"before", "after"} - - def __init__(self, name, value): - super().__init__(name, value) - self._before = self._validate_key_if_present("before", StringDatetimeValidator) - self._after = self._validate_key_if_present("after", StringDatetimeValidator) - - @property - def before(self) -> Optional[StringDatetimeValidator]: - """Optional. Only download videos before this datetime.""" - return self._before - - @property - def after(self) -> Optional[StringDatetimeValidator]: - """Optional. Only download videos after this datetime.""" - return self._after - - def get_date_range(self) -> Optional[DateRange]: - """ - Returns - ------- - Date range if the 'before' or 'after' is defined. None otherwise. - """ - if self._before or self._after: - return DateRange( - start=self._after.datetime_str if self._after else None, - end=self._before.datetime_str if self._before else None, - ) - return None diff --git a/src/ytdl_sub/validators/string_datetime.py b/src/ytdl_sub/validators/string_datetime.py index 5e85c581..5b1c43f0 100644 --- a/src/ytdl_sub/validators/string_datetime.py +++ b/src/ytdl_sub/validators/string_datetime.py @@ -1,9 +1,11 @@ +from typing import Dict + from yt_dlp.utils import datetime_from_str -from ytdl_sub.validators.validators import Validator +from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator -class StringDatetimeValidator(Validator): +class StringDatetimeValidator(OverridesStringFormatterValidator): """ String that contains a yt-dlp datetime. From their docs: @@ -15,18 +17,12 @@ class StringDatetimeValidator(Validator): Valid examples are ``now-2weeks`` or ``20200101``. """ - _expected_value_type = str _expected_value_type_name = "datetime string" - def __init__(self, name, value): - super().__init__(name, value) - + def apply_formatter(self, variable_dict: Dict[str, str]) -> str: + output = super().apply_formatter(variable_dict) try: _ = datetime_from_str(self._value) except Exception as exc: - raise self._validation_exception(str(exc)) - - @property - def datetime_str(self) -> str: - """Returns the datetime as a string""" - return self._value + raise self._validation_exception(f"Invalid datetime string: {str(exc)}") + return output diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index 9a590e7f..97c61a84 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -151,7 +151,6 @@ class StringFormatterValidator(Validator): value=formatter.format_string.format(**OrderedDict(variable_dict)), ) - @final def apply_formatter(self, variable_dict: Dict[str, str]) -> str: """ Calls `format` on the format string using the variable_dict as input kwargs