insane number of changes

This commit is contained in:
jbannon 2022-04-22 20:34:44 +00:00
parent 6157648183
commit f2ec156138
14 changed files with 299 additions and 198 deletions

View file

@ -1,68 +0,0 @@
import copy
from abc import ABC
from typing import Any
from typing import Dict
from typing import Type
from ytdl_subscribe.downloaders.downloader import DownloaderValidator
from ytdl_subscribe.downloaders.soundcloud_downloader import (
SoundcloudAlbumsAndSinglesDownloadOptions,
)
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeChannelDownloaderOptions
from ytdl_subscribe.downloaders.youtube_downloader import YoutubePlaylistDownloaderOptions
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeVideoDownloaderOptions
from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.validators import StringValidator
class DownloadStrategyValidator(StrictDictValidator, ABC):
"""
Validates the download strategy of a source. Does not validate the source options.
"""
# 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
_download_strategy_to_source_mapping: Dict[str, Type[DownloaderValidator]] = {}
def __init__(self, name: str, value: Any):
super().__init__(name=name, value=value)
download_strategy_name = self._validate_key(
key="download_strategy",
validator=StringValidator,
).value
if download_strategy_name not in self._possible_download_strategies:
raise self._validation_exception(
f"download_strategy must be one of the following: "
f"{', '.join(self._possible_download_strategies)}"
)
# Remove the 'download_strategy' key before passing the other keys to the actual
# source validator
source_validator_dict = copy.deepcopy(self._dict)
del source_validator_dict["download_strategy"]
source_validator_class = self._download_strategy_to_source_mapping[download_strategy_name]
self.source_validator = source_validator_class(name=self._name, value=source_validator_dict)
@property
def _possible_download_strategies(self):
return sorted(list(self._download_strategy_to_source_mapping.keys()))
class SoundcloudDownloadStrategyValidator(DownloadStrategyValidator):
_download_strategy_to_source_mapping = {
"albums_and_singles": SoundcloudAlbumsAndSinglesDownloadOptions
}
class YoutubeDownloadStrategyValidator(DownloadStrategyValidator):
_download_strategy_to_source_mapping = {
"channel": YoutubeChannelDownloaderOptions,
"playlist": YoutubePlaylistDownloaderOptions,
"video": YoutubeVideoDownloaderOptions,
}

View file

@ -1,16 +1,20 @@
from abc import ABC
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from yt_dlp.utils import sanitize_filename
from ytdl_subscribe.config.download_strategy_validators import DownloadStrategyValidator
from ytdl_subscribe.config.download_strategy_validators import SoundcloudDownloadStrategyValidator
from ytdl_subscribe.config.download_strategy_validators import YoutubeDownloadStrategyValidator
from ytdl_subscribe.config.preset_class_mappings import DownloadStrategyMapping
from ytdl_subscribe.config.preset_class_mappings import PluginMapping
from ytdl_subscribe.downloaders.downloader import Downloader
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 PluginOptions
from ytdl_subscribe.utils.exceptions import StringFormattingVariableNotFoundException
from ytdl_subscribe.utils.exceptions import ValidationException
from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
@ -20,19 +24,15 @@ from ytdl_subscribe.validators.string_formatter_validators import StringFormatte
from ytdl_subscribe.validators.validators import BoolValidator
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
PRESET_SOURCE_VALIDATOR_MAPPING: Dict[str, Type[DownloadStrategyValidator]] = {
"soundcloud": SoundcloudDownloadStrategyValidator,
"youtube": YoutubeDownloadStrategyValidator,
}
PRESET_REQUIRED_KEYS = {"output_options"}
PRESET_OPTIONAL_KEYS = {
"metadata_options",
"ytdl_options",
"overrides",
*PRESET_SOURCE_VALIDATOR_MAPPING.keys(),
*DownloadStrategyMapping.sources(),
*PluginMapping.plugins(),
}
@ -109,38 +109,95 @@ class OutputOptions(StrictDictValidator):
)
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
_optional_keys = PRESET_OPTIONAL_KEYS
@property
def __available_sources(self) -> List[str]:
return sorted(list(PRESET_SOURCE_VALIDATOR_MAPPING.keys()))
def __validate_and_get_downloader(self, downloader_source: str) -> Type[Downloader]:
downloader_strategy = self._validate_key(
key=downloader_source, validator=DownloadStrategyValidator
).name
def __validate_and_get_subscription_source(self) -> DownloaderValidator:
download_strategy_validator: Optional[DownloadStrategyValidator] = None
return DownloadStrategyMapping.get(
source=downloader_source, download_strategy=downloader_strategy
)
def __validate_and_get_downloader_options(
self, downloader_source: str, downloader: Type[Downloader]
) -> DownloaderValidator:
# Remove the download_strategy key before validating it against the downloader options
# TODO: make this cleaner
del self._dict[downloader_source]["download_strategy"]
return self._validate_key(
key=downloader_source, validator=downloader.downloader_options_type
)
def __validate_and_get_downloader_and_options(
self,
) -> Tuple[Type[Downloader], DownloaderValidator]:
downloader: Optional[Type[Downloader]] = None
download_options: Optional[DownloaderValidator] = None
downloader_sources = DownloadStrategyMapping.sources()
for key in self._keys:
# skip if the key is not a download source
if key not in downloader_sources:
continue
# Ensure there are not multiple sources, i.e. youtube and soundcloud
if key in self.__available_sources and download_strategy_validator:
if downloader:
raise ValidationException(
f"'{self._name}' can only have one of the following sources: "
f"{', '.join(self.__available_sources)}"
f"{', '.join(downloader_sources)}"
)
if key in PRESET_SOURCE_VALIDATOR_MAPPING:
download_strategy_validator = self._validate_key(
key=key, validator=PRESET_SOURCE_VALIDATOR_MAPPING[key]
)
# If subscription source was not set, error
if not download_strategy_validator:
raise ValidationException(
f"'{self._name} must have one of the following sources: "
f"{', '.join(self.__available_sources)}"
downloader = self.__validate_and_get_downloader(downloader_source=key)
download_options = self.__validate_and_get_downloader_options(
downloader_source=key, downloader=downloader
)
return download_strategy_validator.source_validator
# If downloader was not set, error since it is required
if not downloader:
raise ValidationException(
f"'{self._name} must have one of the following sources: "
f"{', '.join(downloader_sources)}"
)
return downloader, download_options
def __validate_and_get_plugins(self) -> List[Tuple[Type[Plugin], PluginOptions]]:
plugins: List[Tuple[Type[Plugin], PluginOptions]] = []
for key in self._keys:
if key not in PluginMapping.plugins():
continue
plugin = PluginMapping.get(plugin=key)
plugin_options = self._validate_key(key=key, validator=plugin.plugin_options_type)
plugins.append((plugin, plugin_options))
return plugins
def __validate_override_string_formatter_validator(
self, formatter_validator: OverridesStringFormatterValidator
@ -186,20 +243,19 @@ class PresetValidator(StrictDictValidator):
def __init__(self, name: str, value: Any):
super().__init__(name=name, value=value)
self.subscription_source = self.__validate_and_get_subscription_source()
self.downloader, self.downloader_options = self.__validate_and_get_downloader_and_options()
self.output_options = self._validate_key(
key="output_options",
validator=OutputOptions,
)
# TODO: REPLACE METADATA OPTIONS WITH PLUGINS
self.ytdl_options = self._validate_key(
key="ytdl_options", validator=YTDLOptions, default={}
)
self.overrides = self._validate_key(key="overrides", validator=Overrides, default={})
self.plugins = self.__validate_and_get_plugins()
# After all options are initialized, perform a recursive post-validate that requires
# values from multiple validators

View file

@ -0,0 +1,107 @@
from typing import Dict
from typing import List
from typing import Type
from ytdl_subscribe.downloaders.downloader import Downloader
from ytdl_subscribe.downloaders.soundcloud_downloader import SoundcloudAlbumsAndSinglesDownloader
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeChannelDownloader
from ytdl_subscribe.downloaders.youtube_downloader import YoutubePlaylistDownloader
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeVideoDownloader
from ytdl_subscribe.plugins.convert_thumbnail import ConvertThumbnailPlugin
from ytdl_subscribe.plugins.music_tags import MusicTagsPlugin
from ytdl_subscribe.plugins.nfo_tags import NfoTagsPlugin
from ytdl_subscribe.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin
from ytdl_subscribe.plugins.plugin import Plugin
class DownloadStrategyMapping:
"""
Maps downloader strategies defined in the preset to its respective downloader class
"""
_MAPPING: Dict[str, Dict[str, Type[Downloader]]] = {
"youtube": {
"video": YoutubeVideoDownloader,
"playlist": YoutubePlaylistDownloader,
"channel": YoutubeChannelDownloader,
},
"soundcloud": {
"albums and singles": SoundcloudAlbumsAndSinglesDownloader,
},
}
@classmethod
def sources(cls) -> List[str]:
"""
Returns
-------
Available download sources
"""
return sorted(list(cls._MAPPING.keys()))
@classmethod
def _validate_is_source(cls, source: str) -> None:
"""
Ensure the source exists
"""
if source not in cls.sources():
raise ValueError(f"Tried to use source '{source}' that does not exist")
@classmethod
def source_download_strategies(cls, source: str) -> List[str]:
"""
Parameters
----------
source
Name of the source
Returns
-------
Available download strategies for the given source
"""
cls._validate_is_source(source)
return sorted(list(cls._MAPPING[source].keys()))
@classmethod
def _validate_is_download_strategy(cls, source: str, download_strategy: str) -> None:
"""
Ensure the download strategy for the given source exists
"""
if download_strategy not in cls.source_download_strategies(source):
raise ValueError(
f"Tried to use download strategy '{download_strategy}' with source '{source}', "
f"which does not exist"
)
@classmethod
def get(cls, source: str, download_strategy: str) -> Type[Downloader]:
cls._validate_is_download_strategy(source, download_strategy)
return cls._MAPPING[source][download_strategy]
class PluginMapping:
"""
Maps plugins defined in the preset to its respective plugin class
"""
_MAPPING: Dict[str, Type[Plugin]] = {
"convert_thumbnail": ConvertThumbnailPlugin,
"music_tags": MusicTagsPlugin,
"nfo_tags": NfoTagsPlugin,
"output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin,
}
@classmethod
def plugins(cls) -> List[str]:
"""
Returns
-------
Available download sources
"""
return sorted(list(cls._MAPPING.keys()))
@classmethod
def get(cls, plugin: str) -> Type[Plugin]:
if plugin not in cls.plugins():
raise ValueError(f"Tried to use plugin '{plugin}' that does not exist")
return cls._MAPPING[plugin]

View file

@ -58,25 +58,11 @@ class SubscriptionValidator(StrictDictValidator):
)
def to_subscription(self) -> Subscription:
# TODO: Fix this abomination
return None
# subscription_class: Optional[Type[Subscription]] = None
# if isinstance(self.preset.subscription_source, SoundcloudAlbumsAndSinglesDownloadOptions):
# subscription_class = SoundcloudAlbumsAndSinglesSubscription
# elif isinstance(self.preset.subscription_source, YoutubePlaylistDownloaderValidator):
# subscription_class = YoutubePlaylistSubscription
# elif isinstance(self.preset.subscription_source, YoutubeVideoDownloaderValidator):
# subscription_class = YoutubeVideoSubscription
# elif isinstance(self.preset.subscription_source, YoutubeChannelDownloaderValidator):
# subscription_class = YoutubeChannelSubscription
# if subscription_class is None:
# raise ValueError("subscription source class not found")
#
# return subscription_class(
# name=self._name,
# config_options=self.config.config_options,
# preset_options=self.preset,
# )
return Subscription(
name=self._name,
config_options=self.config.config_options,
preset_options=self.preset,
)
@classmethod
def from_dict(

View file

@ -6,6 +6,7 @@ from typing import Dict
from typing import Generic
from typing import List
from typing import Optional
from typing import Type
from typing import TypeVar
import yt_dlp as ytdl
@ -24,12 +25,15 @@ DownloaderOptionsT = TypeVar("DownloaderOptionsT", bound=DownloaderValidator)
DownloaderEntryT = TypeVar("DownloaderEntryT", bound=Entry)
class Downloader(Generic[DownloaderOptionsT, DownloaderEntryT]):
class Downloader(Generic[DownloaderOptionsT, DownloaderEntryT], ABC):
"""
Class that interacts with ytdl to perform the download of metadata and content,
and should translate that to list of Entry objects.
"""
downloader_options_type: Type[DownloaderOptionsT] = NotImplemented
downloader_entry_type: Type[DownloaderEntryT] = NotImplemented
@classmethod
def ytdl_option_overrides(cls) -> Dict:
"""Global overrides that even overwrite user input"""

View file

@ -35,8 +35,8 @@ SoundcloudDownloaderOptionsT = TypeVar(
class SoundcloudDownloader(
Generic[SoundcloudDownloaderOptionsT],
Downloader[SoundcloudDownloaderOptionsT, SoundcloudTrack],
Generic[SoundcloudDownloaderOptionsT],
ABC,
):
"""
@ -44,6 +44,8 @@ class SoundcloudDownloader(
SoundcloudTrack / SoundcloudAlbumTrack objects
"""
downloader_entry_type = SoundcloudTrack
@classmethod
def ytdl_option_defaults(cls) -> Dict:
"""Returns default format to be best mp3"""
@ -97,6 +99,8 @@ class SoundcloudAlbumsAndSinglesDownloadOptions(SoundcloudDownloaderOptions):
class SoundcloudAlbumsAndSinglesDownloader(
SoundcloudDownloader[SoundcloudAlbumsAndSinglesDownloadOptions]
):
downloader_options_type = SoundcloudDownloaderOptions
def download(self) -> List[SoundcloudTrack]:
"""
Soundcloud subscription to download albums and tracks as singles.

View file

@ -30,13 +30,15 @@ YoutubeDownloaderOptionsT = TypeVar("YoutubeDownloaderOptionsT", bound=YoutubeDo
class YoutubeDownloader(
Generic[YoutubeDownloaderOptionsT], Downloader[YoutubeDownloaderOptionsT, YoutubeVideo], ABC
Downloader[YoutubeDownloaderOptionsT, YoutubeVideo], Generic[YoutubeDownloaderOptionsT], ABC
):
"""
Class that handles downloading youtube entries via ytdl and converting them into
YoutubeVideo objects
"""
downloader_entry_type = YoutubeVideo
@classmethod
def playlist_url(cls, playlist_id: str) -> str:
"""Returns full playlist url"""
@ -131,6 +133,8 @@ class YoutubeVideoDownloaderOptions(YoutubeDownloaderOptions):
class YoutubeVideoDownloader(YoutubeDownloader[YoutubeVideoDownloaderOptions]):
downloader_options_type = YoutubeVideoDownloaderOptions
def download(self) -> List[YoutubeVideo]:
video = self.download_video(video_id=self.download_options.video_id.value)
return [video]
@ -149,6 +153,8 @@ class YoutubePlaylistDownloaderOptions(YoutubeDownloaderOptions):
class YoutubePlaylistDownloader(YoutubeDownloader[YoutubePlaylistDownloaderOptions]):
downloader_options_type = YoutubePlaylistDownloaderOptions
def download(self) -> List[YoutubeVideo]:
return self.download_playlist(playlist_id=self.download_options.playlist_id.value)
@ -168,6 +174,8 @@ class YoutubeChannelDownloaderOptions(YoutubeDownloaderOptions, DownloadDateRang
class YoutubeChannelDownloader(YoutubeDownloader[YoutubeChannelDownloaderOptions]):
downloader_options_type = YoutubeChannelDownloaderOptions
def download(self) -> List[YoutubeVideo]:
ytdl_options_overrides = {}
source_date_range = self.download_options.get_date_range()

View file

@ -4,7 +4,7 @@ from PIL.Image import Image
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.plugins.plugin import PluginOptions
from ytdl_subscribe.validators.string_select_validator import StringSelectValidator
@ -14,7 +14,7 @@ class ThumbnailTypes(StringSelectValidator):
_select_values = {"jpg"}
class ConvertThumbnailValidator(PluginValidator):
class ConvertThumbnailOptions(PluginOptions):
_required_keys = {"to"}
def __init__(self, name, value):
@ -22,7 +22,9 @@ class ConvertThumbnailValidator(PluginValidator):
self.convert_to = self._validate_key(key="to", validator=ThumbnailTypes)
class ConvertThumbnail(Plugin[ConvertThumbnailValidator]):
class ConvertThumbnailPlugin(Plugin[ConvertThumbnailOptions]):
plugin_options_type = ConvertThumbnailOptions
def post_process_entry(self, entry: Entry):
"""
Convert the source thumbnail to the desired type. Leave the original file name and

View file

@ -2,12 +2,12 @@ import music_tag
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.plugins.plugin import PluginOptions
from ytdl_subscribe.validators.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.validators import StringValidator
class MusicTagsValidator(PluginValidator):
class MusicTagsOptions(PluginOptions):
_required_keys = {"tags"}
_optional_keys = {"multi_value_separator"}
@ -21,7 +21,9 @@ class MusicTagsValidator(PluginValidator):
)
class MusicTags(Plugin[MusicTagsValidator]):
class MusicTagsPlugin(Plugin[MusicTagsOptions]):
plugin_options_type = MusicTagsOptions
def post_process_entry(self, entry: Entry):
"""
Tags the entry's audio file using values defined in the metadata options

View file

@ -4,12 +4,12 @@ import dicttoxml
from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.plugins.plugin import PluginOptions
from ytdl_subscribe.validators.string_formatter_validators import DictFormatterValidator
from ytdl_subscribe.validators.string_formatter_validators import StringFormatterValidator
class NfoTagsValidator(PluginValidator):
class NfoTagsOptions(PluginOptions):
_required_keys = {"nfo_name", "nfo_root", "tags"}
def __init__(self, name, value):
@ -20,7 +20,9 @@ class NfoTagsValidator(PluginValidator):
self.tags = self._validate_key(key="tags", validator=DictFormatterValidator)
class NfoTags(Plugin[NfoTagsValidator]):
class NfoTagsPlugin(Plugin[NfoTagsOptions]):
plugin_options_type = NfoTagsOptions
def post_process_entry(self, entry: Entry):
"""
Creates an entry's NFO file using values defined in the metadata options

View file

@ -3,12 +3,12 @@ from pathlib import Path
import dicttoxml
from ytdl_subscribe.plugins.plugin import Plugin
from ytdl_subscribe.plugins.plugin import PluginValidator
from ytdl_subscribe.plugins.plugin import PluginOptions
from ytdl_subscribe.validators.string_formatter_validators import OverridesDictFormatterValidator
from ytdl_subscribe.validators.string_formatter_validators import OverridesStringFormatterValidator
class OutputDirectoryNfoTagsValidator(PluginValidator):
class OutputDirectoryNfoTagsOptions(PluginOptions):
"""
Validates config values for the output directory nfo tags plugin.
"""
@ -28,7 +28,9 @@ class OutputDirectoryNfoTagsValidator(PluginValidator):
self.tags = self._validate_key(key="tags", validator=OverridesDictFormatterValidator)
class NfoTags(Plugin[OutputDirectoryNfoTagsValidator]):
class OutputDirectoryNfoTagsPlugin(Plugin[OutputDirectoryNfoTagsOptions]):
plugin_options_type = OutputDirectoryNfoTagsOptions
def post_process_subscription(self):
"""
Creates an NFO file in the root of the output directory

View file

@ -1,5 +1,6 @@
from abc import ABC
from typing import Generic
from typing import Type
from typing import TypeVar
from typing import final
@ -9,24 +10,26 @@ from ytdl_subscribe.validators.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
class PluginValidator(StrictDictValidator):
class PluginOptions(StrictDictValidator):
"""
Class that defines the parameters to a plugin
"""
PluginValidatorT = TypeVar("PluginValidatorT", bound=PluginValidator)
PluginOptionsT = TypeVar("PluginOptionsT", bound=PluginOptions)
class Plugin(Generic[PluginValidatorT], ABC):
class Plugin(Generic[PluginOptionsT], ABC):
"""
Class to define the new plugin functionality
"""
plugin_options_type: Type[PluginOptionsT] = NotImplemented
@final
def __init__(
self,
plugin_options: PluginValidatorT,
plugin_options: PluginOptionsT,
output_directory: str,
overrides: Overrides,
enhanced_download_archive: EnhancedDownloadArchive,

View file

@ -1,32 +1,29 @@
import contextlib
import os
import shutil
from abc import ABC
from pathlib import Path
from shutil import copyfile
from typing import Dict
from typing import Generic
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TypeVar
from ytdl_subscribe.config.config_file import ConfigOptions
from ytdl_subscribe.config.preset import OutputOptions
from ytdl_subscribe.config.preset import Overrides
from ytdl_subscribe.config.preset import PresetValidator
from ytdl_subscribe.config.preset import YTDLOptions
from ytdl_subscribe.downloaders.downloader import Downloader
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 PluginOptions
from ytdl_subscribe.validators.date_range_validator import DownloadDateRangeSource
from ytdl_subscribe.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
SourceT = TypeVar("SourceT", bound=DownloaderValidator)
EntryT = TypeVar("EntryT", bound=Entry)
DownloaderT = TypeVar("DownloaderT", bound=Downloader)
class Subscription(Generic[SourceT, EntryT], ABC):
class Subscription:
"""
Subscription classes are the 'controllers' that perform...
@ -65,16 +62,20 @@ class Subscription(Generic[SourceT, EntryT], ABC):
)
@property
def entry_type(self) -> Type[EntryT]:
"""
:return: The Entry type this subscription uses to represent downloaded media
"""
return EntryT.__class__
def downloader_class(self) -> Type[Downloader]:
return self.__preset_options.downloader
@property
def source_options(self) -> SourceT:
"""Returns the source options defined for this subscription"""
return self.__preset_options.subscription_source
def downloader_options(self) -> DownloaderValidator:
return self.__preset_options.downloader_options
@property
def plugins(self) -> List[Tuple[Type[Plugin], PluginOptions]]:
return self.__preset_options.plugins
@property
def ytdl_options(self) -> YTDLOptions:
return self.__preset_options.ytdl_options
@property
def output_options(self) -> OutputOptions:
@ -98,21 +99,6 @@ class Subscription(Generic[SourceT, EntryT], ABC):
"""
return self.overrides.apply_formatter(formatter=self.output_options.output_directory)
def get_downloader(
self, downloader_type: Type[DownloaderT], source_ytdl_options: Optional[Dict] = None
) -> DownloaderT:
"""Returns the downloader that will be used to download media for this subscription"""
# if source_ytdl_options are present, override the ytdl_options with them
ytdl_options = self.__preset_options.ytdl_options.dict
if source_ytdl_options:
ytdl_options = dict(ytdl_options, **source_ytdl_options)
return downloader_type(
working_directory=self.working_directory,
ytdl_options=ytdl_options,
download_archive_file_name=self._enhanced_download_archive.archive_file_name,
)
def _copy_file_to_output_directory(self, source_file_path: str, output_file_name: str):
destination_file_path = Path(self.output_directory) / Path(output_file_name)
os.makedirs(os.path.dirname(destination_file_path), exist_ok=True)
@ -157,42 +143,49 @@ class Subscription(Generic[SourceT, EntryT], ABC):
yield
date_range = None
if isinstance(self.source_options, DownloadDateRangeSource):
date_range = self.source_options.get_date_range()
# TODO: add date range in output options
# date_range = None
# if isinstance(self.source_options, DownloadDateRangeSource):
# date_range = self.source_options.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()
if date_range and self.output_options.maintain_stale_file_deletion:
self._enhanced_download_archive.remove_stale_files(date_range=date_range)
def _initialize_plugins(self) -> List[Plugin]:
plugins: List[Plugin] = []
for plugin_type, plugin_options in self.plugins:
plugin = plugin_type(
plugin_options=plugin_options,
output_directory=self.output_directory,
overrides=self.overrides,
enhanced_download_archive=self._enhanced_download_archive,
)
self._enhanced_download_archive.save_download_archive()
plugins.append(plugin)
return plugins
def download(self):
"""
Performs the subscription download.
"""
plugins = self._initialize_plugins()
with self._prepare_working_directory(), self._maintain_archive_file():
entries = self._extract_info()
downloader = self.downloader_class(
working_directory=self.working_directory,
download_options=self.downloader_options,
ytdl_options=self.ytdl_options.dict,
download_archive_file_name=self._enhanced_download_archive.archive_file_name,
)
entries = downloader.download()
for plugin in plugins:
for entry in entries:
plugin.post_process_entry(entry)
plugin.post_process_subscription()
for entry in entries:
self.post_process_entry(entry)
def _extract_info(self) -> List[EntryT]:
"""
Extracts only the info of the source, does not download it
"""
raise NotImplementedError("Each source needs to implement how it extracts info")
def post_process_entry(self, entry: Entry) -> None:
"""
After downloading an entry to the working directory, perform all post-processing, which
includes:
* Adding metadata to the entry file itself (music tags)
* Moving the entry file + thumbnail to the output directory with its formatted name
* Creating new metadata files (NFO) to reside alongside the entry files
:param entry: The entry to post-process
"""
# before move
self._copy_entry_files_to_output_directory(entry=entry)
# after move
self._copy_entry_files_to_output_directory(entry=entry)