no more entry script, maybe bad idea???

This commit is contained in:
Jesse Bannon 2023-12-10 08:07:46 -08:00
parent a4920c03c3
commit 8cd6ed12e0
16 changed files with 289 additions and 296 deletions

View file

@ -1,22 +1,29 @@
import copy import copy
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import Optional
from typing import Set from typing import Set
from typing import Type
from typing import TypeVar
from typing import final
from yt_dlp.utils import sanitize_filename from yt_dlp.utils import sanitize_filename
from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import Variable
from ytdl_sub.entries.script.variable_scripts import UNRESOLVED_VARIABLES
from ytdl_sub.entries.script.variable_scripts import VARIABLE_SCRIPTS
from ytdl_sub.entries.variables.override_variables import SUBSCRIPTION_NAME from ytdl_sub.entries.variables.override_variables import SUBSCRIPTION_NAME
from ytdl_sub.script.parser import parse from ytdl_sub.script.parser import parse
from ytdl_sub.script.script import Script from ytdl_sub.script.script import Script
from ytdl_sub.utils.scriptable import Scriptable from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
TType = TypeVar("TType")
class Overrides(DictFormatterValidator, Scriptable):
class Overrides(DictFormatterValidator):
""" """
Optional. This section allows you to define variables that can be used in any string formatter. Optional. This section allows you to define variables that can be used in any string formatter.
For example, if you want your file and thumbnail files to match without copy-pasting a large For example, if you want your file and thumbnail files to match without copy-pasting a large
@ -62,8 +69,7 @@ class Overrides(DictFormatterValidator, Scriptable):
) )
def __init__(self, name, value): def __init__(self, name, value):
DictFormatterValidator.__init__(self, name, value) super().__init__(name, value)
Scriptable.__init__(self)
# Add sanitized overrides # Add sanitized overrides
for key in self._keys: for key in self._keys:
@ -81,14 +87,53 @@ class Overrides(DictFormatterValidator, Scriptable):
sanitize=sanitized, sanitize=sanitized,
) )
self.unresolvable.add(VARIABLES.entry_metadata.variable_name) self.script = Script(copy.deepcopy(VARIABLE_SCRIPTS))
self.unresolvable: Set[str] = copy.deepcopy(UNRESOLVED_VARIABLES)
def initialize_script(self, unresolved_variables: Dict[str, str]) -> None:
self.script.add(dict(self.dict_with_format_strings, **unresolved_variables))
self.unresolvable.update(set(unresolved_variables.keys()))
def initialize_script(self, unresolved_variables: Set[str]) -> None:
self.unresolvable |= unresolved_variables
self.script.add(
dict(
self.dict_with_format_strings,
**{
unresolved: f"{{%throw('Variable {unresolved} has not been resolved yet')}}"
for unresolved in self.unresolvable
},
)
)
self.update_script() self.update_script()
def add_entry_kwargs(self, entry_kwargs: Dict[str, Any]) -> "Overrides":
self.unresolvable.remove(VARIABLES.entry_metadata.variable_name)
self.script.add(
{VARIABLES.entry_metadata.variable_name: ScriptUtils.to_script(entry_kwargs)}
)
self.update_script()
return self
def add(self, values: Dict[str, Any]) -> None:
self.unresolvable -= set(list(values.keys()))
self.script.add(
ScriptUtils.add_sanitized_variables(
{name: ScriptUtils.to_script(value) for name, value in values.items()}
),
unresolvable=self.unresolvable,
)
self.update_script()
def update_script(self) -> None:
self.script.resolve(unresolvable=self.unresolvable, update=True)
def get(self, variable: Variable | str, expected_type: Type[TType]) -> TType:
out = self.script.resolve(unresolvable=self.unresolvable).get_native(variable.variable_name)
return expected_type(out)
def get_str(self, variable: Variable) -> str:
return self.get(variable, str)
def get_int(self, variable: Variable) -> int:
return self.get(variable, int)
@property @property
def subscription_name(self) -> str: def subscription_name(self) -> str:
""" """
@ -98,10 +143,18 @@ class Overrides(DictFormatterValidator, Scriptable):
""" """
return self._root_name return self._root_name
@final
def to_dict(self) -> Dict[str, str]:
"""
Returns
-------
Dictionary containing all variables
"""
return self.script.resolve().as_native()
def apply_formatter( def apply_formatter(
self, self,
formatter: StringFormatterValidator, formatter: StringFormatterValidator,
entry: Optional[Entry] = None,
function_overrides: Dict[str, str] = None, function_overrides: Dict[str, str] = None,
) -> str: ) -> str:
""" """
@ -118,17 +171,15 @@ class Overrides(DictFormatterValidator, Scriptable):
------- -------
The format_string after .format has been called The format_string after .format has been called
""" """
script: Script = self.script
unresolvable: Set[str] = self.unresolvable
if entry:
script = entry.script
unresolvable = entry.unresolvable
return formatter.post_process( return formatter.post_process(
str( str(
script.resolve_once( self.script.resolve_once(
dict({"tmp_var": formatter.format_string}, **(function_overrides or {})), dict({"tmp_var": formatter.format_string}, **(function_overrides or {})),
unresolvable=unresolvable, unresolvable=self.unresolvable.union(
VARIABLES.entry_metadata.variable_name
if isinstance(formatter, OverridesStringFormatterValidator)
else set()
),
)["tmp_var"] )["tmp_var"]
) )
) )

