diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index f6b5989..4f50eac 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -339,8 +339,7 @@ class ComicArchive: md.apply_default_page_list(self.get_page_name_list()) if calc_page_sizes: for index, p in enumerate(md.pages): - idx = int(p["image_index"]) - + idx = p.display_index if self.pil_available: try: from PIL import Image @@ -348,13 +347,9 @@ class ComicArchive: self.pil_available = True except ImportError: self.pil_available = False - if ( - "size" not in p - or "height" not in p - or "width" not in p - or ("double_page" not in p and detect_double_page) - ): + if p.byte_size is None or p.height is None or p.width is None or p.double_page is None: data = self.get_page(idx) + p.byte_size = len(data) if data: try: if isinstance(data, bytes): @@ -363,19 +358,16 @@ class ComicArchive: im = Image.open(io.StringIO(data)) w, h = im.size - p["size"] = str(len(data)) - p["height"] = str(h) - p["width"] = str(w) + p.height = h + p.width = w if detect_double_page: - p["double_page"] = utils.is_double_page(p) + p.double_page = p.is_double_page() except Exception as e: logger.warning("Error decoding image [%s] %s :: image %s", e, self.path, index) - p["size"] = str(len(data)) - else: - if "size" not in p: + if p.byte_size is not None: data = self.get_page(idx) - p["size"] = str(len(data)) + p.byte_size = len(data) def metadata_from_filename( self, diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 86f7e76..62ee7d8 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -25,9 +25,9 @@ import copy import dataclasses import logging from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, TypedDict, Union, overload +from typing import TYPE_CHECKING, Any, Union, overload -from typing_extensions import NamedTuple, Required +from typing_extensions import NamedTuple from comicapi import merge, utils from comicapi._url import Url, parse_url @@ -42,7 +42,7 @@ logger = logging.getLogger(__name__) REMOVE = object() -class PageType: +class PageType(merge.StrEnum): """ These page info classes are exactly the same as the CIX scheme, since it's unique @@ -61,15 +61,37 @@ class PageType: Deleted = "Deleted" -class ImageMetadata(TypedDict, total=False): +@dataclasses.dataclass +class PageMetadata: filename: str type: str bookmark: str - double_page: bool - image_index: Required[int] - size: str - height: str - width: str + display_index: int + archive_index: int + # These are optional because getting this info requires reading in each page + double_page: bool | None = None + byte_size: int | None = None + height: int | None = None + width: int | None = None + + def set_type(self, value: str) -> None: + values = {x.casefold(): x for x in PageType} + self.type = values.get(value.casefold(), value) + + def is_double_page(self) -> bool: + w = self.width or 0 + h = self.height or 0 + return self.double_page or (w >= h and w > 0 and h > 0) + + def __lt__(self, other: Any) -> bool: + if not isinstance(other, PageMetadata): + return False + return self.archive_index < other.archive_index + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, PageMetadata): + return False + return self.archive_index == other.archive_index Credit = merge.Credit @@ -150,13 +172,13 @@ class GenericMetadata: scan_info: str | None = None tags: set[str] = dataclasses.field(default_factory=set) - pages: list[ImageMetadata] = dataclasses.field(default_factory=list) + pages: list[PageMetadata] = dataclasses.field(default_factory=list) page_count: int | None = None characters: set[str] = dataclasses.field(default_factory=set) teams: set[str] = dataclasses.field(default_factory=set) locations: set[str] = dataclasses.field(default_factory=set) - credits: list[Credit] = dataclasses.field(default_factory=list) + credits: list[merge.Credit] = dataclasses.field(default_factory=list) # Some CoMet-only items price: float | None = None @@ -300,31 +322,34 @@ class GenericMetadata: self.page_count = assign(self.page_count, new_md.page_count) def apply_default_page_list(self, page_list: Sequence[str]) -> None: - # generate a default page list, with the first page marked as the cover + """apply a default page list, with the first page marked as the cover""" - # Create a dictionary of all pages in the metadata - pages = self.pages + # Create a dictionary in the weird case that the metadata doesn't match the archive + pages = {p.archive_index: p for p in self.pages} cover_set = False - # Go through each page in the archive - # The indexes should always match up - # It might be a good idea to validate that each page in `pages` is found + + # It might be a good idea to validate that each page in `pages` is found in page_list for i, filename in enumerate(page_list): - if i < len(pages): - pages[i]["filename"] = filename - else: - pages.append(ImageMetadata(image_index=i, filename=filename)) + page = pages.get(i, PageMetadata(archive_index=i, display_index=i, filename="", type="", bookmark="")) + page.filename = filename + pages[i] = page # Check if we know what the cover is - cover_set = pages[i].get("type", None) == PageType.FrontCover or cover_set + cover_set = page.type == PageType.FrontCover or cover_set + self.pages = sorted(pages.values()) - # Set the cover to the first image if we don't know what the cover is + self.page_count = len(self.pages) + if self.page_count != len(page_list): + logger.warning("Wrong count of pages: expected %d got %d", len(self.pages), len(page_list)) + # Set the cover to the first image acording to hte display index if we don't know what the cover is if not cover_set: - self.pages[0]["type"] = PageType.FrontCover + first_page = self.get_archive_page_index(0) + self.pages[first_page].type = PageType.FrontCover def get_archive_page_index(self, pagenum: int) -> int: - # convert the displayed page number to the page index of the file in the archive + """convert the displayed page number to the page index of the file in the archive""" if pagenum < len(self.pages): - return int(self.pages[pagenum]["image_index"]) + return int(sorted(self.pages, key=lambda p: p.display_index)[pagenum].archive_index) return 0 @@ -334,28 +359,28 @@ class GenericMetadata: return [0] coverlist = [] for p in self.pages: - if p.get("type", "") == PageType.FrontCover: - coverlist.append(int(p["image_index"])) + if p.type == PageType.FrontCover: + coverlist.append(p.archive_index) if len(coverlist) == 0: - coverlist.append(self.pages[0].get("image_index", 0)) + coverlist.append(self.get_archive_page_index(0)) return coverlist @overload - def add_credit(self, person: Credit) -> None: ... + def add_credit(self, person: merge.Credit) -> None: ... @overload def add_credit(self, person: str, role: str, primary: bool = False) -> None: ... - def add_credit(self, person: str | Credit, role: str | None = None, primary: bool = False) -> None: + def add_credit(self, person: str | merge.Credit, role: str | None = None, primary: bool = False) -> None: - credit: Credit - if isinstance(person, Credit): + credit: merge.Credit + if isinstance(person, merge.Credit): credit = person else: assert role is not None - credit = Credit(person=person, role=role, primary=primary) + credit = merge.Credit(person=person, role=role, primary=primary) if credit.role is None: raise TypeError("GenericMetadata.add_credit takes either a Credit object or a person name and role") @@ -526,46 +551,252 @@ md_test: GenericMetadata = GenericMetadata( teams={"Fahrenheit"}, locations=set(utils.split("lonely cottage ", ",")), credits=[ - Credit(primary=False, person="Dara Naraghi", role="Writer"), - Credit(primary=False, person="Esteve Polls", role="Penciller"), - Credit(primary=False, person="Esteve Polls", role="Inker"), - Credit(primary=False, person="Neil Uyetake", role="Letterer"), - Credit(primary=False, person="Sam Kieth", role="Cover"), - Credit(primary=False, person="Ted Adams", role="Editor"), + merge.Credit(primary=False, person="Dara Naraghi", role="Writer"), + merge.Credit(primary=False, person="Esteve Polls", role="Penciller"), + merge.Credit(primary=False, person="Esteve Polls", role="Inker"), + merge.Credit(primary=False, person="Neil Uyetake", role="Letterer"), + merge.Credit(primary=False, person="Sam Kieth", role="Cover"), + merge.Credit(primary=False, person="Ted Adams", role="Editor"), ], tags=set(), pages=[ - ImageMetadata( - image_index=0, height="1280", size="195977", width="800", type=PageType.FrontCover, filename="!cover.jpg" + PageMetadata( + archive_index=0, + display_index=0, + height=1280, + byte_size=195977, + width=800, + type=PageType.FrontCover, + filename="!cover.jpg", + bookmark="", ), - ImageMetadata(image_index=1, height="2039", size="611993", width="1327", filename="01.jpg"), - ImageMetadata(image_index=2, height="2039", size="783726", width="1327", filename="02.jpg"), - ImageMetadata(image_index=3, height="2039", size="679584", width="1327", filename="03.jpg"), - ImageMetadata(image_index=4, height="2039", size="788179", width="1327", filename="04.jpg"), - ImageMetadata(image_index=5, height="2039", size="864433", width="1327", filename="05.jpg"), - ImageMetadata(image_index=6, height="2039", size="765606", width="1327", filename="06.jpg"), - ImageMetadata(image_index=7, height="2039", size="876427", width="1327", filename="07.jpg"), - ImageMetadata(image_index=8, height="2039", size="852622", width="1327", filename="08.jpg"), - ImageMetadata(image_index=9, height="2039", size="800205", width="1327", filename="09.jpg"), - ImageMetadata(image_index=10, height="2039", size="746243", width="1326", filename="10.jpg"), - ImageMetadata(image_index=11, height="2039", size="718062", width="1327", filename="11.jpg"), - ImageMetadata(image_index=12, height="2039", size="532179", width="1326", filename="12.jpg"), - ImageMetadata(image_index=13, height="2039", size="686708", width="1327", filename="13.jpg"), - ImageMetadata(image_index=14, height="2039", size="641907", width="1327", filename="14.jpg"), - ImageMetadata(image_index=15, height="2039", size="805388", width="1327", filename="15.jpg"), - ImageMetadata(image_index=16, height="2039", size="668927", width="1326", filename="16.jpg"), - ImageMetadata(image_index=17, height="2039", size="710605", width="1327", filename="17.jpg"), - ImageMetadata(image_index=18, height="2039", size="761398", width="1326", filename="18.jpg"), - ImageMetadata(image_index=19, height="2039", size="743807", width="1327", filename="19.jpg"), - ImageMetadata(image_index=20, height="2039", size="552911", width="1326", filename="20.jpg"), - ImageMetadata(image_index=21, height="2039", size="556827", width="1327", filename="21.jpg"), - ImageMetadata(image_index=22, height="2039", size="675078", width="1326", filename="22.jpg"), - ImageMetadata( + PageMetadata( + archive_index=1, + display_index=1, + height=2039, + byte_size=611993, + width=1327, + filename="01.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=2, + display_index=2, + height=2039, + byte_size=783726, + width=1327, + filename="02.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=3, + display_index=3, + height=2039, + byte_size=679584, + width=1327, + filename="03.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=4, + display_index=4, + height=2039, + byte_size=788179, + width=1327, + filename="04.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=5, + display_index=5, + height=2039, + byte_size=864433, + width=1327, + filename="05.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=6, + display_index=6, + height=2039, + byte_size=765606, + width=1327, + filename="06.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=7, + display_index=7, + height=2039, + byte_size=876427, + width=1327, + filename="07.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=8, + display_index=8, + height=2039, + byte_size=852622, + width=1327, + filename="08.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=9, + display_index=9, + height=2039, + byte_size=800205, + width=1327, + filename="09.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=10, + display_index=10, + height=2039, + byte_size=746243, + width=1326, + filename="10.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=11, + display_index=11, + height=2039, + byte_size=718062, + width=1327, + filename="11.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=12, + display_index=12, + height=2039, + byte_size=532179, + width=1326, + filename="12.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=13, + display_index=13, + height=2039, + byte_size=686708, + width=1327, + filename="13.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=14, + display_index=14, + height=2039, + byte_size=641907, + width=1327, + filename="14.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=15, + display_index=15, + height=2039, + byte_size=805388, + width=1327, + filename="15.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=16, + display_index=16, + height=2039, + byte_size=668927, + width=1326, + filename="16.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=17, + display_index=17, + height=2039, + byte_size=710605, + width=1327, + filename="17.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=18, + display_index=18, + height=2039, + byte_size=761398, + width=1326, + filename="18.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=19, + display_index=19, + height=2039, + byte_size=743807, + width=1327, + filename="19.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=20, + display_index=20, + height=2039, + byte_size=552911, + width=1326, + filename="20.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=21, + display_index=21, + height=2039, + byte_size=556827, + width=1327, + filename="21.jpg", + bookmark="", + type="", + ), + PageMetadata( + archive_index=22, + display_index=22, + height=2039, + byte_size=675078, + width=1326, + filename="22.jpg", + bookmark="", + type="", + ), + PageMetadata( bookmark="Interview", - image_index=23, - height="2032", - size="800965", - width="1338", + archive_index=23, + display_index=23, + height=2032, + byte_size=800965, + width=1338, type=PageType.Letters, filename="23.jpg", ), @@ -583,7 +814,7 @@ __all__ = ( "Url", "parse_url", "PageType", - "ImageMetadata", + "PageMetadata", "Credit", "ComicSeries", "MetadataOrigin", diff --git a/comicapi/tags/comet.py b/comicapi/tags/comet.py index a6306cb..c69d7ec 100644 --- a/comicapi/tags/comet.py +++ b/comicapi/tags/comet.py @@ -24,7 +24,7 @@ from typing import Any from comicapi import utils from comicapi.archivers import Archiver from comicapi.comicarchive import ComicArchive -from comicapi.genericmetadata import GenericMetadata, ImageMetadata, PageType +from comicapi.genericmetadata import GenericMetadata, PageMetadata, PageType from comicapi.tags import Tag logger = logging.getLogger(__name__) @@ -194,8 +194,8 @@ class CoMet(Tag): date_str += f"-{md.month:02}" assign("date", date_str) - page = md.get_cover_page_index_list()[0] - assign("coverImage", md.pages[page]["filename"]) + cover_index = md.get_cover_page_index_list()[0] + assign("coverImage", md.pages[cover_index].filename) # loop thru credits, and build a list for each role that CoMet supports for credit in metadata.credits: @@ -265,7 +265,15 @@ class CoMet(Tag): page_list = ca.get_page_name_list() if cover_filename in page_list: cover_index = page_list.index(cover_filename) - md.pages = [ImageMetadata(image_index=cover_index, filename=cover_filename, type=PageType.FrontCover)] + md.pages = [ + PageMetadata( + archive_index=cover_index, + display_index=0, + filename=cover_filename, + type=PageType.FrontCover, + bookmark="", + ) + ] reading_direction = utils.xlate(get("readingDirection")) if reading_direction is not None and reading_direction == "rtl": diff --git a/comicapi/tags/comicrack.py b/comicapi/tags/comicrack.py index eb11422..8399d00 100644 --- a/comicapi/tags/comicrack.py +++ b/comicapi/tags/comicrack.py @@ -17,12 +17,11 @@ from __future__ import annotations import logging import xml.etree.ElementTree as ET -from collections import OrderedDict from typing import Any from comicapi import utils from comicapi.archivers import Archiver -from comicapi.genericmetadata import GenericMetadata, ImageMetadata +from comicapi.genericmetadata import GenericMetadata, PageMetadata from comicapi.tags import Tag logger = logging.getLogger(__name__) @@ -253,24 +252,23 @@ class ComicRack(Tag): else: pages_node = ET.SubElement(root, "Pages") - for page_dict in md.pages: + for page in md.pages: page_node = ET.SubElement(pages_node, "Page") - page_node.attrib = {} - if "bookmark" in page_dict: - page_node.attrib["Bookmark"] = str(page_dict["bookmark"]) - if "double_page" in page_dict: - page_node.attrib["DoublePage"] = str(page_dict["double_page"]) - if "image_index" in page_dict: - page_node.attrib["Image"] = str(page_dict["image_index"]) - if "height" in page_dict: - page_node.attrib["ImageHeight"] = str(page_dict["height"]) - if "size" in page_dict: - page_node.attrib["ImageSize"] = str(page_dict["size"]) - if "width" in page_dict: - page_node.attrib["ImageWidth"] = str(page_dict["width"]) - if "type" in page_dict: - page_node.attrib["Type"] = str(page_dict["type"]) - page_node.attrib = OrderedDict(sorted(page_node.attrib.items())) + page_node.attrib = {"Image": str(page.display_index)} + if page.bookmark: + page_node.attrib["Bookmark"] = page.bookmark + if page.type: + page_node.attrib["Type"] = page.type + + if page.double_page is not None: + page_node.attrib["DoublePage"] = str(page.double_page) + if page.height is not None: + page_node.attrib["ImageHeight"] = str(page.height) + if page.byte_size is not None: + page_node.attrib["ImageSize"] = str(page.byte_size) + if page.width is not None: + page_node.attrib["ImageWidth"] = str(page.width) + page_node.attrib = dict(sorted(page_node.attrib.items())) ET.indent(root) @@ -352,20 +350,23 @@ class ComicRack(Tag): if pages_node is not None: for i, page in enumerate(pages_node): p: dict[str, Any] = page.attrib - md_page = ImageMetadata(image_index=int(p.get("Image", i))) + md_page = PageMetadata( + filename="", # cr doesn't record the filename it just assumes it's always ordered the same + display_index=int(p.get("Image", i)), + archive_index=i, + bookmark=p.get("Bookmark", ""), + type="", + ) + md_page.set_type(p.get("Type", "")) - if "Bookmark" in p: - md_page["bookmark"] = p["Bookmark"] - if "DoublePage" in p: - md_page["double_page"] = True if p["DoublePage"].casefold() in ("yes", "true", "1") else False - if "ImageHeight" in p: - md_page["height"] = p["ImageHeight"] - if "ImageSize" in p: - md_page["size"] = p["ImageSize"] - if "ImageWidth" in p: - md_page["width"] = p["ImageWidth"] - if "Type" in p: - md_page["type"] = p["Type"] + if isinstance(p.get("DoublePage", None), str): + md_page.double_page = p["DoublePage"].casefold() in ("yes", "true", "1") + if p.get("ImageHeight", "").isnumeric(): + md_page.height = int(float(p["ImageHeight"])) + if p.get("ImageWidth", "").isnumeric(): + md_page.width = int(float(p["ImageWidth"])) + if p.get("ImageSize", "").isnumeric(): + md_page.byte_size = int(float(p["ImageSize"])) md.pages.append(md_page) diff --git a/comicapi/utils.py b/comicapi/utils.py index a7165b8..a6b6831 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -26,7 +26,7 @@ from collections import defaultdict from collections.abc import Iterable, Mapping from enum import Enum, auto from shutil import which # noqa: F401 -from typing import TYPE_CHECKING, Any, TypeVar, cast +from typing import Any, TypeVar, cast from comicfn2dict import comicfn2dict @@ -36,8 +36,6 @@ from comicapi._url import LocationParseError as LocationParseError # noqa: F401 from comicapi._url import Url as Url from comicapi._url import parse_url as parse_url -if TYPE_CHECKING: - from comicapi.genericmetadata import ImageMetadata try: import icu @@ -131,12 +129,6 @@ def _custom_key(tup: Any) -> Any: T = TypeVar("T") -def is_double_page(page: ImageMetadata) -> bool: - w = int(page.get("width", 0)) - h = int(page.get("height", 0)) - return page.get("double_page") or (w >= h and w > 0 and h > 0) - - def os_sorted(lst: Iterable[T]) -> Iterable[T]: import natsort diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index ab15837..4165b0c 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -399,7 +399,7 @@ class IssueIdentifier: assert md covers: list[tuple[str, Image.Image]] = [] for cover_index in range(1, min(3, ca.get_number_of_pages())): - image_data = ca.get_page(cover_index) + image_data = ca.get_page(md.get_archive_page_index(cover_index)) covers.extend(self._process_cover(f"{cover_index}", image_data)) return covers diff --git a/comictaggerlib/pagebrowser.py b/comictaggerlib/pagebrowser.py index a6cec10..4316d36 100644 --- a/comictaggerlib/pagebrowser.py +++ b/comictaggerlib/pagebrowser.py @@ -102,9 +102,9 @@ class PageBrowserWindow(QtWidgets.QDialog): def set_page(self) -> None: if not self.metadata.is_empty: - archive_page_index = self.metadata.get_archive_page_index(self.current_page_num) + selected_page_index = self.metadata.get_archive_page_index(self.current_page_num) else: - archive_page_index = self.current_page_num + selected_page_index = self.current_page_num - self.pageWidget.set_page(archive_page_index) - self.setWindowTitle(f"Page Browser - Page {self.current_page_num + 1} (of {self.page_count}) ") + self.pageWidget.set_page(selected_page_index) + self.setWindowTitle(f"Page Browser - Page {self.current_page_num+1} (of {self.page_count})") diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index b95294d..4e22c74 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -21,7 +21,7 @@ import logging from PyQt5 import QtCore, QtWidgets, uic from comicapi.comicarchive import ComicArchive, tags -from comicapi.genericmetadata import GenericMetadata, ImageMetadata, PageType +from comicapi.genericmetadata import GenericMetadata, PageMetadata, PageType from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import enable_widget @@ -118,7 +118,7 @@ class PageListEditor(QtWidgets.QWidget): self.first_front_page: int | None = None self.comic_archive: ComicArchive | None = None - self.pages_list: list[ImageMetadata] = [] + self.pages_list: list[PageMetadata] = [] self.tag_ids: list[str] = [] def set_blur(self, blur: bool) -> None: @@ -156,23 +156,23 @@ class PageListEditor(QtWidgets.QWidget): row = self.comic_archive.get_scanner_page_index() if row is None: return - page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) + page: PageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) - page_dict["type"] = PageType.Deleted + page.type = PageType.Deleted item = self.listWidget.item(row) - item.setData(QtCore.Qt.ItemDataRole.UserRole, page_dict) - item.setText(self.list_entry_text(page_dict)) + item.setData(QtCore.Qt.ItemDataRole.UserRole, page) + item.setText(self.list_entry_text(page)) self.change_page() def identify_double_page(self) -> None: if self.comic_archive is None: return md = GenericMetadata(pages=self.get_page_list()) - double_pages = [x.get("double_page", False) for x in md.pages] + double_pages = [bool(x.double_page) for x in md.pages] self.comic_archive.apply_archive_info_to_metadata(md, True, True) self.set_data(self.comic_archive, pages_list=md.pages) - if double_pages != [x.get("double_page", False) for x in md.pages]: + if double_pages != [bool(x.double_page) for x in md.pages]: self.modified.emit() def select_page_type_item(self, idx: int) -> None: @@ -272,93 +272,81 @@ class PageListEditor(QtWidgets.QWidget): i = self.cbPageType.findData(pagetype) self.cbPageType.setCurrentIndex(i) - page = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) - self.chkDoublePage.setChecked(page.get("double_page", False)) + page: PageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) + self.chkDoublePage.setChecked(bool(page.double_page)) - self.leBookmark.setText(page.get("bookmark", "")) - - idx = int(page["image_index"]) + self.leBookmark.setText(page.bookmark) if self.comic_archive is not None: - self.pageWidget.set_archive(self.comic_archive, idx) + self.pageWidget.set_archive(self.comic_archive, page.archive_index) def get_first_front_cover(self) -> int: front_cover = 0 if self.listWidget.count() > 0: - front_cover = int(self.listWidget.item(0).data(QtCore.Qt.ItemDataRole.UserRole).get("image_index", 0)) + page: PageMetadata = self.listWidget.item(0).data(QtCore.Qt.ItemDataRole.UserRole) + front_cover = page.archive_index for i in range(self.listWidget.count()): item = self.listWidget.item(i) - page_dict: ImageMetadata = item.data(QtCore.Qt.ItemDataRole.UserRole) - if page_dict.get("type", "") == PageType.FrontCover: - front_cover = int(page_dict["image_index"]) + page = item.data(QtCore.Qt.ItemDataRole.UserRole) + if page.type == PageType.FrontCover: + front_cover = page.archive_index break return front_cover def get_current_page_type(self) -> str: row = self.listWidget.currentRow() - page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) - return page_dict.get("type", "") + page: PageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) + return page.type def set_current_page_type(self, t: str) -> None: rows = self.listWidget.selectionModel().selectedRows() for index in rows: row = index.row() - page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) + page: PageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) - if t == "": - if "type" in page_dict: - del page_dict["type"] - else: - page_dict["type"] = t + page.type = t item = self.listWidget.item(row) - item.setData(QtCore.Qt.ItemDataRole.UserRole, page_dict) - item.setText(self.list_entry_text(page_dict)) + item.setData(QtCore.Qt.ItemDataRole.UserRole, page) + item.setText(self.list_entry_text(page)) def toggle_double_page(self) -> None: rows = self.listWidget.selectionModel().selectedRows() for index in rows: row = index.row() - page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) + page: PageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) cbx = self.sender() - if isinstance(cbx, QtWidgets.QCheckBox) and cbx.isChecked(): - if not page_dict.get("double_page", False): - page_dict["double_page"] = True - self.modified.emit() - elif "double_page" in page_dict: - del page_dict["double_page"] + if isinstance(cbx, QtWidgets.QCheckBox): + page.double_page = cbx.isChecked() self.modified.emit() item = self.listWidget.item(row) - item.setData(QtCore.Qt.ItemDataRole.UserRole, page_dict) - item.setText(self.list_entry_text(page_dict)) + item.setData(QtCore.Qt.ItemDataRole.UserRole, page) + item.setText(self.list_entry_text(page)) self.listWidget.setFocus() def save_bookmark(self) -> None: row = self.listWidget.currentRow() - page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) + page: PageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole) - current_bookmark = page_dict.get("bookmark", "") + previous_bookmark = page.bookmark + new_bookmark = self.leBookmark.text().strip() - if self.leBookmark.text().strip(): - new_bookmark = str(self.leBookmark.text().strip()) - if current_bookmark != new_bookmark: - page_dict["bookmark"] = new_bookmark - self.modified.emit() - elif current_bookmark != "": - del page_dict["bookmark"] - self.modified.emit() + if previous_bookmark == new_bookmark: + return + page.bookmark = new_bookmark + self.modified.emit() item = self.listWidget.item(row) - item.setData(QtCore.Qt.ItemDataRole.UserRole, page_dict) - item.setText(self.list_entry_text(page_dict)) + item.setData(QtCore.Qt.ItemDataRole.UserRole, page) + item.setText(self.list_entry_text(page)) self.listWidget.setFocus() - def set_data(self, comic_archive: ComicArchive, pages_list: list[ImageMetadata]) -> None: + def set_data(self, comic_archive: ComicArchive, pages_list: list[PageMetadata]) -> None: self.cbxBlur.setChecked(self.blur) self.comic_archive = comic_archive self.pages_list = pages_list @@ -372,7 +360,7 @@ class PageListEditor(QtWidgets.QWidget): self.listWidget.itemSelectionChanged.disconnect(self.change_page) self.listWidget.clear() - for p in pages_list: + for p in sorted(pages_list, key=lambda p: p.display_index): item = QtWidgets.QListWidgetItem(self.list_entry_text(p)) item.setData(QtCore.Qt.ItemDataRole.UserRole, p) @@ -381,24 +369,26 @@ class PageListEditor(QtWidgets.QWidget): self.listWidget.itemSelectionChanged.connect(self.change_page) self.listWidget.setCurrentRow(0) - def list_entry_text(self, page_dict: ImageMetadata) -> str: - text = str(int(page_dict["image_index"]) + 1) - if page_type := page_dict.get("type", ""): - if page_type in self.pageTypeNames: - text += " (" + self.pageTypeNames[page_type] + ")" + def list_entry_text(self, page: PageMetadata) -> str: + # indexes start at 0 but we display starting at 1. This should be consistent for all indexes in ComicTagger + text = str(int(page.archive_index) + 1) + if page.type: + if page.type.casefold() in {x.casefold() for x in PageType}: + text += " (" + self.pageTypeNames[PageType(page.type)] + ")" else: - text += " (Error: " + page_type + ")" - if page_dict.get("double_page", False): + text += f" (Unknown: {page.type})" + if page.double_page: text += " ②" - if page_dict.get("bookmark", ""): + if page.bookmark: text += " 🔖" return text - def get_page_list(self) -> list[ImageMetadata]: - page_list = [] + def get_page_list(self) -> list[PageMetadata]: + page_list: list[PageMetadata] = [] for i in range(self.listWidget.count()): item = self.listWidget.item(i) page_list.append(item.data(QtCore.Qt.ItemDataRole.UserRole)) + page_list[i].display_index = i return page_list def emit_front_cover_change(self) -> None: diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index b65c92d..b060bcb 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -2068,12 +2068,9 @@ class TaggerWindow(QtWidgets.QMainWindow): def recalc_page_dimensions(self) -> None: QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) for p in self.metadata.pages: - if "size" in p: - del p["size"] - if "height" in p: - del p["height"] - if "width" in p: - del p["width"] + p.byte_size = None + p.height = None + p.width = None self.set_dirty_flag() QtWidgets.QApplication.restoreOverrideCursor() diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index fb801d7..12a009d 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -34,7 +34,7 @@ def test_getPageNameList(): def test_page_type_read(cbz): md = cbz.read_tags("cr") - assert isinstance(md.pages[0]["type"], str) + assert md.pages[0].type == comicapi.genericmetadata.PageType.FrontCover def test_read_tags(cbz, md_saved): @@ -94,7 +94,7 @@ def test_save_cbi_rar(tmp_path, md_saved): def test_page_type_write(tmp_comic): md = tmp_comic.read_tags("cr") t = md.pages[0] - t["type"] = "" + t.type = "" assert tmp_comic.write_tags(md, "cr") diff --git a/tests/genericmetadata_test.py b/tests/genericmetadata_test.py index 2d7c15b..062e58d 100644 --- a/tests/genericmetadata_test.py +++ b/tests/genericmetadata_test.py @@ -15,7 +15,8 @@ def test_apply_default_page_list(tmp_path): md.pages = [] md.apply_default_page_list(["testing"]) - assert isinstance(md.pages[0]["image_index"], int) + assert md.pages[0].display_index == 0 + assert md.pages[0].archive_index == 0 @pytest.mark.parametrize("md, new, expected", testing.comicdata.metadata)