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 import os
from abc import ABC from abc import ABC
from pathlib import Path from pathlib import Path
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 ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import Plugin
from ytdl_sub.plugins.plugin import PluginOptions from ytdl_sub.plugins.plugin import PluginOptions
from ytdl_sub.utils.file_handler import FileMetadata 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_dict
from ytdl_sub.utils.xml import to_max_3_byte_utf8_string from ytdl_sub.utils.xml import to_max_3_byte_utf8_string
from ytdl_sub.utils.xml import to_xml 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 DictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
from ytdl_sub.validators.validators import BoolValidator 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( class SharedNfoTagsOptions(
@ -102,7 +31,9 @@ class SharedNfoTagsOptions(
""" """
_formatter_validator: Type[TStringFormatterValidator] _formatter_validator: Type[TStringFormatterValidator]
_tags_validator: Type[_TagsValidator] _tags_validator: Type[
SharedNfoTagsValidator[TStringFormatterValidator, TDictFormatterValidator]
]
_required_keys = {"nfo_name", "nfo_root", "tags"} _required_keys = {"nfo_name", "nfo_root", "tags"}
_optional_keys = {"kodi_safe"} _optional_keys = {"kodi_safe"}
@ -117,20 +48,78 @@ class SharedNfoTagsOptions(
key="kodi_safe", validator=BoolValidator, default=False key="kodi_safe", validator=BoolValidator, default=False
).value ).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 Shared code between NFO tags and Ouptut Directory NFO Tags
""" """
def _create_nfo(self, entry: Optional[Entry] = None) -> None: 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()): for key, string_tag in self.plugin_options.tags.string_tags.items():
nfo[tag] = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) 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 # Write the nfo tags to XML with the nfo_root
nfo_root = self.overrides.apply_formatter( nfo_root = self.overrides.apply_formatter(
@ -138,10 +127,14 @@ class SharedNfoTagsPlugin(Plugin[TSharedNfoTagsOptions], Generic[TSharedNfoTagsO
) )
if self.plugin_options.kodi_safe: if self.plugin_options.kodi_safe:
nfo = to_max_3_byte_utf8_dict(nfo)
nfo_root = to_max_3_byte_utf8_string(nfo_root) 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( nfo_file_name = self.overrides.apply_formatter(
formatter=self.plugin_options.nfo_name, entry=entry formatter=self.plugin_options.nfo_name, entry=entry
@ -154,7 +147,12 @@ class SharedNfoTagsPlugin(Plugin[TSharedNfoTagsOptions], Generic[TSharedNfoTagsO
nfo_file.write(xml) nfo_file.write(xml)
# Save the nfo file and log its metadata # 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) 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 _formatter_validator = StringFormatterValidator
_dict_formatter_validator = DictFormatterValidator _dict_formatter_validator = DictFormatterValidator
_tags_validator = NfoTagsValidator
@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
class NfoTagsPlugin(SharedNfoTagsPlugin[NfoTagsOptions]): class NfoTagsPlugin(SharedNfoTagsPlugin[StringFormatterValidator, DictFormatterValidator]):
plugin_options_type = NfoTagsOptions plugin_options_type = NfoTagsOptions
def post_process_entry(self, entry: Entry) -> None: 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 NfoTagsValidator
from ytdl_sub.plugins.nfo_tags import SharedNfoTagsOptions from ytdl_sub.plugins.nfo_tags import SharedNfoTagsOptions
from ytdl_sub.plugins.nfo_tags import SharedNfoTagsPlugin 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 OverridesDictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
@ -32,6 +33,7 @@ class OutputDirectoryNfoTagsOptions(
_formatter_validator = OverridesStringFormatterValidator _formatter_validator = OverridesStringFormatterValidator
_dict_formatter_validator = OverridesDictFormatterValidator _dict_formatter_validator = OverridesDictFormatterValidator
_tags_validator = NfoOverrideTagsValidator
@property @property
def nfo_name(self) -> OverridesStringFormatterValidator: def nfo_name(self) -> OverridesStringFormatterValidator:
@ -56,7 +58,7 @@ class OutputDirectoryNfoTagsOptions(
@property @property
def tags( def tags(
self, self,
) -> NfoTagsValidator[OverridesStringFormatterValidator, OverridesDictFormatterValidator]: ) -> NfoTagsValidator:
""" """
Tags within the nfo_root tag. In the usage above, it would look like Tags within the nfo_root tag. In the usage above, it would look like
@ -79,7 +81,9 @@ class OutputDirectoryNfoTagsOptions(
return self._kodi_safe return self._kodi_safe
class OutputDirectoryNfoTagsPlugin(SharedNfoTagsPlugin[OutputDirectoryNfoTagsOptions]): class OutputDirectoryNfoTagsPlugin(
SharedNfoTagsPlugin[OverridesStringFormatterValidator, OverridesDictFormatterValidator]
):
plugin_options_type = OutputDirectoryNfoTagsOptions plugin_options_type = OutputDirectoryNfoTagsOptions
def post_process_subscription(self): def post_process_subscription(self):

View file

@ -1,5 +1,27 @@
import xml.etree.ElementTree as et import xml.etree.ElementTree as et
from dataclasses import dataclass
from typing import Any
from typing import Dict 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: 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 Transforms a dict to XML
@ -53,9 +75,10 @@ def to_xml(nfo_dict: Dict[str, str], nfo_root: str) -> bytes:
XML bytes XML bytes
""" """
xml_root = et.Element(nfo_root) xml_root = et.Element(nfo_root)
for key, value in nfo_dict.items(): for key, xml_elem in sorted(nfo_dict.items()):
sub_element = et.SubElement(xml_root, key) sorted_attr = dict(sorted(xml_elem.attributes.items()))
sub_element.text = value 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)

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