Add plugin support for metadata

This commit is contained in:
Timmy Welch 2023-12-17 21:47:43 -08:00
parent 04b3b6b4ab
commit ae5e246180
34 changed files with 1059 additions and 1025 deletions

View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

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

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1,5 @@
from __future__ import annotations
from comicapi.metadata.metadata import Metadata
__all__ = ["Metadata"]

View File

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

View File

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

View File

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

View File

@ -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 ""

View File

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

View File

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

View File

@ -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] = []

View File

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

View File

@ -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,

View File

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

View File

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

View File

@ -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()):

View File

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

View File

@ -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(

View File

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

View File

@ -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(

View File

@ -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:

View File

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

View File

@ -50,21 +50,10 @@
</column>
<column>
<property name="text">
<string>CR</string>
<string>MD</string>
</property>
<property name="toolTip">
<string>Has ComicRack Tags</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>CBL</string>
</property>
<property name="toolTip">
<string>Has ComicBookLover Tags</string>
<string>Metadata</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>

View File

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

View File

@ -1178,15 +1178,11 @@
<string>Remove Tags</string>
</property>
<addaction name="actionRemoveAuto"/>
<addaction name="actionRemoveCBLTags"/>
<addaction name="actionRemoveCRTags"/>
</widget>
<widget class="QMenu" name="menuViewRawTags">
<property name="title">
<string>View Raw Tags</string>
</property>
<addaction name="actionViewRawCRTags"/>
<addaction name="actionViewRawCBLTags"/>
</widget>
<addaction name="actionLoad"/>
<addaction name="actionLoadFolder"/>

View File

@ -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 =

View File

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

View File

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

View File

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

View File

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