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
|
coverage==6.3.2
|
||||||
dicttoxml==1.7.4
|
dicttoxml==1.7.4
|
||||||
isort==5.10.1
|
isort==5.10.1
|
||||||
|
jsonschema==4.4.0
|
||||||
mergedeep==1.3.4
|
mergedeep==1.3.4
|
||||||
music-tag==0.4.3
|
music-tag==0.4.3
|
||||||
pathvalidate==2.4.1
|
pathvalidate==2.4.1
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,42 @@
|
||||||
config:
|
ALISON:
|
||||||
working_directory: 'tmp'
|
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:
|
delorra:
|
||||||
soundcloud_with_id3_tags:
|
preset: "soundcloud_with_id3_tags"
|
||||||
soundcloud:
|
soundcloud:
|
||||||
download_strategy: "albums_then_tracks"
|
username: delorra
|
||||||
skip_premiere_tracks: True
|
output_path: "/mnt/nas/Downloads/ytdl/music/DeLorra"
|
||||||
ytdl_opts:
|
overrides:
|
||||||
format: 'bestaudio[ext=mp3]'
|
artist: "DeLorra"
|
||||||
post_process:
|
genre: "Synthwave / Electronic"
|
||||||
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:
|
bl00dwave:
|
||||||
youtube:
|
preset: "soundcloud_with_id3_tags"
|
||||||
noop: True
|
soundcloud:
|
||||||
ytdl_opts:
|
username: bl00dwave
|
||||||
format: 'best'
|
output_path: "/tmp/bl00dwave"
|
||||||
ignoreerrors: True
|
overrides:
|
||||||
post_process:
|
artist: "bl00dwave"
|
||||||
file_name: "{sanitized_artist} - {sanitized_title}.{ext}"
|
genre: "Synthwave / Electronic"
|
||||||
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}"
|
|
||||||
|
|
||||||
subscriptions:
|
tom_petty:
|
||||||
ALISON:
|
youtube:
|
||||||
preset: "soundcloud_with_id3_tags"
|
playlist_id: PLoopXDarluPBnuxs4PTC55Sc_2ShAXC0i
|
||||||
soundcloud:
|
preset: "music_videos"
|
||||||
username: alis_on
|
output_path: "/tmp/Tom Petty"
|
||||||
output_path: "/mnt/nas/Downloads/ytdl/music/ALISON"
|
overrides:
|
||||||
overrides:
|
artist: "Tom Petty and the Heartbreakers"
|
||||||
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:
|
rammstein:
|
||||||
youtube:
|
youtube:
|
||||||
playlist_id: PLoopXDarluPBnuxs4PTC55Sc_2ShAXC0i
|
playlist_id: "PLVTLbc6i-h_iuhdwUfuPDLFLXG2QQnz-x"
|
||||||
preset: "music_videos"
|
preset: "music_videos"
|
||||||
output_path: "/tmp/Tom Petty"
|
output_path: "/mnt/nas/Downloads/ytdl/music_videos/Rammstein"
|
||||||
overrides:
|
overrides:
|
||||||
artist: "Tom Petty and the Heartbreakers"
|
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"
|
YOUTUBE = "youtube"
|
||||||
SOUNDCLOUD = "soundcloud"
|
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):
|
class YAMLSection(object):
|
||||||
CONFIG_KEY = "config"
|
CONFIG_KEY = "config"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from ytdl_subscribe.parse import parse_subscriptions
|
||||||
from ytdl_subscribe.parse import parse_subscriptions_file
|
from ytdl_subscribe.parse import parse_subscriptions_file
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
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(
|
parser.add_argument(
|
||||||
"-c",
|
"-c",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import Optional
|
||||||
import yaml
|
import yaml
|
||||||
from mergedeep import mergedeep
|
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.enums import YAMLSection
|
||||||
from ytdl_subscribe.subscriptions.soundcloud import SoundcloudSubscription
|
from ytdl_subscribe.subscriptions.soundcloud import SoundcloudSubscription
|
||||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
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"]]
|
preset = presets[subscription["preset"]]
|
||||||
subscription = mergedeep.merge({}, preset, subscription)
|
subscription = mergedeep.merge({}, preset, subscription)
|
||||||
|
|
||||||
if SubscriptionSource.SOUNDCLOUD in subscription:
|
if SubscriptionSourceName.SOUNDCLOUD in subscription:
|
||||||
subscription_source = SubscriptionSource.SOUNDCLOUD
|
subscription_source = SubscriptionSourceName.SOUNDCLOUD
|
||||||
subscription_class = SoundcloudSubscription
|
subscription_class = SoundcloudSubscription
|
||||||
elif SubscriptionSource.YOUTUBE in subscription:
|
elif SubscriptionSourceName.YOUTUBE in subscription:
|
||||||
subscription_source = SubscriptionSource.YOUTUBE
|
subscription_source = SubscriptionSourceName.YOUTUBE
|
||||||
subscription_class = YoutubeSubscription
|
subscription_class = YoutubeSubscription
|
||||||
else:
|
else:
|
||||||
raise ValueError("dne")
|
raise ValueError("dne")
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,10 @@ from typing import List
|
||||||
from ytdl_subscribe.downloaders.soundcloud_downloader import SoundcloudDownloader
|
from ytdl_subscribe.downloaders.soundcloud_downloader import SoundcloudDownloader
|
||||||
from ytdl_subscribe.entries.soundcloud import SoundcloudAlbum
|
from ytdl_subscribe.entries.soundcloud import SoundcloudAlbum
|
||||||
from ytdl_subscribe.entries.soundcloud import SoundcloudTrack
|
from ytdl_subscribe.entries.soundcloud import SoundcloudTrack
|
||||||
from ytdl_subscribe.enums import SubscriptionSource
|
|
||||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||||
|
|
||||||
|
|
||||||
class SoundcloudSubscription(Subscription):
|
class SoundcloudSubscription(Subscription):
|
||||||
source = SubscriptionSource.SOUNDCLOUD
|
|
||||||
|
|
||||||
def extract_info(self):
|
def extract_info(self):
|
||||||
tracks: List[SoundcloudTrack] = []
|
tracks: List[SoundcloudTrack] = []
|
||||||
soundcloud_downloader = SoundcloudDownloader(
|
soundcloud_downloader = SoundcloudDownloader(
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ from ytdl_subscribe.entries.entry import Entry
|
||||||
class Subscription(object):
|
class Subscription(object):
|
||||||
WORKING_DIRECTORY = ""
|
WORKING_DIRECTORY = ""
|
||||||
|
|
||||||
source = None
|
|
||||||
|
|
||||||
def __init__(self, name, options, ytdl_opts, post_process, overrides, output_path):
|
def __init__(self, name, options, ytdl_opts, post_process, overrides, output_path):
|
||||||
"""
|
"""
|
||||||
Parameters
|
Parameters
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeDownloader
|
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeDownloader
|
||||||
from ytdl_subscribe.enums import SubscriptionSource
|
|
||||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||||
|
|
||||||
|
|
||||||
class YoutubeSubscription(Subscription):
|
class YoutubeSubscription(Subscription):
|
||||||
source = SubscriptionSource.YOUTUBE
|
|
||||||
|
|
||||||
def extract_info(self):
|
def extract_info(self):
|
||||||
youtube_downloader = YoutubeDownloader(
|
youtube_downloader = YoutubeDownloader(
|
||||||
output_directory=self.output_path,
|
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