too many changes for single commit
This commit is contained in:
parent
e0a375164c
commit
6215496c10
8 changed files with 94 additions and 64 deletions
|
|
@ -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 += [
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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]] = {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue