xml-att working

This commit is contained in:
Jesse Bannon 2022-08-27 22:41:56 -07:00
parent 90ed66ffac
commit fd80b93597
4 changed files with 243 additions and 138 deletions

View file

@ -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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
</episodedetails>
"""
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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
<title>Awesome Youtube Video</title>
<season>2022</season>
<episode>502</episode>
</episodedetails>
"""
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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
</episodedetails>
"""
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
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<episodedetails>
<title>Awesome Youtube Video</title>
<season>2022</season>
<episode>502</episode>
</episodedetails>
"""
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:

View file

@ -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):

View file

@ -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)

View file

@ -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