diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py
index 20df6ef3..fbbbe161 100644
--- a/src/ytdl_sub/config/preset.py
+++ b/src/ytdl_sub/config/preset.py
@@ -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(
diff --git a/src/ytdl_sub/downloaders/youtube/merge_playlist.py b/src/ytdl_sub/downloaders/youtube/merge_playlist.py
index 4ef24b61..2c0da03f 100644
--- a/src/ytdl_sub/downloaders/youtube/merge_playlist.py
+++ b/src/ytdl_sub/downloaders/youtube/merge_playlist.py
@@ -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:
"""
diff --git a/src/ytdl_sub/plugins/chapters.py b/src/ytdl_sub/plugins/chapters.py
index dc4d4693..870b144c 100644
--- a/src/ytdl_sub/plugins/chapters.py
+++ b/src/ytdl_sub/plugins/chapters.py
@@ -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 = {}
diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py
index c74c85a1..040e2ceb 100644
--- a/src/ytdl_sub/plugins/music_tags.py
+++ b/src/ytdl_sub/plugins/music_tags.py
@@ -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")
diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py
index 86a100bf..dd56c4ca 100644
--- a/src/ytdl_sub/plugins/nfo_tags.py
+++ b/src/ytdl_sub/plugins/nfo_tags.py
@@ -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(
502
- 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
2022
+ Comedy
+ Drama
"""
return self._tags
diff --git a/src/ytdl_sub/plugins/output_directory_nfo_tags.py b/src/ytdl_sub/plugins/output_directory_nfo_tags.py
index 0f15296b..582e2b3c 100644
--- a/src/ytdl_sub/plugins/output_directory_nfo_tags.py
+++ b/src/ytdl_sub/plugins/output_directory_nfo_tags.py
@@ -62,7 +62,7 @@ class OutputDirectoryNfoTagsOptions(
Sweet youtube TV show
- 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
Sweet youtube TV show
+ Comedy
+ Drama
"""
return self._tags
diff --git a/src/ytdl_sub/plugins/split_by_chapters.py b/src/ytdl_sub/plugins/split_by_chapters.py
index 78fd43cb..98ba5378 100644
--- a/src/ytdl_sub/plugins/split_by_chapters.py
+++ b/src/ytdl_sub/plugins/split_by_chapters.py
@@ -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,
)
diff --git a/src/ytdl_sub/plugins/video_tags.py b/src/ytdl_sub/plugins/video_tags.py
index aa5989bb..61d89a78 100644
--- a/src/ytdl_sub/plugins/video_tags.py
+++ b/src/ytdl_sub/plugins/video_tags.py
@@ -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")
diff --git a/src/ytdl_sub/utils/file_handler.py b/src/ytdl_sub/utils/file_handler.py
index c9531370..7c339f1a 100644
--- a/src/ytdl_sub/utils/file_handler.py
+++ b/src/ytdl_sub/utils/file_handler.py
@@ -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:
diff --git a/src/ytdl_sub/utils/xml.py b/src/ytdl_sub/utils/xml.py
index f4f721d4..1e5e3179 100644
--- a/src/ytdl_sub/utils/xml.py
+++ b/src/ytdl_sub/utils/xml.py
@@ -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)
diff --git a/src/ytdl_sub/validators/nfo_validators.py b/src/ytdl_sub/validators/nfo_validators.py
index 36cb360e..b221970b 100644
--- a/src/ytdl_sub/validators/nfo_validators.py
+++ b/src/ytdl_sub/validators/nfo_validators.py
@@ -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
diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py
index 97c61a84..cd62b4a4 100644
--- a/src/ytdl_sub/validators/string_formatter_validators.py
+++ b/src/ytdl_sub/validators/string_formatter_validators.py
@@ -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
diff --git a/tests/e2e/plugins/test_nfo_tags.py b/tests/e2e/plugins/test_nfo_tags.py
index 11363a09..a3260f8c 100644
--- a/tests/e2e/plugins/test_nfo_tags.py
+++ b/tests/e2e/plugins/test_nfo_tags.py
@@ -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,
+ )
diff --git a/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo.txt b/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo.txt
index a95d6bde..4efa621d 100644
--- a/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo.txt
+++ b/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo.txt
@@ -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 🎸🎸
\ No newline at end of file
+ tag 🎸🎸
\ No newline at end of file
diff --git a/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo_kodi_safe.txt b/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo_kodi_safe.txt
index 3076ae3f..6cc1f4a9 100644
--- a/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo_kodi_safe.txt
+++ b/tests/e2e/resources/transaction_log_summaries/plugins/nfo_tags/test_nfo_kodi_safe.txt
@@ -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 â–¡â–¡
\ No newline at end of file
+ tag â–¡â–¡
\ No newline at end of file
diff --git a/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_from_ts_with_subs.txt b/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_from_ts_with_subs.txt
index 6fa3cad8..b3c05cb2 100644
--- a/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_from_ts_with_subs.txt
+++ b/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_from_ts_with_subs.txt
@@ -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:
diff --git a/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_sb_and_embedded_subs.txt b/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_sb_and_embedded_subs.txt
index 3db149e6..e64bf6e0 100644
--- a/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_sb_and_embedded_subs.txt
+++ b/tests/e2e/resources/transaction_log_summaries/plugins/test_chapters_sb_and_embedded_subs.txt
@@ -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
diff --git a/tests/unit/config/test_preset.py b/tests/unit/config/test_preset.py
index fd32204e..54729004 100644
--- a/tests/unit/config/test_preset.py
+++ b/tests/unit/config/test_preset.py
@@ -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 == {