Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion darkseid/metadata/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ class Metadata:
genres (list[Basic]): The list of genres.
comments (Optional[str]): The comments.
community_rating (Optional[Decimal]): The community rating (0-5, up to 2 decimal places).
rating_count (Optional[int]): The number of ratings contributing to the community rating.
alternate_series (Optional[str]): The alternate series.
alternate_number (Optional[str]): The alternate number.
alternate_count (Optional[int]): The count of alternates.
Expand Down Expand Up @@ -644,6 +645,7 @@ class Metadata:
comments: str | None = None # use same way as Summary in CIX

community_rating: Decimal | None = None
rating_count: int | None = None
main_character_or_team: str | None = None
review: str | None = None

Expand Down Expand Up @@ -775,6 +777,7 @@ def assign(cur: str, new: any) -> None:
if len(new_md.genres) > 0:
assign("genre", new_md.genres)
assign("community_rating", new_md.community_rating)
assign("rating_count", new_md.rating_count)
assign("main_character_or_team", new_md.main_character_or_team)
assign("review", new_md.review)
assign("alternate_series", new_md.alternate_series)
Expand Down Expand Up @@ -1152,7 +1155,10 @@ def __str__(self: Metadata) -> str: # noqa: PLR0912
# Technical info
tech_info = []
if self.community_rating is not None:
tech_info.append(f"Community Rating: {self.community_rating}")
rating_display = f"Community Rating: {self.community_rating}"
if self.rating_count is not None:
rating_display += f" ({self.rating_count} ratings)"
tech_info.append(rating_display)
if self.scan_info:
tech_info.append(f"Scan: {self.scan_info}")
if tech_info:
Expand Down
59 changes: 55 additions & 4 deletions darkseid/metadata/metroninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class MetronInfo(BaseMetadataHandler):
def __init__(self) -> None:
"""Initialize the MetronInfo instance."""
self._schema_path = (
Path(__file__).parent.parent / "schemas" / "MetronInfo" / "v1" / "MetronInfo.xsd"
Path(__file__).parent.parent / "schemas" / "MetronInfo" / "v1_1" / "MetronInfo.xsd"
)

def metadata_from_string(self, xml_string: str) -> Metadata:
Expand Down Expand Up @@ -261,13 +261,13 @@ def _get_root(xml_bytes: bytes | None) -> ET.Element:
root = ET.Element("MetronInfo")

