begin parsing yaml better
This commit is contained in:
parent
f887b56729
commit
d31c4313f2
16 changed files with 276 additions and 92 deletions
39
config.yaml
Normal file
39
config.yaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
config:
|
||||
working_directory: 'tmp'
|
||||
|
||||
presets:
|
||||
soundcloud_with_id3_tags:
|
||||
soundcloud:
|
||||
download_strategy: "albums_then_tracks"
|
||||
skip_premiere_tracks: True
|
||||
ytdl_opts:
|
||||
format: 'bestaudio[ext=mp3]'
|
||||
post_process:
|
||||
file_name: "[{album_year}] {sanitized_album}/{track_number_padded} - {sanitized_title}.{ext}"
|
||||
thumbnail_name: "[{album_year}] {sanitized_album}/folder.{thumbnail_ext}"
|
||||
tagging:
|
||||
artist: "{artist}"
|
||||
albumartist: "{artist}"
|
||||
title: "{title}"
|
||||
album: "{album}"
|
||||
tracknumber: "{track_number}"
|
||||
year: "{album_year}"
|
||||
genre: "{genre}"
|
||||
|
||||
music_videos:
|
||||
youtube:
|
||||
download_strategy: "playlist"
|
||||
ytdl_opts:
|
||||
format: 'best'
|
||||
ignoreerrors: True
|
||||
post_process:
|
||||
file_name: "{sanitized_artist} - {sanitized_title}.{ext}"
|
||||
convert_thumbnail: "jpg"
|
||||
thumbnail_name: "{sanitized_artist} - {sanitized_title}.jpg"
|
||||
nfo_name: "{sanitized_artist} - {sanitized_title}.nfo"
|
||||
nfo_root: "musicvideo"
|
||||
nfo:
|
||||
artist: "{artist}"
|
||||
title: "{title}"
|
||||
album: "Music Videos"
|
||||
year: "{upload_year}"
|
||||
|
|
@ -3,6 +3,7 @@ black==21.11b1
|
|||
coverage==6.3.2
|
||||
dicttoxml==1.7.4
|
||||
isort==5.10.1
|
||||
jsonschema==4.4.0
|
||||
mergedeep==1.3.4
|
||||
music-tag==0.4.3
|
||||
pathvalidate==2.4.1
|
||||
|
|
|
|||
|
|
@ -1,81 +1,42 @@
|
|||
config:
|
||||
working_directory: 'tmp'
|
||||
ALISON:
|
||||
preset: "soundcloud_with_id3_tags"
|
||||
soundcloud:
|
||||
username: alis_on
|
||||
output_path: "/mnt/nas/Downloads/ytdl/music/ALISON"
|
||||
overrides:
|
||||
artist: "A.L.I.S.O.N."
|
||||
genre: "Synthwave / Electronic"
|
||||
|
||||
presets:
|
||||
soundcloud_with_id3_tags:
|
||||
soundcloud:
|
||||
download_strategy: "albums_then_tracks"
|
||||
skip_premiere_tracks: True
|
||||
ytdl_opts:
|
||||
format: 'bestaudio[ext=mp3]'
|
||||
post_process:
|
||||
file_name: "[{album_year}] {sanitized_album}/{track_number_padded} - {sanitized_title}.{ext}"
|
||||
thumbnail_name: "[{album_year}] {sanitized_album}/folder.{thumbnail_ext}"
|
||||
tagging:
|
||||
artist: "{artist}"
|
||||
albumartist: "{artist}"
|
||||
title: "{title}"
|
||||
album: "{album}"
|
||||
tracknumber: "{track_number}"
|
||||
year: "{album_year}"
|
||||
genre: "{genre}"
|
||||
delorra:
|
||||
preset: "soundcloud_with_id3_tags"
|
||||
soundcloud:
|
||||
username: delorra
|
||||
output_path: "/mnt/nas/Downloads/ytdl/music/DeLorra"
|
||||
overrides:
|
||||
artist: "DeLorra"
|
||||
genre: "Synthwave / Electronic"
|
||||
|
||||
music_videos:
|
||||
youtube:
|
||||
noop: True
|
||||
ytdl_opts:
|
||||
format: 'best'
|
||||
ignoreerrors: True
|
||||
post_process:
|
||||
file_name: "{sanitized_artist} - {sanitized_title}.{ext}"
|
||||
convert_thumbnail: "jpg"
|
||||
thumbnail_name: "{sanitized_artist} - {sanitized_title}.jpg"
|
||||
nfo_name: "{sanitized_artist} - {sanitized_title}.nfo"
|
||||
nfo_root: "musicvideo"
|
||||
nfo:
|
||||
artist: "{artist}"
|
||||
title: "{title}"
|
||||
album: "Music Videos"
|
||||
year: "{upload_year}"
|
||||
bl00dwave:
|
||||
preset: "soundcloud_with_id3_tags"
|
||||
soundcloud:
|
||||
username: bl00dwave
|
||||
output_path: "/tmp/bl00dwave"
|
||||
overrides:
|
||||
artist: "bl00dwave"
|
||||
genre: "Synthwave / Electronic"
|
||||
|
||||
subscriptions:
|
||||
ALISON:
|
||||
preset: "soundcloud_with_id3_tags"
|
||||
soundcloud:
|
||||
username: alis_on
|
||||
output_path: "/mnt/nas/Downloads/ytdl/music/ALISON"
|
||||
overrides:
|
||||
artist: "A.L.I.S.O.N."
|
||||
genre: "Synthwave / Electronic"
|
||||
delorra:
|
||||
preset: "soundcloud_with_id3_tags"
|
||||
soundcloud:
|
||||
username: delorra
|
||||
output_path: "/mnt/nas/Downloads/ytdl/music/DeLorra"
|
||||
overrides:
|
||||
artist: "DeLorra"
|
||||
genre: "Synthwave / Electronic"
|
||||
bl00dwave:
|
||||
preset: "soundcloud_with_id3_tags"
|
||||
soundcloud:
|
||||
username: bl00dwave
|
||||
output_path: "/tmp/bl00dwave"
|
||||
overrides:
|
||||
artist: "bl00dwave"
|
||||
genre: "Synthwave / Electronic"
|
||||
tom_petty:
|
||||
youtube:
|
||||
playlist_id: PLoopXDarluPBnuxs4PTC55Sc_2ShAXC0i
|
||||
preset: "music_videos"
|
||||
output_path: "/tmp/Tom Petty"
|
||||
overrides:
|
||||
artist: "Tom Petty and the Heartbreakers"
|
||||
|
||||
tom_petty:
|
||||
youtube:
|
||||
playlist_id: PLoopXDarluPBnuxs4PTC55Sc_2ShAXC0i
|
||||
preset: "music_videos"
|
||||
output_path: "/tmp/Tom Petty"
|
||||
overrides:
|
||||
artist: "Tom Petty and the Heartbreakers"
|
||||
|
||||
rammstein:
|
||||
youtube:
|
||||
playlist_id: "PLVTLbc6i-h_iuhdwUfuPDLFLXG2QQnz-x"
|
||||
preset: "music_videos"
|
||||
output_path: "/mnt/nas/Downloads/ytdl/music_videos/Rammstein"
|
||||
overrides:
|
||||
artist: "Rammstein"
|
||||
rammstein:
|
||||
youtube:
|
||||
playlist_id: "PLVTLbc6i-h_iuhdwUfuPDLFLXG2QQnz-x"
|
||||
preset: "music_videos"
|
||||
output_path: "/mnt/nas/Downloads/ytdl/music_videos/Rammstein"
|
||||
overrides:
|
||||
artist: "Rammstein"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
class SubscriptionSource(object):
|
||||
from typing import List
|
||||
|
||||
|
||||
class SubscriptionSourceName(object):
|
||||
YOUTUBE = "youtube"
|
||||
SOUNDCLOUD = "soundcloud"
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> List[str]:
|
||||
"""Returns all Subscription sources sorted as a list"""
|
||||
return sorted([cls.YOUTUBE, cls.SOUNDCLOUD])
|
||||
|
||||
@classmethod
|
||||
def pretty_all(cls) -> str:
|
||||
"""Returns all subscription sources pretty printed"""
|
||||
return ", ".join(cls.all())
|
||||
|
||||
|
||||
class YAMLSection(object):
|
||||
CONFIG_KEY = "config"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from ytdl_subscribe.parse import parse_subscriptions
|
|||
from ytdl_subscribe.parse import parse_subscriptions_file
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="YoutubeDL-Subscribe: Download and organize your favorite media easily."
|
||||
description="YoutubeDL-Subscribe: Download and organize your favorite media hassle-free."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from typing import Optional
|
|||
import yaml
|
||||
from mergedeep import mergedeep
|
||||
|
||||
from ytdl_subscribe.enums import SubscriptionSource
|
||||
from ytdl_subscribe.enums import SubscriptionSourceName
|
||||
from ytdl_subscribe.enums import YAMLSection
|
||||
from ytdl_subscribe.subscriptions.soundcloud import SoundcloudSubscription
|
||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||
|
|
@ -84,11 +84,11 @@ def parse_subscriptions(yaml_dict: dict, presets: dict, subscriptions: Optional[
|
|||
preset = presets[subscription["preset"]]
|
||||
subscription = mergedeep.merge({}, preset, subscription)
|
||||
|
||||
if SubscriptionSource.SOUNDCLOUD in subscription:
|
||||
subscription_source = SubscriptionSource.SOUNDCLOUD
|
||||
if SubscriptionSourceName.SOUNDCLOUD in subscription:
|
||||
subscription_source = SubscriptionSourceName.SOUNDCLOUD
|
||||
subscription_class = SoundcloudSubscription
|
||||
elif SubscriptionSource.YOUTUBE in subscription:
|
||||
subscription_source = SubscriptionSource.YOUTUBE
|
||||
elif SubscriptionSourceName.YOUTUBE in subscription:
|
||||
subscription_source = SubscriptionSourceName.YOUTUBE
|
||||
subscription_class = YoutubeSubscription
|
||||
else:
|
||||
raise ValueError("dne")
|
||||
|
|
|
|||
|
|
@ -3,13 +3,10 @@ from typing import List
|
|||
from ytdl_subscribe.downloaders.soundcloud_downloader import SoundcloudDownloader
|
||||
from ytdl_subscribe.entries.soundcloud import SoundcloudAlbum
|
||||
from ytdl_subscribe.entries.soundcloud import SoundcloudTrack
|
||||
from ytdl_subscribe.enums import SubscriptionSource
|
||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||
|
||||
|
||||
class SoundcloudSubscription(Subscription):
|
||||
source = SubscriptionSource.SOUNDCLOUD
|
||||
|
||||
def extract_info(self):
|
||||
tracks: List[SoundcloudTrack] = []
|
||||
soundcloud_downloader = SoundcloudDownloader(
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ from ytdl_subscribe.entries.entry import Entry
|
|||
class Subscription(object):
|
||||
WORKING_DIRECTORY = ""
|
||||
|
||||
source = None
|
||||
|
||||
def __init__(self, name, options, ytdl_opts, post_process, overrides, output_path):
|
||||
"""
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeDownloader
|
||||
from ytdl_subscribe.enums import SubscriptionSource
|
||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||
|
||||
|
||||
class YoutubeSubscription(Subscription):
|
||||
source = SubscriptionSource.YOUTUBE
|
||||
|
||||
def extract_info(self):
|
||||
youtube_downloader = YoutubeDownloader(
|
||||
output_directory=self.output_path,
|
||||
|
|
|
|||
0
ytdl_subscribe/validators/__init__.py
Normal file
0
ytdl_subscribe/validators/__init__.py
Normal file
0
ytdl_subscribe/validators/native/__init__.py
Normal file
0
ytdl_subscribe/validators/native/__init__.py
Normal file
56
ytdl_subscribe/validators/native/object_validator.py
Normal file
56
ytdl_subscribe/validators/native/object_validator.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from typing import Set
|
||||
|
||||
from ytdl_subscribe.validators.native.validator import Validator
|
||||
|
||||
|
||||
class ObjectValidator(Validator):
|
||||
|
||||
required_fields: Set[str] = []
|
||||
optional_fields: Set[str] = []
|
||||
allow_extra_fields = False
|
||||
|
||||
@property
|
||||
def allowed_fields(self):
|
||||
return sorted(self.required_fields.union(self.optional_fields))
|
||||
|
||||
@property
|
||||
def object_keys(self):
|
||||
return sorted(list(self.value.keys()))
|
||||
|
||||
@property
|
||||
def object_items(self):
|
||||
return self.value.items()
|
||||
|
||||
def validate(self) -> "ObjectValidator":
|
||||
super().validate()
|
||||
|
||||
# Ensure the value is an object
|
||||
if not isinstance(self.value, dict):
|
||||
error_msg = f"'{self.key}' must be an object"
|
||||
if self.required_fields:
|
||||
error_msg = f"{error_msg} with the required field: {', '.join(self.required_fields)}"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Ensure the object contains all the required fields
|
||||
for required_key in self.required_fields:
|
||||
if required_key not in self.value:
|
||||
error_msg = (
|
||||
f"'{self.key}' is missing the required field '{required_key}'"
|
||||
)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# If extra fields are not allowed, ensure we only have required or optional keys
|
||||
if not self.allow_extra_fields:
|
||||
for object_key in self.object_keys:
|
||||
if (
|
||||
object_key not in self.required_fields
|
||||
or object_key not in self.optional_fields
|
||||
):
|
||||
|
||||
error_msg = (
|
||||
f"'{self.key}' contains the field '{object_key} which is not allowed. "
|
||||
f"Allowed fields: {', '.join(self.allowed_fields)}"
|
||||
)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
return self
|
||||
10
ytdl_subscribe/validators/native/validator.py
Normal file
10
ytdl_subscribe/validators/native/validator.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from typing import Any
|
||||
|
||||
|
||||
class Validator:
|
||||
def __init__(self, key: str, value: Any):
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
def validate(self) -> "Validator":
|
||||
return self
|
||||
0
ytdl_subscribe/validators/subscriptions/__init__.py
Normal file
0
ytdl_subscribe/validators/subscriptions/__init__.py
Normal file
66
ytdl_subscribe/validators/subscriptions/sources.py
Normal file
66
ytdl_subscribe/validators/subscriptions/sources.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_subscribe.validators.native.object_validator import ObjectValidator
|
||||
|
||||
|
||||
class SubscriptionSource(ObjectValidator):
|
||||
required_fields = {"download_strategy"}
|
||||
download_strategies: Set[str] = {}
|
||||
|
||||
def __init__(self, key: str, value: Any):
|
||||
super().__init__(key=key, value=value)
|
||||
self.download_strategy: Optional[str] = None
|
||||
|
||||
def validate(self) -> "SubscriptionSource":
|
||||
super().validate()
|
||||
|
||||
if self.value["download_strategy"] not in self.download_strategies:
|
||||
raise ValueError(
|
||||
f"'download_strategy' must be one of the following: {', '.join(self.download_strategies)}"
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class YoutubeSource(SubscriptionSource):
|
||||
optional_fields = {"playlist_id"}
|
||||
download_strategies = {"playlist"}
|
||||
|
||||
def __init__(self, key: str, value: Any):
|
||||
super().__init__(key=key, value=value)
|
||||
self.playlist_id: Optional[str] = None
|
||||
|
||||
def validate(self) -> "YoutubeSource":
|
||||
super().validate()
|
||||
|
||||
if self.download_strategy == "playlist":
|
||||
self.playlist_id = self.value.get("playlist_id")
|
||||
if not self.playlist_id or not isinstance(self.playlist_id, str):
|
||||
raise ValueError(
|
||||
"Youtube download_strategy 'playlist' requires the field 'playlist_id' to be a string"
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class SoundcloudSource(SubscriptionSource):
|
||||
optional_fields = {"username"}
|
||||
download_strategies = {"albums_and_singles"}
|
||||
|
||||
def __init__(self, key: str, value: Any):
|
||||
super().__init__(key=key, value=value)
|
||||
self.username: Optional[str] = None
|
||||
|
||||
def validate(self) -> "SoundcloudSource":
|
||||
super().validate()
|
||||
|
||||
if self.download_strategy == "albums_and_singles":
|
||||
self.username = self.value.get("username")
|
||||
if not self.username or not isinstance(self.username, str):
|
||||
raise ValueError(
|
||||
"Soundcloud download_strategy 'albums_and_singles' requires the field 'username'"
|
||||
)
|
||||
|
||||
return self
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_subscribe.enums import SubscriptionSourceName
|
||||
from ytdl_subscribe.validators.native.object_validator import ObjectValidator
|
||||
from ytdl_subscribe.validators.subscriptions.sources import SoundcloudSource
|
||||
from ytdl_subscribe.validators.subscriptions.sources import SubscriptionSource
|
||||
from ytdl_subscribe.validators.subscriptions.sources import YoutubeSource
|
||||
|
||||
|
||||
class SubscriptionPreset(ObjectValidator):
|
||||
required_fields = {"post_process"}
|
||||
optional_fields = {"ytdl_options", *SubscriptionSourceName.all()}
|
||||
allow_extra_fields = False
|
||||
|
||||
def __init__(self, key: str, value: Any):
|
||||
super().__init__(key=key, value=value)
|
||||
self.subscription_source: Optional[SubscriptionSource] = None
|
||||
|
||||
def _validate_subscription_source(self):
|
||||
# Make sure we only have a single subscription source
|
||||
for object_key, object_value in self.object_items:
|
||||
if object_key in SubscriptionSourceName.all() and self.subscription_source:
|
||||
raise ValueError(
|
||||
f"'{self.key}' can only have one of the following sources: {SubscriptionSourceName.pretty_all()}"
|
||||
)
|
||||
|
||||
if object_key == SubscriptionSourceName.SOUNDCLOUD:
|
||||
self.subscription_source = SoundcloudSource(
|
||||
key=object_key, value=object_value
|
||||
).validate()
|
||||
elif object_key == SubscriptionSourceName.YOUTUBE:
|
||||
self.subscription_source = YoutubeSource(
|
||||
key=object_key, value=object_value
|
||||
).validate()
|
||||
|
||||
# If subscription source was not set, error
|
||||
if not self.subscription_source:
|
||||
raise ValueError(
|
||||
f"'{self.key} must have one of the following sources: {SubscriptionSourceName.pretty_all()}"
|
||||
)
|
||||
|
||||
def validate(self) -> "SubscriptionPreset":
|
||||
super().validate()
|
||||
self._validate_subscription_source()
|
||||
return self
|
||||
Loading…
Reference in a new issue