From 2a3ccd0f5bf8be12185235118cea4631031a38b7 Mon Sep 17 00:00:00 2001 From: Buried-In-Code <6057651+Buried-In-Code@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:53:07 +1300 Subject: [PATCH] Fix URL naming, closes #84 Add timezone to last_modified using local tz as default Fix ComicInfo pages --- perdoo/__main__.py | 27 ++++++++++++++++++++++--- perdoo/metadata/comic_info.py | 37 ++++------------------------------ perdoo/metadata/metron_info.py | 22 +++++++++++--------- 3 files changed, 40 insertions(+), 46 deletions(-) diff --git a/perdoo/__main__.py b/perdoo/__main__.py index 384c49d..9afd60c 100644 --- a/perdoo/__main__.py +++ b/perdoo/__main__.py @@ -1,6 +1,7 @@ import logging from datetime import date from enum import Enum +from io import BytesIO from pathlib import Path from platform import python_version from typing import Annotated @@ -129,6 +130,10 @@ def get_search_details( def load_page_info(entry: Comic, comic_info: ComicInfo) -> list[Page]: + from PIL import Image # noqa: PLC0415 + + from perdoo.metadata.comic_info import PageType # noqa: PLC0415 + pages = set() image_files = [ x @@ -136,10 +141,26 @@ def load_page_info(entry: Comic, comic_info: ComicInfo) -> list[Page]: if Path(x).suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS ] for idx, file in enumerate(image_files): - img_file = Path(file) - is_final_page = idx == len(image_files) - 1 page = next((x for x in comic_info.pages if x.image == idx), None) - pages.add(Page.from_path(file=img_file, index=idx, is_final_page=is_final_page, page=page)) + if page: + page_type = page.type + elif idx == 0: + page_type = PageType.FRONT_COVER + elif idx == len(image_files) - 1: + page_type = PageType.BACK_COVER + else: + page_type = PageType.STORY + if not page: + page = Page(image=idx) + page.type = page_type + page_bytes = entry.archive.read_file(file) + page.image_size = len(page_bytes) + with Image.open(BytesIO(page_bytes)) as page_data: + width, height = page_data.size + page.double_page = width >= height + page.image_height = height + page.image_width = width + pages.add(page) return sorted(pages) diff --git a/perdoo/metadata/comic_info.py b/perdoo/metadata/comic_info.py index 0202c27..0953646 100644 --- a/perdoo/metadata/comic_info.py +++ b/perdoo/metadata/comic_info.py @@ -4,22 +4,14 @@ from collections.abc import Callable from datetime import date from enum import Enum -from pathlib import Path from natsort import humansorted, ns -from PIL import Image from pydantic import HttpUrl, NonNegativeFloat from pydantic_xml import attr, computed_attr, element, wrapped from perdoo.metadata._base import PascalModel from perdoo.settings import Naming -try: - from typing import Self # Python >= 3.11 -except ImportError: - from typing_extensions import Self # Python < 3.11 - - LOGGER = logging.getLogger(__name__) @@ -41,7 +33,7 @@ class YesNo(Enum): YES = "Yes" @staticmethod - def load(value: str) -> Self: + def load(value: str) -> "YesNo": for entry in YesNo: if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold(): return entry @@ -59,7 +51,7 @@ class Manga(Enum): YES_AND_RIGHT_TO_LEFT = "YesAndRightToLeft" @staticmethod - def load(value: str) -> Self: + def load(value: str) -> "Manga": for entry in Manga: if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold(): return entry @@ -88,7 +80,7 @@ class AgeRating(Enum): X18 = "X18+" @staticmethod - def load(value: str) -> Self: + def load(value: str) -> "AgeRating": for entry in AgeRating: if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold(): return entry @@ -113,7 +105,7 @@ class PageType(Enum): DELETED = "Deleted" @staticmethod - def load(value: str) -> Self: + def load(value: str) -> "PageType": for entry in PageType: if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold(): return entry @@ -147,27 +139,6 @@ def __eq__(self, other) -> bool: # noqa: ANN001 def __hash__(self) -> int: return hash((type(self), self.image)) - @staticmethod - def from_path(file: Path, index: int, is_final_page: bool, page: Self | None) -> Self: - if page: - page_type = page.type - elif index == 0: - page_type = PageType.FRONT_COVER - elif is_final_page: - page_type = PageType.BACK_COVER - else: - page_type = PageType.STORY - with Image.open(file) as img: - width, height = img.size - return Page( - image=index, - type=page_type, - double_page=width >= height, - image_size=file.stat().st_size, - image_height=height, - image_width=width, - ) - class ComicInfo(PascalModel): age_rating: AgeRating = element(default=AgeRating.UNKNOWN) diff --git a/perdoo/metadata/metron_info.py b/perdoo/metadata/metron_info.py index 2a27bc9..bddfdf9 100644 --- a/perdoo/metadata/metron_info.py +++ b/perdoo/metadata/metron_info.py @@ -24,17 +24,12 @@ from enum import Enum from typing import Generic, TypeVar -from pydantic import HttpUrl, NonNegativeInt, PositiveInt +from pydantic import HttpUrl, NonNegativeInt, PositiveInt, field_validator from pydantic_xml import attr, computed_attr, element, wrapped from perdoo.metadata._base import PascalModel from perdoo.settings import Naming -try: - from typing import Self # Python >= 3.11 -except ImportError: - from typing_extensions import Self # Python < 3.11 - LOGGER = logging.getLogger(__name__) T = TypeVar("T") @@ -49,7 +44,7 @@ class AgeRating(Enum): ADULT = "Adult" @staticmethod - def load(value: str) -> Self: + def load(value: str) -> "AgeRating": for entry in AgeRating: if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold(): return entry @@ -142,7 +137,7 @@ class Role(Enum): OTHER = "Other" @staticmethod - def load(value: str) -> Self: + def load(value: str) -> "Role": for entry in Role: if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold(): return entry @@ -191,7 +186,7 @@ class InformationSource(Enum): LEAGUE_OF_COMIC_GEEKS = "League of Comic Geeks" @staticmethod - def load(value: str) -> Self: + def load(value: str) -> "InformationSource": for entry in InformationSource: if entry.value.replace(" ", "").casefold() == value.replace(" ", "").casefold(): return entry @@ -366,7 +361,7 @@ class MetronInfo(PascalModel): universes: list[Universe] = wrapped( path="Universes", entity=element(tag="Universe", default_factory=list) ) - urls: list[Url] = wrapped(path="URLS", entity=element(tag="URLs", default_factory=list)) + urls: list[Url] = wrapped(path="URLs", entity=element(tag="URL", default_factory=list)) @computed_attr(ns="xsi", name="noNamespaceSchemaLocation") def schema_location(self) -> str: @@ -389,6 +384,13 @@ def get_filename(self, settings: Naming) -> str: seperator=settings.seperator, ) + @field_validator("last_modified", mode="before") + def ensure_timezone(cls, v: str | datetime | None) -> str | datetime | None: + if isinstance(v, datetime) and v.tzinfo is None: + timezone = datetime.now().astimezone().tzinfo + return v.replace(tzinfo=timezone) + return v + PATTERN_MAP: dict[str, Callable[[MetronInfo], str | int | None]] = { "cover-date": lambda x: x.cover_date,