root.attrib["xmlns:metroninfo"] = (
"https://metron-project.github.io/docs/metroninfo/schemas/v1.0"
"https://metron-project.github.io/docs/metroninfo/schemas/v1.1"
)
root.attrib["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema"
root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib["xsi:schemaLocation"] = (
"https://metron-project.github.io/docs/metroninfo/schemas/v1.0 "
"https://raw.githubusercontent.com/Metron-Project/metroninfo/refs/heads/master/schema/v1.0/MetronInfo.xsd"
"https://metron-project.github.io/docs/metroninfo/schemas/v1.1 "
"https://raw.githubusercontent.com/Metron-Project/metroninfo/refs/heads/master/schema/v1.1/MetronInfo.xsd"
)

return root
Expand Down Expand Up @@ -527,6 +527,25 @@ def _add_info_sources(self, root: ET.Element, info_sources: list[InfoSources]) -
child_node = ET.SubElement(id_node, "ID", attrib=attributes)
child_node.text = str(source.id_)

def _add_community_rating(
self, root: ET.Element, rating: Decimal | None, rating_count: int | None
) -> None:
"""Add community rating information to XML.

Args:
root: Root element.
rating: Average community rating.
rating_count: Number of ratings contributing to the average.

"""
if rating is None:
return

rating_node = self._get_or_create_element(root, "CommunityRating")
ET.SubElement(rating_node, "AverageRating").text = str(rating)
if rating_count:
ET.SubElement(rating_node, "RatingCount").text = str(rating_count)

def _add_gtin(self, root: ET.Element, gtin: GTIN | None) -> None:
"""Add GTIN information to XML.

Expand Down Expand Up @@ -655,6 +674,29 @@ def _parse_gtin_element(self, gtin_element: ET.Element | None) -> GTIN | None:

return gtin if found_data else None

def _parse_community_rating_element(
self, rating_element: ET.Element | None
) -> tuple[Decimal | None, int | None]:
"""Parse CommunityRating element into an average rating and rating count.

Args:
rating_element: The CommunityRating XML element.

Returns:
Tuple of (average rating, rating count), either of which may be None.

"""
if rating_element is None:
return None, None

average_elem = rating_element.find("AverageRating")
rating = self._parse_decimal(average_elem.text) if average_elem is not None else None

count_elem = rating_element.find("RatingCount")
rating_count = self._parse_int(count_elem.text) if count_elem is not None else None

return rating, rating_count

def _parse_info_sources_element(
self, ids_element: ET.Element | None
) -> list[InfoSources] | None:
Expand Down Expand Up @@ -928,6 +970,7 @@ def _convert_metadata_to_xml( # noqa: C901,PLR0912
self._add_series(root, metadata.series)
self._set_element_text(root, "CollectionTitle", metadata.collection_title)
self._set_element_text(root, "Number", metadata.issue)
self._set_element_text(root, "AlternativeNumber", metadata.alternate_number)

if metadata.stories:
self._add_basic_children(root, "Stories", "Story", metadata.stories)
Expand Down Expand Up @@ -971,6 +1014,8 @@ def _convert_metadata_to_xml( # noqa: C901,PLR0912
root, "AgeRating", self._normalize_age_rating(metadata.age_rating)
)

self._add_community_rating(root, metadata.community_rating, metadata.rating_count)

if metadata.web_link:
self._add_urls(root, metadata.web_link)

Expand Down Expand Up @@ -1016,6 +1061,7 @@ def _convert_xml_to_metadata(self, tree: ET.ElementTree) -> Metadata:
"URLs": root.find("URLs"),
"Notes": root.find("Notes"),
"AgeRating": root.find("AgeRating"),
"CommunityRating": root.find("CommunityRating"),
"Stories": root.find("Stories"),
"Genres": root.find("Genres"),
"Tags": root.find("Tags"),
Expand All @@ -1036,6 +1082,7 @@ def _convert_xml_to_metadata(self, tree: ET.ElementTree) -> Metadata:
# Handle issue number with IssueString
issue_number = self._get_text_content(root, "Number")
md.issue = IssueString(issue_number).as_string() if issue_number else None
md.alternate_number = self._get_text_content(root, "AlternativeNumber")

md.stories = self._parse_basic_list_element(element_cache["Stories"])
md.comments = self._get_text_content(root, "Summary")
Expand Down Expand Up @@ -1066,6 +1113,10 @@ def _convert_xml_to_metadata(self, tree: ET.ElementTree) -> Metadata:
else None
)

md.community_rating, md.rating_count = self._parse_community_rating_element(
element_cache["CommunityRating"]
)

md.web_link = self._parse_urls_element(element_cache["URLs"])

# Handle last modified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<xs:element name="MangaVolume" type="xs:string" minOccurs="0" /> <!-- This is used for Manga -->
<xs:element name="CollectionTitle" type="xs:string" minOccurs="0" />
<xs:element name="Number" type="xs:string" minOccurs="0" />
<xs:element name="AlternativeNumber" type="xs:string" minOccurs="0" />
<xs:element name="Stories" type="storyType" minOccurs="0" /> <!-- Story titles in issue -->
<xs:element name="Summary" type="xs:string" minOccurs="0" />
<xs:element name="Prices" type="pricesType" minOccurs="0" />
Expand All @@ -28,6 +29,7 @@
<xs:element name="Reprints" type="reprintsType" minOccurs="0" />
<xs:element name="GTIN" type="gtinType" minOccurs="0" />
<xs:element name="AgeRating" type="ageRatingType" minOccurs="0" default="Unknown" />
<xs:element name="CommunityRating" type="communityRatingType" minOccurs="0" />
<xs:element name="URLs" type="urlsType" minOccurs="0" />
<xs:element name="Credits" type="creditsType" minOccurs="0" />
<xs:element name="LastModified" type="xs:dateTime" minOccurs="0" />
Expand Down Expand Up @@ -204,17 +206,9 @@
</xs:all>
</xs:complexType>

<xs:complexType name="genreType">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="id" type="xs:string" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>

<xs:complexType name="genresType">
<xs:sequence>
<xs:element name="Genre" type="genreType" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="Genre" type="resourceType" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>

Expand All @@ -239,6 +233,13 @@
</xs:all>
</xs:complexType>

<xs:complexType name="communityRatingType">
<xs:all>
<xs:element name="AverageRating" type="averageRatingType" />
<xs:element name="RatingCount" type="xs:positiveInteger" minOccurs="0" />
</xs:all>
</xs:complexType>

<!-- Simple Types -->
<xs:simpleType name="formatType">
<xs:restriction base="xs:string">
Expand Down Expand Up @@ -333,6 +334,13 @@
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="averageRatingType">
<xs:restriction base="xs:decimal">
<xs:minInclusive value="0" />
<xs:maxInclusive value="5.0" />
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="countryCode">
<xs:restriction base="xs:string">
<xs:pattern value="[A-Z][A-Z]" />
Expand Down
12 changes: 7 additions & 5 deletions darkseid/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
__all__ = ["SchemaVersion", "ValidateMetadata", "ValidationError"]

import logging
import re
from contextlib import contextmanager
from enum import Enum, auto, unique
from importlib.resources import as_file, files
Expand Down Expand Up @@ -49,14 +50,15 @@ class SchemaVersion(Enum):
for validation priority.
"""

METRON_INFO_V1 = auto()
METRON_INFO_V1_1 = auto()
COMIC_INFO_V2 = auto()
COMIC_INFO_V1 = auto()
UNKNOWN = auto()

def __str__(self) -> str:
"""Return a human-readable string representation."""
return self.name.replace("_", " ").title()
title = self.name.replace("_", " ").title()
return re.sub(r"(\d) (\d)", r"\1.\2", title)


class ValidateMetadata:
Expand All @@ -80,16 +82,16 @@ class ValidateMetadata:
"file_name": "ComicInfo.xsd",
"schema_class": XMLSchema10,
},
SchemaVersion.METRON_INFO_V1: {
"module_path": "darkseid.schemas.MetronInfo.v1",
SchemaVersion.METRON_INFO_V1_1: {
"module_path": "darkseid.schemas.MetronInfo.v1_1",
"file_name": "MetronInfo.xsd",
"schema_class": XMLSchema11,
},
}

