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
-
-
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)