Add plugin support for metadata
This commit is contained in:
parent
04b3b6b4ab
commit
ae5e246180
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -25,14 +25,14 @@ from typing import cast
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.archivers import Archiver, UnknownArchiver, ZipArchiver
|
||||
from comicapi.comet import CoMet
|
||||
from comicapi.comicbookinfo import ComicBookInfo
|
||||
from comicapi.comicinfoxml import ComicInfoXml
|
||||
from comicapi.genericmetadata import GenericMetadata, PageType
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.metadata import Metadata
|
||||
from comictaggerlib.ctversion import version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
archivers: list[type[Archiver]] = []
|
||||
metadata_styles: dict[str, Metadata] = {}
|
||||
|
||||
|
||||
def load_archive_plugins() -> None:
|
||||
@ -54,12 +54,27 @@ def load_archive_plugins() -> None:
|
||||
archivers.extend(builtin)
|
||||
|
||||
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
CIX = 1
|
||||
COMET = 2
|
||||
name = ["ComicBookLover", "ComicRack", "CoMet"]
|
||||
short_name = ["cbl", "cr", "comet"]
|
||||
def load_metadata_plugins(version: str = f"ComicAPI/{version}") -> None:
|
||||
if not metadata_styles:
|
||||
if sys.version_info < (3, 10):
|
||||
from importlib_metadata import entry_points
|
||||
else:
|
||||
from importlib.metadata import entry_points
|
||||
builtin: dict[str, Metadata] = {}
|
||||
styles: dict[str, Metadata] = {}
|
||||
for arch in entry_points(group="comicapi.metadata"):
|
||||
try:
|
||||
style: type[Metadata] = arch.load()
|
||||
if style.enabled:
|
||||
if arch.module.startswith("comicapi"):
|
||||
builtin[style.short_name] = style(version)
|
||||
else:
|
||||
styles[style.short_name] = style(version)
|
||||
except Exception:
|
||||
logger.exception("Failed to load metadata plugin: %s", arch.name)
|
||||
metadata_styles.clear()
|
||||
metadata_styles.update(builtin)
|
||||
metadata_styles.update(styles)
|
||||
|
||||
|
||||
class ComicArchive:
|
||||
@ -67,25 +82,18 @@ class ComicArchive:
|
||||
pil_available = True
|
||||
|
||||
def __init__(self, path: pathlib.Path | str, default_image_path: pathlib.Path | str | None = None) -> None:
|
||||
self.cbi_md: GenericMetadata | None = None
|
||||
self.cix_md: GenericMetadata | None = None
|
||||
self.comet_filename: str | None = None
|
||||
self.comet_md: GenericMetadata | None = None
|
||||
self._has_cbi: bool | None = None
|
||||
self._has_cix: bool | None = None
|
||||
self._has_comet: bool | None = None
|
||||
self.md: dict[str, GenericMetadata] = {}
|
||||
self.path = pathlib.Path(path).absolute()
|
||||
self.page_count: int | None = None
|
||||
self.page_list: list[str] = []
|
||||
|
||||
self.ci_xml_filename = "ComicInfo.xml"
|
||||
self.comet_default_filename = "CoMet.xml"
|
||||
self.reset_cache()
|
||||
self.default_image_path = default_image_path
|
||||
|
||||
self.archiver: Archiver = UnknownArchiver.open(self.path)
|
||||
|
||||
load_archive_plugins()
|
||||
load_metadata_plugins()
|
||||
for archiver in archivers:
|
||||
if archiver.enabled and archiver.is_valid(self.path):
|
||||
self.archiver = archiver.open(self.path)
|
||||
@ -98,19 +106,19 @@ class ComicArchive:
|
||||
def reset_cache(self) -> None:
|
||||
"""Clears the cached data"""
|
||||
|
||||
self._has_cix = None
|
||||
self._has_cbi = None
|
||||
self._has_comet = None
|
||||
self.comet_filename = None
|
||||
self.page_count = None
|
||||
self.page_list = []
|
||||
self.cix_md = None
|
||||
self.cbi_md = None
|
||||
self.comet_md = None
|
||||
self.page_list.clear()
|
||||
self.md.clear()
|
||||
|
||||
def load_cache(self, style_list: list[int]) -> None:
|
||||
def load_cache(self, style_list: list[str]) -> None:
|
||||
for style in style_list:
|
||||
self.read_metadata(style)
|
||||
if style in metadata_styles:
|
||||
md = metadata_styles[style].get_metadata(self.archiver)
|
||||
if not md.is_empty:
|
||||
self.md[style] = md
|
||||
|
||||
def get_supported_metadata(self) -> list[str]:
|
||||
return [style[0] for style in metadata_styles.items() if style[1].supports_metadata(self.archiver)]
|
||||
|
||||
def rename(self, path: pathlib.Path | str) -> None:
|
||||
new_path = pathlib.Path(path).absolute()
|
||||
@ -133,8 +141,10 @@ class ComicArchive:
|
||||
|
||||
return True
|
||||
|
||||
def is_writable_for_style(self, data_style: int) -> bool:
|
||||
return not (data_style == MetaDataStyle.CBI and not self.archiver.supports_comment())
|
||||
def is_writable_for_style(self, style: str) -> bool:
|
||||
if style in metadata_styles:
|
||||
return self.archiver.is_writable() and metadata_styles[style].supports_metadata(self.archiver)
|
||||
return False
|
||||
|
||||
def is_zip(self) -> bool:
|
||||
return self.archiver.name() == "ZIP"
|
||||
@ -148,43 +158,28 @@ class ComicArchive:
|
||||
def extension(self) -> str:
|
||||
return self.archiver.extension()
|
||||
|
||||
def read_metadata(self, style: int) -> GenericMetadata:
|
||||
if style == MetaDataStyle.CIX:
|
||||
return self.read_cix()
|
||||
if style == MetaDataStyle.CBI:
|
||||
return self.read_cbi()
|
||||
if style == MetaDataStyle.COMET:
|
||||
return self.read_comet()
|
||||
return GenericMetadata()
|
||||
def read_metadata(self, style: str) -> GenericMetadata:
|
||||
if style in self.md:
|
||||
return self.md[style]
|
||||
return metadata_styles[style].get_metadata(self.archiver)
|
||||
|
||||
def write_metadata(self, metadata: GenericMetadata, style: int) -> bool:
|
||||
retcode = False
|
||||
if style == MetaDataStyle.CIX:
|
||||
retcode = self.write_cix(metadata)
|
||||
if style == MetaDataStyle.CBI:
|
||||
retcode = self.write_cbi(metadata)
|
||||
if style == MetaDataStyle.COMET:
|
||||
retcode = self.write_comet(metadata)
|
||||
return retcode
|
||||
def read_metadata_string(self, style: str) -> str:
|
||||
return metadata_styles[style].get_metadata_string(self.archiver)
|
||||
|
||||
def has_metadata(self, style: int) -> bool:
|
||||
if style == MetaDataStyle.CIX:
|
||||
return self.has_cix()
|
||||
if style == MetaDataStyle.CBI:
|
||||
return self.has_cbi()
|
||||
if style == MetaDataStyle.COMET:
|
||||
return self.has_comet()
|
||||
return False
|
||||
def write_metadata(self, metadata: GenericMetadata, style: str) -> bool:
|
||||
if style in self.md:
|
||||
del self.md[style]
|
||||
return metadata_styles[style].set_metadata(metadata, self.archiver)
|
||||
|
||||
def remove_metadata(self, style: int) -> bool:
|
||||
retcode = True
|
||||
if style == MetaDataStyle.CIX:
|
||||
retcode = self.remove_cix()
|
||||
elif style == MetaDataStyle.CBI:
|
||||
retcode = self.remove_cbi()
|
||||
elif style == MetaDataStyle.COMET:
|
||||
retcode = self.remove_co_met()
|
||||
return retcode
|
||||
def has_metadata(self, style: str) -> bool:
|
||||
if style in self.md:
|
||||
return True
|
||||
return metadata_styles[style].has_metadata(self.archiver)
|
||||
|
||||
def remove_metadata(self, style: str) -> bool:
|
||||
if style in self.md:
|
||||
del self.md[style]
|
||||
return metadata_styles[style].remove_metadata(self.archiver)
|
||||
|
||||
def get_page(self, index: int) -> bytes:
|
||||
image_data = b""
|
||||
@ -288,239 +283,12 @@ class ComicArchive:
|
||||
self.page_count = len(self.get_page_name_list())
|
||||
return self.page_count
|
||||
|
||||
def read_cbi(self) -> GenericMetadata:
|
||||
if self.cbi_md is None:
|
||||
raw_cbi = self.read_raw_cbi()
|
||||
if raw_cbi:
|
||||
self.cbi_md = ComicBookInfo().metadata_from_string(raw_cbi)
|
||||
else:
|
||||
self.cbi_md = GenericMetadata()
|
||||
|
||||
self.cbi_md.set_default_page_list(self.get_number_of_pages())
|
||||
|
||||
return self.cbi_md
|
||||
|
||||
def read_raw_cbi(self) -> str:
|
||||
if not self.has_cbi():
|
||||
return ""
|
||||
|
||||
return self.archiver.get_comment()
|
||||
|
||||
def has_cbi(self) -> bool:
|
||||
if self._has_cbi is None:
|
||||
if not self.seems_to_be_a_comic_archive():
|
||||
self._has_cbi = False
|
||||
else:
|
||||
comment = self.archiver.get_comment()
|
||||
self._has_cbi = ComicBookInfo().validate_string(comment)
|
||||
|
||||
return self._has_cbi
|
||||
|
||||
def write_cbi(self, metadata: GenericMetadata) -> bool:
|
||||
if metadata is not None:
|
||||
try:
|
||||
self.apply_archive_info_to_metadata(metadata)
|
||||
cbi_string = ComicBookInfo().string_from_metadata(metadata)
|
||||
write_success = self.archiver.set_comment(cbi_string)
|
||||
if write_success:
|
||||
self._has_cbi = True
|
||||
self.cbi_md = metadata
|
||||
self.reset_cache()
|
||||
return write_success
|
||||
except Exception as e:
|
||||
tb = traceback.extract_tb(e.__traceback__)
|
||||
logger.error("%s:%s: Error saving CBI! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e)
|
||||
|
||||
return False
|
||||
|
||||
def remove_cbi(self) -> bool:
|
||||
if self.has_cbi():
|
||||
write_success = self.archiver.set_comment("")
|
||||
if write_success:
|
||||
self._has_cbi = False
|
||||
self.cbi_md = None
|
||||
self.reset_cache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
def read_cix(self) -> GenericMetadata:
|
||||
if self.cix_md is None:
|
||||
raw_cix = self.read_raw_cix()
|
||||
if raw_cix:
|
||||
self.cix_md = ComicInfoXml().metadata_from_string(raw_cix)
|
||||
else:
|
||||
self.cix_md = GenericMetadata()
|
||||
|
||||
# validate the existing page list (make sure count is correct)
|
||||
if len(self.cix_md.pages) != 0:
|
||||
if len(self.cix_md.pages) != self.get_number_of_pages():
|
||||
# pages array doesn't match the actual number of images we're seeing
|
||||
# in the archive, so discard the data
|
||||
self.cix_md.pages = []
|
||||
|
||||
if len(self.cix_md.pages) == 0:
|
||||
self.cix_md.set_default_page_list(self.get_number_of_pages())
|
||||
|
||||
return self.cix_md
|
||||
|
||||
def read_raw_cix(self) -> bytes:
|
||||
if not self.has_cix():
|
||||
return b""
|
||||
try:
|
||||
raw_cix = self.archiver.read_file(self.ci_xml_filename) or b""
|
||||
except Exception as e:
|
||||
tb = traceback.extract_tb(e.__traceback__)
|
||||
logger.error("%s:%s: Error reading in raw CIX! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e)
|
||||
raw_cix = b""
|
||||
return raw_cix
|
||||
|
||||
def write_cix(self, metadata: GenericMetadata) -> bool:
|
||||
if metadata is not None:
|
||||
try:
|
||||
self.apply_archive_info_to_metadata(metadata, calc_page_sizes=True)
|
||||
raw_cix = self.read_raw_cix()
|
||||
cix_string = ComicInfoXml().string_from_metadata(metadata, xml=raw_cix)
|
||||
write_success = self.archiver.write_file(self.ci_xml_filename, cix_string.encode("utf-8"))
|
||||
if write_success:
|
||||
self._has_cix = True
|
||||
self.cix_md = metadata
|
||||
self.reset_cache()
|
||||
return write_success
|
||||
except Exception as e:
|
||||
tb = traceback.extract_tb(e.__traceback__)
|
||||
logger.error("%s:%s: Error saving CIX! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e)
|
||||
|
||||
return False
|
||||
|
||||
def remove_cix(self) -> bool:
|
||||
if self.has_cix():
|
||||
write_success = self.archiver.remove_file(self.ci_xml_filename)
|
||||
if write_success:
|
||||
self._has_cix = False
|
||||
self.cix_md = None
|
||||
self.reset_cache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
def has_cix(self) -> bool:
|
||||
if self._has_cix is None:
|
||||
if not self.seems_to_be_a_comic_archive():
|
||||
self._has_cix = False
|
||||
elif self.ci_xml_filename in self.archiver.get_filename_list():
|
||||
self._has_cix = True
|
||||
else:
|
||||
self._has_cix = False
|
||||
return self._has_cix
|
||||
|
||||
def read_comet(self) -> GenericMetadata:
|
||||
if self.comet_md is None:
|
||||
raw_comet = self.read_raw_comet()
|
||||
if raw_comet is None or raw_comet == "":
|
||||
self.comet_md = GenericMetadata()
|
||||
else:
|
||||
self.comet_md = CoMet().metadata_from_string(raw_comet)
|
||||
|
||||
self.comet_md.set_default_page_list(self.get_number_of_pages())
|
||||
# use the coverImage value from the comet_data to mark the cover in this struct
|
||||
# walk through list of images in file, and find the matching one for md.coverImage
|
||||
# need to remove the existing one in the default
|
||||
if self.comet_md._cover_image is not None:
|
||||
cover_idx = 0
|
||||
for idx, f in enumerate(self.get_page_name_list()):
|
||||
if self.comet_md._cover_image == f:
|
||||
cover_idx = idx
|
||||
break
|
||||
if cover_idx != 0:
|
||||
del self.comet_md.pages[0]["Type"]
|
||||
self.comet_md.pages[cover_idx]["Type"] = PageType.FrontCover
|
||||
|
||||
return self.comet_md
|
||||
|
||||
def read_raw_comet(self) -> str:
|
||||
raw_comet = ""
|
||||
if not self.has_comet():
|
||||
raw_comet = ""
|
||||
else:
|
||||
try:
|
||||
raw_bytes = self.archiver.read_file(cast(str, self.comet_filename))
|
||||
if raw_bytes:
|
||||
raw_comet = raw_bytes.decode("utf-8")
|
||||
except OSError as e:
|
||||
tb = traceback.extract_tb(e.__traceback__)
|
||||
logger.error(
|
||||
"%s:%s: Error reading in raw CoMet! for %s: %s", tb[1].filename, tb[1].lineno, self.path, e
|
||||
)
|
||||
return raw_comet
|
||||
|
||||
def write_comet(self, metadata: GenericMetadata) -> bool:
|
||||
if metadata is not None:
|
||||
if not self.has_comet():
|
||||
self.comet_filename = self.comet_default_filename
|
||||
|
||||
self.apply_archive_info_to_metadata(metadata)
|
||||
# Set the coverImage value, if it's not the first page
|
||||
cover_idx = int(metadata.get_cover_page_index_list()[0])
|
||||
if cover_idx != 0:
|
||||
metadata._cover_image = self.get_page_name(cover_idx)
|
||||
|
||||
comet_string = CoMet().string_from_metadata(metadata)
|
||||
write_success = self.archiver.write_file(cast(str, self.comet_filename), comet_string.encode("utf-8"))
|
||||
if write_success:
|
||||
self._has_comet = True
|
||||
self.comet_md = metadata
|
||||
self.reset_cache()
|
||||
return write_success
|
||||
|
||||
return False
|
||||
|
||||
def remove_co_met(self) -> bool:
|
||||
if self.has_comet():
|
||||
write_success = self.archiver.remove_file(cast(str, self.comet_filename))
|
||||
if write_success:
|
||||
self._has_comet = False
|
||||
self.comet_md = None
|
||||
self.reset_cache()
|
||||
return write_success
|
||||
return True
|
||||
|
||||
def has_comet(self) -> bool:
|
||||
if self._has_comet is None:
|
||||
self._has_comet = False
|
||||
if not self.seems_to_be_a_comic_archive():
|
||||
return self._has_comet
|
||||
|
||||
# look at all xml files in root, and search for CoMet data, get first
|
||||
for n in self.archiver.get_filename_list():
|
||||
if os.path.dirname(n) == "" and os.path.splitext(n)[1].casefold() == ".xml":
|
||||
# read in XML file, and validate it
|
||||
data = ""
|
||||
try:
|
||||
d = self.archiver.read_file(n)
|
||||
if d:
|
||||
data = d.decode("utf-8")
|
||||
except Exception as e:
|
||||
tb = traceback.extract_tb(e.__traceback__)
|
||||
logger.warning(
|
||||
"%s:%s: Error reading in Comet XML for validation! from %s: %s",
|
||||
tb[1].filename,
|
||||
tb[1].lineno,
|
||||
self.path,
|
||||
e,
|
||||
)
|
||||
if CoMet().validate_string(data):
|
||||
# since we found it, save it!
|
||||
self.comet_filename = n
|
||||
self._has_comet = True
|
||||
break
|
||||
|
||||
return self._has_comet
|
||||
|
||||
def apply_archive_info_to_metadata(self, md: GenericMetadata, calc_page_sizes: bool = False) -> None:
|
||||
md.page_count = self.get_number_of_pages()
|
||||
|
||||
if calc_page_sizes:
|
||||
for index, p in enumerate(md.pages):
|
||||
idx = int(p["Image"])
|
||||
idx = int(p["image_index"])
|
||||
if self.pil_available:
|
||||
try:
|
||||
from PIL import Image
|
||||
@ -528,7 +296,7 @@ class ComicArchive:
|
||||
self.pil_available = True
|
||||
except ImportError:
|
||||
self.pil_available = False
|
||||
if "ImageSize" not in p or "ImageHeight" not in p or "ImageWidth" not in p:
|
||||
if "size" not in p or "height" not in p or "width" not in p:
|
||||
data = self.get_page(idx)
|
||||
if data:
|
||||
try:
|
||||
@ -538,17 +306,17 @@ class ComicArchive:
|
||||
im = Image.open(io.StringIO(data))
|
||||
w, h = im.size
|
||||
|
||||
p["ImageSize"] = str(len(data))
|
||||
p["ImageHeight"] = str(h)
|
||||
p["ImageWidth"] = str(w)
|
||||
p["size"] = str(len(data))
|
||||
p["height"] = str(h)
|
||||
p["width"] = str(w)
|
||||
except Exception as e:
|
||||
logger.warning("Error decoding image [%s] %s :: image %s", e, self.path, index)
|
||||
p["ImageSize"] = str(len(data))
|
||||
p["size"] = str(len(data))
|
||||
|
||||
else:
|
||||
if "ImageSize" not in p:
|
||||
if "size" not in p:
|
||||
data = self.get_page(idx)
|
||||
p["ImageSize"] = str(len(data))
|
||||
p["size"] = str(len(data))
|
||||
|
||||
def metadata_from_filename(
|
||||
self,
|
||||
|
@ -54,13 +54,13 @@ class PageType:
|
||||
|
||||
|
||||
class ImageMetadata(TypedDict, total=False):
|
||||
Type: str
|
||||
Bookmark: str
|
||||
DoublePage: bool
|
||||
Image: int
|
||||
ImageSize: str
|
||||
ImageHeight: str
|
||||
ImageWidth: str
|
||||
type: str
|
||||
bookmark: str
|
||||
double_page: bool
|
||||
image_index: int
|
||||
size: str
|
||||
height: str
|
||||
width: str
|
||||
|
||||
|
||||
class Credit(TypedDict):
|
||||
@ -173,6 +173,33 @@ class GenericMetadata:
|
||||
tmp.__dict__.update(kwargs)
|
||||
return tmp
|
||||
|
||||
def get_clean_metadata(self, *attributes: str) -> GenericMetadata:
|
||||
new_md = GenericMetadata()
|
||||
for attr in sorted(attributes):
|
||||
if "." in attr:
|
||||
lst, _, name = attr.partition(".")
|
||||
old_value = getattr(self, lst)
|
||||
new_value = getattr(new_md, lst)
|
||||
if old_value:
|
||||
if not new_value:
|
||||
for x in old_value:
|
||||
new_value.append(x.__class__())
|
||||
for i, x in enumerate(old_value):
|
||||
if isinstance(x, dict):
|
||||
if name in x:
|
||||
new_value[i][name] = x[name]
|
||||
else:
|
||||
setattr(new_value[i], name, getattr(x, name))
|
||||
|
||||
else:
|
||||
old_value = getattr(self, attr)
|
||||
if isinstance(old_value, list):
|
||||
continue
|
||||
setattr(new_md, attr, old_value)
|
||||
|
||||
new_md.__post_init__()
|
||||
return new_md
|
||||
|
||||
def overlay(self, new_md: GenericMetadata) -> None:
|
||||
"""Overlay a metadata object on this one
|
||||
|
||||
@ -262,15 +289,15 @@ class GenericMetadata:
|
||||
def set_default_page_list(self, count: int) -> None:
|
||||
# generate a default page list, with the first page marked as the cover
|
||||
for i in range(count):
|
||||
page_dict = ImageMetadata(Image=i)
|
||||
page_dict = ImageMetadata(image_index=i)
|
||||
if i == 0:
|
||||
page_dict["Type"] = PageType.FrontCover
|
||||
page_dict["type"] = PageType.FrontCover
|
||||
self.pages.append(page_dict)
|
||||
|
||||
def get_archive_page_index(self, pagenum: int) -> int:
|
||||
# convert the displayed page number to the page index of the file in the archive
|
||||
if pagenum < len(self.pages):
|
||||
return int(self.pages[pagenum]["Image"])
|
||||
return int(self.pages[pagenum]["image_index"])
|
||||
|
||||
return 0
|
||||
|
||||
@ -278,8 +305,8 @@ class GenericMetadata:
|
||||
# return a list of archive page indices of cover pages
|
||||
coverlist = []
|
||||
for p in self.pages:
|
||||
if "Type" in p and p["Type"] == PageType.FrontCover:
|
||||
coverlist.append(int(p["Image"]))
|
||||
if "type" in p and p["type"] == PageType.FrontCover:
|
||||
coverlist.append(int(p["image_index"]))
|
||||
|
||||
if len(coverlist) == 0:
|
||||
coverlist.append(0)
|
||||
@ -459,36 +486,36 @@ md_test: GenericMetadata = GenericMetadata(
|
||||
],
|
||||
tags=set(),
|
||||
pages=[
|
||||
ImageMetadata(Image=0, ImageHeight="1280", ImageSize="195977", ImageWidth="800", Type=PageType.FrontCover),
|
||||
ImageMetadata(Image=1, ImageHeight="2039", ImageSize="611993", ImageWidth="1327"),
|
||||
ImageMetadata(Image=2, ImageHeight="2039", ImageSize="783726", ImageWidth="1327"),
|
||||
ImageMetadata(Image=3, ImageHeight="2039", ImageSize="679584", ImageWidth="1327"),
|
||||
ImageMetadata(Image=4, ImageHeight="2039", ImageSize="788179", ImageWidth="1327"),
|
||||
ImageMetadata(Image=5, ImageHeight="2039", ImageSize="864433", ImageWidth="1327"),
|
||||
ImageMetadata(Image=6, ImageHeight="2039", ImageSize="765606", ImageWidth="1327"),
|
||||
ImageMetadata(Image=7, ImageHeight="2039", ImageSize="876427", ImageWidth="1327"),
|
||||
ImageMetadata(Image=8, ImageHeight="2039", ImageSize="852622", ImageWidth="1327"),
|
||||
ImageMetadata(Image=9, ImageHeight="2039", ImageSize="800205", ImageWidth="1327"),
|
||||
ImageMetadata(Image=10, ImageHeight="2039", ImageSize="746243", ImageWidth="1326"),
|
||||
ImageMetadata(Image=11, ImageHeight="2039", ImageSize="718062", ImageWidth="1327"),
|
||||
ImageMetadata(Image=12, ImageHeight="2039", ImageSize="532179", ImageWidth="1326"),
|
||||
ImageMetadata(Image=13, ImageHeight="2039", ImageSize="686708", ImageWidth="1327"),
|
||||
ImageMetadata(Image=14, ImageHeight="2039", ImageSize="641907", ImageWidth="1327"),
|
||||
ImageMetadata(Image=15, ImageHeight="2039", ImageSize="805388", ImageWidth="1327"),
|
||||
ImageMetadata(Image=16, ImageHeight="2039", ImageSize="668927", ImageWidth="1326"),
|
||||
ImageMetadata(Image=17, ImageHeight="2039", ImageSize="710605", ImageWidth="1327"),
|
||||
ImageMetadata(Image=18, ImageHeight="2039", ImageSize="761398", ImageWidth="1326"),
|
||||
ImageMetadata(Image=19, ImageHeight="2039", ImageSize="743807", ImageWidth="1327"),
|
||||
ImageMetadata(Image=20, ImageHeight="2039", ImageSize="552911", ImageWidth="1326"),
|
||||
ImageMetadata(Image=21, ImageHeight="2039", ImageSize="556827", ImageWidth="1327"),
|
||||
ImageMetadata(Image=22, ImageHeight="2039", ImageSize="675078", ImageWidth="1326"),
|
||||
ImageMetadata(image_index=0, height="1280", size="195977", width="800", type=PageType.FrontCover),
|
||||
ImageMetadata(image_index=1, height="2039", size="611993", width="1327"),
|
||||
ImageMetadata(image_index=2, height="2039", size="783726", width="1327"),
|
||||
ImageMetadata(image_index=3, height="2039", size="679584", width="1327"),
|
||||
ImageMetadata(image_index=4, height="2039", size="788179", width="1327"),
|
||||
ImageMetadata(image_index=5, height="2039", size="864433", width="1327"),
|
||||
ImageMetadata(image_index=6, height="2039", size="765606", width="1327"),
|
||||
ImageMetadata(image_index=7, height="2039", size="876427", width="1327"),
|
||||
ImageMetadata(image_index=8, height="2039", size="852622", width="1327"),
|
||||
ImageMetadata(image_index=9, height="2039", size="800205", width="1327"),
|
||||
ImageMetadata(image_index=10, height="2039", size="746243", width="1326"),
|
||||
ImageMetadata(image_index=11, height="2039", size="718062", width="1327"),
|
||||
ImageMetadata(image_index=12, height="2039", size="532179", width="1326"),
|
||||
ImageMetadata(image_index=13, height="2039", size="686708", width="1327"),
|
||||
ImageMetadata(image_index=14, height="2039", size="641907", width="1327"),
|
||||
ImageMetadata(image_index=15, height="2039", size="805388", width="1327"),
|
||||
ImageMetadata(image_index=16, height="2039", size="668927", width="1326"),
|
||||
ImageMetadata(image_index=17, height="2039", size="710605", width="1327"),
|
||||
ImageMetadata(image_index=18, height="2039", size="761398", width="1326"),
|
||||
ImageMetadata(image_index=19, height="2039", size="743807", width="1327"),
|
||||
ImageMetadata(image_index=20, height="2039", size="552911", width="1326"),
|
||||
ImageMetadata(image_index=21, height="2039", size="556827", width="1327"),
|
||||
ImageMetadata(image_index=22, height="2039", size="675078", width="1326"),
|
||||
ImageMetadata(
|
||||
Bookmark="Interview",
|
||||
Image=23,
|
||||
ImageHeight="2032",
|
||||
ImageSize="800965",
|
||||
ImageWidth="1338",
|
||||
Type=PageType.Letters,
|
||||
bookmark="Interview",
|
||||
image_index=23,
|
||||
height="2032",
|
||||
size="800965",
|
||||
width="1338",
|
||||
type=PageType.Letters,
|
||||
),
|
||||
],
|
||||
price=None,
|
||||
|
5
comicapi/metadata/__init__.py
Normal file
5
comicapi/metadata/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from comicapi.metadata.metadata import Metadata
|
||||
|
||||
__all__ = ["Metadata"]
|
@ -16,33 +16,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Any
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.archivers import Archiver
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.metadata import Metadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CoMet:
|
||||
writer_synonyms = ["writer", "plotter", "scripter"]
|
||||
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
|
||||
inker_synonyms = ["inker", "artist", "finishes"]
|
||||
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
|
||||
letterer_synonyms = ["letterer"]
|
||||
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
|
||||
editor_synonyms = ["editor"]
|
||||
class CoMet(Metadata):
|
||||
_writer_synonyms = ("writer", "plotter", "scripter")
|
||||
_penciller_synonyms = ("artist", "penciller", "penciler", "breakdowns")
|
||||
_inker_synonyms = ("inker", "artist", "finishes")
|
||||
_colorist_synonyms = ("colorist", "colourist", "colorer", "colourer")
|
||||
_letterer_synonyms = ("letterer",)
|
||||
_cover_synonyms = ("cover", "covers", "coverartist", "cover artist")
|
||||
_editor_synonyms = ("editor",)
|
||||
|
||||
def metadata_from_string(self, string: str) -> GenericMetadata:
|
||||
enabled = True
|
||||
|
||||
short_name = "comet"
|
||||
|
||||
def __init__(self, version: str) -> None:
|
||||
super().__init__(version)
|
||||
|
||||
self.comet_filename = "CoMet.xml"
|
||||
self.file = "CoMet.xml"
|
||||
self.supported_attributes = {
|
||||
"characters",
|
||||
"description",
|
||||
"credits",
|
||||
"credits.person",
|
||||
"credits.primary",
|
||||
"credits.role",
|
||||
"format",
|
||||
"genres",
|
||||
"identifier",
|
||||
"is_version_of",
|
||||
"issue",
|
||||
"language",
|
||||
"last_mark",
|
||||
"maturity_rating",
|
||||
"month",
|
||||
"page_count",
|
||||
"price",
|
||||
"publisher",
|
||||
"rights",
|
||||
"series",
|
||||
"title",
|
||||
"volume",
|
||||
"year",
|
||||
}
|
||||
|
||||
def supports_metadata(self, archive: Archiver) -> bool:
|
||||
return archive.supports_files()
|
||||
|
||||
def has_metadata(self, archive: Archiver) -> bool:
|
||||
if not self.supports_metadata(archive):
|
||||
return False
|
||||
has_metadata = False
|
||||
# look at all xml files in root, and search for CoMet data, get first
|
||||
for n in archive.get_filename_list():
|
||||
if os.path.dirname(n) == "" and os.path.splitext(n)[1].casefold() == ".xml":
|
||||
# read in XML file, and validate it
|
||||
data = b""
|
||||
try:
|
||||
data = archive.read_file(n)
|
||||
except Exception as e:
|
||||
logger.warning("Error reading in Comet XML for validation! from %s: %s", archive.path, e)
|
||||
if self._validate_bytes(data):
|
||||
# since we found it, save it!
|
||||
self.file = n
|
||||
has_metadata = True
|
||||
break
|
||||
return has_metadata
|
||||
|
||||
def remove_metadata(self, archive: Archiver) -> bool:
|
||||
return self.has_metadata(archive) and archive.remove_file(self.file)
|
||||
|
||||
def get_metadata(self, archive: Archiver) -> GenericMetadata:
|
||||
if self.has_metadata(archive):
|
||||
metadata = archive.read_file(self.file) or b""
|
||||
if self._validate_bytes(metadata):
|
||||
return self._metadata_from_bytes(metadata)
|
||||
return GenericMetadata()
|
||||
|
||||
def get_metadata_string(self, archive: Archiver) -> str:
|
||||
if self.has_metadata(archive):
|
||||
return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding="unicode", xml_declaration=True)
|
||||
return ""
|
||||
|
||||
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
|
||||
if self.supports_metadata(archive):
|
||||
success = True
|
||||
if self.file != self.comet_filename:
|
||||
success = self.remove_metadata(archive)
|
||||
return archive.write_file(self.comet_filename, self._bytes_from_metadata(metadata)) and success
|
||||
else:
|
||||
logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata")
|
||||
return False
|
||||
|
||||
def name(self) -> str:
|
||||
return "Comic Metadata (CoMet)"
|
||||
|
||||
def _metadata_from_bytes(self, string: bytes) -> GenericMetadata:
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
return self.convert_xml_to_metadata(tree)
|
||||
return self._convert_xml_to_metadata(tree)
|
||||
|
||||
def string_from_metadata(self, metadata: GenericMetadata) -> str:
|
||||
tree = self.convert_metadata_to_xml(metadata)
|
||||
return str(ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True).decode("utf-8"))
|
||||
def _bytes_from_metadata(self, metadata: GenericMetadata) -> bytes:
|
||||
tree = self._convert_metadata_to_xml(metadata)
|
||||
return ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True)
|
||||
|
||||
def convert_metadata_to_xml(self, metadata: GenericMetadata) -> ET.ElementTree:
|
||||
def _convert_metadata_to_xml(self, metadata: GenericMetadata) -> ET.ElementTree:
|
||||
# shorthand for the metadata
|
||||
md = metadata
|
||||
|
||||
@ -95,25 +184,25 @@ class CoMet:
|
||||
|
||||
# loop thru credits, and build a list for each role that CoMet supports
|
||||
for credit in metadata.credits:
|
||||
if credit["role"].casefold() in set(self.writer_synonyms):
|
||||
if credit["role"].casefold() in set(self._writer_synonyms):
|
||||
ET.SubElement(root, "writer").text = str(credit["person"])
|
||||
|
||||
if credit["role"].casefold() in set(self.penciller_synonyms):
|
||||
if credit["role"].casefold() in set(self._penciller_synonyms):
|
||||
ET.SubElement(root, "penciller").text = str(credit["person"])
|
||||
|
||||
if credit["role"].casefold() in set(self.inker_synonyms):
|
||||
if credit["role"].casefold() in set(self._inker_synonyms):
|
||||
ET.SubElement(root, "inker").text = str(credit["person"])
|
||||
|
||||
if credit["role"].casefold() in set(self.colorist_synonyms):
|
||||
if credit["role"].casefold() in set(self._colorist_synonyms):
|
||||
ET.SubElement(root, "colorist").text = str(credit["person"])
|
||||
|
||||
if credit["role"].casefold() in set(self.letterer_synonyms):
|
||||
if credit["role"].casefold() in set(self._letterer_synonyms):
|
||||
ET.SubElement(root, "letterer").text = str(credit["person"])
|
||||
|
||||
if credit["role"].casefold() in set(self.cover_synonyms):
|
||||
if credit["role"].casefold() in set(self._cover_synonyms):
|
||||
ET.SubElement(root, "coverDesigner").text = str(credit["person"])
|
||||
|
||||
if credit["role"].casefold() in set(self.editor_synonyms):
|
||||
if credit["role"].casefold() in set(self._editor_synonyms):
|
||||
ET.SubElement(root, "editor").text = str(credit["person"])
|
||||
|
||||
ET.indent(root)
|
||||
@ -122,7 +211,7 @@ class CoMet:
|
||||
tree = ET.ElementTree(root)
|
||||
return tree
|
||||
|
||||
def convert_xml_to_metadata(self, tree: ET.ElementTree) -> GenericMetadata:
|
||||
def _convert_xml_to_metadata(self, tree: ET.ElementTree) -> GenericMetadata:
|
||||
root = tree.getroot()
|
||||
|
||||
if root.tag != "comet":
|
||||
@ -194,7 +283,7 @@ class CoMet:
|
||||
return metadata
|
||||
|
||||
# verify that the string actually contains CoMet data in XML format
|
||||
def validate_string(self, string: str) -> bool:
|
||||
def _validate_bytes(self, string: bytes) -> bool:
|
||||
try:
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
root = tree.getroot()
|
||||
@ -204,11 +293,3 @@ class CoMet:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def write_to_external_file(self, filename: str, metadata: GenericMetadata) -> None:
|
||||
tree = self.convert_metadata_to_xml(metadata)
|
||||
tree.write(filename, encoding="utf-8")
|
||||
|
||||
def read_from_external_file(self, filename: str) -> GenericMetadata:
|
||||
tree = ET.parse(filename)
|
||||
return self.convert_xml_to_metadata(tree)
|
@ -20,11 +20,13 @@ from datetime import datetime
|
||||
from typing import Any, Literal, TypedDict
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.archivers import Archiver
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.metadata import Metadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CBILiteralType = Literal[
|
||||
_CBILiteralType = Literal[
|
||||
"series",
|
||||
"title",
|
||||
"issue",
|
||||
@ -44,13 +46,13 @@ CBILiteralType = Literal[
|
||||
]
|
||||
|
||||
|
||||
class Credits(TypedDict):
|
||||
class _Credits(TypedDict):
|
||||
person: str
|
||||
role: str
|
||||
primary: bool
|
||||
|
||||
|
||||
class ComicBookInfoJson(TypedDict, total=False):
|
||||
class _ComicBookInfoJson(TypedDict, total=False):
|
||||
series: str
|
||||
title: str
|
||||
publisher: str
|
||||
@ -64,17 +66,77 @@ class ComicBookInfoJson(TypedDict, total=False):
|
||||
genre: str
|
||||
language: str
|
||||
country: str
|
||||
credits: list[Credits]
|
||||
credits: list[_Credits]
|
||||
tags: list[str]
|
||||
comments: str
|
||||
|
||||
|
||||
CBIContainer = TypedDict("CBIContainer", {"appID": str, "lastModified": str, "ComicBookInfo/1.0": ComicBookInfoJson})
|
||||
_CBIContainer = TypedDict("_CBIContainer", {"appID": str, "lastModified": str, "ComicBookInfo/1.0": _ComicBookInfoJson})
|
||||
|
||||
|
||||
class ComicBookInfo:
|
||||
def metadata_from_string(self, string: str) -> GenericMetadata:
|
||||
cbi_container: CBIContainer = json.loads(string)
|
||||
class ComicBookInfo(Metadata):
|
||||
enabled = True
|
||||
|
||||
short_name = "cbi"
|
||||
|
||||
def __init__(self, version: str) -> None:
|
||||
super().__init__(version)
|
||||
|
||||
self.supported_attributes = {
|
||||
"description",
|
||||
"country",
|
||||
"credits",
|
||||
"credits.person",
|
||||
"credits.primary",
|
||||
"credits.role",
|
||||
"critical_rating",
|
||||
"genres",
|
||||
"issue",
|
||||
"issue_count",
|
||||
"language",
|
||||
"month",
|
||||
"publisher",
|
||||
"series",
|
||||
"tags",
|
||||
"title",
|
||||
"volume",
|
||||
"volume_count",
|
||||
"year",
|
||||
}
|
||||
|
||||
def supports_metadata(self, archive: Archiver) -> bool:
|
||||
return archive.supports_comment()
|
||||
|
||||
def has_metadata(self, archive: Archiver) -> bool:
|
||||
return self.supports_metadata(archive) and self._validate_string(archive.get_comment())
|
||||
|
||||
def remove_metadata(self, archive: Archiver) -> bool:
|
||||
return archive.set_comment("")
|
||||
|
||||
def get_metadata(self, archive: Archiver) -> GenericMetadata:
|
||||
if self.has_metadata(archive):
|
||||
comment = archive.get_comment()
|
||||
if self._validate_string(comment):
|
||||
return self._metadata_from_string(comment)
|
||||
return GenericMetadata()
|
||||
|
||||
def get_metadata_string(self, archive: Archiver) -> str:
|
||||
if self.has_metadata(archive):
|
||||
return json.dumps(json.loads(archive.get_comment()), indent=2)
|
||||
return ""
|
||||
|
||||
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
|
||||
if self.supports_metadata(archive):
|
||||
return archive.set_comment(self._string_from_metadata(metadata))
|
||||
else:
|
||||
logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata")
|
||||
return False
|
||||
|
||||
def name(self) -> str:
|
||||
return "ComicBookInfo"
|
||||
|
||||
def _metadata_from_string(self, string: str) -> GenericMetadata:
|
||||
cbi_container: _CBIContainer = json.loads(string)
|
||||
|
||||
metadata = GenericMetadata()
|
||||
|
||||
@ -96,7 +158,7 @@ class ComicBookInfo:
|
||||
metadata.critical_rating = utils.xlate_int(cbi.get("rating"))
|
||||
|
||||
metadata.credits = [
|
||||
Credits(
|
||||
_Credits(
|
||||
person=x["person"] if "person" in x else "",
|
||||
role=x["role"] if "role" in x else "",
|
||||
primary=x["primary"] if "primary" in x else False,
|
||||
@ -113,11 +175,11 @@ class ComicBookInfo:
|
||||
|
||||
return metadata
|
||||
|
||||
def string_from_metadata(self, metadata: GenericMetadata) -> str:
|
||||
cbi_container = self.create_json_dictionary(metadata)
|
||||
def _string_from_metadata(self, metadata: GenericMetadata) -> str:
|
||||
cbi_container = self._create_json_dictionary(metadata)
|
||||
return json.dumps(cbi_container)
|
||||
|
||||
def validate_string(self, string: bytes | str) -> bool:
|
||||
def _validate_string(self, string: bytes | str) -> bool:
|
||||
"""Verify that the string actually contains CBI data in JSON format"""
|
||||
|
||||
try:
|
||||
@ -127,10 +189,10 @@ class ComicBookInfo:
|
||||
|
||||
return "ComicBookInfo/1.0" in cbi_container
|
||||
|
||||
def create_json_dictionary(self, metadata: GenericMetadata) -> CBIContainer:
|
||||
def _create_json_dictionary(self, metadata: GenericMetadata) -> _CBIContainer:
|
||||
"""Create the dictionary that we will convert to JSON text"""
|
||||
|
||||
cbi_container = CBIContainer(
|
||||
cbi_container = _CBIContainer(
|
||||
{
|
||||
"appID": "ComicTagger/1.0.0",
|
||||
"lastModified": str(datetime.now()),
|
||||
@ -139,7 +201,7 @@ class ComicBookInfo:
|
||||
) # TODO: ctversion.version,
|
||||
|
||||
# helper func
|
||||
def assign(cbi_entry: CBILiteralType, md_entry: Any) -> None:
|
||||
def assign(cbi_entry: _CBILiteralType, md_entry: Any) -> None:
|
||||
if md_entry is not None or isinstance(md_entry, str) and md_entry != "":
|
||||
cbi_container["ComicBookInfo/1.0"][cbi_entry] = md_entry
|
||||
|
||||
@ -161,9 +223,3 @@ class ComicBookInfo:
|
||||
assign("tags", list(metadata.tags))
|
||||
|
||||
return cbi_container
|
||||
|
||||
def write_to_external_file(self, filename: str, metadata: GenericMetadata) -> None:
|
||||
cbi_container = self.create_json_dictionary(metadata)
|
||||
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(cbi_container, indent=4))
|
@ -18,49 +18,125 @@ import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import OrderedDict
|
||||
from typing import Any, cast
|
||||
from xml.etree.ElementTree import ElementTree
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.archivers import Archiver
|
||||
from comicapi.genericmetadata import GenericMetadata, ImageMetadata
|
||||
from comicapi.metadata import Metadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComicInfoXml:
|
||||
writer_synonyms = ["writer", "plotter", "scripter"]
|
||||
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
|
||||
inker_synonyms = ["inker", "artist", "finishes"]
|
||||
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
|
||||
letterer_synonyms = ["letterer"]
|
||||
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
|
||||
editor_synonyms = ["editor"]
|
||||
class ComicRack(Metadata):
|
||||
_writer_synonyms = ("writer", "plotter", "scripter")
|
||||
_penciller_synonyms = ("artist", "penciller", "penciler", "breakdowns")
|
||||
_inker_synonyms = ("inker", "artist", "finishes")
|
||||
_colorist_synonyms = ("colorist", "colourist", "colorer", "colourer")
|
||||
_letterer_synonyms = ("letterer",)
|
||||
_cover_synonyms = ("cover", "covers", "coverartist", "cover artist")
|
||||
_editor_synonyms = ("editor",)
|
||||
|
||||
def get_parseable_credits(self) -> list[str]:
|
||||
parsable_credits = []
|
||||
parsable_credits.extend(self.writer_synonyms)
|
||||
parsable_credits.extend(self.penciller_synonyms)
|
||||
parsable_credits.extend(self.inker_synonyms)
|
||||
parsable_credits.extend(self.colorist_synonyms)
|
||||
parsable_credits.extend(self.letterer_synonyms)
|
||||
parsable_credits.extend(self.cover_synonyms)
|
||||
parsable_credits.extend(self.editor_synonyms)
|
||||
return parsable_credits
|
||||
enabled = True
|
||||
|
||||
def metadata_from_string(self, string: bytes) -> GenericMetadata:
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
return self.convert_xml_to_metadata(tree)
|
||||
short_name = "cr"
|
||||
|
||||
def string_from_metadata(self, metadata: GenericMetadata, xml: bytes = b"") -> str:
|
||||
tree = self.convert_metadata_to_xml(metadata, xml)
|
||||
tree_str = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True).decode("utf-8")
|
||||
return str(tree_str)
|
||||
def __init__(self, version: str) -> None:
|
||||
super().__init__(version)
|
||||
|
||||
def convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b"") -> ElementTree:
|
||||
self.file = "ComicInfo.xml"
|
||||
self.supported_attributes = {
|
||||
"alternate_count",
|
||||
"alternate_number",
|
||||
"alternate_series",
|
||||
"black_and_white",
|
||||
"characters",
|
||||
"description",
|
||||
"credits",
|
||||
"credits.person",
|
||||
"credits.role",
|
||||
"critical_rating",
|
||||
"day",
|
||||
"format",
|
||||
"genres",
|
||||
"imprint",
|
||||
"issue",
|
||||
"issue_count",
|
||||
"language",
|
||||
"locations",
|
||||
"manga",
|
||||
"maturity_rating",
|
||||
"month",
|
||||
"notes",
|
||||
"page_count",
|
||||
"pages",
|
||||
"pages.bookmark",
|
||||
"pages.double_page",
|
||||
"pages.height",
|
||||
"pages.image_index",
|
||||
"pages.size",
|
||||
"pages.type",
|
||||
"pages.width",
|
||||
"publisher",
|
||||
"scan_info",
|
||||
"series",
|
||||
"series_groups",
|
||||
"story_arcs",
|
||||
"teams",
|
||||
"title",
|
||||
"volume",
|
||||
"web_link",
|
||||
"year",
|
||||
}
|
||||
|
||||
def supports_metadata(self, archive: Archiver) -> bool:
|
||||
return True
|
||||
|
||||
def has_metadata(self, archive: Archiver) -> bool:
|
||||
return (
|
||||
self.supports_metadata(archive)
|
||||
and self.file in archive.get_filename_list()
|
||||
and self._validate_bytes(archive.read_file(self.file))
|
||||
)
|
||||
|
||||
def remove_metadata(self, archive: Archiver) -> bool:
|
||||
return self.has_metadata(archive) and archive.remove_file(self.file)
|
||||
|
||||
def get_metadata(self, archive: Archiver) -> GenericMetadata:
|
||||
if self.has_metadata(archive):
|
||||
metadata = archive.read_file(self.file) or b""
|
||||
if self._validate_bytes(metadata):
|
||||
return self._metadata_from_bytes(metadata)
|
||||
return GenericMetadata()
|
||||
|
||||
def get_metadata_string(self, archive: Archiver) -> str:
|
||||
if self.has_metadata(archive):
|
||||
return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding="unicode", xml_declaration=True)
|
||||
return ""
|
||||
|
||||
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
|
||||
if self.supports_metadata(archive):
|
||||
return archive.write_file(self.file, self._bytes_from_metadata(metadata))
|
||||
else:
|
||||
logger.warning(f"Archive ({archive.name()}) does not support {self.name()} metadata")
|
||||
return False
|
||||
|
||||
def name(self) -> str:
|
||||
return "Comic Rack"
|
||||
|
||||
def _metadata_from_bytes(self, string: bytes) -> GenericMetadata:
|
||||
root = ET.fromstring(string)
|
||||
return self._convert_xml_to_metadata(root)
|
||||
|
||||
def _bytes_from_metadata(self, metadata: GenericMetadata, xml: bytes = b"") -> bytes:
|
||||
root = self._convert_metadata_to_xml(metadata, xml)
|
||||
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
def _convert_metadata_to_xml(self, metadata: GenericMetadata, xml: bytes = b"") -> ET.Element:
|
||||
# shorthand for the metadata
|
||||
md = metadata
|
||||
|
||||
if xml:
|
||||
root = ET.ElementTree(ET.fromstring(xml)).getroot()
|
||||
root = ET.fromstring(xml)
|
||||
else:
|
||||
# build a tree structure
|
||||
root = ET.Element("ComicInfo")
|
||||
@ -68,7 +144,7 @@ class ComicInfoXml:
|
||||
root.attrib["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema"
|
||||
# helper func
|
||||
|
||||
def assign(cix_entry: str, md_entry: Any) -> None:
|
||||
def assign(cr_entry: str, md_entry: Any) -> None:
|
||||
if md_entry:
|
||||
text = ""
|
||||
if isinstance(md_entry, str):
|
||||
@ -77,13 +153,13 @@ class ComicInfoXml:
|
||||
text = ",".join(md_entry)
|
||||
else:
|
||||
text = str(md_entry)
|
||||
et_entry = root.find(cix_entry)
|
||||
et_entry = root.find(cr_entry)
|
||||
if et_entry is not None:
|
||||
et_entry.text = text
|
||||
else:
|
||||
ET.SubElement(root, cix_entry).text = text
|
||||
ET.SubElement(root, cr_entry).text = text
|
||||
else:
|
||||
et_entry = root.find(cix_entry)
|
||||
et_entry = root.find(cr_entry)
|
||||
if et_entry is not None:
|
||||
root.remove(et_entry)
|
||||
|
||||
@ -116,25 +192,25 @@ class ComicInfoXml:
|
||||
# first, loop thru credits, and build a list for each role that CIX
|
||||
# supports
|
||||
for credit in metadata.credits:
|
||||
if credit["role"].casefold() in set(self.writer_synonyms):
|
||||
if credit["role"].casefold() in set(self._writer_synonyms):
|
||||
credit_writer_list.append(credit["person"].replace(",", ""))
|
||||
|
||||
if credit["role"].casefold() in set(self.penciller_synonyms):
|
||||
if credit["role"].casefold() in set(self._penciller_synonyms):
|
||||
credit_penciller_list.append(credit["person"].replace(",", ""))
|
||||
|
||||
if credit["role"].casefold() in set(self.inker_synonyms):
|
||||
if credit["role"].casefold() in set(self._inker_synonyms):
|
||||
credit_inker_list.append(credit["person"].replace(",", ""))
|
||||
|
||||
if credit["role"].casefold() in set(self.colorist_synonyms):
|
||||
if credit["role"].casefold() in set(self._colorist_synonyms):
|
||||
credit_colorist_list.append(credit["person"].replace(",", ""))
|
||||
|
||||
if credit["role"].casefold() in set(self.letterer_synonyms):
|
||||
if credit["role"].casefold() in set(self._letterer_synonyms):
|
||||
credit_letterer_list.append(credit["person"].replace(",", ""))
|
||||
|
||||
if credit["role"].casefold() in set(self.cover_synonyms):
|
||||
if credit["role"].casefold() in set(self._cover_synonyms):
|
||||
credit_cover_list.append(credit["person"].replace(",", ""))
|
||||
|
||||
if credit["role"].casefold() in set(self.editor_synonyms):
|
||||
if credit["role"].casefold() in set(self._editor_synonyms):
|
||||
credit_editor_list.append(credit["person"].replace(",", ""))
|
||||
|
||||
# second, convert each list to string, and add to XML struct
|
||||
@ -171,17 +247,28 @@ class ComicInfoXml:
|
||||
|
||||
for page_dict in md.pages:
|
||||
page_node = ET.SubElement(pages_node, "Page")
|
||||
page_node.attrib = OrderedDict(sorted((k, str(v)) for k, v in page_dict.items()))
|
||||
page_node.attrib = {}
|
||||
if "bookmark" in page_dict:
|
||||
page_node.attrib["Bookmark"] = str(page_dict["bookmark"])
|
||||
if "double_page" in page_dict:
|
||||
page_node.attrib["DoublePage"] = str(page_dict["double_page"])
|
||||
if "image_index" in page_dict:
|
||||
page_node.attrib["Image"] = str(page_dict["image_index"])
|
||||
if "height" in page_dict:
|
||||
page_node.attrib["ImageHeight"] = str(page_dict["height"])
|
||||
if "size" in page_dict:
|
||||
page_node.attrib["ImageSize"] = str(page_dict["size"])
|
||||
if "width" in page_dict:
|
||||
page_node.attrib["ImageWidth"] = str(page_dict["width"])
|
||||
if "type" in page_dict:
|
||||
page_node.attrib["Type"] = str(page_dict["type"])
|
||||
page_node.attrib = OrderedDict(sorted(page_node.attrib.items()))
|
||||
|
||||
ET.indent(root)
|
||||
|
||||
# wrap it in an ElementTree instance, and save as XML
|
||||
tree = ET.ElementTree(root)
|
||||
return tree
|
||||
|
||||
def convert_xml_to_metadata(self, tree: ElementTree) -> GenericMetadata:
|
||||
root = tree.getroot()
|
||||
return root
|
||||
|
||||
def _convert_xml_to_metadata(self, root: ET.Element) -> GenericMetadata:
|
||||
if root.tag != "ComicInfo":
|
||||
raise Exception("Not a ComicInfo file")
|
||||
|
||||
@ -252,20 +339,36 @@ class ComicInfoXml:
|
||||
if pages_node is not None:
|
||||
for page in pages_node:
|
||||
p: dict[str, Any] = page.attrib
|
||||
if "Image" in p:
|
||||
p["Image"] = int(p["Image"])
|
||||
md_page = ImageMetadata()
|
||||
|
||||
if "Bookmark" in p:
|
||||
md_page["bookmark"] = p["Bookmark"]
|
||||
if "DoublePage" in p:
|
||||
p["DoublePage"] = True if p["DoublePage"].casefold() in ("yes", "true", "1") else False
|
||||
md.pages.append(cast(ImageMetadata, p))
|
||||
md_page["double_page"] = True if p["DoublePage"].casefold() in ("yes", "true", "1") else False
|
||||
if "Image" in p:
|
||||
md_page["image_index"] = int(p["Image"])
|
||||
if "ImageHeight" in p:
|
||||
md_page["height"] = p["ImageHeight"]
|
||||
if "ImageSize" in p:
|
||||
md_page["size"] = p["ImageSize"]
|
||||
if "ImageWidth" in p:
|
||||
md_page["width"] = p["ImageWidth"]
|
||||
if "Type" in p:
|
||||
md_page["type"] = p["Type"]
|
||||
|
||||
md.pages.append(cast(ImageMetadata, md_page))
|
||||
|
||||
md.is_empty = False
|
||||
|
||||
return md
|
||||
|
||||
def write_to_external_file(self, filename: str, metadata: GenericMetadata, xml: bytes = b"") -> None:
|
||||
tree = self.convert_metadata_to_xml(metadata, xml)
|
||||
tree.write(filename, encoding="utf-8", xml_declaration=True)
|
||||
def _validate_bytes(self, string: bytes) -> bool:
|
||||
"""verify that the string actually contains CIX data in XML format"""
|
||||
try:
|
||||
root = ET.fromstring(string)
|
||||
if root.tag != "ComicInfo":
|
||||
return False
|
||||
except ET.ParseError:
|
||||
return False
|
||||
|
||||
def read_from_external_file(self, filename: str) -> GenericMetadata:
|
||||
tree = ET.parse(filename)
|
||||
return self.convert_xml_to_metadata(tree)
|
||||
return True
|
118
comicapi/metadata/metadata.py
Normal file
118
comicapi/metadata/metadata.py
Normal file
@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from comicapi.archivers import Archiver
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
|
||||
|
||||
class Metadata:
|
||||
enabled: bool = False
|
||||
short_name: str = ""
|
||||
|
||||
def __init__(self, version: str) -> None:
|
||||
self.version: str = version
|
||||
self.supported_attributes = {
|
||||
"tag_origin",
|
||||
"issue_id",
|
||||
"series",
|
||||
"issue",
|
||||
"title",
|
||||
"publisher",
|
||||
"month",
|
||||
"year",
|
||||
"day",
|
||||
"issue_count",
|
||||
"volume",
|
||||
"genre",
|
||||
"language",
|
||||
"comments",
|
||||
"volume_count",
|
||||
"critical_rating",
|
||||
"country",
|
||||
"alternate_series",
|
||||
"alternate_number",
|
||||
"alternate_count",
|
||||
"imprint",
|
||||
"notes",
|
||||
"web_link",
|
||||
"format",
|
||||
"manga",
|
||||
"black_and_white",
|
||||
"page_count",
|
||||
"maturity_rating",
|
||||
"story_arc",
|
||||
"series_group",
|
||||
"scan_info",
|
||||
"characters",
|
||||
"teams",
|
||||
"locations",
|
||||
"credits",
|
||||
"credits.person",
|
||||
"credits.role",
|
||||
"credits.primary",
|
||||
"tags",
|
||||
"pages",
|
||||
"pages.type",
|
||||
"pages.bookmark",
|
||||
"pages.double_page",
|
||||
"pages.image_index",
|
||||
"pages.size",
|
||||
"pages.height",
|
||||
"pages.width",
|
||||
"price",
|
||||
"is_version_of",
|
||||
"rights",
|
||||
"identifier",
|
||||
"last_mark",
|
||||
"cover_image",
|
||||
}
|
||||
|
||||
def supports_metadata(self, archive: Archiver) -> bool:
|
||||
"""
|
||||
Checks the given archive for the ability to save this metadata style.
|
||||
Should always return a bool. Failures should return False.
|
||||
Typically consists of a call to either `archive.supports_comment` or `archive.supports_file`
|
||||
"""
|
||||
return False
|
||||
|
||||
def has_metadata(self, archive: Archiver) -> bool:
|
||||
"""
|
||||
Checks the given archive for metadata.
|
||||
Should always return a bool. Failures should return False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def remove_metadata(self, archive: Archiver) -> bool:
|
||||
"""
|
||||
Removes the metadata from the given archive.
|
||||
Should always return a bool. Failures should return False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_metadata(self, archive: Archiver) -> GenericMetadata:
|
||||
"""
|
||||
Returns a GenericMetadata representing the data saved in the given archive.
|
||||
Should always return a GenericMetadata. Failures should return an empty metadata object.
|
||||
"""
|
||||
return GenericMetadata()
|
||||
|
||||
def get_metadata_string(self, archive: Archiver) -> str:
|
||||
"""
|
||||
Returns the raw metadata as a string.
|
||||
If the metadata is a binary format a roughly similar text format should be used.
|
||||
Should always return a string. Failures should return the empty string.
|
||||
"""
|
||||
return ""
|
||||
|
||||
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
|
||||
"""
|
||||
Saves the given metadata to the given archive.
|
||||
Should always return a bool. Failures should return False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def name(self) -> str:
|
||||
"""
|
||||
Returns the name of this metadata for display purposes eg "Comic Rack".
|
||||
Should always return a string. Failures should return the empty string.
|
||||
"""
|
||||
return ""
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -28,7 +28,8 @@ from datetime import datetime
|
||||
from typing import Any, TextIO
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comicapi.comicarchive import metadata_styles as md_styles
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib import ctversion
|
||||
from comictaggerlib.cbltransformer import CBLTransformer
|
||||
@ -134,10 +135,10 @@ class CLI:
|
||||
|
||||
def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool:
|
||||
if not self.config.Runtime_Options__dryrun:
|
||||
for metadata_style in self.config.Runtime_Options__type:
|
||||
for style in self.config.Runtime_Options__type:
|
||||
# write out the new data
|
||||
if not ca.write_metadata(md, metadata_style):
|
||||
logger.error("The tag save seemed to fail for style: %s!", MetaDataStyle.name[metadata_style])
|
||||
if not ca.write_metadata(md, style):
|
||||
logger.error("The tag save seemed to fail for style: %s!", md_styles[style].name())
|
||||
return False
|
||||
|
||||
self.output("Save complete.")
|
||||
@ -258,10 +259,10 @@ class CLI:
|
||||
|
||||
md.overlay(f_md)
|
||||
|
||||
for metadata_style in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(metadata_style):
|
||||
for style in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(style):
|
||||
try:
|
||||
t_md = ca.read_metadata(metadata_style)
|
||||
t_md = ca.read_metadata(style)
|
||||
md.overlay(t_md)
|
||||
break
|
||||
except Exception as e:
|
||||
@ -286,20 +287,9 @@ class CLI:
|
||||
brief += f"({page_count: >3} pages)"
|
||||
brief += " tags:[ "
|
||||
|
||||
if not (
|
||||
ca.has_metadata(MetaDataStyle.CBI)
|
||||
or ca.has_metadata(MetaDataStyle.CIX)
|
||||
or ca.has_metadata(MetaDataStyle.COMET)
|
||||
):
|
||||
brief += "none "
|
||||
else:
|
||||
if ca.has_metadata(MetaDataStyle.CBI):
|
||||
brief += "CBL "
|
||||
if ca.has_metadata(MetaDataStyle.CIX):
|
||||
brief += "CR "
|
||||
if ca.has_metadata(MetaDataStyle.COMET):
|
||||
brief += "CoMet "
|
||||
brief += "]"
|
||||
metadata_styles = [md_styles[style].name() for style in md_styles if ca.has_metadata(style)]
|
||||
brief += " ".join(metadata_styles)
|
||||
brief += " ]"
|
||||
|
||||
self.output(brief)
|
||||
|
||||
@ -308,57 +298,23 @@ class CLI:
|
||||
|
||||
self.output()
|
||||
|
||||
raw: str | bytes = ""
|
||||
md = None
|
||||
if not self.config.Runtime_Options__type or MetaDataStyle.CIX in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(MetaDataStyle.CIX):
|
||||
self.output("--------- ComicRack tags ---------")
|
||||
try:
|
||||
if self.config.Runtime_Options__raw:
|
||||
raw = ca.read_raw_cix()
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
self.output(raw)
|
||||
else:
|
||||
md = ca.read_cix()
|
||||
self.output(md)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if not self.config.Runtime_Options__type or MetaDataStyle.CBI in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(MetaDataStyle.CBI):
|
||||
self.output("------- ComicBookLover tags -------")
|
||||
try:
|
||||
if self.config.Runtime_Options__raw:
|
||||
raw = ca.read_raw_cbi()
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
self.output(raw)
|
||||
else:
|
||||
md = ca.read_cbi()
|
||||
self.output(md)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if not self.config.Runtime_Options__type or MetaDataStyle.COMET in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(MetaDataStyle.COMET):
|
||||
self.output("----------- CoMet tags -----------")
|
||||
try:
|
||||
if self.config.Runtime_Options__raw:
|
||||
raw = ca.read_raw_comet()
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
self.output(raw)
|
||||
else:
|
||||
md = ca.read_comet()
|
||||
self.output(md)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
for style, style_obj in md_styles.items():
|
||||
if not self.config.Runtime_Options__type or style in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(style):
|
||||
self.output(f"--------- {style_obj.name()} tags ---------")
|
||||
try:
|
||||
if self.config.Runtime_Options__raw:
|
||||
self.output(ca.read_metadata_string(style))
|
||||
else:
|
||||
md = ca.read_metadata(style)
|
||||
self.output(md)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
return Result(Action.print, Status.success, ca.path, md=md)
|
||||
|
||||
def delete_style(self, ca: ComicArchive, style: int) -> Status:
|
||||
style_name = MetaDataStyle.name[style]
|
||||
def delete_style(self, ca: ComicArchive, style: str) -> Status:
|
||||
style_name = md_styles[style].name()
|
||||
|
||||
if ca.has_metadata(style):
|
||||
if not self.config.Runtime_Options__dryrun:
|
||||
@ -376,16 +332,16 @@ class CLI:
|
||||
|
||||
def delete(self, ca: ComicArchive) -> Result:
|
||||
res = Result(Action.delete, Status.success, ca.path)
|
||||
for metadata_style in self.config.Runtime_Options__type:
|
||||
status = self.delete_style(ca, metadata_style)
|
||||
for style in self.config.Runtime_Options__type:
|
||||
status = self.delete_style(ca, style)
|
||||
if status == Status.success:
|
||||
res.tags_deleted.append(metadata_style)
|
||||
res.tags_deleted.append(style)
|
||||
else:
|
||||
res.status = status
|
||||
return res
|
||||
|
||||
def copy_style(self, ca: ComicArchive, md: GenericMetadata, style: int) -> Status:
|
||||
dst_style_name = MetaDataStyle.name[style]
|
||||
def copy_style(self, ca: ComicArchive, md: GenericMetadata, style: str) -> Status:
|
||||
dst_style_name = md_styles[style].name()
|
||||
if not self.config.Runtime_Options__overwrite and ca.has_metadata(style):
|
||||
self.output(f"{ca.path}: Already has {dst_style_name} tags. Not overwriting.")
|
||||
return Status.existing_tags
|
||||
@ -393,44 +349,47 @@ class CLI:
|
||||
self.output(f"{ca.path}: Destination and source are same: {dst_style_name}. Nothing to do.")
|
||||
return Status.existing_tags
|
||||
|
||||
src_style_name = MetaDataStyle.name[self.config.Commands__copy]
|
||||
if ca.has_metadata(self.config.Commands__copy):
|
||||
if not self.config.Runtime_Options__dryrun:
|
||||
if self.config.Comic_Book_Lover__apply_transform_on_bulk_operation == MetaDataStyle.CBI:
|
||||
md = CBLTransformer(md, self.config).apply()
|
||||
src_style_name = md_styles[self.config.Commands__copy].name()
|
||||
if not self.config.Runtime_Options__dryrun:
|
||||
if self.config.Comic_Book_Lover__apply_transform_on_bulk_operation == "cbi":
|
||||
md = CBLTransformer(md, self.config).apply()
|
||||
|
||||
if ca.write_metadata(md, style):
|
||||
self.output(f"{ca.path}: Copied {src_style_name} tags to {dst_style_name}.")
|
||||
return Status.success
|
||||
else:
|
||||
self.output(f"{ca.path}: Tag copy seemed to fail!")
|
||||
return Status.write_failure
|
||||
else:
|
||||
self.output(f"{ca.path}: dry-run. {src_style_name} tags not copied")
|
||||
if ca.write_metadata(md, style):
|
||||
self.output(f"{ca.path}: Copied {src_style_name} tags to {dst_style_name}.")
|
||||
return Status.success
|
||||
self.output(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.")
|
||||
else:
|
||||
self.output(f"{ca.path}: Tag copy seemed to fail!")
|
||||
return Status.write_failure
|
||||
else:
|
||||
self.output(f"{ca.path}: dry-run. {src_style_name} tags not copied")
|
||||
return Status.success
|
||||
return Status.read_failure
|
||||
|
||||
def copy(self, ca: ComicArchive) -> Result:
|
||||
src_style_name = md_styles[self.config.Commands__copy].name()
|
||||
res = Result(Action.copy, Status.success, ca.path)
|
||||
if not ca.has_metadata(self.config.Commands__copy):
|
||||
self.output(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.")
|
||||
res.status = Status.read_failure
|
||||
return res
|
||||
try:
|
||||
res.md = ca.read_metadata(self.config.Commands__copy)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
return res
|
||||
for metadata_style in self.config.Runtime_Options__type:
|
||||
status = self.copy_style(ca, res.md, metadata_style)
|
||||
for style in self.config.Runtime_Options__type:
|
||||
status = self.copy_style(ca, res.md, style)
|
||||
if status == Status.success:
|
||||
res.tags_written.append(metadata_style)
|
||||
res.tags_written.append(style)
|
||||
else:
|
||||
res.status = status
|
||||
return res
|
||||
|
||||
def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> Result:
|
||||
if not self.config.Runtime_Options__overwrite:
|
||||
for metadata_style in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(metadata_style):
|
||||
self.output(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
|
||||
for style in self.config.Runtime_Options__type:
|
||||
if ca.has_metadata(style):
|
||||
self.output(f"{ca.path}: Already has {md_styles[style].name()} tags. Not overwriting.")
|
||||
return Result(
|
||||
Action.save,
|
||||
original_path=ca.path,
|
||||
@ -443,7 +402,7 @@ class CLI:
|
||||
|
||||
md = self.create_local_metadata(ca)
|
||||
if md.issue is None or md.issue == "":
|
||||
if self.config.Auto_Tag__assume_1_if_no_issue_num:
|
||||
if self.config.Auto_Tag__assume_issue_one:
|
||||
md.issue = "1"
|
||||
|
||||
matches: list[IssueResult] = []
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -17,8 +17,8 @@ def general(parser: settngs.Manager) -> None:
|
||||
def internal(parser: settngs.Manager) -> None:
|
||||
# automatic settings
|
||||
parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False)
|
||||
parser.add_setting("save_data_style", default=0, cmdline=False)
|
||||
parser.add_setting("load_data_style", default=0, cmdline=False)
|
||||
parser.add_setting("save_data_style", default="cbi", cmdline=False)
|
||||
parser.add_setting("load_data_style", default="cbi", cmdline=False)
|
||||
parser.add_setting("last_opened_folder", default="", cmdline=False)
|
||||
parser.add_setting("window_width", default=0, cmdline=False)
|
||||
parser.add_setting("window_height", default=0, cmdline=False)
|
||||
@ -212,7 +212,6 @@ def autotag(parser: settngs.Manager) -> None:
|
||||
parser.add_setting(
|
||||
"-1",
|
||||
"--assume-issue-one",
|
||||
dest="assume_1_if_no_issue_num",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Assume issue number is 1 if not found (relevant for -s).\n\n",
|
||||
default=False,
|
||||
|
@ -11,7 +11,7 @@ import comictaggerlib.resulttypes
|
||||
class settngs_namespace(settngs.TypedNS):
|
||||
Commands__version: bool
|
||||
Commands__command: comictaggerlib.resulttypes.Action
|
||||
Commands__copy: int
|
||||
Commands__copy: str
|
||||
|
||||
Runtime_Options__config: comictaggerlib.ctsettings.types.ComicTaggerPaths
|
||||
Runtime_Options__verbose: int
|
||||
@ -32,14 +32,14 @@ class settngs_namespace(settngs.TypedNS):
|
||||
Runtime_Options__glob: bool
|
||||
Runtime_Options__quiet: bool
|
||||
Runtime_Options__json: bool
|
||||
Runtime_Options__type: list[int]
|
||||
Runtime_Options__type: list[str]
|
||||
Runtime_Options__overwrite: bool
|
||||
Runtime_Options__no_gui: bool
|
||||
Runtime_Options__files: list[str]
|
||||
|
||||
internal__install_id: str
|
||||
internal__save_data_style: int
|
||||
internal__load_data_style: int
|
||||
internal__save_data_style: str
|
||||
internal__load_data_style: str
|
||||
internal__last_opened_folder: str
|
||||
internal__window_width: int
|
||||
internal__window_height: int
|
||||
@ -91,7 +91,7 @@ class settngs_namespace(settngs.TypedNS):
|
||||
|
||||
Auto_Tag__save_on_low_confidence: bool
|
||||
Auto_Tag__dont_use_year_when_identifying: bool
|
||||
Auto_Tag__assume_1_if_no_issue_num: bool
|
||||
Auto_Tag__assume_issue_one: bool
|
||||
Auto_Tag__ignore_leading_numbers_in_filename: bool
|
||||
Auto_Tag__remove_archive_after_successful_match: bool
|
||||
|
||||
|
@ -6,7 +6,7 @@ import pathlib
|
||||
from appdirs import AppDirs
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import MetaDataStyle
|
||||
from comicapi.comicarchive import metadata_styles
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
|
||||
|
||||
@ -58,22 +58,22 @@ class ComicTaggerPaths(AppDirs):
|
||||
return pathlib.Path(super().site_config_dir)
|
||||
|
||||
|
||||
def metadata_type_single(types: str) -> int:
|
||||
def metadata_type_single(types: str) -> str:
|
||||
result = metadata_type(types)
|
||||
if len(result) > 1:
|
||||
raise argparse.ArgumentTypeError(f"invalid choice: {result} (only one metadata style allowed)")
|
||||
return result[0]
|
||||
|
||||
|
||||
def metadata_type(types: str) -> list[int]:
|
||||
def metadata_type(types: str) -> list[str]:
|
||||
result = []
|
||||
types = types.casefold()
|
||||
for typ in utils.split(types, ","):
|
||||
typ = typ.strip()
|
||||
if typ not in MetaDataStyle.short_name:
|
||||
choices = ", ".join(MetaDataStyle.short_name)
|
||||
if typ not in metadata_styles:
|
||||
choices = ", ".join(metadata_styles)
|
||||
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
|
||||
result.append(MetaDataStyle.short_name.index(typ))
|
||||
result.append(metadata_styles[typ].short_name)
|
||||
return result
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@ from typing import Callable, cast
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comicapi.comicarchive import ComicArchive, metadata_styles
|
||||
from comictaggerlib.ctsettings import ct_ns
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
from comictaggerlib.optionalmsgdialog import OptionalMessageDialog
|
||||
@ -206,7 +206,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
if row is not None:
|
||||
ca = self.get_archive_by_row(row)
|
||||
rar_added_ro = bool(ca and ca.archiver.name() == "RAR" and not ca.archiver.is_writable())
|
||||
if first_added is None:
|
||||
if first_added is None and row != -1:
|
||||
first_added = row
|
||||
|
||||
progdialog.hide()
|
||||
@ -286,7 +286,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
|
||||
filename_item = QtWidgets.QTableWidgetItem()
|
||||
folder_item = QtWidgets.QTableWidgetItem()
|
||||
cix_item = FileTableWidgetItem()
|
||||
md_item = FileTableWidgetItem()
|
||||
cbi_item = FileTableWidgetItem()
|
||||
readonly_item = FileTableWidgetItem()
|
||||
type_item = QtWidgets.QTableWidgetItem()
|
||||
@ -301,9 +301,9 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
type_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
|
||||
|
||||
cix_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
cix_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
|
||||
md_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
md_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
|
||||
self.twList.setItem(row, FileSelectionList.CRFlagColNum, md_item)
|
||||
|
||||
cbi_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
cbi_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
|
||||
@ -324,8 +324,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
|
||||
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
|
||||
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
|
||||
cix_item = self.twList.item(row, FileSelectionList.CRFlagColNum)
|
||||
cbi_item = self.twList.item(row, FileSelectionList.CBLFlagColNum)
|
||||
md_item = self.twList.item(row, FileSelectionList.CRFlagColNum)
|
||||
type_item = self.twList.item(row, FileSelectionList.typeColNum)
|
||||
readonly_item = self.twList.item(row, FileSelectionList.readonlyColNum)
|
||||
|
||||
@ -341,19 +340,10 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
type_item.setText(item_text)
|
||||
type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
|
||||
if fi.ca.has_cix():
|
||||
cix_item.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
|
||||
else:
|
||||
cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
|
||||
cix_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
|
||||
if fi.ca.has_cbi():
|
||||
cbi_item.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
|
||||
else:
|
||||
cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
|
||||
cbi_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
styles = ", ".join(
|
||||
metadata_styles[x].name() for x in fi.ca.get_supported_metadata() if fi.ca.has_metadata(x)
|
||||
)
|
||||
md_item.setText(styles)
|
||||
|
||||
if not fi.ca.is_writable():
|
||||
readonly_item.setCheckState(QtCore.Qt.CheckState.Checked)
|
||||
@ -362,13 +352,6 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
|
||||
readonly_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
|
||||
|
||||
# Reading these will force them into the ComicArchive's cache
|
||||
try:
|
||||
fi.ca.read_cix()
|
||||
except Exception:
|
||||
pass
|
||||
fi.ca.has_cbi()
|
||||
|
||||
def get_selected_archive_list(self) -> list[ComicArchive]:
|
||||
ca_list: list[ComicArchive] = []
|
||||
for r in range(self.twList.rowCount()):
|
||||
|
@ -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)
|
||||
|
@ -122,6 +122,7 @@ class App:
|
||||
|
||||
def load_plugins(self, opts: argparse.Namespace) -> None:
|
||||
comicapi.comicarchive.load_archive_plugins()
|
||||
comicapi.comicarchive.load_metadata_plugins(version=version)
|
||||
self.talkers = comictalker.get_talkers(version, opts.config.user_cache_dir)
|
||||
|
||||
def list_plugins(
|
||||
|
@ -17,12 +17,13 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.comicarchive import ComicArchive, metadata_styles
|
||||
from comicapi.genericmetadata import ImageMetadata, PageType
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import enable_widget
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -71,6 +72,15 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
with (ui_path / "pagelisteditor.ui").open(encoding="utf-8") as uifile:
|
||||
uic.loadUi(uifile, self)
|
||||
|
||||
self.md_attributes = {
|
||||
"page.image_index": [self.btnDown, self.btnUp, self.listWidget],
|
||||
"page.type": self.cbPageType,
|
||||
"page.double_page": self.chkDoublePage,
|
||||
"page.bookmark": self.leBookmark,
|
||||
# Python dicts are order preserving this must be placed last
|
||||
"pages": [self.btnDown, self.btnUp, self.listWidget, self.cbPageType, self.chkDoublePage, self.leBookmark],
|
||||
}
|
||||
|
||||
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode, None, None)
|
||||
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
|
||||
gridlayout.addWidget(self.pageWidget)
|
||||
@ -105,6 +115,7 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
|
||||
self.comic_archive: ComicArchive | None = None
|
||||
self.pages_list: list[ImageMetadata] = []
|
||||
self.data_style = ""
|
||||
|
||||
def reset_page(self) -> None:
|
||||
self.pageWidget.clear()
|
||||
@ -220,14 +231,14 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
i = self.cbPageType.findData(pagetype)
|
||||
self.cbPageType.setCurrentIndex(i)
|
||||
|
||||
self.chkDoublePage.setChecked("DoublePage" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0])
|
||||
self.chkDoublePage.setChecked("double_page" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0])
|
||||
|
||||
if "Bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]:
|
||||
self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["Bookmark"])
|
||||
if "bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]:
|
||||
self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["bookmark"])
|
||||
else:
|
||||
self.leBookmark.setText("")
|
||||
|
||||
idx = int(self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]["Image"])
|
||||
idx = int(self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]["image_index"])
|
||||
|
||||
if self.comic_archive is not None:
|
||||
self.pageWidget.set_archive(self.comic_archive, idx)
|
||||
@ -237,16 +248,16 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
for i in range(self.listWidget.count()):
|
||||
item = self.listWidget.item(i)
|
||||
page_dict: ImageMetadata = item.data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
if "Type" in page_dict and page_dict["Type"] == PageType.FrontCover:
|
||||
front_cover = int(page_dict["Image"])
|
||||
if "type" in page_dict and page_dict["type"] == PageType.FrontCover:
|
||||
front_cover = int(page_dict["image_index"])
|
||||
break
|
||||
return front_cover
|
||||
|
||||
def get_current_page_type(self) -> str:
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
if "Type" in page_dict:
|
||||
return page_dict["Type"]
|
||||
if "type" in page_dict:
|
||||
return page_dict["type"]
|
||||
|
||||
return ""
|
||||
|
||||
@ -255,10 +266,10 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]
|
||||
|
||||
if t == "":
|
||||
if "Type" in page_dict:
|
||||
del page_dict["Type"]
|
||||
if "type" in page_dict:
|
||||
del page_dict["type"]
|
||||
else:
|
||||
page_dict["Type"] = t
|
||||
page_dict["type"] = t
|
||||
|
||||
item = self.listWidget.item(row)
|
||||
# wrap the dict in a tuple to keep from being converted to QtWidgets.QStrings
|
||||
@ -272,11 +283,11 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
cbx = self.sender()
|
||||
|
||||
if isinstance(cbx, QtWidgets.QCheckBox) and cbx.isChecked():
|
||||
if "DoublePage" not in page_dict:
|
||||
page_dict["DoublePage"] = True
|
||||
if "double_page" not in page_dict:
|
||||
page_dict["double_page"] = True
|
||||
self.modified.emit()
|
||||
elif "DoublePage" in page_dict:
|
||||
del page_dict["DoublePage"]
|
||||
elif "double_page" in page_dict:
|
||||
del page_dict["double_page"]
|
||||
self.modified.emit()
|
||||
|
||||
item = self.listWidget.item(row)
|
||||
@ -291,16 +302,16 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
page_dict: ImageMetadata = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]
|
||||
|
||||
current_bookmark = ""
|
||||
if "Bookmark" in page_dict:
|
||||
current_bookmark = page_dict["Bookmark"]
|
||||
if "bookmark" in page_dict:
|
||||
current_bookmark = page_dict["bookmark"]
|
||||
|
||||
if self.leBookmark.text().strip():
|
||||
new_bookmark = str(self.leBookmark.text().strip())
|
||||
if current_bookmark != new_bookmark:
|
||||
page_dict["Bookmark"] = new_bookmark
|
||||
page_dict["bookmark"] = new_bookmark
|
||||
self.modified.emit()
|
||||
elif current_bookmark != "":
|
||||
del page_dict["Bookmark"]
|
||||
del page_dict["bookmark"]
|
||||
self.modified.emit()
|
||||
|
||||
item = self.listWidget.item(row)
|
||||
@ -313,10 +324,12 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
def set_data(self, comic_archive: ComicArchive, pages_list: list[ImageMetadata]) -> None:
|
||||
self.comic_archive = comic_archive
|
||||
self.pages_list = pages_list
|
||||
if pages_list is not None and len(pages_list) > 0:
|
||||
self.cbPageType.setDisabled(False)
|
||||
self.chkDoublePage.setDisabled(False)
|
||||
self.leBookmark.setDisabled(False)
|
||||
if pages_list:
|
||||
self.set_metadata_style(self.data_style)
|
||||
else:
|
||||
self.cbPageType.setEnabled(False)
|
||||
self.chkDoublePage.setEnabled(False)
|
||||
self.leBookmark.setEnabled(False)
|
||||
|
||||
self.listWidget.itemSelectionChanged.disconnect(self.change_page)
|
||||
|
||||
@ -332,15 +345,15 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
self.listWidget.setCurrentRow(0)
|
||||
|
||||
def list_entry_text(self, page_dict: ImageMetadata) -> str:
|
||||
text = str(int(page_dict["Image"]) + 1)
|
||||
if "Type" in page_dict:
|
||||
if page_dict["Type"] in self.pageTypeNames:
|
||||
text += " (" + self.pageTypeNames[page_dict["Type"]] + ")"
|
||||
text = str(int(page_dict["image_index"]) + 1)
|
||||
if "type" in page_dict:
|
||||
if page_dict["type"] in self.pageTypeNames:
|
||||
text += " (" + self.pageTypeNames[page_dict["type"]] + ")"
|
||||
else:
|
||||
text += " (Error: " + page_dict["Type"] + ")"
|
||||
if "DoublePage" in page_dict:
|
||||
text += " (Error: " + page_dict["type"] + ")"
|
||||
if "double_page" in page_dict:
|
||||
text += " ②"
|
||||
if "Bookmark" in page_dict:
|
||||
if "bookmark" in page_dict:
|
||||
text += " 🔖"
|
||||
return text
|
||||
|
||||
@ -356,42 +369,11 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
self.first_front_page = self.get_first_front_cover()
|
||||
self.firstFrontCoverChanged.emit(self.first_front_page)
|
||||
|
||||
def set_metadata_style(self, data_style: int) -> None:
|
||||
def set_metadata_style(self, data_style: str) -> None:
|
||||
# depending on the current data style, certain fields are disabled
|
||||
if data_style:
|
||||
self.metadata_style = data_style
|
||||
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
active_palette = self.cbPageType.palette()
|
||||
|
||||
inactive_palette3 = self.cbPageType.palette()
|
||||
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
||||
|
||||
if data_style == MetaDataStyle.CIX:
|
||||
self.btnUp.setEnabled(True)
|
||||
self.btnDown.setEnabled(True)
|
||||
self.cbPageType.setEnabled(True)
|
||||
self.chkDoublePage.setEnabled(True)
|
||||
self.leBookmark.setEnabled(True)
|
||||
self.listWidget.setEnabled(True)
|
||||
|
||||
self.leBookmark.setPalette(active_palette)
|
||||
self.listWidget.setPalette(active_palette)
|
||||
|
||||
elif data_style == MetaDataStyle.CBI:
|
||||
self.btnUp.setEnabled(False)
|
||||
self.btnDown.setEnabled(False)
|
||||
self.cbPageType.setEnabled(False)
|
||||
self.chkDoublePage.setEnabled(False)
|
||||
self.leBookmark.setEnabled(False)
|
||||
self.listWidget.setEnabled(False)
|
||||
|
||||
self.leBookmark.setPalette(inactive_palette3)
|
||||
self.listWidget.setPalette(inactive_palette3)
|
||||
|
||||
elif data_style == MetaDataStyle.COMET:
|
||||
pass
|
||||
|
||||
# make sure combo is disabled when no list
|
||||
if self.comic_archive is None:
|
||||
self.cbPageType.setEnabled(False)
|
||||
self.chkDoublePage.setEnabled(False)
|
||||
self.leBookmark.setEnabled(False)
|
||||
enabled_widgets = metadata_styles[data_style].supported_attributes
|
||||
for metadata, widget in self.md_attributes.items():
|
||||
enable_widget(widget, metadata in enabled_widgets)
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -15,13 +15,12 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import functools
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import pickle
|
||||
import platform
|
||||
import pprint
|
||||
import re
|
||||
import sys
|
||||
import webbrowser
|
||||
@ -33,9 +32,9 @@ import natsort
|
||||
import settngs
|
||||
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic
|
||||
|
||||
import comictaggerlib.ui
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.comicinfoxml import ComicInfoXml
|
||||
from comicapi.comicarchive import ComicArchive, metadata_styles
|
||||
from comicapi.filenameparser import FileNameParser
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
@ -61,7 +60,7 @@ from comictaggerlib.resulttypes import Action, IssueResult, MatchStatus, OnlineM
|
||||
from comictaggerlib.seriesselectionwindow import SeriesSelectionWindow
|
||||
from comictaggerlib.settingswindow import SettingsWindow
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent, enable_widget, reduce_widget_font_size
|
||||
from comictaggerlib.versionchecker import VersionChecker
|
||||
from comictalker.comictalker import ComicTalker, TalkerError
|
||||
from comictalker.talker_utils import cleanup_html
|
||||
@ -88,6 +87,62 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
with (ui_path / "taggerwindow.ui").open(encoding="utf-8") as uifile:
|
||||
uic.loadUi(uifile, self)
|
||||
|
||||
self.md_attributes = {
|
||||
"tag_origin": None,
|
||||
"issue_id": None,
|
||||
"series": self.leSeries,
|
||||
"issue": self.leIssueNum,
|
||||
"title": self.leTitle,
|
||||
"publisher": self.lePublisher,
|
||||
"month": self.lePubMonth,
|
||||
"year": self.lePubYear,
|
||||
"day": self.lePubDay,
|
||||
"issue_count": self.leIssueCount,
|
||||
"volume": self.leVolumeNum,
|
||||
"genres": self.leGenre,
|
||||
"language": self.cbLanguage,
|
||||
"description": self.teComments,
|
||||
"volume_count": self.leVolumeCount,
|
||||
"critical_rating": self.dsbCriticalRating,
|
||||
"country": self.cbCountry,
|
||||
"alternate_series": self.leAltSeries,
|
||||
"alternate_number": self.leAltIssueNum,
|
||||
"alternate_count": self.leAltIssueCount,
|
||||
"imprint": self.leImprint,
|
||||
"notes": self.teNotes,
|
||||
"web_link": self.leWebLink,
|
||||
"format": self.cbFormat,
|
||||
"manga": self.cbManga,
|
||||
"black_and_white": self.cbBW,
|
||||
"page_count": None,
|
||||
"maturity_rating": self.cbMaturityRating,
|
||||
"story_arcs": self.leStoryArc,
|
||||
"series_groups": self.leSeriesGroup,
|
||||
"scan_info": self.leScanInfo,
|
||||
"characters": self.teCharacters,
|
||||
"teams": self.teTeams,
|
||||
"locations": self.teLocations,
|
||||
"credits": [self.twCredits, self.btnAddCredit, self.btnEditCredit, self.btnRemoveCredit],
|
||||
"credits.person": 2,
|
||||
"credits.role": 1,
|
||||
"credits.primary": 0,
|
||||
"tags": self.teTags,
|
||||
"pages": None,
|
||||
"page.type": None,
|
||||
"page.bookmark": None,
|
||||
"page.double_page": None,
|
||||
"page.image_index": None,
|
||||
"page.size": None,
|
||||
"page.height": None,
|
||||
"page.width": None,
|
||||
"price": None,
|
||||
"is_version_of": None,
|
||||
"rights": None,
|
||||
"identifier": None,
|
||||
"last_mark": None,
|
||||
}
|
||||
comictaggerlib.ui.qtutils.active_palette = self.leSeries.palette()
|
||||
self.config = config
|
||||
self.talkers = talkers
|
||||
self.log_window = self.setup_logger()
|
||||
@ -157,15 +212,20 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
|
||||
if config[0].Runtime_Options__type and isinstance(config[0].Runtime_Options__type[0], int):
|
||||
if config[0].Runtime_Options__type and isinstance(config[0].Runtime_Options__type[0], str):
|
||||
# respect the command line option tag type
|
||||
config[0].internal__save_data_style = config[0].Runtime_Options__type[0]
|
||||
config[0].internal__load_data_style = config[0].Runtime_Options__type[0]
|
||||
|
||||
if config[0].internal__save_data_style not in metadata_styles:
|
||||
config[0].internal__save_data_style = list(metadata_styles.keys())[0]
|
||||
if config[0].internal__load_data_style not in metadata_styles:
|
||||
config[0].internal__load_data_style = list(metadata_styles.keys())[0]
|
||||
self.save_data_style = config[0].internal__save_data_style
|
||||
self.load_data_style = config[0].internal__load_data_style
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
self.view_tag_actions, self.remove_tag_actions = self.tag_actions()
|
||||
self.config_menus()
|
||||
self.statusBar()
|
||||
self.populate_combo_boxes()
|
||||
@ -225,7 +285,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.page_list_editor.listOrderChanged.connect(self.page_list_order_changed)
|
||||
self.tabWidget.currentChanged.connect(self.tab_changed)
|
||||
|
||||
self.update_style_tweaks()
|
||||
self.update_metadata_style_tweaks()
|
||||
|
||||
self.show()
|
||||
self.set_app_position()
|
||||
@ -244,7 +304,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.fileSelectionList.add_app_action(self.actionRemoveAuto)
|
||||
self.fileSelectionList.add_app_action(self.actionRepackage)
|
||||
|
||||
if len(file_list) != 0:
|
||||
if file_list:
|
||||
self.fileSelectionList.add_path_list(file_list)
|
||||
|
||||
if self.config[0].Dialog_Flags__show_disclaimer:
|
||||
@ -271,6 +331,20 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if self.config[0].General__check_for_new_version:
|
||||
self.check_latest_version_online()
|
||||
|
||||
def tag_actions(self) -> tuple[dict[str, QtGui.QAction], dict[str, QtGui.QAction]]:
|
||||
view_raw_tags = {}
|
||||
remove_raw_tags = {}
|
||||
for style in metadata_styles.values():
|
||||
view_raw_tags[style.short_name] = self.menuViewRawTags.addAction(f"View Raw {style.name()} Tags")
|
||||
view_raw_tags[style.short_name].setStatusTip(f"View raw {style.name()} tag block from file")
|
||||
view_raw_tags[style.short_name].triggered.connect(functools.partial(self.view_raw_tags, style.short_name))
|
||||
|
||||
remove_raw_tags[style.short_name] = self.menuRemove.addAction(f"Remove Raw {style.name()} Tags")
|
||||
remove_raw_tags[style.short_name].setStatusTip(f"Remove {style.name()} tags from comic archive")
|
||||
remove_raw_tags[style.short_name].triggered.connect(functools.partial(self.remove_tags, style.short_name))
|
||||
|
||||
return view_raw_tags, remove_raw_tags
|
||||
|
||||
def current_talker(self) -> ComicTalker:
|
||||
if self.config[0].Sources__source in self.talkers:
|
||||
return self.talkers[self.config[0].Sources__source]
|
||||
@ -366,18 +440,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.actionRemoveAuto.setStatusTip("Remove currently selected modify tag style from the archive")
|
||||
self.actionRemoveAuto.triggered.connect(self.remove_auto)
|
||||
|
||||
self.actionRemoveCBLTags.setStatusTip("Remove ComicBookLover tags from comic archive")
|
||||
self.actionRemoveCBLTags.triggered.connect(self.remove_cbl_tags)
|
||||
|
||||
self.actionRemoveCRTags.setStatusTip("Remove ComicRack tags from comic archive")
|
||||
self.actionRemoveCRTags.triggered.connect(self.remove_cr_tags)
|
||||
|
||||
self.actionViewRawCRTags.setStatusTip("View raw ComicRack tag block from file")
|
||||
self.actionViewRawCRTags.triggered.connect(self.view_raw_cr_tags)
|
||||
|
||||
self.actionViewRawCBLTags.setStatusTip("View raw ComicBookLover tag block from file")
|
||||
self.actionViewRawCBLTags.triggered.connect(self.view_raw_cbl_tags)
|
||||
|
||||
self.actionRepackage.setShortcut("Ctrl+E")
|
||||
self.actionRepackage.setStatusTip("Re-create archive as CBZ")
|
||||
self.actionRepackage.triggered.connect(self.repackage_archive)
|
||||
@ -648,48 +710,27 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.archiveCoverWidget.set_archive(self.comic_archive, cover_idx)
|
||||
|
||||
def update_menus(self) -> None:
|
||||
# First just disable all the questionable items
|
||||
self.actionAutoTag.setEnabled(False)
|
||||
self.actionCopyTags.setEnabled(False)
|
||||
self.actionRemoveAuto.setEnabled(False)
|
||||
self.actionRemoveCRTags.setEnabled(False)
|
||||
self.actionRemoveCBLTags.setEnabled(False)
|
||||
self.actionWrite_Tags.setEnabled(False)
|
||||
self.actionRepackage.setEnabled(False)
|
||||
self.actionViewRawCBLTags.setEnabled(False)
|
||||
self.actionViewRawCRTags.setEnabled(False)
|
||||
self.actionParse_Filename.setEnabled(False)
|
||||
self.actionParse_Filename_split_words.setEnabled(False)
|
||||
self.actionAutoIdentify.setEnabled(False)
|
||||
self.actionRename.setEnabled(False)
|
||||
self.actionApplyCBLTransform.setEnabled(False)
|
||||
self.actionReCalcPageDims.setEnabled(False)
|
||||
enabled = self.comic_archive is not None
|
||||
writeable = self.comic_archive is not None and self.comic_archive.is_writable()
|
||||
self.actionApplyCBLTransform.setEnabled(enabled)
|
||||
self.actionAutoIdentify.setEnabled(enabled)
|
||||
self.actionAutoTag.setEnabled(enabled)
|
||||
self.actionCopyTags.setEnabled(enabled)
|
||||
self.actionParse_Filename.setEnabled(enabled)
|
||||
self.actionParse_Filename_split_words.setEnabled(enabled)
|
||||
self.actionReCalcPageDims.setEnabled(enabled)
|
||||
self.actionRemoveAuto.setEnabled(enabled)
|
||||
self.actionRename.setEnabled(enabled)
|
||||
self.actionRepackage.setEnabled(enabled)
|
||||
|
||||
# now, selectively re-enable
|
||||
self.menuRemove.setEnabled(enabled)
|
||||
self.menuViewRawTags.setEnabled(enabled)
|
||||
if self.comic_archive is not None:
|
||||
has_cix = self.comic_archive.has_cix()
|
||||
has_cbi = self.comic_archive.has_cbi()
|
||||
for style in metadata_styles:
|
||||
self.view_tag_actions[style].setEnabled(self.comic_archive.has_metadata(style))
|
||||
self.remove_tag_actions[style].setEnabled(self.comic_archive.has_metadata(style))
|
||||
|
||||
self.actionParse_Filename.setEnabled(True)
|
||||
self.actionParse_Filename_split_words.setEnabled(True)
|
||||
self.actionAutoIdentify.setEnabled(True)
|
||||
self.actionAutoTag.setEnabled(True)
|
||||
self.actionRename.setEnabled(True)
|
||||
self.actionApplyCBLTransform.setEnabled(True)
|
||||
self.actionReCalcPageDims.setEnabled(True)
|
||||
self.actionRepackage.setEnabled(True)
|
||||
self.actionRemoveAuto.setEnabled(True)
|
||||
self.actionRemoveCRTags.setEnabled(True)
|
||||
self.actionRemoveCBLTags.setEnabled(True)
|
||||
self.actionCopyTags.setEnabled(True)
|
||||
|
||||
if has_cix:
|
||||
self.actionViewRawCRTags.setEnabled(True)
|
||||
if has_cbi:
|
||||
self.actionViewRawCBLTags.setEnabled(True)
|
||||
|
||||
if self.comic_archive.is_writable():
|
||||
self.actionWrite_Tags.setEnabled(True)
|
||||
self.actionWrite_Tags.setEnabled(writeable)
|
||||
|
||||
def update_info_box(self) -> None:
|
||||
ca = self.comic_archive
|
||||
@ -712,13 +753,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
page_count = f" ({ca.get_number_of_pages()} pages)"
|
||||
self.lblPageCount.setText(page_count)
|
||||
|
||||
supported_md = ca.get_supported_metadata()
|
||||
tag_info = ""
|
||||
if ca.has_cix():
|
||||
tag_info = "• ComicRack tags"
|
||||
if ca.has_cbi():
|
||||
if tag_info != "":
|
||||
tag_info += "\n"
|
||||
tag_info += "• ComicBookLover tags"
|
||||
for md in supported_md:
|
||||
if ca.has_metadata(md):
|
||||
tag_info += "• " + metadata_styles[md].name() + "\n"
|
||||
|
||||
self.lblTagList.setText(tag_info)
|
||||
|
||||
@ -872,7 +911,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
)
|
||||
|
||||
self.twCredits.setSortingEnabled(True)
|
||||
self.update_credit_colors()
|
||||
self.update_metadata_credit_colors()
|
||||
|
||||
def add_new_credit_entry(self, row: int, role: str, name: str, primary_flag: bool = False) -> None:
|
||||
self.twCredits.insertRow(row)
|
||||
@ -1111,7 +1150,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"Save Tags",
|
||||
f"Are you sure you wish to save {MetaDataStyle.name[self.save_data_style]} tags to this archive?",
|
||||
f"Are you sure you wish to save {metadata_styles[self.save_data_style].name()} tags to this archive?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
@ -1121,7 +1160,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.form_to_metadata()
|
||||
|
||||
success = self.comic_archive.write_metadata(self.metadata, self.save_data_style)
|
||||
self.comic_archive.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
self.comic_archive.load_cache(list(metadata_styles))
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
|
||||
if not success:
|
||||
@ -1137,7 +1176,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to commit!")
|
||||
|
||||
def set_load_data_style(self, s: int) -> None:
|
||||
def set_load_data_style(self, s: str) -> None:
|
||||
if self.dirty_flag_verification(
|
||||
"Change Tag Read Style", "If you change read tag style now, data in the form will be lost. Are you sure?"
|
||||
):
|
||||
@ -1151,118 +1190,35 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.adjust_load_style_combo()
|
||||
self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style)
|
||||
|
||||
def set_save_data_style(self, s: int) -> None:
|
||||
def set_save_data_style(self, s: str) -> None:
|
||||
self.save_data_style = self.cbSaveDataStyle.itemData(s)
|
||||
self.config[0].internal__save_data_style = self.save_data_style
|
||||
self.update_style_tweaks()
|
||||
self.update_metadata_style_tweaks()
|
||||
self.update_menus()
|
||||
|
||||
def set_source(self, s: int) -> None:
|
||||
self.config[0].Sources__source = self.cbx_sources.itemData(s)
|
||||
|
||||
def update_credit_colors(self) -> None:
|
||||
# !!!ATB qt5 porting TODO
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
active_palette = self.leSeries.palette()
|
||||
active_color = active_palette.color(QtGui.QPalette.ColorRole.Base)
|
||||
def update_metadata_credit_colors(self) -> None:
|
||||
style = metadata_styles[self.save_data_style]
|
||||
enabled = style.supported_attributes
|
||||
credit_attributes = [x for x in self.md_attributes.items() if "credits." in x[0]]
|
||||
|
||||
inactive_brush = QtGui.QBrush(inactive_color)
|
||||
active_brush = QtGui.QBrush(active_color)
|
||||
for r in range(self.twCredits.rowCount()):
|
||||
for credit in credit_attributes:
|
||||
widget_enabled = credit[0] in enabled
|
||||
widget = self.twCredits.item(r, credit[1])
|
||||
enable_widget(widget, widget_enabled)
|
||||
|
||||
cix_credits = ComicInfoXml().get_parseable_credits()
|
||||
|
||||
if self.save_data_style == MetaDataStyle.CIX:
|
||||
# loop over credit table, mark selected rows
|
||||
for r in range(self.twCredits.rowCount()):
|
||||
if str(self.twCredits.item(r, 1).text()).casefold() not in cix_credits:
|
||||
self.twCredits.item(r, 1).setBackground(inactive_brush)
|
||||
else:
|
||||
self.twCredits.item(r, 1).setBackground(active_brush)
|
||||
# turn off entire primary column
|
||||
self.twCredits.item(r, 0).setBackground(inactive_brush)
|
||||
|
||||
if self.save_data_style == MetaDataStyle.CBI:
|
||||
# loop over credit table, make all active color
|
||||
for r in range(self.twCredits.rowCount()):
|
||||
self.twCredits.item(r, 0).setBackground(active_brush)
|
||||
self.twCredits.item(r, 1).setBackground(active_brush)
|
||||
|
||||
def update_style_tweaks(self) -> None:
|
||||
def update_metadata_style_tweaks(self) -> None:
|
||||
# depending on the current data style, certain fields are disabled
|
||||
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
active_palette = self.leSeries.palette()
|
||||
enabled_widgets = metadata_styles[self.save_data_style].supported_attributes
|
||||
for metadata, widget in self.md_attributes.items():
|
||||
if isinstance(widget, QtWidgets.QWidget):
|
||||
enable_widget(widget, metadata in enabled_widgets)
|
||||
|
||||
inactive_palette1 = self.leSeries.palette()
|
||||
inactive_palette1.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
||||
|
||||
inactive_palette2 = self.leSeries.palette()
|
||||
|
||||
inactive_palette3 = self.leSeries.palette()
|
||||
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
||||
|
||||
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
||||
|
||||
# helper func
|
||||
def enable_widget(widget: QtWidgets.QWidget, enable: bool) -> None:
|
||||
inactive_palette3.setColor(widget.backgroundRole(), inactive_color)
|
||||
inactive_palette2.setColor(widget.backgroundRole(), inactive_color)
|
||||
inactive_palette3.setColor(widget.foregroundRole(), inactive_color)
|
||||
|
||||
if enable:
|
||||
widget.setPalette(active_palette)
|
||||
widget.setAutoFillBackground(False)
|
||||
if isinstance(widget, QtWidgets.QCheckBox):
|
||||
widget.setEnabled(True)
|
||||
elif isinstance(widget, QtWidgets.QComboBox):
|
||||
widget.setEnabled(True)
|
||||
elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
|
||||
widget.setReadOnly(False)
|
||||
else:
|
||||
widget.setAutoFillBackground(True)
|
||||
if isinstance(widget, QtWidgets.QCheckBox):
|
||||
widget.setPalette(inactive_palette2)
|
||||
widget.setEnabled(False)
|
||||
elif isinstance(widget, QtWidgets.QComboBox):
|
||||
widget.setPalette(inactive_palette3)
|
||||
widget.setEnabled(False)
|
||||
elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
|
||||
widget.setReadOnly(True)
|
||||
widget.setPalette(inactive_palette1)
|
||||
|
||||
cbi_only = [self.leVolumeCount, self.cbCountry, self.teTags]
|
||||
cix_only = [
|
||||
self.leImprint,
|
||||
self.teNotes,
|
||||
self.cbBW,
|
||||
self.cbManga,
|
||||
self.leStoryArc,
|
||||
self.leScanInfo,
|
||||
self.leSeriesGroup,
|
||||
self.leAltSeries,
|
||||
self.leAltIssueNum,
|
||||
self.leAltIssueCount,
|
||||
self.leWebLink,
|
||||
self.teCharacters,
|
||||
self.teTeams,
|
||||
self.teLocations,
|
||||
self.cbMaturityRating,
|
||||
self.cbFormat,
|
||||
]
|
||||
|
||||
if self.save_data_style == MetaDataStyle.CIX:
|
||||
for item in cix_only:
|
||||
enable_widget(item, True)
|
||||
for item in cbi_only:
|
||||
enable_widget(item, False)
|
||||
|
||||
if self.save_data_style == MetaDataStyle.CBI:
|
||||
for item in cbi_only:
|
||||
enable_widget(item, True)
|
||||
for item in cix_only:
|
||||
enable_widget(item, False)
|
||||
|
||||
self.update_credit_colors()
|
||||
self.update_metadata_credit_colors()
|
||||
self.page_list_editor.set_metadata_style(self.save_data_style)
|
||||
|
||||
def cell_double_clicked(self, r: int, c: int) -> None:
|
||||
@ -1353,7 +1309,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
row = self.twCredits.rowCount()
|
||||
self.add_new_credit_entry(row, new_role, new_name, new_primary)
|
||||
|
||||
self.update_credit_colors()
|
||||
self.update_metadata_credit_colors()
|
||||
self.set_dirty_flag()
|
||||
|
||||
def remove_credit(self) -> None:
|
||||
@ -1393,27 +1349,20 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def adjust_load_style_combo(self) -> None:
|
||||
# select the current style
|
||||
if self.load_data_style == MetaDataStyle.CBI:
|
||||
self.cbLoadDataStyle.setCurrentIndex(0)
|
||||
elif self.load_data_style == MetaDataStyle.CIX:
|
||||
self.cbLoadDataStyle.setCurrentIndex(1)
|
||||
self.cbLoadDataStyle.setCurrentIndex(self.cbLoadDataStyle.findData(self.load_data_style))
|
||||
|
||||
def adjust_save_style_combo(self) -> None:
|
||||
# select the current style
|
||||
if self.save_data_style == MetaDataStyle.CBI:
|
||||
self.cbSaveDataStyle.setCurrentIndex(0)
|
||||
elif self.save_data_style == MetaDataStyle.CIX:
|
||||
self.cbSaveDataStyle.setCurrentIndex(1)
|
||||
self.update_style_tweaks()
|
||||
self.cbSaveDataStyle.setCurrentIndex(self.cbSaveDataStyle.findData(self.save_data_style))
|
||||
self.update_metadata_style_tweaks()
|
||||
|
||||
def populate_combo_boxes(self) -> None:
|
||||
# Add the entries to the tag style combobox
|
||||
self.cbLoadDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI)
|
||||
self.cbLoadDataStyle.addItem("ComicRack", MetaDataStyle.CIX)
|
||||
self.adjust_load_style_combo()
|
||||
for style in metadata_styles.values():
|
||||
self.cbLoadDataStyle.addItem(style.name(), style.short_name)
|
||||
self.cbSaveDataStyle.addItem(style.name(), style.short_name)
|
||||
|
||||
self.cbSaveDataStyle.addItem("ComicBookLover", MetaDataStyle.CBI)
|
||||
self.cbSaveDataStyle.addItem("ComicRack", MetaDataStyle.CIX)
|
||||
self.adjust_load_style_combo()
|
||||
self.adjust_save_style_combo()
|
||||
|
||||
# Add talker entries
|
||||
@ -1514,13 +1463,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def remove_auto(self) -> None:
|
||||
self.remove_tags(self.save_data_style)
|
||||
|
||||
def remove_cbl_tags(self) -> None:
|
||||
self.remove_tags(MetaDataStyle.CBI)
|
||||
|
||||
def remove_cr_tags(self) -> None:
|
||||
self.remove_tags(MetaDataStyle.CIX)
|
||||
|
||||
def remove_tags(self, style: int) -> None:
|
||||
def remove_tags(self, style: str) -> None:
|
||||
# remove the indicated tags from the archive
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
has_md_count = 0
|
||||
@ -1530,7 +1473,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
if has_md_count == 0:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Remove Tags", f"No archives with {MetaDataStyle.name[style]} tags selected!"
|
||||
self, "Remove Tags", f"No archives with {metadata_styles[style].name()} tags selected!"
|
||||
)
|
||||
return
|
||||
|
||||
@ -1543,7 +1486,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"Remove Tags",
|
||||
f"Are you sure you wish to remove the {MetaDataStyle.name[style]} tags from {has_md_count} archive(s)?",
|
||||
f"Are you sure you wish to remove the {metadata_styles[style].name()} tags from {has_md_count} archive(s)?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
@ -1565,13 +1508,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
progdialog.setValue(prog_idx)
|
||||
progdialog.setLabelText(str(ca.path))
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if ca.has_metadata(style):
|
||||
if ca.is_writable():
|
||||
if not ca.remove_metadata(style):
|
||||
failed_list.append(ca.path)
|
||||
else:
|
||||
success_count += 1
|
||||
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
if ca.has_metadata(style) and ca.is_writable():
|
||||
if not ca.remove_metadata(style):
|
||||
failed_list.append(ca.path)
|
||||
else:
|
||||
success_count += 1
|
||||
ca.load_cache(list(metadata_styles))
|
||||
|
||||
progdialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1621,8 +1563,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"Copy Tags",
|
||||
f"Are you sure you wish to copy the {MetaDataStyle.name[src_style]}"
|
||||
+ f" tags to {MetaDataStyle.name[dest_style]} tags in {has_src_count} archive(s)?",
|
||||
f"Are you sure you wish to copy the {metadata_styles[src_style].name()}"
|
||||
+ f" tags to {metadata_styles[dest_style].name()} tags in {has_src_count} archive(s)?",
|
||||
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||
QtWidgets.QMessageBox.StandardButton.No,
|
||||
)
|
||||
@ -1649,10 +1591,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if ca.has_metadata(src_style) and ca.is_writable():
|
||||
md = ca.read_metadata(src_style)
|
||||
|
||||
if (
|
||||
dest_style == MetaDataStyle.CBI
|
||||
and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation
|
||||
):
|
||||
if dest_style == "cbi" and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation:
|
||||
md = CBLTransformer(md, self.config[0]).apply()
|
||||
|
||||
if not ca.write_metadata(md, dest_style):
|
||||
@ -1660,7 +1599,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
else:
|
||||
success_count += 1
|
||||
|
||||
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
ca.load_cache([self.load_data_style, self.save_data_style, src_style, dest_style])
|
||||
|
||||
prog_dialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1874,7 +1813,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
)
|
||||
success = True
|
||||
self.auto_tag_log("Save complete!\n")
|
||||
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
ca.load_cache([self.load_data_style, self.save_data_style])
|
||||
|
||||
return success, match_results
|
||||
|
||||
@ -1896,7 +1835,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.config[0],
|
||||
(
|
||||
f"You have selected {len(ca_list)} archive(s) to automatically identify and write "
|
||||
+ MetaDataStyle.name[style]
|
||||
+ metadata_styles[style].name()
|
||||
+ " tags to.\n\nPlease choose config below, and select OK to Auto-Tag."
|
||||
),
|
||||
)
|
||||
@ -2067,19 +2006,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def page_browser_closed(self) -> None:
|
||||
self.page_browser = None
|
||||
|
||||
def view_raw_cr_tags(self) -> None:
|
||||
if self.comic_archive is not None and self.comic_archive.has_cix():
|
||||
def view_raw_tags(self, style: str) -> None:
|
||||
if self.comic_archive is not None and self.comic_archive.has_metadata(style):
|
||||
md_style = metadata_styles[style]
|
||||
dlg = LogWindow(self)
|
||||
dlg.set_text(self.comic_archive.read_raw_cix())
|
||||
dlg.setWindowTitle("Raw ComicRack Tag View")
|
||||
dlg.exec()
|
||||
|
||||
def view_raw_cbl_tags(self) -> None:
|
||||
if self.comic_archive is not None and self.comic_archive.has_cbi():
|
||||
dlg = LogWindow(self)
|
||||
text = pprint.pformat(json.loads(self.comic_archive.read_raw_cbi()), indent=4)
|
||||
dlg.set_text(text)
|
||||
dlg.setWindowTitle("Raw ComicBookLover Tag View")
|
||||
dlg.set_text(self.comic_archive.read_metadata_string(style))
|
||||
dlg.setWindowTitle(f"Raw {md_style.name()} Tag View")
|
||||
dlg.exec()
|
||||
|
||||
def show_wiki(self) -> None:
|
||||
@ -2106,12 +2038,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def recalc_page_dimensions(self) -> None:
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
||||
for p in self.metadata.pages:
|
||||
if "ImageSize" in p:
|
||||
del p["ImageSize"]
|
||||
if "ImageHeight" in p:
|
||||
del p["ImageHeight"]
|
||||
if "ImageWidth" in p:
|
||||
del p["ImageWidth"]
|
||||
if "size" in p:
|
||||
del p["size"]
|
||||
if "height" in p:
|
||||
del p["height"]
|
||||
if "width" in p:
|
||||
del p["width"]
|
||||
self.set_dirty_flag()
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
|
||||
|
@ -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>
|
||||
|
@ -152,3 +152,64 @@ if qt_available:
|
||||
trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__))
|
||||
|
||||
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", msg + trace)
|
||||
|
||||
active_palette = None
|
||||
|
||||
def enable_widget(widget: QtWidgets.QWidget | list[QtWidgets.QWidget], enable: bool) -> None:
|
||||
if isinstance(widget, list):
|
||||
for w in widget:
|
||||
_enable_widget(w, enable)
|
||||
else:
|
||||
_enable_widget(widget, enable)
|
||||
|
||||
def _enable_widget(widget: QtWidgets.QWidget, enable: bool) -> None:
|
||||
global active_palette
|
||||
if not (widget and active_palette and widget):
|
||||
return
|
||||
active_color = active_palette.color(QtGui.QPalette.ColorRole.Base)
|
||||
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
inactive_brush = QtGui.QBrush(inactive_color)
|
||||
active_brush = QtGui.QBrush(active_color)
|
||||
|
||||
def palettes() -> tuple[QtGui.QPalette, QtGui.QPalette, QtGui.QPalette]:
|
||||
inactive_palette1 = QtGui.QPalette(active_palette)
|
||||
inactive_palette1.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
||||
|
||||
inactive_palette2 = QtGui.QPalette(active_palette)
|
||||
inactive_palette2.setColor(widget.backgroundRole(), inactive_color)
|
||||
|
||||
inactive_palette3 = QtGui.QPalette(active_palette)
|
||||
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
|
||||
inactive_palette3.setColor(widget.foregroundRole(), inactive_color)
|
||||
return inactive_palette1, inactive_palette2, inactive_palette3
|
||||
|
||||
if enable:
|
||||
if isinstance(widget, QtWidgets.QTableWidgetItem):
|
||||
widget.setBackground(active_brush)
|
||||
return
|
||||
|
||||
widget.setAutoFillBackground(False)
|
||||
widget.setPalette(active_palette)
|
||||
if isinstance(widget, (QtWidgets.QCheckBox, QtWidgets.QComboBox, QtWidgets.QPushButton)):
|
||||
widget.setEnabled(True)
|
||||
elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
|
||||
widget.setReadOnly(False)
|
||||
elif isinstance(widget, QtWidgets.QListWidget):
|
||||
widget.setMovement(QtWidgets.QListWidget.Free)
|
||||
else:
|
||||
if isinstance(widget, QtWidgets.QTableWidgetItem):
|
||||
widget.setBackground(inactive_brush)
|
||||
return
|
||||
|
||||
widget.setAutoFillBackground(True)
|
||||
if isinstance(widget, (QtWidgets.QCheckBox, QtWidgets.QComboBox, QtWidgets.QPushButton)):
|
||||
inactive_palette = palettes()
|
||||
widget.setPalette(inactive_palette[1])
|
||||
widget.setEnabled(False)
|
||||
elif isinstance(widget, (QtWidgets.QTextEdit, QtWidgets.QLineEdit, QtWidgets.QAbstractSpinBox)):
|
||||
inactive_palette = palettes()
|
||||
widget.setReadOnly(True)
|
||||
widget.setPalette(inactive_palette[0])
|
||||
elif isinstance(widget, QtWidgets.QListWidget):
|
||||
widget.setMovement(QtWidgets.QListWidget.Static)
|
||||
|
@ -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"/>
|
||||
|
@ -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 =
|
||||
|
@ -32,45 +32,45 @@ def test_getPageNameList():
|
||||
|
||||
|
||||
def test_page_type_read(cbz):
|
||||
md = cbz.read_cix()
|
||||
md = cbz.read_metadata("cr")
|
||||
|
||||
assert isinstance(md.pages[0]["Type"], str)
|
||||
assert isinstance(md.pages[0]["type"], str)
|
||||
|
||||
|
||||
def test_metadata_read(cbz, md_saved):
|
||||
md = cbz.read_cix()
|
||||
md = cbz.read_metadata("cr")
|
||||
assert md == md_saved
|
||||
|
||||
|
||||
def test_save_cix(tmp_comic):
|
||||
md = tmp_comic.read_cix()
|
||||
def test_save_cr(tmp_comic):
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
md.set_default_page_list(tmp_comic.get_number_of_pages())
|
||||
|
||||
assert tmp_comic.write_cix(md)
|
||||
assert tmp_comic.write_metadata(md, "cr")
|
||||
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
|
||||
|
||||
def test_save_cbi(tmp_comic):
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
md.set_default_page_list(tmp_comic.get_number_of_pages())
|
||||
|
||||
assert tmp_comic.write_cbi(md)
|
||||
assert tmp_comic.write_metadata(md, "cbi")
|
||||
|
||||
md = tmp_comic.read_cbi()
|
||||
md = tmp_comic.read_metadata("cbi")
|
||||
|
||||
|
||||
@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support")
|
||||
def test_save_cix_rar(tmp_path, md_saved):
|
||||
cbr_path = pathlib.Path(str(datadir)) / "fake_cbr.cbr"
|
||||
def test_save_cr_rar(tmp_path, md_saved):
|
||||
cbr_path = datadir / "fake_cbr.cbr"
|
||||
shutil.copy(cbr_path, tmp_path)
|
||||
|
||||
tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name)
|
||||
assert tmp_comic.seems_to_be_a_comic_archive()
|
||||
assert tmp_comic.write_cix(comicapi.genericmetadata.md_test)
|
||||
assert tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cr")
|
||||
|
||||
md = tmp_comic.read_cix()
|
||||
assert md.replace(pages=[], page_count=0) == md_saved.replace(pages=[], page_count=0)
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
assert md == md_saved
|
||||
|
||||
|
||||
@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support")
|
||||
@ -80,46 +80,28 @@ def test_save_cbi_rar(tmp_path, md_saved):
|
||||
|
||||
tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name)
|
||||
assert tmp_comic.seems_to_be_a_comic_archive()
|
||||
assert tmp_comic.write_cbi(comicapi.genericmetadata.md_test)
|
||||
assert tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cbi")
|
||||
|
||||
md = tmp_comic.read_cbi()
|
||||
assert md.replace(pages=[]) == md_saved.replace(
|
||||
pages=[],
|
||||
day=None,
|
||||
alternate_series=None,
|
||||
alternate_number=None,
|
||||
alternate_count=None,
|
||||
imprint=None,
|
||||
notes=None,
|
||||
web_link=None,
|
||||
format=None,
|
||||
manga=None,
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
scan_info=None,
|
||||
characters=set(),
|
||||
teams=set(),
|
||||
locations=set(),
|
||||
)
|
||||
md = tmp_comic.read_metadata("cbi")
|
||||
supported_attributes = comicapi.comicarchive.metadata_styles["cbi"].supported_attributes
|
||||
assert md.get_clean_metadata(*supported_attributes) == md_saved.get_clean_metadata(*supported_attributes)
|
||||
|
||||
|
||||
def test_page_type_save(tmp_comic):
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
t = md.pages[0]
|
||||
t["Type"] = ""
|
||||
t["type"] = ""
|
||||
|
||||
assert tmp_comic.write_cix(md)
|
||||
assert tmp_comic.write_metadata(md, "cr")
|
||||
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
|
||||
|
||||
def test_invalid_zip(tmp_comic):
|
||||
with open(tmp_comic.path, mode="b+r") as f:
|
||||
f.write(b"PK\000\000")
|
||||
|
||||
result = tmp_comic.write_cix(comicapi.genericmetadata.md_test)
|
||||
result = tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cr")
|
||||
assert not result
|
||||
|
||||
|
||||
@ -149,7 +131,7 @@ def test_copy_from_archive(archiver, tmp_path, cbz, md_saved):
|
||||
assert comic_archive.seems_to_be_a_comic_archive()
|
||||
assert set(cbz.archiver.get_filename_list()) == set(comic_archive.archiver.get_filename_list())
|
||||
|
||||
md = comic_archive.read_cix()
|
||||
md = comic_archive.read_metadata("cr")
|
||||
assert md == md_saved
|
||||
|
||||
|
||||
|
@ -12,7 +12,7 @@ def test_set_default_page_list(tmp_path):
|
||||
md.pages = []
|
||||
md.set_default_page_list(len(comicapi.genericmetadata.md_test.pages))
|
||||
|
||||
assert isinstance(md.pages[0]["Image"], int)
|
||||
assert isinstance(md.pages[0]["image_index"], int)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("replaced, expected", metadata)
|
||||
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import settngs
|
||||
|
||||
import comicapi.comicarchive
|
||||
import comicapi.comicinfoxml
|
||||
import comicapi.genericmetadata
|
||||
import comictaggerlib.resulttypes
|
||||
from comictaggerlib import ctsettings
|
||||
@ -19,9 +18,9 @@ def test_save(
|
||||
mock_now,
|
||||
) -> None:
|
||||
# Overwrite the series so it has definitely changed
|
||||
tmp_comic.write_cix(md_saved.replace(series="nothing"))
|
||||
tmp_comic.write_metadata(md_saved.replace(series="nothing"), "cr")
|
||||
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
|
||||
# Check that it changed
|
||||
assert md != md_saved
|
||||
@ -41,14 +40,14 @@ def test_save(
|
||||
# Use the temporary comic we created
|
||||
config[0].Runtime_Options__files = [tmp_comic.path]
|
||||
# Save ComicRack tags
|
||||
config[0].Runtime_Options__type = [comicapi.comicarchive.MetaDataStyle.CIX]
|
||||
config[0].Runtime_Options__type = ["cr"]
|
||||
# Search using the correct series since we just put the wrong series name in the CBZ
|
||||
config[0].Runtime_Options__metadata = comicapi.genericmetadata.GenericMetadata(series=md_saved.series)
|
||||
# Run ComicTagger
|
||||
CLI(config[0], talkers).run()
|
||||
|
||||
# Read the CBZ
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
|
||||
# Validate that we got the correct metadata back
|
||||
assert md == md_saved
|
||||
@ -61,7 +60,7 @@ def test_delete(
|
||||
md_saved,
|
||||
mock_now,
|
||||
) -> None:
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
|
||||
# Check that the metadata starts correct
|
||||
assert md == md_saved
|
||||
@ -79,16 +78,16 @@ def test_delete(
|
||||
# Use the temporary comic we created
|
||||
config[0].Runtime_Options__files = [tmp_comic.path]
|
||||
# Delete ComicRack tags
|
||||
config[0].Runtime_Options__type = [comicapi.comicarchive.MetaDataStyle.CIX]
|
||||
config[0].Runtime_Options__type = ["cr"]
|
||||
# Run ComicTagger
|
||||
CLI(config[0], talkers).run()
|
||||
|
||||
# Read the CBZ
|
||||
md = tmp_comic.read_cix()
|
||||
md = tmp_comic.read_metadata("cr")
|
||||
|
||||
# Currently we set the default page list on load
|
||||
empty_md = comicapi.genericmetadata.GenericMetadata()
|
||||
empty_md.set_default_page_list(tmp_comic.get_number_of_pages())
|
||||
# empty_md.set_default_page_list(tmp_comic.get_number_of_pages())
|
||||
|
||||
# Validate that we got an empty metadata back
|
||||
assert md == empty_md
|
||||
|
@ -1,64 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import comicapi.comicbookinfo
|
||||
import comicapi.comicinfoxml
|
||||
import pytest
|
||||
from importlib_metadata import entry_points
|
||||
|
||||
import comicapi.genericmetadata
|
||||
|
||||
metadata_styles = []
|
||||
|
||||
def test_cix(md_saved):
|
||||
CIX = comicapi.comicinfoxml.ComicInfoXml()
|
||||
string = CIX.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CIX.metadata_from_string(string)
|
||||
assert md == md_saved
|
||||
|
||||
|
||||
def test_cbi(md_saved):
|
||||
CBI = comicapi.comicbookinfo.ComicBookInfo()
|
||||
string = CBI.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CBI.metadata_from_string(string)
|
||||
md_test = md_saved.replace(
|
||||
day=None,
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
scan_info=None,
|
||||
characters=set(),
|
||||
teams=set(),
|
||||
locations=set(),
|
||||
pages=[],
|
||||
alternate_series=None,
|
||||
alternate_number=None,
|
||||
alternate_count=None,
|
||||
imprint=None,
|
||||
notes=None,
|
||||
web_link=None,
|
||||
format=None,
|
||||
manga=None,
|
||||
for x in entry_points(group="comicapi.metadata"):
|
||||
meetadata = x.load()
|
||||
supported = meetadata.enabled
|
||||
exe_found = True
|
||||
metadata_styles.append(
|
||||
pytest.param(meetadata, marks=pytest.mark.xfail(not supported, reason="metadata not enabled"))
|
||||
)
|
||||
assert md == md_test
|
||||
|
||||
|
||||
def test_comet(md_saved):
|
||||
CBI = comicapi.comet.CoMet()
|
||||
string = CBI.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CBI.metadata_from_string(string)
|
||||
md_test = md_saved.replace(
|
||||
day=None,
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
scan_info=None,
|
||||
teams=set(),
|
||||
locations=set(),
|
||||
pages=[],
|
||||
alternate_series=None,
|
||||
alternate_number=None,
|
||||
alternate_count=None,
|
||||
imprint=None,
|
||||
notes=None,
|
||||
web_link=None,
|
||||
manga=None,
|
||||
critical_rating=None,
|
||||
issue_count=None,
|
||||
)
|
||||
assert md == md_test
|
||||
@pytest.mark.parametrize("metadata", metadata_styles)
|
||||
def test_metadata(mock_version, tmp_comic, md, metadata):
|
||||
md_style = metadata(mock_version[0])
|
||||
supported_attributes = md_style.supported_attributes
|
||||
md_style.set_metadata(comicapi.genericmetadata.md_test, tmp_comic.archiver)
|
||||
written_metadata = md_style.get_metadata(tmp_comic.archiver)
|
||||
assert written_metadata.get_clean_metadata(*supported_attributes) == md.get_clean_metadata(*supported_attributes)
|
||||
|
Loading…
x
Reference in New Issue
Block a user