Merge branch 'metadata-plugin' into develop

This commit is contained in:
Timmy Welch 2023-12-24 18:32:42 -08:00
commit 7d1bf8525b
37 changed files with 1517 additions and 1342 deletions

View File

@ -3,4 +3,8 @@ from __future__ import annotations
from PyInstaller.utils.hooks import collect_data_files, collect_entry_point
datas, hiddenimports = collect_entry_point("comicapi.archiver")
mdatas, mhiddenimports = collect_entry_point("comicapi.metadata")
hiddenimports += mhiddenimports
datas += mdatas
datas += collect_data_files("comicapi.data")

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

@ -1,214 +0,0 @@
"""A class to encapsulate CoMet data"""
#
# Copyright 2012-2014 ComicTagger Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import logging
import xml.etree.ElementTree as ET
from typing import Any
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
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"]
def metadata_from_string(self, string: str) -> GenericMetadata:
tree = ET.ElementTree(ET.fromstring(string))
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 convert_metadata_to_xml(self, metadata: GenericMetadata) -> ET.ElementTree:
# shorthand for the metadata
md = metadata
# build a tree structure
root = ET.Element("comet")
root.attrib["xmlns:comet"] = "http://www.denvog.com/comet/"
root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib["xsi:schemaLocation"] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
# helper func
def assign(comet_entry: str, md_entry: Any) -> None:
if md_entry is not None:
ET.SubElement(root, comet_entry).text = str(md_entry)
# title is manditory
if md.title is None:
md.title = ""
assign("title", md.title)
assign("series", md.series)
assign("issue", md.issue) # must be int??
assign("volume", md.volume)
assign("description", md.description)
assign("publisher", md.publisher)
assign("pages", md.page_count)
assign("format", md.format)
assign("language", md.language)
assign("rating", md.maturity_rating)
assign("price", md.price)
assign("isVersionOf", md.is_version_of)
assign("rights", md.rights)
assign("identifier", md.identifier)
assign("lastMark", md.last_mark)
assign("genre", ",".join(md.genres)) # TODO repeatable
if md.characters is not None:
char_list = [c.strip() for c in md.characters]
for c in char_list:
assign("character", c)
if md.manga is not None and md.manga == "YesAndRightToLeft":
assign("readingDirection", "rtl")
if md.year is not None:
date_str = f"{md.year:04}"
if md.month is not None:
date_str += f"-{md.month:02}"
assign("date", date_str)
assign("coverImage", md._cover_image)
# 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):
ET.SubElement(root, "writer").text = str(credit["person"])
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):
ET.SubElement(root, "inker").text = str(credit["person"])
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):
ET.SubElement(root, "letterer").text = str(credit["person"])
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):
ET.SubElement(root, "editor").text = str(credit["person"])
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: ET.ElementTree) -> GenericMetadata:
root = tree.getroot()
if root.tag != "comet":
raise Exception("Not a CoMet file")
metadata = GenericMetadata()
md = metadata
# Helper function
def get(tag: str) -> Any:
node = root.find(tag)
if node is not None:
return node.text
return None
md.series = utils.xlate(get("series"))
md.title = utils.xlate(get("title"))
md.issue = utils.xlate(get("issue"))
md.volume = utils.xlate_int(get("volume"))
md.description = utils.xlate(get("description"))
md.publisher = utils.xlate(get("publisher"))
md.language = utils.xlate(get("language"))
md.format = utils.xlate(get("format"))
md.page_count = utils.xlate_int(get("pages"))
md.maturity_rating = utils.xlate(get("rating"))
md.price = utils.xlate_float(get("price"))
md.is_version_of = utils.xlate(get("isVersionOf"))
md.rights = utils.xlate(get("rights"))
md.identifier = utils.xlate(get("identifier"))
md.last_mark = utils.xlate(get("lastMark"))
_, md.month, md.year = utils.parse_date_str(utils.xlate(get("date")))
md._cover_image = utils.xlate(get("coverImage"))
reading_direction = utils.xlate(get("readingDirection"))
if reading_direction is not None and reading_direction == "rtl":
md.manga = "YesAndRightToLeft"
# loop for genre tags
for n in root:
if n.tag == "genre":
md.genres.add((n.text or "").strip())
# loop for character tags
for n in root:
if n.tag == "character":
md.characters.add((n.text or "").strip())
# Now extract the credit info
for n in root:
if any(
[
n.tag == "writer",
n.tag == "penciller",
n.tag == "inker",
n.tag == "colorist",
n.tag == "letterer",
n.tag == "editor",
]
):
metadata.add_credit((n.text or "").strip(), n.tag.title())
if n.tag == "coverDesigner":
metadata.add_credit((n.text or "").strip(), "Cover")
metadata.is_empty = False
return metadata
# verify that the string actually contains CoMet data in XML format
def validate_string(self, string: str) -> bool:
try:
tree = ET.ElementTree(ET.fromstring(string))
root = tree.getroot()
if root.tag != "comet":
return False
except ET.ParseError:
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

