[FEATURE] Support duplicate NFO keys (#220)
This commit is contained in:
parent
6383d133dc
commit
819bddc9d0
18 changed files with 320 additions and 99 deletions
|
|
@ -28,6 +28,7 @@ from ytdl_sub.validators.string_formatter_validators import OverridesDictFormatt
|
|||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_sub.validators.validators import DictValidator
|
||||
from ytdl_sub.validators.validators import ListValidator
|
||||
from ytdl_sub.validators.validators import StringListValidator
|
||||
from ytdl_sub.validators.validators import StringValidator
|
||||
from ytdl_sub.validators.validators import Validator
|
||||
|
|
@ -244,28 +245,30 @@ class Preset(StrictDictValidator):
|
|||
|
||||
def __recursive_preset_validate(
|
||||
self,
|
||||
validator_dict: Optional[Dict[str, Validator]] = None,
|
||||
validator: Optional[Validator] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
|
||||
and resolve.
|
||||
"""
|
||||
if validator_dict is None:
|
||||
validator_dict = self._validator_dict
|
||||
if validator is None:
|
||||
validator = self
|
||||
|
||||
for validator in validator_dict.values():
|
||||
if isinstance(validator, DictValidator):
|
||||
# pylint: disable=protected-access
|
||||
# Usage of protected variables in other validators is fine. The reason to keep them
|
||||
# protected is for readability when using them in subscriptions.
|
||||
self.__recursive_preset_validate(validator._validator_dict)
|
||||
# pylint: enable=protected-access
|
||||
|
||||
if isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
|
||||
self.__validate_override_string_formatter_validator(validator)
|
||||
if isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
|
||||
for validator_value in validator.dict.values():
|
||||
self.__validate_override_string_formatter_validator(validator_value)
|
||||
if isinstance(validator, DictValidator):
|
||||
# pylint: disable=protected-access
|
||||
# Usage of protected variables in other validators is fine. The reason to keep
|
||||
# them protected is for readability when using them in subscriptions.
|
||||
for validator_value in validator._validator_dict.values():
|
||||
self.__recursive_preset_validate(validator_value)
|
||||
# pylint: enable=protected-access
|
||||
elif isinstance(validator, ListValidator):
|
||||
for list_value in validator.list:
|
||||
self.__recursive_preset_validate(list_value)
|
||||
elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
|
||||
self.__validate_override_string_formatter_validator(validator)
|
||||
elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)):
|
||||
for validator_value in validator.dict.values():
|
||||
self.__validate_override_string_formatter_validator(validator_value)
|
||||
|
||||
def __merge_parent_preset_dicts_if_present(self, config: ConfigFile):
|
||||
parent_preset_validator = self._validate_key_if_present(
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class YoutubeMergePlaylistDownloader(
|
|||
file_duration_sec=merged_video.kwargs("duration"),
|
||||
)
|
||||
|
||||
return chapters.to_file_metadata(title="Timestamps of playlist videos in the merged file:")
|
||||
return chapters.to_file_metadata(title="Timestamps of playlist videos in the merged file")
|
||||
|
||||
def _to_merged_video(self, entry_dict: Dict) -> YoutubeVideo:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ class ChaptersPlugin(Plugin[ChaptersOptions]):
|
|||
chapters = Chapters.from_timestamps_file(
|
||||
chapters_file_path=self.plugin_options.embed_chapter_timestamps
|
||||
)
|
||||
return chapters.to_file_metadata(title="Chapters embedded from timestamp file:")
|
||||
return chapters.to_file_metadata(title="Chapters embedded from timestamp file")
|
||||
|
||||
if self.plugin_options.embed_chapters:
|
||||
metadata_dict = {}
|
||||
|
|
|
|||
|
|
@ -84,4 +84,4 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
|
|||
audio_file.save()
|
||||
|
||||
# report the tags written
|
||||
return FileMetadata.from_dict(value_dict=tags_to_write, title="Music Tags:")
|
||||
return FileMetadata.from_dict(value_dict=tags_to_write, title="Music Tags")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import os
|
||||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
|
@ -95,22 +97,30 @@ class SharedNfoTagsPlugin(
|
|||
Shared code between NFO tags and Ouptut Directory NFO Tags
|
||||
"""
|
||||
|
||||
def _get_xml_element_dict(self, entry: Optional[Entry]) -> Dict[str, XmlElement]:
|
||||
nfo_tags: Dict[str, XmlElement] = {}
|
||||
def _get_xml_element_dict(self, entry: Optional[Entry]) -> Dict[str, List[XmlElement]]:
|
||||
nfo_tags: Dict[str, List[XmlElement]] = defaultdict(list)
|
||||
|
||||
for key, string_tag in self.plugin_options.tags.string_tags.items():
|
||||
nfo_tags[key] = XmlElement(
|
||||
text=self.overrides.apply_formatter(formatter=string_tag, entry=entry),
|
||||
attributes={},
|
||||
for key, string_tags in self.plugin_options.tags.string_tags.items():
|
||||
nfo_tags[key].extend(
|
||||
XmlElement(
|
||||
text=self.overrides.apply_formatter(formatter=string_tag, entry=entry),
|
||||
attributes={},
|
||||
)
|
||||
for string_tag in string_tags
|
||||
)
|
||||
|
||||
for key, attribute_tag in self.plugin_options.tags.attribute_tags.items():
|
||||
nfo_tags[key] = XmlElement(
|
||||
text=self.overrides.apply_formatter(formatter=attribute_tag.tag, entry=entry),
|
||||
attributes={
|
||||
attr_name: self.overrides.apply_formatter(formatter=attr_formatter, entry=entry)
|
||||
for attr_name, attr_formatter in attribute_tag.attributes.dict.items()
|
||||
},
|
||||
for key, attribute_tags in self.plugin_options.tags.attribute_tags.items():
|
||||
nfo_tags[key].extend(
|
||||
XmlElement(
|
||||
text=self.overrides.apply_formatter(formatter=attribute_tag.tag, entry=entry),
|
||||
attributes={
|
||||
attr_name: self.overrides.apply_formatter(
|
||||
formatter=attr_formatter, entry=entry
|
||||
)
|
||||
for attr_name, attr_formatter in attribute_tag.attributes.dict.items()
|
||||
},
|
||||
)
|
||||
for attribute_tag in attribute_tags
|
||||
)
|
||||
|
||||
return nfo_tags
|
||||
|
|
@ -125,11 +135,14 @@ class SharedNfoTagsPlugin(
|
|||
if self.plugin_options.kodi_safe:
|
||||
nfo_root = to_max_3_byte_utf8_string(nfo_root)
|
||||
nfo_tags = {
|
||||
to_max_3_byte_utf8_string(key): XmlElement(
|
||||
text=to_max_3_byte_utf8_string(xml_elem.text),
|
||||
attributes=to_max_3_byte_utf8_dict(xml_elem.attributes),
|
||||
)
|
||||
for key, xml_elem in nfo_tags.items()
|
||||
to_max_3_byte_utf8_string(key): [
|
||||
XmlElement(
|
||||
text=to_max_3_byte_utf8_string(xml_elem.text),
|
||||
attributes=to_max_3_byte_utf8_dict(xml_elem.attributes),
|
||||
)
|
||||
for xml_elem in xml_elems
|
||||
]
|
||||
for key, xml_elems in nfo_tags.items()
|
||||
}
|
||||
|
||||
xml = to_xml(nfo_dict=nfo_tags, nfo_root=nfo_root)
|
||||
|
|
@ -147,9 +160,16 @@ class SharedNfoTagsPlugin(
|
|||
# Save the nfo file and log its metadata
|
||||
nfo_metadata = FileMetadata.from_dict(
|
||||
value_dict={
|
||||
nfo_root: {key: xml_elem.to_dict_value() for key, xml_elem in nfo_tags.items()}
|
||||
nfo_root: {
|
||||
key: (
|
||||
xml_elems[0].to_dict_value()
|
||||
if len(xml_elems) == 1
|
||||
else [xml_elem.to_dict_value() for xml_elem in xml_elems]
|
||||
)
|
||||
for key, xml_elems in nfo_tags.items()
|
||||
}
|
||||
},
|
||||
title="NFO tags:",
|
||||
title="NFO tags",
|
||||
)
|
||||
self.save_file(file_name=nfo_file_name, file_metadata=nfo_metadata, entry=entry)
|
||||
|
||||
|
|
@ -210,7 +230,7 @@ class NfoTagsOptions(
|
|||
<episode>502</episode>
|
||||
</episodedetails>
|
||||
|
||||
Also supports xml attributes:
|
||||
Also supports xml attributes and duplicate keys:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -219,12 +239,17 @@ class NfoTagsOptions(
|
|||
attributes:
|
||||
name: "Best Year"
|
||||
tag: "{upload_year}"
|
||||
genre:
|
||||
- "Comedy"
|
||||
- "Drama"
|
||||
|
||||
Which translates to
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<season name="Best Year">2022</season>
|
||||
<genre>Comedy</genre>
|
||||
<genre>Drama</genre>
|
||||
"""
|
||||
return self._tags
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class OutputDirectoryNfoTagsOptions(
|
|||
<title>Sweet youtube TV show</title>
|
||||
</tvshow>
|
||||
|
||||
Also supports xml attributes:
|
||||
Also supports xml attributes and duplicate keys:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -71,12 +71,17 @@ class OutputDirectoryNfoTagsOptions(
|
|||
attributes:
|
||||
year: "2022"
|
||||
tag: "Sweet youtube TV show"
|
||||
genre:
|
||||
- "Comedy"
|
||||
- "Drama"
|
||||
|
||||
Which translates to
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<title year="2022">Sweet youtube TV show</season>
|
||||
<genre>Comedy</genre>
|
||||
<genre>Drama</genre>
|
||||
"""
|
||||
return self._tags
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
|
|||
|
||||
metadata = FileMetadata.from_dict(
|
||||
value_dict=metadata_value_dict,
|
||||
title="From Chapter Split:",
|
||||
title="From Chapter Split",
|
||||
sort_dict=False,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,4 +59,4 @@ class VideoTagsPlugin(Plugin[VideoTagsOptions]):
|
|||
)
|
||||
|
||||
# report the tags written
|
||||
return FileMetadata.from_dict(value_dict=tags_to_write, title="Video Tags:")
|
||||
return FileMetadata.from_dict(value_dict=tags_to_write, title="Video Tags")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
|
@ -56,32 +57,60 @@ class FileMetadata:
|
|||
sort_dict
|
||||
Whether to sort dicts in the value_dict. Defaults to true.
|
||||
"""
|
||||
lines: List[str] = []
|
||||
if title is not None:
|
||||
lines.append(title)
|
||||
if title:
|
||||
value_dict = {title: value_dict}
|
||||
|
||||
def _recursive_add_dict_lines(rdict: Dict, indent: int):
|
||||
rdict_items = rdict.items()
|
||||
if sort_dict:
|
||||
rdict_items = sorted(rdict_items)
|
||||
if sort_dict:
|
||||
value_dict = json.loads(json.dumps(value_dict, sort_keys=True, ensure_ascii=False))
|
||||
|
||||
for key, value in rdict_items:
|
||||
_indent = " " * indent
|
||||
if isinstance(value, Dict):
|
||||
lines.append(f"{_indent}{key}:")
|
||||
_recursive_add_dict_lines(rdict=value, indent=indent + 2)
|
||||
else:
|
||||
value = str(value)
|
||||
# If there are newlines in the value, print them indented
|
||||
if "\n" in value:
|
||||
lines.append(f"{_indent}{key}:")
|
||||
for value_line in value.split("\n"):
|
||||
lines.append(f" {_indent}{value_line.strip()}")
|
||||
def _indent_lines(value: str, indent: int) -> str:
|
||||
if "\n" not in value:
|
||||
return value
|
||||
|
||||
output_str = ""
|
||||
_indent = " " * indent
|
||||
for line in value.split("\n"):
|
||||
output_str += f"{_indent}{line}\n"
|
||||
return f"{output_str.rstrip()}\n"
|
||||
|
||||
def _single_value(value: Any) -> Optional[str]:
|
||||
if isinstance(value, list) and len(value) == 1:
|
||||
return _single_value(value=value[0])
|
||||
if isinstance(value, (dict, list)):
|
||||
return None
|
||||
if isinstance(value, str) and "\n" in value:
|
||||
return None
|
||||
return value
|
||||
|
||||
def _recursive_lines(value: Any, indent: int = 0) -> str:
|
||||
_indent = " " * indent
|
||||
|
||||
output_str = ""
|
||||
if isinstance(value, dict):
|
||||
for key, sub_value in value.items():
|
||||
single_sub_value = _single_value(sub_value)
|
||||
if single_sub_value is not None:
|
||||
output_str += f"{_indent}{key}: {single_sub_value}\n"
|
||||
else:
|
||||
lines.append(f"{_indent}{key}: {value}")
|
||||
output_str += f"{_indent}{key}:\n"
|
||||
output_str += _indent_lines(_recursive_lines(sub_value), indent=indent + 2)
|
||||
|
||||
_recursive_add_dict_lines(rdict=value_dict, indent=2)
|
||||
return cls(metadata=lines)
|
||||
elif isinstance(value, list):
|
||||
for sub_value in value:
|
||||
single_sub_value = _single_value(sub_value)
|
||||
if single_sub_value is not None:
|
||||
output_str += f"{_indent}- {single_sub_value}\n"
|
||||
else:
|
||||
output_str += f"{_indent}- \n"
|
||||
output_str += _indent_lines(_recursive_lines(sub_value), indent=indent + 2)
|
||||
elif isinstance(value, str): # multi-line string
|
||||
output_str += _indent_lines(value, indent=indent)
|
||||
else:
|
||||
assert False, "should never reach here"
|
||||
return output_str
|
||||
|
||||
out = _recursive_lines(value_dict).rstrip().split("\n")
|
||||
return cls(metadata=out)
|
||||
|
||||
|
||||
class FileHandlerTransactionLog:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import xml.etree.ElementTree as et
|
|||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Union
|
||||
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ def to_max_3_byte_utf8_dict(string_dict: Dict[str, str]) -> Dict[str, str]:
|
|||
}
|
||||
|
||||
|
||||
def to_xml(nfo_dict: Dict[str, XmlElement], nfo_root: str) -> bytes:
|
||||
def to_xml(nfo_dict: Dict[str, List[XmlElement]], nfo_root: str) -> bytes:
|
||||
"""
|
||||
Transforms a dict to XML
|
||||
|
||||
|
|
@ -75,10 +76,11 @@ def to_xml(nfo_dict: Dict[str, XmlElement], nfo_root: str) -> bytes:
|
|||
XML bytes
|
||||
"""
|
||||
xml_root = et.Element(nfo_root)
|
||||
for key, xml_elem in sorted(nfo_dict.items()):
|
||||
sorted_attr = dict(sorted(xml_elem.attributes.items()))
|
||||
sub_element = et.SubElement(xml_root, key, sorted_attr)
|
||||
sub_element.text = xml_elem.text
|
||||
for key, xml_elems in sorted(nfo_dict.items()):
|
||||
for xml_elem in xml_elems:
|
||||
sorted_attr = dict(sorted(xml_elem.attributes.items()))
|
||||
sub_element = et.SubElement(xml_root, key, sorted_attr)
|
||||
sub_element.text = xml_elem.text
|
||||
|
||||
et.indent(tree=xml_root, space=" ", level=0)
|
||||
return et.tostring(element=xml_root, encoding="utf-8", xml_declaration=True)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import List
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import ListOverridesFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesDictFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_sub.validators.validators import DictValidator
|
||||
from ytdl_sub.validators.validators import ListValidator
|
||||
|
||||
TStringFormatterValidator = TypeVar("TStringFormatterValidator", bound=StringFormatterValidator)
|
||||
TDictFormatterValidator = TypeVar("TDictFormatterValidator", bound=DictFormatterValidator)
|
||||
|
|
@ -53,50 +58,80 @@ class _NfoTagsWithAttributesValidator(
|
|||
class NfoTagsWithAttributesValidator(
|
||||
_NfoTagsWithAttributesValidator[StringFormatterValidator, DictFormatterValidator]
|
||||
):
|
||||
"""TagsWithAttributes for the entry NFO validator"""
|
||||
|
||||
formatter_validator = StringFormatterValidator
|
||||
dict_formatter_validator = DictFormatterValidator
|
||||
|
||||
|
||||
class NfoTagsWithAttributesListValidator(ListValidator[NfoTagsWithAttributesValidator]):
|
||||
"""TagsWithAttributes list for the entry NFO validator"""
|
||||
|
||||
_inner_list_type = NfoTagsWithAttributesValidator
|
||||
|
||||
|
||||
class NfoOverrideTagsWithAttributesValidator(
|
||||
_NfoTagsWithAttributesValidator[
|
||||
OverridesStringFormatterValidator, OverridesDictFormatterValidator
|
||||
]
|
||||
):
|
||||
"""TagsWithAttributes for the output directory NFO validator"""
|
||||
|
||||
formatter_validator = OverridesStringFormatterValidator
|
||||
dict_formatter_validator = OverridesDictFormatterValidator
|
||||
|
||||
|
||||
class NfoOverrideTagsWithAttributesListValidator(
|
||||
ListValidator[NfoOverrideTagsWithAttributesValidator]
|
||||
):
|
||||
"""TagsWithAttributes list for the output directory NFO validator"""
|
||||
|
||||
_inner_list_type = NfoOverrideTagsWithAttributesValidator
|
||||
|
||||
|
||||
# Generic TagsWithAttribute to use for SharedNfoTagsValidator
|
||||
TNfoTagsWithAttributesValidator = _NfoTagsWithAttributesValidator[
|
||||
TStringFormatterValidator, TDictFormatterValidator
|
||||
]
|
||||
|
||||
# List validators
|
||||
TNfoTagsWithAttributesListValidator = ListValidator[TNfoTagsWithAttributesValidator]
|
||||
TNfoTagsListValidator = ListValidator[TStringFormatterValidator]
|
||||
|
||||
class SharedNfoTagsValidator(
|
||||
DictValidator, Generic[TStringFormatterValidator, TDictFormatterValidator], ABC
|
||||
):
|
||||
|
||||
_tags_with_attributes_validator: Type[TNfoTagsWithAttributesValidator]
|
||||
class SharedNfoTagsValidator(DictValidator, ABC):
|
||||
_tags_validator: Type[TNfoTagsListValidator]
|
||||
_tags_with_attributes_validator: Type[TNfoTagsWithAttributesListValidator]
|
||||
|
||||
def __init__(self, name, value):
|
||||
super().__init__(name, value)
|
||||
|
||||
self._string_tags: Dict[str, StringFormatterValidator] = {}
|
||||
self._attribute_tags: Dict[str, TNfoTagsWithAttributesValidator] = {}
|
||||
self._string_tags: Dict[str, List[TStringFormatterValidator]] = defaultdict(list)
|
||||
self._attribute_tags: Dict[str, List[TNfoTagsWithAttributesValidator]] = defaultdict(list)
|
||||
|
||||
for key, tag_value in self._dict.items():
|
||||
if isinstance(tag_value, str):
|
||||
self._string_tags[key] = self._validate_key(
|
||||
key=key, validator=self._tags_with_attributes_validator.formatter_validator
|
||||
# Turn each value into a list if it's not
|
||||
if not isinstance(tag_value, list):
|
||||
tag_value = [tag_value]
|
||||
|
||||
if isinstance(tag_value[0], str):
|
||||
self._string_tags[key].extend(
|
||||
self._validate_key(
|
||||
key=key,
|
||||
validator=self._tags_validator,
|
||||
).list
|
||||
)
|
||||
elif isinstance(tag_value, dict):
|
||||
self._attribute_tags[key] = self._validate_key(
|
||||
key=key, validator=self._tags_with_attributes_validator
|
||||
elif isinstance(tag_value[0], dict):
|
||||
self._attribute_tags[key].extend(
|
||||
self._validate_key(key=key, validator=self._tags_with_attributes_validator).list
|
||||
)
|
||||
else:
|
||||
raise self._validation_exception("must either be a string or attributes object")
|
||||
raise self._validation_exception(
|
||||
"must either be a single or list of string/attribute object"
|
||||
)
|
||||
|
||||
@property
|
||||
def string_tags(self) -> Dict[str, StringFormatterValidator]:
|
||||
def string_tags(self) -> Dict[str, List[TStringFormatterValidator]]:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -105,7 +140,7 @@ class SharedNfoTagsValidator(
|
|||
return self._string_tags
|
||||
|
||||
@property
|
||||
def attribute_tags(self) -> Dict[str, TNfoTagsWithAttributesValidator]:
|
||||
def attribute_tags(self) -> Dict[str, List[TNfoTagsWithAttributesValidator]]:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -114,11 +149,11 @@ class SharedNfoTagsValidator(
|
|||
return self._attribute_tags
|
||||
|
||||
|
||||
class NfoTagsValidator(SharedNfoTagsValidator[StringFormatterValidator, DictFormatterValidator]):
|
||||
_tags_with_attributes_validator = NfoTagsWithAttributesValidator
|
||||
class NfoTagsValidator(SharedNfoTagsValidator):
|
||||
_tags_validator = ListFormatterValidator
|
||||
_tags_with_attributes_validator = NfoTagsWithAttributesListValidator
|
||||
|
||||
|
||||
class NfoOverrideTagsValidator(
|
||||
SharedNfoTagsValidator[OverridesStringFormatterValidator, OverridesDictFormatterValidator]
|
||||
):
|
||||
_tags_with_attributes_validator = NfoOverrideTagsWithAttributesValidator
|
||||
class NfoOverrideTagsValidator(SharedNfoTagsValidator):
|
||||
_tags_validator = ListOverridesFormatterValidator
|
||||
_tags_with_attributes_validator = NfoOverrideTagsWithAttributesListValidator
|
||||
|
|
|
|||
|
|
@ -209,6 +209,10 @@ class ListFormatterValidator(ListValidator[StringFormatterValidator]):
|
|||
_inner_list_type = StringFormatterValidator
|
||||
|
||||
|
||||
class ListOverridesFormatterValidator(ListValidator[StringFormatterValidator]):
|
||||
_inner_list_type = OverridesStringFormatterValidator
|
||||
|
||||
|
||||
class DictFormatterValidator(LiteralDictValidator):
|
||||
"""
|
||||
A dict made up of
|
||||
|
|
|
|||
|
|
@ -23,6 +23,17 @@ def subscription_dict(output_directory):
|
|||
"attributes": {"🎸?": "value\nnewlines 🎸"},
|
||||
"tag": "the \n tag 🎸🎸",
|
||||
},
|
||||
"kodi_safe_multi_title 🎸": ["value 1 🎸", "value 2 🎸"],
|
||||
"kodi_safe_multi_title_with_attrs": [
|
||||
{
|
||||
"attributes": {"🎸?": "value\nnewlines 🎸"},
|
||||
"tag": "the \n tag 1 🎸🎸",
|
||||
},
|
||||
{
|
||||
"attributes": {"🎸?": "value\nnewlines 🎸"},
|
||||
"tag": "the \n tag 2 🎸🎸",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"output_directory_nfo_tags": {
|
||||
|
|
@ -34,6 +45,17 @@ def subscription_dict(output_directory):
|
|||
"attributes": {"🎸?": "value\nnewlines 🎸"},
|
||||
"tag": "the \n tag 🎸🎸",
|
||||
},
|
||||
"kodi_safe_multi_title 🎸": ["value 1 🎸", "value 2 🎸"],
|
||||
"kodi_safe_multi_title_with_attrs": [
|
||||
{
|
||||
"attributes": {"🎸?": "value\nnewlines 🎸"},
|
||||
"tag": "the \n tag 1 🎸🎸",
|
||||
},
|
||||
{
|
||||
"attributes": {"🎸?": "value\nnewlines 🎸"},
|
||||
"tag": "the \n tag 2 🎸🎸",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -74,3 +96,17 @@ class TestNfoTagsPlugins:
|
|||
preset_name="kodi_safe_xml",
|
||||
preset_dict=subscription_dict,
|
||||
)
|
||||
|
||||
def test_source_variable_in_output_directory_nfo_tags_list_errors(
|
||||
self, subscription_dict, music_video_config
|
||||
):
|
||||
subscription_dict["output_directory_nfo_tags"]["tags"]["kodi_safe_title_with_attrs"] = [
|
||||
"okay value",
|
||||
"not okay {title}",
|
||||
]
|
||||
with pytest.raises(ValidationException):
|
||||
Subscription.from_dict(
|
||||
config=music_video_config,
|
||||
preset_name="kodi_safe_xml",
|
||||
preset_dict=subscription_dict,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,26 @@ Rick Beato - Can you hear the difference? 🎸🔥 #shorts.nfo
|
|||
musicvideo:
|
||||
album: Music Videos
|
||||
artist: Rick Beato
|
||||
kodi_safe_multi_title 🎸:
|
||||
- value 1 🎸
|
||||
- value 2 🎸
|
||||
kodi_safe_multi_title_with_attrs:
|
||||
-
|
||||
attributes:
|
||||
🎸?:
|
||||
value
|
||||
newlines 🎸
|
||||
tag:
|
||||
the
|
||||
tag 1 🎸🎸
|
||||
-
|
||||
attributes:
|
||||
🎸?:
|
||||
value
|
||||
newlines 🎸
|
||||
tag:
|
||||
the
|
||||
tag 2 🎸🎸
|
||||
kodi_safe_title 🎸: kodi_safe_value 🎸
|
||||
kodi_safe_title_with_attrs:
|
||||
attributes:
|
||||
|
|
@ -16,12 +36,32 @@ Rick Beato - Can you hear the difference? 🎸🔥 #shorts.nfo
|
|||
newlines 🎸
|
||||
tag:
|
||||
the
|
||||
tag 🎸🎸
|
||||
tag 🎸🎸
|
||||
title: Can you hear the difference? 🎸🔥 #shorts
|
||||
year: 2022
|
||||
test.nfo
|
||||
NFO tags:
|
||||
kodi_safe_root 🎸:
|
||||
kodi_safe_multi_title 🎸:
|
||||
- value 1 🎸
|
||||
- value 2 🎸
|
||||
kodi_safe_multi_title_with_attrs:
|
||||
-
|
||||
attributes:
|
||||
🎸?:
|
||||
value
|
||||
newlines 🎸
|
||||
tag:
|
||||
the
|
||||
tag 1 🎸🎸
|
||||
-
|
||||
attributes:
|
||||
🎸?:
|
||||
value
|
||||
newlines 🎸
|
||||
tag:
|
||||
the
|
||||
tag 2 🎸🎸
|
||||
kodi_safe_title 🎸: kodi_safe_value 🎸
|
||||
kodi_safe_title_with_attrs:
|
||||
attributes:
|
||||
|
|
@ -30,4 +70,4 @@ test.nfo
|
|||
newlines 🎸
|
||||
tag:
|
||||
the
|
||||
tag 🎸🎸
|
||||
tag 🎸🎸
|
||||
|
|
@ -8,6 +8,26 @@ Rick Beato - Can you hear the difference? 🎸🔥 #shorts.nfo
|
|||
musicvideo:
|
||||
album: Music Videos
|
||||
artist: Rick Beato
|
||||
kodi_safe_multi_title □:
|
||||
- value 1 □
|
||||
- value 2 □
|
||||
kodi_safe_multi_title_with_attrs:
|
||||
-
|
||||
attributes:
|
||||
□?:
|
||||
value
|
||||
newlines □
|
||||
tag:
|
||||
the
|
||||
tag 1 □□
|
||||
-
|
||||
attributes:
|
||||
□?:
|
||||
value
|
||||
newlines □
|
||||
tag:
|
||||
the
|
||||
tag 2 □□
|
||||
kodi_safe_title □: kodi_safe_value □
|
||||
kodi_safe_title_with_attrs:
|
||||
attributes:
|
||||
|
|
@ -16,12 +36,32 @@ Rick Beato - Can you hear the difference? 🎸🔥 #shorts.nfo
|
|||
newlines □
|
||||
tag:
|
||||
the
|
||||
tag □□
|
||||
tag □□
|
||||
title: Can you hear the difference? □□ #shorts
|
||||
year: 2022
|
||||
test.nfo
|
||||
NFO tags:
|
||||
kodi_safe_root □:
|
||||
kodi_safe_multi_title □:
|
||||
- value 1 □
|
||||
- value 2 □
|
||||
kodi_safe_multi_title_with_attrs:
|
||||
-
|
||||
attributes:
|
||||
□?:
|
||||
value
|
||||
newlines □
|
||||
tag:
|
||||
the
|
||||
tag 1 □□
|
||||
-
|
||||
attributes:
|
||||
□?:
|
||||
value
|
||||
newlines □
|
||||
tag:
|
||||
the
|
||||
tag 2 □□
|
||||
kodi_safe_title □: kodi_safe_value □
|
||||
kodi_safe_title_with_attrs:
|
||||
attributes:
|
||||
|
|
@ -30,4 +70,4 @@ test.nfo
|
|||
newlines □
|
||||
tag:
|
||||
the
|
||||
tag □□
|
||||
tag □□
|
||||
|
|
@ -14,7 +14,7 @@ JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4
|
|||
Video Tags:
|
||||
description:
|
||||
🎸 / ' "
|
||||
newline?
|
||||
newline?
|
||||
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.nfo
|
||||
NFO tags:
|
||||
musicvideo:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Files created in '{output_directory}'
|
|||
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case-thumb.jpg
|
||||
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.info.json
|
||||
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4
|
||||
Embedded Chapters
|
||||
Embedded Chapters:
|
||||
Removed Chapter(s): Intro, Outro
|
||||
Removed SponsorBlock Category Count(s):
|
||||
Sponsor: 2
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ class TestPreset:
|
|||
|
||||
nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions)
|
||||
tags_string_dict = {
|
||||
key: formatter.format_string for key, formatter in nfo_options.tags.string_tags.items()
|
||||
key: formatter[0].format_string
|
||||
for key, formatter in nfo_options.tags.string_tags.items()
|
||||
}
|
||||
|
||||
assert tags_string_dict == {"key-1": "preset_0", "key-2": "this-preset"}
|
||||
|
|
@ -126,7 +127,8 @@ class TestPreset:
|
|||
|
||||
nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions)
|
||||
tags_string_dict = {
|
||||
key: formatter.format_string for key, formatter in nfo_options.tags.string_tags.items()
|
||||
key: formatter[0].format_string
|
||||
for key, formatter in nfo_options.tags.string_tags.items()
|
||||
}
|
||||
|
||||
assert tags_string_dict == {
|
||||
|
|
|
|||
Loading…
Reference in a new issue