# Validation order: newest/most specific schemas first
_VALIDATION_ORDER: ClassVar[list[SchemaVersion]] = [
SchemaVersion.METRON_INFO_V1,
SchemaVersion.METRON_INFO_V1_1,
SchemaVersion.COMIC_INFO_V2,
SchemaVersion.COMIC_INFO_V1,
]
Expand Down
38 changes: 38 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,44 @@ def test_metadata_overlay_community_rating_preserves_existing():
assert md1.community_rating == Decimal("3.00")


def test_rating_count_none():
"""Test that None rating_count stays None."""
md = Metadata()
assert md.rating_count is None


def test_rating_count_in_str():
"""Test that rating_count is appended to the community rating in __str__ output."""
md = Metadata(community_rating=Decimal("4.50"), rating_count=150)
result = str(md)
assert "Community Rating: 4.50 (150 ratings)" in result


def test_rating_count_omitted_from_str_without_community_rating():
"""Test that rating_count alone doesn't produce a Community Rating line."""
md = Metadata(rating_count=150)
result = str(md)
assert "Community Rating" not in result


def test_metadata_overlay_rating_count():
"""Test that overlay replaces rating_count with a non-None value."""
md1 = Metadata(rating_count=10)
md2 = Metadata(rating_count=150)

md1.overlay(md2)
assert md1.rating_count == 150


def test_metadata_overlay_rating_count_preserves_existing():
"""Test that overlay leaves rating_count unchanged when the new value is None."""
md1 = Metadata(rating_count=10)
md2 = Metadata() # rating_count is None

md1.overlay(md2)
assert md1.rating_count == 10


def test_metadata_comprehensive_str():
"""Test comprehensive string representation with many fields."""
# Create a metadata object with many fields populated
Expand Down
Loading
Loading