@ -21,18 +21,17 @@ import pathlib
import shutil
import sys
import traceback
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:
@ -50,42 +49,64 @@ def load_archive_plugins() -> None:
else:
archivers.append(archiver)
except Exception:
logger.warning("Failed to load talker: %s", arch.name)
logger.exception("Failed to load archive plugin: %s", arch.name)
archivers.extend(builtin)
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:
if style.short_name in styles:
logger.warning(
"Plugin %s is overriding the existing metadata plugin for %s tags",
arch.module,
style.short_name,
)
styles[style.short_name] = style(version)
except Exception:
logger.exception("Failed to load metadata plugin: %s", arch.name)
for style_name in set(builtin.keys()).intersection(styles):
logger.warning("Builtin metadata for %s tags are being overridden by a plugin", style_name)
metadata_styles.clear()
metadata_styles.update(builtin)
metadata_styles.update(styles)
class ComicArchive:
logo_data = b""
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.path = pathlib.Path(path).absolute()
def __init__(
self, path: pathlib.Path | str | Archiver, default_image_path: pathlib.Path | str | None = None
) -> None:
self.md: dict[str, GenericMetadata] = {}
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)
if isinstance(path, Archiver):
self.path = path.path
self.archiver: Archiver = path
else:
self.path = pathlib.Path(path).absolute()
self.archiver = UnknownArchiver.open(self.path)
load_archive_plugins()
load_metadata_plugins()
for archiver in archivers:
if archiver.enabled and archiver.is_valid(self.path):
self.archiver = archiver.open(self.path)
@ -98,19 +119,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 +154,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 +171,33 @@ 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]
md = GenericMetadata()
if metadata_styles[style].has_metadata(self.archiver):
md = metadata_styles[style].get_metadata(self.archiver)
md.apply_default_page_list(self.get_page_name_list())
return md
def 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]
metadata.apply_default_page_list(self.get_page_name_list())
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""
@ -263,23 +276,9 @@ class ComicArchive:
return scanner_page_index
def get_page_name_list(self, sort_list: bool = True) -> list[str]:
def get_page_name_list(self) -> list[str]:
if not self.page_list:
# get the list file names in the archive, and sort
files: list[str] = self.archiver.get_filename_list()
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
if sort_list:
files = cast(list[str], utils.os_sorted(files))
# make a sub-list of image files
self.page_list = []
for name in files:
if (
os.path.splitext(name)[1].casefold() in [".jpg", ".jpeg", ".png", ".gif", ".webp"]
and os.path.basename(name)[0] != "."
):
self.page_list.append(name)
self.page_list = utils.get_page_name_list(self.archiver.get_filename_list())
return self.page_list
@ -288,239 +287,13 @@ 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"])
p["filename"] = self.get_page_name(idx)
if self.pil_available:
try:
from PIL import Image
@ -528,7 +301,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 +311,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

@ -26,7 +26,7 @@ import logging
from collections.abc import Sequence
from typing import Any, TypedDict
from typing_extensions import NamedTuple
from typing_extensions import NamedTuple, Required
from comicapi import utils
@ -54,13 +54,14 @@ class PageType:
class ImageMetadata(TypedDict, total=False):
Type: str
Bookmark: str
DoublePage: bool
Image: int
ImageSize: str
ImageHeight: str
ImageWidth: str
filename: str
type: str
bookmark: str
double_page: bool
image_index: Required[int]
size: str
height: str
width: str
class Credit(TypedDict):
@ -173,6 +174,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
@ -259,18 +287,34 @@ class GenericMetadata:
else:
self.add_credit(c["person"], c["role"], primary)
def set_default_page_list(self, count: int) -> None:
def apply_default_page_list(self, page_list: Sequence[str]) -> None:
# generate a default page list, with the first page marked as the cover
for i in range(count):
page_dict = ImageMetadata(Image=i)
if i == 0:
page_dict["Type"] = PageType.FrontCover
self.pages.append(page_dict)
# Create a dictionary of all pages in the metadata
pages = {p["image_index"]: p for p in self.pages}
cover_set = False
# Go through each page in the archive
# The indexes should always match up
# It might be a good idea to validate that each page in `pages` is found
for i, filename in enumerate(page_list):
if i not in pages:
pages[i] = ImageMetadata(image_index=i, filename=filename)
else:
pages[i]["filename"] = filename
# Check if we know what the cover is
cover_set = pages[i].get("type", None) == PageType.FrontCover or cover_set
self.pages = [p[1] for p in sorted(pages.items())]
# Set the cover to the first image if we don't know what the cover is
if not cover_set:
self.pages[0]["type"] = PageType.FrontCover
def get_archive_page_index(self, pagenum: int) -> int:
# convert the displayed page number to the page index of the file in the archive
if pagenum < len(self.pages):
return int(self.pages[pagenum]["Image"])
return int(self.pages[pagenum]["image_index"])
return 0
@ -278,8 +322,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 +503,39 @@ 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(
Bookmark="Interview",
Image=23,
ImageHeight="2032",
ImageSize="800965",
ImageWidth="1338",
Type=PageType.Letters,
image_index=0, height="1280", size="195977", width="800", type=PageType.FrontCover, filename="!cover.jpg"
),
ImageMetadata(image_index=1, height="2039", size="611993", width="1327", filename="01.jpg"),
ImageMetadata(image_index=2, height="2039", size="783726", width="1327", filename="02.jpg"),
ImageMetadata(image_index=3, height="2039", size="679584", width="1327", filename="03.jpg"),
ImageMetadata(image_index=4, height="2039", size="788179", width="1327", filename="04.jpg"),
ImageMetadata(image_index=5, height="2039", size="864433", width="1327", filename="05.jpg"),
ImageMetadata(image_index=6, height="2039", size="765606", width="1327", filename="06.jpg"),
ImageMetadata(image_index=7, height="2039", size="876427", width="1327", filename="07.jpg"),
ImageMetadata(image_index=8, height="2039", size="852622", width="1327", filename="08.jpg"),
ImageMetadata(image_index=9, height="2039", size="800205", width="1327", filename="09.jpg"),
ImageMetadata(image_index=10, height="2039", size="746243", width="1326", filename="10.jpg"),
ImageMetadata(image_index=11, height="2039", size="718062", width="1327", filename="11.jpg"),
ImageMetadata(image_index=12, height="2039", size="532179", width="1326", filename="12.jpg"),
ImageMetadata(image_index=13, height="2039", size="686708", width="1327", filename="13.jpg"),
ImageMetadata(image_index=14, height="2039", size="641907", width="1327", filename="14.jpg"),
ImageMetadata(image_index=15, height="2039", size="805388", width="1327", filename="15.jpg"),
ImageMetadata(image_index=16, height="2039", size="668927", width="1326", filename="16.jpg"),
ImageMetadata(image_index=17, height="2039", size="710605", width="1327", filename="17.jpg"),
ImageMetadata(image_index=18, height="2039", size="761398", width="1326", filename="18.jpg"),
ImageMetadata(image_index=19, height="2039", size="743807", width="1327", filename="19.jpg"),
ImageMetadata(image_index=20, height="2039", size="552911", width="1326", filename="20.jpg"),
ImageMetadata(image_index=21, height="2039", size="556827", width="1327", filename="21.jpg"),
ImageMetadata(image_index=22, height="2039", size="675078", width="1326", filename="22.jpg"),
ImageMetadata(
bookmark="Interview",
image_index=23,
height="2032",
size="800965",
width="1338",
type=PageType.Letters,
filename="23.jpg",
),
],
price=None,

