diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py
index 39e65f02..05b1e7f3 100644
--- a/src/ytdl_sub/plugins/nfo_tags.py
+++ b/src/ytdl_sub/plugins/nfo_tags.py
@@ -1,97 +1,26 @@
import os
from abc import ABC
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
from ytdl_sub.entries.entry import Entry
from ytdl_sub.plugins.plugin import Plugin
from ytdl_sub.plugins.plugin import PluginOptions
from ytdl_sub.utils.file_handler import FileMetadata
+from ytdl_sub.utils.xml import XmlElement
from ytdl_sub.utils.xml import to_max_3_byte_utf8_dict
from ytdl_sub.utils.xml import to_max_3_byte_utf8_string
from ytdl_sub.utils.xml import to_xml
-from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
+from ytdl_sub.validators.nfo_validators import NfoTagsValidator
+from ytdl_sub.validators.nfo_validators import SharedNfoTagsValidator
+from ytdl_sub.validators.nfo_validators import TDictFormatterValidator
+from ytdl_sub.validators.nfo_validators import TStringFormatterValidator
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
from ytdl_sub.validators.validators import BoolValidator
-from ytdl_sub.validators.validators import DictValidator
-
-TStringFormatterValidator = TypeVar("TStringFormatterValidator", bound=StringFormatterValidator)
-TDictFormatterValidator = TypeVar("TDictFormatterValidator", bound=DictFormatterValidator)
-
-
-class NfoTagsWithAttributesValidator(
- StrictDictValidator, Generic[TStringFormatterValidator, TDictFormatterValidator], ABC
-):
-
- _required_keys = {"attributes", "tag"}
-
- formatter_validator: Type[TStringFormatterValidator]
- dict_formatter_validator: Type[TDictFormatterValidator]
-
- def __init__(self, name, value):
- super().__init__(name, value)
- self._attributes = self._validate_key(
- key="attributes", validator=self.dict_formatter_validator
- )
- self._tag = self._validate_key(key="tag", validator=self.formatter_validator)
-
- @property
- def attributes(self) -> TDictFormatterValidator:
- """
- Returns
- -------
- The attributes for this NFO tag
- """
- return self._attributes
-
- @property
- def tag(self) -> TStringFormatterValidator:
- """
- Returns
- -------
- The value for this NFO tag
- """
- return self._tag
-
-
-_TagsWithAttributesValidator = NfoTagsWithAttributesValidator[
- TStringFormatterValidator, TDictFormatterValidator
-]
-
-
-class NfoTagsValidator(
- DictValidator, Generic[TStringFormatterValidator, TDictFormatterValidator], ABC
-):
-
- _tags_with_attributes_validator: Type[_TagsWithAttributesValidator]
-
- def __init__(self, name, value):
- super().__init__(name, value)
-
- self.tags: List[TStringFormatterValidator] = []
- self.tags_with_attributes: List[_TagsWithAttributesValidator] = []
-
- for key in self._keys:
- if isinstance(value, str):
- validated = self._validate_key(
- key=key, validator=self._tags_with_attributes_validator.formatter_validator
- )
- self.tags.append(validated)
- elif isinstance(value, dict):
- validated = self._validate_key(
- key=key, validator=self._tags_with_attributes_validator
- )
- self.tags_with_attributes.append(validated)
- else:
- raise self._validation_exception("must either be a string or attributes object")
-
-
-_TagsValidator = NfoTagsValidator[TStringFormatterValidator, TDictFormatterValidator]
class SharedNfoTagsOptions(
@@ -102,7 +31,9 @@ class SharedNfoTagsOptions(
"""
_formatter_validator: Type[TStringFormatterValidator]
- _tags_validator: Type[_TagsValidator]
+ _tags_validator: Type[
+ SharedNfoTagsValidator[TStringFormatterValidator, TDictFormatterValidator]
+ ]
_required_keys = {"nfo_name", "nfo_root", "tags"}
_optional_keys = {"kodi_safe"}
@@ -117,20 +48,78 @@ class SharedNfoTagsOptions(
key="kodi_safe", validator=BoolValidator, default=False
).value
+ @property
+ def nfo_name(self) -> StringFormatterValidator:
+ """
+ The NFO file name.
+ """
+ return self._nfo_name
-TSharedNfoTagsOptions = TypeVar("TSharedNfoTagsOptions", bound=SharedNfoTagsOptions)
+ @property
+ def nfo_root(self) -> StringFormatterValidator:
+ """
+ The root tag of the NFO's XML. In the usage above, it would look like
+
+ .. code-block:: xml
+
+
+
+
+ """
+ return self._nfo_root
+
+ @property
+ def tags(self) -> SharedNfoTagsValidator[TStringFormatterValidator, TDictFormatterValidator]:
+ """
+ Tags within the nfo_root tag. In the usage above, it would look like
+
+ .. code-block:: xml
+
+
+
+ Awesome Youtube Video
+ 2022
+ 502
+
+ """
+ return self._tags
+
+ @property
+ def kodi_safe(self) -> Optional[bool]:
+ """
+ Optional. Kodi does not support > 3-byte unicode characters, which include emojis and some
+ foreign language characters. Setting this to True will replace those characters with '□'.
+ Defaults to False.
+ """
+ return self._kodi_safe
-class SharedNfoTagsPlugin(Plugin[TSharedNfoTagsOptions], Generic[TSharedNfoTagsOptions], ABC):
+class SharedNfoTagsPlugin(
+ Plugin[SharedNfoTagsOptions[TStringFormatterValidator, TDictFormatterValidator]],
+ Generic[TStringFormatterValidator, TDictFormatterValidator],
+ ABC,
+):
"""
Shared code between NFO tags and Ouptut Directory NFO Tags
"""
def _create_nfo(self, entry: Optional[Entry] = None) -> None:
- nfo = {}
+ nfo_tags: Dict[str, XmlElement] = {}
- for tag, tag_formatter in sorted(self.plugin_options.tags.dict.items()):
- nfo[tag] = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry)
+ 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, 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()
+ },
+ )
# Write the nfo tags to XML with the nfo_root
nfo_root = self.overrides.apply_formatter(
@@ -138,10 +127,14 @@ class SharedNfoTagsPlugin(Plugin[TSharedNfoTagsOptions], Generic[TSharedNfoTagsO
)
if self.plugin_options.kodi_safe:
- nfo = to_max_3_byte_utf8_dict(nfo)
nfo_root = to_max_3_byte_utf8_string(nfo_root)
+ for key, xml_elem in nfo_tags.items():
+ nfo_tags[key] = XmlElement(
+ text=to_max_3_byte_utf8_string(xml_elem.text),
+ attributes=to_max_3_byte_utf8_dict(xml_elem.attributes),
+ )
- xml = to_xml(nfo_dict=nfo, nfo_root=nfo_root)
+ xml = to_xml(nfo_dict=nfo_tags, nfo_root=nfo_root)
nfo_file_name = self.overrides.apply_formatter(
formatter=self.plugin_options.nfo_name, entry=entry
@@ -154,7 +147,12 @@ class SharedNfoTagsPlugin(Plugin[TSharedNfoTagsOptions], Generic[TSharedNfoTagsO
nfo_file.write(xml)
# Save the nfo file and log its metadata
- nfo_metadata = FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:")
+ nfo_metadata = FileMetadata.from_dict(
+ value_dict={
+ nfo_root: {key: xml_elem.to_dict_value() for key, xml_elem in nfo_tags.items()}
+ },
+ title="NFO tags:",
+ )
self.save_file(file_name=nfo_file_name, file_metadata=nfo_metadata, entry=entry)
@@ -183,54 +181,10 @@ class NfoTagsOptions(SharedNfoTagsOptions[StringFormatterValidator, DictFormatte
_formatter_validator = StringFormatterValidator
_dict_formatter_validator = DictFormatterValidator
-
- @property
- def nfo_name(self) -> StringFormatterValidator:
- """
- The NFO file name.
- """
- return self._nfo_name
-
- @property
- def nfo_root(self) -> StringFormatterValidator:
- """
- The root tag of the NFO's XML. In the usage above, it would look like
-
- .. code-block:: xml
-
-
-
-
- """
- return self._nfo_root
-
- @property
- def tags(self) -> NfoTagsValidator[StringFormatterValidator, DictFormatterValidator]:
- """
- Tags within the nfo_root tag. In the usage above, it would look like
-
- .. code-block:: xml
-
-
-
- Awesome Youtube Video
- 2022
- 502
-
- """
- return self._tags
-
- @property
- def kodi_safe(self) -> Optional[bool]:
- """
- Optional. Kodi does not support > 3-byte unicode characters, which include emojis and some
- foreign language characters. Setting this to True will replace those characters with '□'.
- Defaults to False.
- """
- return self._kodi_safe
+ _tags_validator = NfoTagsValidator
-class NfoTagsPlugin(SharedNfoTagsPlugin[NfoTagsOptions]):
+class NfoTagsPlugin(SharedNfoTagsPlugin[StringFormatterValidator, DictFormatterValidator]):
plugin_options_type = NfoTagsOptions
def post_process_entry(self, entry: Entry) -> None:
diff --git a/src/ytdl_sub/plugins/output_directory_nfo_tags.py b/src/ytdl_sub/plugins/output_directory_nfo_tags.py
index fa611b61..31c07da4 100644
--- a/src/ytdl_sub/plugins/output_directory_nfo_tags.py
+++ b/src/ytdl_sub/plugins/output_directory_nfo_tags.py
@@ -3,6 +3,7 @@ from typing import Optional
from ytdl_sub.plugins.nfo_tags import NfoTagsValidator
from ytdl_sub.plugins.nfo_tags import SharedNfoTagsOptions
from ytdl_sub.plugins.nfo_tags import SharedNfoTagsPlugin
+from ytdl_sub.validators.nfo_validators import NfoOverrideTagsValidator
from ytdl_sub.validators.string_formatter_validators import OverridesDictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
@@ -32,6 +33,7 @@ class OutputDirectoryNfoTagsOptions(
_formatter_validator = OverridesStringFormatterValidator
_dict_formatter_validator = OverridesDictFormatterValidator
+ _tags_validator = NfoOverrideTagsValidator
@property
def nfo_name(self) -> OverridesStringFormatterValidator:
@@ -56,7 +58,7 @@ class OutputDirectoryNfoTagsOptions(
@property
def tags(
self,
- ) -> NfoTagsValidator[OverridesStringFormatterValidator, OverridesDictFormatterValidator]:
+ ) -> NfoTagsValidator:
"""
Tags within the nfo_root tag. In the usage above, it would look like
@@ -79,7 +81,9 @@ class OutputDirectoryNfoTagsOptions(
return self._kodi_safe
-class OutputDirectoryNfoTagsPlugin(SharedNfoTagsPlugin[OutputDirectoryNfoTagsOptions]):
+class OutputDirectoryNfoTagsPlugin(
+ SharedNfoTagsPlugin[OverridesStringFormatterValidator, OverridesDictFormatterValidator]
+):
plugin_options_type = OutputDirectoryNfoTagsOptions
def post_process_subscription(self):
diff --git a/src/ytdl_sub/utils/xml.py b/src/ytdl_sub/utils/xml.py
index bf452fd5..f4f721d4 100644
--- a/src/ytdl_sub/utils/xml.py
+++ b/src/ytdl_sub/utils/xml.py
@@ -1,5 +1,27 @@
import xml.etree.ElementTree as et
+from dataclasses import dataclass
+from typing import Any
from typing import Dict
+from typing import Union
+
+
+@dataclass
+class XmlElement:
+ text: str
+ attributes: Dict[str, str]
+
+ def to_dict_value(self) -> Union[str, Dict[str, Any]]:
+ """
+ Returns
+ -------
+ Only the tag if no attributes, otherwise a dict containing both attributes and the tag
+ """
+ if not self.attributes:
+ return self.text
+ return {
+ "attributes": self.attributes,
+ "tag": self.text,
+ }
def _to_max_3_byte_utf8_char(char: str) -> str:
@@ -37,7 +59,7 @@ def to_max_3_byte_utf8_dict(string_dict: Dict[str, str]) -> Dict[str, str]:
}
-def to_xml(nfo_dict: Dict[str, str], nfo_root: str) -> bytes:
+def to_xml(nfo_dict: Dict[str, XmlElement], nfo_root: str) -> bytes:
"""
Transforms a dict to XML
@@ -53,9 +75,10 @@ def to_xml(nfo_dict: Dict[str, str], nfo_root: str) -> bytes:
XML bytes
"""
xml_root = et.Element(nfo_root)
- for key, value in nfo_dict.items():
- sub_element = et.SubElement(xml_root, key)
- sub_element.text = value
+ 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
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
new file mode 100644
index 00000000..36cb360e
--- /dev/null
+++ b/src/ytdl_sub/validators/nfo_validators.py
@@ -0,0 +1,124 @@
+from abc import ABC
+from typing import Dict
+from typing import Generic
+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 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
+
+TStringFormatterValidator = TypeVar("TStringFormatterValidator", bound=StringFormatterValidator)
+TDictFormatterValidator = TypeVar("TDictFormatterValidator", bound=DictFormatterValidator)
+
+
+class _NfoTagsWithAttributesValidator(
+ StrictDictValidator, Generic[TStringFormatterValidator, TDictFormatterValidator], ABC
+):
+
+ _required_keys = {"attributes", "tag"}
+
+ formatter_validator: Type[TStringFormatterValidator]
+ dict_formatter_validator: Type[TDictFormatterValidator]
+
+ def __init__(self, name, value):
+ super().__init__(name, value)
+ self._attributes = self._validate_key(
+ key="attributes", validator=self.dict_formatter_validator
+ )
+ self._tag = self._validate_key(key="tag", validator=self.formatter_validator)
+
+ @property
+ def attributes(self) -> TDictFormatterValidator:
+ """
+ Returns
+ -------
+ The attributes for this NFO tag
+ """
+ return self._attributes
+
+ @property
+ def tag(self) -> TStringFormatterValidator:
+ """
+ Returns
+ -------
+ The value for this NFO tag
+ """
+ return self._tag
+
+
+class NfoTagsWithAttributesValidator(
+ _NfoTagsWithAttributesValidator[StringFormatterValidator, DictFormatterValidator]
+):
+ formatter_validator = StringFormatterValidator
+ dict_formatter_validator = DictFormatterValidator
+
+
+class NfoOverrideTagsWithAttributesValidator(
+ _NfoTagsWithAttributesValidator[
+ OverridesStringFormatterValidator, OverridesDictFormatterValidator
+ ]
+):
+ formatter_validator = OverridesStringFormatterValidator
+ dict_formatter_validator = OverridesDictFormatterValidator
+
+
+TNfoTagsWithAttributesValidator = _NfoTagsWithAttributesValidator[
+ TStringFormatterValidator, TDictFormatterValidator
+]
+
+
+class SharedNfoTagsValidator(
+ DictValidator, Generic[TStringFormatterValidator, TDictFormatterValidator], ABC
+):
+
+ _tags_with_attributes_validator: Type[TNfoTagsWithAttributesValidator]
+
+ def __init__(self, name, value):
+ super().__init__(name, value)
+
+ self._string_tags: Dict[str, StringFormatterValidator] = {}
+ self._attribute_tags: Dict[str, TNfoTagsWithAttributesValidator] = {}
+
+ 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
+ )
+ elif isinstance(tag_value, dict):
+ self._attribute_tags[key] = self._validate_key(
+ key=key, validator=self._tags_with_attributes_validator
+ )
+ else:
+ raise self._validation_exception("must either be a string or attributes object")
+
+ @property
+ def string_tags(self) -> Dict[str, StringFormatterValidator]:
+ """
+ Returns
+ -------
+ Tags with no attributes
+ """
+ return self._string_tags
+
+ @property
+ def attribute_tags(self) -> Dict[str, TNfoTagsWithAttributesValidator]:
+ """
+ Returns
+ -------
+ Tags with attributes
+ """
+ return self._attribute_tags
+
+
+class NfoTagsValidator(SharedNfoTagsValidator[StringFormatterValidator, DictFormatterValidator]):
+ _tags_with_attributes_validator = NfoTagsWithAttributesValidator
+
+
+class NfoOverrideTagsValidator(
+ SharedNfoTagsValidator[OverridesStringFormatterValidator, OverridesDictFormatterValidator]
+):
+ _tags_with_attributes_validator = NfoOverrideTagsWithAttributesValidator