[FEATURE] Support duplicate NFO keys (#220)

This commit is contained in:
Jesse Bannon 2022-09-07 14:06:13 -07:00 committed by GitHub
parent 6383d133dc
commit 819bddc9d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 320 additions and 99 deletions

View file

@ -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 OverridesStringFormatterValidator
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
from ytdl_sub.validators.validators import DictValidator 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 StringListValidator
from ytdl_sub.validators.validators import StringValidator from ytdl_sub.validators.validators import StringValidator
from ytdl_sub.validators.validators import Validator from ytdl_sub.validators.validators import Validator
@ -244,28 +245,30 @@ class Preset(StrictDictValidator):
def __recursive_preset_validate( def __recursive_preset_validate(
self, self,
validator_dict: Optional[Dict[str, Validator]] = None, validator: Optional[Validator] = None,
) -> None: ) -> None:
""" """
Ensure all OverridesStringFormatterValidator's only contain variables from the overrides Ensure all OverridesStringFormatterValidator's only contain variables from the overrides
and resolve. and resolve.
""" """
if validator_dict is None: if validator is None:
validator_dict = self._validator_dict validator = self
for validator in validator_dict.values(): if isinstance(validator, DictValidator):
if isinstance(validator, DictValidator): # pylint: disable=protected-access
# pylint: disable=protected-access # Usage of protected variables in other validators is fine. The reason to keep
# Usage of protected variables in other validators is fine. The reason to keep them # them protected is for readability when using them in subscriptions.
# protected is for readability when using them in subscriptions. for validator_value in validator._validator_dict.values():
self.__recursive_preset_validate(validator._validator_dict) self.__recursive_preset_validate(validator_value)
# pylint: enable=protected-access # pylint: enable=protected-access
elif isinstance(validator, ListValidator):
if isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)): for list_value in validator.list:
self.__validate_override_string_formatter_validator(validator) self.__recursive_preset_validate(list_value)
if isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)): elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)):
for validator_value in validator.dict.values(): self.__validate_override_string_formatter_validator(validator)
self.__validate_override_string_formatter_validator(validator_value) 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): def __merge_parent_preset_dicts_if_present(self, config: ConfigFile):
parent_preset_validator = self._validate_key_if_present( parent_preset_validator = self._validate_key_if_present(

View file

@ -122,7 +122,7 @@ class YoutubeMergePlaylistDownloader(
file_duration_sec=merged_video.kwargs("duration"), 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: def _to_merged_video(self, entry_dict: Dict) -> YoutubeVideo:
""" """

View file

@ -334,7 +334,7 @@ class ChaptersPlugin(Plugin[ChaptersOptions]):
chapters = Chapters.from_timestamps_file( chapters = Chapters.from_timestamps_file(
chapters_file_path=self.plugin_options.embed_chapter_timestamps 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: if self.plugin_options.embed_chapters:
metadata_dict = {} metadata_dict = {}

View file

@ -84,4 +84,4 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
audio_file.save() audio_file.save()
# report the tags written # 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")

View file

@ -1,8 +1,10 @@
import os import os
from abc import ABC from abc import ABC
from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from typing import Generic from typing import Generic
from typing import List
from typing import Optional from typing import Optional
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
@ -95,22 +97,30 @@ class SharedNfoTagsPlugin(
Shared code between NFO tags and Ouptut Directory NFO Tags Shared code between NFO tags and Ouptut Directory NFO Tags
""" """
def _get_xml_element_dict(self, entry: Optional[Entry]) -> Dict[str, XmlElement]: def _get_xml_element_dict(self, entry: Optional[Entry]) -> Dict[str, List[XmlElement]]:
nfo_tags: Dict[str, XmlElement] = {} nfo_tags: Dict[str, List[XmlElement]] = defaultdict(list)
for key, string_tag in self.plugin_options.tags.string_tags.items(): for key, string_tags in self.plugin_options.tags.string_tags.items():
nfo_tags[key] = XmlElement( nfo_tags[key].extend(
text=self.overrides.apply_formatter(formatter=string_tag, entry=entry), XmlElement(
attributes={}, 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(): for key, attribute_tags in self.plugin_options.tags.attribute_tags.items():
nfo_tags[key] = XmlElement( nfo_tags[key].extend(
text=self.overrides.apply_formatter(formatter=attribute_tag.tag, entry=entry), XmlElement(
attributes={ text=self.overrides.apply_formatter(formatter=attribute_tag.tag, entry=entry),
attr_name: self.overrides.apply_formatter(formatter=attr_formatter, entry=entry) attributes={
for attr_name, attr_formatter in attribute_tag.attributes.dict.items() 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 return nfo_tags
@ -125,11 +135,14 @@ class SharedNfoTagsPlugin(
if self.plugin_options.kodi_safe: if self.plugin_options.kodi_safe:
nfo_root = to_max_3_byte_utf8_string(nfo_root) nfo_root = to_max_3_byte_utf8_string(nfo_root)
nfo_tags = { nfo_tags = {
to_max_3_byte_utf8_string(key): XmlElement( to_max_3_byte_utf8_string(key): [
text=to_max_3_byte_utf8_string(xml_elem.text), XmlElement(
attributes=to_max_3_byte_utf8_dict(xml_elem.attributes), 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() )
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) 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 # Save the nfo file and log its metadata
nfo_metadata = FileMetadata.from_dict( nfo_metadata = FileMetadata.from_dict(
value_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) self.save_file(file_name=nfo_file_name, file_metadata=nfo_metadata, entry=entry)
@ -210,7 +230,7 @@ class NfoTagsOptions(
<episode>502</episode> <episode>502</episode>
</episodedetails> </episodedetails>
Also supports xml attributes: Also supports xml attributes and duplicate keys:
.. code-block:: yaml .. code-block:: yaml
@ -219,12 +239,17 @@ class NfoTagsOptions(
attributes: attributes:
name: "Best Year" name: "Best Year"
tag: "{upload_year}" tag: "{upload_year}"
genre:
- "Comedy"
- "Drama"
Which translates to Which translates to
.. code-block:: xml .. code-block:: xml
<season name="Best Year">2022</season> <season name="Best Year">2022</season>
<genre>Comedy</genre>
<genre>Drama</genre>
""" """
return self._tags return self._tags

View file

@ -62,7 +62,7 @@ class OutputDirectoryNfoTagsOptions(
<title>Sweet youtube TV show</title> <title>Sweet youtube TV show</title>
</tvshow> </tvshow>
Also supports xml attributes: Also supports xml attributes and duplicate keys:
.. code-block:: yaml .. code-block:: yaml
@ -71,12 +71,17 @@ class OutputDirectoryNfoTagsOptions(
attributes: attributes:
year: "2022" year: "2022"
tag: "Sweet youtube TV show" tag: "Sweet youtube TV show"
genre:
- "Comedy"
- "Drama"
Which translates to Which translates to
.. code-block:: xml .. code-block:: xml
<title year="2022">Sweet youtube TV show</season> <title year="2022">Sweet youtube TV show</season>
<genre>Comedy</genre>
<genre>Drama</genre>
""" """
return self._tags return self._tags

View file

@ -136,7 +136,7 @@ class SplitByChaptersPlugin(Plugin[SplitByChaptersOptions]):
metadata = FileMetadata.from_dict( metadata = FileMetadata.from_dict(
value_dict=metadata_value_dict, value_dict=metadata_value_dict,
title="From Chapter Split:", title="From Chapter Split",
sort_dict=False, sort_dict=False,
) )

View file

@ -59,4 +59,4 @@ class VideoTagsPlugin(Plugin[VideoTagsOptions]):
) )
# report the tags written # 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")

View file

@ -1,3 +1,4 @@
import json
import os import os
import shutil import shutil
from pathlib import Path from pathlib import Path
@ -56,32 +57,60 @@ class FileMetadata:
sort_dict sort_dict
Whether to sort dicts in the value_dict. Defaults to true. Whether to sort dicts in the value_dict. Defaults to true.
""" """
lines: List[str] = [] if title:
if title is not None: value_dict = {title: value_dict}
lines.append(title)
def _recursive_add_dict_lines(rdict: Dict, indent: int): if sort_dict:
rdict_items = rdict.items() value_dict = json.loads(json.dumps(value_dict, sort_keys=True, ensure_ascii=False))
if sort_dict:
rdict_items = sorted(rdict_items)
for key, value in rdict_items: def _indent_lines(value: str, indent: int) -> str:
_indent = " " * indent if "\n" not in value:
if isinstance(value, Dict): return value
lines.append(f"{_indent}{key}:")
_recursive_add_dict_lines(rdict=value, indent=indent + 2) output_str = ""
else: _indent = " " * indent
value = str(value) for line in value.split("\n"):
# If there are newlines in the value, print them indented output_str += f"{_indent}{line}\n"
if "\n" in value: return f"{output_str.rstrip()}\n"
lines.append(f"{_indent}{key}:")
for value_line in value.split("\n"): def _single_value(value: Any) -> Optional[str]:
lines.append(f" {_indent}{value_line.strip()}") 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: 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) elif isinstance(value, list):
return cls(metadata=lines) 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: class FileHandlerTransactionLog:

View file

@ -2,6 +2,7 @@ import xml.etree.ElementTree as et
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List
from typing import Union 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 Transforms a dict to XML
@ -75,10 +76,11 @@ def to_xml(nfo_dict: Dict[str, XmlElement], nfo_root: str) -> bytes:
XML bytes XML bytes
""" """
xml_root = et.Element(nfo_root) xml_root = et.Element(nfo_root)
for key, xml_elem in sorted(nfo_dict.items()): for key, xml_elems in sorted(nfo_dict.items()):
sorted_attr = dict(sorted(xml_elem.attributes.items())) for xml_elem in xml_elems:
sub_element = et.SubElement(xml_root, key, sorted_attr) sorted_attr = dict(sorted(xml_elem.attributes.items()))
sub_element.text = xml_elem.text sub_element = et.SubElement(xml_root, key, sorted_attr)
sub_element.text = xml_elem.text
et.indent(tree=xml_root, space=" ", level=0) et.indent(tree=xml_root, space=" ", level=0)
return et.tostring(element=xml_root, encoding="utf-8", xml_declaration=True) return et.tostring(element=xml_root, encoding="utf-8", xml_declaration=True)

View file

@ -1,15 +1,20 @@
from abc import ABC from abc import ABC
from collections import defaultdict
from typing import Dict from typing import Dict
from typing import Generic from typing import Generic
from typing import List
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator 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 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 OverridesDictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator 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
from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import DictValidator
from ytdl_sub.validators.validators import ListValidator
TStringFormatterValidator = TypeVar("TStringFormatterValidator", bound=StringFormatterValidator) TStringFormatterValidator = TypeVar("TStringFormatterValidator", bound=StringFormatterValidator)
TDictFormatterValidator = TypeVar("TDictFormatterValidator", bound=DictFormatterValidator) TDictFormatterValidator = TypeVar("TDictFormatterValidator", bound=DictFormatterValidator)
@ -53,50 +58,80 @@ class _NfoTagsWithAttributesValidator(
class NfoTagsWithAttributesValidator( class NfoTagsWithAttributesValidator(
_NfoTagsWithAttributesValidator[StringFormatterValidator, DictFormatterValidator] _NfoTagsWithAttributesValidator[StringFormatterValidator, DictFormatterValidator]
): ):
"""TagsWithAttributes for the entry NFO validator"""
formatter_validator = StringFormatterValidator formatter_validator = StringFormatterValidator
dict_formatter_validator = DictFormatterValidator dict_formatter_validator = DictFormatterValidator
class NfoTagsWithAttributesListValidator(ListValidator[NfoTagsWithAttributesValidator]):
"""TagsWithAttributes list for the entry NFO validator"""
_inner_list_type = NfoTagsWithAttributesValidator
class NfoOverrideTagsWithAttributesValidator( class NfoOverrideTagsWithAttributesValidator(
_NfoTagsWithAttributesValidator[ _NfoTagsWithAttributesValidator[
OverridesStringFormatterValidator, OverridesDictFormatterValidator OverridesStringFormatterValidator, OverridesDictFormatterValidator
] ]
): ):
"""TagsWithAttributes for the output directory NFO validator"""
formatter_validator = OverridesStringFormatterValidator formatter_validator = OverridesStringFormatterValidator
dict_formatter_validator = OverridesDictFormatterValidator 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[ TNfoTagsWithAttributesValidator = _NfoTagsWithAttributesValidator[
TStringFormatterValidator, TDictFormatterValidator 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): def __init__(self, name, value):
super().__init__(name, value) super().__init__(name, value)
self._string_tags: Dict[str, StringFormatterValidator] = {} self._string_tags: Dict[str, List[TStringFormatterValidator]] = defaultdict(list)
self._attribute_tags: Dict[str, TNfoTagsWithAttributesValidator] = {} self._attribute_tags: Dict[str, List[TNfoTagsWithAttributesValidator]] = defaultdict(list)
for key, tag_value in self._dict.items(): for key, tag_value in self._dict.items():
if isinstance(tag_value, str): # Turn each value into a list if it's not
self._string_tags[key] = self._validate_key( if not isinstance(tag_value, list):
key=key, validator=self._tags_with_attributes_validator.formatter_validator 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): elif isinstance(tag_value[0], dict):
self._attribute_tags[key] = self._validate_key( self._attribute_tags[key].extend(
key=key, validator=self._tags_with_attributes_validator self._validate_key(key=key, validator=self._tags_with_attributes_validator).list
) )
else: 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 @property
def string_tags(self) -> Dict[str, StringFormatterValidator]: def string_tags(self) -> Dict[str, List[TStringFormatterValidator]]:
""" """
Returns Returns
------- -------
@ -105,7 +140,7 @@ class SharedNfoTagsValidator(
return self._string_tags return self._string_tags
@property @property
def attribute_tags(self) -> Dict[str, TNfoTagsWithAttributesValidator]: def attribute_tags(self) -> Dict[str, List[TNfoTagsWithAttributesValidator]]:
""" """
Returns Returns
------- -------
@ -114,11 +149,11 @@ class SharedNfoTagsValidator(
return self._attribute_tags return self._attribute_tags
class NfoTagsValidator(SharedNfoTagsValidator[StringFormatterValidator, DictFormatterValidator]): class NfoTagsValidator(SharedNfoTagsValidator):
_tags_with_attributes_validator = NfoTagsWithAttributesValidator _tags_validator = ListFormatterValidator
_tags_with_attributes_validator = NfoTagsWithAttributesListValidator
class NfoOverrideTagsValidator( class NfoOverrideTagsValidator(SharedNfoTagsValidator):
SharedNfoTagsValidator[OverridesStringFormatterValidator, OverridesDictFormatterValidator] _tags_validator = ListOverridesFormatterValidator
): _tags_with_attributes_validator = NfoOverrideTagsWithAttributesListValidator
_tags_with_attributes_validator = NfoOverrideTagsWithAttributesValidator

View file

@ -209,6 +209,10 @@ class ListFormatterValidator(ListValidator[StringFormatterValidator]):
_inner_list_type = StringFormatterValidator _inner_list_type = StringFormatterValidator
class ListOverridesFormatterValidator(ListValidator[StringFormatterValidator]):
_inner_list_type = OverridesStringFormatterValidator
class DictFormatterValidator(LiteralDictValidator): class DictFormatterValidator(LiteralDictValidator):
""" """
A dict made up of A dict made up of

View file

@ -23,6 +23,17 @@ def subscription_dict(output_directory):
"attributes": {"🎸?": "value\nnewlines 🎸"}, "attributes": {"🎸?": "value\nnewlines 🎸"},
"tag": "the \n tag 🎸🎸", "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": { "output_directory_nfo_tags": {
@ -34,6 +45,17 @@ def subscription_dict(output_directory):
"attributes": {"🎸?": "value\nnewlines 🎸"}, "attributes": {"🎸?": "value\nnewlines 🎸"},
"tag": "the \n tag 🎸🎸", "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_name="kodi_safe_xml",
preset_dict=subscription_dict, 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,
)

View file

@ -8,6 +8,26 @@ Rick Beato - Can you hear the difference 🎸🔥 #shorts.nfo
musicvideo: musicvideo:
album: Music Videos album: Music Videos
artist: Rick Beato 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 🎸: kodi_safe_value 🎸
kodi_safe_title_with_attrs: kodi_safe_title_with_attrs:
attributes: attributes:
@ -16,12 +36,32 @@ Rick Beato - Can you hear the difference 🎸🔥 #shorts.nfo
newlines 🎸 newlines 🎸
tag: tag:
the the
tag 🎸🎸 tag 🎸🎸
title: Can you hear the difference? 🎸🔥 #shorts title: Can you hear the difference? 🎸🔥 #shorts
year: 2022 year: 2022
test.nfo test.nfo
NFO tags: NFO tags:
kodi_safe_root 🎸: 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 🎸: kodi_safe_value 🎸
kodi_safe_title_with_attrs: kodi_safe_title_with_attrs:
attributes: attributes:
@ -30,4 +70,4 @@ test.nfo
newlines 🎸 newlines 🎸
tag: tag:
the the
tag 🎸🎸 tag 🎸🎸

View file

@ -8,6 +8,26 @@ Rick Beato - Can you hear the difference 🎸🔥 #shorts.nfo
musicvideo: musicvideo:
album: Music Videos album: Music Videos
artist: Rick Beato 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 □: kodi_safe_value □
kodi_safe_title_with_attrs: kodi_safe_title_with_attrs:
attributes: attributes:
@ -16,12 +36,32 @@ Rick Beato - Can you hear the difference 🎸🔥 #shorts.nfo
newlines □ newlines □
tag: tag:
the the
tag □□ tag □□
title: Can you hear the difference? □□ #shorts title: Can you hear the difference? □□ #shorts
year: 2022 year: 2022
test.nfo test.nfo
NFO tags: NFO tags:
kodi_safe_root □: 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 □: kodi_safe_value □
kodi_safe_title_with_attrs: kodi_safe_title_with_attrs:
attributes: attributes:
@ -30,4 +70,4 @@ test.nfo
newlines □ newlines □
tag: tag:
the the
tag □□ tag □□

View file

@ -14,7 +14,7 @@ JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4
Video Tags: Video Tags:
description: description:
🎸 / ' " 🎸 / ' "
newline? newline?
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.nfo JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.nfo
NFO tags: NFO tags:
musicvideo: musicvideo:

View file

@ -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-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.info.json
JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4 JMC - This GPU SLIDES into this Case! - Silverstone SUGO 16 ITX Case.mp4
Embedded Chapters Embedded Chapters:
Removed Chapter(s): Intro, Outro Removed Chapter(s): Intro, Outro
Removed SponsorBlock Category Count(s): Removed SponsorBlock Category Count(s):
Sponsor: 2 Sponsor: 2

View file

@ -107,7 +107,8 @@ class TestPreset:
nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions) nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions)
tags_string_dict = { 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"} 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) nfo_options: NfoTagsOptions = preset.plugins.get(NfoTagsOptions)
tags_string_dict = { 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 == { assert tags_string_dict == {