playlist_max maybe working? need to eval
This commit is contained in:
parent
229af2351a
commit
6bb9e96384
13 changed files with 322 additions and 230 deletions
132
src/ytdl_sub/entries/script/custom_functions.py
Normal file
132
src/ytdl_sub/entries/script/custom_functions.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import os
|
||||
import posixpath
|
||||
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.script.functions import Functions
|
||||
from ytdl_sub.script.types.map import Map
|
||||
from ytdl_sub.script.types.resolvable import AnyArgument
|
||||
from ytdl_sub.script.types.resolvable import Integer
|
||||
from ytdl_sub.script.types.resolvable import ReturnableArgument
|
||||
from ytdl_sub.script.types.resolvable import String
|
||||
from ytdl_sub.script.utils.exceptions import RuntimeException
|
||||
from ytdl_sub.utils.file_path import FilePathTruncater
|
||||
|
||||
|
||||
def _pad(num: int, width: int):
|
||||
return str(num).zfill(width)
|
||||
|
||||
|
||||
_days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
|
||||
|
||||
class CustomFunctions:
|
||||
@staticmethod
|
||||
def legacy_bracket_safety(value: ReturnableArgument) -> ReturnableArgument:
|
||||
if isinstance(value, String):
|
||||
value = String(value.value.replace("{", "{").replace("}", "}"))
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def to_native_filepath(filepath: String) -> String:
|
||||
return String(filepath.value.replace(posixpath.sep, os.sep))
|
||||
|
||||
@staticmethod
|
||||
def truncate_filepath_if_too_long(filepath: String) -> String:
|
||||
return String(FilePathTruncater.maybe_truncate_file_path(filepath.value))
|
||||
|
||||
@staticmethod
|
||||
def sanitize(value: AnyArgument) -> String:
|
||||
return String(sanitize_filename(str(value)))
|
||||
|
||||
@staticmethod
|
||||
def sanitize_plex_episode(string: String) -> String:
|
||||
sanitized_string = CustomFunctions.sanitize(string).value
|
||||
out = ""
|
||||
for char in sanitized_string:
|
||||
match char:
|
||||
case "0":
|
||||
out += "0"
|
||||
case "1":
|
||||
out += "1"
|
||||
case "2":
|
||||
out += "2"
|
||||
case "3":
|
||||
out += "3"
|
||||
case "4":
|
||||
out += "4"
|
||||
case "5":
|
||||
out += "5"
|
||||
case "6":
|
||||
out += "6"
|
||||
case "7":
|
||||
out += "7"
|
||||
case "8":
|
||||
out += "8"
|
||||
case "9":
|
||||
out += "9"
|
||||
case _:
|
||||
out += char
|
||||
return String(out)
|
||||
|
||||
@staticmethod
|
||||
def to_date_metadata(yyyymmdd: String) -> Map:
|
||||
date_str = yyyymmdd.value
|
||||
if not (date_str.isnumeric() and len(date_str) == 8):
|
||||
raise RuntimeException(
|
||||
f"Expected input of date_metadata to be YYYYMMDD, but received {date_str}"
|
||||
)
|
||||
|
||||
year: int = int(date_str[:4])
|
||||
month_padded: str = date_str[4:6]
|
||||
day_padded: str = date_str[6:8]
|
||||
|
||||
month: int = int(month_padded)
|
||||
day: int = int(day_padded)
|
||||
year_truncated: int = int(str(year)[-2:])
|
||||
|
||||
day_of_year: int = sum(_days_in_month[:month]) + day
|
||||
total_days_in_month: int = _days_in_month[month]
|
||||
total_days_in_year: int = 365
|
||||
if year % 4 == 0:
|
||||
total_days_in_year += 1
|
||||
if month == 2:
|
||||
total_days_in_month += 1
|
||||
if month > 2:
|
||||
day_of_year += 1
|
||||
|
||||
day_of_year_reversed: int = total_days_in_year + 1 - day_of_year
|
||||
month_reversed: int = 13 - month
|
||||
day_reversed: int = total_days_in_month + 1 - day
|
||||
|
||||
return Map(
|
||||
{
|
||||
String("date"): yyyymmdd,
|
||||
String("date_standardized"): String(f"{year}-{month_padded}-{day_padded}"),
|
||||
String("year"): Integer(year),
|
||||
String("month"): Integer(month),
|
||||
String("day"): Integer(day),
|
||||
String("year_truncated"): String(year_truncated),
|
||||
String("month_padded"): String(month_padded),
|
||||
String("day_padded"): String(day_padded),
|
||||
String("year_truncated_reversed"): Integer(100 - year_truncated),
|
||||
String("month_reversed"): Integer(month_reversed),
|
||||
String("month_reversed_padded"): String(_pad(month_reversed, width=2)),
|
||||
String("day_reversed"): Integer(day_reversed),
|
||||
String("day_reversed_padded"): String(_pad(day_reversed, width=2)),
|
||||
String("day_of_year"): Integer(day_of_year),
|
||||
String("day_of_year_padded"): String(_pad(day_of_year, width=3)),
|
||||
String("day_of_year_reversed"): Integer(day_of_year_reversed),
|
||||
String("day_of_year_reversed_padded"): String(_pad(day_of_year_reversed, width=3)),
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
if not Functions.is_built_in("sanitize"):
|
||||
Functions.register_function(CustomFunctions.legacy_bracket_safety)
|
||||
Functions.register_function(CustomFunctions.truncate_filepath_if_too_long)
|
||||
Functions.register_function(CustomFunctions.to_native_filepath)
|
||||
Functions.register_function(CustomFunctions.sanitize)
|
||||
Functions.register_function(CustomFunctions.sanitize_plex_episode)
|
||||
Functions.register_function(CustomFunctions.to_date_metadata)
|
||||
|
|
@ -1,132 +1,32 @@
|
|||
import os
|
||||
import posixpath
|
||||
from typing import Dict
|
||||
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES as v
|
||||
|
||||
from ytdl_sub.script.functions import Functions
|
||||
from ytdl_sub.script.types.map import Map
|
||||
from ytdl_sub.script.types.resolvable import AnyArgument
|
||||
from ytdl_sub.script.types.resolvable import Integer
|
||||
from ytdl_sub.script.types.resolvable import ReturnableArgument
|
||||
from ytdl_sub.script.types.resolvable import String
|
||||
from ytdl_sub.script.utils.exceptions import RuntimeException
|
||||
from ytdl_sub.utils.file_path import FilePathTruncater
|
||||
|
||||
|
||||
def _pad(num: int, width: int):
|
||||
return str(num).zfill(width)
|
||||
|
||||
|
||||
_days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
|
||||
|
||||
class CustomFunctions:
|
||||
@staticmethod
|
||||
def legacy_bracket_safety(value: ReturnableArgument) -> ReturnableArgument:
|
||||
if isinstance(value, String):
|
||||
value = String(value.value.replace("{", "{").replace("}", "}"))
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def to_native_filepath(filepath: String) -> String:
|
||||
return String(filepath.value.replace(posixpath.sep, os.sep))
|
||||
|
||||
@staticmethod
|
||||
def truncate_filepath_if_too_long(filepath: String) -> String:
|
||||
return String(FilePathTruncater.maybe_truncate_file_path(filepath.value))
|
||||
|
||||
@staticmethod
|
||||
def sanitize(value: AnyArgument) -> String:
|
||||
return String(sanitize_filename(str(value)))
|
||||
|
||||
@staticmethod
|
||||
def sanitize_plex_episode(string: String) -> String:
|
||||
sanitized_string = CustomFunctions.sanitize(string).value
|
||||
out = ""
|
||||
for char in sanitized_string:
|
||||
match char:
|
||||
case "0":
|
||||
out += "0"
|
||||
case "1":
|
||||
out += "1"
|
||||
case "2":
|
||||
out += "2"
|
||||
case "3":
|
||||
out += "3"
|
||||
case "4":
|
||||
out += "4"
|
||||
case "5":
|
||||
out += "5"
|
||||
case "6":
|
||||
out += "6"
|
||||
case "7":
|
||||
out += "7"
|
||||
case "8":
|
||||
out += "8"
|
||||
case "9":
|
||||
out += "9"
|
||||
case _:
|
||||
out += char
|
||||
return String(out)
|
||||
|
||||
@staticmethod
|
||||
def to_date_metadata(yyyymmdd: String) -> Map:
|
||||
date_str = yyyymmdd.value
|
||||
if not (date_str.isnumeric() and len(date_str) == 8):
|
||||
raise RuntimeException(
|
||||
f"Expected input of date_metadata to be YYYYMMDD, but received {date_str}"
|
||||
)
|
||||
|
||||
year: int = int(date_str[:4])
|
||||
month_padded: str = date_str[4:6]
|
||||
day_padded: str = date_str[6:8]
|
||||
|
||||
month: int = int(month_padded)
|
||||
day: int = int(day_padded)
|
||||
year_truncated: int = int(str(year)[-2:])
|
||||
|
||||
day_of_year: int = sum(_days_in_month[:month]) + day
|
||||
total_days_in_month: int = _days_in_month[month]
|
||||
total_days_in_year: int = 365
|
||||
if year % 4 == 0:
|
||||
total_days_in_year += 1
|
||||
if month == 2:
|
||||
total_days_in_month += 1
|
||||
if month > 2:
|
||||
day_of_year += 1
|
||||
|
||||
day_of_year_reversed: int = total_days_in_year + 1 - day_of_year
|
||||
month_reversed: int = 13 - month
|
||||
day_reversed: int = total_days_in_month + 1 - day
|
||||
|
||||
return Map(
|
||||
{
|
||||
String("date"): yyyymmdd,
|
||||
String("date_standardized"): String(f"{year}-{month_padded}-{day_padded}"),
|
||||
String("year"): Integer(year),
|
||||
String("month"): Integer(month),
|
||||
String("day"): Integer(day),
|
||||
String("year_truncated"): String(year_truncated),
|
||||
String("month_padded"): String(month_padded),
|
||||
String("day_padded"): String(day_padded),
|
||||
String("year_truncated_reversed"): Integer(100 - year_truncated),
|
||||
String("month_reversed"): Integer(month_reversed),
|
||||
String("month_reversed_padded"): String(_pad(month_reversed, width=2)),
|
||||
String("day_reversed"): Integer(day_reversed),
|
||||
String("day_reversed_padded"): String(_pad(day_reversed, width=2)),
|
||||
String("day_of_year"): Integer(day_of_year),
|
||||
String("day_of_year_padded"): String(_pad(day_of_year, width=3)),
|
||||
String("day_of_year_reversed"): Integer(day_of_year_reversed),
|
||||
String("day_of_year_reversed_padded"): String(_pad(day_of_year_reversed, width=3)),
|
||||
}
|
||||
CUSTOM_FUNCTION_SCRIPTS: Dict[str, str] = {
|
||||
"%extract_field_from_metadata_array_getter": "{ %map_get( %map(%array_at($0, 0)), %array_at($0, 1) ) }",
|
||||
"%extract_field_from_metadata_array": f"""{{
|
||||
%if(
|
||||
%bool($0),
|
||||
%array_extend(
|
||||
%array_apply(
|
||||
%array_product(
|
||||
%array($0),
|
||||
[ %string($1) ]
|
||||
),
|
||||
%extract_field_from_metadata_array_getter
|
||||
)
|
||||
),
|
||||
[]
|
||||
)
|
||||
}}""",
|
||||
"%extract_field_from_siblings": f"""{{
|
||||
%if(
|
||||
%bool({v.sibling_metadata.variable_name}),
|
||||
%extract_field_from_metadata_array(
|
||||
{v.sibling_metadata.variable_name},
|
||||
$0
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
if not Functions.is_built_in("sanitize"):
|
||||
Functions.register_function(CustomFunctions.legacy_bracket_safety)
|
||||
Functions.register_function(CustomFunctions.truncate_filepath_if_too_long)
|
||||
Functions.register_function(CustomFunctions.to_native_filepath)
|
||||
Functions.register_function(CustomFunctions.sanitize)
|
||||
Functions.register_function(CustomFunctions.sanitize_plex_episode)
|
||||
Functions.register_function(CustomFunctions.to_date_metadata)
|
||||
}}""",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from typing import Set
|
|||
|
||||
import mergedeep
|
||||
|
||||
from ytdl_sub.entries.script.function_scripts import CustomFunctions
|
||||
from ytdl_sub.entries.script.custom_functions import CustomFunctions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES as v
|
||||
from ytdl_sub.entries.script.variable_definitions import Metadata
|
||||
from ytdl_sub.entries.script.variable_definitions import MetadataVariable
|
||||
|
|
@ -35,8 +35,11 @@ def date_metadata(date_key: Variable, metadata_key: str) -> str:
|
|||
# Metadata Getters
|
||||
|
||||
|
||||
def _get_internal(
|
||||
metadata: Metadata, key: MetadataVariable, default: Optional[Variable | str | int | Dict | List]
|
||||
def _get(
|
||||
cast: str,
|
||||
metadata: Metadata,
|
||||
key: MetadataVariable,
|
||||
default: Optional[Variable | str | int | Dict | List],
|
||||
) -> str:
|
||||
if default is None:
|
||||
# TODO: assert with good error message if key DNE
|
||||
|
|
@ -52,57 +55,51 @@ def _get_internal(
|
|||
else:
|
||||
out = f"%map_get_non_empty({metadata.variable_name}, '{key.metadata_key}', {default})"
|
||||
|
||||
return f"%legacy_bracket_safety({out})"
|
||||
|
||||
|
||||
def _get_int(
|
||||
metadata: Metadata, key: MetadataVariable, default: Optional[Variable | int] = None
|
||||
) -> str:
|
||||
return f"{{%int({_get_internal(metadata=metadata, key=key, default=default)})}}"
|
||||
|
||||
|
||||
def _get(
|
||||
metadata: Metadata, key: MetadataVariable, default: Optional[Variable | str | int | Dict | List]
|
||||
) -> str:
|
||||
return f"{{{_get_internal(metadata=metadata, key=key, default=default)}}}"
|
||||
return f"{{ %legacy_bracket_safety(%{cast}({out})) }}"
|
||||
|
||||
|
||||
###############################################################################################
|
||||
# Entry Getters
|
||||
|
||||
|
||||
def entry_get(
|
||||
key: MetadataVariable, default: Optional[Variable | str | int | Dict | List] = None
|
||||
) -> str:
|
||||
return _get(metadata=v.entry_metadata, key=key, default=default)
|
||||
def entry_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
|
||||
return _get("string", metadata=v.entry_metadata, key=key, default=default)
|
||||
|
||||
|
||||
def entry_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
|
||||
return _get_int(metadata=v.entry_metadata, key=key, default=default)
|
||||
return _get("int", metadata=v.entry_metadata, key=key, default=default)
|
||||
|
||||
|
||||
def entry_get_map(key: MetadataVariable, default: Optional[Variable | Dict] = None):
|
||||
return _get("map", metadata=v.entry_metadata, key=key, default=default)
|
||||
|
||||
|
||||
def entry_get_array(key: MetadataVariable, default: Optional[Variable | List] = None):
|
||||
return _get("array", metadata=v.entry_metadata, key=key, default=default)
|
||||
|
||||
|
||||
###############################################################################################
|
||||
# Playlist Getters
|
||||
|
||||
|
||||
def playlist_get(key: MetadataVariable, default: Optional[Variable | str | int] = None) -> str:
|
||||
return _get(metadata=v.playlist_metadata, key=key, default=default)
|
||||
def playlist_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
|
||||
return _get("string", metadata=v.playlist_metadata, key=key, default=default)
|
||||
|
||||
|
||||
def playlist_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
|
||||
return _get_int(metadata=v.playlist_metadata, key=key, default=default)
|
||||
return _get("int", metadata=v.playlist_metadata, key=key, default=default)
|
||||
|
||||
|
||||
###############################################################################################
|
||||
# Source Getters
|
||||
|
||||
|
||||
def source_get(key: MetadataVariable, default: Optional[Variable | str | int] = None) -> str:
|
||||
return _get(metadata=v.source_metadata, key=key, default=default)
|
||||
def source_get_str(key: MetadataVariable, default: Optional[Variable | str] = None) -> str:
|
||||
return _get("string", metadata=v.source_metadata, key=key, default=default)
|
||||
|
||||
|
||||
def source_get_int(key: MetadataVariable, default: Optional[Variable | int] = None) -> str:
|
||||
return _get_int(metadata=v.source_metadata, key=key, default=default)
|
||||
return _get("int", metadata=v.source_metadata, key=key, default=default)
|
||||
|
||||
|
||||
###############################################################################################
|
||||
|
|
@ -116,32 +113,32 @@ ENTRY_HARDCODED_VARIABLES: Dict[Variable, str] = {
|
|||
}
|
||||
|
||||
ENTRY_RELATIVE_VARIABLES: Dict[MetadataVariable, str] = {
|
||||
v.playlist_metadata: entry_get(v.playlist_metadata, {}),
|
||||
v.source_metadata: entry_get(v.source_metadata, {}),
|
||||
v.sibling_metadata: entry_get(v.sibling_metadata, []),
|
||||
v.playlist_metadata: entry_get_map(v.playlist_metadata, {}),
|
||||
v.source_metadata: entry_get_map(v.source_metadata, {}),
|
||||
v.sibling_metadata: entry_get_array(v.sibling_metadata, []),
|
||||
}
|
||||
|
||||
ENTRY_REQUIRED_VARIABLES: Dict[MetadataVariable, str] = {
|
||||
v.uid: entry_get(v.uid),
|
||||
v.extractor_key: entry_get(v.extractor_key),
|
||||
v.uid: entry_get_str(v.uid),
|
||||
v.extractor_key: entry_get_str(v.extractor_key),
|
||||
v.epoch: entry_get_int(v.epoch),
|
||||
v.webpage_url: entry_get(v.webpage_url),
|
||||
v.ext: entry_get(v.ext),
|
||||
v.webpage_url: entry_get_str(v.webpage_url),
|
||||
v.ext: entry_get_str(v.ext),
|
||||
}
|
||||
|
||||
ENTRY_DEFAULT_VARIABLES: Dict[MetadataVariable, str] = {
|
||||
v.playlist_index: playlist_get_int(v.playlist_index, 1),
|
||||
v.title: entry_get(v.title, v.uid),
|
||||
v.extractor: entry_get(v.extractor, v.extractor_key),
|
||||
v.description: entry_get(v.description, ""),
|
||||
v.uploader_id: entry_get(v.uploader_id, v.uid),
|
||||
v.uploader: entry_get(v.uploader, v.uploader_id),
|
||||
v.uploader_url: entry_get(v.uploader_url, v.webpage_url),
|
||||
v.upload_date: entry_get(v.upload_date, v.epoch_date),
|
||||
v.release_date: entry_get(v.release_date, v.upload_date),
|
||||
v.channel: entry_get(v.channel, v.uploader),
|
||||
v.creator: entry_get(v.creator, v.channel),
|
||||
v.channel_id: entry_get(v.channel_id, v.uploader_id),
|
||||
v.title: entry_get_str(v.title, v.uid),
|
||||
v.extractor: entry_get_str(v.extractor, v.extractor_key),
|
||||
v.description: entry_get_str(v.description, ""),
|
||||
v.uploader_id: entry_get_str(v.uploader_id, v.uid),
|
||||
v.uploader: entry_get_str(v.uploader, v.uploader_id),
|
||||
v.uploader_url: entry_get_str(v.uploader_url, v.webpage_url),
|
||||
v.upload_date: entry_get_str(v.upload_date, v.epoch_date),
|
||||
v.release_date: entry_get_str(v.release_date, v.upload_date),
|
||||
v.channel: entry_get_str(v.channel, v.uploader),
|
||||
v.creator: entry_get_str(v.creator, v.channel),
|
||||
v.channel_id: entry_get_str(v.channel_id, v.uploader_id),
|
||||
}
|
||||
|
||||
# MARK AS UNRESOLVABLE UNTIL THEY ARE ADDED
|
||||
|
|
@ -212,14 +209,14 @@ ENTRY_RELEASE_DATE_VARIABLES: Dict[Variable, str] = {
|
|||
}
|
||||
|
||||
PLAYLIST_VARIABLES: Dict[Variable, str] = {
|
||||
v.playlist_uid: playlist_get(v.playlist_uid, v.uid),
|
||||
v.playlist_title: playlist_get(v.playlist_title, v.title),
|
||||
v.playlist_webpage_url: playlist_get(v.playlist_webpage_url, v.webpage_url),
|
||||
v.playlist_uid: playlist_get_str(v.playlist_uid, v.uid),
|
||||
v.playlist_title: playlist_get_str(v.playlist_title, v.title),
|
||||
v.playlist_webpage_url: playlist_get_str(v.playlist_webpage_url, v.webpage_url),
|
||||
v.playlist_count: playlist_get_int(v.playlist_count, 1),
|
||||
v.playlist_description: playlist_get(v.playlist_description, v.description),
|
||||
v.playlist_uploader_id: playlist_get(v.playlist_uploader_id, v.uploader_id),
|
||||
v.playlist_uploader: playlist_get(v.playlist_uploader, v.uploader),
|
||||
v.playlist_uploader_url: playlist_get(v.playlist_uploader_url, v.playlist_webpage_url),
|
||||
v.playlist_description: playlist_get_str(v.playlist_description, v.description),
|
||||
v.playlist_uploader_id: playlist_get_str(v.playlist_uploader_id, v.uploader_id),
|
||||
v.playlist_uploader: playlist_get_str(v.playlist_uploader, v.uploader),
|
||||
v.playlist_uploader_url: playlist_get_str(v.playlist_uploader_url, v.playlist_webpage_url),
|
||||
}
|
||||
|
||||
PLAYLIST_DERIVED_VARIABLES: Dict[Variable, str] = {
|
||||
|
|
@ -234,15 +231,15 @@ PLAYLIST_DERIVED_VARIABLES: Dict[Variable, str] = {
|
|||
|
||||
|
||||
SOURCE_VARIABLES: Dict[Variable, str] = {
|
||||
v.source_uid: source_get(v.source_uid, v.playlist_uid),
|
||||
v.source_title: source_get(v.source_title, v.playlist_title),
|
||||
v.source_webpage_url: source_get(v.source_webpage_url, v.playlist_webpage_url),
|
||||
v.source_uid: source_get_str(v.source_uid, v.playlist_uid),
|
||||
v.source_title: source_get_str(v.source_title, v.playlist_title),
|
||||
v.source_webpage_url: source_get_str(v.source_webpage_url, v.playlist_webpage_url),
|
||||
v.source_index: source_get_int(v.source_index, 1),
|
||||
v.source_count: source_get_int(v.source_count, 1),
|
||||
v.source_description: source_get(v.source_description, v.playlist_description),
|
||||
v.source_uploader_id: source_get(v.source_uploader_id, v.playlist_uploader_id),
|
||||
v.source_uploader: source_get(v.source_uploader, v.playlist_uploader),
|
||||
v.source_uploader_url: source_get(v.source_uploader_url, v.source_webpage_url),
|
||||
v.source_description: source_get_str(v.source_description, v.playlist_description),
|
||||
v.source_uploader_id: source_get_str(v.source_uploader_id, v.playlist_uploader_id),
|
||||
v.source_uploader: source_get_str(v.source_uploader, v.playlist_uploader),
|
||||
v.source_uploader_url: source_get_str(v.source_uploader_url, v.source_webpage_url),
|
||||
}
|
||||
|
||||
SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = {
|
||||
|
|
@ -251,7 +248,15 @@ SOURCE_DERIVED_VARIABLES: Dict[Variable, str] = {
|
|||
}
|
||||
|
||||
SIBLING_VARIABLES: Dict[Variable, str] = {
|
||||
v.playlist_max_upload_year: "TODO!!!",
|
||||
v.playlist_max_upload_year: f"""{{
|
||||
%array_reduce(
|
||||
%if_passthrough(
|
||||
%extract_field_from_siblings('{v.upload_year.variable_name}'),
|
||||
[{v.upload_year.variable_name}]
|
||||
),
|
||||
%max
|
||||
)
|
||||
}}"""
|
||||
}
|
||||
|
||||
SIBLING_DERIVED_VARIABLES: Dict[Variable, str] = {
|
||||
|
|
@ -270,6 +275,8 @@ mergedeep.merge(
|
|||
ENTRY_DERIVED_VARIABLES,
|
||||
ENTRY_UPLOAD_DATE_VARIABLES,
|
||||
ENTRY_RELEASE_DATE_VARIABLES,
|
||||
SIBLING_VARIABLES,
|
||||
SIBLING_DERIVED_VARIABLES,
|
||||
PLAYLIST_VARIABLES,
|
||||
PLAYLIST_DERIVED_VARIABLES,
|
||||
SOURCE_VARIABLES,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,22 @@ from ytdl_sub.script.types.resolvable import LambdaTwo
|
|||
from ytdl_sub.script.types.resolvable import Resolvable
|
||||
from ytdl_sub.script.utils.exceptions import UNREACHABLE
|
||||
from ytdl_sub.script.utils.exceptions import ArrayValueDoesNotExist
|
||||
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
|
||||
|
||||
|
||||
class ArrayFunctions:
|
||||
@staticmethod
|
||||
def array(maybe_array: AnyArgument) -> Array:
|
||||
if not isinstance(maybe_array, Array):
|
||||
raise FunctionRuntimeException(
|
||||
f"Tried and failed to cast {maybe_array.type_name()} as an Array"
|
||||
)
|
||||
return maybe_array
|
||||
|
||||
@staticmethod
|
||||
def array_size(array: Array) -> Integer:
|
||||
return Integer(len(array.value))
|
||||
|
||||
@staticmethod
|
||||
def array_extend(*arrays: Array) -> Array:
|
||||
"""
|
||||
|
|
@ -27,7 +40,7 @@ class ArrayFunctions:
|
|||
return Array(output)
|
||||
|
||||
@staticmethod
|
||||
def array_at(array: Array, idx: Integer) -> Resolvable:
|
||||
def array_at(array: Array, idx: Integer) -> AnyArgument:
|
||||
"""
|
||||
Return the element in the Array at index ``idx``.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -17,3 +17,15 @@ class ConditionalFunctions:
|
|||
if condition.value:
|
||||
return true
|
||||
return false
|
||||
|
||||
@staticmethod
|
||||
def if_passthrough(
|
||||
maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB
|
||||
) -> Union[ReturnableArgumentA, ReturnableArgumentB]:
|
||||
"""
|
||||
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
|
||||
otherwise returns ``else_arg``.
|
||||
"""
|
||||
if bool(maybe_true_arg.value):
|
||||
return maybe_true_arg
|
||||
return else_arg
|
||||
|
|
|
|||
|
|
@ -9,17 +9,45 @@ from ytdl_sub.script.types.resolvable import Integer
|
|||
from ytdl_sub.script.types.resolvable import LambdaThree
|
||||
from ytdl_sub.script.types.resolvable import LambdaTwo
|
||||
from ytdl_sub.script.types.resolvable import String
|
||||
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
|
||||
from ytdl_sub.script.utils.exceptions import KeyDoesNotExistRuntimeException
|
||||
from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException
|
||||
|
||||
|
||||
class MapFunctions:
|
||||
@staticmethod
|
||||
def map_get(mapping: Map, key: Hashable, default: Optional[AnyArgument] = None) -> AnyArgument:
|
||||
def map(maybe_mapping: AnyArgument) -> Map:
|
||||
if not isinstance(maybe_mapping, Map):
|
||||
raise FunctionRuntimeException(
|
||||
f"Tried and failed to cast {maybe_mapping.type_name()} as a Map"
|
||||
)
|
||||
return maybe_mapping
|
||||
|
||||
@staticmethod
|
||||
def map_size(mapping: Map) -> Integer:
|
||||
return Integer(len(mapping.value))
|
||||
|
||||
@staticmethod
|
||||
def map_contains(mapping: Map, key: AnyArgument) -> Boolean:
|
||||
"""
|
||||
Returns True if the key is in the Map. False otherwise.
|
||||
"""
|
||||
if not isinstance(key, Hashable):
|
||||
raise KeyNotHashableRuntimeException(
|
||||
f"Tried to use {key.type_name()} as a Map key, but it is not hashable."
|
||||
)
|
||||
|
||||
return Boolean(key in mapping.value)
|
||||
|
||||
@staticmethod
|
||||
def map_get(
|
||||
mapping: Map, key: AnyArgument, default: Optional[AnyArgument] = None
|
||||
) -> AnyArgument:
|
||||
"""
|
||||
Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is
|
||||
provided, it will return ``default``. Otherwise, will error.
|
||||
"""
|
||||
if key not in mapping.value:
|
||||
if not MapFunctions.map_contains(mapping=mapping, key=key).value:
|
||||
if default is not None:
|
||||
return default
|
||||
|
||||
|
|
@ -29,7 +57,7 @@ class MapFunctions:
|
|||
return mapping.value[key]
|
||||
|
||||
@staticmethod
|
||||
def map_get_non_empty(mapping: Map, key: Hashable, default: AnyArgument) -> AnyArgument:
|
||||
def map_get_non_empty(mapping: Map, key: AnyArgument, default: AnyArgument) -> AnyArgument:
|
||||
"""
|
||||
Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string,
|
||||
return ``default``. Otherwise, will error.
|
||||
|
|
@ -39,13 +67,6 @@ class MapFunctions:
|
|||
return default
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def map_contains(mapping: Map, key: Hashable) -> Boolean:
|
||||
"""
|
||||
Returns True if the key is in the Map. False otherwise.
|
||||
"""
|
||||
return Boolean(key in mapping.value)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from ytdl_sub.script.types.array import UnresolvedArray
|
|||
from ytdl_sub.script.types.resolvable import Argument
|
||||
from ytdl_sub.script.types.resolvable import BuiltInFunctionType
|
||||
from ytdl_sub.script.types.resolvable import FunctionType
|
||||
from ytdl_sub.script.types.resolvable import FutureResolvable
|
||||
from ytdl_sub.script.types.resolvable import Lambda
|
||||
from ytdl_sub.script.types.resolvable import NamedCustomFunction
|
||||
from ytdl_sub.script.types.resolvable import Resolvable
|
||||
|
|
@ -105,6 +106,8 @@ class BuiltInFunction(Function, BuiltInFunctionType):
|
|||
def _arg_output_type(cls, arg: Argument) -> Type[Argument]:
|
||||
if isinstance(arg, BuiltInFunction):
|
||||
return arg.output_type()
|
||||
if isinstance(arg, FutureResolvable):
|
||||
return arg.future_resolvable_type()
|
||||
return type(arg)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ def is_type_compatible(
|
|||
arg_type: Type[NamedType] = arg.__class__
|
||||
if isinstance(arg, BuiltInFunctionType):
|
||||
arg_type = arg.output_type() # built-in function
|
||||
elif isinstance(arg, FutureResolvable):
|
||||
|
||||
if isinstance(arg, FutureResolvable):
|
||||
arg_type = arg.future_resolvable_type()
|
||||
elif isinstance(arg, FunctionType):
|
||||
return True # custom-function, can be anything, so pass for now
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from typing import Any
|
|||
from typing import Dict
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
|
||||
from ytdl_sub.entries.script.variable_scripts import UNRESOLVED_VARIABLES
|
||||
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS
|
||||
from ytdl_sub.script.script import Script
|
||||
|
|
@ -12,7 +13,9 @@ from ytdl_sub.utils.script import ScriptUtils
|
|||
|
||||
class Scriptable(ABC):
|
||||
def __init__(self):
|
||||
self.script = Script(copy.deepcopy(VARIABLE_SCRIPTS))
|
||||
self.script = Script(
|
||||
dict(copy.deepcopy(VARIABLE_SCRIPTS), **copy.deepcopy(CUSTOM_FUNCTION_SCRIPTS))
|
||||
)
|
||||
self.unresolvable: Set[str] = copy.deepcopy(UNRESOLVED_VARIABLES)
|
||||
|
||||
def update_script(self) -> None:
|
||||
|
|
|
|||
|
|
@ -19,7 +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.entries.script.custom_functions 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
|
||||
|
|
|
|||
|
|
@ -15,21 +15,21 @@ from ytdl_sub.script.utils.exceptions import InvalidSyntaxException
|
|||
|
||||
class TestArray:
|
||||
def test_return(self):
|
||||
assert Script({"array": "{['a', 3.14]}"}).resolve() == ScriptOutput(
|
||||
{"array": Array([String("a"), Float(3.14)])}
|
||||
assert Script({"arr": "{['a', 3.14]}"}).resolve() == ScriptOutput(
|
||||
{"arr": Array([String("a"), Float(3.14)])}
|
||||
)
|
||||
|
||||
def test_return_as_str(self):
|
||||
assert Script({"array": "str: {['a', 3.14]}"}).resolve() == ScriptOutput(
|
||||
{"array": String('str: ["a", 3.14]')}
|
||||
assert Script({"arr": "str: {['a', 3.14]}"}).resolve() == ScriptOutput(
|
||||
{"arr": String('str: ["a", 3.14]')}
|
||||
)
|
||||
|
||||
def test_nested_array(self):
|
||||
assert Script(
|
||||
{"array": "{['level1', ['level2', ['level3', 'level3'], 'level2'], 'level1']}"}
|
||||
{"arr": "{['level1', ['level2', ['level3', 'level3'], 'level2'], 'level1']}"}
|
||||
).resolve() == ScriptOutput(
|
||||
{
|
||||
"array": Array(
|
||||
"arr": Array(
|
||||
[
|
||||
String("level1"),
|
||||
Array(
|
||||
|
|
@ -55,7 +55,7 @@ class TestArray:
|
|||
],
|
||||
)
|
||||
def test_empty(self, array: str):
|
||||
assert Script({"array": array}).resolve() == ScriptOutput({"array": Array([])})
|
||||
assert Script({"arr": array}).resolve() == ScriptOutput({"arr": Array([])})
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"array",
|
||||
|
|
@ -73,7 +73,7 @@ class TestArray:
|
|||
InvalidSyntaxException,
|
||||
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.ARRAY))),
|
||||
):
|
||||
Script({"array": array}).resolve()
|
||||
Script({"arr": array}).resolve()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"array",
|
||||
|
|
@ -90,7 +90,7 @@ class TestArray:
|
|||
InvalidSyntaxException,
|
||||
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.ARRAY))),
|
||||
):
|
||||
assert Script({"array": array}).resolve()
|
||||
assert Script({"arr": array}).resolve()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"array",
|
||||
|
|
@ -104,7 +104,7 @@ class TestArray:
|
|||
InvalidSyntaxException,
|
||||
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.SCRIPT))),
|
||||
):
|
||||
assert Script({"array": array}).resolve()
|
||||
assert Script({"arr": array}).resolve()
|
||||
|
||||
def test_custom_function(self):
|
||||
assert Script(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class TestFunction:
|
|||
with pytest.raises(
|
||||
IncompatibleFunctionArguments,
|
||||
match=_incompatible_arguments_match(
|
||||
expected="Map, Hashable, Optional[AnyArgument]",
|
||||
expected="Map, AnyArgument, Optional[AnyArgument]",
|
||||
recieved="%if(...)->Union[Array, Map], String",
|
||||
),
|
||||
):
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException
|
|||
|
||||
class TestMap:
|
||||
def test_return(self):
|
||||
assert Script({"map": "{{'a': 3.14}}"}).resolve() == ScriptOutput(
|
||||
{"map": Map({String("a"): Float(3.14)})}
|
||||
assert Script({"dict": "{{'a': 3.14}}"}).resolve() == ScriptOutput(
|
||||
{"dict": Map({String("a"): Float(3.14)})}
|
||||
)
|
||||
|
||||
def test_return_as_str(self):
|
||||
assert Script({"map": "json: {{'a': 3.14}}"}).resolve() == ScriptOutput(
|
||||
{"map": String('json: {"a": 3.14}')}
|
||||
assert Script({"dict": "json: {{'a': 3.14}}"}).resolve() == ScriptOutput(
|
||||
{"dict": String('json: {"a": 3.14}')}
|
||||
)
|
||||
|
||||
def test_nested_map(self):
|
||||
|
|
@ -45,9 +45,9 @@ class TestMap:
|
|||
}
|
||||
}"""
|
||||
|
||||
assert Script({"map": map_str}).resolve() == ScriptOutput(
|
||||
assert Script({"dict": map_str}).resolve() == ScriptOutput(
|
||||
{
|
||||
"map": Map(
|
||||
"dict": Map(
|
||||
{
|
||||
String("level1"): Map(
|
||||
{
|
||||
|
|
@ -78,7 +78,7 @@ class TestMap:
|
|||
],
|
||||
)
|
||||
def test_empty_map(self, empty_map: str):
|
||||
assert Script({"map": empty_map}).resolve() == ScriptOutput({"map": Map({})})
|
||||
assert Script({"dict": empty_map}).resolve() == ScriptOutput({"dict": Map({})})
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"map",
|
||||
|
|
@ -93,7 +93,7 @@ class TestMap:
|
|||
)
|
||||
def test_map_not_closed(self, map: str):
|
||||
with pytest.raises(InvalidSyntaxException, match=re.escape(str(BRACKET_NOT_CLOSED))):
|
||||
Script({"map": map}).resolve()
|
||||
Script({"dict": map}).resolve()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
|
|
@ -108,7 +108,7 @@ class TestMap:
|
|||
)
|
||||
def test_key_has_no_value(self, value: str):
|
||||
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_WITH_NO_VALUE))):
|
||||
Script({"map": value}).resolve()
|
||||
Script({"dict": value}).resolve()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
|
|
@ -124,7 +124,7 @@ class TestMap:
|
|||
InvalidSyntaxException,
|
||||
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.MAP_KEY))),
|
||||
):
|
||||
Script({"map": value}).resolve()
|
||||
Script({"dict": value}).resolve()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
|
|
@ -136,7 +136,7 @@ class TestMap:
|
|||
)
|
||||
def test_map_multiple_keys(self, value: str):
|
||||
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_MULTIPLE_VALUES))):
|
||||
Script({"map": value}).resolve()
|
||||
Script({"dict": value}).resolve()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
|
|
@ -148,7 +148,7 @@ class TestMap:
|
|||
)
|
||||
def test_map_missing_key(self, value: str):
|
||||
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_MISSING_KEY))):
|
||||
Script({"map": value}).resolve()
|
||||
Script({"dict": value}).resolve()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
|
|
@ -161,18 +161,18 @@ class TestMap:
|
|||
)
|
||||
def test_map_key_not_hashable(self, value: str):
|
||||
with pytest.raises(InvalidSyntaxException, match=re.escape(str(MAP_KEY_NOT_HASHABLE))):
|
||||
Script({"map": value}).resolve()
|
||||
Script({"dict": value}).resolve()
|
||||
|
||||
def test_map_key_is_hashable_variable(self):
|
||||
assert Script(
|
||||
{
|
||||
"map": "{{key_variable : 'value' }}",
|
||||
"dict": "{{key_variable : 'value' }}",
|
||||
"key_variable": "hashable",
|
||||
}
|
||||
).resolve() == ScriptOutput(
|
||||
{
|
||||
"key_variable": String("hashable"),
|
||||
"map": Map({String("hashable"): String("value")}),
|
||||
"dict": Map({String("hashable"): String("value")}),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ class TestMap:
|
|||
):
|
||||
Script(
|
||||
{
|
||||
"map": "{{key_variable : 'value' }}",
|
||||
"dict": "{{key_variable : 'value' }}",
|
||||
"key_variable": "{['non-hashable']}",
|
||||
}
|
||||
).resolve()
|
||||
|
|
|
|||
Loading…
Reference in a new issue