channel support with range
This commit is contained in:
parent
a1ccb0669e
commit
e3239153ec
12 changed files with 105 additions and 28 deletions
|
|
@ -12,4 +12,4 @@ pylint==2.13.3
|
||||||
pytest==7.1.1
|
pytest==7.1.1
|
||||||
pyYAML==5.4.1
|
pyYAML==5.4.1
|
||||||
sanitize-filename==1.2.0
|
sanitize-filename==1.2.0
|
||||||
yt-dlp==2021.9.2
|
yt-dlp==2022.4.8
|
||||||
|
|
@ -61,10 +61,11 @@
|
||||||
recent_channel_test:
|
recent_channel_test:
|
||||||
preset: "yt_channel"
|
preset: "yt_channel"
|
||||||
ytdl_options:
|
ytdl_options:
|
||||||
daterange: now-2weeks
|
break_on_reject: True
|
||||||
youtube:
|
youtube:
|
||||||
download_strategy: "channel"
|
download_strategy: "channel"
|
||||||
channel_id: "UCRcCVDu_4oARsAKFkKYydAQ"
|
channel_id: "UCRcCVDu_4oARsAKFkKYydAQ"
|
||||||
|
after: today-8months
|
||||||
output_options:
|
output_options:
|
||||||
output_directory: "/tmp/dunkey"
|
output_directory: "/tmp/dunkey"
|
||||||
overrides:
|
overrides:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from yt_dlp.utils import RejectedVideoReached
|
||||||
|
|
||||||
from ytdl_subscribe.downloaders.downloader import Downloader
|
from ytdl_subscribe.downloaders.downloader import Downloader
|
||||||
from ytdl_subscribe.entries.youtube import YoutubeVideo
|
from ytdl_subscribe.entries.youtube import YoutubeVideo
|
||||||
|
|
||||||
|
|
@ -38,7 +40,11 @@ class YoutubeDownloader(Downloader):
|
||||||
"download_archive": str(Path(self.output_directory) / "ytdl-download-archive.txt"),
|
"download_archive": str(Path(self.output_directory) / "ytdl-download-archive.txt"),
|
||||||
"writeinfojson": True,
|
"writeinfojson": True,
|
||||||
}
|
}
|
||||||
_ = self.extract_info(ytdl_options_overrides=ytdl_metadata_override, url=url)
|
|
||||||
|
try:
|
||||||
|
_ = self.extract_info(ytdl_options_overrides=ytdl_metadata_override, url=url)
|
||||||
|
except RejectedVideoReached:
|
||||||
|
pass
|
||||||
|
|
||||||
def download_video(self, video_id: str) -> YoutubeVideo:
|
def download_video(self, video_id: str) -> YoutubeVideo:
|
||||||
"""Download a single Youtube video"""
|
"""Download a single Youtube video"""
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,14 @@ class Entry:
|
||||||
"uid": self.uid,
|
"uid": self.uid,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"sanitized_title": self.sanitized_title,
|
"sanitized_title": self.sanitized_title,
|
||||||
|
"description": self.description,
|
||||||
"ext": self.ext,
|
"ext": self.ext,
|
||||||
"upload_date": self.upload_date,
|
"upload_date": self.upload_date,
|
||||||
"upload_year": self.upload_year,
|
"upload_year": self.upload_year,
|
||||||
|
"upload_month": self.upload_month,
|
||||||
|
"upload_month_padded": self.upload_month_padded,
|
||||||
|
"upload_day": self.upload_day,
|
||||||
|
"upload_day_padded": self.upload_day_padded,
|
||||||
"thumbnail": self.thumbnail,
|
"thumbnail": self.thumbnail,
|
||||||
"thumbnail_ext": self.thumbnail_ext,
|
"thumbnail_ext": self.thumbnail_ext,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@ import os
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from typing import Generic, overload, Optional
|
from typing import Dict
|
||||||
|
from typing import Generic
|
||||||
|
from typing import Optional
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
import dicttoxml
|
import dicttoxml
|
||||||
import music_tag
|
import music_tag
|
||||||
|
|
@ -55,12 +58,17 @@ class Subscription(Generic[S], ABC):
|
||||||
"""Returns the source options defined for this subscription"""
|
"""Returns the source options defined for this subscription"""
|
||||||
return self.__preset_options.subscription_source
|
return self.__preset_options.subscription_source
|
||||||
|
|
||||||
def get_downloader(self, downloader_type: Type[D]) -> D:
|
def get_downloader(
|
||||||
|
self, downloader_type: Type[D], source_ytdl_options: Optional[Dict] = None
|
||||||
|
) -> D:
|
||||||
"""Returns the downloader that will be used to download media for this subscription"""
|
"""Returns the downloader that will be used to download media for this subscription"""
|
||||||
return downloader_type(
|
|
||||||
output_directory=self.working_directory,
|
# if source_ytdl_options are present, override the ytdl_options with them
|
||||||
ytdl_options=self.__preset_options.ytdl_options.dict,
|
ytdl_options = self.__preset_options.ytdl_options.dict
|
||||||
)
|
if source_ytdl_options:
|
||||||
|
ytdl_options = dict(ytdl_options, **source_ytdl_options)
|
||||||
|
|
||||||
|
return downloader_type(output_directory=self.working_directory, ytdl_options=ytdl_options)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def output_options(self) -> OutputOptionsValidator:
|
def output_options(self) -> OutputOptionsValidator:
|
||||||
|
|
@ -82,7 +90,9 @@ class Subscription(Generic[S], ABC):
|
||||||
"""Returns the directory that the downloader saves files to"""
|
"""Returns the directory that the downloader saves files to"""
|
||||||
return str(Path(self.__config_options.working_directory.value) / Path(self.name))
|
return str(Path(self.__config_options.working_directory.value) / Path(self.name))
|
||||||
|
|
||||||
def _apply_formatter(self, formatter: StringFormatterValidator, entry: Optional[Entry] = None) -> str:
|
def _apply_formatter(
|
||||||
|
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the format_string after .format has been called on it using entry (if provided) and
|
Returns the format_string after .format has been called on it using entry (if provided) and
|
||||||
override values
|
override values
|
||||||
|
|
@ -182,4 +192,4 @@ class Subscription(Generic[S], ABC):
|
||||||
self._post_process_nfo(nfo_options=self.metadata_options.nfo, entry=entry)
|
self._post_process_nfo(nfo_options=self.metadata_options.nfo, entry=entry)
|
||||||
|
|
||||||
if self.metadata_options.output_directory_nfo:
|
if self.metadata_options.output_directory_nfo:
|
||||||
self._post_process_nfo(nfo_options=self.metadata_options.output_directory_nfo)
|
self._post_process_nfo(nfo_options=self.metadata_options.output_directory_nfo)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
|
from yt_dlp import DateRange
|
||||||
|
|
||||||
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeDownloader
|
from ytdl_subscribe.downloaders.youtube_downloader import YoutubeDownloader
|
||||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||||
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
YoutubePlaylistSourceValidator, YoutubeChannelSourceValidator,
|
YoutubeChannelSourceValidator,
|
||||||
|
)
|
||||||
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
|
YoutubePlaylistSourceValidator,
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
YoutubeVideoSourceValidator,
|
YoutubeVideoSourceValidator,
|
||||||
|
|
@ -20,9 +25,15 @@ class YoutubePlaylistSubscription(Subscription[YoutubePlaylistSourceValidator]):
|
||||||
|
|
||||||
class YoutubeChannelSubscription(Subscription[YoutubeChannelSourceValidator]):
|
class YoutubeChannelSubscription(Subscription[YoutubeChannelSourceValidator]):
|
||||||
def extract_info(self):
|
def extract_info(self):
|
||||||
entries = self.get_downloader(YoutubeDownloader).download_channel(
|
source_ytdl_options = {}
|
||||||
channel_id=self.source_options.channel_id.value
|
if self.source_options.before or self.source_options.after:
|
||||||
)
|
source_ytdl_options["daterange"] = DateRange(
|
||||||
|
start=self.source_options.after.datetime_str if self.source_options.after else None,
|
||||||
|
end=self.source_options.before.datetime_str if self.source_options.before else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
downloader = self.get_downloader(YoutubeDownloader, source_ytdl_options=source_ytdl_options)
|
||||||
|
entries = downloader.download_channel(channel_id=self.source_options.channel_id.value)
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
self.post_process_entry(entry)
|
self.post_process_entry(entry)
|
||||||
|
|
|
||||||
26
ytdl_subscribe/validators/base/string_datetime.py
Normal file
26
ytdl_subscribe/validators/base/string_datetime.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from yt_dlp.utils import datetime_from_str
|
||||||
|
|
||||||
|
from ytdl_subscribe.validators.base.validators import Validator
|
||||||
|
from ytdl_subscribe.validators.exceptions import ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class StringDatetimeValidator(Validator):
|
||||||
|
"""
|
||||||
|
Validates a ytdl datetime string value
|
||||||
|
"""
|
||||||
|
|
||||||
|
_expected_value_type = str
|
||||||
|
_expected_value_type_name = "datetime string"
|
||||||
|
|
||||||
|
def __init__(self, name, value):
|
||||||
|
super().__init__(name, value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_ = datetime_from_str(self._value)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationException(e)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datetime_str(self) -> str:
|
||||||
|
"""Returns the datetime as a string"""
|
||||||
|
return self._value
|
||||||
|
|
@ -78,10 +78,12 @@ class StringFormatterValidator(Validator):
|
||||||
"""
|
"""
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
@final
|
def __apply_formatter(
|
||||||
def apply_formatter(self, variable_dict: Dict[str, str]) -> str:
|
self, formatter: "StringFormatterValidator", variable_dict: Dict[str, str]
|
||||||
|
) -> "StringFormatterValidator":
|
||||||
|
"""TODO: test recursive case where variable not found"""
|
||||||
# Ensure the variable names exist within the entry and overrides
|
# Ensure the variable names exist within the entry and overrides
|
||||||
for variable_name in self.format_variables:
|
for variable_name in formatter.format_variables:
|
||||||
if variable_name not in variable_dict:
|
if variable_name not in variable_dict:
|
||||||
available_fields = ", ".join(sorted(variable_dict.keys()))
|
available_fields = ", ".join(sorted(variable_dict.keys()))
|
||||||
raise self._validation_exception(
|
raise self._validation_exception(
|
||||||
|
|
@ -90,16 +92,20 @@ class StringFormatterValidator(Validator):
|
||||||
exception_class=StringFormattingException,
|
exception_class=StringFormattingException,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return StringFormatterValidator(
|
||||||
|
name=self._name,
|
||||||
|
value=formatter.format_string.format(**OrderedDict(variable_dict)),
|
||||||
|
)
|
||||||
|
|
||||||
|
@final
|
||||||
|
def apply_formatter(self, variable_dict: Dict[str, str]) -> str:
|
||||||
# Keep formatting the format string until no format_variables are present
|
# Keep formatting the format string until no format_variables are present
|
||||||
formatter = self
|
formatter = self
|
||||||
recursion_depth = 0
|
recursion_depth = 0
|
||||||
max_depth = StringFormatterValidator.__max_format_recursion
|
max_depth = StringFormatterValidator.__max_format_recursion
|
||||||
|
|
||||||
while formatter.format_variables and recursion_depth < max_depth:
|
while formatter.format_variables and recursion_depth < max_depth:
|
||||||
formatter = StringFormatterValidator(
|
formatter = self.__apply_formatter(formatter=formatter, variable_dict=variable_dict)
|
||||||
name=self._name,
|
|
||||||
value=formatter.format_string.format(**OrderedDict(variable_dict)),
|
|
||||||
)
|
|
||||||
recursion_depth += 1
|
recursion_depth += 1
|
||||||
|
|
||||||
if formatter.format_variables:
|
if formatter.format_variables:
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,6 @@ class MetadataOptionsValidator(StrictDictValidator):
|
||||||
self.nfo = self._validate_key_if_present(key="nfo", validator=NFOValidator)
|
self.nfo = self._validate_key_if_present(key="nfo", validator=NFOValidator)
|
||||||
|
|
||||||
# TODO: Ensure this does not depend on entry variables, only overrides
|
# TODO: Ensure this does not depend on entry variables, only overrides
|
||||||
self.output_directory_nfo = self._validate_key_if_present(key="output_directory_nfo", validator=NFOValidator)
|
self.output_directory_nfo = self._validate_key_if_present(
|
||||||
|
key="output_directory_nfo", validator=NFOValidator
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ from ytdl_subscribe.validators.config.source_options.soundcloud_validators impor
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.config.source_options.source_validators import SourceValidator
|
from ytdl_subscribe.validators.config.source_options.source_validators import SourceValidator
|
||||||
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
YoutubePlaylistSourceValidator, YoutubeChannelSourceValidator,
|
YoutubeChannelSourceValidator,
|
||||||
|
)
|
||||||
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
|
YoutubePlaylistSourceValidator,
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
YoutubeVideoSourceValidator,
|
YoutubeVideoSourceValidator,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from ytdl_subscribe.validators.base.string_datetime import StringDatetimeValidator
|
||||||
from ytdl_subscribe.validators.base.validators import StringValidator
|
from ytdl_subscribe.validators.base.validators import StringValidator
|
||||||
from ytdl_subscribe.validators.config.source_options.source_validators import YoutubeSourceValidator
|
from ytdl_subscribe.validators.config.source_options.source_validators import YoutubeSourceValidator
|
||||||
|
|
||||||
|
|
@ -20,7 +21,10 @@ class YoutubeVideoSourceValidator(YoutubeSourceValidator):
|
||||||
|
|
||||||
class YoutubeChannelSourceValidator(YoutubeSourceValidator):
|
class YoutubeChannelSourceValidator(YoutubeSourceValidator):
|
||||||
_required_keys = {"channel_id"}
|
_required_keys = {"channel_id"}
|
||||||
|
_optional_keys = {"before", "after"}
|
||||||
|
|
||||||
def __init__(self, name, value):
|
def __init__(self, name, value):
|
||||||
super().__init__(name, value)
|
super().__init__(name, value)
|
||||||
self.channel_id = self._validate_key("channel_id", StringValidator)
|
self.channel_id = self._validate_key("channel_id", StringValidator)
|
||||||
|
self.before = self._validate_key_if_present("before", StringDatetimeValidator)
|
||||||
|
self.after = self._validate_key_if_present("after", StringDatetimeValidator)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ from mergedeep import mergedeep
|
||||||
|
|
||||||
from ytdl_subscribe.subscriptions.soundcloud import SoundcloudAlbumsAndSinglesSubscription
|
from ytdl_subscribe.subscriptions.soundcloud import SoundcloudAlbumsAndSinglesSubscription
|
||||||
from ytdl_subscribe.subscriptions.subscription import Subscription
|
from ytdl_subscribe.subscriptions.subscription import Subscription
|
||||||
from ytdl_subscribe.subscriptions.youtube import YoutubePlaylistSubscription, \
|
from ytdl_subscribe.subscriptions.youtube import YoutubeChannelSubscription
|
||||||
YoutubeChannelSubscription
|
from ytdl_subscribe.subscriptions.youtube import YoutubePlaylistSubscription
|
||||||
from ytdl_subscribe.subscriptions.youtube import YoutubeVideoSubscription
|
from ytdl_subscribe.subscriptions.youtube import YoutubeVideoSubscription
|
||||||
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
|
||||||
|
|
@ -23,7 +23,10 @@ from ytdl_subscribe.validators.config.source_options.soundcloud_validators impor
|
||||||
SoundcloudAlbumsAndSinglesSourceValidator,
|
SoundcloudAlbumsAndSinglesSourceValidator,
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
YoutubePlaylistSourceValidator, YoutubeChannelSourceValidator,
|
YoutubeChannelSourceValidator,
|
||||||
|
)
|
||||||
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
|
YoutubePlaylistSourceValidator,
|
||||||
)
|
)
|
||||||
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
from ytdl_subscribe.validators.config.source_options.youtube_validators import (
|
||||||
YoutubeVideoSourceValidator,
|
YoutubeVideoSourceValidator,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue