[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 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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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 = {}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 🎸🎸
|
||||||
|
|
@ -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 □□
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 == {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue