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</season> + <genre>Comedy</genre> + <genre>Drama</genre> """ 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 == {