From ae5e246180ca633ce6d547ca5ceb39ba10f45923 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 17 Dec 2023 21:47:43 -0800 Subject: [PATCH 01/15] Add plugin support for metadata --- comicapi/archivers/archiver.py | 8 + comicapi/archivers/folder.py | 3 + comicapi/archivers/rar.py | 6 + comicapi/archivers/sevenzip.py | 3 + comicapi/archivers/zip.py | 6 + comicapi/comicarchive.py | 368 +++-------------- comicapi/genericmetadata.py | 109 +++-- comicapi/metadata/__init__.py | 5 + comicapi/{ => metadata}/comet.py | 143 +++++-- comicapi/{ => metadata}/comicbookinfo.py | 98 ++++- .../comicrack.py} | 215 +++++++--- comicapi/metadata/metadata.py | 118 ++++++ comictaggerlib/autotagmatchwindow.py | 6 +- comictaggerlib/autotagstartwindow.py | 4 +- comictaggerlib/cli.py | 149 +++---- comictaggerlib/ctsettings/commandline.py | 5 +- comictaggerlib/ctsettings/file.py | 5 +- .../ctsettings/settngs_namespace.py | 10 +- comictaggerlib/ctsettings/types.py | 12 +- comictaggerlib/fileselectionlist.py | 39 +- comictaggerlib/issueidentifier.py | 10 +- comictaggerlib/main.py | 1 + comictaggerlib/pagelisteditor.py | 120 +++--- comictaggerlib/renamewindow.py | 7 +- comictaggerlib/resulttypes.py | 4 +- comictaggerlib/taggerwindow.py | 386 ++++++++---------- comictaggerlib/ui/fileselectionlist.ui | 15 +- comictaggerlib/ui/qtutils.py | 61 +++ comictaggerlib/ui/taggerwindow.ui | 4 - setup.cfg | 4 + tests/comicarchive_test.py | 68 ++- tests/genericmetadata_test.py | 2 +- tests/integration_test.py | 17 +- tests/metadata_test.py | 73 +--- 34 files changed, 1059 insertions(+), 1025 deletions(-) create mode 100644 comicapi/metadata/__init__.py rename comicapi/{ => metadata}/comet.py (57%) rename comicapi/{ => metadata}/comicbookinfo.py (63%) rename comicapi/{comicinfoxml.py => metadata/comicrack.py} (55%) create mode 100644 comicapi/metadata/metadata.py diff --git a/comicapi/archivers/archiver.py b/comicapi/archivers/archiver.py index 51d1a68..5a94c76 100644 --- a/comicapi/archivers/archiver.py +++ b/comicapi/archivers/archiver.py @@ -81,6 +81,14 @@ class Archiver(Protocol): """ return [] + def supports_files(self) -> bool: + """ + Returns True if the current archive supports arbitrary non-picture files. + Should always return a boolean. + If arbitrary non-picture files are not supported in the archive False should be returned. + """ + return False + def copy_from_archive(self, other_archive: Archiver) -> bool: """ Copies the contents of another achive to the current archive. diff --git a/comicapi/archivers/folder.py b/comicapi/archivers/folder.py index c7341b0..9a3c0c3 100644 --- a/comicapi/archivers/folder.py +++ b/comicapi/archivers/folder.py @@ -72,6 +72,9 @@ class FolderArchiver(Archiver): logger.error("Error listing files in folder archive [%s]: %s", e, self.path) return [] + def supports_files(self) -> bool: + return True + def copy_from_archive(self, other_archive: Archiver) -> bool: """Replace the current zip with one copied from another archive""" try: diff --git a/comicapi/archivers/rar.py b/comicapi/archivers/rar.py index 6bac3e9..b0f7d53 100644 --- a/comicapi/archivers/rar.py +++ b/comicapi/archivers/rar.py @@ -91,6 +91,9 @@ class RarArchiver(Archiver): else: return False + def supports_comment(self) -> bool: + return True + def read_file(self, archive_file: str) -> bytes: rarc = self.get_rar_obj() if rarc is None: @@ -213,6 +216,9 @@ class RarArchiver(Archiver): return namelist return [] + def supports_files(self) -> bool: + return True + def copy_from_archive(self, other_archive: Archiver) -> bool: """Replace the current archive with one copied from another archive""" try: diff --git a/comicapi/archivers/sevenzip.py b/comicapi/archivers/sevenzip.py index be6e059..3dd89f1 100644 --- a/comicapi/archivers/sevenzip.py +++ b/comicapi/archivers/sevenzip.py @@ -75,6 +75,9 @@ class SevenZipArchiver(Archiver): logger.error("Error listing files in 7zip archive [%s]: %s", e, self.path) return [] + def supports_files(self) -> bool: + return True + def rebuild(self, exclude_list: list[str]) -> bool: """Zip helper func diff --git a/comicapi/archivers/zip.py b/comicapi/archivers/zip.py index 5ec3ba2..519f483 100644 --- a/comicapi/archivers/zip.py +++ b/comicapi/archivers/zip.py @@ -23,6 +23,9 @@ class ZipArchiver(Archiver): def __init__(self) -> None: super().__init__() + def supports_comment(self) -> bool: + return True + def get_comment(self) -> str: with zipfile.ZipFile(self.path, "r") as zf: encoding = chardet.detect(zf.comment, True) @@ -79,6 +82,9 @@ class ZipArchiver(Archiver): logger.error("Error listing files in zip archive [%s]: %s", e, self.path) return [] + def supports_files(self) -> bool: + return True + def rebuild(self, exclude_list: list[str]) -> bool: """Zip helper func diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 54e1624..125d351 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -25,14 +25,14 @@ from typing import cast from comicapi import utils from comicapi.archivers import Archiver, UnknownArchiver, ZipArchiver -from comicapi.comet import CoMet -from comicapi.comicbookinfo import ComicBookInfo -from comicapi.comicinfoxml import ComicInfoXml -from comicapi.genericmetadata import GenericMetadata, PageType +from comicapi.genericmetadata import GenericMetadata +from comicapi.metadata import Metadata +from comictaggerlib.ctversion import version logger = logging.getLogger(__name__) archivers: list[type[Archiver]] = [] +metadata_styles: dict[str, Metadata] = {} def load_archive_plugins() -> None: @@ -54,12 +54,27 @@ def load_archive_plugins() -> None: archivers.extend(builtin) -class MetaDataStyle: - CBI = 0 - CIX = 1 - COMET = 2 - name = ["ComicBookLover", "ComicRack", "CoMet"] - short_name = ["cbl", "cr", "comet"] +def load_metadata_plugins(version: str = f"ComicAPI/{version}") -> None: + if not metadata_styles: + if sys.version_info < (3, 10): + from importlib_metadata import entry_points + else: + from importlib.metadata import entry_points + builtin: dict[str, Metadata] = {} + styles: dict[str, Metadata] = {} + for arch in entry_points(group="comicapi.metadata"): + try: + style: type[Metadata] = arch.load() + if style.enabled: + if arch.module.startswith("comicapi"): + builtin[style.short_name] = style(version) + else: + styles[style.short_name] = style(version) + except Exception: + logger.exception("Failed to load metadata plugin: %s", arch.name) + metadata_styles.clear() + metadata_styles.update(builtin) + metadata_styles.update(styles) class ComicArchive: @@ -67,25 +82,18 @@ class ComicArchive: pil_available = True def __init__(self, path: pathlib.Path | str, default_image_path: pathlib.Path | str | None = None) -> None: - self.cbi_md: GenericMetadata | None = None - self.cix_md: GenericMetadata | None = None - self.comet_filename: str | None = None - self.comet_md: GenericMetadata | None = None - self._has_cbi: bool | None = None - self._has_cix: bool | None = None - self._has_comet: bool | None = None + self.md: dict[str, GenericMetadata] = {} self.path = pathlib.Path(path).absolute() self.page_count: int | None = None self.page_list: list[str] = [] - self.ci_xml_filename = "ComicInfo.xml" - self.comet_default_filename = "CoMet.xml" self.reset_cache() self.default_image_path = default_image_path self.archiver: Archiver = UnknownArchiver.open(self.path) load_archive_plugins() + load_metadata_plugins() for archiver in archivers: if archiver.enabled and archiver.is_valid(self.path): self.archiver = archiver.open(self.path) @@ -98,19 +106,19 @@ class ComicArchive: def reset_cache(self) -> None: """Clears the cached data""" - self._has_cix = None - self._has_cbi = None - self._has_comet = None - self.comet_filename = None self.page_count = None - self.page_list = [] - self.cix_md = None - self.cbi_md = None - self.comet_md = None + self.page_list.clear() + self.md.clear() - def load_cache(self, style_list: list[int]) -> None: + def load_cache(self, style_list: list[str]) -> None: for style in style_list: - self.read_metadata(style) + if style in metadata_styles: + md = metadata_styles[style].get_metadata(self.archiver) + if not md.is_empty: + self.md[style] = md + + def get_supported_metadata(self) -> list[str]: + return [style[0] for style in metadata_styles.items() if style[1].supports_metadata(self.archiver)] def rename(self, path: pathlib.Path | str) -> None: new_path = pathlib.Path(path).absolute() @@ -133,8 +141,10 @@ class ComicArchive: return True - def is_writable_for_style(self, data_style: int) -> bool: - return not (data_style == MetaDataStyle.CBI and not self.archiver.supports_comment()) + def is_writable_for_style(self, style: str) -> bool: + if style in metadata_styles: + return self.archiver.is_writable() and metadata_styles[style].supports_metadata(self.archiver) + return False def is_zip(self) -> bool: return self.archiver.name() == "ZIP" @@ -148,43 +158,28 @@ class ComicArchive: def extension(self) -> str: return self.archiver.extension() - def read_metadata(self, style: int) -> GenericMetadata: - if style == MetaDataStyle.CIX: - return self.read_cix() - if style == MetaDataStyle.CBI: - return self.read_cbi() - if style == MetaDataStyle.COMET: - return self.read_comet() - return GenericMetadata() + def read_metadata(self, style: str) -> GenericMetadata: + if style in self.md: + return self.md[style] + return metadata_styles[style].get_metadata(self.archiver) - def write_metadata(self, metadata: GenericMetadata, style: int) -> bool: - retcode = False - if style == MetaDataStyle.CIX: - retcode = self.write_cix(metadata) - if style == MetaDataStyle.CBI: - retcode = self.write_cbi(metadata) - if style == MetaDataStyle.COMET: - retcode = self.write_comet(metadata) - return retcode + def read_metadata_string(self, style: str) -> str: + return metadata_styles[style].get_metadata_string(self.archiver) - def has_metadata(self, style: int) -> bool: - if style == MetaDataStyle.CIX: - return self.has_cix() - if style == MetaDataStyle.CBI: - return self.has_cbi() - if style == MetaDataStyle.COMET: - return self.has_comet() - return False + def write_metadata(self, metadata: GenericMetadata, style: str) -> bool: + if style in self.md: + del self.md[style] + return metadata_styles[style].set_metadata(metadata, self.archiver) - def remove_metadata(self, style: int) -> bool: - retcode = True - if style == MetaDataStyle.CIX: - retcode = self.remove_cix() - elif style == MetaDataStyle.CBI: - retcode = self.remove_cbi() - elif style == MetaDataStyle.COMET: - retcode = self.remove_co_met() - return retcode + def has_metadata(self, style: str) -> bool: + if style in self.md: + return True + return metadata_styles[style].has_metadata(self.archiver) + + def remove_metadata(self, style: str) -> bool: + if style in self.md: + del self.md[style] + return metadata_styles[style].remove_metadata(self.archiver) def get_page(self, index: int) -> bytes: image_data = b"" @@ -288,239 +283,12 @@ class ComicArchive: self.page_count = len(self.get_page_name_list()) return self.page_count - def read_cbi(self) -> GenericMetadata: - if self.cbi_md is None: - raw_cbi = self.read_raw_cbi() - if raw_cbi: - self.cbi_md = ComicBookInfo().metadata_from_string(raw_cbi) - else: - self.cbi_md = GenericMetadata() - - self.cbi_md.set_default_page_list(self.get_number_of_pages()) - - return self.cbi_md - - def read_raw_cbi(self) -> str: - if not self.has_cbi(): - return "" - - return self.archiver.get_comment() - - def has_cbi(self) -> bool: - if self._has_cbi is None: - if not self.seems_to_be_a_comic_archive(): - self._has_cbi = False - else: - comment = self.archiver.get_comment() - self._has_cbi = ComicBookInfo().validate_string(comment) - - return self._has_cbi - - def write_cbi(self, metadata: GenericMetadata) -> bool: - if metadata is not None: - try: - self.apply_archive_info_to_metadata(metadata) - cbi_string = ComicBookInfo().string_from_metadata(metadata) - write_success = self.archiver.set_comment(cbi_string) - if write_success: - self._has_cbi = True - self.cbi_md = metadata - self.reset_cache() - return write_success - except Exception as e: - tb = traceback.extract_tb(e.__traceback__) - logger.error("%s:%s: Error saving CBI! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e) - - return False - - def remove_cbi(self) -> bool: - if self.has_cbi(): - write_success = self.archiver.set_comment("") - if write_success: - self._has_cbi = False - self.cbi_md = None - self.reset_cache() - return write_success - return True - - def read_cix(self) -> GenericMetadata: - if self.cix_md is None: - raw_cix = self.read_raw_cix() - if raw_cix: - self.cix_md = ComicInfoXml().metadata_from_string(raw_cix) - else: - self.cix_md = GenericMetadata() - - # validate the existing page list (make sure count is correct) - if len(self.cix_md.pages) != 0: - if len(self.cix_md.pages) != self.get_number_of_pages(): - # pages array doesn't match the actual number of images we're seeing - # in the archive, so discard the data - self.cix_md.pages = [] - - if len(self.cix_md.pages) == 0: - self.cix_md.set_default_page_list(self.get_number_of_pages()) - - return self.cix_md - - def read_raw_cix(self) -> bytes: - if not self.has_cix(): - return b"" - try: - raw_cix = self.archiver.read_file(self.ci_xml_filename) or b"" - except Exception as e: - tb = traceback.extract_tb(e.__traceback__) - logger.error("%s:%s: Error reading in raw CIX! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e) - raw_cix = b"" - return raw_cix - - def write_cix(self, metadata: GenericMetadata) -> bool: - if metadata is not None: - try: - self.apply_archive_info_to_metadata(metadata, calc_page_sizes=True) - raw_cix = self.read_raw_cix() - cix_string = ComicInfoXml().string_from_metadata(metadata, xml=raw_cix) - write_success = self.archiver.write_file(self.ci_xml_filename, cix_string.encode("utf-8")) - if write_success: - self._has_cix = True - self.cix_md = metadata - self.reset_cache() - return write_success - except Exception as e: - tb = traceback.extract_tb(e.__traceback__) - logger.error("%s:%s: Error saving CIX! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e) - - return False - - def remove_cix(self) -> bool: - if self.has_cix(): - write_success = self.archiver.remove_file(self.ci_xml_filename) - if write_success: - self._has_cix = False - self.cix_md = None - self.reset_cache() - return write_success - return True - - def has_cix(self) -> bool: - if self._has_cix is None: - if not self.seems_to_be_a_comic_archive(): - self._has_cix = False - elif self.ci_xml_filename in self.archiver.get_filename_list(): - self._has_cix = True - else: - self._has_cix = False - return self._has_cix - - def read_comet(self) -> GenericMetadata: - if self.comet_md is None: - raw_comet = self.read_raw_comet() - if raw_comet is None or raw_comet == "": - self.comet_md = GenericMetadata() - else: - self.comet_md = CoMet().metadata_from_string(raw_comet) - - self.comet_md.set_default_page_list(self.get_number_of_pages()) - # use the coverImage value from the comet_data to mark the cover in this struct - # walk through list of images in file, and find the matching one for md.coverImage - # need to remove the existing one in the default - if self.comet_md._cover_image is not None: - cover_idx = 0 - for idx, f in enumerate(self.get_page_name_list()): - if self.comet_md._cover_image == f: - cover_idx = idx - break - if cover_idx != 0: - del self.comet_md.pages[0]["Type"] - self.comet_md.pages[cover_idx]["Type"] = PageType.FrontCover - - return self.comet_md - - def read_raw_comet(self) -> str: - raw_comet = "" - if not self.has_comet(): - raw_comet = "" - else: - try: - raw_bytes = self.archiver.read_file(cast(str, self.comet_filename)) - if raw_bytes: - raw_comet = raw_bytes.decode("utf-8") - except OSError as e: - tb = traceback.extract_tb(e.__traceback__) - logger.error( - "%s:%s: Error reading in raw CoMet! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e - ) - return raw_comet - - def write_comet(self, metadata: GenericMetadata) -> bool: - if metadata is not None: - if not self.has_comet(): - self.comet_filename = self.comet_default_filename - - self.apply_archive_info_to_metadata(metadata) - # Set the coverImage value, if it's not the first page - cover_idx = int(metadata.get_cover_page_index_list()[0]) - if cover_idx != 0: - metadata._cover_image = self.get_page_name(cover_idx) - - comet_string = CoMet().string_from_metadata(metadata) - write_success = self.archiver.write_file(cast(str, self.comet_filename), comet_string.encode("utf-8")) - if write_success: - self._has_comet = True - self.comet_md = metadata - self.reset_cache() - return write_success - - return False - - def remove_co_met(self) -> bool: - if self.has_comet(): - write_success = self.archiver.remove_file(cast(str, self.comet_filename)) - if write_success: - self._has_comet = False - self.comet_md = None - self.reset_cache() - return write_success - return True - - def has_comet(self) -> bool: - if self._has_comet is None: - self._has_comet = False - if not self.seems_to_be_a_comic_archive(): - return self._has_comet - - # look at all xml files in root, and search for CoMet data, get first - for n in self.archiver.get_filename_list(): - if os.path.dirname(n) == "" and os.path.splitext(n)[1].casefold() == ".xml": - # read in XML file, and validate it - data = "" - try: - d = self.archiver.read_file(n) - if d: - data = d.decode("utf-8") - except Exception as e: - tb = traceback.extract_tb(e.__traceback__) - logger.warning( - "%s:%s: Error reading in Comet XML for validation! from %s: %s", - tb[1].filename, - tb[1].lineno, - self.path, - e, - ) - if CoMet().validate_string(data): - # since we found it, save it! - self.comet_filename = n - self._has_comet = True - break - - return self._has_comet - def apply_archive_info_to_metadata(self, md: GenericMetadata, calc_page_sizes: bool = False) -> None: md.page_count = self.get_number_of_pages() if calc_page_sizes: for index, p in enumerate(md.pages): - idx = int(p["Image"]) + idx = int(p["image_index"]) if self.pil_available: try: from PIL import Image @@ -528,7 +296,7 @@ class ComicArchive: self.pil_available = True except ImportError: self.pil_available = False - if "ImageSize" not in p or "ImageHeight" not in p or "ImageWidth" not in p: + if "size" not in p or "height" not in p or "width" not in p: data = self.get_page(idx) if data: try: @@ -538,17 +306,17 @@ class ComicArchive: im = Image.open(io.StringIO(data)) w, h = im.size - p["ImageSize"] = str(len(data)) - p["ImageHeight"] = str(h) - p["ImageWidth"] = str(w) + p["size"] = str(len(data)) + p["height"] = str(h) + p["width"] = str(w) except Exception as e: logger.warning("Error decoding image [%s] %s :: image %s", e, self.path, index) - p["ImageSize"] = str(len(data)) + p["size"] = str(len(data)) else: - if "ImageSize" not in p: + if "size" not in p: data = self.get_page(idx) - p["ImageSize"] = str(len(data)) + p["size"] = str(len(data)) def metadata_from_filename( self, diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 1a45349..506c461 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -54,13 +54,13 @@ class PageType: class ImageMetadata(TypedDict, total=False): - Type: str - Bookmark: str - DoublePage: bool - Image: int - ImageSize: str - ImageHeight: str - ImageWidth: str + type: str + bookmark: str + double_page: bool + image_index: int + size: str + height: str + width: str class Credit(TypedDict): @@ -173,6 +173,33 @@ class GenericMetadata: tmp.__dict__.update(kwargs) return tmp + def get_clean_metadata(self, *attributes: str) -> GenericMetadata: + new_md = GenericMetadata() + for attr in sorted(attributes): + if "." in attr: + lst, _, name = attr.partition(".") + old_value = getattr(self, lst) + new_value = getattr(new_md, lst) + if old_value: + if not new_value: + for x in old_value: + new_value.append(x.__class__()) + for i, x in enumerate(old_value): + if isinstance(x, dict): + if name in x: + new_value[i][name] = x[name] + else: + setattr(new_value[i], name, getattr(x, name)) + + else: + old_value = getattr(self, attr) + if isinstance(old_value, list): + continue + setattr(new_md, attr, old_value) + + new_md.__post_init__() + return new_md + def overlay(self, new_md: GenericMetadata) -> None: """Overlay a metadata object on this one @@ -262,15 +289,15 @@ class GenericMetadata: def set_default_page_list(self, count: int) -> None: # generate a default page list, with the first page marked as the cover for i in range(count): - page_dict = ImageMetadata(Image=i) + page_dict = ImageMetadata(image_index=i) if i == 0: - page_dict["Type"] = PageType.FrontCover + page_dict["type"] = PageType.FrontCover self.pages.append(page_dict) def get_archive_page_index(self, pagenum: int) -> int: # 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"]) + return int(self.pages[pagenum]["image_index"]) return 0 @@ -278,8 +305,8 @@ class GenericMetadata: # return a list of archive page indices of cover pages coverlist = [] for p in self.pages: - if "Type" in p and p["Type"] == PageType.FrontCover: - coverlist.append(int(p["Image"])) + if "type" in p and p["type"] == PageType.FrontCover: + coverlist.append(int(p["image_index"])) if len(coverlist) == 0: coverlist.append(0) @@ -459,36 +486,36 @@ md_test: GenericMetadata = GenericMetadata( ], tags=set(), pages=[ - ImageMetadata(Image=0, ImageHeight="1280", ImageSize="195977", ImageWidth="800", Type=PageType.FrontCover), - ImageMetadata(Image=1, ImageHeight="2039", ImageSize="611993", ImageWidth="1327"), - ImageMetadata(Image=2, ImageHeight="2039", ImageSize="783726", ImageWidth="1327"), - ImageMetadata(Image=3, ImageHeight="2039", ImageSize="679584", ImageWidth="1327"), - ImageMetadata(Image=4, ImageHeight="2039", ImageSize="788179", ImageWidth="1327"), - ImageMetadata(Image=5, ImageHeight="2039", ImageSize="864433", ImageWidth="1327"), - ImageMetadata(Image=6, ImageHeight="2039", ImageSize="765606", ImageWidth="1327"), - ImageMetadata(Image=7, ImageHeight="2039", ImageSize="876427", ImageWidth="1327"), - ImageMetadata(Image=8, ImageHeight="2039", ImageSize="852622", ImageWidth="1327"), - ImageMetadata(Image=9, ImageHeight="2039", ImageSize="800205", ImageWidth="1327"), - ImageMetadata(Image=10, ImageHeight="2039", ImageSize="746243", ImageWidth="1326"), - ImageMetadata(Image=11, ImageHeight="2039", ImageSize="718062", ImageWidth="1327"), - ImageMetadata(Image=12, ImageHeight="2039", ImageSize="532179", ImageWidth="1326"), - ImageMetadata(Image=13, ImageHeight="2039", ImageSize="686708", ImageWidth="1327"), - ImageMetadata(Image=14, ImageHeight="2039", ImageSize="641907", ImageWidth="1327"), - ImageMetadata(Image=15, ImageHeight="2039", ImageSize="805388", ImageWidth="1327"), - ImageMetadata(Image=16, ImageHeight="2039", ImageSize="668927", ImageWidth="1326"), - ImageMetadata(Image=17, ImageHeight="2039", ImageSize="710605", ImageWidth="1327"), - ImageMetadata(Image=18, ImageHeight="2039", ImageSize="761398", ImageWidth="1326"), - ImageMetadata(Image=19, ImageHeight="2039", ImageSize="743807", ImageWidth="1327"), - ImageMetadata(Image=20, ImageHeight="2039", ImageSize="552911", ImageWidth="1326"), - ImageMetadata(Image=21, ImageHeight="2039", ImageSize="556827", ImageWidth="1327"), - ImageMetadata(Image=22, ImageHeight="2039", ImageSize="675078", ImageWidth="1326"), + ImageMetadata(image_index=0, height="1280", size="195977", width="800", type=PageType.FrontCover), + ImageMetadata(image_index=1, height="2039", size="611993", width="1327"), + ImageMetadata(image_index=2, height="2039", size="783726", width="1327"), + ImageMetadata(image_index=3, height="2039", size="679584", width="1327"), + ImageMetadata(image_index=4, height="2039", size="788179", width="1327"), + ImageMetadata(image_index=5, height="2039", size="864433", width="1327"), + ImageMetadata(image_index=6, height="2039", size="765606", width="1327"), + ImageMetadata(image_index=7, height="2039", size="876427", width="1327"), + ImageMetadata(image_index=8, height="2039", size="852622", width="1327"), + ImageMetadata(image_index=9, height="2039", size="800205", width="1327"), + ImageMetadata(image_index=10, height="2039", size="746243", width="1326"), + ImageMetadata(image_index=11, height="2039", size="718062", width="1327"), + ImageMetadata(image_index=12, height="2039", size="532179", width="1326"), + ImageMetadata(image_index=13, height="2039", size="686708", width="1327"), + ImageMetadata(image_index=14, height="2039", size="641907", width="1327"), + ImageMetadata(image_index=15, height="2039", size="805388", width="1327"), + ImageMetadata(image_index=16, height="2039", size="668927", width="1326"), + ImageMetadata(image_index=17, height="2039", size="710605", width="1327"), + ImageMetadata(image_index=18, height="2039", size="761398", width="1326"), + ImageMetadata(image_index=19, height="2039", size="743807", width="1327"), + ImageMetadata(image_index=20, height="2039", size="552911", width="1326"), + ImageMetadata(image_index=21, height="2039", size="556827", width="1327"), + ImageMetadata(image_index=22, height="2039", size="675078", width="1326"), ImageMetadata( - Bookmark="Interview", - Image=23, - ImageHeight="2032", - ImageSize="800965", - ImageWidth="1338", - Type=PageType.Letters, + bookmark="Interview", + image_index=23, + height="2032", + size="800965", + width="1338", + type=PageType.Letters, ), ], price=None, diff --git a/comicapi/metadata/__init__.py b/comicapi/metadata/__init__.py new file mode 100644 index 0000000..05e9bb4 --- /dev/null +++ b/comicapi/metadata/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from comicapi.metadata.metadata import Metadata + +__all__ = ["Metadata"] diff --git a/comicapi/comet.py b/comicapi/metadata/comet.py similarity index 57% rename from comicapi/comet.py rename to comicapi/metadata/comet.py index 138d602..2e4dce8 100644 --- a/comicapi/comet.py +++ b/comicapi/metadata/comet.py @@ -16,33 +16,122 @@ from __future__ import annotations import logging +import os import xml.etree.ElementTree as ET from typing import Any from comicapi import utils +from comicapi.archivers import Archiver from comicapi.genericmetadata import GenericMetadata +from comicapi.metadata import Metadata logger = logging.getLogger(__name__) -class CoMet: - writer_synonyms = ["writer", "plotter", "scripter"] - penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"] - inker_synonyms = ["inker", "artist", "finishes"] - colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"] - letterer_synonyms = ["letterer"] - cover_synonyms = ["cover", "covers", "coverartist", "cover artist"] - editor_synonyms = ["editor"] +class CoMet(Metadata): + _writer_synonyms = ("writer", "plotter", "scripter") + _penciller_synonyms = ("artist", "penciller", "penciler", "breakdowns") + _inker_synonyms = ("inker", "artist", "finishes") + _colorist_synonyms = ("colorist", "colourist", "colorer", "colourer") + _letterer_synonyms = ("letterer",) + _cover_synonyms = ("cover", "covers", "coverartist", "cover artist") + _editor_synonyms = ("editor",) - def metadata_from_string(self, string: str) -> GenericMetadata: + enabled = True + + short_name = "comet" + + def __init__(self, version: str) -> None: + super().__init__(version) + + self.comet_filename = "CoMet.xml" + self.file = "CoMet.xml" + self.supported_attributes = { + "characters", + "description", + "credits", + "credits.person", + "credits.primary", + "credits.role", + "format", + "genres", + "identifier", + "is_version_of", + "issue", + "language", + "last_mark", + "maturity_rating", + "month", + "page_count", + "price", + "publisher", + "rights", + "series", + "title", + "volume", + "year", + } + + def supports_metadata(self, archive: Archiver) -> bool: + return archive.supports_files() + + def has_metadata(self, archive: Archiver) -> bool: + if not self.supports_metadata(archive): + return False + has_metadata = False + # look at all xml files in root, and search for CoMet data, get first + for n in archive.get_filename_list(): + if os.path.dirname(n) == "" and os.path.splitext(n)[1].casefold() == ".xml": + # read in XML file, and validate it + data = b"" + try: + data = archive.read_file(n) + except Exception as e: + logger.warning("Error reading in Comet XML for validation! from %s: %s", archive.path, e) + if self._validate_bytes(data): + # since we found it, save it! + self.file = n + has_metadata = True + break + return has_metadata + + def remove_metadata(self, archive: Archiver) -> bool: + return self.has_metadata(archive) and archive.remove_file(self.file) + + def get_metadata(self, archive: Archiver) -> GenericMetadata: + if self.has_metadata(archive): + metadata = archive.read_file(self.file) or b"" + if self._validate_bytes(metadata): + return self._metadata_from_bytes(metadata) + return GenericMetadata() + + def get_metadata_string(self, archive: Archiver) -> str: + if self.has_metadata(archive): + return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding="unicode", xml_declaration=True) + return "" + + def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool: + if self.supports_metadata(archive): + success = True + if self.file != self.comet_filename: + success = self.remove_metadata(archive) + return archive.write_file(self.comet_filename, self._bytes_from_metadata(metadata)) and success + else: + logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") + return False + + def name(self) -> str: + return "Comic Metadata (CoMet)" + + def _metadata_from_bytes(self, string: bytes) -> GenericMetadata: tree = ET.ElementTree(ET.fromstring(string)) - return self.convert_xml_to_metadata(tree) + return self._convert_xml_to_metadata(tree) - def string_from_metadata(self, metadata: GenericMetadata) -> str: - tree = self.convert_metadata_to_xml(metadata) - return str(ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True).decode("utf-8")) + def _bytes_from_metadata(self, metadata: GenericMetadata) -> bytes: + tree = self._convert_metadata_to_xml(metadata) + return ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True) - def convert_metadata_to_xml(self, metadata: GenericMetadata) -> ET.ElementTree: + def _convert_metadata_to_xml(self, metadata: GenericMetadata) -> ET.ElementTree: # shorthand for the metadata md = metadata @@ -95,25 +184,25 @@ class CoMet: # loop thru credits, and build a list for each role that CoMet supports for credit in metadata.credits: - if credit["role"].casefold() in set(self.writer_synonyms): + if credit["role"].casefold() in set(self._writer_synonyms): ET.SubElement(root, "writer").text = str(credit["person"]) - if credit["role"].casefold() in set(self.penciller_synonyms): + if credit["role"].casefold() in set(self._penciller_synonyms): ET.SubElement(root, "penciller").text = str(credit["person"]) - if credit["role"].casefold() in set(self.inker_synonyms): + if credit["role"].casefold() in set(self._inker_synonyms): ET.SubElement(root, "inker").text = str(credit["person"]) - if credit["role"].casefold() in set(self.colorist_synonyms): + if credit["role"].casefold() in set(self._colorist_synonyms): ET.SubElement(root, "colorist").text = str(credit["person"]) - if credit["role"].casefold() in set(self.letterer_synonyms): + if credit["role"].casefold() in set(self._letterer_synonyms): ET.SubElement(root, "letterer").text = str(credit["person"]) - if credit["role"].casefold() in set(self.cover_synonyms): + if credit["role"].casefold() in set(self._cover_synonyms): ET.SubElement(root, "coverDesigner").text = str(credit["person"]) - if credit["role"].casefold() in set(self.editor_synonyms): + if credit["role"].casefold() in set(self._editor_synonyms): ET.SubElement(root, "editor").text = str(credit["person"]) ET.indent(root) @@ -122,7 +211,7 @@ class CoMet: tree = ET.ElementTree(root) return tree - def convert_xml_to_metadata(self, tree: ET.ElementTree) -> GenericMetadata: + def _convert_xml_to_metadata(self, tree: ET.ElementTree) -> GenericMetadata: root = tree.getroot() if root.tag != "comet": @@ -194,7 +283,7 @@ class CoMet: return metadata # verify that the string actually contains CoMet data in XML format - def validate_string(self, string: str) -> bool: + def _validate_bytes(self, string: bytes) -> bool: try: tree = ET.ElementTree(ET.fromstring(string)) root = tree.getroot() @@ -204,11 +293,3 @@ class CoMet: return False return True - - def write_to_external_file(self, filename: str, metadata: GenericMetadata) -> None: - tree = self.convert_metadata_to_xml(metadata) - tree.write(filename, encoding="utf-8") - - def read_from_external_file(self, filename: str) -> GenericMetadata: - tree = ET.parse(filename) - return self.convert_xml_to_metadata(tree) diff --git a/comicapi/comicbookinfo.py b/comicapi/metadata/comicbookinfo.py similarity index 63% rename from comicapi/comicbookinfo.py rename to comicapi/metadata/comicbookinfo.py index 49e41bd..c3ab882 100644 --- a/comicapi/comicbookinfo.py +++ b/comicapi/metadata/comicbookinfo.py @@ -20,11 +20,13 @@ from datetime import datetime from typing import Any, Literal, TypedDict from comicapi import utils +from comicapi.archivers import Archiver from comicapi.genericmetadata import GenericMetadata +from comicapi.metadata import Metadata logger = logging.getLogger(__name__) -CBILiteralType = Literal[ +_CBILiteralType = Literal[ "series", "title", "issue", @@ -44,13 +46,13 @@ CBILiteralType = Literal[ ] -class Credits(TypedDict): +class _Credits(TypedDict): person: str role: str primary: bool -class ComicBookInfoJson(TypedDict, total=False): +class _ComicBookInfoJson(TypedDict, total=False): series: str title: str publisher: str @@ -64,17 +66,77 @@ class ComicBookInfoJson(TypedDict, total=False): genre: str language: str country: str - credits: list[Credits] + credits: list[_Credits] tags: list[str] comments: str -CBIContainer = TypedDict("CBIContainer", {"appID": str, "lastModified": str, "ComicBookInfo/1.0": ComicBookInfoJson}) +_CBIContainer = TypedDict("_CBIContainer", {"appID": str, "lastModified": str, "ComicBookInfo/1.0": _ComicBookInfoJson}) -class ComicBookInfo: - def metadata_from_string(self, string: str) -> GenericMetadata: - cbi_container: CBIContainer = json.loads(string) +class ComicBookInfo(Metadata): + enabled = True + + short_name = "cbi" + + def __init__(self, version: str) -> None: + super().__init__(version) + + self.supported_attributes = { + "description", + "country", + "credits", + "credits.person", + "credits.primary", + "credits.role", + "critical_rating", + "genres", + "issue", + "issue_count", + "language", + "month", + "publisher", + "series", + "tags", + "title", + "volume", + "volume_count", + "year", + } + + def supports_metadata(self, archive: Archiver) -> bool: + return archive.supports_comment() + + def has_metadata(self, archive: Archiver) -> bool: + return self.supports_metadata(archive) and self._validate_string(archive.get_comment()) + + def remove_metadata(self, archive: Archiver) -> bool: + return archive.set_comment("") + + def get_metadata(self, archive: Archiver) -> GenericMetadata: + if self.has_metadata(archive): + comment = archive.get_comment() + if self._validate_string(comment): + return self._metadata_from_string(comment) + return GenericMetadata() + + def get_metadata_string(self, archive: Archiver) -> str: + if self.has_metadata(archive): + return json.dumps(json.loads(archive.get_comment()), indent=2) + return "" + + def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool: + if self.supports_metadata(archive): + return archive.set_comment(self._string_from_metadata(metadata)) + else: + logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") + return False + + def name(self) -> str: + return "ComicBookInfo" + + def _metadata_from_string(self, string: str) -> GenericMetadata: + cbi_container: _CBIContainer = json.loads(string) metadata = GenericMetadata() @@ -96,7 +158,7 @@ class ComicBookInfo: metadata.critical_rating = utils.xlate_int(cbi.get("rating")) metadata.credits = [ - Credits( + _Credits( person=x["person"] if "person" in x else "", role=x["role"] if "role" in x else "", primary=x["primary"] if "primary" in x else False, @@ -113,11 +175,11 @@ class ComicBookInfo: return metadata - def string_from_metadata(self, metadata: GenericMetadata) -> str: - cbi_container = self.create_json_dictionary(metadata) + def _string_from_metadata(self, metadata: GenericMetadata) -> str: + cbi_container = self._create_json_dictionary(metadata) return json.dumps(cbi_container) - def validate_string(self, string: bytes | str) -> bool: + def _validate_string(self, string: bytes | str) -> bool: """Verify that the string actually contains CBI data in JSON format""" try: @@ -127,10 +189,10 @@ class ComicBookInfo: return "ComicBookInfo/1.0" in cbi_container - def create_json_dictionary(self, metadata: GenericMetadata) -> CBIContainer: + def _create_json_dictionary(self, metadata: GenericMetadata) -> _CBIContainer: """Create the dictionary that we will convert to JSON text""" - cbi_container = CBIContainer( + cbi_container = _CBIContainer( { "appID": "ComicTagger/1.0.0", "lastModified": str(datetime.now()), @@ -139,7 +201,7 @@ class ComicBookInfo: ) # TODO: ctversion.version, # helper func - def assign(cbi_entry: CBILiteralType, md_entry: Any) -> None: + def assign(cbi_entry: _CBILiteralType, md_entry: Any) -> None: if md_entry is not None or isinstance(md_entry, str) and md_entry != "": cbi_container["ComicBookInfo/1.0"][cbi_entry] = md_entry @@ -161,9 +223,3 @@ class ComicBookInfo: assign("tags", list(metadata.tags)) return cbi_container - - def write_to_external_file(self, filename: str, metadata: GenericMetadata) -> None: - cbi_container = self.create_json_dictionary(metadata) - - with open(filename, "w", encoding="utf-8") as f: - f.write(json.dumps(cbi_container, indent=4)) diff --git a/comicapi/comicinfoxml.py b/comicapi/metadata/comicrack.py similarity index 55% rename from comicapi/comicinfoxml.py rename to comicapi/metadata/comicrack.py index 7c62ebf..76ad543 100644 --- a/comicapi/comicinfoxml.py +++ b/comicapi/metadata/comicrack.py @@ -18,49 +18,125 @@ import logging import xml.etree.ElementTree as ET from collections import OrderedDict from typing import Any, cast -from xml.etree.ElementTree import ElementTree from comicapi import utils +from comicapi.archivers import Archiver from comicapi.genericmetadata import GenericMetadata, ImageMetadata +from comicapi.metadata import Metadata logger = logging.getLogger(__name__) -class ComicInfoXml: - writer_synonyms = ["writer", "plotter", "scripter"] - penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"] - inker_synonyms = ["inker", "artist", "finishes"] - colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"] - letterer_synonyms = ["letterer"] - cover_synonyms = ["cover", "covers", "coverartist", "cover artist"] - editor_synonyms = ["editor"] +class ComicRack(Metadata): + _writer_synonyms = ("writer", "plotter", "scripter") + _penciller_synonyms = ("artist", "penciller", "penciler", "breakdowns") + _inker_synonyms = ("inker", "artist", "finishes") + _colorist_synonyms = ("colorist", "colourist", "colorer", "colourer") + _letterer_synonyms = ("letterer",) + _cover_synonyms = ("cover", "covers", "coverartist", "cover artist") + _editor_synonyms = ("editor",) - def get_parseable_credits(self) -> list[str]: - parsable_credits = [] - parsable_credits.extend(self.writer_synonyms) - parsable_credits.extend(self.penciller_synonyms) - parsable_credits.extend(self.inker_synonyms) - parsable_credits.extend(self.colorist_synonyms) - parsable_credits.extend(self.letterer_synonyms) - parsable_credits.extend(self.cover_synonyms) - parsable_credits.extend(self.editor_synonyms) - return parsable_credits + enabled = True - def metadata_from_string(self, string: bytes) -> GenericMetadata: - tree = ET.ElementTree(ET.fromstring(string)) - return self.convert_xml_to_metadata(tree) + short_name = "cr" - def string_from_metadata(self, metadata: GenericMetadata, xml: bytes = b"") -> str: - tree = self.convert_metadata_to_xml(metadata, xml) - tree_str = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True).decode("utf-8") - return str(tree_str) + def __init__(self, version: str) -> None: + super().__init__(version) - def convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b"") -> ElementTree: + self.file = "ComicInfo.xml" + self.supported_attributes = { + "alternate_count", + "alternate_number", + "alternate_series", + "black_and_white", + "characters", + "description", + "credits", + "credits.person", + "credits.role", + "critical_rating", + "day", + "format", + "genres", + "imprint", + "issue", + "issue_count", + "language", + "locations", + "manga", + "maturity_rating", + "month", + "notes", + "page_count", + "pages", + "pages.bookmark", + "pages.double_page", + "pages.height", + "pages.image_index", + "pages.size", + "pages.type", + "pages.width", + "publisher", + "scan_info", + "series", + "series_groups", + "story_arcs", + "teams", + "title", + "volume", + "web_link", + "year", + } + + def supports_metadata(self, archive: Archiver) -> bool: + return True + + def has_metadata(self, archive: Archiver) -> bool: + return ( + self.supports_metadata(archive) + and self.file in archive.get_filename_list() + and self._validate_bytes(archive.read_file(self.file)) + ) + + def remove_metadata(self, archive: Archiver) -> bool: + return self.has_metadata(archive) and archive.remove_file(self.file) + + def get_metadata(self, archive: Archiver) -> GenericMetadata: + if self.has_metadata(archive): + metadata = archive.read_file(self.file) or b"" + if self._validate_bytes(metadata): + return self._metadata_from_bytes(metadata) + return GenericMetadata() + + def get_metadata_string(self, archive: Archiver) -> str: + if self.has_metadata(archive): + return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding="unicode", xml_declaration=True) + return "" + + def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool: + if self.supports_metadata(archive): + return archive.write_file(self.file, self._bytes_from_metadata(metadata)) + else: + logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") + return False + + def name(self) -> str: + return "Comic Rack" + + def _metadata_from_bytes(self, string: bytes) -> GenericMetadata: + root = ET.fromstring(string) + return self._convert_xml_to_metadata(root) + + def _bytes_from_metadata(self, metadata: GenericMetadata, xml: bytes = b"") -> bytes: + root = self._convert_metadata_to_xml(metadata, xml) + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + def _convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b"") -> ET.Element: # shorthand for the metadata md = metadata if xml: - root = ET.ElementTree(ET.fromstring(xml)).getroot() + root = ET.fromstring(xml) else: # build a tree structure root = ET.Element("ComicInfo") @@ -68,7 +144,7 @@ class ComicInfoXml: root.attrib["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema" # helper func - def assign(cix_entry: str, md_entry: Any) -> None: + def assign(cr_entry: str, md_entry: Any) -> None: if md_entry: text = "" if isinstance(md_entry, str): @@ -77,13 +153,13 @@ class ComicInfoXml: text = ",".join(md_entry) else: text = str(md_entry) - et_entry = root.find(cix_entry) + et_entry = root.find(cr_entry) if et_entry is not None: et_entry.text = text else: - ET.SubElement(root, cix_entry).text = text + ET.SubElement(root, cr_entry).text = text else: - et_entry = root.find(cix_entry) + et_entry = root.find(cr_entry) if et_entry is not None: root.remove(et_entry) @@ -116,25 +192,25 @@ class ComicInfoXml: # first, loop thru credits, and build a list for each role that CIX # supports for credit in metadata.credits: - if credit["role"].casefold() in set(self.writer_synonyms): + if credit["role"].casefold() in set(self._writer_synonyms): credit_writer_list.append(credit["person"].replace(",", "")) - if credit["role"].casefold() in set(self.penciller_synonyms): + if credit["role"].casefold() in set(self._penciller_synonyms): credit_penciller_list.append(credit["person"].replace(",", "")) - if credit["role"].casefold() in set(self.inker_synonyms): + if credit["role"].casefold() in set(self._inker_synonyms): credit_inker_list.append(credit["person"].replace(",", "")) - if credit["role"].casefold() in set(self.colorist_synonyms): + if credit["role"].casefold() in set(self._colorist_synonyms): credit_colorist_list.append(credit["person"].replace(",", "")) - if credit["role"].casefold() in set(self.letterer_synonyms): + if credit["role"].casefold() in set(self._letterer_synonyms): credit_letterer_list.append(credit["person"].replace(",", "")) - if credit["role"].casefold() in set(self.cover_synonyms): + if credit["role"].casefold() in set(self._cover_synonyms): credit_cover_list.append(credit["person"].replace(",", "")) - if credit["role"].casefold() in set(self.editor_synonyms): + if credit["role"].casefold() in set(self._editor_synonyms): credit_editor_list.append(credit["person"].replace(",", "")) # second, convert each list to string, and add to XML struct @@ -171,17 +247,28 @@ class ComicInfoXml: for page_dict in md.pages: page_node = ET.SubElement(pages_node, "Page") - page_node.attrib = OrderedDict(sorted((k, str(v)) for k, v in page_dict.items())) + 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())) ET.indent(root) - # wrap it in an ElementTree instance, and save as XML - tree = ET.ElementTree(root) - return tree - - def convert_xml_to_metadata(self, tree: ElementTree) -> GenericMetadata: - root = tree.getroot() + return root + def _convert_xml_to_metadata(self, root: ET.Element) -> GenericMetadata: if root.tag != "ComicInfo": raise Exception("Not a ComicInfo file") @@ -252,20 +339,36 @@ class ComicInfoXml: if pages_node is not None: for page in pages_node: p: dict[str, Any] = page.attrib - if "Image" in p: - p["Image"] = int(p["Image"]) + md_page = ImageMetadata() + + if "Bookmark" in p: + md_page["bookmark"] = p["Bookmark"] if "DoublePage" in p: - p["DoublePage"] = True if p["DoublePage"].casefold() in ("yes", "true", "1") else False - md.pages.append(cast(ImageMetadata, p)) + md_page["double_page"] = True if p["DoublePage"].casefold() in ("yes", "true", "1") else False + if "Image" in p: + md_page["image_index"] = int(p["Image"]) + 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"] + + md.pages.append(cast(ImageMetadata, md_page)) md.is_empty = False return md - def write_to_external_file(self, filename: str, metadata: GenericMetadata, xml: bytes = b"") -> None: - tree = self.convert_metadata_to_xml(metadata, xml) - tree.write(filename, encoding="utf-8", xml_declaration=True) + def _validate_bytes(self, string: bytes) -> bool: + """verify that the string actually contains CIX data in XML format""" + try: + root = ET.fromstring(string) + if root.tag != "ComicInfo": + return False + except ET.ParseError: + return False - def read_from_external_file(self, filename: str) -> GenericMetadata: - tree = ET.parse(filename) - return self.convert_xml_to_metadata(tree) + return True diff --git a/comicapi/metadata/metadata.py b/comicapi/metadata/metadata.py new file mode 100644 index 0000000..5959bda --- /dev/null +++ b/comicapi/metadata/metadata.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from comicapi.archivers import Archiver +from comicapi.genericmetadata import GenericMetadata + + +class Metadata: + enabled: bool = False + short_name: str = "" + + def __init__(self, version: str) -> None: + self.version: str = version + self.supported_attributes = { + "tag_origin", + "issue_id", + "series", + "issue", + "title", + "publisher", + "month", + "year", + "day", + "issue_count", + "volume", + "genre", + "language", + "comments", + "volume_count", + "critical_rating", + "country", + "alternate_series", + "alternate_number", + "alternate_count", + "imprint", + "notes", + "web_link", + "format", + "manga", + "black_and_white", + "page_count", + "maturity_rating", + "story_arc", + "series_group", + "scan_info", + "characters", + "teams", + "locations", + "credits", + "credits.person", + "credits.role", + "credits.primary", + "tags", + "pages", + "pages.type", + "pages.bookmark", + "pages.double_page", + "pages.image_index", + "pages.size", + "pages.height", + "pages.width", + "price", + "is_version_of", + "rights", + "identifier", + "last_mark", + "cover_image", + } + + def supports_metadata(self, archive: Archiver) -> bool: + """ + Checks the given archive for the ability to save this metadata style. + Should always return a bool. Failures should return False. + Typically consists of a call to either `archive.supports_comment` or `archive.supports_file` + """ + return False + + def has_metadata(self, archive: Archiver) -> bool: + """ + Checks the given archive for metadata. + Should always return a bool. Failures should return False. + """ + return False + + def remove_metadata(self, archive: Archiver) -> bool: + """ + Removes the metadata from the given archive. + Should always return a bool. Failures should return False. + """ + return False + + def get_metadata(self, archive: Archiver) -> GenericMetadata: + """ + Returns a GenericMetadata representing the data saved in the given archive. + Should always return a GenericMetadata. Failures should return an empty metadata object. + """ + return GenericMetadata() + + def get_metadata_string(self, archive: Archiver) -> str: + """ + Returns the raw metadata as a string. + If the metadata is a binary format a roughly similar text format should be used. + Should always return a string. Failures should return the empty string. + """ + return "" + + def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool: + """ + Saves the given metadata to the given archive. + Should always return a bool. Failures should return False. + """ + return False + + def name(self) -> str: + """ + Returns the name of this metadata for display purposes eg "Comic Rack". + Should always return a string. Failures should return the empty string. + """ + return "" diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index 96b4723..a99d607 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -21,7 +21,7 @@ from typing import Callable from PyQt5 import QtCore, QtGui, QtWidgets, uic -from comicapi.comicarchive import ComicArchive, MetaDataStyle +from comicapi.comicarchive import ComicArchive, metadata_styles from comicapi.genericmetadata import GenericMetadata from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ctsettings import ct_ns @@ -38,7 +38,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): self, parent: QtWidgets.QWidget, match_set_list: list[Result], - style: int, + style: str, fetch_func: Callable[[IssueResult], GenericMetadata], config: ct_ns, talker: ComicTalker, @@ -247,7 +247,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) md.overlay(ct_md) success = ca.write_metadata(md, self._style) - ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX]) + ca.load_cache(list(metadata_styles)) QtWidgets.QApplication.restoreOverrideCursor() diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 4e44448..fae8037 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -46,7 +46,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.cbxSaveOnLowConfidence.setChecked(self.config.Auto_Tag__save_on_low_confidence) self.cbxDontUseYear.setChecked(self.config.Auto_Tag__dont_use_year_when_identifying) - self.cbxAssumeIssueOne.setChecked(self.config.Auto_Tag__assume_1_if_no_issue_num) + self.cbxAssumeIssueOne.setChecked(self.config.Auto_Tag__assume_issue_one) self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.Auto_Tag__ignore_leading_numbers_in_filename) self.cbxRemoveAfterSuccess.setChecked(self.config.Auto_Tag__remove_archive_after_successful_match) self.cbxAutoImprint.setChecked(self.config.Issue_Identifier__auto_imprint) @@ -95,7 +95,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): # persist some settings self.config.Auto_Tag__save_on_low_confidence = self.auto_save_on_low self.config.Auto_Tag__dont_use_year_when_identifying = self.dont_use_year - self.config.Auto_Tag__assume_1_if_no_issue_num = self.assume_issue_one + self.config.Auto_Tag__assume_issue_one = self.assume_issue_one self.config.Auto_Tag__ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename self.config.Auto_Tag__remove_archive_after_successful_match = self.remove_after_success diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index daa2e30..0e750f5 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -28,7 +28,8 @@ from datetime import datetime from typing import Any, TextIO from comicapi import utils -from comicapi.comicarchive import ComicArchive, MetaDataStyle +from comicapi.comicarchive import ComicArchive +from comicapi.comicarchive import metadata_styles as md_styles from comicapi.genericmetadata import GenericMetadata from comictaggerlib import ctversion from comictaggerlib.cbltransformer import CBLTransformer @@ -134,10 +135,10 @@ class CLI: def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool: if not self.config.Runtime_Options__dryrun: - for metadata_style in self.config.Runtime_Options__type: + for style in self.config.Runtime_Options__type: # write out the new data - if not ca.write_metadata(md, metadata_style): - logger.error("The tag save seemed to fail for style: %s!", MetaDataStyle.name[metadata_style]) + if not ca.write_metadata(md, style): + logger.error("The tag save seemed to fail for style: %s!", md_styles[style].name()) return False self.output("Save complete.") @@ -258,10 +259,10 @@ class CLI: md.overlay(f_md) - for metadata_style in self.config.Runtime_Options__type: - if ca.has_metadata(metadata_style): + for style in self.config.Runtime_Options__type: + if ca.has_metadata(style): try: - t_md = ca.read_metadata(metadata_style) + t_md = ca.read_metadata(style) md.overlay(t_md) break except Exception as e: @@ -286,20 +287,9 @@ class CLI: brief += f"({page_count: >3} pages)" brief += " tags:[ " - if not ( - ca.has_metadata(MetaDataStyle.CBI) - or ca.has_metadata(MetaDataStyle.CIX) - or ca.has_metadata(MetaDataStyle.COMET) - ): - brief += "none " - else: - if ca.has_metadata(MetaDataStyle.CBI): - brief += "CBL " - if ca.has_metadata(MetaDataStyle.CIX): - brief += "CR " - if ca.has_metadata(MetaDataStyle.COMET): - brief += "CoMet " - brief += "]" + metadata_styles = [md_styles[style].name() for style in md_styles if ca.has_metadata(style)] + brief += " ".join(metadata_styles) + brief += " ]" self.output(brief) @@ -308,57 +298,23 @@ class CLI: self.output() - raw: str | bytes = "" md = None - if not self.config.Runtime_Options__type or MetaDataStyle.CIX in self.config.Runtime_Options__type: - if ca.has_metadata(MetaDataStyle.CIX): - self.output("--------- ComicRack tags ---------") - try: - if self.config.Runtime_Options__raw: - raw = ca.read_raw_cix() - if isinstance(raw, bytes): - raw = raw.decode("utf-8") - self.output(raw) - else: - md = ca.read_cix() - self.output(md) - except Exception as e: - logger.error("Failed to load metadata for %s: %s", ca.path, e) - - if not self.config.Runtime_Options__type or MetaDataStyle.CBI in self.config.Runtime_Options__type: - if ca.has_metadata(MetaDataStyle.CBI): - self.output("------- ComicBookLover tags -------") - try: - if self.config.Runtime_Options__raw: - raw = ca.read_raw_cbi() - if isinstance(raw, bytes): - raw = raw.decode("utf-8") - self.output(raw) - else: - md = ca.read_cbi() - self.output(md) - except Exception as e: - logger.error("Failed to load metadata for %s: %s", ca.path, e) - - if not self.config.Runtime_Options__type or MetaDataStyle.COMET in self.config.Runtime_Options__type: - if ca.has_metadata(MetaDataStyle.COMET): - self.output("----------- CoMet tags -----------") - try: - if self.config.Runtime_Options__raw: - raw = ca.read_raw_comet() - if isinstance(raw, bytes): - raw = raw.decode("utf-8") - self.output(raw) - else: - md = ca.read_comet() - self.output(md) - except Exception as e: - logger.error("Failed to load metadata for %s: %s", ca.path, e) - + for style, style_obj in md_styles.items(): + if not self.config.Runtime_Options__type or style in self.config.Runtime_Options__type: + if ca.has_metadata(style): + self.output(f"--------- {style_obj.name()} tags ---------") + try: + if self.config.Runtime_Options__raw: + self.output(ca.read_metadata_string(style)) + else: + md = ca.read_metadata(style) + self.output(md) + except Exception as e: + logger.error("Failed to load metadata for %s: %s", ca.path, e) return Result(Action.print, Status.success, ca.path, md=md) - def delete_style(self, ca: ComicArchive, style: int) -> Status: - style_name = MetaDataStyle.name[style] + def delete_style(self, ca: ComicArchive, style: str) -> Status: + style_name = md_styles[style].name() if ca.has_metadata(style): if not self.config.Runtime_Options__dryrun: @@ -376,16 +332,16 @@ class CLI: def delete(self, ca: ComicArchive) -> Result: res = Result(Action.delete, Status.success, ca.path) - for metadata_style in self.config.Runtime_Options__type: - status = self.delete_style(ca, metadata_style) + for style in self.config.Runtime_Options__type: + status = self.delete_style(ca, style) if status == Status.success: - res.tags_deleted.append(metadata_style) + res.tags_deleted.append(style) else: res.status = status return res - def copy_style(self, ca: ComicArchive, md: GenericMetadata, style: int) -> Status: - dst_style_name = MetaDataStyle.name[style] + def copy_style(self, ca: ComicArchive, md: GenericMetadata, style: str) -> Status: + dst_style_name = md_styles[style].name() if not self.config.Runtime_Options__overwrite and ca.has_metadata(style): self.output(f"{ca.path}: Already has {dst_style_name} tags. Not overwriting.") return Status.existing_tags @@ -393,44 +349,47 @@ class CLI: self.output(f"{ca.path}: Destination and source are same: {dst_style_name}. Nothing to do.") return Status.existing_tags - src_style_name = MetaDataStyle.name[self.config.Commands__copy] - if ca.has_metadata(self.config.Commands__copy): - if not self.config.Runtime_Options__dryrun: - if self.config.Comic_Book_Lover__apply_transform_on_bulk_operation == MetaDataStyle.CBI: - md = CBLTransformer(md, self.config).apply() + src_style_name = md_styles[self.config.Commands__copy].name() + if not self.config.Runtime_Options__dryrun: + if self.config.Comic_Book_Lover__apply_transform_on_bulk_operation == "cbi": + md = CBLTransformer(md, self.config).apply() - if ca.write_metadata(md, style): - self.output(f"{ca.path}: Copied {src_style_name} tags to {dst_style_name}.") - return Status.success - else: - self.output(f"{ca.path}: Tag copy seemed to fail!") - return Status.write_failure - else: - self.output(f"{ca.path}: dry-run. {src_style_name} tags not copied") + if ca.write_metadata(md, style): + self.output(f"{ca.path}: Copied {src_style_name} tags to {dst_style_name}.") return Status.success - self.output(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.") + else: + self.output(f"{ca.path}: Tag copy seemed to fail!") + return Status.write_failure + else: + self.output(f"{ca.path}: dry-run. {src_style_name} tags not copied") + return Status.success return Status.read_failure def copy(self, ca: ComicArchive) -> Result: + src_style_name = md_styles[self.config.Commands__copy].name() res = Result(Action.copy, Status.success, ca.path) + if not ca.has_metadata(self.config.Commands__copy): + self.output(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.") + res.status = Status.read_failure + return res try: res.md = ca.read_metadata(self.config.Commands__copy) except Exception as e: logger.error("Failed to load metadata for %s: %s", ca.path, e) return res - for metadata_style in self.config.Runtime_Options__type: - status = self.copy_style(ca, res.md, metadata_style) + for style in self.config.Runtime_Options__type: + status = self.copy_style(ca, res.md, style) if status == Status.success: - res.tags_written.append(metadata_style) + res.tags_written.append(style) else: res.status = status return res def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> Result: if not self.config.Runtime_Options__overwrite: - for metadata_style in self.config.Runtime_Options__type: - if ca.has_metadata(metadata_style): - self.output(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.") + for style in self.config.Runtime_Options__type: + if ca.has_metadata(style): + self.output(f"{ca.path}: Already has {md_styles[style].name()} tags. Not overwriting.") return Result( Action.save, original_path=ca.path, @@ -443,7 +402,7 @@ class CLI: md = self.create_local_metadata(ca) if md.issue is None or md.issue == "": - if self.config.Auto_Tag__assume_1_if_no_issue_num: + if self.config.Auto_Tag__assume_issue_one: md.issue = "1" matches: list[IssueResult] = [] diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index 0b44007..2f1f968 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -23,6 +23,7 @@ import platform import settngs from comicapi import utils +from comicapi.comicarchive import metadata_styles from comicapi.genericmetadata import GenericMetadata from comictaggerlib import ctversion from comictaggerlib.ctsettings.settngs_namespace import settngs_namespace as ct_ns @@ -167,7 +168,7 @@ def register_runtime(parser: settngs.Manager) -> None: parser.add_setting( "-t", "--type", - metavar="{CR,CBL,COMET}", + metavar=f"{{{','.join(metadata_styles).upper()}}}", default=[], type=metadata_type, help="""Specify TYPE as either CR, CBL or COMET\n(as either ComicRack, ComicBookLover,\nor CoMet style tags, respectively).\nUse commas for multiple types.\nFor searching the metadata will use the first listed:\neg '-t cbl,cr' with no CBL tags, CR will be used if they exist\n\n""", @@ -210,7 +211,7 @@ def register_commands(parser: settngs.Manager) -> None: "-c", "--copy", type=metadata_type_single, - metavar="{CR,CBL,COMET}", + metavar=f"{{{','.join(metadata_styles).upper()}}}", help="Copy the specified source tag block to\ndestination style specified via -t\n(potentially lossy operation).\n\n", file=False, ) diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 880459e..9bee2a3 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -17,8 +17,8 @@ def general(parser: settngs.Manager) -> None: def internal(parser: settngs.Manager) -> None: # automatic settings parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False) - parser.add_setting("save_data_style", default=0, cmdline=False) - parser.add_setting("load_data_style", default=0, cmdline=False) + parser.add_setting("save_data_style", default="cbi", cmdline=False) + parser.add_setting("load_data_style", default="cbi", cmdline=False) parser.add_setting("last_opened_folder", default="", cmdline=False) parser.add_setting("window_width", default=0, cmdline=False) parser.add_setting("window_height", default=0, cmdline=False) @@ -212,7 +212,6 @@ def autotag(parser: settngs.Manager) -> None: parser.add_setting( "-1", "--assume-issue-one", - dest="assume_1_if_no_issue_num", action=argparse.BooleanOptionalAction, help="Assume issue number is 1 if not found (relevant for -s).\n\n", default=False, diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py index 66d03f3..1f1c156 100644 --- a/comictaggerlib/ctsettings/settngs_namespace.py +++ b/comictaggerlib/ctsettings/settngs_namespace.py @@ -11,7 +11,7 @@ import comictaggerlib.resulttypes class settngs_namespace(settngs.TypedNS): Commands__version: bool Commands__command: comictaggerlib.resulttypes.Action - Commands__copy: int + Commands__copy: str Runtime_Options__config: comictaggerlib.ctsettings.types.ComicTaggerPaths Runtime_Options__verbose: int @@ -32,14 +32,14 @@ class settngs_namespace(settngs.TypedNS): Runtime_Options__glob: bool Runtime_Options__quiet: bool Runtime_Options__json: bool - Runtime_Options__type: list[int] + Runtime_Options__type: list[str] Runtime_Options__overwrite: bool Runtime_Options__no_gui: bool Runtime_Options__files: list[str] internal__install_id: str - internal__save_data_style: int - internal__load_data_style: int + internal__save_data_style: str + internal__load_data_style: str internal__last_opened_folder: str internal__window_width: int internal__window_height: int @@ -91,7 +91,7 @@ class settngs_namespace(settngs.TypedNS): Auto_Tag__save_on_low_confidence: bool Auto_Tag__dont_use_year_when_identifying: bool - Auto_Tag__assume_1_if_no_issue_num: bool + Auto_Tag__assume_issue_one: bool Auto_Tag__ignore_leading_numbers_in_filename: bool Auto_Tag__remove_archive_after_successful_match: bool diff --git a/comictaggerlib/ctsettings/types.py b/comictaggerlib/ctsettings/types.py index 91e4d3a..3be08cf 100644 --- a/comictaggerlib/ctsettings/types.py +++ b/comictaggerlib/ctsettings/types.py @@ -6,7 +6,7 @@ import pathlib from appdirs import AppDirs from comicapi import utils -from comicapi.comicarchive import MetaDataStyle +from comicapi.comicarchive import metadata_styles from comicapi.genericmetadata import GenericMetadata @@ -58,22 +58,22 @@ class ComicTaggerPaths(AppDirs): return pathlib.Path(super().site_config_dir) -def metadata_type_single(types: str) -> int: +def metadata_type_single(types: str) -> str: result = metadata_type(types) if len(result) > 1: raise argparse.ArgumentTypeError(f"invalid choice: {result} (only one metadata style allowed)") return result[0] -def metadata_type(types: str) -> list[int]: +def metadata_type(types: str) -> list[str]: result = [] types = types.casefold() for typ in utils.split(types, ","): typ = typ.strip() - if typ not in MetaDataStyle.short_name: - choices = ", ".join(MetaDataStyle.short_name) + if typ not in metadata_styles: + choices = ", ".join(metadata_styles) raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})") - result.append(MetaDataStyle.short_name.index(typ)) + result.append(metadata_styles[typ].short_name) return result diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index de8e26d..bd5c5ce 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -23,7 +23,7 @@ from typing import Callable, cast from PyQt5 import QtCore, QtWidgets, uic from comicapi import utils -from comicapi.comicarchive import ComicArchive +from comicapi.comicarchive import ComicArchive, metadata_styles from comictaggerlib.ctsettings import ct_ns from comictaggerlib.graphics import graphics_path from comictaggerlib.optionalmsgdialog import OptionalMessageDialog @@ -206,7 +206,7 @@ class FileSelectionList(QtWidgets.QWidget): if row is not None: ca = self.get_archive_by_row(row) rar_added_ro = bool(ca and ca.archiver.name() == "RAR" and not ca.archiver.is_writable()) - if first_added is None: + if first_added is None and row != -1: first_added = row progdialog.hide() @@ -286,7 +286,7 @@ class FileSelectionList(QtWidgets.QWidget): filename_item = QtWidgets.QTableWidgetItem() folder_item = QtWidgets.QTableWidgetItem() - cix_item = FileTableWidgetItem() + md_item = FileTableWidgetItem() cbi_item = FileTableWidgetItem() readonly_item = FileTableWidgetItem() type_item = QtWidgets.QTableWidgetItem() @@ -301,9 +301,9 @@ class FileSelectionList(QtWidgets.QWidget): type_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) self.twList.setItem(row, FileSelectionList.typeColNum, type_item) - cix_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) - cix_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) - self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item) + md_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) + md_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) + self.twList.setItem(row, FileSelectionList.CRFlagColNum, md_item) cbi_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) cbi_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) @@ -324,8 +324,7 @@ class FileSelectionList(QtWidgets.QWidget): filename_item = self.twList.item(row, FileSelectionList.fileColNum) folder_item = self.twList.item(row, FileSelectionList.folderColNum) - cix_item = self.twList.item(row, FileSelectionList.CRFlagColNum) - cbi_item = self.twList.item(row, FileSelectionList.CBLFlagColNum) + md_item = self.twList.item(row, FileSelectionList.CRFlagColNum) type_item = self.twList.item(row, FileSelectionList.typeColNum) readonly_item = self.twList.item(row, FileSelectionList.readonlyColNum) @@ -341,19 +340,10 @@ class FileSelectionList(QtWidgets.QWidget): type_item.setText(item_text) type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text) - if fi.ca.has_cix(): - cix_item.setCheckState(QtCore.Qt.CheckState.Checked) - cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, True) - else: - cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, False) - cix_item.setCheckState(QtCore.Qt.CheckState.Unchecked) - - if fi.ca.has_cbi(): - cbi_item.setCheckState(QtCore.Qt.CheckState.Checked) - cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, True) - else: - cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, False) - cbi_item.setCheckState(QtCore.Qt.CheckState.Unchecked) + styles = ", ".join( + metadata_styles[x].name() for x in fi.ca.get_supported_metadata() if fi.ca.has_metadata(x) + ) + md_item.setText(styles) if not fi.ca.is_writable(): readonly_item.setCheckState(QtCore.Qt.CheckState.Checked) @@ -362,13 +352,6 @@ class FileSelectionList(QtWidgets.QWidget): readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, False) readonly_item.setCheckState(QtCore.Qt.CheckState.Unchecked) - # Reading these will force them into the ComicArchive's cache - try: - fi.ca.read_cix() - except Exception: - pass - fi.ca.has_cbi() - def get_selected_archive_list(self) -> list[ComicArchive]: ca_list: list[ComicArchive] = [] for r in range(self.twList.rowCount()): diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index 82c6d58..18be982 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -23,7 +23,7 @@ from typing import Any, Callable from typing_extensions import NotRequired, TypedDict from comicapi import utils -from comicapi.comicarchive import ComicArchive +from comicapi.comicarchive import ComicArchive, metadata_styles from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString from comictaggerlib.ctsettings import ct_ns @@ -229,10 +229,10 @@ class IssueIdentifier: # see if the archive has any useful meta data for searching with try: - if ca.has_cix(): - internal_metadata = ca.read_cix() - else: - internal_metadata = ca.read_cbi() + for style in metadata_styles: + internal_metadata = ca.read_metadata(style) + if not internal_metadata.is_empty: + break except Exception as e: internal_metadata = GenericMetadata() logger.error("Failed to load metadata for %s: %s", ca.path, e) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index c42a8e4..6e4c2d6 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -122,6 +122,7 @@ class App: def load_plugins(self, opts: argparse.Namespace) -> None: comicapi.comicarchive.load_archive_plugins() + comicapi.comicarchive.load_metadata_plugins(version=version) self.talkers = comictalker.get_talkers(version, opts.config.user_cache_dir) def list_plugins( diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 43315ff..e64279e 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -17,12 +17,13 @@ from __future__ import annotations import logging -from PyQt5 import QtCore, QtGui, QtWidgets, uic +from PyQt5 import QtCore, QtWidgets, uic -from comicapi.comicarchive import ComicArchive, MetaDataStyle +from comicapi.comicarchive import ComicArchive, metadata_styles from comicapi.genericmetadata import ImageMetadata, PageType from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ui import ui_path +from comictaggerlib.ui.qtutils import enable_widget logger = logging.getLogger(__name__) @@ -71,6 +72,15 @@ class PageListEditor(QtWidgets.QWidget): with (ui_path / "pagelisteditor.ui").open(encoding="utf-8") as uifile: uic.loadUi(uifile, self) + self.md_attributes = { + "page.image_index": [self.btnDown, self.btnUp, self.listWidget], + "page.type": self.cbPageType, + "page.double_page": self.chkDoublePage, + "page.bookmark": self.leBookmark, + # Python dicts are order preserving this must be placed last + "pages": [self.btnDown, self.btnUp, self.listWidget, self.cbPageType, self.chkDoublePage, self.leBookmark], + } + self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode, None, None) gridlayout = QtWidgets.QGridLayout(self.pageContainer) gridlayout.addWidget(self.pageWidget) @@ -105,6 +115,7 @@ class PageListEditor(QtWidgets.QWidget): self.comic_archive: ComicArchive | None = None self.pages_list: list[ImageMetadata] = [] + self.data_style = "" def reset_page(self) -> None: self.pageWidget.clear() @@ -220,14 +231,14 @@ class PageListEditor(QtWidgets.QWidget): i = self.cbPageType.findData(pagetype) self.cbPageType.setCurrentIndex(i) - self.chkDoublePage.setChecked("DoublePage" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]) + self.chkDoublePage.setChecked("double_page" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]) - if "Bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]: - self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["Bookmark"]) + if "bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]: + self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["bookmark"]) else: self.leBookmark.setText("") - idx = int(self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]["Image"]) + idx = int(self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]["image_index"]) if self.comic_archive is not None: self.pageWidget.set_archive(self.comic_archive, idx) @@ -237,16 +248,16 @@ class PageListEditor(QtWidgets.QWidget): for i in range(self.listWidget.count()): item = self.listWidget.item(i) page_dict: ImageMetadata = item.data(QtCore.Qt.ItemDataRole.UserRole)[0] - if "Type" in page_dict and page_dict["Type"] == PageType.FrontCover: - front_cover = int(page_dict["Image"]) + if "type" in page_dict and page_dict["type"] == PageType.FrontCover: + front_cover = int(page_dict["image_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)[0] - if "Type" in page_dict: - return page_dict["Type"] + if "type" in page_dict: + return page_dict["type"] return "" @@ -255,10 +266,10 @@ class PageListEditor(QtWidgets.QWidget): page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0] if t == "": - if "Type" in page_dict: - del page_dict["Type"] + if "type" in page_dict: + del page_dict["type"] else: - page_dict["Type"] = t + page_dict["type"] = t item = self.listWidget.item(row) # wrap the dict in a tuple to keep from being converted to QtWidgets.QStrings @@ -272,11 +283,11 @@ class PageListEditor(QtWidgets.QWidget): cbx = self.sender() if isinstance(cbx, QtWidgets.QCheckBox) and cbx.isChecked(): - if "DoublePage" not in page_dict: - page_dict["DoublePage"] = True + if "double_page" not in page_dict: + page_dict["double_page"] = True self.modified.emit() - elif "DoublePage" in page_dict: - del page_dict["DoublePage"] + elif "double_page" in page_dict: + del page_dict["double_page"] self.modified.emit() item = self.listWidget.item(row) @@ -291,16 +302,16 @@ class PageListEditor(QtWidgets.QWidget): page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0] current_bookmark = "" - if "Bookmark" in page_dict: - current_bookmark = page_dict["Bookmark"] + if "bookmark" in page_dict: + current_bookmark = page_dict["bookmark"] if self.leBookmark.text().strip(): new_bookmark = str(self.leBookmark.text().strip()) if current_bookmark != new_bookmark: - page_dict["Bookmark"] = new_bookmark + page_dict["bookmark"] = new_bookmark self.modified.emit() elif current_bookmark != "": - del page_dict["Bookmark"] + del page_dict["bookmark"] self.modified.emit() item = self.listWidget.item(row) @@ -313,10 +324,12 @@ class PageListEditor(QtWidgets.QWidget): def set_data(self, comic_archive: ComicArchive, pages_list: list[ImageMetadata]) -> None: self.comic_archive = comic_archive self.pages_list = pages_list - if pages_list is not None and len(pages_list) > 0: - self.cbPageType.setDisabled(False) - self.chkDoublePage.setDisabled(False) - self.leBookmark.setDisabled(False) + if pages_list: + self.set_metadata_style(self.data_style) + else: + self.cbPageType.setEnabled(False) + self.chkDoublePage.setEnabled(False) + self.leBookmark.setEnabled(False) self.listWidget.itemSelectionChanged.disconnect(self.change_page) @@ -332,15 +345,15 @@ class PageListEditor(QtWidgets.QWidget): self.listWidget.setCurrentRow(0) def list_entry_text(self, page_dict: ImageMetadata) -> str: - text = str(int(page_dict["Image"]) + 1) - if "Type" in page_dict: - if page_dict["Type"] in self.pageTypeNames: - text += " (" + self.pageTypeNames[page_dict["Type"]] + ")" + text = str(int(page_dict["image_index"]) + 1) + if "type" in page_dict: + if page_dict["type"] in self.pageTypeNames: + text += " (" + self.pageTypeNames[page_dict["type"]] + ")" else: - text += " (Error: " + page_dict["Type"] + ")" - if "DoublePage" in page_dict: + text += " (Error: " + page_dict["type"] + ")" + if "double_page" in page_dict: text += " ②" - if "Bookmark" in page_dict: + if "bookmark" in page_dict: text += " 🔖" return text @@ -356,42 +369,11 @@ class PageListEditor(QtWidgets.QWidget): self.first_front_page = self.get_first_front_cover() self.firstFrontCoverChanged.emit(self.first_front_page) - def set_metadata_style(self, data_style: int) -> None: + def set_metadata_style(self, data_style: str) -> None: # depending on the current data style, certain fields are disabled + if data_style: + self.metadata_style = data_style - inactive_color = QtGui.QColor(255, 170, 150) - active_palette = self.cbPageType.palette() - - inactive_palette3 = self.cbPageType.palette() - inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color) - - if data_style == MetaDataStyle.CIX: - self.btnUp.setEnabled(True) - self.btnDown.setEnabled(True) - self.cbPageType.setEnabled(True) - self.chkDoublePage.setEnabled(True) - self.leBookmark.setEnabled(True) - self.listWidget.setEnabled(True) - - self.leBookmark.setPalette(active_palette) - self.listWidget.setPalette(active_palette) - - elif data_style == MetaDataStyle.CBI: - self.btnUp.setEnabled(False) - self.btnDown.setEnabled(False) - self.cbPageType.setEnabled(False) - self.chkDoublePage.setEnabled(False) - self.leBookmark.setEnabled(False) - self.listWidget.setEnabled(False) - - self.leBookmark.setPalette(inactive_palette3) - self.listWidget.setPalette(inactive_palette3) - - elif data_style == MetaDataStyle.COMET: - pass - - # make sure combo is disabled when no list - if self.comic_archive is None: - self.cbPageType.setEnabled(False) - self.chkDoublePage.setEnabled(False) - self.leBookmark.setEnabled(False) + enabled_widgets = metadata_styles[data_style].supported_attributes + for metadata, widget in self.md_attributes.items(): + enable_widget(widget, metadata in enabled_widgets) diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 16c5cdc..28078d7 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -21,7 +21,7 @@ import settngs from PyQt5 import QtCore, QtWidgets, uic from comicapi import utils -from comicapi.comicarchive import ComicArchive, MetaDataStyle +from comicapi.comicarchive import ComicArchive, metadata_styles from comicapi.genericmetadata import GenericMetadata from comictaggerlib.ctsettings import ct_ns from comictaggerlib.filerenamer import FileRenamer, get_rename_dir @@ -38,7 +38,7 @@ class RenameWindow(QtWidgets.QDialog): self, parent: QtWidgets.QWidget, comic_archive_list: list[ComicArchive], - data_style: int, + data_style: str, config: settngs.Config[ct_ns], talkers: dict[str, ComicTalker], ) -> None: @@ -46,7 +46,8 @@ class RenameWindow(QtWidgets.QDialog): with (ui_path / "renamewindow.ui").open(encoding="utf-8") as uifile: uic.loadUi(uifile, self) - self.label.setText(f"Preview (based on {MetaDataStyle.name[data_style]} tags):") + + self.label.setText(f"Preview (based on {metadata_styles[data_style].name()} tags):") self.setWindowFlags( QtCore.Qt.WindowType( diff --git a/comictaggerlib/resulttypes.py b/comictaggerlib/resulttypes.py index e0196ba..a0237a1 100644 --- a/comictaggerlib/resulttypes.py +++ b/comictaggerlib/resulttypes.py @@ -121,8 +121,8 @@ class Result: md: GenericMetadata | None = None - tags_deleted: list[int] = dataclasses.field(default_factory=list) - tags_written: list[int] = dataclasses.field(default_factory=list) + tags_deleted: list[str] = dataclasses.field(default_factory=list) + tags_written: list[str] = dataclasses.field(default_factory=list) def __str__(self) -> str: if len(self.online_results) == 0: diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 8a5601b..9601917 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -15,13 +15,12 @@ # limitations under the License. from __future__ import annotations -import json +import functools import logging import operator import os import pickle import platform -import pprint import re import sys import webbrowser @@ -33,9 +32,9 @@ import natsort import settngs from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic +import comictaggerlib.ui from comicapi import utils -from comicapi.comicarchive import ComicArchive, MetaDataStyle -from comicapi.comicinfoxml import ComicInfoXml +from comicapi.comicarchive import ComicArchive, metadata_styles from comicapi.filenameparser import FileNameParser from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString @@ -61,7 +60,7 @@ from comictaggerlib.resulttypes import Action, IssueResult, MatchStatus, OnlineM from comictaggerlib.seriesselectionwindow import SeriesSelectionWindow from comictaggerlib.settingswindow import SettingsWindow from comictaggerlib.ui import ui_path -from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size +from comictaggerlib.ui.qtutils import center_window_on_parent, enable_widget, reduce_widget_font_size from comictaggerlib.versionchecker import VersionChecker from comictalker.comictalker import ComicTalker, TalkerError from comictalker.talker_utils import cleanup_html @@ -88,6 +87,62 @@ class TaggerWindow(QtWidgets.QMainWindow): with (ui_path / "taggerwindow.ui").open(encoding="utf-8") as uifile: uic.loadUi(uifile, self) + + self.md_attributes = { + "tag_origin": None, + "issue_id": None, + "series": self.leSeries, + "issue": self.leIssueNum, + "title": self.leTitle, + "publisher": self.lePublisher, + "month": self.lePubMonth, + "year": self.lePubYear, + "day": self.lePubDay, + "issue_count": self.leIssueCount, + "volume": self.leVolumeNum, + "genres": self.leGenre, + "language": self.cbLanguage, + "description": self.teComments, + "volume_count": self.leVolumeCount, + "critical_rating": self.dsbCriticalRating, + "country": self.cbCountry, + "alternate_series": self.leAltSeries, + "alternate_number": self.leAltIssueNum, + "alternate_count": self.leAltIssueCount, + "imprint": self.leImprint, + "notes": self.teNotes, + "web_link": self.leWebLink, + "format": self.cbFormat, + "manga": self.cbManga, + "black_and_white": self.cbBW, + "page_count": None, + "maturity_rating": self.cbMaturityRating, + "story_arcs": self.leStoryArc, + "series_groups": self.leSeriesGroup, + "scan_info": self.leScanInfo, + "characters": self.teCharacters, + "teams": self.teTeams, + "locations": self.teLocations, + "credits": [self.twCredits, self.btnAddCredit, self.btnEditCredit, self.btnRemoveCredit], + "credits.person": 2, + "credits.role": 1, + "credits.primary": 0, + "tags": self.teTags, + "pages": None, + "page.type": None, + "page.bookmark": None, + "page.double_page": None, + "page.image_index": None, + "page.size": None, + "page.height": None, + "page.width": None, + "price": None, + "is_version_of": None, + "rights": None, + "identifier": None, + "last_mark": None, + } + comictaggerlib.ui.qtutils.active_palette = self.leSeries.palette() self.config = config self.talkers = talkers self.log_window = self.setup_logger() @@ -157,15 +212,20 @@ class TaggerWindow(QtWidgets.QMainWindow): self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png"))) - if config[0].Runtime_Options__type and isinstance(config[0].Runtime_Options__type[0], int): + if config[0].Runtime_Options__type and isinstance(config[0].Runtime_Options__type[0], str): # respect the command line option tag type config[0].internal__save_data_style = config[0].Runtime_Options__type[0] config[0].internal__load_data_style = config[0].Runtime_Options__type[0] + if config[0].internal__save_data_style not in metadata_styles: + config[0].internal__save_data_style = list(metadata_styles.keys())[0] + if config[0].internal__load_data_style not in metadata_styles: + config[0].internal__load_data_style = list(metadata_styles.keys())[0] self.save_data_style = config[0].internal__save_data_style self.load_data_style = config[0].internal__load_data_style self.setAcceptDrops(True) + self.view_tag_actions, self.remove_tag_actions = self.tag_actions() self.config_menus() self.statusBar() self.populate_combo_boxes() @@ -225,7 +285,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.page_list_editor.listOrderChanged.connect(self.page_list_order_changed) self.tabWidget.currentChanged.connect(self.tab_changed) - self.update_style_tweaks() + self.update_metadata_style_tweaks() self.show() self.set_app_position() @@ -244,7 +304,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.fileSelectionList.add_app_action(self.actionRemoveAuto) self.fileSelectionList.add_app_action(self.actionRepackage) - if len(file_list) != 0: + if file_list: self.fileSelectionList.add_path_list(file_list) if self.config[0].Dialog_Flags__show_disclaimer: @@ -271,6 +331,20 @@ class TaggerWindow(QtWidgets.QMainWindow): if self.config[0].General__check_for_new_version: self.check_latest_version_online() + def tag_actions(self) -> tuple[dict[str, QtGui.QAction], dict[str, QtGui.QAction]]: + view_raw_tags = {} + remove_raw_tags = {} + for style in metadata_styles.values(): + view_raw_tags[style.short_name] = self.menuViewRawTags.addAction(f"View Raw {style.name()} Tags") + view_raw_tags[style.short_name].setStatusTip(f"View raw {style.name()} tag block from file") + view_raw_tags[style.short_name].triggered.connect(functools.partial(self.view_raw_tags, style.short_name)) + + remove_raw_tags[style.short_name] = self.menuRemove.addAction(f"Remove Raw {style.name()} Tags") + remove_raw_tags[style.short_name].setStatusTip(f"Remove {style.name()} tags from comic archive") + remove_raw_tags[style.short_name].triggered.connect(functools.partial(self.remove_tags, style.short_name)) + + return view_raw_tags, remove_raw_tags + def current_talker(self) -> ComicTalker: if self.config[0].Sources__source in self.talkers: return self.talkers[self.config[0].Sources__source] @@ -366,18 +440,6 @@ class TaggerWindow(QtWidgets.QMainWindow): self.actionRemoveAuto.setStatusTip("Remove currently selected modify tag style from the archive") self.actionRemoveAuto.triggered.connect(self.remove_auto) - self.actionRemoveCBLTags.setStatusTip("Remove ComicBookLover tags from comic archive") - self.actionRemoveCBLTags.triggered.connect(self.remove_cbl_tags) - - self.actionRemoveCRTags.setStatusTip("Remove ComicRack tags from comic archive") - self.actionRemoveCRTags.triggered.connect(self.remove_cr_tags) - - self.actionViewRawCRTags.setStatusTip("View raw ComicRack tag block from file") - self.actionViewRawCRTags.triggered.connect(self.view_raw_cr_tags) - - self.actionViewRawCBLTags.setStatusTip("View raw ComicBookLover tag block from file") - self.actionViewRawCBLTags.triggered.connect(self.view_raw_cbl_tags) - self.actionRepackage.setShortcut("Ctrl+E") self.actionRepackage.setStatusTip("Re-create archive as CBZ") self.actionRepackage.triggered.connect(self.repackage_archive) @@ -648,48 +710,27 @@ class TaggerWindow(QtWidgets.QMainWindow): self.archiveCoverWidget.set_archive(self.comic_archive, cover_idx) def update_menus(self) -> None: - # First just disable all the questionable items - self.actionAutoTag.setEnabled(False) - self.actionCopyTags.setEnabled(False) - self.actionRemoveAuto.setEnabled(False) - self.actionRemoveCRTags.setEnabled(False) - self.actionRemoveCBLTags.setEnabled(False) - self.actionWrite_Tags.setEnabled(False) - self.actionRepackage.setEnabled(False) - self.actionViewRawCBLTags.setEnabled(False) - self.actionViewRawCRTags.setEnabled(False) - self.actionParse_Filename.setEnabled(False) - self.actionParse_Filename_split_words.setEnabled(False) - self.actionAutoIdentify.setEnabled(False) - self.actionRename.setEnabled(False) - self.actionApplyCBLTransform.setEnabled(False) - self.actionReCalcPageDims.setEnabled(False) + enabled = self.comic_archive is not None + writeable = self.comic_archive is not None and self.comic_archive.is_writable() + self.actionApplyCBLTransform.setEnabled(enabled) + self.actionAutoIdentify.setEnabled(enabled) + self.actionAutoTag.setEnabled(enabled) + self.actionCopyTags.setEnabled(enabled) + self.actionParse_Filename.setEnabled(enabled) + self.actionParse_Filename_split_words.setEnabled(enabled) + self.actionReCalcPageDims.setEnabled(enabled) + self.actionRemoveAuto.setEnabled(enabled) + self.actionRename.setEnabled(enabled) + self.actionRepackage.setEnabled(enabled) - # now, selectively re-enable + self.menuRemove.setEnabled(enabled) + self.menuViewRawTags.setEnabled(enabled) if self.comic_archive is not None: - has_cix = self.comic_archive.has_cix() - has_cbi = self.comic_archive.has_cbi() + for style in metadata_styles: + self.view_tag_actions[style].setEnabled(self.comic_archive.has_metadata(style)) + self.remove_tag_actions[style].setEnabled(self.comic_archive.has_metadata(style)) - self.actionParse_Filename.setEnabled(True) - self.actionParse_Filename_split_words.setEnabled(True) - self.actionAutoIdentify.setEnabled(True) - self.actionAutoTag.setEnabled(True) - self.actionRename.setEnabled(True) - self.actionApplyCBLTransform.setEnabled(True) - self.actionReCalcPageDims.setEnabled(True) - self.actionRepackage.setEnabled(True) - self.actionRemoveAuto.setEnabled(True) - self.actionRemoveCRTags.setEnabled(True) - self.actionRemoveCBLTags.setEnabled(True) - self.actionCopyTags.setEnabled(True) - - if has_cix: - self.actionViewRawCRTags.setEnabled(True) - if has_cbi: - self.actionViewRawCBLTags.setEnabled(True) - - if self.comic_archive.is_writable(): - self.actionWrite_Tags.setEnabled(True) + self.actionWrite_Tags.setEnabled(writeable) def update_info_box(self) -> None: ca = self.comic_archive @@ -712,13 +753,11 @@ class TaggerWindow(QtWidgets.QMainWindow): page_count = f" ({ca.get_number_of_pages()} pages)" self.lblPageCount.setText(page_count) + supported_md = ca.get_supported_metadata() tag_info = "" - if ca.has_cix(): - tag_info = "• ComicRack tags" - if ca.has_cbi(): - if tag_info != "": - tag_info += "\n" - tag_info += "• ComicBookLover tags" + for md in supported_md: + if ca.has_metadata(md): + tag_info += "• " + metadata_styles[md].name() + "\n" self.lblTagList.setText(tag_info) @@ -872,7 +911,7 @@ class TaggerWindow(QtWidgets.QMainWindow): ) self.twCredits.setSortingEnabled(True) - self.update_credit_colors() + self.update_metadata_credit_colors() def add_new_credit_entry(self, row: int, role: str, name: str, primary_flag: bool = False) -> None: self.twCredits.insertRow(row) @@ -1111,7 +1150,7 @@ class TaggerWindow(QtWidgets.QMainWindow): reply = QtWidgets.QMessageBox.question( self, "Save Tags", - f"Are you sure you wish to save {MetaDataStyle.name[self.save_data_style]} tags to this archive?", + f"Are you sure you wish to save {metadata_styles[self.save_data_style].name()} tags to this archive?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No, ) @@ -1121,7 +1160,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.form_to_metadata() success = self.comic_archive.write_metadata(self.metadata, self.save_data_style) - self.comic_archive.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX]) + self.comic_archive.load_cache(list(metadata_styles)) QtWidgets.QApplication.restoreOverrideCursor() if not success: @@ -1137,7 +1176,7 @@ class TaggerWindow(QtWidgets.QMainWindow): else: QtWidgets.QMessageBox.information(self, "Whoops!", "No data to commit!") - def set_load_data_style(self, s: int) -> None: + def set_load_data_style(self, s: str) -> None: if self.dirty_flag_verification( "Change Tag Read Style", "If you change read tag style now, data in the form will be lost. Are you sure?" ): @@ -1151,118 +1190,35 @@ class TaggerWindow(QtWidgets.QMainWindow): self.adjust_load_style_combo() self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style) - def set_save_data_style(self, s: int) -> None: + def set_save_data_style(self, s: str) -> None: self.save_data_style = self.cbSaveDataStyle.itemData(s) self.config[0].internal__save_data_style = self.save_data_style - self.update_style_tweaks() + self.update_metadata_style_tweaks() self.update_menus() def set_source(self, s: int) -> None: self.config[0].Sources__source = self.cbx_sources.itemData(s) - def update_credit_colors(self) -> None: - # !!!ATB qt5 porting TODO - inactive_color = QtGui.QColor(255, 170, 150) - active_palette = self.leSeries.palette() - active_color = active_palette.color(QtGui.QPalette.ColorRole.Base) + def update_metadata_credit_colors(self) -> None: + style = metadata_styles[self.save_data_style] + enabled = style.supported_attributes + credit_attributes = [x for x in self.md_attributes.items() if "credits." in x[0]] - inactive_brush = QtGui.QBrush(inactive_color) - active_brush = QtGui.QBrush(active_color) + for r in range(self.twCredits.rowCount()): + for credit in credit_attributes: + widget_enabled = credit[0] in enabled + widget = self.twCredits.item(r, credit[1]) + enable_widget(widget, widget_enabled) - cix_credits = ComicInfoXml().get_parseable_credits() - - if self.save_data_style == MetaDataStyle.CIX: - # loop over credit table, mark selected rows - for r in range(self.twCredits.rowCount()): - if str(self.twCredits.item(r, 1).text()).casefold() not in cix_credits: - self.twCredits.item(r, 1).setBackground(inactive_brush) - else: - self.twCredits.item(r, 1).setBackground(active_brush) - # turn off entire primary column - self.twCredits.item(r, 0).setBackground(inactive_brush) - - if self.save_data_style == MetaDataStyle.CBI: - # loop over credit table, make all active color - for r in range(self.twCredits.rowCount()): - self.twCredits.item(r, 0).setBackground(active_brush) - self.twCredits.item(r, 1).setBackground(active_brush) - - def update_style_tweaks(self) -> None: + def update_metadata_style_tweaks(self) -> None: # depending on the current data style, certain fields are disabled - inactive_color = QtGui.QColor(255, 170, 150) - active_palette = self.leSeries.palette() + enabled_widgets = metadata_styles[self.save_data_style].supported_attributes + for metadata, widget in self.md_attributes.items(): + if isinstance(widget, QtWidgets.QWidget): + enable_widget(widget, metadata in enabled_widgets) - inactive_palette1 = self.leSeries.palette() - inactive_palette1.setColor(QtGui.QPalette.ColorRole.Base, inactive_color) - - inactive_palette2 = self.leSeries.palette() - - inactive_palette3 = self.leSeries.palette() - inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color) - - inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color) - - # helper func - def enable_widget(widget: QtWidgets.QWidget, enable: bool) -> None: - inactive_palette3.setColor(widget.backgroundRole(), inactive_color) - inactive_palette2.setColor(widget.backgroundRole(), inactive_color) - inactive_palette3.setColor(widget.foregroundRole(), inactive_color) - - if enable: - widget.setPalette(active_palette) - widget.setAutoFillBackground(False) - if isinstance(widget, QtWidgets.QCheckBox): - widget.setEnabled(True) - elif isinstance(widget, QtWidgets.QComboBox): - widget.setEnabled(True) - elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)): - widget.setReadOnly(False) - else: - widget.setAutoFillBackground(True) - if isinstance(widget, QtWidgets.QCheckBox): - widget.setPalette(inactive_palette2) - widget.setEnabled(False) - elif isinstance(widget, QtWidgets.QComboBox): - widget.setPalette(inactive_palette3) - widget.setEnabled(False) - elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)): - widget.setReadOnly(True) - widget.setPalette(inactive_palette1) - - cbi_only = [self.leVolumeCount, self.cbCountry, self.teTags] - cix_only = [ - self.leImprint, - self.teNotes, - self.cbBW, - self.cbManga, - self.leStoryArc, - self.leScanInfo, - self.leSeriesGroup, - self.leAltSeries, - self.leAltIssueNum, - self.leAltIssueCount, - self.leWebLink, - self.teCharacters, - self.teTeams, - self.teLocations, - self.cbMaturityRating, - self.cbFormat, - ] - - if self.save_data_style == MetaDataStyle.CIX: - for item in cix_only: - enable_widget(item, True) - for item in cbi_only: - enable_widget(item, False) - - if self.save_data_style == MetaDataStyle.CBI: - for item in cbi_only: - enable_widget(item, True) - for item in cix_only: - enable_widget(item, False) - - self.update_credit_colors() + self.update_metadata_credit_colors() self.page_list_editor.set_metadata_style(self.save_data_style) def cell_double_clicked(self, r: int, c: int) -> None: @@ -1353,7 +1309,7 @@ class TaggerWindow(QtWidgets.QMainWindow): row = self.twCredits.rowCount() self.add_new_credit_entry(row, new_role, new_name, new_primary) - self.update_credit_colors() + self.update_metadata_credit_colors() self.set_dirty_flag() def remove_credit(self) -> None: @@ -1393,27 +1349,20 @@ class TaggerWindow(QtWidgets.QMainWindow): def adjust_load_style_combo(self) -> None: # select the current style - if self.load_data_style == MetaDataStyle.CBI: - self.cbLoadDataStyle.setCurrentIndex(0) - elif self.load_data_style == MetaDataStyle.CIX: - self.cbLoadDataStyle.setCurrentIndex(1) + self.cbLoadDataStyle.setCurrentIndex(self.cbLoadDataStyle.findData(self.load_data_style)) def adjust_save_style_combo(self) -> None: # select the current style - if self.save_data_style == MetaDataStyle.CBI: - self.cbSaveDataStyle.setCurrentIndex(0) - elif self.save_data_style == MetaDataStyle.CIX: - self.cbSaveDataStyle.setCurrentIndex(1) - self.update_style_tweaks() + self.cbSaveDataStyle.setCurrentIndex(self.cbSaveDataStyle.findData(self.save_data_style)) + self.update_metadata_style_tweaks() def populate_combo_boxes(self) -> None: # Add the entries to the tag style combobox - self.cbLoadDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI) - self.cbLoadDataStyle.addItem("ComicRack", MetaDataStyle.CIX) - self.adjust_load_style_combo() + for style in metadata_styles.values(): + self.cbLoadDataStyle.addItem(style.name(), style.short_name) + self.cbSaveDataStyle.addItem(style.name(), style.short_name) - self.cbSaveDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI) - self.cbSaveDataStyle.addItem("ComicRack", MetaDataStyle.CIX) + self.adjust_load_style_combo() self.adjust_save_style_combo() # Add talker entries @@ -1514,13 +1463,7 @@ class TaggerWindow(QtWidgets.QMainWindow): def remove_auto(self) -> None: self.remove_tags(self.save_data_style) - def remove_cbl_tags(self) -> None: - self.remove_tags(MetaDataStyle.CBI) - - def remove_cr_tags(self) -> None: - self.remove_tags(MetaDataStyle.CIX) - - def remove_tags(self, style: int) -> None: + def remove_tags(self, style: str) -> None: # remove the indicated tags from the archive ca_list = self.fileSelectionList.get_selected_archive_list() has_md_count = 0 @@ -1530,7 +1473,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if has_md_count == 0: QtWidgets.QMessageBox.information( - self, "Remove Tags", f"No archives with {MetaDataStyle.name[style]} tags selected!" + self, "Remove Tags", f"No archives with {metadata_styles[style].name()} tags selected!" ) return @@ -1543,7 +1486,7 @@ class TaggerWindow(QtWidgets.QMainWindow): reply = QtWidgets.QMessageBox.question( self, "Remove Tags", - f"Are you sure you wish to remove the {MetaDataStyle.name[style]} tags from {has_md_count} archive(s)?", + f"Are you sure you wish to remove the {metadata_styles[style].name()} tags from {has_md_count} archive(s)?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No, ) @@ -1565,13 +1508,12 @@ class TaggerWindow(QtWidgets.QMainWindow): progdialog.setValue(prog_idx) progdialog.setLabelText(str(ca.path)) QtCore.QCoreApplication.processEvents() - if ca.has_metadata(style): - if ca.is_writable(): - if not ca.remove_metadata(style): - failed_list.append(ca.path) - else: - success_count += 1 - ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX]) + if ca.has_metadata(style) and ca.is_writable(): + if not ca.remove_metadata(style): + failed_list.append(ca.path) + else: + success_count += 1 + ca.load_cache(list(metadata_styles)) progdialog.hide() QtCore.QCoreApplication.processEvents() @@ -1621,8 +1563,8 @@ class TaggerWindow(QtWidgets.QMainWindow): reply = QtWidgets.QMessageBox.question( self, "Copy Tags", - f"Are you sure you wish to copy the {MetaDataStyle.name[src_style]}" - + f" tags to {MetaDataStyle.name[dest_style]} tags in {has_src_count} archive(s)?", + f"Are you sure you wish to copy the {metadata_styles[src_style].name()}" + + f" tags to {metadata_styles[dest_style].name()} tags in {has_src_count} archive(s)?", QtWidgets.QMessageBox.StandardButton.Yes, QtWidgets.QMessageBox.StandardButton.No, ) @@ -1649,10 +1591,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if ca.has_metadata(src_style) and ca.is_writable(): md = ca.read_metadata(src_style) - if ( - dest_style == MetaDataStyle.CBI - and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation - ): + if dest_style == "cbi" and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation: md = CBLTransformer(md, self.config[0]).apply() if not ca.write_metadata(md, dest_style): @@ -1660,7 +1599,7 @@ class TaggerWindow(QtWidgets.QMainWindow): else: success_count += 1 - ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX]) + ca.load_cache([self.load_data_style, self.save_data_style, src_style, dest_style]) prog_dialog.hide() QtCore.QCoreApplication.processEvents() @@ -1874,7 +1813,7 @@ class TaggerWindow(QtWidgets.QMainWindow): ) success = True self.auto_tag_log("Save complete!\n") - ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX]) + ca.load_cache([self.load_data_style, self.save_data_style]) return success, match_results @@ -1896,7 +1835,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.config[0], ( f"You have selected {len(ca_list)} archive(s) to automatically identify and write " - + MetaDataStyle.name[style] + + metadata_styles[style].name() + " tags to.\n\nPlease choose config below, and select OK to Auto-Tag." ), ) @@ -2067,19 +2006,12 @@ class TaggerWindow(QtWidgets.QMainWindow): def page_browser_closed(self) -> None: self.page_browser = None - def view_raw_cr_tags(self) -> None: - if self.comic_archive is not None and self.comic_archive.has_cix(): + def view_raw_tags(self, style: str) -> None: + if self.comic_archive is not None and self.comic_archive.has_metadata(style): + md_style = metadata_styles[style] dlg = LogWindow(self) - dlg.set_text(self.comic_archive.read_raw_cix()) - dlg.setWindowTitle("Raw ComicRack Tag View") - dlg.exec() - - def view_raw_cbl_tags(self) -> None: - if self.comic_archive is not None and self.comic_archive.has_cbi(): - dlg = LogWindow(self) - text = pprint.pformat(json.loads(self.comic_archive.read_raw_cbi()), indent=4) - dlg.set_text(text) - dlg.setWindowTitle("Raw ComicBookLover Tag View") + dlg.set_text(self.comic_archive.read_metadata_string(style)) + dlg.setWindowTitle(f"Raw {md_style.name()} Tag View") dlg.exec() def show_wiki(self) -> None: @@ -2106,12 +2038,12 @@ 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 "ImageSize" in p: - del p["ImageSize"] - if "ImageHeight" in p: - del p["ImageHeight"] - if "ImageWidth" in p: - del p["ImageWidth"] + if "size" in p: + del p["size"] + if "height" in p: + del p["height"] + if "width" in p: + del p["width"] self.set_dirty_flag() QtWidgets.QApplication.restoreOverrideCursor() diff --git a/comictaggerlib/ui/fileselectionlist.ui b/comictaggerlib/ui/fileselectionlist.ui index d59b082..1e98060 100644 --- a/comictaggerlib/ui/fileselectionlist.ui +++ b/comictaggerlib/ui/fileselectionlist.ui @@ -50,21 +50,10 @@ - CR + MD - Has ComicRack Tags - - - AlignCenter - - - - - CBL - - - Has ComicBookLover Tags + Metadata AlignCenter diff --git a/comictaggerlib/ui/qtutils.py b/comictaggerlib/ui/qtutils.py index 94a2f42..ea335a6 100644 --- a/comictaggerlib/ui/qtutils.py +++ b/comictaggerlib/ui/qtutils.py @@ -152,3 +152,64 @@ if qt_available: trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__)) QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", msg + trace) + + active_palette = None + + def enable_widget(widget: QtWidgets.QWidget | list[QtWidgets.QWidget], enable: bool) -> None: + if isinstance(widget, list): + for w in widget: + _enable_widget(w, enable) + else: + _enable_widget(widget, enable) + + def _enable_widget(widget: QtWidgets.QWidget, enable: bool) -> None: + global active_palette + if not (widget and active_palette and widget): + return + active_color = active_palette.color(QtGui.QPalette.ColorRole.Base) + + inactive_color = QtGui.QColor(255, 170, 150) + inactive_brush = QtGui.QBrush(inactive_color) + active_brush = QtGui.QBrush(active_color) + + def palettes() -> tuple[QtGui.QPalette, QtGui.QPalette, QtGui.QPalette]: + inactive_palette1 = QtGui.QPalette(active_palette) + inactive_palette1.setColor(QtGui.QPalette.ColorRole.Base, inactive_color) + + inactive_palette2 = QtGui.QPalette(active_palette) + inactive_palette2.setColor(widget.backgroundRole(), inactive_color) + + inactive_palette3 = QtGui.QPalette(active_palette) + inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color) + inactive_palette3.setColor(widget.foregroundRole(), inactive_color) + return inactive_palette1, inactive_palette2, inactive_palette3 + + if enable: + if isinstance(widget, QtWidgets.QTableWidgetItem): + widget.setBackground(active_brush) + return + + widget.setAutoFillBackground(False) + widget.setPalette(active_palette) + if isinstance(widget, (QtWidgets.QCheckBox, QtWidgets.QComboBox, QtWidgets.QPushButton)): + widget.setEnabled(True) + elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)): + widget.setReadOnly(False) + elif isinstance(widget, QtWidgets.QListWidget): + widget.setMovement(QtWidgets.QListWidget.Free) + else: + if isinstance(widget, QtWidgets.QTableWidgetItem): + widget.setBackground(inactive_brush) + return + + widget.setAutoFillBackground(True) + if isinstance(widget, (QtWidgets.QCheckBox, QtWidgets.QComboBox, QtWidgets.QPushButton)): + inactive_palette = palettes() + widget.setPalette(inactive_palette[1]) + widget.setEnabled(False) + elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)): + inactive_palette = palettes() + widget.setReadOnly(True) + widget.setPalette(inactive_palette[0]) + elif isinstance(widget, QtWidgets.QListWidget): + widget.setMovement(QtWidgets.QListWidget.Static) diff --git a/comictaggerlib/ui/taggerwindow.ui b/comictaggerlib/ui/taggerwindow.ui index 0dc4b2e..0033a28 100644 --- a/comictaggerlib/ui/taggerwindow.ui +++ b/comictaggerlib/ui/taggerwindow.ui @@ -1178,15 +1178,11 @@ Remove Tags - - View Raw Tags - - diff --git a/setup.cfg b/setup.cfg index 0f12bd0..9402946 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,6 +62,10 @@ comicapi.archiver = sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver rar = comicapi.archivers.rar:RarArchiver folder = comicapi.archivers.folder:FolderArchiver +comicapi.metadata = + cr = comicapi.metadata.comicrack:ComicRack + cbi = comicapi.metadata.comicbookinfo:ComicBookInfo + comet = comicapi.metadata.comet:CoMet comictagger.talker = comicvine = comictalker.talkers.comicvine:ComicVineTalker pyinstaller40 = diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index eebd5b2..1f4fb8e 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -32,45 +32,45 @@ def test_getPageNameList(): def test_page_type_read(cbz): - md = cbz.read_cix() + md = cbz.read_metadata("cr") - assert isinstance(md.pages[0]["Type"], str) + assert isinstance(md.pages[0]["type"], str) def test_metadata_read(cbz, md_saved): - md = cbz.read_cix() + md = cbz.read_metadata("cr") assert md == md_saved -def test_save_cix(tmp_comic): - md = tmp_comic.read_cix() +def test_save_cr(tmp_comic): + md = tmp_comic.read_metadata("cr") md.set_default_page_list(tmp_comic.get_number_of_pages()) - assert tmp_comic.write_cix(md) + assert tmp_comic.write_metadata(md, "cr") - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") def test_save_cbi(tmp_comic): - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") md.set_default_page_list(tmp_comic.get_number_of_pages()) - assert tmp_comic.write_cbi(md) + assert tmp_comic.write_metadata(md, "cbi") - md = tmp_comic.read_cbi() + md = tmp_comic.read_metadata("cbi") @pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support") -def test_save_cix_rar(tmp_path, md_saved): - cbr_path = pathlib.Path(str(datadir)) / "fake_cbr.cbr" +def test_save_cr_rar(tmp_path, md_saved): + cbr_path = datadir / "fake_cbr.cbr" shutil.copy(cbr_path, tmp_path) tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name) assert tmp_comic.seems_to_be_a_comic_archive() - assert tmp_comic.write_cix(comicapi.genericmetadata.md_test) + assert tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cr") - md = tmp_comic.read_cix() - assert md.replace(pages=[], page_count=0) == md_saved.replace(pages=[], page_count=0) + md = tmp_comic.read_metadata("cr") + assert md == md_saved @pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support") @@ -80,46 +80,28 @@ def test_save_cbi_rar(tmp_path, md_saved): tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name) assert tmp_comic.seems_to_be_a_comic_archive() - assert tmp_comic.write_cbi(comicapi.genericmetadata.md_test) + assert tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cbi") - md = tmp_comic.read_cbi() - assert md.replace(pages=[]) == md_saved.replace( - pages=[], - day=None, - alternate_series=None, - alternate_number=None, - alternate_count=None, - imprint=None, - notes=None, - web_link=None, - format=None, - manga=None, - page_count=None, - maturity_rating=None, - story_arcs=[], - series_groups=[], - scan_info=None, - characters=set(), - teams=set(), - locations=set(), - ) + md = tmp_comic.read_metadata("cbi") + supported_attributes = comicapi.comicarchive.metadata_styles["cbi"].supported_attributes + assert md.get_clean_metadata(*supported_attributes) == md_saved.get_clean_metadata(*supported_attributes) def test_page_type_save(tmp_comic): - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") t = md.pages[0] - t["Type"] = "" + t["type"] = "" - assert tmp_comic.write_cix(md) + assert tmp_comic.write_metadata(md, "cr") - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") def test_invalid_zip(tmp_comic): with open(tmp_comic.path, mode="b+r") as f: f.write(b"PK\000\000") - result = tmp_comic.write_cix(comicapi.genericmetadata.md_test) + result = tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cr") assert not result @@ -149,7 +131,7 @@ def test_copy_from_archive(archiver, tmp_path, cbz, md_saved): assert comic_archive.seems_to_be_a_comic_archive() assert set(cbz.archiver.get_filename_list()) == set(comic_archive.archiver.get_filename_list()) - md = comic_archive.read_cix() + md = comic_archive.read_metadata("cr") assert md == md_saved diff --git a/tests/genericmetadata_test.py b/tests/genericmetadata_test.py index d594c4b..30ee097 100644 --- a/tests/genericmetadata_test.py +++ b/tests/genericmetadata_test.py @@ -12,7 +12,7 @@ def test_set_default_page_list(tmp_path): md.pages = [] md.set_default_page_list(len(comicapi.genericmetadata.md_test.pages)) - assert isinstance(md.pages[0]["Image"], int) + assert isinstance(md.pages[0]["image_index"], int) @pytest.mark.parametrize("replaced, expected", metadata) diff --git a/tests/integration_test.py b/tests/integration_test.py index cc3850b..ce751f4 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -3,7 +3,6 @@ from __future__ import annotations import settngs import comicapi.comicarchive -import comicapi.comicinfoxml import comicapi.genericmetadata import comictaggerlib.resulttypes from comictaggerlib import ctsettings @@ -19,9 +18,9 @@ def test_save( mock_now, ) -> None: # Overwrite the series so it has definitely changed - tmp_comic.write_cix(md_saved.replace(series="nothing")) + tmp_comic.write_metadata(md_saved.replace(series="nothing"), "cr") - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") # Check that it changed assert md != md_saved @@ -41,14 +40,14 @@ def test_save( # Use the temporary comic we created config[0].Runtime_Options__files = [tmp_comic.path] # Save ComicRack tags - config[0].Runtime_Options__type = [comicapi.comicarchive.MetaDataStyle.CIX] + config[0].Runtime_Options__type = ["cr"] # Search using the correct series since we just put the wrong series name in the CBZ config[0].Runtime_Options__metadata = comicapi.genericmetadata.GenericMetadata(series=md_saved.series) # Run ComicTagger CLI(config[0], talkers).run() # Read the CBZ - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") # Validate that we got the correct metadata back assert md == md_saved @@ -61,7 +60,7 @@ def test_delete( md_saved, mock_now, ) -> None: - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") # Check that the metadata starts correct assert md == md_saved @@ -79,16 +78,16 @@ def test_delete( # Use the temporary comic we created config[0].Runtime_Options__files = [tmp_comic.path] # Delete ComicRack tags - config[0].Runtime_Options__type = [comicapi.comicarchive.MetaDataStyle.CIX] + config[0].Runtime_Options__type = ["cr"] # Run ComicTagger CLI(config[0], talkers).run() # Read the CBZ - md = tmp_comic.read_cix() + md = tmp_comic.read_metadata("cr") # Currently we set the default page list on load empty_md = comicapi.genericmetadata.GenericMetadata() - empty_md.set_default_page_list(tmp_comic.get_number_of_pages()) + # empty_md.set_default_page_list(tmp_comic.get_number_of_pages()) # Validate that we got an empty metadata back assert md == empty_md diff --git a/tests/metadata_test.py b/tests/metadata_test.py index 364bc99..635a59f 100644 --- a/tests/metadata_test.py +++ b/tests/metadata_test.py @@ -1,64 +1,25 @@ from __future__ import annotations -import comicapi.comicbookinfo -import comicapi.comicinfoxml +import pytest +from importlib_metadata import entry_points + import comicapi.genericmetadata +metadata_styles = [] -def test_cix(md_saved): - CIX = comicapi.comicinfoxml.ComicInfoXml() - string = CIX.string_from_metadata(comicapi.genericmetadata.md_test) - md = CIX.metadata_from_string(string) - assert md == md_saved - - -def test_cbi(md_saved): - CBI = comicapi.comicbookinfo.ComicBookInfo() - string = CBI.string_from_metadata(comicapi.genericmetadata.md_test) - md = CBI.metadata_from_string(string) - md_test = md_saved.replace( - day=None, - page_count=None, - maturity_rating=None, - story_arcs=[], - series_groups=[], - scan_info=None, - characters=set(), - teams=set(), - locations=set(), - pages=[], - alternate_series=None, - alternate_number=None, - alternate_count=None, - imprint=None, - notes=None, - web_link=None, - format=None, - manga=None, +for x in entry_points(group="comicapi.metadata"): + meetadata = x.load() + supported = meetadata.enabled + exe_found = True + metadata_styles.append( + pytest.param(meetadata, marks=pytest.mark.xfail(not supported, reason="metadata not enabled")) ) - assert md == md_test -def test_comet(md_saved): - CBI = comicapi.comet.CoMet() - string = CBI.string_from_metadata(comicapi.genericmetadata.md_test) - md = CBI.metadata_from_string(string) - md_test = md_saved.replace( - day=None, - story_arcs=[], - series_groups=[], - scan_info=None, - teams=set(), - locations=set(), - pages=[], - alternate_series=None, - alternate_number=None, - alternate_count=None, - imprint=None, - notes=None, - web_link=None, - manga=None, - critical_rating=None, - issue_count=None, - ) - assert md == md_test +@pytest.mark.parametrize("metadata", metadata_styles) +def test_metadata(mock_version, tmp_comic, md, metadata): + md_style = metadata(mock_version[0]) + supported_attributes = md_style.supported_attributes + md_style.set_metadata(comicapi.genericmetadata.md_test, tmp_comic.archiver) + written_metadata = md_style.get_metadata(tmp_comic.archiver) + assert written_metadata.get_clean_metadata(*supported_attributes) == md.get_clean_metadata(*supported_attributes) From fd868d95968ad1b5aa706d8b61e4a0dd6cfa1ed3 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 17 Dec 2023 21:36:02 -0800 Subject: [PATCH 02/15] Add supports_credit_role to metadata plugins --- comicapi/metadata/comet.py | 15 +++++++++++++++ comicapi/metadata/comicbookinfo.py | 3 +++ comicapi/metadata/comicrack.py | 15 +++++++++++++++ comicapi/metadata/metadata.py | 3 +++ comictaggerlib/taggerwindow.py | 2 ++ 5 files changed, 38 insertions(+) diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py index 2e4dce8..154b29e 100644 --- a/comicapi/metadata/comet.py +++ b/comicapi/metadata/comet.py @@ -72,6 +72,9 @@ class CoMet(Metadata): "year", } + def supports_credit_role(self, role: str) -> bool: + return role.casefold() in self._get_parseable_credits() + def supports_metadata(self, archive: Archiver) -> bool: return archive.supports_files() @@ -123,6 +126,18 @@ class CoMet(Metadata): def name(self) -> str: return "Comic Metadata (CoMet)" + @classmethod + def _get_parseable_credits(cls) -> list[str]: + parsable_credits: list[str] = [] + parsable_credits.extend(cls._writer_synonyms) + parsable_credits.extend(cls._penciller_synonyms) + parsable_credits.extend(cls._inker_synonyms) + parsable_credits.extend(cls._colorist_synonyms) + parsable_credits.extend(cls._letterer_synonyms) + parsable_credits.extend(cls._cover_synonyms) + parsable_credits.extend(cls._editor_synonyms) + return parsable_credits + def _metadata_from_bytes(self, string: bytes) -> GenericMetadata: tree = ET.ElementTree(ET.fromstring(string)) return self._convert_xml_to_metadata(tree) diff --git a/comicapi/metadata/comicbookinfo.py b/comicapi/metadata/comicbookinfo.py index c3ab882..c1e91f4 100644 --- a/comicapi/metadata/comicbookinfo.py +++ b/comicapi/metadata/comicbookinfo.py @@ -104,6 +104,9 @@ class ComicBookInfo(Metadata): "year", } + def supports_credit_role(self, role: str) -> bool: + return True + def supports_metadata(self, archive: Archiver) -> bool: return archive.supports_comment() diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index 76ad543..d397ac6 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -88,6 +88,9 @@ class ComicRack(Metadata): "year", } + def supports_credit_role(self, role: str) -> bool: + return role.casefold() in self._get_parseable_credits() + def supports_metadata(self, archive: Archiver) -> bool: return True @@ -123,6 +126,18 @@ class ComicRack(Metadata): def name(self) -> str: return "Comic Rack" + @classmethod + def _get_parseable_credits(cls) -> list[str]: + parsable_credits: list[str] = [] + parsable_credits.extend(cls._writer_synonyms) + parsable_credits.extend(cls._penciller_synonyms) + parsable_credits.extend(cls._inker_synonyms) + parsable_credits.extend(cls._colorist_synonyms) + parsable_credits.extend(cls._letterer_synonyms) + parsable_credits.extend(cls._cover_synonyms) + parsable_credits.extend(cls._editor_synonyms) + return parsable_credits + def _metadata_from_bytes(self, string: bytes) -> GenericMetadata: root = ET.fromstring(string) return self._convert_xml_to_metadata(root) diff --git a/comicapi/metadata/metadata.py b/comicapi/metadata/metadata.py index 5959bda..39efc41 100644 --- a/comicapi/metadata/metadata.py +++ b/comicapi/metadata/metadata.py @@ -66,6 +66,9 @@ class Metadata: "cover_image", } + def supports_credit_role(self, role: str) -> bool: + return False + def supports_metadata(self, archive: Archiver) -> bool: """ Checks the given archive for the ability to save this metadata style. diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 9601917..92aa7f1 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1208,6 +1208,8 @@ class TaggerWindow(QtWidgets.QMainWindow): for credit in credit_attributes: widget_enabled = credit[0] in enabled widget = self.twCredits.item(r, credit[1]) + if credit[0] == "credits.role": + widget_enabled = widget_enabled and style.supports_credit_role(str(widget.text())) enable_widget(widget, widget_enabled) def update_metadata_style_tweaks(self) -> None: From da373764e091e0880807414276a0358eddf63b61 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 17 Dec 2023 21:37:12 -0800 Subject: [PATCH 03/15] Let the original ComicRack metadata be disabled Ensure metadata styles can be overridden by other plugins --- comictaggerlib/ctsettings/file.py | 6 ++++++ comictaggerlib/ctsettings/settngs_namespace.py | 1 + comictaggerlib/main.py | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 9bee2a3..8102a8c 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -12,6 +12,12 @@ from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replaceme def general(parser: settngs.Manager) -> None: # General Settings parser.add_setting("check_for_new_version", default=False, cmdline=False) + parser.add_setting( + "--disable-cr", + default=False, + action=argparse.BooleanOptionalAction, + help="Disable the ComicRack metadata type", + ) def internal(parser: settngs.Manager) -> None: diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py index 1f1c156..14d115a 100644 --- a/comictaggerlib/ctsettings/settngs_namespace.py +++ b/comictaggerlib/ctsettings/settngs_namespace.py @@ -96,6 +96,7 @@ class settngs_namespace(settngs.TypedNS): Auto_Tag__remove_archive_after_successful_match: bool General__check_for_new_version: bool + General__disable_cr: bool Dialog_Flags__show_disclaimer: bool Dialog_Flags__dont_notify_about_this_version: str diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 6e4c2d6..9b34078 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -223,6 +223,10 @@ class App: # config already loaded error = None + if self.config[0].General__disable_cr: + if "cr" in comicapi.comicarchive.metadata_styles: + del comicapi.comicarchive.metadata_styles["cr"] + if len(self.talkers) < 1: error = error = ( "Failed to load any talkers, please re-install and check the log located in '" From 8b7443945b632bb5d83f72c2da3ac7e58f310658 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 17 Dec 2023 22:01:47 -0800 Subject: [PATCH 04/15] Use ids for metadata type in file selection list Removed unnecessary FileInfo class --- comictaggerlib/fileselectionlist.py | 33 +++++++++++------------------ comictaggerlib/taggerwindow.py | 7 ++---- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index bd5c5ce..7ab12de 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -23,7 +23,7 @@ from typing import Callable, cast from PyQt5 import QtCore, QtWidgets, uic from comicapi import utils -from comicapi.comicarchive import ComicArchive, metadata_styles +from comicapi.comicarchive import ComicArchive from comictaggerlib.ctsettings import ct_ns from comictaggerlib.graphics import graphics_path from comictaggerlib.optionalmsgdialog import OptionalMessageDialog @@ -39,11 +39,6 @@ class FileTableWidgetItem(QtWidgets.QTableWidgetItem): return self.data(QtCore.Qt.ItemDataRole.UserRole) < other.data(QtCore.Qt.ItemDataRole.UserRole) # type: ignore -class FileInfo: - def __init__(self, ca: ComicArchive) -> None: - self.ca: ComicArchive = ca - - class FileSelectionList(QtWidgets.QWidget): selectionChanged = QtCore.pyqtSignal(QtCore.QVariant) listCleared = QtCore.pyqtSignal() @@ -137,8 +132,8 @@ class FileSelectionList(QtWidgets.QWidget): def get_archive_by_row(self, row: int) -> ComicArchive | None: if row >= 0: - fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole) - return fi.ca + ca: ComicArchive = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole) + return ca return None def get_current_archive(self) -> ComicArchive | None: @@ -282,8 +277,6 @@ class FileSelectionList(QtWidgets.QWidget): row: int = self.twList.rowCount() self.twList.insertRow(row) - fi = FileInfo(ca) - filename_item = QtWidgets.QTableWidgetItem() folder_item = QtWidgets.QTableWidgetItem() md_item = FileTableWidgetItem() @@ -292,7 +285,7 @@ class FileSelectionList(QtWidgets.QWidget): type_item = QtWidgets.QTableWidgetItem() filename_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) - filename_item.setData(QtCore.Qt.ItemDataRole.UserRole, fi) + filename_item.setData(QtCore.Qt.ItemDataRole.UserRole, ca) self.twList.setItem(row, FileSelectionList.fileColNum, filename_item) folder_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) @@ -320,7 +313,7 @@ class FileSelectionList(QtWidgets.QWidget): def update_row(self, row: int) -> None: if row >= 0: - fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole) + ca: ComicArchive = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole) filename_item = self.twList.item(row, FileSelectionList.fileColNum) folder_item = self.twList.item(row, FileSelectionList.folderColNum) @@ -328,24 +321,22 @@ class FileSelectionList(QtWidgets.QWidget): type_item = self.twList.item(row, FileSelectionList.typeColNum) readonly_item = self.twList.item(row, FileSelectionList.readonlyColNum) - item_text = os.path.split(fi.ca.path)[0] + item_text = os.path.split(ca.path)[0] folder_item.setText(item_text) folder_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text) - item_text = os.path.split(fi.ca.path)[1] + item_text = os.path.split(ca.path)[1] filename_item.setText(item_text) filename_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text) - item_text = fi.ca.archiver.name() + item_text = ca.archiver.name() type_item.setText(item_text) type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text) - styles = ", ".join( - metadata_styles[x].name() for x in fi.ca.get_supported_metadata() if fi.ca.has_metadata(x) - ) + styles = ", ".join(x for x in ca.get_supported_metadata() if ca.has_metadata(x)) md_item.setText(styles) - if not fi.ca.is_writable(): + if not ca.is_writable(): readonly_item.setCheckState(QtCore.Qt.CheckState.Checked) readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, True) else: @@ -357,8 +348,8 @@ class FileSelectionList(QtWidgets.QWidget): for r in range(self.twList.rowCount()): item = self.twList.item(r, FileSelectionList.dataColNum) if item.isSelected(): - fi: FileInfo = item.data(QtCore.Qt.ItemDataRole.UserRole) - ca_list.append(fi.ca) + ca: ComicArchive = item.data(QtCore.Qt.ItemDataRole.UserRole) + ca_list.append(ca) return ca_list diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 92aa7f1..828c69d 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -48,7 +48,7 @@ from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.crediteditorwindow import CreditEditorWindow from comictaggerlib.ctsettings import ct_ns from comictaggerlib.exportwindow import ExportConflictOpts, ExportWindow -from comictaggerlib.fileselectionlist import FileInfo, FileSelectionList +from comictaggerlib.fileselectionlist import FileSelectionList from comictaggerlib.graphics import graphics_path from comictaggerlib.issueidentifier import IssueIdentifier from comictaggerlib.logwindow import LogWindow @@ -191,7 +191,7 @@ class TaggerWindow(QtWidgets.QMainWindow): grid_layout = QtWidgets.QGridLayout(self.widgetListHolder) grid_layout.addWidget(self.fileSelectionList) - self.fileSelectionList.selectionChanged.connect(self.file_list_selection_changed) + self.fileSelectionList.selectionChanged.connect(self.load_archive) self.fileSelectionList.listCleared.connect(self.file_list_cleared) self.fileSelectionList.set_sorting( self.config[0].internal__sort_column, QtCore.Qt.SortOrder(self.config[0].internal__sort_direction) @@ -2065,9 +2065,6 @@ class TaggerWindow(QtWidgets.QMainWindow): self.fileSelectionList.update_selected_rows() self.load_archive(self.comic_archive) - def file_list_selection_changed(self, fi: FileInfo) -> None: - self.load_archive(fi.ca) - def load_archive(self, comic_archive: ComicArchive) -> None: self.comic_archive = None self.clear_form() From 989470772f1fc0025a95d90b0357876afb31d531 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Dec 2023 01:24:30 -0800 Subject: [PATCH 05/15] Make widget disabling more consistent --- comicapi/metadata/comet.py | 34 +++++++++--------- comicapi/metadata/comicbookinfo.py | 26 +++++++------- comicapi/metadata/comicrack.py | 58 +++++++++++++++--------------- comicapi/metadata/metadata.py | 46 ++++++++++++------------ comictaggerlib/pagelisteditor.py | 18 +++++----- comictaggerlib/taggerwindow.py | 6 ++-- comictaggerlib/ui/qtutils.py | 5 ++- 7 files changed, 101 insertions(+), 92 deletions(-) diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py index 154b29e..4e2106a 100644 --- a/comicapi/metadata/comet.py +++ b/comicapi/metadata/comet.py @@ -47,29 +47,31 @@ class CoMet(Metadata): self.comet_filename = "CoMet.xml" self.file = "CoMet.xml" self.supported_attributes = { - "characters", + "series", + "issue", + "title", + "volume", + "genres", "description", + "publisher", + "language", + "format", + "maturity_rating", + "month", + "year", + "page_count", + "characters", "credits", "credits.person", "credits.primary", "credits.role", - "format", - "genres", - "identifier", - "is_version_of", - "issue", - "language", - "last_mark", - "maturity_rating", - "month", - "page_count", "price", - "publisher", + "is_version_of", "rights", - "series", - "title", - "volume", - "year", + "identifier", + "last_mark", + "pages.type", # This is required for setting the cover image none of the other types will be saved + "pages", } def supports_credit_role(self, role: str) -> bool: diff --git a/comicapi/metadata/comicbookinfo.py b/comicapi/metadata/comicbookinfo.py index c1e91f4..96f3183 100644 --- a/comicapi/metadata/comicbookinfo.py +++ b/comicapi/metadata/comicbookinfo.py @@ -83,25 +83,25 @@ class ComicBookInfo(Metadata): super().__init__(version) self.supported_attributes = { + "series", + "issue", + "issue_count", + "title", + "volume", + "volume_count", + "genres", "description", + "publisher", + "month", + "year", + "language", "country", + "critical_rating", + "tags", "credits", "credits.person", "credits.primary", "credits.role", - "critical_rating", - "genres", - "issue", - "issue_count", - "language", - "month", - "publisher", - "series", - "tags", - "title", - "volume", - "volume_count", - "year", } def supports_credit_role(self, role: str) -> bool: diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index d397ac6..01a79f6 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -45,29 +45,32 @@ class ComicRack(Metadata): self.file = "ComicInfo.xml" self.supported_attributes = { - "alternate_count", - "alternate_number", - "alternate_series", - "black_and_white", - "characters", - "description", - "credits", - "credits.person", - "credits.role", - "critical_rating", - "day", - "format", - "genres", - "imprint", + "series", "issue", "issue_count", - "language", - "locations", - "manga", - "maturity_rating", - "month", + "title", + "volume", + "genres", + "description", "notes", - "page_count", + "alternate_series", + "alternate_number", + "alternate_count", + "story_arcs", + "series_groups", + "publisher", + "imprint", + "day", + "month", + "year", + "language", + "web_link", + "format", + "manga", + "black_and_white", + "maturity_rating", + "critical_rating", + "scan_info", "pages", "pages.bookmark", "pages.double_page", @@ -76,16 +79,13 @@ class ComicRack(Metadata): "pages.size", "pages.type", "pages.width", - "publisher", - "scan_info", - "series", - "series_groups", - "story_arcs", + "page_count", + "characters", "teams", - "title", - "volume", - "web_link", - "year", + "locations", + "credits", + "credits.person", + "credits.role", } def supports_credit_role(self, role: str) -> bool: diff --git a/comicapi/metadata/metadata.py b/comicapi/metadata/metadata.py index 39efc41..bc256ea 100644 --- a/comicapi/metadata/metadata.py +++ b/comicapi/metadata/metadata.py @@ -13,42 +13,37 @@ class Metadata: self.supported_attributes = { "tag_origin", "issue_id", + "series_id", "series", + "series_aliases", "issue", - "title", - "publisher", - "month", - "year", - "day", "issue_count", + "title", + "title_aliases", "volume", - "genre", - "language", - "comments", "volume_count", - "critical_rating", - "country", + "genres", + "description", + "notes", "alternate_series", "alternate_number", "alternate_count", + "story_arcs", + "series_groups", + "publisher", "imprint", - "notes", + "day", + "month", + "year", + "language", + "country", "web_link", "format", "manga", "black_and_white", - "page_count", "maturity_rating", - "story_arc", - "series_group", + "critical_rating", "scan_info", - "characters", - "teams", - "locations", - "credits", - "credits.person", - "credits.role", - "credits.primary", "tags", "pages", "pages.type", @@ -58,12 +53,19 @@ class Metadata: "pages.size", "pages.height", "pages.width", + "page_count", + "characters", + "teams", + "locations", + "credits", + "credits.person", + "credits.role", + "credits.primary", "price", "is_version_of", "rights", "identifier", "last_mark", - "cover_image", } def supports_credit_role(self, role: str) -> bool: diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index e64279e..b457a19 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -73,12 +73,11 @@ class PageListEditor(QtWidgets.QWidget): uic.loadUi(uifile, self) self.md_attributes = { - "page.image_index": [self.btnDown, self.btnUp, self.listWidget], - "page.type": self.cbPageType, - "page.double_page": self.chkDoublePage, - "page.bookmark": self.leBookmark, - # Python dicts are order preserving this must be placed last - "pages": [self.btnDown, self.btnUp, self.listWidget, self.cbPageType, self.chkDoublePage, self.leBookmark], + "pages.image_index": [self.btnDown, self.btnUp], + "pages.type": self.cbPageType, + "pages.double_page": self.chkDoublePage, + "pages.bookmark": self.leBookmark, + "pages": self, } self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode, None, None) @@ -119,9 +118,10 @@ class PageListEditor(QtWidgets.QWidget): def reset_page(self) -> None: self.pageWidget.clear() - self.cbPageType.setDisabled(True) - self.chkDoublePage.setDisabled(True) - self.leBookmark.setDisabled(True) + self.cbPageType.setEnabled(False) + self.chkDoublePage.setEnabled(False) + self.leBookmark.setEnabled(False) + self.listWidget.clear() self.comic_archive = None self.pages_list = [] diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 828c69d..9bcf76e 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1205,11 +1205,13 @@ class TaggerWindow(QtWidgets.QMainWindow): credit_attributes = [x for x in self.md_attributes.items() if "credits." in x[0]] for r in range(self.twCredits.rowCount()): + w = self.twCredits.item(r, 1) + supports_role = style.supports_credit_role(str(w.text())) for credit in credit_attributes: widget_enabled = credit[0] in enabled widget = self.twCredits.item(r, credit[1]) if credit[0] == "credits.role": - widget_enabled = widget_enabled and style.supports_credit_role(str(widget.text())) + widget_enabled = widget_enabled and supports_role enable_widget(widget, widget_enabled) def update_metadata_style_tweaks(self) -> None: @@ -1217,7 +1219,7 @@ class TaggerWindow(QtWidgets.QMainWindow): enabled_widgets = metadata_styles[self.save_data_style].supported_attributes for metadata, widget in self.md_attributes.items(): - if isinstance(widget, QtWidgets.QWidget): + if widget is not None and not isinstance(widget, (int)): enable_widget(widget, metadata in enabled_widgets) self.update_metadata_credit_colors() diff --git a/comictaggerlib/ui/qtutils.py b/comictaggerlib/ui/qtutils.py index ea335a6..c85f18c 100644 --- a/comictaggerlib/ui/qtutils.py +++ b/comictaggerlib/ui/qtutils.py @@ -164,7 +164,7 @@ if qt_available: def _enable_widget(widget: QtWidgets.QWidget, enable: bool) -> None: global active_palette - if not (widget and active_palette and widget): + if not (widget is not None and active_palette is not None): return active_color = active_palette.color(QtGui.QPalette.ColorRole.Base) @@ -184,6 +184,9 @@ if qt_available: inactive_palette3.setColor(widget.foregroundRole(), inactive_color) return inactive_palette1, inactive_palette2, inactive_palette3 + if hasattr(widget, "setEnabled"): + widget.setEnabled(enable) + if enable: if isinstance(widget, QtWidgets.QTableWidgetItem): widget.setBackground(active_brush) From 9c231d7e116e514cfff3245959258e9e90aa47c2 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Dec 2023 02:37:34 -0800 Subject: [PATCH 06/15] Add better page info handling Rename set_default_page_list to apply_default_page_list and apply during read_metadata Add a filename attribute to the ImageMetadata class Mark image_index as required Always sort the page name list, a comic application will never need the unsorted list of names Assign the first result from get_cover_page_index_list to coverImage in CoMet tags Allow an Archiver to be passed to the ComicArchive constructor --- comicapi/comicarchive.py | 23 ++++++---- comicapi/genericmetadata.py | 81 +++++++++++++++++++++------------- comicapi/metadata/comet.py | 21 ++++++--- comicapi/metadata/comicrack.py | 10 ++--- comictaggerlib/cli.py | 2 +- comictaggerlib/taggerwindow.py | 29 ++++++------ tests/comicarchive_test.py | 8 +++- tests/genericmetadata_test.py | 4 +- tests/integration_test.py | 2 +- tests/metadata_test.py | 19 +++++++- 10 files changed, 124 insertions(+), 75 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 125d351..ad7c1cf 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -81,16 +81,22 @@ class ComicArchive: logo_data = b"" pil_available = True - def __init__(self, path: pathlib.Path | str, default_image_path: pathlib.Path | str | None = None) -> None: + def __init__( + self, path: pathlib.Path | str | Archiver, default_image_path: pathlib.Path | str | None = None + ) -> None: self.md: dict[str, GenericMetadata] = {} - self.path = pathlib.Path(path).absolute() self.page_count: int | None = None self.page_list: list[str] = [] self.reset_cache() self.default_image_path = default_image_path - self.archiver: Archiver = UnknownArchiver.open(self.path) + if isinstance(path, Archiver): + self.path = path.path + self.archiver: Archiver = path + else: + self.path = pathlib.Path(path).absolute() + self.archiver = UnknownArchiver.open(self.path) load_archive_plugins() load_metadata_plugins() @@ -161,7 +167,9 @@ class ComicArchive: def read_metadata(self, style: str) -> GenericMetadata: if style in self.md: return self.md[style] - return metadata_styles[style].get_metadata(self.archiver) + md = metadata_styles[style].get_metadata(self.archiver) + md.apply_default_page_list(self.get_page_name_list()) + return md def read_metadata_string(self, style: str) -> str: return metadata_styles[style].get_metadata_string(self.archiver) @@ -258,14 +266,12 @@ class ComicArchive: return scanner_page_index - def get_page_name_list(self, sort_list: bool = True) -> list[str]: + def get_page_name_list(self) -> list[str]: if not self.page_list: # get the list file names in the archive, and sort files: list[str] = self.archiver.get_filename_list() - # seems like some archive creators are on Windows, and don't know about case-sensitivity! - if sort_list: - files = cast(list[str], utils.os_sorted(files)) + files = cast(list[str], utils.os_sorted(files)) # make a sub-list of image files self.page_list = [] @@ -289,6 +295,7 @@ class ComicArchive: if calc_page_sizes: for index, p in enumerate(md.pages): idx = int(p["image_index"]) + p["filename"] = self.get_page_name(idx) if self.pil_available: try: from PIL import Image diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 506c461..22f89c7 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -26,7 +26,7 @@ import logging from collections.abc import Sequence from typing import Any, TypedDict -from typing_extensions import NamedTuple +from typing_extensions import NamedTuple, Required from comicapi import utils @@ -54,10 +54,11 @@ class PageType: class ImageMetadata(TypedDict, total=False): + filename: str type: str bookmark: str double_page: bool - image_index: int + image_index: Required[int] size: str height: str width: str @@ -286,13 +287,28 @@ class GenericMetadata: else: self.add_credit(c["person"], c["role"], primary) - def set_default_page_list(self, count: int) -> None: + def apply_default_page_list(self, page_list: Sequence[str]) -> None: # generate a default page list, with the first page marked as the cover - for i in range(count): - page_dict = ImageMetadata(image_index=i) - if i == 0: - page_dict["type"] = PageType.FrontCover - self.pages.append(page_dict) + # Create a dictionary of all pages in the metadata + pages = {p["image_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 + for i, filename in enumerate(page_list): + if i not in pages: + pages[i] = ImageMetadata(image_index=i, filename=filename) + else: + pages[i]["filename"] = filename + + # Check if we know what the cover is + cover_set = pages[i].get("type", None) == PageType.FrontCover or cover_set + + self.pages = [p[1] for p in sorted(pages.items())] + + # Set the cover to the first image if we don't know what the cover is + if not cover_set: + self.pages[0]["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 @@ -486,29 +502,31 @@ md_test: GenericMetadata = GenericMetadata( ], tags=set(), pages=[ - ImageMetadata(image_index=0, height="1280", size="195977", width="800", type=PageType.FrontCover), - ImageMetadata(image_index=1, height="2039", size="611993", width="1327"), - ImageMetadata(image_index=2, height="2039", size="783726", width="1327"), - ImageMetadata(image_index=3, height="2039", size="679584", width="1327"), - ImageMetadata(image_index=4, height="2039", size="788179", width="1327"), - ImageMetadata(image_index=5, height="2039", size="864433", width="1327"), - ImageMetadata(image_index=6, height="2039", size="765606", width="1327"), - ImageMetadata(image_index=7, height="2039", size="876427", width="1327"), - ImageMetadata(image_index=8, height="2039", size="852622", width="1327"), - ImageMetadata(image_index=9, height="2039", size="800205", width="1327"), - ImageMetadata(image_index=10, height="2039", size="746243", width="1326"), - ImageMetadata(image_index=11, height="2039", size="718062", width="1327"), - ImageMetadata(image_index=12, height="2039", size="532179", width="1326"), - ImageMetadata(image_index=13, height="2039", size="686708", width="1327"), - ImageMetadata(image_index=14, height="2039", size="641907", width="1327"), - ImageMetadata(image_index=15, height="2039", size="805388", width="1327"), - ImageMetadata(image_index=16, height="2039", size="668927", width="1326"), - ImageMetadata(image_index=17, height="2039", size="710605", width="1327"), - ImageMetadata(image_index=18, height="2039", size="761398", width="1326"), - ImageMetadata(image_index=19, height="2039", size="743807", width="1327"), - ImageMetadata(image_index=20, height="2039", size="552911", width="1326"), - ImageMetadata(image_index=21, height="2039", size="556827", width="1327"), - ImageMetadata(image_index=22, height="2039", size="675078", width="1326"), + ImageMetadata( + image_index=0, height="1280", size="195977", width="800", type=PageType.FrontCover, filename="!cover.jpg" + ), + 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( bookmark="Interview", image_index=23, @@ -516,6 +534,7 @@ md_test: GenericMetadata = GenericMetadata( size="800965", width="1338", type=PageType.Letters, + filename="23.jpg", ), ], price=None, diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py index 4e2106a..688a6e7 100644 --- a/comicapi/metadata/comet.py +++ b/comicapi/metadata/comet.py @@ -22,7 +22,8 @@ from typing import Any from comicapi import utils from comicapi.archivers import Archiver -from comicapi.genericmetadata import GenericMetadata +from comicapi.comicarchive import ComicArchive +from comicapi.genericmetadata import GenericMetadata, ImageMetadata, PageType from comicapi.metadata import Metadata logger = logging.getLogger(__name__) @@ -107,7 +108,7 @@ class CoMet(Metadata): if self.has_metadata(archive): metadata = archive.read_file(self.file) or b"" if self._validate_bytes(metadata): - return self._metadata_from_bytes(metadata) + return self._metadata_from_bytes(metadata, archive) return GenericMetadata() def get_metadata_string(self, archive: Archiver) -> str: @@ -140,9 +141,9 @@ class CoMet(Metadata): parsable_credits.extend(cls._editor_synonyms) return parsable_credits - def _metadata_from_bytes(self, string: bytes) -> GenericMetadata: + def _metadata_from_bytes(self, string: bytes, archive: Archiver) -> GenericMetadata: tree = ET.ElementTree(ET.fromstring(string)) - return self._convert_xml_to_metadata(tree) + return self._convert_xml_to_metadata(tree, archive) def _bytes_from_metadata(self, metadata: GenericMetadata) -> bytes: tree = self._convert_metadata_to_xml(metadata) @@ -197,7 +198,8 @@ class CoMet(Metadata): date_str += f"-{md.month:02}" assign("date", date_str) - assign("coverImage", md._cover_image) + page = md.get_cover_page_index_list()[0] + assign("coverImage", md.pages[page]["filename"]) # loop thru credits, and build a list for each role that CoMet supports for credit in metadata.credits: @@ -228,7 +230,7 @@ class CoMet(Metadata): tree = ET.ElementTree(root) return tree - def _convert_xml_to_metadata(self, tree: ET.ElementTree) -> GenericMetadata: + def _convert_xml_to_metadata(self, tree: ET.ElementTree, archive: Archiver) -> GenericMetadata: root = tree.getroot() if root.tag != "comet": @@ -262,7 +264,12 @@ class CoMet(Metadata): _, md.month, md.year = utils.parse_date_str(utils.xlate(get("date"))) - md._cover_image = utils.xlate(get("coverImage")) + ca = ComicArchive(archive) + cover_filename = utils.xlate(get("coverImage")) + 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)] reading_direction = utils.xlate(get("readingDirection")) if reading_direction is not None and reading_direction == "rtl": diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index 01a79f6..5e4e3fc 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -17,7 +17,7 @@ from __future__ import annotations import logging import xml.etree.ElementTree as ET from collections import OrderedDict -from typing import Any, cast +from typing import Any from comicapi import utils from comicapi.archivers import Archiver @@ -352,16 +352,14 @@ class ComicRack(Metadata): # parse page data now pages_node = root.find("Pages") if pages_node is not None: - for page in pages_node: + for i, page in enumerate(pages_node): p: dict[str, Any] = page.attrib - md_page = ImageMetadata() + md_page = ImageMetadata(image_index=int(p.get("Image", i))) 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 "Image" in p: - md_page["image_index"] = int(p["Image"]) if "ImageHeight" in p: md_page["height"] = p["ImageHeight"] if "ImageSize" in p: @@ -371,7 +369,7 @@ class ComicRack(Metadata): if "Type" in p: md_page["type"] = p["Type"] - md.pages.append(cast(ImageMetadata, md_page)) + md.pages.append(md_page) md.is_empty = False diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 0e750f5..295e2a6 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -245,7 +245,7 @@ class CLI: def create_local_metadata(self, ca: ComicArchive) -> GenericMetadata: md = GenericMetadata() - md.set_default_page_list(ca.get_number_of_pages()) + md.apply_default_page_list(ca.get_page_name_list()) # now, overlay the parsed filename info if self.config.Runtime_Options__parse_filename: diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 9bcf76e..3a09a8f 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -679,16 +679,17 @@ class TaggerWindow(QtWidgets.QMainWindow): self.fileSelectionList.add_path_list(self.droppedFiles) event.accept() - def actual_load_current_archive(self) -> None: - if self.metadata.is_empty and self.comic_archive is not None: - self.metadata = self.comic_archive.metadata_from_filename( - self.config[0].Filename_Parsing__complicated_parser, - self.config[0].Filename_Parsing__remove_c2c, - self.config[0].Filename_Parsing__remove_fcbd, - self.config[0].Filename_Parsing__remove_publisher, - ) - if len(self.metadata.pages) == 0 and self.comic_archive is not None: - self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages()) + def update_ui_for_archive(self, parse_filename: bool = True) -> None: + if self.comic_archive is not None: + if self.metadata.is_empty and parse_filename: + self.metadata = self.comic_archive.metadata_from_filename( + self.config[0].Filename_Parsing__complicated_parser, + self.config[0].Filename_Parsing__remove_c2c, + self.config[0].Filename_Parsing__remove_fcbd, + self.config[0].Filename_Parsing__remove_publisher, + ) + + self.metadata.apply_default_page_list(self.comic_archive.get_page_name_list()) self.update_cover_image() @@ -795,15 +796,13 @@ class TaggerWindow(QtWidgets.QMainWindow): def clear_form(self) -> None: # get a minty fresh metadata object self.metadata = GenericMetadata() - if self.comic_archive is not None: - self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages()) - self.page_list_editor.set_data(self.comic_archive, self.metadata.pages) # recursively clear the tab form self.clear_children(self.tabWidget) # clear the dirty flag, since there is nothing in there now to lose self.clear_dirty_flag() + self.update_ui_for_archive(parse_filename=False) def clear_children(self, widget: QtCore.QObject) -> None: if isinstance(widget, (QtWidgets.QLineEdit, QtWidgets.QTextEdit)): @@ -1172,7 +1171,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.fileSelectionList.update_current_row() self.metadata = self.comic_archive.read_metadata(self.load_data_style) - self.actual_load_current_archive() + self.update_ui_for_archive() else: QtWidgets.QMessageBox.information(self, "Whoops!", "No data to commit!") @@ -2086,7 +2085,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.exception(f"Failed to load metadata for {self.comic_archive.path}:\n\n{e}") self.metadata = GenericMetadata() - self.actual_load_current_archive() + self.update_ui_for_archive() def file_list_cleared(self) -> None: self.reset_app() diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index 1f4fb8e..996ae34 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -44,7 +44,7 @@ def test_metadata_read(cbz, md_saved): def test_save_cr(tmp_comic): md = tmp_comic.read_metadata("cr") - md.set_default_page_list(tmp_comic.get_number_of_pages()) + md.apply_default_page_list(tmp_comic.get_page_name_list()) assert tmp_comic.write_metadata(md, "cr") @@ -53,7 +53,7 @@ def test_save_cr(tmp_comic): def test_save_cbi(tmp_comic): md = tmp_comic.read_metadata("cr") - md.set_default_page_list(tmp_comic.get_number_of_pages()) + md.apply_default_page_list(tmp_comic.get_page_name_list()) assert tmp_comic.write_metadata(md, "cbi") @@ -70,6 +70,10 @@ def test_save_cr_rar(tmp_path, md_saved): assert tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cr") md = tmp_comic.read_metadata("cr") + + # This is a fake CBR we don't need to care about the pages for this test + md.pages = [] + md_saved.pages = [] assert md == md_saved diff --git a/tests/genericmetadata_test.py b/tests/genericmetadata_test.py index 30ee097..984a370 100644 --- a/tests/genericmetadata_test.py +++ b/tests/genericmetadata_test.py @@ -6,11 +6,11 @@ import comicapi.genericmetadata from testing.comicdata import credits, metadata -def test_set_default_page_list(tmp_path): +def test_apply_default_page_list(tmp_path): md = comicapi.genericmetadata.GenericMetadata() md.overlay(comicapi.genericmetadata.md_test) md.pages = [] - md.set_default_page_list(len(comicapi.genericmetadata.md_test.pages)) + md.apply_default_page_list(["testing"]) assert isinstance(md.pages[0]["image_index"], int) diff --git a/tests/integration_test.py b/tests/integration_test.py index ce751f4..ae6d0f0 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -87,7 +87,7 @@ def test_delete( # Currently we set the default page list on load empty_md = comicapi.genericmetadata.GenericMetadata() - # empty_md.set_default_page_list(tmp_comic.get_number_of_pages()) + empty_md.apply_default_page_list(tmp_comic.get_page_name_list()) # Validate that we got an empty metadata back assert md == empty_md diff --git a/tests/metadata_test.py b/tests/metadata_test.py index 635a59f..c59f205 100644 --- a/tests/metadata_test.py +++ b/tests/metadata_test.py @@ -17,9 +17,24 @@ for x in entry_points(group="comicapi.metadata"): @pytest.mark.parametrize("metadata", metadata_styles) -def test_metadata(mock_version, tmp_comic, md, metadata): +def test_metadata(mock_version, tmp_comic, md_saved, metadata): md_style = metadata(mock_version[0]) supported_attributes = md_style.supported_attributes md_style.set_metadata(comicapi.genericmetadata.md_test, tmp_comic.archiver) written_metadata = md_style.get_metadata(tmp_comic.archiver) - assert written_metadata.get_clean_metadata(*supported_attributes) == md.get_clean_metadata(*supported_attributes) + md = md_saved.get_clean_metadata(*supported_attributes) + + # Hack back in the pages variable because CoMet supports identifying the cover by the filename + if md_style.short_name == "comet": + md.pages = [ + comicapi.genericmetadata.ImageMetadata( + image_index=0, filename="!cover.jpg", type=comicapi.genericmetadata.PageType.FrontCover + ) + ] + written_metadata = written_metadata.get_clean_metadata(*supported_attributes).replace( + pages=written_metadata.pages + ) + else: + written_metadata = written_metadata.get_clean_metadata(*supported_attributes) + + assert written_metadata == md From 4660b144533ebe193c8305c5d2898a0fa28747a3 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Dec 2023 01:34:04 -0800 Subject: [PATCH 07/15] Fixup metadata handling --- comicapi/metadata/comet.py | 10 +-- comicapi/metadata/comicbookinfo.py | 12 +--- comicapi/metadata/comicrack.py | 105 +++++++++++++++-------------- comictaggerlib/ctsettings/types.py | 1 - 4 files changed, 61 insertions(+), 67 deletions(-) diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py index 688a6e7..67a42a3 100644 --- a/comicapi/metadata/comet.py +++ b/comicapi/metadata/comet.py @@ -165,9 +165,7 @@ class CoMet(Metadata): ET.SubElement(root, comet_entry).text = str(md_entry) # title is manditory - if md.title is None: - md.title = "" - assign("title", md.title) + assign("title", md.title or "") assign("series", md.series) assign("issue", md.issue) # must be int?? assign("volume", md.volume) @@ -184,10 +182,8 @@ class CoMet(Metadata): assign("lastMark", md.last_mark) assign("genre", ",".join(md.genres)) # TODO repeatable - if md.characters is not None: - char_list = [c.strip() for c in md.characters] - for c in char_list: - assign("character", c) + for c in md.characters: + assign("character", c.strip()) if md.manga is not None and md.manga == "YesAndRightToLeft": assign("readingDirection", "rtl") diff --git a/comicapi/metadata/comicbookinfo.py b/comicapi/metadata/comicbookinfo.py index 96f3183..2e90475 100644 --- a/comicapi/metadata/comicbookinfo.py +++ b/comicapi/metadata/comicbookinfo.py @@ -21,7 +21,7 @@ from typing import Any, Literal, TypedDict from comicapi import utils from comicapi.archivers import Archiver -from comicapi.genericmetadata import GenericMetadata +from comicapi.genericmetadata import Credit, GenericMetadata from comicapi.metadata import Metadata logger = logging.getLogger(__name__) @@ -46,12 +46,6 @@ _CBILiteralType = Literal[ ] -class _Credits(TypedDict): - person: str - role: str - primary: bool - - class _ComicBookInfoJson(TypedDict, total=False): series: str title: str @@ -66,7 +60,7 @@ class _ComicBookInfoJson(TypedDict, total=False): genre: str language: str country: str - credits: list[_Credits] + credits: list[Credit] tags: list[str] comments: str @@ -161,7 +155,7 @@ class ComicBookInfo(Metadata): metadata.critical_rating = utils.xlate_int(cbi.get("rating")) metadata.credits = [ - _Credits( + Credit( person=x["person"] if "person" in x else "", role=x["role"] if "role" in x else "", primary=x["primary"] if "primary" in x else False, diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index 5e4e3fc..43893da 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -92,7 +92,7 @@ class ComicRack(Metadata): return role.casefold() in self._get_parseable_credits() def supports_metadata(self, archive: Archiver) -> bool: - return True + return archive.supports_files() def has_metadata(self, archive: Archiver) -> bool: return ( @@ -178,22 +178,6 @@ class ComicRack(Metadata): if et_entry is not None: root.remove(et_entry) - assign("Title", md.title) - assign("Series", md.series) - assign("Number", md.issue) - assign("Count", md.issue_count) - assign("Volume", md.volume) - assign("AlternateSeries", md.alternate_series) - assign("AlternateNumber", md.alternate_number) - assign("StoryArc", md.story_arcs) - assign("SeriesGroup", md.series_groups) - assign("AlternateCount", md.alternate_count) - assign("Summary", md.description) - assign("Notes", md.notes) - assign("Year", md.year) - assign("Month", md.month) - assign("Day", md.day) - # need to specially process the credits, since they are structured # differently than CIX credit_writer_list = [] @@ -228,7 +212,40 @@ class ComicRack(Metadata): if credit["role"].casefold() in set(self._editor_synonyms): credit_editor_list.append(credit["person"].replace(",", "")) - # second, convert each list to string, and add to XML struct + assign("Series", md.series) + assign("Number", md.issue) + assign("Count", md.issue_count) + assign("Title", md.title) + assign("Volume", md.volume) + assign("Genre", md.genres) + assign("Summary", md.description) + assign("Notes", md.notes) + + assign("AlternateSeries", md.alternate_series) + assign("AlternateNumber", md.alternate_number) + assign("AlternateCount", md.alternate_count) + assign("StoryArc", md.story_arcs) + assign("SeriesGroup", md.series_groups) + + assign("Publisher", md.publisher) + assign("Imprint", md.imprint) + assign("Day", md.day) + assign("Month", md.month) + assign("Year", md.year) + assign("LanguageISO", md.language) + assign("Web", md.web_link) + assign("Format", md.format) + assign("Manga", md.manga) + assign("BlackAndWhite", "Yes" if md.black_and_white else None) + assign("AgeRating", md.maturity_rating) + assign("CommunityRating", md.critical_rating) + assign("ScanInformation", md.scan_info) + + assign("PageCount", md.page_count) + + assign("Characters", md.characters) + assign("Teams", md.teams) + assign("Locations", md.locations) assign("Writer", ", ".join(credit_writer_list)) assign("Penciller", ", ".join(credit_penciller_list)) assign("Inker", ", ".join(credit_inker_list)) @@ -237,22 +254,6 @@ class ComicRack(Metadata): assign("CoverArtist", ", ".join(credit_cover_list)) assign("Editor", ", ".join(credit_editor_list)) - assign("Publisher", md.publisher) - assign("Imprint", md.imprint) - assign("Genre", md.genres) - assign("Web", md.web_link) - assign("PageCount", md.page_count) - assign("LanguageISO", md.language) - assign("Format", md.format) - assign("AgeRating", md.maturity_rating) - assign("CommunityRating", md.critical_rating) - assign("BlackAndWhite", "Yes" if md.black_and_white else None) - assign("Manga", md.manga) - assign("Characters", md.characters) - assign("Teams", md.teams) - assign("Locations", md.locations) - assign("ScanInformation", md.scan_info) - # loop and add the page entries under pages node pages_node = root.find("Pages") if pages_node is not None: @@ -296,38 +297,42 @@ class ComicRack(Metadata): md = GenericMetadata() md.series = utils.xlate(get("Series")) - md.title = utils.xlate(get("Title")) md.issue = utils.xlate(get("Number")) md.issue_count = utils.xlate_int(get("Count")) + md.title = utils.xlate(get("Title")) md.volume = utils.xlate_int(get("Volume")) + md.genres = set(utils.split(get("Genre"), ",")) + md.description = utils.xlate(get("Summary")) + md.notes = utils.xlate(get("Notes")) + md.alternate_series = utils.xlate(get("AlternateSeries")) md.alternate_number = utils.xlate(get("AlternateNumber")) md.alternate_count = utils.xlate_int(get("AlternateCount")) - md.description = utils.xlate(get("Summary")) - md.notes = utils.xlate(get("Notes")) - md.year = utils.xlate_int(get("Year")) - md.month = utils.xlate_int(get("Month")) - md.day = utils.xlate_int(get("Day")) + md.story_arcs = utils.split(get("StoryArc"), ",") + md.series_groups = utils.split(get("SeriesGroup"), ",") + md.publisher = utils.xlate(get("Publisher")) md.imprint = utils.xlate(get("Imprint")) - md.genres = set(utils.split(get("Genre"), ",")) - md.web_link = utils.xlate(get("Web")) + md.day = utils.xlate_int(get("Day")) + md.month = utils.xlate_int(get("Month")) + md.year = utils.xlate_int(get("Year")) md.language = utils.xlate(get("LanguageISO")) + md.web_link = utils.xlate(get("Web")) md.format = utils.xlate(get("Format")) md.manga = utils.xlate(get("Manga")) + md.maturity_rating = utils.xlate(get("AgeRating")) + md.critical_rating = utils.xlate_float(get("CommunityRating")) + md.scan_info = utils.xlate(get("ScanInformation")) + + md.page_count = utils.xlate_int(get("PageCount")) + md.characters = set(utils.split(get("Characters"), ",")) md.teams = set(utils.split(get("Teams"), ",")) md.locations = set(utils.split(get("Locations"), ",")) - md.page_count = utils.xlate_int(get("PageCount")) - md.scan_info = utils.xlate(get("ScanInformation")) - md.story_arcs = utils.split(get("StoryArc"), ",") - md.series_groups = utils.split(get("SeriesGroup"), ",") - md.maturity_rating = utils.xlate(get("AgeRating")) - md.critical_rating = utils.xlate_float(get("CommunityRating")) tmp = utils.xlate(get("BlackAndWhite")) - if tmp is not None and tmp.casefold() in ["yes", "true", "1"]: - md.black_and_white = True + md.black_and_white = tmp is not None and tmp.casefold() in ["yes", "true", "1"] + # Now extract the credit info for n in root: if any( diff --git a/comictaggerlib/ctsettings/types.py b/comictaggerlib/ctsettings/types.py index 3be08cf..d8ac1c6 100644 --- a/comictaggerlib/ctsettings/types.py +++ b/comictaggerlib/ctsettings/types.py @@ -69,7 +69,6 @@ def metadata_type(types: str) -> list[str]: result = [] types = types.casefold() for typ in utils.split(types, ","): - typ = typ.strip() if typ not in metadata_styles: choices = ", ".join(metadata_styles) raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})") From addddaf44e22876d33f56649478ce25a0fb8ea69 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Dec 2023 01:34:55 -0800 Subject: [PATCH 08/15] List metadata styles when listing plugins --- comictaggerlib/main.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 9b34078..4721363 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -24,6 +24,7 @@ import os import signal import subprocess import sys +from collections.abc import Collection from typing import cast import settngs @@ -126,7 +127,10 @@ class App: self.talkers = comictalker.get_talkers(version, opts.config.user_cache_dir) def list_plugins( - self, talkers: list[comictalker.ComicTalker], archivers: list[type[comicapi.comicarchive.Archiver]] + self, + talkers: Collection[comictalker.ComicTalker], + archivers: Collection[type[comicapi.comicarchive.Archiver]], + metadata_styles: Collection[comicapi.comicarchive.Metadata], ) -> None: if self.config[0].Runtime_Options__json: for talker in talkers: @@ -167,15 +171,31 @@ class App: } ) ) + + for style in metadata_styles: + print( # noqa: T201 + json.dumps( + { + "type": "metadata", + "enabled": style.enabled, + "name": style.name(), + "short_name": style.short_name, + } + ) + ) else: - print("Metadata Sources: (ID: Name URL)") # noqa: T201 + print("Metadata Sources: (ID: Name, URL)") # noqa: T201 for talker in talkers: - print(f"{talker.id}: {talker.name} {talker.website}") # noqa: T201 + print(f"{talker.id:<10}: {talker.name:<21}, {talker.website}") # noqa: T201 print("\nComic Archive: (Name: extension, exe)") # noqa: T201 for archiver in archivers: a = archiver() - print(f"{a.name()}: {a.extension()}, {a.exe}") # noqa: T201 + print(f"{a.name():<10}: {a.extension():<5}, {a.exe}") # noqa: T201 + + print("\nMetadata Style: (Short Name: Name)") # noqa: T201 + for style in metadata_styles: + print(f"{style.short_name:<10}: {style.name()}") # noqa: T201 def initialize(self) -> argparse.Namespace: conf, _ = self.initial_arg_parser.parse_known_intermixed_args() @@ -245,7 +265,11 @@ class App: update_publishers(self.config) if self.config[0].Commands__command == Action.list_plugins: - self.list_plugins(list(self.talkers.values()), comicapi.comicarchive.archivers) + self.list_plugins( + list(self.talkers.values()), + comicapi.comicarchive.archivers, + comicapi.comicarchive.metadata_styles.values(), + ) return if self.config[0].Commands__command == Action.save_config: From df781f67e3bda67f62e6fe222836da1758f8c31d Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Dec 2023 02:46:53 -0800 Subject: [PATCH 09/15] Fix assigning black_and_white value --- comicapi/metadata/comicrack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index 43893da..96744ec 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -331,7 +331,8 @@ class ComicRack(Metadata): md.locations = set(utils.split(get("Locations"), ",")) tmp = utils.xlate(get("BlackAndWhite")) - md.black_and_white = tmp is not None and tmp.casefold() in ["yes", "true", "1"] + if tmp is not None: + md.black_and_white = tmp.casefold() in ["yes", "true", "1"] # Now extract the credit info for n in root: From c3a8221d996c57ac7cfa15e6491c19edc93f2aed Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 18 Dec 2023 16:56:54 -0800 Subject: [PATCH 10/15] Return an empty object if an archive does not have the requested style --- comicapi/comicarchive.py | 6 ++++-- tests/integration_test.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index ad7c1cf..1f7e517 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -167,8 +167,10 @@ class ComicArchive: def read_metadata(self, style: str) -> GenericMetadata: if style in self.md: return self.md[style] - md = metadata_styles[style].get_metadata(self.archiver) - md.apply_default_page_list(self.get_page_name_list()) + md = GenericMetadata() + if metadata_styles[style].has_metadata(self.archiver): + md = metadata_styles[style].get_metadata(self.archiver) + md.apply_default_page_list(self.get_page_name_list()) return md def read_metadata_string(self, style: str) -> str: diff --git a/tests/integration_test.py b/tests/integration_test.py index ae6d0f0..62658c4 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -85,9 +85,8 @@ def test_delete( # Read the CBZ md = tmp_comic.read_metadata("cr") - # Currently we set the default page list on load + # The default page list is set on load if the comic has the requested metadata style empty_md = comicapi.genericmetadata.GenericMetadata() - empty_md.apply_default_page_list(tmp_comic.get_page_name_list()) # Validate that we got an empty metadata back assert md == empty_md From ceb3b30e5c328462db57f0466e2ff00cce78b98c Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Wed, 20 Dec 2023 21:24:12 -0800 Subject: [PATCH 11/15] Always apply the default page list when writing metadata --- comicapi/comicarchive.py | 16 ++-------------- comicapi/genericmetadata.py | 1 + comicapi/utils.py | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 1f7e517..ed33e5d 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -21,7 +21,6 @@ import pathlib import shutil import sys import traceback -from typing import cast from comicapi import utils from comicapi.archivers import Archiver, UnknownArchiver, ZipArchiver @@ -179,6 +178,7 @@ class ComicArchive: def write_metadata(self, metadata: GenericMetadata, style: str) -> bool: if style in self.md: del self.md[style] + metadata.apply_default_page_list(self.get_page_name_list()) return metadata_styles[style].set_metadata(metadata, self.archiver) def has_metadata(self, style: str) -> bool: @@ -270,19 +270,7 @@ class ComicArchive: def get_page_name_list(self) -> list[str]: if not self.page_list: - # get the list file names in the archive, and sort - files: list[str] = self.archiver.get_filename_list() - - files = cast(list[str], utils.os_sorted(files)) - - # make a sub-list of image files - self.page_list = [] - for name in files: - if ( - os.path.splitext(name)[1].casefold() in [".jpg", ".jpeg", ".png", ".gif", ".webp"] - and os.path.basename(name)[0] != "." - ): - self.page_list.append(name) + self.page_list = utils.get_page_name_list(self.archiver.get_filename_list()) return self.page_list diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 22f89c7..765ac28 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -289,6 +289,7 @@ class GenericMetadata: def apply_default_page_list(self, page_list: Sequence[str]) -> None: # generate a default page list, with the first page marked as the cover + # Create a dictionary of all pages in the metadata pages = {p["image_index"]: p for p in self.pages} cover_set = False diff --git a/comicapi/utils.py b/comicapi/utils.py index 56e8c85..db511e1 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -23,7 +23,7 @@ import unicodedata from collections import defaultdict from collections.abc import Iterable, Mapping from shutil import which # noqa: F401 -from typing import Any, TypeVar +from typing import Any, TypeVar, cast import comicapi.data from comicapi import filenamelexer, filenameparser @@ -195,6 +195,21 @@ def path_to_short_str(original_path: pathlib.Path, renamed_path: pathlib.Path | return path_str +def get_page_name_list(files: list[str]) -> list[str]: + # get the list file names in the archive, and sort + files = cast(list[str], os_sorted(files)) + + # make a sub-list of image files + page_list = [] + for name in files: + if ( + os.path.splitext(name)[1].casefold() in [".jpg", ".jpeg", ".png", ".gif", ".webp"] + and os.path.basename(name)[0] != "." + ): + page_list.append(name) + return page_list + + def get_recursive_filelist(pathlist: list[str]) -> list[str]: """Get a recursive list of of all files under all path items in the list""" From 28be4d9dd718281ceaf4e30d81fc248f3d493eb9 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 23 Dec 2023 23:47:44 -0800 Subject: [PATCH 12/15] Improve errors when loading plugins --- comicapi/comicarchive.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index ed33e5d..4b28773 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -49,7 +49,7 @@ def load_archive_plugins() -> None: else: archivers.append(archiver) except Exception: - logger.warning("Failed to load talker: %s", arch.name) + logger.exception("Failed to load archive plugin: %s", arch.name) archivers.extend(builtin) @@ -68,9 +68,17 @@ def load_metadata_plugins(version: str = f"ComicAPI/{version}") -> None: if arch.module.startswith("comicapi"): builtin[style.short_name] = style(version) else: + if style.short_name in styles: + logger.warning( + "Plugin %s is overriding the existing metadata plugin for %s tags", + arch.module, + style.short_name, + ) styles[style.short_name] = style(version) except Exception: logger.exception("Failed to load metadata plugin: %s", arch.name) + for style_name in set(builtin.keys()).intersection(styles): + logger.warning("Builtin metadata for %s tags are being overridden by a plugin", style_name) metadata_styles.clear() metadata_styles.update(builtin) metadata_styles.update(styles) From c8507c08a91aee6a8c2b8ee3e012879edb663506 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 23 Dec 2023 23:50:58 -0800 Subject: [PATCH 13/15] Ensure ComicRack and CoMet metadata preserve unknown xml tags --- comicapi/metadata/comet.py | 22 +++++++++++++--------- comicapi/metadata/comicrack.py | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py index 67a42a3..e87616b 100644 --- a/comicapi/metadata/comet.py +++ b/comicapi/metadata/comet.py @@ -121,7 +121,8 @@ class CoMet(Metadata): success = True if self.file != self.comet_filename: success = self.remove_metadata(archive) - return archive.write_file(self.comet_filename, self._bytes_from_metadata(metadata)) and success + xml = self._bytes_from_metadata(metadata, archive.read_file(self.comet_filename)) + return success and archive.write_file(self.comet_filename, xml) else: logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") return False @@ -145,19 +146,22 @@ class CoMet(Metadata): tree = ET.ElementTree(ET.fromstring(string)) return self._convert_xml_to_metadata(tree, archive) - def _bytes_from_metadata(self, metadata: GenericMetadata) -> bytes: - tree = self._convert_metadata_to_xml(metadata) + def _bytes_from_metadata(self, metadata: GenericMetadata, xml: bytes = b"") -> bytes: + tree = self._convert_metadata_to_xml(metadata, xml) return ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True) - def _convert_metadata_to_xml(self, metadata: GenericMetadata) -> ET.ElementTree: + def _convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b"") -> ET.ElementTree: # shorthand for the metadata md = metadata - # build a tree structure - root = ET.Element("comet") - root.attrib["xmlns:comet"] = "http://www.denvog.com/comet/" - root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" - root.attrib["xsi:schemaLocation"] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd" + if xml: + root = ET.fromstring(xml) + else: + # build a tree structure + root = ET.Element("comet") + root.attrib["xmlns:comet"] = "http://www.denvog.com/comet/" + root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance" + root.attrib["xsi:schemaLocation"] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd" # helper func def assign(comet_entry: str, md_entry: Any) -> None: diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index 96744ec..e1faca0 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -118,7 +118,7 @@ class ComicRack(Metadata): def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool: if self.supports_metadata(archive): - return archive.write_file(self.file, self._bytes_from_metadata(metadata)) + return archive.write_file(self.file, self._bytes_from_metadata(metadata, archive.read_file(self.file))) else: logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") return False From 109d8efc0b14aef33039aad5dc8fbf4a40f2bb75 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 24 Dec 2023 18:04:35 -0800 Subject: [PATCH 14/15] Update pyinstaller hook --- comicapi/__pyinstaller/hook-comicapi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/comicapi/__pyinstaller/hook-comicapi.py b/comicapi/__pyinstaller/hook-comicapi.py index 6101d69..58b0164 100644 --- a/comicapi/__pyinstaller/hook-comicapi.py +++ b/comicapi/__pyinstaller/hook-comicapi.py @@ -3,4 +3,8 @@ from __future__ import annotations from PyInstaller.utils.hooks import collect_data_files, collect_entry_point datas, hiddenimports = collect_entry_point("comicapi.archiver") +mdatas, mhiddenimports = collect_entry_point("comicapi.metadata") + +hiddenimports += mhiddenimports +datas += mdatas datas += collect_data_files("comicapi.data") From 59694993ffc129e64de91f73c5236c9d4e6d201c Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 24 Dec 2023 18:28:38 -0800 Subject: [PATCH 15/15] Fix loading previous existing xml --- comicapi/metadata/comet.py | 7 +++++-- comicapi/metadata/comicrack.py | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py index e87616b..d8b67e7 100644 --- a/comicapi/metadata/comet.py +++ b/comicapi/metadata/comet.py @@ -119,10 +119,13 @@ class CoMet(Metadata): def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool: if self.supports_metadata(archive): success = True + xml = b"" + if self.has_metadata(archive): + xml = archive.read_file(self.file) if self.file != self.comet_filename: success = self.remove_metadata(archive) - xml = self._bytes_from_metadata(metadata, archive.read_file(self.comet_filename)) - return success and archive.write_file(self.comet_filename, xml) + + return success and archive.write_file(self.comet_filename, self._bytes_from_metadata(metadata, xml)) else: logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") return False diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py index e1faca0..7fea529 100644 --- a/comicapi/metadata/comicrack.py +++ b/comicapi/metadata/comicrack.py @@ -118,7 +118,10 @@ class ComicRack(Metadata): def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool: if self.supports_metadata(archive): - return archive.write_file(self.file, self._bytes_from_metadata(metadata, archive.read_file(self.file))) + xml = b"" + if self.has_metadata(archive): + xml = archive.read_file(self.file) + return archive.write_file(self.file, self._bytes_from_metadata(metadata, xml)) else: logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata") return False