asdfasdfasedf
This commit is contained in:
parent
2bc6f6adff
commit
7dbeb7a4ff
10 changed files with 94 additions and 982 deletions
|
|
@ -20,6 +20,7 @@ from ytdl_sub.config.preset_options import TOptionsValidator
|
|||
from ytdl_sub.config.preset_options import YTDLOptions
|
||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS
|
||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES
|
||||
from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
|
|
@ -156,7 +157,7 @@ class Preset(_PresetShell):
|
|||
|
||||
@property
|
||||
def _source_variables(self) -> List[str]:
|
||||
return Entry.source_variables()
|
||||
return list(VARIABLE_SCRIPTS.keys())
|
||||
|
||||
def __validate_and_get_plugins(self) -> PresetPlugins:
|
||||
preset_plugins = PresetPlugins()
|
||||
|
|
@ -201,29 +202,24 @@ class Preset(_PresetShell):
|
|||
# Set the formatter variables to be the overrides
|
||||
variable_dict = copy.deepcopy(self.overrides.dict_with_format_strings)
|
||||
|
||||
# If the formatter supports source variables, set the formatter variables to include
|
||||
# both source and override variables
|
||||
if not isinstance(formatter_validator, OverridesStringFormatterValidator):
|
||||
source_variables = {
|
||||
source_var: "dummy_string"
|
||||
for source_var in self._source_variables
|
||||
+ self.downloader_options.added_source_variables()
|
||||
source_variables = {
|
||||
source_var: "dummy_string"
|
||||
for source_var in self._source_variables
|
||||
+ self.downloader_options.added_source_variables()
|
||||
}
|
||||
variable_dict = dict(source_variables, **variable_dict)
|
||||
|
||||
# For all plugins, add in any extra added source variables
|
||||
# TODO: Check in order variables are added
|
||||
for plugin_options in self.plugins.plugin_options:
|
||||
added_plugin_variables = {
|
||||
source_var: "dummy_string" for source_var in plugin_options.added_source_variables()
|
||||
}
|
||||
variable_dict = dict(source_variables, **variable_dict)
|
||||
# sanity check plugin variables do not override source variables
|
||||
expected_len = len(variable_dict) + len(added_plugin_variables)
|
||||
variable_dict = dict(variable_dict, **added_plugin_variables)
|
||||
|
||||
# For all plugins, add in any extra added source variables
|
||||
for plugin_options in self.plugins.plugin_options:
|
||||
added_plugin_variables = {
|
||||
source_var: "dummy_string"
|
||||
for source_var in plugin_options.added_source_variables()
|
||||
}
|
||||
# sanity check plugin variables do not override source variables
|
||||
expected_len = len(variable_dict) + len(added_plugin_variables)
|
||||
variable_dict = dict(variable_dict, **added_plugin_variables)
|
||||
|
||||
assert (
|
||||
len(variable_dict) == expected_len
|
||||
), "plugin variables overwrote source variables"
|
||||
assert len(variable_dict) == expected_len, "plugin variables overwrote source variables"
|
||||
|
||||
_ = formatter_validator.apply_formatter(variable_dict=variable_dict)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import copy
|
||||
import json
|
||||
from abc import ABC
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
|
@ -5,10 +7,13 @@ from typing import List
|
|||
from typing import Optional
|
||||
from typing import TypeVar
|
||||
|
||||
import mergedeep
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS
|
||||
from ytdl_sub.entries.variables.override_variables import SUBSCRIPTION_NAME
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator
|
||||
|
|
@ -212,11 +217,13 @@ class Overrides(DictFormatterValidator):
|
|||
-------
|
||||
The format_string after .format has been called
|
||||
"""
|
||||
variable_dict = self.dict_with_format_strings
|
||||
variable_dict = copy.deepcopy(VARIABLE_SCRIPTS)
|
||||
mergedeep.merge(variable_dict, self.dict_with_format_strings)
|
||||
|
||||
if entry:
|
||||
variable_dict = dict(entry.to_dict(), **variable_dict)
|
||||
mergedeep.merge(variable_dict, {VARIABLES.entry_metadata.variable_name: json.dumps(entry._kwargs)})
|
||||
if function_overrides:
|
||||
variable_dict = dict(variable_dict, **function_overrides)
|
||||
mergedeep.merge(variable_dict, function_overrides)
|
||||
|
||||
return formatter.apply_formatter(variable_dict)
|
||||
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ class MultiUrlValidator(OptionsValidator):
|
|||
# Ensure at least URL is non-empty
|
||||
has_non_empty_url = False
|
||||
for url_validator in self.urls.list:
|
||||
has_non_empty_url |= bool(url_validator.url.apply_formatter(override_variables))
|
||||
has_non_empty_url |= bool(url_validator.url.apply_formatter(base_variables))
|
||||
|
||||
if not has_non_empty_url:
|
||||
raise self._validation_exception("Must contain at least one url that is non-empty")
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ from typing import Optional
|
|||
from typing import final
|
||||
|
||||
from ytdl_sub.entries.base_entry import BaseEntry
|
||||
from ytdl_sub.entries.variables.entry_variables import EntryVariables
|
||||
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
|
||||
from ytdl_sub.validators.audo_codec_validator import VIDEO_CODEC_EXTS
|
||||
|
||||
|
||||
class Entry(EntryVariables, BaseEntry):
|
||||
class Entry(BaseEntry):
|
||||
"""
|
||||
Entry object to represent a single media object returned from yt-dlp.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ class CustomFunctions:
|
|||
|
||||
@staticmethod
|
||||
def register():
|
||||
Functions.register_function(CustomFunctions.legacy_bracket_safety)
|
||||
Functions.register_function(CustomFunctions.sanitize)
|
||||
Functions.register_function(CustomFunctions.sanitize_plex_episode)
|
||||
Functions.register_function(CustomFunctions.to_date_metadata)
|
||||
if not Functions.is_built_in("sanitize"):
|
||||
Functions.register_function(CustomFunctions.legacy_bracket_safety)
|
||||
Functions.register_function(CustomFunctions.sanitize)
|
||||
Functions.register_function(CustomFunctions.sanitize_plex_episode)
|
||||
Functions.register_function(CustomFunctions.to_date_metadata)
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ def source_get_int(key: MetadataVariable, default: Optional[Variable | int] = No
|
|||
###############################################################################################
|
||||
# Scripts
|
||||
|
||||
ENTRY_EMPTY_METADATA: Dict[Variable, str] = {
|
||||
v.entry_metadata: "{ {} }"
|
||||
}
|
||||
|
||||
ENTRY_HARDCODED_VARIABLES: Dict[Variable, str] = {
|
||||
v.info_json_ext: "info.json",
|
||||
|
|
@ -236,6 +239,7 @@ SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = {
|
|||
_VARIABLE_SCRIPTS: Dict[Variable, str] = {}
|
||||
mergedeep.merge(
|
||||
_VARIABLE_SCRIPTS,
|
||||
ENTRY_EMPTY_METADATA,
|
||||
ENTRY_HARDCODED_VARIABLES,
|
||||
ENTRY_REQUIRED_VARIABLES,
|
||||
ENTRY_DEFAULT_VARIABLES,
|
||||
|
|
|
|||
|
|
@ -1,857 +0,0 @@
|
|||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.entries.base_entry import BaseEntry
|
||||
from ytdl_sub.entries.base_entry import BaseEntryVariables
|
||||
from ytdl_sub.entries.variables.kwargs import CHANNEL
|
||||
from ytdl_sub.entries.variables.kwargs import CHANNEL_ID
|
||||
from ytdl_sub.entries.variables.kwargs import CREATOR
|
||||
from ytdl_sub.entries.variables.kwargs import DOWNLOAD_INDEX
|
||||
from ytdl_sub.entries.variables.kwargs import EXT
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_COUNT
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_DESCRIPTION
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_INDEX
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_MAX_UPLOAD_YEAR
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_MAX_UPLOAD_YEAR_TRUNCATED
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_TITLE
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_UID
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_UPLOADER
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_UPLOADER_ID
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_UPLOADER_URL
|
||||
from ytdl_sub.entries.variables.kwargs import PLAYLIST_WEBPAGE_URL
|
||||
from ytdl_sub.entries.variables.kwargs import RELEASE_DATE
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_COUNT
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_DESCRIPTION
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_INDEX
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_TITLE
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_UID
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_UPLOADER
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_UPLOADER_ID
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_UPLOADER_URL
|
||||
from ytdl_sub.entries.variables.kwargs import SOURCE_WEBPAGE_URL
|
||||
from ytdl_sub.entries.variables.kwargs import UPLOAD_DATE
|
||||
from ytdl_sub.entries.variables.kwargs import UPLOAD_DATE_INDEX
|
||||
|
||||
# This file contains mixins to a BaseEntry subclass. Ignore pylint's "no kwargs member" suggestion
|
||||
# pylint: disable=no-member
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
|
||||
def pad(num: int, width: int = 2):
|
||||
"""Pad integers"""
|
||||
return str(num).zfill(width)
|
||||
|
||||
|
||||
_days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
|
||||
Self = Union[BaseEntry, "EntryVariables"]
|
||||
|
||||
|
||||
class EntryVariables(BaseEntryVariables):
|
||||
@property
|
||||
def source_title(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Name of the source (i.e. channel with multiple playlists) if it exists, otherwise
|
||||
returns its playlist_title.
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_TITLE, self.playlist_title)
|
||||
|
||||
@property
|
||||
def source_title_sanitized(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The source title, sanitized
|
||||
"""
|
||||
return sanitize_filename(self.source_title)
|
||||
|
||||
@property
|
||||
def source_uid(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The source unique id if it exists, otherwise returns the playlist unique ID.
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_UID, self.playlist_uid)
|
||||
|
||||
@property
|
||||
def source_index(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Source index if it exists, otherwise returns ``1``.
|
||||
|
||||
It is recommended to not use this unless you know the source will never add new content
|
||||
(it is easy for this value to change).
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_INDEX, self.playlist_index)
|
||||
|
||||
@property
|
||||
def source_index_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The source index, padded.
|
||||
"""
|
||||
return pad(self.source_index, 2)
|
||||
|
||||
@property
|
||||
def source_count(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The source count if it exists, otherwise returns 1.
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_COUNT, self.playlist_count)
|
||||
|
||||
@property
|
||||
def source_webpage_url(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The source webpage url if it exists, otherwise returns the playlist webpage url.
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_WEBPAGE_URL, self.playlist_webpage_url)
|
||||
|
||||
@property
|
||||
def source_description(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The source description if it exists, otherwise returns the playlist description.
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_DESCRIPTION, self.playlist_description)
|
||||
|
||||
@property
|
||||
def playlist_uid(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist unique ID if it exists, otherwise return the entry unique ID.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_UID, self.uid)
|
||||
|
||||
@property
|
||||
def playlist_title(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Name of its parent playlist/channel if it exists, otherwise returns its title.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_TITLE, self.title)
|
||||
|
||||
@property
|
||||
def playlist_title_sanitized(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist name, sanitized
|
||||
"""
|
||||
return sanitize_filename(self.playlist_title)
|
||||
|
||||
@property
|
||||
def playlist_index(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Playlist index if it exists, otherwise returns ``1``.
|
||||
|
||||
Note that for channels/playlists, any change (i.e. adding or removing a video) will make
|
||||
this value change. Use with caution.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_INDEX, 1)
|
||||
|
||||
@property
|
||||
def playlist_index_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Playlist index reversed via ``playlist_count - playlist_index + 1``
|
||||
"""
|
||||
return self.playlist_count - self.playlist_index + 1
|
||||
|
||||
@property
|
||||
def playlist_index_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
playlist_index padded two digits
|
||||
"""
|
||||
return pad(self.playlist_index, width=2)
|
||||
|
||||
@property
|
||||
def playlist_index_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
playlist_index_reversed padded two digits
|
||||
"""
|
||||
return pad(self.playlist_index_reversed, width=2)
|
||||
|
||||
@property
|
||||
def playlist_index_padded6(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
playlist_index padded six digits.
|
||||
"""
|
||||
return pad(self.playlist_index, width=6)
|
||||
|
||||
@property
|
||||
def playlist_index_reversed_padded6(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
playlist_index_reversed padded six digits.
|
||||
"""
|
||||
return pad(self.playlist_index_reversed, width=6)
|
||||
|
||||
@property
|
||||
def playlist_count(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Playlist count if it exists, otherwise returns ``1``.
|
||||
|
||||
Note that for channels/playlists, any change (i.e. adding or removing a video) will make
|
||||
this value change. Use with caution.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_COUNT, 1)
|
||||
|
||||
@property
|
||||
def playlist_description(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist description if it exists, otherwise returns the entry's description.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_DESCRIPTION, self.description)
|
||||
|
||||
@property
|
||||
def playlist_webpage_url(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist webpage url if it exists. Otherwise, returns the entry webpage url.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_WEBPAGE_URL, self.webpage_url)
|
||||
|
||||
@property
|
||||
def playlist_max_upload_year(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Max upload_year for all entries in this entry's playlist if it exists, otherwise returns
|
||||
``upload_year``
|
||||
"""
|
||||
# override in EntryParent
|
||||
return self.kwargs_get(PLAYLIST_MAX_UPLOAD_YEAR, self.upload_year)
|
||||
|
||||
@property
|
||||
def playlist_max_upload_year_truncated(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The max playlist truncated upload year for all entries in this entry's playlist if it
|
||||
exists, otherwise returns ``upload_year_truncated``.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_MAX_UPLOAD_YEAR_TRUNCATED, self.upload_year_truncated)
|
||||
|
||||
@property
|
||||
def playlist_uploader_id(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist uploader id if it exists, otherwise returns the entry uploader ID.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_UPLOADER_ID, self.uploader_id)
|
||||
|
||||
@property
|
||||
def playlist_uploader(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist uploader if it exists, otherwise return the entry uploader.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_UPLOADER, self.uploader)
|
||||
|
||||
@property
|
||||
def playlist_uploader_sanitized(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist uploader, sanitized.
|
||||
"""
|
||||
return sanitize_filename(self.playlist_uploader)
|
||||
|
||||
@property
|
||||
def playlist_uploader_url(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The playlist uploader url if it exists, otherwise returns the playlist webpage_url.
|
||||
"""
|
||||
return self.kwargs_get(PLAYLIST_UPLOADER_URL, self.playlist_webpage_url)
|
||||
|
||||
@property
|
||||
def source_uploader_id(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The source uploader id if it exists, otherwise returns the playlist_uploader_id
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_UPLOADER_ID, self.playlist_uploader_id)
|
||||
|
||||
@property
|
||||
def source_uploader(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The source uploader if it exists, otherwise return the playlist_uploader
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_UPLOADER, self.playlist_uploader)
|
||||
|
||||
@property
|
||||
def source_uploader_url(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The source uploader url if it exists, otherwise returns the source webpage_url.
|
||||
"""
|
||||
return self.kwargs_get(SOURCE_UPLOADER_URL, self.source_webpage_url)
|
||||
|
||||
@property
|
||||
def creator(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The creator name if it exists, otherwise returns the channel.
|
||||
"""
|
||||
return self.kwargs_get(CREATOR, self.channel)
|
||||
|
||||
@property
|
||||
def creator_sanitized(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The creator name, sanitized
|
||||
"""
|
||||
return sanitize_filename(self.creator)
|
||||
|
||||
@property
|
||||
def channel(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The channel name if it exists, otherwise returns the uploader.
|
||||
"""
|
||||
return self.kwargs_get(CHANNEL, self.uploader)
|
||||
|
||||
@property
|
||||
def channel_sanitized(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The channel name, sanitized.
|
||||
"""
|
||||
return sanitize_filename(self.channel)
|
||||
|
||||
@property
|
||||
def channel_id(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The channel id if it exists, otherwise returns the entry uploader ID.
|
||||
"""
|
||||
return self.kwargs_get(CHANNEL_ID, self.uploader_id)
|
||||
|
||||
@property
|
||||
def ext(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The downloaded entry's file extension
|
||||
"""
|
||||
return self.kwargs(EXT)
|
||||
|
||||
@property
|
||||
def thumbnail_ext(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The download entry's thumbnail extension. Will always return 'jpg'. Until there is a
|
||||
need to support other image types, we always convert to jpg.
|
||||
"""
|
||||
return "jpg"
|
||||
|
||||
@property
|
||||
def download_index(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The i'th entry downloaded. NOTE that this is fetched dynamically from the download
|
||||
archive.
|
||||
"""
|
||||
return self.kwargs_get(DOWNLOAD_INDEX, 0) + 1
|
||||
|
||||
@property
|
||||
def download_index_padded6(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The download_index padded six digits
|
||||
"""
|
||||
return pad(self.download_index, 6)
|
||||
|
||||
@property
|
||||
def upload_date_index(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The i'th entry downloaded with this upload date.
|
||||
"""
|
||||
return self.kwargs_get(UPLOAD_DATE_INDEX, 0) + 1
|
||||
|
||||
@property
|
||||
def upload_date_index_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload_date_index padded two digits
|
||||
"""
|
||||
return pad(self.upload_date_index, 2)
|
||||
|
||||
@property
|
||||
def upload_date_index_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
100 - upload_date_index
|
||||
"""
|
||||
return 100 - self.upload_date_index
|
||||
|
||||
@property
|
||||
def upload_date_index_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload_date_index padded two digits
|
||||
"""
|
||||
return pad(self.upload_date_index_reversed, 2)
|
||||
|
||||
@property
|
||||
def upload_date(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The entry's uploaded date, in YYYYMMDD format. If not present, return today's date.
|
||||
"""
|
||||
return self.kwargs_get(UPLOAD_DATE, datetime.now().strftime("%Y%m%d"))
|
||||
|
||||
@property
|
||||
def upload_year(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The entry's upload year
|
||||
"""
|
||||
return int(self.upload_date[:4])
|
||||
|
||||
@property
|
||||
def upload_year_truncated(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The last two digits of the upload year, i.e. 22 in 2022
|
||||
"""
|
||||
return int(str(self.upload_year)[-2:])
|
||||
|
||||
@property
|
||||
def upload_year_truncated_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e.
|
||||
2022 returns ``100 - 22`` = ``78``
|
||||
"""
|
||||
return 100 - self.upload_year_truncated
|
||||
|
||||
@property
|
||||
def upload_month_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10``
|
||||
"""
|
||||
return 13 - self.upload_month
|
||||
|
||||
@property
|
||||
def upload_month_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The reversed upload month, but padded. i.e. November returns "02"
|
||||
"""
|
||||
return pad(self.upload_month_reversed)
|
||||
|
||||
@property
|
||||
def upload_month_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The entry's upload month padded to two digits, i.e. March returns "03"
|
||||
"""
|
||||
return self.upload_date[4:6]
|
||||
|
||||
@property
|
||||
def upload_day_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The entry's upload day padded to two digits, i.e. the fifth returns "05"
|
||||
"""
|
||||
return self.upload_date[6:8]
|
||||
|
||||
@property
|
||||
def upload_month(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload month as an integer (no padding).
|
||||
"""
|
||||
return int(self.upload_month_padded.lstrip("0"))
|
||||
|
||||
@property
|
||||
def upload_day(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload day as an integer (no padding).
|
||||
"""
|
||||
return int(self.upload_day_padded.lstrip("0"))
|
||||
|
||||
@property
|
||||
def upload_day_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload day, but reversed using ``{total_days_in_month} + 1 - {upload_day}``,
|
||||
i.e. August 8th would have upload_day_reversed of ``31 + 1 - 8`` = ``24``
|
||||
"""
|
||||
total_days_in_month = _days_in_month[self.upload_month]
|
||||
if self.upload_month == 2 and self.upload_year % 4 == 0: # leap year
|
||||
total_days_in_month += 1
|
||||
|
||||
return total_days_in_month + 1 - self.upload_day
|
||||
|
||||
@property
|
||||
def upload_day_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The reversed upload day, but padded. i.e. August 30th returns "02".
|
||||
"""
|
||||
return pad(self.upload_day_reversed)
|
||||
|
||||
@property
|
||||
def upload_day_of_year(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The day of the year, i.e. February 1st returns ``32``
|
||||
"""
|
||||
output = sum(_days_in_month[: self.upload_month]) + self.upload_day
|
||||
if self.upload_month > 2 and self.upload_year % 4 == 0:
|
||||
output += 1
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def upload_day_of_year_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The upload day of year, but padded i.e. February 1st returns "032"
|
||||
"""
|
||||
return pad(self.upload_day_of_year, width=3)
|
||||
|
||||
@property
|
||||
def upload_day_of_year_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The upload day, but reversed using ``{total_days_in_year} + 1 - {upload_day}``,
|
||||
i.e. February 2nd would have upload_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
|
||||
"""
|
||||
total_days_in_year = 365
|
||||
if self.upload_year % 4 == 0:
|
||||
total_days_in_year += 1
|
||||
|
||||
return total_days_in_year + 1 - self.upload_day_of_year
|
||||
|
||||
@property
|
||||
def upload_day_of_year_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The reversed upload day of year, but padded i.e. December 31st returns "001"
|
||||
"""
|
||||
return pad(self.upload_day_of_year_reversed, width=3)
|
||||
|
||||
@property
|
||||
def upload_date_standardized(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The uploaded date formatted as YYYY-MM-DD
|
||||
"""
|
||||
return f"{self.upload_year}-{self.upload_month_padded}-{self.upload_day_padded}"
|
||||
|
||||
@property
|
||||
def release_date(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The entry's release date, in YYYYMMDD format. If not present, return the upload date.
|
||||
"""
|
||||
return self.kwargs_get(RELEASE_DATE, self.upload_date)
|
||||
|
||||
@property
|
||||
def release_year(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The entry's release year
|
||||
"""
|
||||
return int(self.release_date[:4])
|
||||
|
||||
@property
|
||||
def release_year_truncated(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The last two digits of the release year, i.e. 22 in 2022
|
||||
"""
|
||||
return int(str(self.release_year)[-2:])
|
||||
|
||||
@property
|
||||
def release_year_truncated_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The release year truncated, but reversed using ``100 - {release_year_truncated}``, i.e.
|
||||
2022 returns ``100 - 22`` = ``78``
|
||||
"""
|
||||
return 100 - self.release_year_truncated
|
||||
|
||||
@property
|
||||
def release_month_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The release month, but reversed
|
||||
using ``13 - {release_month}``, i.e. March returns ``10``
|
||||
"""
|
||||
return 13 - self.release_month
|
||||
|
||||
@property
|
||||
def release_month_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The reversed release month, but padded. i.e. November returns "02"
|
||||
"""
|
||||
return pad(self.release_month_reversed)
|
||||
|
||||
@property
|
||||
def release_month_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The entry's release month padded to two digits, i.e. March returns "03"
|
||||
"""
|
||||
return self.release_date[4:6]
|
||||
|
||||
@property
|
||||
def release_day_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The entry's release day padded to two digits, i.e. the fifth returns "05"
|
||||
"""
|
||||
return self.release_date[6:8]
|
||||
|
||||
@property
|
||||
def release_month(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The release month as an integer (no padding).
|
||||
"""
|
||||
return int(self.release_month_padded.lstrip("0"))
|
||||
|
||||
@property
|
||||
def release_day(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The release day as an integer (no padding).
|
||||
"""
|
||||
return int(self.release_day_padded.lstrip("0"))
|
||||
|
||||
@property
|
||||
def release_day_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The release day, but reversed using ``{total_days_in_month} + 1 - {release_day}``,
|
||||
i.e. August 8th would have release_day_reversed of ``31 + 1 - 8`` = ``24``
|
||||
"""
|
||||
total_days_in_month = _days_in_month[self.release_month]
|
||||
if self.release_month == 2 and self.release_year % 4 == 0: # leap year
|
||||
total_days_in_month += 1
|
||||
|
||||
return total_days_in_month + 1 - self.release_day
|
||||
|
||||
@property
|
||||
def release_day_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The reversed release day, but padded. i.e. August 30th returns "02".
|
||||
"""
|
||||
return pad(self.release_day_reversed)
|
||||
|
||||
@property
|
||||
def release_day_of_year(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The day of the year, i.e. February 1st returns ``32``
|
||||
"""
|
||||
output = sum(_days_in_month[: self.release_month]) + self.release_day
|
||||
if self.release_month > 2 and self.release_year % 4 == 0:
|
||||
output += 1
|
||||
|
||||
return output
|
||||
|
||||
@property
|
||||
def release_day_of_year_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The release day of year, but padded i.e. February 1st returns "032"
|
||||
"""
|
||||
return pad(self.release_day_of_year, width=3)
|
||||
|
||||
@property
|
||||
def release_day_of_year_reversed(self: Self) -> int:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The release day, but reversed using ``{total_days_in_year} + 1 - {release_day}``,
|
||||
i.e. February 2nd would have release_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
|
||||
"""
|
||||
total_days_in_year = 365
|
||||
if self.release_year % 4 == 0:
|
||||
total_days_in_year += 1
|
||||
|
||||
return total_days_in_year + 1 - self.release_day_of_year
|
||||
|
||||
@property
|
||||
def release_day_of_year_reversed_padded(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The reversed release day of year, but padded i.e. December 31st returns "001"
|
||||
"""
|
||||
return pad(self.release_day_of_year_reversed, width=3)
|
||||
|
||||
@property
|
||||
def release_date_standardized(self: Self) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The release date formatted as YYYY-MM-DD
|
||||
"""
|
||||
return f"{self.release_year}-{self.release_month_padded}-{self.release_day_padded}"
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from enum import Enum
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
|
@ -159,6 +160,8 @@ class _Parser:
|
|||
self._error_highlight_pos = pos if pos is not None else self._pos
|
||||
|
||||
def _read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]:
|
||||
if isinstance(self._text, int):
|
||||
pass
|
||||
if self._pos >= len(self._text):
|
||||
return None
|
||||
|
||||
|
|
@ -562,7 +565,7 @@ def parse(
|
|||
Entrypoint for parsing ytdl-sub code into a Syntax Tree
|
||||
"""
|
||||
return _Parser(
|
||||
text=text,
|
||||
text=json.dumps(text) if not isinstance(text, str) else text,
|
||||
name=name,
|
||||
custom_function_names=custom_function_names,
|
||||
variable_names=variable_names,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ from typing import final
|
|||
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.script.parser import parse
|
||||
from ytdl_sub.script.script import Script
|
||||
from ytdl_sub.script.types.resolvable import Resolvable
|
||||
from ytdl_sub.utils.exceptions import InvalidVariableNameException
|
||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||
from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException
|
||||
|
|
@ -74,54 +78,10 @@ class StringFormatterValidator(StringValidator):
|
|||
"""
|
||||
|
||||
_expected_value_type_name = "format string"
|
||||
_variable_not_found_error_msg_formatter = (
|
||||
"Format variable '{variable_name}' does not exist. Available variables: {available_fields}"
|
||||
)
|
||||
|
||||
_max_format_recursion = 8
|
||||
|
||||
def __validate_and_get_format_variables(self) -> List[str]:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
List of format variables in the format string
|
||||
|
||||
Raises
|
||||
------
|
||||
ValidationException
|
||||
If the format string contains invalid variable formatting
|
||||
"""
|
||||
open_bracket_count = self.format_string.count("{")
|
||||
close_bracket_count = self.format_string.count("}")
|
||||
|
||||
if open_bracket_count != close_bracket_count:
|
||||
raise self._validation_exception(
|
||||
"Brackets are reserved for {variable_names} and should contain "
|
||||
"a single open and close bracket.",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
format_variables: List[str] = list(re.findall(_fields_validator, self.format_string))
|
||||
|
||||
if len(format_variables) != open_bracket_count:
|
||||
raise self._validation_exception(
|
||||
error_message=_fields_validator_exception_message,
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
for variable in format_variables:
|
||||
if iskeyword(variable):
|
||||
raise self._validation_exception(
|
||||
f"'{variable}' is a Python keyword and cannot be used as a variable.",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
return format_variables
|
||||
|
||||
def __init__(self, name, value: str):
|
||||
super().__init__(name=name, value=value)
|
||||
self.format_variables = self.__validate_and_get_format_variables()
|
||||
_ = parse(str(value))
|
||||
|
||||
@final
|
||||
@property
|
||||
|
|
@ -133,36 +93,12 @@ class StringFormatterValidator(StringValidator):
|
|||
"""
|
||||
return self._value
|
||||
|
||||
def _apply_formatter(
|
||||
self, formatter: "StringFormatterValidator", variable_dict: Dict[str, str]
|
||||
) -> "StringFormatterValidator":
|
||||
# Ensure the variable names exist within the entry and overrides
|
||||
for variable_name in formatter.format_variables:
|
||||
# If the variable exists, but is sanitized...
|
||||
if (
|
||||
variable_name.endswith("_sanitized")
|
||||
and variable_name.removesuffix("_sanitized") in variable_dict
|
||||
):
|
||||
# Resolve just the non-sanitized version, then sanitize it
|
||||
variable_dict[variable_name] = sanitize_filename(
|
||||
StringFormatterValidator(
|
||||
name=self._name, value=f"{{{variable_name.removesuffix('_sanitized')}}}"
|
||||
).apply_formatter(variable_dict)
|
||||
)
|
||||
# If the variable doesn't exist, error
|
||||
elif variable_name not in variable_dict:
|
||||
available_fields = ", ".join(sorted(variable_dict.keys()))
|
||||
raise self._validation_exception(
|
||||
self._variable_not_found_error_msg_formatter.format(
|
||||
variable_name=variable_name, available_fields=available_fields
|
||||
),
|
||||
exception_class=StringFormattingVariableNotFoundException,
|
||||
)
|
||||
|
||||
return StringFormatterValidator(
|
||||
name=self._name,
|
||||
value=formatter.format_string.format(**OrderedDict(variable_dict)),
|
||||
)
|
||||
def _variable_dict(self, variable_dict: Dict[str, str]) -> Dict[str, str]:
|
||||
sanitized_variables = {
|
||||
f"{var_name}_sanitized": f"{{%sanitize({var_name})}}"
|
||||
for var_name in variable_dict.keys()
|
||||
}
|
||||
return dict(variable_dict, **sanitized_variables)
|
||||
|
||||
def apply_formatter(self, variable_dict: Dict[str, str]) -> str:
|
||||
"""
|
||||
|
|
@ -177,23 +113,12 @@ class StringFormatterValidator(StringValidator):
|
|||
-------
|
||||
Format string formatted
|
||||
"""
|
||||
formatter = self
|
||||
recursion_depth = 0
|
||||
max_depth = self._max_format_recursion
|
||||
|
||||
while formatter.format_variables and recursion_depth < max_depth:
|
||||
formatter = self._apply_formatter(formatter=formatter, variable_dict=variable_dict)
|
||||
recursion_depth += 1
|
||||
|
||||
if formatter.format_variables:
|
||||
raise self._validation_exception(
|
||||
f"Attempted to format but failed after reaching max recursion depth of "
|
||||
f"{max_depth}. Try to keep variables dependent on only one other variable at max. "
|
||||
f"Unresolved variables: {', '.join(sorted(formatter.format_variables))}",
|
||||
exception_class=StringFormattingException,
|
||||
)
|
||||
|
||||
return formatter.format_string
|
||||
out = (
|
||||
Script(self._variable_dict(variable_dict))
|
||||
.add({"tmp_var": self.format_string})
|
||||
.resolve()["tmp_var"]
|
||||
)
|
||||
return str(out)
|
||||
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
|
|
@ -214,6 +139,31 @@ class OverridesStringFormatterValidator(StringFormatterValidator):
|
|||
"static string. Available override variables: {available_fields}"
|
||||
)
|
||||
|
||||
def apply_formatter(self, variable_dict: Dict[str, str]) -> str:
|
||||
"""
|
||||
Calls `format` on the format string using the variable_dict as input kwargs
|
||||
|
||||
Parameters
|
||||
----------
|
||||
variable_dict
|
||||
kwargs to pass to the format string
|
||||
|
||||
Returns
|
||||
-------
|
||||
Format string formatted
|
||||
"""
|
||||
output = (
|
||||
Script(self._variable_dict(variable_dict))
|
||||
.add({"tmp_var": self.format_string})
|
||||
.resolve(unresolvable={VARIABLES.entry_metadata.variable_name})
|
||||
)
|
||||
if "tmp_var" not in output:
|
||||
raise self._validation_exception(
|
||||
"Has a dependency on entry variables when it is not allowed",
|
||||
exception_class=StringFormattingVariableNotFoundException,
|
||||
)
|
||||
return str(output["tmp_var"])
|
||||
|
||||
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from resources import file_fixture_path
|
|||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.config.config_file import ConfigFile
|
||||
from ytdl_sub.entries.script.function_scripts import CustomFunctions
|
||||
from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
|
|
@ -26,6 +27,14 @@ from ytdl_sub.utils.logger import LoggerLevels
|
|||
from ytdl_sub.utils.yaml import load_yaml
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_custom_functions():
|
||||
"""
|
||||
Clean logs after every test
|
||||
"""
|
||||
CustomFunctions.register()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_debug_file():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue