too many changes for single commit

This commit is contained in:
jbannon 2022-04-05 06:45:26 +00:00
parent e0a375164c
commit 6215496c10
8 changed files with 94 additions and 64 deletions

View file

@ -12,14 +12,22 @@ from ytdl_subscribe.validators.config.sources.soundcloud_validators import (
) )
class SoundcloudAlbumsAndSinglesSubscription(Subscription): class SoundcloudSubscription(Subscription):
source_validator_type = SoundcloudSourceValidator source_validator_type = SoundcloudSourceValidator
download_strategy_type = SoundcloudAlbumsAndSinglesDownloadValidator downloader_type = SoundcloudDownloader
@property @property
def source_options(self) -> SoundcloudSourceValidator: def source_options(self) -> SoundcloudSourceValidator:
return super().source_options return super().source_options
@property
def downloader(self) -> SoundcloudDownloader:
return super().downloader
class SoundcloudAlbumsAndSinglesSubscription(SoundcloudSubscription):
download_strategy_type = SoundcloudAlbumsAndSinglesDownloadValidator
@property @property
def download_strategy_options(self) -> SoundcloudAlbumsAndSinglesDownloadValidator: def download_strategy_options(self) -> SoundcloudAlbumsAndSinglesDownloadValidator:
return super().download_strategy_options return super().download_strategy_options
@ -27,16 +35,11 @@ class SoundcloudAlbumsAndSinglesSubscription(Subscription):
def extract_info( def extract_info(
self, self,
): ):
tracks: List[SoundcloudTrack] = [] tracks: List[SoundcloudTrack] = []
soundcloud_downloader = SoundcloudDownloader(
output_directory=self.config_options.working_directory.value,
ytdl_options=self.ytdl_options.dict,
)
# Get the album info first. This tells us which track ids belong # Get the album info first. This tells us which track ids belong
# to an album. Unfortunately we cannot use download_archive or info.json for this # to an album. Unfortunately we cannot use download_archive or info.json for this
albums: List[SoundcloudAlbum] = soundcloud_downloader.download_albums( albums: List[SoundcloudAlbum] = self.downloader.download_albums(
artist_name=self.download_strategy_options.username.value artist_name=self.download_strategy_options.username.value
) )
@ -46,7 +49,7 @@ class SoundcloudAlbumsAndSinglesSubscription(Subscription):
) )
# only add tracks that are not part of an album # only add tracks that are not part of an album
single_tracks = soundcloud_downloader.download_tracks( single_tracks = self.downloader.download_tracks(
artist_name=self.download_strategy_options.username.value artist_name=self.download_strategy_options.username.value
) )
tracks += [ tracks += [

View file

@ -8,6 +8,7 @@ import dicttoxml
import music_tag import music_tag
from PIL import Image from PIL import Image
from ytdl_subscribe.downloaders.downloader import Downloader
from ytdl_subscribe.entries.entry import Entry from ytdl_subscribe.entries.entry import Entry
from ytdl_subscribe.validators.config.config_validator import ConfigOptionsValidator from ytdl_subscribe.validators.config.config_validator import ConfigOptionsValidator
from ytdl_subscribe.validators.config.metadata_options.metadata_options_validator import ( from ytdl_subscribe.validators.config.metadata_options.metadata_options_validator import (
@ -25,11 +26,15 @@ from ytdl_subscribe.validators.config.sources.source_validator import SourceVali
SOURCE_T = TypeVar("SOURCE_T", bound=SourceValidator) SOURCE_T = TypeVar("SOURCE_T", bound=SourceValidator)
DOWNLOAD_STRATEGY_T = TypeVar("DOWNLOAD_STRATEGY_T", bound=DownloadStrategyValidator) DOWNLOAD_STRATEGY_T = TypeVar("DOWNLOAD_STRATEGY_T", bound=DownloadStrategyValidator)
DOWNLOADER_T = TypeVar("DOWNLOADER_T", bound=Downloader)
class Subscription(object): class Subscription(object):
SOURCE_T = TypeVar("SOURCE_T", bound=SourceValidator)
source_validator_type: Type[SOURCE_T] source_validator_type: Type[SOURCE_T]
download_strategy_type: Type[DOWNLOAD_STRATEGY_T] download_strategy_type: Type[DOWNLOAD_STRATEGY_T]
downloader_type: Type[Downloader]
def __init__( def __init__(
self, self,
@ -54,17 +59,14 @@ class Subscription(object):
overrides: OverridesValidator overrides: OverridesValidator
""" """
self.name = name self.name = name
self.config_options = config_options self._config_options = config_options
self._source_options = source_options
self._download_strategy_options = source_options.download_strategy
self.__source_options = source_options if not isinstance(self._source_options, self.source_validator_type):
self.__download_strategy_options = source_options.download_strategy
if not isinstance(self.__source_options, self.source_validator_type):
raise ValueError("Source options does not match the expected type") raise ValueError("Source options does not match the expected type")
if not isinstance( if not isinstance(self._download_strategy_options, self.download_strategy_type):
self.__download_strategy_options, self.download_strategy_type
):
raise ValueError("Download strategy does not match the expected type") raise ValueError("Download strategy does not match the expected type")
self.output_options = output_options self.output_options = output_options
@ -74,18 +76,28 @@ class Subscription(object):
@property @property
def source_options(self) -> SOURCE_T: def source_options(self) -> SOURCE_T:
return self.__source_options return self._source_options
@property @property
def download_strategy_options(self) -> DOWNLOAD_STRATEGY_T: def download_strategy_options(self) -> DOWNLOAD_STRATEGY_T:
return self.__download_strategy_options return self._download_strategy_options
@property
def working_directory(self) -> str:
"""Returns the directory that the downloader saves files to"""
return str(Path(self._config_options.working_directory.value) / Path(self.name))
@property
def downloader(self) -> DOWNLOADER_T:
return self.downloader_type(
output_directory=self.working_directory,
ytdl_options=self.ytdl_options.dict,
)
def _post_process_tagging(self, entry: Entry): def _post_process_tagging(self, entry: Entry):
id3_options = self.metadata_options.id3 id3_options = self.metadata_options.id3
t = music_tag.load_file( t = music_tag.load_file(
entry.file_path( entry.file_path(relative_directory=self.working_directory)
relative_directory=self.config_options.working_directory.value
)
) )
for tag, tag_formatter in id3_options.tags.dict.items(): for tag, tag_formatter in id3_options.tags.dict.items():
t[tag] = entry.apply_formatter( t[tag] = entry.apply_formatter(
@ -131,7 +143,7 @@ class Subscription(object):
# Move the file after all direct file modifications are complete # Move the file after all direct file modifications are complete
entry_source_file_path = entry.file_path( entry_source_file_path = entry.file_path(
relative_directory=self.config_options.working_directory.value relative_directory=self.working_directory
) )
output_directory = entry.apply_formatter( output_directory = entry.apply_formatter(
@ -150,7 +162,7 @@ class Subscription(object):
# Download the thumbnail if its present # Download the thumbnail if its present
if self.output_options.thumbnail_name: if self.output_options.thumbnail_name:
source_thumbnail_path = entry.thumbnail_path( source_thumbnail_path = entry.thumbnail_path(
relative_directory=self.config_options.working_directory.value relative_directory=self.working_directory
) )
output_thumbnail_name = entry.apply_formatter( output_thumbnail_name = entry.apply_formatter(

View file

@ -10,23 +10,26 @@ from ytdl_subscribe.validators.config.sources.youtube_validators import (
class YoutubeSubscription(Subscription): class YoutubeSubscription(Subscription):
source_validator_type = YoutubeSourceValidator source_validator_type = YoutubeSourceValidator
download_strategy_type = YoutubePlaylistDownloadValidator downloader_type = YoutubeDownloader
@property @property
def source_options(self) -> YoutubeSourceValidator: def source_options(self) -> YoutubeSourceValidator:
return super().source_options return super().source_options
@property
def downloader(self) -> YoutubeDownloader:
return super().downloader
class YoutubePlaylistSubscription(YoutubeSubscription):
download_strategy_type = YoutubePlaylistDownloadValidator
@property @property
def download_strategy_options(self) -> YoutubePlaylistDownloadValidator: def download_strategy_options(self) -> YoutubePlaylistDownloadValidator:
return super().download_strategy_options return super().download_strategy_options
def extract_info(self): def extract_info(self):
youtube_downloader = YoutubeDownloader( entries = self.downloader.download_playlist(
output_directory=self.config_options.working_directory.value,
ytdl_options=self.ytdl_options.dict,
)
entries = youtube_downloader.download_playlist(
playlist_id=self.download_strategy_options.playlist_id.value playlist_id=self.download_strategy_options.playlist_id.value
) )

View file

@ -1,9 +1,8 @@
import re import re
from keyword import iskeyword from keyword import iskeyword
from typing import Dict
from typing import List from typing import List
from ytdl_subscribe.validators.base.validators import DictValidator from ytdl_subscribe.validators.base.validators import LiteralDictValidator
from ytdl_subscribe.validators.base.validators import StringValidator from ytdl_subscribe.validators.base.validators import StringValidator
@ -69,7 +68,7 @@ class StringFormatterValidator(StringValidator):
return self._value return self._value
class DictFormatterValidator(DictValidator): class DictFormatterValidator(LiteralDictValidator):
""" """
Validates a dictionary made up of key: string_formatters Validates a dictionary made up of key: string_formatters
""" """
@ -79,7 +78,3 @@ class DictFormatterValidator(DictValidator):
for key in self._keys: for key in self._keys:
_ = self._validate_key(key=key, validator=StringFormatterValidator) _ = self._validate_key(key=key, validator=StringFormatterValidator)
@property
def dict(self) -> Dict[str, str]:
return self._dict

View file

@ -1,4 +1,5 @@
from typing import Any from typing import Any
from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Type from typing import Type
@ -154,3 +155,15 @@ class DictValidator(Validator):
return None return None
return self._validate_key(key=key, validator=validator, default=default) return self._validate_key(key=key, validator=validator, default=default)
class LiteralDictValidator(DictValidator):
"""DictValidator with exposed dict and keys method"""
@property
def dict(self) -> Dict:
return super()._dict
@property
def keys(self) -> List[str]:
return super()._keys

View file

@ -1,9 +1,11 @@
from typing import Any from typing import Any
from typing import Dict
import yaml import yaml
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.validators import DictValidator from ytdl_subscribe.validators.base.validators import DictValidator
from ytdl_subscribe.validators.base.validators import LiteralDictValidator
from ytdl_subscribe.validators.base.validators import StringValidator from ytdl_subscribe.validators.base.validators import StringValidator
@ -18,6 +20,12 @@ class ConfigOptionsValidator(StrictDictValidator):
) )
class ConfigPresetsValidator(LiteralDictValidator):
"""Shallow validator checking for the presets dict in the config"""
pass
class ConfigFileValidator(StrictDictValidator): class ConfigFileValidator(StrictDictValidator):
_required_keys = {"configuration", "presets"} _required_keys = {"configuration", "presets"}
@ -26,7 +34,7 @@ class ConfigFileValidator(StrictDictValidator):
self.config_options = self._validate_key( self.config_options = self._validate_key(
"configuration", ConfigOptionsValidator "configuration", ConfigOptionsValidator
) )
self.presets = self._validate_key("presets", DictValidator) self.presets = self._validate_key("presets", ConfigPresetsValidator)
@classmethod @classmethod
def from_dict(cls, config_dict) -> "ConfigFileValidator": def from_dict(cls, config_dict) -> "ConfigFileValidator":

View file

@ -13,6 +13,7 @@ from ytdl_subscribe.validators.base.string_formatter_validator import (
DictFormatterValidator, DictFormatterValidator,
) )
from ytdl_subscribe.validators.base.validators import DictValidator from ytdl_subscribe.validators.base.validators import DictValidator
from ytdl_subscribe.validators.base.validators import LiteralDictValidator
from ytdl_subscribe.validators.config.metadata_options.metadata_options_validator import ( from ytdl_subscribe.validators.config.metadata_options.metadata_options_validator import (
MetadataOptionsValidator, MetadataOptionsValidator,
) )
@ -29,23 +30,21 @@ from ytdl_subscribe.validators.config.sources.youtube_validators import (
from ytdl_subscribe.validators.exceptions import ValidationException from ytdl_subscribe.validators.exceptions import ValidationException
class YTDLOptionsValidator(DictValidator): class YTDLOptionsValidator(LiteralDictValidator):
@property """Ensures `ytdl_options` is a dict"""
def dict(self) -> Dict:
return self._dict pass
class OverridesValidator(DictFormatterValidator): class OverridesValidator(DictFormatterValidator):
@property """Ensures `overrides` is a dict"""
def dict(self) -> Dict[str, str]:
"""For overrides, create sanitized versions of each entry for convenience"""
output_dict = copy.deepcopy(self._dict)
for key in self._keys:
output_dict[f"sanitized_{key}"] = sanitize_filename.sanitize(
output_dict[key]
)
return output_dict def __init__(self, name, value):
super().__init__(name, value)
for key in self._keys:
self._value[f"sanitized_{key}"] = sanitize_filename.sanitize(
self._value[key]
)
PRESET_SOURCE_VALIDATOR_MAPPING: Dict[str, Type[SourceValidator]] = { PRESET_SOURCE_VALIDATOR_MAPPING: Dict[str, Type[SourceValidator]] = {

View file

@ -1,3 +1,4 @@
import copy
from typing import Any from typing import Any
from typing import List from typing import List
@ -8,15 +9,12 @@ from ytdl_subscribe.subscriptions.soundcloud import (
SoundcloudAlbumsAndSinglesSubscription, SoundcloudAlbumsAndSinglesSubscription,
) )
from ytdl_subscribe.subscriptions.subscription import Subscription from ytdl_subscribe.subscriptions.subscription import Subscription
from ytdl_subscribe.subscriptions.youtube import YoutubeSubscription from ytdl_subscribe.subscriptions.youtube import YoutubePlaylistSubscription
from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator from ytdl_subscribe.validators.base.strict_dict_validator import StrictDictValidator
from ytdl_subscribe.validators.base.validators import StringValidator from ytdl_subscribe.validators.base.validators import StringValidator
from ytdl_subscribe.validators.config.config_validator import ConfigFileValidator from ytdl_subscribe.validators.config.config_validator import ConfigFileValidator
from ytdl_subscribe.validators.config.preset_validator import PRESET_OPTIONAL_KEYS from ytdl_subscribe.validators.config.preset_validator import PRESET_OPTIONAL_KEYS
from ytdl_subscribe.validators.config.preset_validator import PRESET_REQUIRED_KEYS from ytdl_subscribe.validators.config.preset_validator import PRESET_REQUIRED_KEYS
from ytdl_subscribe.validators.config.preset_validator import (
PRESET_SOURCE_VALIDATOR_MAPPING,
)
from ytdl_subscribe.validators.config.preset_validator import OverridesValidator from ytdl_subscribe.validators.config.preset_validator import OverridesValidator
from ytdl_subscribe.validators.config.preset_validator import PresetValidator from ytdl_subscribe.validators.config.preset_validator import PresetValidator
from ytdl_subscribe.validators.config.sources.soundcloud_validators import ( from ytdl_subscribe.validators.config.sources.soundcloud_validators import (
@ -51,18 +49,17 @@ class SubscriptionValidator(StrictDictValidator):
validator=StringValidator, validator=StringValidator,
).value ).value
available_presets = self.config.presets._keys if preset_name not in self.config.presets.keys:
if preset_name not in available_presets:
raise self._validation_exception( raise self._validation_exception(
f"'preset '{preset_name}' does not exist in the provided config. " f"'preset '{preset_name}' does not exist in the provided config. "
f"Available presets: {', '.join(available_presets)}" f"Available presets: {', '.join(self.config.presets.keys)}"
) )
# A little hacky, we will override the preset with the contents of this subscription, then validate it # A little hacky, we will override the preset with the contents of this subscription,
# then validate it
preset_dict = copy.deepcopy(self.config.presets.dict[preset_name])
preset_dict = mergedeep.merge( preset_dict = mergedeep.merge(
self.config.presets._dict[preset_name], preset_dict, self._dict, strategy=mergedeep.Strategy.REPLACE
self._dict,
strategy=mergedeep.Strategy.REPLACE,
) )
del preset_dict["preset"] del preset_dict["preset"]
@ -81,7 +78,7 @@ class SubscriptionValidator(StrictDictValidator):
self.preset.subscription_source.download_strategy, self.preset.subscription_source.download_strategy,
YoutubePlaylistDownloadValidator, YoutubePlaylistDownloadValidator,
): ):
subscription_class = YoutubeSubscription subscription_class = YoutubePlaylistSubscription
else: else:
raise ValueError("subscription source class not found") raise ValueError("subscription source class not found")