View file

@ -361,12 +361,7 @@ class Preset(_PresetShell):
# values from multiple validators # values from multiple validators
self.__recursive_preset_validate() self.__recursive_preset_validate()
self.overrides.initialize_script( self.overrides.initialize_script(unresolved_variables=set(list(self._added_variables.keys())))
unresolved_variables={
var_name: f"{{%throw('Plugin variable {var_name} has not been created yet')}}"
for var_name in self._added_variables
}
)
@property @property
def name(self) -> str: def name(self) -> str:

View file

@ -109,17 +109,17 @@ class InfoJsonDownloader(SourcePlugin[InfoJsonDownloaderOptions]):
prior_variables = entry.kwargs(YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY) prior_variables = entry.kwargs(YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY)
del entry._kwargs[YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY] del entry._kwargs[YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY]
entry.initialize_script( self.overrides.add(
override_variables=self.overrides.dict_with_format_strings, dict(
unresolvable=self.overrides.unresolvable, entry._kwargs,
).add( **{
{
inj.variable_name: prior_variables.get( inj.variable_name: prior_variables.get(
inj.variable_name, inj.variable_name,
VARIABLE_SCRIPTS[inj.variable_name], VARIABLE_SCRIPTS[inj.variable_name],
) )
for inj in ENTRY_INJECTED_VARIABLES for inj in ENTRY_INJECTED_VARIABLES
} },
)
) )
entries.append(entry) entries.append(entry)

View file

