From ae5e246180ca633ce6d547ca5ceb39ba10f45923 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 17 Dec 2023 21:47:43 -0800 Subject: [PATCH] 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)