View File

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

322
comicapi/metadata/comet.py Normal file
View File

@ -0,0 +1,322 @@
"""A class to encapsulate CoMet data"""
#
# Copyright 2012-2014 ComicTagger Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata, ImageMetadata, PageType
from comicapi.metadata import Metadata
logger = logging.getLogger(__name__)
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",)
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 = {
"series",
"issue",
"title",
"volume",
"genres",
"description",
"publisher",
"language",
"format",
"maturity_rating",
"month",
"year",
"page_count",
"characters",
"credits",
"credits.person",
"credits.primary",
"credits.role",
"price",
"is_version_of",
"rights",
"identifier",
"last_mark",
"pages.type", # This is required for setting the cover image none of the other types will be saved
"pages",
}
def supports_credit_role(self, role: str) -> bool:
return role.casefold() in self._get_parseable_credits()
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, archive)
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
xml = b""
if self.has_metadata(archive):
xml = archive.read_file(self.file)
if self.file != self.comet_filename:
success = self.remove_metadata(archive)
return success and archive.write_file(self.comet_filename, self._bytes_from_metadata(metadata, xml))
else:
logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata")
return False
def name(self) -> str:
return "Comic Metadata (CoMet)"
@classmethod
def _get_parseable_credits(cls) -> list[str]:
parsable_credits: list[str] = []
parsable_credits.extend(cls._writer_synonyms)
parsable_credits.extend(cls._penciller_synonyms)
parsable_credits.extend(cls._inker_synonyms)
parsable_credits.extend(cls._colorist_synonyms)
parsable_credits.extend(cls._letterer_synonyms)
parsable_credits.extend(cls._cover_synonyms)
parsable_credits.extend(cls._editor_synonyms)
return parsable_credits
def _metadata_from_bytes(self, string: bytes, archive: Archiver) -> GenericMetadata:
tree = ET.ElementTree(ET.fromstring(string))
return self._convert_xml_to_metadata(tree, archive)
def _bytes_from_metadata(self, metadata: GenericMetadata, xml: bytes = b"") -> bytes:
tree = self._convert_metadata_to_xml(metadata, xml)
return ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True)
def _convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b"") -> ET.ElementTree:
# shorthand for the metadata
md = metadata
if xml:
root = ET.fromstring(xml)
else:
# build a tree structure
root = ET.Element("comet")
root.attrib["xmlns:comet"] = "http://www.denvog.com/comet/"
root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib["xsi:schemaLocation"] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
# helper func
def assign(comet_entry: str, md_entry: Any) -> None:
if md_entry is not None:
ET.SubElement(root, comet_entry).text = str(md_entry)
# title is manditory
assign("title", md.title or "")
assign("series", md.series)
assign("issue", md.issue) # must be int??
assign("volume", md.volume)
assign("description", md.description)
assign("publisher", md.publisher)
assign("pages", md.page_count)
assign("format", md.format)
assign("language", md.language)
assign("rating", md.maturity_rating)
assign("price", md.price)
assign("isVersionOf", md.is_version_of)
assign("rights", md.rights)
assign("identifier", md.identifier)
assign("lastMark", md.last_mark)
assign("genre", ",".join(md.genres)) # TODO repeatable
for c in md.characters:
assign("character", c.strip())
if md.manga is not None and md.manga == "YesAndRightToLeft":
assign("readingDirection", "rtl")
if md.year is not None:
date_str = f"{md.year:04}"
if md.month is not None:
date_str += f"-{md.month:02}"
assign("date", date_str)
page = md.get_cover_page_index_list()[0]
assign("coverImage", md.pages[page]["filename"])
# loop thru credits, and build a list for each role that CoMet supports
for credit in metadata.credits:
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):
ET.SubElement(root, "penciller").text = str(credit["person"])
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):
ET.SubElement(root, "colorist").text = str(credit["person"])
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):
ET.SubElement(root, "coverDesigner").text = str(credit["person"])
if credit["role"].casefold() in set(self._editor_synonyms):
ET.SubElement(root, "editor").text = str(credit["person"])
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: ET.ElementTree, archive: Archiver) -> GenericMetadata:
root = tree.getroot()
if root.tag != "comet":
raise Exception("Not a CoMet file")
metadata = GenericMetadata()
md = metadata
# Helper function
def get(tag: str) -> Any:
node = root.find(tag)
if node is not None:
return node.text
return None
md.series = utils.xlate(get("series"))
md.title = utils.xlate(get("title"))
md.issue = utils.xlate(get("issue"))
md.volume = utils.xlate_int(get("volume"))
md.description = utils.xlate(get("description"))
md.publisher = utils.xlate(get("publisher"))
md.language = utils.xlate(get("language"))
md.format = utils.xlate(get("format"))
md.page_count = utils.xlate_int(get("pages"))
md.maturity_rating = utils.xlate(get("rating"))
md.price = utils.xlate_float(get("price"))
md.is_version_of = utils.xlate(get("isVersionOf"))
md.rights = utils.xlate(get("rights"))
md.identifier = utils.xlate(get("identifier"))
md.last_mark = utils.xlate(get("lastMark"))
_, md.month, md.year = utils.parse_date_str(utils.xlate(get("date")))
ca = ComicArchive(archive)
cover_filename = utils.xlate(get("coverImage"))
page_list = ca.get_page_name_list()
if cover_filename in page_list:
cover_index = page_list.index(cover_filename)
md.pages = [ImageMetadata(image_index=cover_index, filename=cover_filename, type=PageType.FrontCover)]
reading_direction = utils.xlate(get("readingDirection"))
if reading_direction is not None and reading_direction == "rtl":
md.manga = "YesAndRightToLeft"
# loop for genre tags
for n in root:
if n.tag == "genre":
md.genres.add((n.text or "").strip())
# loop for character tags
for n in root:
if n.tag == "character":
md.characters.add((n.text or "").strip())
# Now extract the credit info
for n in root:
if any(
[
n.tag == "writer",
n.tag == "penciller",
n.tag == "inker",
n.tag == "colorist",
n.tag == "letterer",
n.tag == "editor",
]
):
metadata.add_credit((n.text or "").strip(), n.tag.title())
if n.tag == "coverDesigner":
metadata.add_credit((n.text or "").strip(), "Cover")
metadata.is_empty = False
return metadata
# verify that the string actually contains CoMet data in XML format
def _validate_bytes(self, string: bytes) -> bool:
try:
tree = ET.ElementTree(ET.fromstring(string))
root = tree.getroot()
if root.tag != "comet":
return False
except ET.ParseError:
return False
return True

View File

@ -20,11 +20,13 @@ from datetime import datetime
from typing import Any, Literal, TypedDict
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
from comicapi.archivers import Archiver
from comicapi.genericmetadata import Credit, GenericMetadata
from comicapi.metadata import Metadata
logger = logging.getLogger(__name__)
CBILiteralType = Literal[
_CBILiteralType = Literal[
"series",
"title",
"issue",
@ -44,13 +46,7 @@ CBILiteralType = Literal[
]
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 +60,80 @@ class ComicBookInfoJson(TypedDict, total=False):
genre: str
language: str
country: str
credits: list[Credits]
credits: list[Credit]
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 = {
"series",
"issue",
"issue_count",
"title",
"volume",
"volume_count",
"genres",
"description",
"publisher",
"month",
"year",
"language",
"country",
"critical_rating",
"tags",
"credits",
"credits.person",
"credits.primary",
"credits.role",
}
def supports_credit_role(self, role: str) -> bool:
return True
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 +155,7 @@ class ComicBookInfo:
metadata.critical_rating = utils.xlate_int(cbi.get("rating"))
metadata.credits = [
Credits(
Credit(
person=x["person"] if "person" in x else "",
role=x["role"] if "role" in x else "",
primary=x["primary"] if "primary" in x else False,
@ -113,11 +172,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 +186,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 +198,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 +220,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

@ -17,50 +17,144 @@ from __future__ import annotations
import logging
import xml.etree.ElementTree as ET
from collections import OrderedDict
from typing import Any, cast
from xml.etree.ElementTree import ElementTree
from typing import Any
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)
enabled = True
short_name = "cr"
def __init__(self, version: str) -> None:
super().__init__(version)
self.file = "ComicInfo.xml"
self.supported_attributes = {
"series",
"issue",
"issue_count",
"title",
"volume",
"genres",
"description",
"notes",
"alternate_series",
"alternate_number",
"alternate_count",
"story_arcs",
"series_groups",
"publisher",
"imprint",
"day",
"month",
"year",
"language",
"web_link",
"format",
"manga",
"black_and_white",
"maturity_rating",
"critical_rating",
"scan_info",
"pages",
"pages.bookmark",
"pages.double_page",
"pages.height",
"pages.image_index",
"pages.size",
"pages.type",
"pages.width",
"page_count",
"characters",
"teams",
"locations",
"credits",
"credits.person",
"credits.role",
}
def supports_credit_role(self, role: str) -> bool:
return role.casefold() in self._get_parseable_credits()
def supports_metadata(self, archive: Archiver) -> bool:
return archive.supports_files()
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):
xml = b""
if self.has_metadata(archive):
xml = archive.read_file(self.file)
return archive.write_file(self.file, self._bytes_from_metadata(metadata, xml))
else:
logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata")
return False
def name(self) -> str:
return "Comic Rack"
@classmethod
def _get_parseable_credits(cls) -> list[str]:
parsable_credits: list[str] = []
parsable_credits.extend(cls._writer_synonyms)
parsable_credits.extend(cls._penciller_synonyms)
parsable_credits.extend(cls._inker_synonyms)
parsable_credits.extend(cls._colorist_synonyms)
parsable_credits.extend(cls._letterer_synonyms)
parsable_credits.extend(cls._cover_synonyms)
parsable_credits.extend(cls._editor_synonyms)
return parsable_credits
def metadata_from_string(self, string: bytes) -> GenericMetadata:
tree = ET.ElementTree(ET.fromstring(string))
return self.convert_xml_to_metadata(tree)
def _metadata_from_bytes(self, string: bytes) -> GenericMetadata:
root = ET.fromstring(string)
return self._convert_xml_to_metadata(root)
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 _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"") -> ElementTree:
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 +162,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,32 +171,16 @@ 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)
assign("Title", md.title)
assign("Series", md.series)
assign("Number", md.issue)
assign("Count", md.issue_count)
assign("Volume", md.volume)
assign("AlternateSeries", md.alternate_series)
assign("AlternateNumber", md.alternate_number)
assign("StoryArc", md.story_arcs)
assign("SeriesGroup", md.series_groups)
assign("AlternateCount", md.alternate_count)
assign("Summary", md.description)
assign("Notes", md.notes)
assign("Year", md.year)
assign("Month", md.month)
assign("Day", md.day)
# need to specially process the credits, since they are structured
# differently than CIX
credit_writer_list = []
@ -116,28 +194,61 @@ 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
assign("Series", md.series)
assign("Number", md.issue)
assign("Count", md.issue_count)
assign("Title", md.title)
assign("Volume", md.volume)
assign("Genre", md.genres)
assign("Summary", md.description)
assign("Notes", md.notes)
assign("AlternateSeries", md.alternate_series)
assign("AlternateNumber", md.alternate_number)
assign("AlternateCount", md.alternate_count)
assign("StoryArc", md.story_arcs)
assign("SeriesGroup", md.series_groups)
assign("Publisher", md.publisher)
assign("Imprint", md.imprint)
assign("Day", md.day)
assign("Month", md.month)
assign("Year", md.year)
assign("LanguageISO", md.language)
assign("Web", md.web_link)
assign("Format", md.format)
assign("Manga", md.manga)
assign("BlackAndWhite", "Yes" if md.black_and_white else None)
assign("AgeRating", md.maturity_rating)
assign("CommunityRating", md.critical_rating)
assign("ScanInformation", md.scan_info)
assign("PageCount", md.page_count)
assign("Characters", md.characters)
assign("Teams", md.teams)
assign("Locations", md.locations)
assign("Writer", ", ".join(credit_writer_list))
assign("Penciller", ", ".join(credit_penciller_list))
assign("Inker", ", ".join(credit_inker_list))
@ -146,22 +257,6 @@ class ComicInfoXml:
assign("CoverArtist", ", ".join(credit_cover_list))
assign("Editor", ", ".join(credit_editor_list))
assign("Publisher", md.publisher)
assign("Imprint", md.imprint)
assign("Genre", md.genres)
assign("Web", md.web_link)
assign("PageCount", md.page_count)
assign("LanguageISO", md.language)
assign("Format", md.format)
assign("AgeRating", md.maturity_rating)
assign("CommunityRating", md.critical_rating)
assign("BlackAndWhite", "Yes" if md.black_and_white else None)
assign("Manga", md.manga)
assign("Characters", md.characters)
assign("Teams", md.teams)
assign("Locations", md.locations)
assign("ScanInformation", md.scan_info)
# loop and add the page entries under pages node
pages_node = root.find("Pages")
if pages_node is not None:
@ -171,17 +266,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")
@ -194,38 +300,43 @@ class ComicInfoXml:
md = GenericMetadata()
md.series = utils.xlate(get("Series"))
md.title = utils.xlate(get("Title"))
md.issue = utils.xlate(get("Number"))
md.issue_count = utils.xlate_int(get("Count"))
md.title = utils.xlate(get("Title"))
md.volume = utils.xlate_int(get("Volume"))
md.genres = set(utils.split(get("Genre"), ","))
md.description = utils.xlate(get("Summary"))
md.notes = utils.xlate(get("Notes"))
md.alternate_series = utils.xlate(get("AlternateSeries"))
md.alternate_number = utils.xlate(get("AlternateNumber"))
md.alternate_count = utils.xlate_int(get("AlternateCount"))
md.description = utils.xlate(get("Summary"))
md.notes = utils.xlate(get("Notes"))
md.year = utils.xlate_int(get("Year"))
md.month = utils.xlate_int(get("Month"))
md.day = utils.xlate_int(get("Day"))
md.story_arcs = utils.split(get("StoryArc"), ",")
md.series_groups = utils.split(get("SeriesGroup"), ",")
md.publisher = utils.xlate(get("Publisher"))
md.imprint = utils.xlate(get("Imprint"))
md.genres = set(utils.split(get("Genre"), ","))
md.web_link = utils.xlate(get("Web"))
md.day = utils.xlate_int(get("Day"))
md.month = utils.xlate_int(get("Month"))
md.year = utils.xlate_int(get("Year"))
md.language = utils.xlate(get("LanguageISO"))
md.web_link = utils.xlate(get("Web"))
md.format = utils.xlate(get("Format"))
md.manga = utils.xlate(get("Manga"))
md.maturity_rating = utils.xlate(get("AgeRating"))
md.critical_rating = utils.xlate_float(get("CommunityRating"))
md.scan_info = utils.xlate(get("ScanInformation"))
md.page_count = utils.xlate_int(get("PageCount"))
md.characters = set(utils.split(get("Characters"), ","))
md.teams = set(utils.split(get("Teams"), ","))
md.locations = set(utils.split(get("Locations"), ","))
md.page_count = utils.xlate_int(get("PageCount"))
md.scan_info = utils.xlate(get("ScanInformation"))
md.story_arcs = utils.split(get("StoryArc"), ",")
md.series_groups = utils.split(get("SeriesGroup"), ",")
md.maturity_rating = utils.xlate(get("AgeRating"))
md.critical_rating = utils.xlate_float(get("CommunityRating"))
tmp = utils.xlate(get("BlackAndWhite"))
if tmp is not None and tmp.casefold() in ["yes", "true", "1"]:
md.black_and_white = True
if tmp is not None:
md.black_and_white = tmp.casefold() in ["yes", "true", "1"]
# Now extract the credit info
for n in root:
if any(
@ -250,22 +361,36 @@ class ComicInfoXml:
# parse page data now
pages_node = root.find("Pages")
if pages_node is not None:
for page in pages_node:
for i, page in enumerate(pages_node):
p: dict[str, Any] = page.attrib
if "Image" in p:
p["Image"] = int(p["Image"])
md_page = ImageMetadata(image_index=int(p.get("Image", i)))
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 "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(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,123 @@
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_id",
"series",
"series_aliases",
"issue",
"issue_count",
"title",
"title_aliases",
"volume",
"volume_count",
"genres",
"description",
"notes",
"alternate_series",
"alternate_number",
"alternate_count",
"story_arcs",
"series_groups",
"publisher",
"imprint",
"day",
"month",
"year",
"language",
"country",
"web_link",
"format",
"manga",
"black_and_white",
"maturity_rating",
"critical_rating",
"scan_info",
"tags",
"pages",
"pages.type",
"pages.bookmark",
"pages.double_page",
"pages.image_index",
"pages.size",
"pages.height",
"pages.width",
"page_count",
"characters",
"teams",
"locations",
"credits",
"credits.person",
"credits.role",
"credits.primary",
"price",
"is_version_of",
"rights",
"identifier",
"last_mark",
}
def supports_credit_role(self, role: str) -> bool:
return False
def supports_metadata(self, archive: Archiver) -> bool:
"""
Checks the given archive for the ability to save this metadata style.
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

@ -23,7 +23,7 @@ import unicodedata
from collections import defaultdict
from collections.abc import Iterable, Mapping
from shutil import which # noqa: F401
from typing import Any, TypeVar
from typing import Any, TypeVar, cast
import comicapi.data
from comicapi import filenamelexer, filenameparser
@ -195,6 +195,21 @@ def path_to_short_str(original_path: pathlib.Path, renamed_path: pathlib.Path |
return path_str
def get_page_name_list(files: list[str]) -> list[str]:
# get the list file names in the archive, and sort
files = cast(list[str], os_sorted(files))
# make a sub-list of image files
page_list = []
for name in files:
if (
os.path.splitext(name)[1].casefold() in [".jpg", ".jpeg", ".png", ".gif", ".webp"]
and os.path.basename(name)[0] != "."
):
page_list.append(name)
return page_list
def get_recursive_filelist(pathlist: list[str]) -> list[str]:
"""Get a recursive list of of all files under all path items in the list"""

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.")
@ -244,7 +245,7 @@ class CLI:
def create_local_metadata(self, ca: ComicArchive) -> GenericMetadata:
md = GenericMetadata()
md.set_default_page_list(ca.get_number_of_pages())
md.apply_default_page_list(ca.get_page_name_list())
# now, overlay the parsed filename info
if self.config.Runtime_Options__parse_filename:
@ -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

@ -12,13 +12,19 @@ from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replaceme
def general(parser: settngs.Manager) -> None:
# General Settings
parser.add_setting("check_for_new_version", default=False, cmdline=False)
parser.add_setting(
"--disable-cr",
default=False,
action=argparse.BooleanOptionalAction,
help="Disable the ComicRack metadata type",
)
def internal(parser: settngs.Manager) -> None:
# 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 +218,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,11 +91,12 @@ 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
General__check_for_new_version: bool
General__disable_cr: bool
Dialog_Flags__show_disclaimer: bool
Dialog_Flags__dont_notify_about_this_version: str

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

@ -39,11 +39,6 @@ class FileTableWidgetItem(QtWidgets.QTableWidgetItem):
return self.data(QtCore.Qt.ItemDataRole.UserRole) < other.data(QtCore.Qt.ItemDataRole.UserRole) # type: ignore
class FileInfo:
def __init__(self, ca: ComicArchive) -> None:
self.ca: ComicArchive = ca
class FileSelectionList(QtWidgets.QWidget):
selectionChanged = QtCore.pyqtSignal(QtCore.QVariant)
listCleared = QtCore.pyqtSignal()
@ -137,8 +132,8 @@ class FileSelectionList(QtWidgets.QWidget):
def get_archive_by_row(self, row: int) -> ComicArchive | None:
if row >= 0:
fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
return fi.ca
ca: ComicArchive = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
return ca
return None
def get_current_archive(self) -> ComicArchive | None:
@ -206,7 +201,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()
@ -282,17 +277,15 @@ class FileSelectionList(QtWidgets.QWidget):
row: int = self.twList.rowCount()
self.twList.insertRow(row)
fi = FileInfo(ca)
filename_item = QtWidgets.QTableWidgetItem()
folder_item = QtWidgets.QTableWidgetItem()
cix_item = FileTableWidgetItem()
md_item = FileTableWidgetItem()
cbi_item = FileTableWidgetItem()
readonly_item = FileTableWidgetItem()
type_item = QtWidgets.QTableWidgetItem()
filename_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
filename_item.setData(QtCore.Qt.ItemDataRole.UserRole, fi)
filename_item.setData(QtCore.Qt.ItemDataRole.UserRole, ca)
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
folder_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
@ -301,9 +294,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)
@ -320,62 +313,43 @@ class FileSelectionList(QtWidgets.QWidget):
def update_row(self, row: int) -> None:
if row >= 0:
fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
ca: ComicArchive = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
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)
item_text = os.path.split(fi.ca.path)[0]
item_text = os.path.split(ca.path)[0]
folder_item.setText(item_text)
folder_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item_text = os.path.split(fi.ca.path)[1]
item_text = os.path.split(ca.path)[1]
filename_item.setText(item_text)
filename_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item_text = fi.ca.archiver.name()
item_text = ca.archiver.name()
type_item.setText(item_text)
type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
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)
styles = ", ".join(x for x in ca.get_supported_metadata() if ca.has_metadata(x))
md_item.setText(styles)
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)
if not fi.ca.is_writable():
if not ca.is_writable():
readonly_item.setCheckState(QtCore.Qt.CheckState.Checked)
readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
else:
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()):
item = self.twList.item(r, FileSelectionList.dataColNum)
if item.isSelected():
fi: FileInfo = item.data(QtCore.Qt.ItemDataRole.UserRole)
ca_list.append(fi.ca)
ca: ComicArchive = item.data(QtCore.Qt.ItemDataRole.UserRole)
ca_list.append(ca)
return ca_list

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

@ -24,6 +24,7 @@ import os
import signal
import subprocess
import sys
from collections.abc import Collection
from typing import cast
import settngs
@ -122,10 +123,14 @@ 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(
self, talkers: list[comictalker.ComicTalker], archivers: list[type[comicapi.comicarchive.Archiver]]
self,
talkers: Collection[comictalker.ComicTalker],
archivers: Collection[type[comicapi.comicarchive.Archiver]],
metadata_styles: Collection[comicapi.comicarchive.Metadata],
) -> None:
if self.config[0].Runtime_Options__json:
for talker in talkers:
@ -166,15 +171,31 @@ class App:
}
)
)
for style in metadata_styles:
print( # noqa: T201
json.dumps(
{
"type": "metadata",
"enabled": style.enabled,
"name": style.name(),
"short_name": style.short_name,
}
)
)
else:
print("Metadata Sources: (ID: Name URL)") # noqa: T201
print("Metadata Sources: (ID: Name, URL)") # noqa: T201
for talker in talkers:
print(f"{talker.id}: {talker.name} {talker.website}") # noqa: T201
print(f"{talker.id:<10}: {talker.name:<21}, {talker.website}") # noqa: T201
print("\nComic Archive: (Name: extension, exe)") # noqa: T201
for archiver in archivers:
a = archiver()
print(f"{a.name()}: {a.extension()}, {a.exe}") # noqa: T201
print(f"{a.name():<10}: {a.extension():<5}, {a.exe}") # noqa: T201
print("\nMetadata Style: (Short Name: Name)") # noqa: T201
for style in metadata_styles:
print(f"{style.short_name:<10}: {style.name()}") # noqa: T201
def initialize(self) -> argparse.Namespace:
conf, _ = self.initial_arg_parser.parse_known_intermixed_args()
@ -222,6 +243,10 @@ class App:
# config already loaded
error = None
if self.config[0].General__disable_cr:
if "cr" in comicapi.comicarchive.metadata_styles:
del comicapi.comicarchive.metadata_styles["cr"]
if len(self.talkers) < 1:
error = error = (
"Failed to load any talkers, please re-install and check the log located in '"
@ -240,7 +265,11 @@ class App:
update_publishers(self.config)
if self.config[0].Commands__command == Action.list_plugins:
self.list_plugins(list(self.talkers.values()), comicapi.comicarchive.archivers)
self.list_plugins(
list(self.talkers.values()),
comicapi.comicarchive.archivers,
comicapi.comicarchive.metadata_styles.values(),
)
return
if self.config[0].Commands__command == Action.save_config:

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,14 @@ class PageListEditor(QtWidgets.QWidget):
with (ui_path / "pagelisteditor.ui").open(encoding="utf-8") as uifile:
uic.loadUi(uifile, self)
self.md_attributes = {
"pages.image_index": [self.btnDown, self.btnUp],
"pages.type": self.cbPageType,
"pages.double_page": self.chkDoublePage,
"pages.bookmark": self.leBookmark,
"pages": self,
}
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode, None, None)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
@ -105,12 +114,14 @@ 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()
self.cbPageType.setDisabled(True)
self.chkDoublePage.setDisabled(True)
self.leBookmark.setDisabled(True)
self.cbPageType.setEnabled(False)
self.chkDoublePage.setEnabled(False)
self.leBookmark.setEnabled(False)
self.listWidget.clear()
self.comic_archive = None
self.pages_list = []
@ -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
@ -49,7 +48,7 @@ from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.crediteditorwindow import CreditEditorWindow
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.exportwindow import ExportConflictOpts, ExportWindow
from comictaggerlib.fileselectionlist import FileInfo, FileSelectionList
from comictaggerlib.fileselectionlist import FileSelectionList
from comictaggerlib.graphics import graphics_path
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.logwindow import LogWindow
@ -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()
@ -136,7 +191,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
grid_layout = QtWidgets.QGridLayout(self.widgetListHolder)
grid_layout.addWidget(self.fileSelectionList)
self.fileSelectionList.selectionChanged.connect(self.file_list_selection_changed)
self.fileSelectionList.selectionChanged.connect(self.load_archive)
self.fileSelectionList.listCleared.connect(self.file_list_cleared)
self.fileSelectionList.set_sorting(
self.config[0].internal__sort_column, QtCore.Qt.SortOrder(self.config[0].internal__sort_direction)
@ -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)
@ -617,16 +679,17 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.fileSelectionList.add_path_list(self.droppedFiles)
event.accept()
def actual_load_current_archive(self) -> None:
if self.metadata.is_empty and self.comic_archive is not None:
self.metadata = self.comic_archive.metadata_from_filename(
self.config[0].Filename_Parsing__complicated_parser,
self.config[0].Filename_Parsing__remove_c2c,
self.config[0].Filename_Parsing__remove_fcbd,
self.config[0].Filename_Parsing__remove_publisher,
)
if len(self.metadata.pages) == 0 and self.comic_archive is not None:
self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages())
def update_ui_for_archive(self, parse_filename: bool = True) -> None:
if self.comic_archive is not None:
if self.metadata.is_empty and parse_filename:
self.metadata = self.comic_archive.metadata_from_filename(
self.config[0].Filename_Parsing__complicated_parser,
self.config[0].Filename_Parsing__remove_c2c,
self.config[0].Filename_Parsing__remove_fcbd,
self.config[0].Filename_Parsing__remove_publisher,
)
self.metadata.apply_default_page_list(self.comic_archive.get_page_name_list())
self.update_cover_image()
@ -648,48 +711,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 +754,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)
@ -756,15 +796,13 @@ class TaggerWindow(QtWidgets.QMainWindow):
def clear_form(self) -> None:
# get a minty fresh metadata object
self.metadata = GenericMetadata()
if self.comic_archive is not None:
self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages())
self.page_list_editor.set_data(self.comic_archive, self.metadata.pages)
# recursively clear the tab form
self.clear_children(self.tabWidget)
# clear the dirty flag, since there is nothing in there now to lose
self.clear_dirty_flag()
self.update_ui_for_archive(parse_filename=False)
def clear_children(self, widget: QtCore.QObject) -> None:
if isinstance(widget, (QtWidgets.QLineEdit, QtWidgets.QTextEdit)):
@ -872,7 +910,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 +1149,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 +1159,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:
@ -1133,11 +1171,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.fileSelectionList.update_current_row()
self.metadata = self.comic_archive.read_metadata(self.load_data_style)
self.actual_load_current_archive()
self.update_ui_for_archive()
else:
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to commit!")
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 +1189,39 @@ 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()):
w = self.twCredits.item(r, 1)
supports_role = style.supports_credit_role(str(w.text()))
for credit in credit_attributes:
widget_enabled = credit[0] in enabled
widget = self.twCredits.item(r, credit[1])
if credit[0] == "credits.role":
widget_enabled = widget_enabled and supports_role
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 widget is not None and not isinstance(widget, (int)):
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 +1312,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 +1352,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 +1466,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 +1476,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 +1489,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 +1511,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 +1566,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 +1594,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 +1602,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 +1816,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 +1838,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 +2009,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 +2041,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()
@ -2131,9 +2066,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.fileSelectionList.update_selected_rows()
self.load_archive(self.comic_archive)
def file_list_selection_changed(self, fi: FileInfo) -> None:
self.load_archive(fi.ca)
def load_archive(self, comic_archive: ComicArchive) -> None:
self.comic_archive = None
self.clear_form()
@ -2153,7 +2085,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.exception(f"Failed to load metadata for {self.comic_archive.path}:\n\n{e}")
self.metadata = GenericMetadata()
self.actual_load_current_archive()
self.update_ui_for_archive()
def file_list_cleared(self) -> None:
self.reset_app()

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,67 @@ 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 is not None and active_palette is not None):
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 hasattr(widget, "setEnabled"):
widget.setEnabled(enable)
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,49 @@ 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()
md.set_default_page_list(tmp_comic.get_number_of_pages())
def test_save_cr(tmp_comic):
md = tmp_comic.read_metadata("cr")
md.apply_default_page_list(tmp_comic.get_page_name_list())
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.set_default_page_list(tmp_comic.get_number_of_pages())
md = tmp_comic.read_metadata("cr")
md.apply_default_page_list(tmp_comic.get_page_name_list())
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")
# This is a fake CBR we don't need to care about the pages for this test
md.pages = []
md_saved.pages = []
assert md == md_saved
@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support")
@ -80,46 +84,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 +135,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

@ -6,13 +6,13 @@ import comicapi.genericmetadata
from testing.comicdata import credits, metadata
def test_set_default_page_list(tmp_path):
def test_apply_default_page_list(tmp_path):
md = comicapi.genericmetadata.GenericMetadata()
md.overlay(comicapi.genericmetadata.md_test)
md.pages = []
md.set_default_page_list(len(comicapi.genericmetadata.md_test.pages))
md.apply_default_page_list(["testing"])
assert isinstance(md.pages[0]["Image"], 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,15 @@ 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
# The default page list is set on load if the comic has the requested metadata style
empty_md = comicapi.genericmetadata.GenericMetadata()
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,40 @@
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_saved, metadata):
md_style = metadata(mock_version[0])
supported_attributes = md_style.supported_attributes
md_style.set_metadata(comicapi.genericmetadata.md_test, tmp_comic.archiver)
written_metadata = md_style.get_metadata(tmp_comic.archiver)
md = md_saved.get_clean_metadata(*supported_attributes)
# Hack back in the pages variable because CoMet supports identifying the cover by the filename
if md_style.short_name == "comet":
md.pages = [
comicapi.genericmetadata.ImageMetadata(
image_index=0, filename="!cover.jpg", type=comicapi.genericmetadata.PageType.FrontCover
)
]
written_metadata = written_metadata.get_clean_metadata(*supported_attributes).replace(
pages=written_metadata.pages
)
else:
written_metadata = written_metadata.get_clean_metadata(*supported_attributes)
assert written_metadata == md