@ -71,7 +71,7 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
Downloads and moves channel avatar and banner images to the output directory. Downloads and moves channel avatar and banner images to the output directory.
""" """
for thumbnail_info in thumbnail_list_info.list: for thumbnail_info in thumbnail_list_info.list:
thumbnail_name = self.overrides.apply_formatter(thumbnail_info.name, entry=entry) thumbnail_name = self.overrides.apply_formatter(thumbnail_info.name)
thumbnail_id = self.overrides.apply_formatter(thumbnail_info.uid) thumbnail_id = self.overrides.apply_formatter(thumbnail_info.uid)
# If the thumbnail name is an empty string, completely ignore trying to download it # If the thumbnail name is an empty string, completely ignore trying to download it
@ -176,7 +176,7 @@ class UrlDownloaderCollectionVariablePlugin(SourcePluginExtension):
""" """
# COLLECTION_URL is a recent variable that may not exist for old entries when updating. # COLLECTION_URL is a recent variable that may not exist for old entries when updating.
# Try to use source_webpage_url if it does not exist # Try to use source_webpage_url if it does not exist
entry_collection_url = entry.get_str(v.ytdl_sub_input_url) entry_collection_url = self.overrides.get_str(v.ytdl_sub_input_url)
# If the collection URL cannot find its mapping, use the last URL # If the collection URL cannot find its mapping, use the last URL
collection_url = ( collection_url = (
@ -459,15 +459,16 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
for entry in self._iterate_entries( for entry in self._iterate_entries(
url_validator=collection_url, parents=parents, orphans=orphan_entries url_validator=collection_url, parents=parents, orphans=orphan_entries
): ):
entry.initialize_script(
override_variables=self.overrides.dict_with_format_strings, self.overrides.add(
unresolvable=self.overrides.unresolvable, dict(
).add( entry._kwargs,
{ **{
v.ytdl_sub_input_url.variable_name: self.overrides.apply_formatter( v.ytdl_sub_input_url.variable_name: self.overrides.apply_formatter(
collection_url.url collection_url.url
) )
} },
)
) )
yield entry yield entry

View file

@ -2,17 +2,13 @@ import copy
import json import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Dict
from typing import Optional from typing import Optional
from typing import Set
from typing import Type
from typing import TypeVar from typing import TypeVar
from typing import final from typing import final
from ytdl_sub.config.overrides import Overrides
from ytdl_sub.entries.base_entry import BaseEntry from ytdl_sub.entries.base_entry import BaseEntry
from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import Variable
from ytdl_sub.utils.scriptable import Scriptable
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
from ytdl_sub.validators.audo_codec_validator import VIDEO_CODEC_EXTS from ytdl_sub.validators.audo_codec_validator import VIDEO_CODEC_EXTS
@ -21,49 +17,11 @@ YTDL_SUB_ENTRY_VARIABLES_KWARG_KEY: str = "ytdl_sub_entry_variables"
TType = TypeVar("TType") TType = TypeVar("TType")
class Entry(BaseEntry, Scriptable): class Entry(BaseEntry):
""" """
Entry object to represent a single media object returned from yt-dlp. Entry object to represent a single media object returned from yt-dlp.
""" """
def __init__(self, entry_dict: Dict, working_directory: str):
BaseEntry.__init__(self, entry_dict=entry_dict, working_directory=working_directory)
Scriptable.__init__(self)
def initialize_script(
self, override_variables: Dict[str, str], unresolvable: Set[str]
) -> "Entry":
# TODO: CLEAN THIS SHIT UP
# Overrides contains added variables that are unresolvable, add them here
self.unresolvable |= unresolvable
# Remove the entry variable
self.unresolvable.remove(VARIABLES.entry_metadata.variable_name)
# Add entry metadata, but avoid the `.add()` helper since it also adds sanitized
self.script.add({VARIABLES.entry_metadata.variable_name: f"{{{json.dumps(self._kwargs)}}}"})
self.script.add(
{
unresolved: f"{{%throw('Variable {unresolved} has not been resolved yet')}}"
for unresolved in self.unresolvable
}
)
# use .add here to get sanitized
self.add(override_variables)
self.update_script()
return self
def get(self, variable: Variable, expected_type: Type[TType]) -> TType:
out = self.script.resolve(unresolvable=self.unresolvable).get_native(variable.variable_name)
return expected_type(out)
def get_str(self, variable: Variable) -> str:
return self.get(variable, str)
def get_int(self, variable: Variable) -> int:
return self.get(variable, int)
@property @property
def ext(self) -> str: def ext(self) -> str:
""" """
@ -71,7 +29,7 @@ class Entry(BaseEntry, Scriptable):
This is not reflected in the entry. See if the mkv file exists and return "mkv" if so, This is not reflected in the entry. See if the mkv file exists and return "mkv" if so,
otherwise, return the original extension. otherwise, return the original extension.
""" """
ext = self.get_str(VARIABLES.ext) ext = self.kwargs(VARIABLES.ext.metadata_key)
for possible_ext in [ext, "mkv"]: for possible_ext in [ext, "mkv"]:
file_path = str(Path(self.working_directory()) / f"{self.uid}.{possible_ext}") file_path = str(Path(self.working_directory()) / f"{self.uid}.{possible_ext}")
if os.path.isfile(file_path): if os.path.isfile(file_path):
@ -97,7 +55,7 @@ class Entry(BaseEntry, Scriptable):
------- -------
The download thumbnail's file name The download thumbnail's file name
""" """
return f"{self.get_str(VARIABLES.uid)}.{self.get_str(VARIABLES.thumbnail_ext)}" return f"{self.kwargs(VARIABLES.uid.metadata_key)}.jpg"
def get_download_thumbnail_path(self) -> str: def get_download_thumbnail_path(self) -> str:
"""Returns the entry's thumbnail's file path to where it was downloaded""" """Returns the entry's thumbnail's file path to where it was downloaded"""
@ -121,12 +79,12 @@ class Entry(BaseEntry, Scriptable):
return None return None
def write_info_json(self) -> None: def write_info_json(self, overrides: Overrides) -> None:
""" """
Write the entry's _kwargs back into the info.json file as well as its source variables Write the entry's _kwargs back into the info.json file as well as its source variables
""" """
kwargs_dict = copy.deepcopy(self._kwargs) kwargs_dict = copy.deepcopy(self._kwargs)
kwargs_dict["ytdl_sub_entry_variables"] = self.to_dict() kwargs_dict["ytdl_sub_entry_variables"] = overrides.to_dict()
kwargs_json = json.dumps(kwargs_dict, ensure_ascii=False, sort_keys=True, indent=2) kwargs_json = json.dumps(kwargs_dict, ensure_ascii=False, sort_keys=True, indent=2)
with open(self.get_download_info_json_path(), "w", encoding="utf-8") as file: with open(self.get_download_info_json_path(), "w", encoding="utf-8") as file:
@ -168,12 +126,3 @@ class Entry(BaseEntry, Scriptable):
break break
return file_exists return file_exists
@final
def to_dict(self) -> Dict[str, str]:
"""
Returns
-------
Dictionary containing all variables
"""
return self.script.resolve().as_native()

View file

@ -118,7 +118,7 @@ ENTRY_HARDCODED_VARIABLES: Dict[Variable, str] = {
ENTRY_RELATIVE_VARIABLES: Dict[MetadataVariable, str] = { ENTRY_RELATIVE_VARIABLES: Dict[MetadataVariable, str] = {
v.playlist_metadata: entry_get(v.playlist_metadata, {}), v.playlist_metadata: entry_get(v.playlist_metadata, {}),
v.source_metadata: entry_get(v.source_metadata, {}), v.source_metadata: entry_get(v.source_metadata, {}),
v.sibling_entry_metadata: entry_get(v.sibling_entry_metadata, "{ [] }"), v.sibling_entry_metadata: entry_get(v.sibling_entry_metadata, []),
} }
ENTRY_REQUIRED_VARIABLES: Dict[MetadataVariable, str] = { ENTRY_REQUIRED_VARIABLES: Dict[MetadataVariable, str] = {
@ -279,6 +279,11 @@ mergedeep.merge(
VARIABLE_SCRIPTS: Dict[str, str] = { VARIABLE_SCRIPTS: Dict[str, str] = {
var.variable_name: script for var, script in _VARIABLE_SCRIPTS.items() var.variable_name: script for var, script in _VARIABLE_SCRIPTS.items()
} }
UNRESOLVED_VARIABLES: Set[str] = {var.variable_name for var in ENTRY_INJECTED_VARIABLES}
UNRESOLVED_VARIABLES: Set[str] = {
var.variable_name
for var in list(ENTRY_EMPTY_METADATA.keys())
+ list(ENTRY_INJECTED_VARIABLES.keys())
+ list(ENTRY_RELATIVE_VARIABLES.keys())
}
CustomFunctions.register() CustomFunctions.register()

View file

@ -58,7 +58,7 @@ class ViewPlugin(Plugin[ViewOptions]):
""" """
Adds all source variables to the entry Adds all source variables to the entry
""" """
source_var_dict = copy.deepcopy(entry.to_dict()) source_var_dict = self.overrides.to_dict()
for key in source_var_dict.keys(): for key in source_var_dict.keys():
source_var_dict[key] = self._truncate_value(source_var_dict[key]) source_var_dict[key] = self._truncate_value(source_var_dict[key])

View file

@ -157,7 +157,7 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
tags_to_write: Dict[str, List[str]] = defaultdict(list) tags_to_write: Dict[str, List[str]] = defaultdict(list)
for tag_name, tag_formatters in self.plugin_options.tags.as_lists.items(): for tag_name, tag_formatters in self.plugin_options.tags.as_lists.items():
for tag_formatter in tag_formatters: for tag_formatter in tag_formatters:
tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) tag_value = self.overrides.apply_formatter(formatter=tag_formatter)
tags_to_write[tag_name].append(tag_value) tags_to_write[tag_name].append(tag_value)
# write the actual tags if its not a dry run # write the actual tags if its not a dry run

View file

@ -99,7 +99,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC):
for key, string_tags in self.plugin_options.tags.string_tags.items(): for key, string_tags in self.plugin_options.tags.string_tags.items():
tags = [ tags = [
XmlElement( XmlElement(
text=self.overrides.apply_formatter(formatter=string_tag, entry=entry), text=self.overrides.apply_formatter(formatter=string_tag),
attributes={}, attributes={},
) )
for string_tag in string_tags for string_tag in string_tags
@ -110,11 +110,9 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC):
for key, attribute_tags in self.plugin_options.tags.attribute_tags.items(): for key, attribute_tags in self.plugin_options.tags.attribute_tags.items():
tags = [ tags = [
XmlElement( XmlElement(
text=self.overrides.apply_formatter(formatter=attribute_tag.tag, entry=entry), text=self.overrides.apply_formatter(formatter=attribute_tag.tag),
attributes={ attributes={
attr_name: self.overrides.apply_formatter( attr_name: self.overrides.apply_formatter(formatter=attr_formatter)
formatter=attr_formatter, entry=entry
)
for attr_name, attr_formatter in attribute_tag.attributes.dict.items() for attr_name, attr_formatter in attribute_tag.attributes.dict.items()
}, },
) )
@ -128,9 +126,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC):
def _create_nfo(self, entry: Entry, save_to_entry: bool = True) -> None: def _create_nfo(self, entry: Entry, save_to_entry: bool = True) -> None:
# Write the nfo tags to XML with the nfo_root # Write the nfo tags to XML with the nfo_root
nfo_root = self.overrides.apply_formatter( nfo_root = self.overrides.apply_formatter(formatter=self.plugin_options.nfo_root)
formatter=self.plugin_options.nfo_root, entry=entry
)
nfo_tags = self._get_xml_element_dict(entry=entry) nfo_tags = self._get_xml_element_dict(entry=entry)
# If the nfo tags are empty, then stop continuing # If the nfo tags are empty, then stop continuing
@ -152,9 +148,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC):
xml = to_xml(nfo_dict=nfo_tags, nfo_root=nfo_root) xml = to_xml(nfo_dict=nfo_tags, nfo_root=nfo_root)
nfo_file_name = self.overrides.apply_formatter( nfo_file_name = self.overrides.apply_formatter(formatter=self.plugin_options.nfo_name)
formatter=self.plugin_options.nfo_name, entry=entry
)
# Save the nfo's XML to file # Save the nfo's XML to file
nfo_file_path = Path(self.working_directory) / nfo_file_name nfo_file_path = Path(self.working_directory) / nfo_file_name

View file

@ -288,34 +288,16 @@ class RegexPlugin(Plugin[RegexOptions]):
# Otherwise, error # Otherwise, error
raise RegexNoMatchException(f"Regex failed to match '{variable_name}' from '{entry.title}'") raise RegexNoMatchException(f"Regex failed to match '{variable_name}' from '{entry.title}'")
def _can_process_at_metadata_stage(self, entry: Entry, variable_name: str) -> bool: def _can_process_at_metadata_stage(self, variable_name: str) -> bool:
# If the variable is an override...
if variable_name in self.overrides.dict:
# Try to see if it can resolve # Try to see if it can resolve
try: try:
self.overrides.apply_formatter( self.overrides.apply_formatter(formatter=self.overrides.dict[variable_name])
formatter=self.overrides.dict[variable_name],
entry=entry,
)
# If it can not from missing variables (from post-metadata stage), return False # If it can not from missing variables (from post-metadata stage), return False
except StringFormattingVariableNotFoundException: except StringFormattingVariableNotFoundException:
return False return False
# If it is a source variable and not present, return false
elif variable_name not in entry.to_dict():
return False
return True return True
def _get_regex_input_string(self, entry: Entry, variable_name: str) -> str:
# Apply override formatter if it's an override
if variable_name in self.overrides.dict:
return self.overrides.apply_formatter(
formatter=self.overrides.dict[variable_name],
entry=entry,
)
# Otherwise pluck from the entry's source variable
return entry.to_dict()[variable_name]
def _modify_entry_metadata(self, entry: Entry, is_metadata_stage: bool) -> Optional[Entry]: def _modify_entry_metadata(self, entry: Entry, is_metadata_stage: bool) -> Optional[Entry]:
""" """
Parameters Parameters
@ -348,16 +330,13 @@ class RegexPlugin(Plugin[RegexOptions]):
# If it's the metadata stage, and it can't be processed, skip until post-metadata # If it's the metadata stage, and it can't be processed, skip until post-metadata
if is_metadata_stage and not self._can_process_at_metadata_stage( if is_metadata_stage and not self._can_process_at_metadata_stage(
entry=entry, variable_name=variable_name variable_name=variable_name
): ):
continue continue
self._add_processed_regex_variable_name(entry, variable_name) self._add_processed_regex_variable_name(entry, variable_name)
regex_input_str = self._get_regex_input_string( regex_input_str = self.overrides.get_str(variable_name)
entry=entry,
variable_name=variable_name,
)
if ( if (
regex_options.exclude is not None regex_options.exclude is not None
@ -375,49 +354,22 @@ class RegexPlugin(Plugin[RegexOptions]):
if not regex_options.has_defaults: if not regex_options.has_defaults:
return self._try_skip_entry(entry=entry, variable_name=variable_name) return self._try_skip_entry(entry=entry, variable_name=variable_name)
# otherwise, use defaults (apply them using the original entry source dict) # otherwise, use defaults
source_variables_and_overrides_dict = dict( self.overrides.add(
entry.to_dict(), **self.overrides.dict_with_format_strings {
regex_options.capture_group_names[i]: default
for i, default in enumerate(regex_options.capture_group_defaults)
}
) )
# add both the default...
entry.add_variables(
variables_to_add={
regex_options.capture_group_names[i]: default.apply_formatter(
variable_dict=source_variables_and_overrides_dict
)
for i, default in enumerate(regex_options.capture_group_defaults)
},
)
# and sanitized default
entry.add_variables(
variables_to_add={
f"{regex_options.capture_group_names[i]}_sanitized": sanitize_filename(
default.apply_formatter(
variable_dict=source_variables_and_overrides_dict
)
)
for i, default in enumerate(regex_options.capture_group_defaults)
},
)
# There is a capture, add the source variables to the entry as # There is a capture, add the source variables to the entry as
# {source_var}_capture_1, {source_var}_capture_2, ... # {source_var}_capture_1, {source_var}_capture_2, ...
else: else:
# Add the value... self.overrides.add(
entry.add_variables( {
variables_to_add={
regex_options.capture_group_names[i]: capture regex_options.capture_group_names[i]: capture
for i, capture in enumerate(maybe_capture) for i, capture in enumerate(maybe_capture)
}, }
)
# And the sanitized value
entry.add_variables(
variables_to_add={
f"{regex_options.capture_group_names[i]}_sanitized": sanitize_filename(
capture
)
for i, capture in enumerate(maybe_capture)
},
) )
return entry return entry

View file

@ -196,7 +196,6 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
subtitle_file_name = f"{entry.uid}.{lang}.{self.plugin_options.subtitles_type}" subtitle_file_name = f"{entry.uid}.{lang}.{self.plugin_options.subtitles_type}"
output_subtitle_file_name = self.overrides.apply_formatter( output_subtitle_file_name = self.overrides.apply_formatter(
formatter=self.plugin_options.subtitles_name, formatter=self.plugin_options.subtitles_name,
entry=entry,
function_overrides={"lang": lang}, function_overrides={"lang": lang},
) )

View file

@ -76,7 +76,7 @@ class VideoTagsPlugin(Plugin[VideoTagsOptions]):
tags_to_write: Dict[str, str] = {} tags_to_write: Dict[str, str] = {}
for tag_name, tag_formatter in self.plugin_options.tags.dict.items(): for tag_name, tag_formatter in self.plugin_options.tags.dict.items():
tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) tag_value = self.overrides.apply_formatter(formatter=tag_formatter)
tags_to_write[tag_name] = tag_value tags_to_write[tag_name] = tag_value
# write the actual tags if its not a dry run # write the actual tags if its not a dry run

View file

@ -62,9 +62,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
Optional. Metadata to record to the transaction log for this entry Optional. Metadata to record to the transaction log for this entry
""" """
# Move the file after all direct file modifications are complete # Move the file after all direct file modifications are complete
output_file_name = self.overrides.apply_formatter( output_file_name = self.overrides.apply_formatter(formatter=self.output_options.file_name)
formatter=self.output_options.file_name, entry=entry
)
self._enhanced_download_archive.save_file_to_output_directory( self._enhanced_download_archive.save_file_to_output_directory(
file_name=entry.get_download_file_name(), file_name=entry.get_download_file_name(),
file_metadata=entry_metadata, file_metadata=entry_metadata,
@ -75,7 +73,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
# Always pretend to include the thumbnail in a dry-run # Always pretend to include the thumbnail in a dry-run
if self.output_options.thumbnail_name and (dry_run or entry.is_thumbnail_downloaded()): if self.output_options.thumbnail_name and (dry_run or entry.is_thumbnail_downloaded()):
output_thumbnail_name = self.overrides.apply_formatter( output_thumbnail_name = self.overrides.apply_formatter(
formatter=self.output_options.thumbnail_name, entry=entry formatter=self.output_options.thumbnail_name
) )
# Copy the thumbnails since they could be used later for other things # Copy the thumbnails since they could be used later for other things
@ -92,7 +90,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
if self.output_options.info_json_name: if self.output_options.info_json_name:
output_info_json_name = self.overrides.apply_formatter( output_info_json_name = self.overrides.apply_formatter(
formatter=self.output_options.info_json_name, entry=entry formatter=self.output_options.info_json_name
) )
# if not dry-run, write the info json # if not dry-run, write the info json

View file

@ -36,6 +36,8 @@ class ScriptUtils:
out = f"{{%float({value})}}" out = f"{{%float({value})}}"
elif isinstance(value, bool): elif isinstance(value, bool):
out = f"{{%bool({value})}}" out = f"{{%bool({value})}}"
elif isinstance(value, dict):
out = f"{{ {json.dumps(value)} }}"
else: else:
out = json.dumps(value) out = json.dumps(value)

View file

@ -50,112 +50,128 @@ def download_file_name(uid, ext):
@pytest.fixture @pytest.fixture
def mock_entry_to_dict( def mock_entry_to_dict():
uid,
title,
ext,
extractor,
upload_date,
thumbnail_ext,
webpage_url,
):
return { return {
"uid": uid, "channel": "abc123",
"uid_sanitized": uid, "channel_id": "abc123",
"channel_sanitized": "abc123",
"comments": "",
"comments_sanitized": "",
"creator": "abc123",
"creator_sanitized": "abc123",
"description": "",
"download_index": 1,
"download_index_padded6": "000001",
"download_index_sanitized": "1",
"entry_metadata": {
"epoch": 1596878400,
"ext": "mp5",
"extractor": "xtract",
"extractor_key": "test_extractor_key",
"id": "abc123",
"thumbnail": "abc123.jpg",
"title": "entry {title}",
"upload_date": "20210112",
"webpage_url": "https://yourname.here",
},
"epoch": 1596878400, "epoch": 1596878400,
"epoch_date": "20200808", "epoch_date": "20200808",
"epoch_hour": "09", "epoch_hour": "09",
"title": "entry title", "ext": "mp5",
"title_sanitized": "entry title", "extractor": "xtract",
"ext": ext,
"description": "",
"comments": "",
"requested_subtitles": "",
"sponsorblock_chapters": "",
"creator": "abc123",
"creator_sanitized": "abc123",
"channel": "abc123",
"channel_sanitized": "abc123",
"channel_id": uid,
"extractor": extractor,
"extractor_key": "test_extractor_key", "extractor_key": "test_extractor_key",
"uploader": "abc123",
"uploader_id": "abc123",
"uploader_url": "https://yourname.here",
"download_index": 1,
"download_index_padded6": "000001",
"upload_date_index": 1,
"upload_date_index_padded": "01",
"upload_date_index_reversed": 99,
"upload_date_index_reversed_padded": "99",
"upload_date": upload_date,
"upload_date_standardized": "2021-01-12",
"upload_year": 2021,
"upload_year_truncated": 21,
"upload_year_truncated_reversed": 79,
"upload_month": 1,
"upload_month_padded": "01",
"upload_month_reversed": 12,
"upload_month_reversed_padded": "12",
"upload_day": 12,
"upload_day_padded": "12",
"upload_day_reversed": 20,
"upload_day_reversed_padded": "20",
"upload_day_of_year": 12,
"upload_day_of_year_padded": "012",
"upload_day_of_year_reversed": 354,
"upload_day_of_year_reversed_padded": "354",
"thumbnail_ext": thumbnail_ext,
"info_json_ext": "info.json", "info_json_ext": "info.json",
"webpage_url": webpage_url, "playlist_count": 1,
"playlist_description": "",
"playlist_index": 1, "playlist_index": 1,
"playlist_index_padded": "01", "playlist_index_padded": "01",
"playlist_index_padded6": "000001", "playlist_index_padded6": "000001",
"playlist_index_reversed": 1, "playlist_index_reversed": 1,
"playlist_index_reversed_padded": "01", "playlist_index_reversed_padded": "01",
"playlist_index_reversed_padded6": "000001", "playlist_index_reversed_padded6": "000001",
"playlist_count": 1, "playlist_metadata": {},
"playlist_max_upload_year": 2021, "playlist_metadata_sanitized": "{}",
"playlist_max_upload_year_truncated": 21,
"playlist_title": "entry title", "playlist_title": "entry title",
"playlist_title_sanitized": "entry title", "playlist_title_sanitized": "entry title",
"playlist_description": "",
"playlist_webpage_url": "https://yourname.here",
"playlist_uid": "abc123", "playlist_uid": "abc123",
"playlist_uploader": "abc123", "playlist_uploader": "abc123",
"playlist_uploader_sanitized": "abc123",
"playlist_uploader_id": "abc123", "playlist_uploader_id": "abc123",
"playlist_uploader_sanitized": "abc123",
"playlist_uploader_url": "https://yourname.here", "playlist_uploader_url": "https://yourname.here",
"source_count": 1, "playlist_webpage_url": "https://yourname.here",
"source_description": "", "release_date": "20210112",
"source_index": 1,
"source_index_padded": "01",
"source_title": "entry title",
"source_title_sanitized": "entry title",
"source_webpage_url": "https://yourname.here",
"source_uid": "abc123",
"source_uploader": "abc123",
"source_uploader_id": "abc123",
"source_uploader_url": "https://yourname.here",
"uid_sanitized_plex": "abc",
"title_sanitized_plex": "entry title",
"release_date": upload_date,
"release_date_standardized": "2021-01-12", "release_date_standardized": "2021-01-12",
"release_year": 2021,
"release_year_truncated": 21,
"release_year_truncated_reversed": 79,
"release_month": 1,
"release_month_padded": "01",
"release_month_reversed": 12,
"release_month_reversed_padded": "12",
"release_day": 12, "release_day": 12,
"release_day_padded": "12",
"release_day_reversed": 20,
"release_day_reversed_padded": "20",
"release_day_of_year": 12, "release_day_of_year": 12,
"release_day_of_year_padded": "012", "release_day_of_year_padded": "012",
"release_day_of_year_reversed": 354, "release_day_of_year_reversed": 354,
"release_day_of_year_reversed_padded": "354", "release_day_of_year_reversed_padded": "354",
"release_day_padded": "12",
"release_day_reversed": 20,
"release_day_reversed_padded": "20",
"release_month": 1,
"release_month_padded": "01",
"release_month_reversed": 12,
"release_month_reversed_padded": "12",
"release_year": 2021,
"release_year_truncated": 21,
"release_year_truncated_reversed": 79,
"requested_subtitles": "",
"requested_subtitles_sanitized": "",
"sibling_entry_metadata": [],
"sibling_entry_metadata_sanitized": "[]",
"source_count": 1,
"source_description": "",
"source_index": 1,
"source_index_padded": "01",
"source_metadata": {},
"source_metadata_sanitized": "{}",
"source_title": "entry title",
"source_title_sanitized": "entry title",
"source_uid": "abc123",
"source_uploader": "abc123",
"source_uploader_id": "abc123",
"source_uploader_url": "https://yourname.here",
"source_webpage_url": "https://yourname.here",
"sponsorblock_chapters": "",
"sponsorblock_chapters_sanitized": "",
"subscription_name": "test",
"subscription_name_sanitized": "test",
"thumbnail_ext": "jpg",
"title": "entry title",
"title_sanitized": "entry title",
"title_sanitized_plex": "entry title",
"uid": "abc123",
"uid_sanitized": "abc123",
"uid_sanitized_plex": "abc",
"upload_date": "20210112",
"upload_date_index": 1,
"upload_date_index_padded": "01",
"upload_date_index_reversed": 99,
"upload_date_index_reversed_padded": "99",
"upload_date_index_sanitized": "1",
"upload_date_standardized": "2021-01-12",
"upload_day": 12,
"upload_day_of_year": 12,
"upload_day_of_year_padded": "012",
"upload_day_of_year_reversed": 354,
"upload_day_of_year_reversed_padded": "354",
"upload_day_padded": "12",
"upload_day_reversed": 20,
"upload_day_reversed_padded": "20",
"upload_month": 1,
"upload_month_padded": "01",
"upload_month_reversed": 12,
"upload_month_reversed_padded": "12",
"upload_year": 2021,
"upload_year_truncated": 21,
"upload_year_truncated_reversed": 79,
"uploader": "abc123",
"uploader_id": "abc123",
"uploader_url": "https://yourname.here",
"webpage_url": "https://yourname.here",
"ytdl_sub_input_url": "https://yourname.here",
"ytdl_sub_input_url_sanitized": "httpsyourname.here",
} }
@ -178,7 +194,4 @@ def mock_entry_kwargs(
@pytest.fixture @pytest.fixture
def mock_entry(mock_entry_kwargs): def mock_entry(mock_entry_kwargs):
return Entry(entry_dict=mock_entry_kwargs, working_directory=".").initialize_script( return Entry(entry_dict=mock_entry_kwargs, working_directory=".")
override_variables={},
unresolvable=set(),
)

View file

@ -1,13 +1,39 @@
from typing import Callable
import pytest import pytest
from ytdl_sub.config.overrides import Overrides
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLES as v from ytdl_sub.entries.script.variable_definitions import VARIABLES as v
from ytdl_sub.entries.script.variable_scripts import ENTRY_INJECTED_VARIABLES
from ytdl_sub.entries.script.variable_scripts import ENTRY_RELATIVE_VARIABLES
from ytdl_sub.entries.script.variable_scripts import UNRESOLVED_VARIABLES
@pytest.fixture
def mock_overrides_factory() -> Callable[[Entry], Overrides]:
def _mock_overrides_factory(entry: Entry) -> Overrides:
overrides = Overrides(name="test", value={})
overrides.initialize_script(unresolved_variables=set())
overrides.add(
{
var.variable_name: format_string
for var, format_string in (
list(ENTRY_INJECTED_VARIABLES.items()) + list(ENTRY_RELATIVE_VARIABLES.items())
)
}
)
overrides.add_entry_kwargs(entry._kwargs)
return overrides
return _mock_overrides_factory
class TestEntry(object): class TestEntry(object):
def test_entry_to_dict(self, mock_entry, mock_entry_to_dict): def test_entry_to_dict(self, mock_overrides_factory, mock_entry, mock_entry_to_dict):
out = mock_entry.to_dict() out = mock_overrides_factory(mock_entry).to_dict()
del out["entry_metadata"]
assert out == mock_entry_to_dict assert out == mock_entry_to_dict
def test_entry_missing_kwarg(self, mock_entry): def test_entry_missing_kwarg(self, mock_entry):
@ -27,6 +53,7 @@ class TestEntry(object):
) )
def test_entry_reverse_variables( def test_entry_reverse_variables(
self, self,
mock_overrides_factory,
mock_entry_kwargs, mock_entry_kwargs,
upload_date, upload_date,
year_rev, year_rev,
@ -35,16 +62,16 @@ class TestEntry(object):
month_rev_pad, month_rev_pad,
day_rev_pad, day_rev_pad,
): ):
mock_entry_kwargs["upload_date"] = upload_date mock_entry_kwargs["upload_date"] = upload_date
entry = Entry(entry_dict=mock_entry_kwargs, working_directory=".").initialize_script( overrides = mock_overrides_factory(
override_variables={}, unresolvable=set() Entry(entry_dict=mock_entry_kwargs, working_directory=".")
) )
assert entry.get_int(v.upload_year_truncated_reversed) == year_rev
assert entry.get_int(v.upload_month_reversed) == month_rev assert overrides.get_int(v.upload_year_truncated_reversed) == year_rev
assert entry.get_int(v.upload_day_reversed) == day_rev assert overrides.get_int(v.upload_month_reversed) == month_rev
assert entry.get_str(v.upload_month_reversed_padded) == month_rev_pad assert overrides.get_int(v.upload_day_reversed) == day_rev
assert entry.get_str(v.upload_day_reversed_padded) == day_rev_pad assert overrides.get_str(v.upload_month_reversed_padded) == month_rev_pad
assert overrides.get_str(v.upload_day_reversed_padded) == day_rev_pad
@pytest.mark.parametrize( @pytest.mark.parametrize(
"upload_date, day_year, day_year_rev, day_year_pad, day_year_rev_pad", "upload_date, day_year, day_year_rev, day_year_pad, day_year_rev_pad",
@ -54,14 +81,21 @@ class TestEntry(object):
], ],
) )
def test_entry_upload_day_of_year_variables( def test_entry_upload_day_of_year_variables(
self, mock_entry_kwargs, upload_date, day_year, day_year_rev, day_year_pad, day_year_rev_pad self,
mock_overrides_factory,
mock_entry_kwargs,
upload_date,
day_year,
day_year_rev,
day_year_pad,
day_year_rev_pad,
): ):
mock_entry_kwargs["upload_date"] = upload_date mock_entry_kwargs["upload_date"] = upload_date
entry = Entry(entry_dict=mock_entry_kwargs, working_directory=".").initialize_script( overrides = mock_overrides_factory(
override_variables={}, unresolvable=set() Entry(entry_dict=mock_entry_kwargs, working_directory=".")
) )
assert entry.get_int(v.upload_day_of_year) == day_year assert overrides.get_int(v.upload_day_of_year) == day_year
assert entry.get_int(v.upload_day_of_year_reversed) == day_year_rev assert overrides.get_int(v.upload_day_of_year_reversed) == day_year_rev
assert entry.get_str(v.upload_day_of_year_padded) == day_year_pad assert overrides.get_str(v.upload_day_of_year_padded) == day_year_pad
assert entry.get_str(v.upload_day_of_year_reversed_padded) == day_year_rev_pad assert overrides.get_str(v.upload_day_of_year_reversed_padded) == day_year_rev_pad