Compare commits

..

No commits in common. "69a9566f4281db221954282424345912c9b559fc" and "0198eb9e2bd2717015792b7aead7ff10d76b8a74" have entirely different histories.

49 changed files with 1044 additions and 20815 deletions

View File

@ -1,4 +1,4 @@
exclude: ^(scripts|comictaggerlib/graphics/resources.py)
exclude: ^scripts
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0

View File

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

View File

@ -30,94 +30,97 @@ from typing import TYPE_CHECKING
from comicapi import utils
from comicapi.archivers import Archiver, UnknownArchiver, ZipArchiver
from comicapi.genericmetadata import GenericMetadata
from comicapi.tags import Tag
from comicapi.metadata import Metadata
from comictaggerlib.ctversion import version
if TYPE_CHECKING:
from importlib.machinery import ModuleSpec
from importlib.metadata import EntryPoint
logger = logging.getLogger(__name__)
archivers: list[type[Archiver]] = []
tags: dict[str, Tag] = {}
metadata_styles: dict[str, Metadata] = {}
def load_archive_plugins(local_plugins: Iterable[EntryPoint] = tuple()) -> None:
if archivers:
return
if sys.version_info < (3, 10):
from importlib_metadata import entry_points
else:
from importlib.metadata import entry_points
builtin: list[type[Archiver]] = []
archive_plugins: list[type[Archiver]] = []
# A list is used first matching plugin wins
if not archivers:
if sys.version_info < (3, 10):
from importlib_metadata import entry_points
else:
from importlib.metadata import entry_points
builtin: list[type[Archiver]] = []
# A list is used first matching plugin wins
for ep in itertools.chain(local_plugins, entry_points(group="comicapi.archiver")):
try:
archiver: type[Archiver] = ep.load()
if ep.module.startswith("comicapi"):
builtin.append(archiver)
else:
archivers.append(archiver)
except Exception:
try:
spec = importlib.util.find_spec(ep.module)
except ValueError:
spec = None
if spec and spec.has_location:
logger.exception("Failed to load archive plugin: %s from %s", ep.name, spec.origin)
else:
logger.exception("Failed to load archive plugin: %s", ep.name)
archivers.extend(builtin)
for ep in itertools.chain(local_plugins, entry_points(group="comicapi.archiver")):
try:
spec = importlib.util.find_spec(ep.module)
except ValueError:
spec = None
try:
archiver: type[Archiver] = ep.load()
if ep.module.startswith("comicapi"):
builtin.append(archiver)
else:
archive_plugins.append(archiver)
except Exception:
def load_metadata_plugins(version: str = f"ComicAPI/{version}", local_plugins: Iterable[EntryPoint] = tuple()) -> 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, tuple[Metadata, ModuleSpec | None]] = {}
# A dict is used, last plugin wins
for ep in itertools.chain(entry_points(group="comicapi.metadata"), local_plugins):
try:
spec = importlib.util.find_spec(ep.module)
except ValueError:
spec = None
try:
style: type[Metadata] = ep.load()
if style.enabled:
if ep.module.startswith("comicapi"):
builtin[style.short_name] = style(version)
else:
if style.short_name in styles:
if spec and spec.has_location:
logger.warning(
"Plugin %s from %s is overriding the existing metadata plugin for %s tags",
ep.module,
spec.origin,
style.short_name,
)
else:
logger.warning(
"Plugin %s is overriding the existing metadata plugin for %s tags",
ep.module,
style.short_name,
)
styles[style.short_name] = (style(version), spec)
except Exception:
if spec and spec.has_location:
logger.exception("Failed to load metadata plugin: %s from %s", ep.name, spec.origin)
else:
logger.exception("Failed to load metadata plugin: %s", ep.name)
for style_name in set(builtin.keys()).intersection(styles):
spec = styles[style_name][1]
if spec and spec.has_location:
logger.exception("Failed to load archive plugin: %s from %s", ep.name, spec.origin)
logger.warning(
"Builtin metadata for %s tags are being overridden by a plugin from %s", style_name, spec.origin
)
else:
logger.exception("Failed to load archive plugin: %s", ep.name)
archivers.clear()
archivers.extend(archive_plugins)
archivers.extend(builtin)
def load_tag_plugins(version: str = f"ComicAPI/{version}", local_plugins: Iterable[EntryPoint] = tuple()) -> None:
if tags:
return
if sys.version_info < (3, 10):
from importlib_metadata import entry_points
else:
from importlib.metadata import entry_points
builtin: dict[str, Tag] = {}
tag_plugins: dict[str, tuple[Tag, str]] = {}
# A dict is used, last plugin wins
for ep in itertools.chain(entry_points(group="comicapi.tags"), local_plugins):
location = "Unknown"
try:
_spec = importlib.util.find_spec(ep.module)
if _spec and _spec.has_location and _spec.origin:
location = _spec.origin
except ValueError:
location = "Unknown"
try:
tag: type[Tag] = ep.load()
if ep.module.startswith("comicapi"):
builtin[tag.id] = tag(version)
else:
if tag.id in tag_plugins:
logger.warning(
"Plugin %s from %s is overriding the existing plugin for %s tags",
ep.module,
location,
tag.id,
)
tag_plugins[tag.id] = (tag(version), location)
except Exception:
logger.exception("Failed to load tag plugin: %s from %s", ep.name, location)
for tag_id in set(builtin.keys()).intersection(tag_plugins):
location = tag_plugins[tag_id][1]
logger.warning("Builtin plugin for %s tags are being overridden by a plugin from %s", tag_id, location)
tags.clear()
tags.update(builtin)
tags.update({s[0]: s[1][0] for s in tag_plugins.items()})
logger.warning("Builtin metadata for %s tags are being overridden by a plugin", style_name)
metadata_styles.clear()
metadata_styles.update(builtin)
metadata_styles.update({s[0]: s[1][0] for s in styles.items()})
class ComicArchive:
@ -142,7 +145,7 @@ class ComicArchive:
self.archiver = UnknownArchiver.open(self.path)
load_archive_plugins()
load_tag_plugins()
load_metadata_plugins()
for archiver in archivers:
if archiver.enabled and archiver.is_valid(self.path):
self.archiver = archiver.open(self.path)
@ -159,19 +162,15 @@ class ComicArchive:
self.page_list.clear()
self.md.clear()
def load_cache(self, tag_ids: Iterable[str]) -> None:
for tag_id in tag_ids:
if tag_id not in tags:
continue
tag = tags[tag_id]
if not tag.enabled:
continue
md = tag.read_tags(self.archiver)
if not md.is_empty:
self.md[tag_id] = md
def load_cache(self, style_list: Iterable[str]) -> None:
for style in style_list:
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_tags(self) -> list[str]:
return [tag_id for tag_id, tag in tags.items() if tag.enabled and tag.supports_tags(self.archiver)]
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()
@ -194,6 +193,11 @@ class ComicArchive:
return True
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"
@ -206,42 +210,33 @@ class ComicArchive:
def extension(self) -> str:
return self.archiver.extension()
def read_tags(self, tag_id: str) -> GenericMetadata:
if tag_id in self.md:
return self.md[tag_id]
def read_metadata(self, style: str) -> GenericMetadata:
if style in self.md:
return self.md[style]
md = GenericMetadata()
tag = tags[tag_id]
if tag.enabled and tag.has_tags(self.archiver):
md = tag.read_tags(self.archiver)
if metadata_styles[style].has_metadata(self.archiver):
md = metadata_styles[style].get_metadata(self.archiver)
md.apply_default_page_list(self.get_page_name_list())
return md
def read_raw_tags(self, tag_id: str) -> str:
if not tags[tag_id].enabled:
return ""
return tags[tag_id].read_raw_tags(self.archiver)
def read_metadata_string(self, style: str) -> str:
return metadata_styles[style].get_metadata_string(self.archiver)
def write_tags(self, metadata: GenericMetadata, tag_id: str) -> bool:
if tag_id in self.md:
del self.md[tag_id]
if not tags[tag_id].enabled:
return False
def write_metadata(self, metadata: GenericMetadata, style: str) -> bool:
if style in self.md:
del self.md[style]
metadata.apply_default_page_list(self.get_page_name_list())
return tags[tag_id].write_tags(metadata, self.archiver)
return metadata_styles[style].set_metadata(metadata, self.archiver)
def has_tags(self, tag_id: str) -> bool:
if tag_id in self.md:
def has_metadata(self, style: str) -> bool:
if style in self.md:
return True
if not tags[tag_id].enabled:
return False
return tags[tag_id].has_tags(self.archiver)
return metadata_styles[style].has_metadata(self.archiver)
def remove_tags(self, tag_id: str) -> bool:
if tag_id in self.md:
del self.md[tag_id]
if not tags[tag_id].enabled:
return False
return tags[tag_id].remove_tags(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""

View File

@ -92,7 +92,7 @@ class ComicSeries:
return copy.deepcopy(self)
class MetadataOrigin(NamedTuple):
class TagOrigin(NamedTuple):
id: str
name: str
@ -109,7 +109,7 @@ class GenericMetadata:
translator_synonyms = ("translator", "translation")
is_empty: bool = True
data_origin: MetadataOrigin | None = None
tag_origin: TagOrigin | None = None
issue_id: str | None = None
series_id: str | None = None
@ -233,7 +233,7 @@ class GenericMetadata:
if not new_md.is_empty:
self.is_empty = False
self.data_origin = assign(self.data_origin, new_md.data_origin) # TODO use and purpose now?
self.tag_origin = assign(self.tag_origin, new_md.tag_origin) # TODO use and purpose now?
self.issue_id = assign(self.issue_id, new_md.issue_id)
self.series_id = assign(self.series_id, new_md.series_id)
@ -471,7 +471,7 @@ class GenericMetadata:
md_test: GenericMetadata = GenericMetadata(
is_empty=False,
data_origin=MetadataOrigin("comicvine", "Comic Vine"),
tag_origin=TagOrigin("comicvine", "Comic Vine"),
series="Cory Doctorow's Futuristic Tales of the Here and Now",
series_id="23437",
issue="1",
@ -573,6 +573,6 @@ __all__ = (
"ImageMetadata",
"Credit",
"ComicSeries",
"MetadataOrigin",
"TagOrigin",
"GenericMetadata",
)

View File

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

View File

@ -25,15 +25,15 @@ from comicapi import utils
from comicapi.archivers import Archiver
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata, ImageMetadata, PageType
from comicapi.tags import Tag
from comicapi.metadata import Metadata
logger = logging.getLogger(__name__)
class CoMet(Tag):
class CoMet(Metadata):
enabled = True
id = "comet"
short_name = "comet"
def __init__(self, version: str) -> None:
super().__init__(version)
@ -71,13 +71,13 @@ class CoMet(Tag):
def supports_credit_role(self, role: str) -> bool:
return role.casefold() in self._get_parseable_credits()
def supports_tags(self, archive: Archiver) -> bool:
def supports_metadata(self, archive: Archiver) -> bool:
return archive.supports_files()
def has_tags(self, archive: Archiver) -> bool:
if not self.supports_tags(archive):
def has_metadata(self, archive: Archiver) -> bool:
if not self.supports_metadata(archive):
return False
has_tags = 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":
@ -90,33 +90,33 @@ class CoMet(Tag):
if self._validate_bytes(data):
# since we found it, save it!
self.file = n
has_tags = True
has_metadata = True
break
return has_tags
return has_metadata
def remove_tags(self, archive: Archiver) -> bool:
return self.has_tags(archive) and archive.remove_file(self.file)
def remove_metadata(self, archive: Archiver) -> bool:
return self.has_metadata(archive) and archive.remove_file(self.file)
def read_tags(self, archive: Archiver) -> GenericMetadata:
if self.has_tags(archive):
def get_metadata(self, archive: Archiver) -> GenericMetadata:
if self.has_metadata(archive):
metadata = archive.read_file(self.file) or b""
if self._validate_bytes(metadata):
return self._metadata_from_bytes(metadata, archive)
return GenericMetadata()
def read_raw_tags(self, archive: Archiver) -> str:
if self.has_tags(archive):
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 write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_tags(archive):
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_metadata(archive):
success = True
xml = b""
if self.has_tags(archive):
if self.has_metadata(archive):
xml = archive.read_file(self.file)
if self.file != self.comet_filename:
success = self.remove_tags(archive)
success = self.remove_metadata(archive)
return success and archive.write_file(self.comet_filename, self._bytes_from_metadata(metadata, xml))
else:

View File

@ -23,7 +23,7 @@ from typing import Any, Literal, TypedDict
from comicapi import utils
from comicapi.archivers import Archiver
from comicapi.genericmetadata import Credit, GenericMetadata
from comicapi.tags import Tag
from comicapi.metadata import Metadata
logger = logging.getLogger(__name__)
@ -75,10 +75,10 @@ class _ComicBookInfoJson(TypedDict, total=False):
_CBIContainer = TypedDict("_CBIContainer", {"appID": str, "lastModified": str, "ComicBookInfo/1.0": _ComicBookInfoJson})
class ComicBookInfo(Tag):
class ComicBookInfo(Metadata):
enabled = True
id = "cbi"
short_name = "cbi"
def __init__(self, version: str) -> None:
super().__init__(version)
@ -108,29 +108,29 @@ class ComicBookInfo(Tag):
def supports_credit_role(self, role: str) -> bool:
return True
def supports_tags(self, archive: Archiver) -> bool:
def supports_metadata(self, archive: Archiver) -> bool:
return archive.supports_comment()
def has_tags(self, archive: Archiver) -> bool:
return self.supports_tags(archive) and self._validate_string(archive.get_comment())
def has_metadata(self, archive: Archiver) -> bool:
return self.supports_metadata(archive) and self._validate_string(archive.get_comment())
def remove_tags(self, archive: Archiver) -> bool:
def remove_metadata(self, archive: Archiver) -> bool:
return archive.set_comment("")
def read_tags(self, archive: Archiver) -> GenericMetadata:
if self.has_tags(archive):
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 read_raw_tags(self, archive: Archiver) -> str:
if self.has_tags(archive):
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 write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_tags(archive):
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")

View File

@ -23,15 +23,15 @@ from typing import Any
from comicapi import utils
from comicapi.archivers import Archiver
from comicapi.genericmetadata import GenericMetadata, ImageMetadata
from comicapi.tags import Tag
from comicapi.metadata import Metadata
logger = logging.getLogger(__name__)
class ComicRack(Tag):
class ComicRack(Metadata):
enabled = True
id = "cr"
short_name = "cr"
def __init__(self, version: str) -> None:
super().__init__(version)
@ -84,35 +84,35 @@ class ComicRack(Tag):
def supports_credit_role(self, role: str) -> bool:
return role.casefold() in self._get_parseable_credits()
def supports_tags(self, archive: Archiver) -> bool:
def supports_metadata(self, archive: Archiver) -> bool:
return archive.supports_files()
def has_tags(self, archive: Archiver) -> bool:
def has_metadata(self, archive: Archiver) -> bool:
return (
self.supports_tags(archive)
self.supports_metadata(archive)
and self.file in archive.get_filename_list()
and self._validate_bytes(archive.read_file(self.file))
)
def remove_tags(self, archive: Archiver) -> bool:
return self.has_tags(archive) and archive.remove_file(self.file)
def remove_metadata(self, archive: Archiver) -> bool:
return self.has_metadata(archive) and archive.remove_file(self.file)
def read_tags(self, archive: Archiver) -> GenericMetadata:
if self.has_tags(archive):
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 read_raw_tags(self, archive: Archiver) -> str:
if self.has_tags(archive):
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 write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_tags(archive):
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_metadata(archive):
xml = b""
if self.has_tags(archive):
if self.has_metadata(archive):
xml = archive.read_file(self.file)
return archive.write_file(self.file, self._bytes_from_metadata(metadata, xml))
else:

View File

@ -4,14 +4,14 @@ from comicapi.archivers import Archiver
from comicapi.genericmetadata import GenericMetadata
class Tag:
class Metadata:
enabled: bool = False
id: str = ""
short_name: str = ""
def __init__(self, version: str) -> None:
self.version: str = version
self.supported_attributes = {
"data_origin",
"tag_origin",
"issue_id",
"series_id",
"series",
@ -71,44 +71,44 @@ class Tag:
def supports_credit_role(self, role: str) -> bool:
return False
def supports_tags(self, archive: Archiver) -> bool:
def supports_metadata(self, archive: Archiver) -> bool:
"""
Checks the given archive for the ability to save these tags.
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_tags(self, archive: Archiver) -> bool:
def has_metadata(self, archive: Archiver) -> bool:
"""
Checks the given archive for tags.
Checks the given archive for metadata.
Should always return a bool. Failures should return False.
"""
return False
def remove_tags(self, archive: Archiver) -> bool:
def remove_metadata(self, archive: Archiver) -> bool:
"""
Removes the tags from the given archive.
Removes the metadata from the given archive.
Should always return a bool. Failures should return False.
"""
return False
def read_tags(self, archive: Archiver) -> GenericMetadata:
def get_metadata(self, archive: Archiver) -> GenericMetadata:
"""
Returns a GenericMetadata representing the tags saved in the given archive.
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 read_raw_tags(self, archive: Archiver) -> str:
def get_metadata_string(self, archive: Archiver) -> str:
"""
Returns the raw tags as a string.
If the tags are a binary format a roughly similar text format should be used.
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 write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
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.
@ -117,7 +117,7 @@ class Tag:
def name(self) -> str:
"""
Returns the name of these tags for display purposes eg "Comic Rack".
Returns the name of this metadata for display purposes eg "Comic Rack".
Should always return a string. Failures should return the empty string.
"""
return ""

View File

@ -1,5 +0,0 @@
from __future__ import annotations
from comicapi.tags.tag import Tag
__all__ = ["Tag"]

View File

@ -32,9 +32,10 @@ from comicfn2dict import comicfn2dict
import comicapi.data
from comicapi import filenamelexer, filenameparser
from comicapi._url import LocationParseError as LocationParseError # noqa: F401
from comicapi._url import Url as Url
from comicapi._url import parse_url as parse_url
from ._url import LocationParseError as LocationParseError # noqa: F401
from ._url import Url as Url
from ._url import parse_url as parse_url
try:
import icu

View File

@ -22,7 +22,7 @@ from typing import Callable
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive, tags
from comicapi.comicarchive import ComicArchive, metadata_styles
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ctsettings import ct_ns
@ -40,7 +40,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
self,
parent: QtWidgets.QWidget,
match_set_list: list[Result],
read_tags: list[str],
load_styles: list[str],
fetch_func: Callable[[IssueResult], GenericMetadata],
config: ct_ns,
talker: ComicTalker,
@ -82,7 +82,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setText("Accept and Write Tags")
self.match_set_list = match_set_list
self._tags = read_tags
self._styles = load_styles
self.fetch_func = fetch_func
self.current_match_set_idx = 0
@ -230,14 +230,14 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
def save_match(self) -> None:
match = self.current_match()
ca = ComicArchive(self.current_match_set.original_path)
md, error = self.parent().read_all_tags(self._tags, ca)
md, error = self.parent().overlay_ca_read_style(self.load_data_styles, ca)
if error is not None:
logger.error("Failed to load tags for %s: %s", ca.path, error)
logger.error("Failed to load metadata for %s: %s", ca.path, error)
QtWidgets.QApplication.restoreOverrideCursor()
QtWidgets.QMessageBox.critical(
self,
"Read Failed!",
f"One or more of the read tags failed to load for {ca.path}, check log for details",
f"One or more of the read styles failed to load for {ca.path}, check log for details",
)
return
@ -264,14 +264,14 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
md = prepare_metadata(md, ct_md, self.config)
for tag_id in self._tags:
success = ca.write_tags(md, tag_id)
for style in self._styles:
success = ca.write_metadata(md, style)
QtWidgets.QApplication.restoreOverrideCursor()
if not success:
QtWidgets.QMessageBox.warning(
self,
"Write Error",
f"Saving {tags[tag_id].name()} the tags to the archive seemed to fail!",
f"Saving {metadata_styles[style].name()} the tags to the archive seemed to fail!",
)
break

View File

@ -22,13 +22,13 @@ import json
import logging
import os
import pathlib
import re
import sys
from collections.abc import Collection
from typing import Any, TextIO
from comicapi import merge, utils
from comicapi.comicarchive import ComicArchive, tags
from comicapi.comicarchive import ComicArchive
from comicapi.comicarchive import metadata_styles as md_styles
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.cbltransformer import CBLTransformer
from comictaggerlib.ctsettings import ct_ns
@ -119,7 +119,7 @@ class CLI:
)
return return_code
def fetch_metadata(self, issue_id: str) -> GenericMetadata:
def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata:
# now get the particular issue data
try:
ct_md = self.current_talker().fetch_comic_data(issue_id)
@ -132,12 +132,12 @@ class CLI:
return ct_md
def write_tags(self, ca: ComicArchive, md: GenericMetadata) -> bool:
def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool:
if not self.config.Runtime_Options__dryrun:
for tag_id in self.config.Runtime_Options__tags_write:
for style in self.config.Runtime_Options__type_modify:
# write out the new data
if not ca.write_tags(md, tag_id):
logger.error("The tag save seemed to fail for: %s!", tags[tag_id].name())
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.")
@ -177,12 +177,12 @@ class CLI:
# save the data!
# we know at this point, that the file is all good to go
ca = ComicArchive(match_set.original_path)
md, match_set.tags_read = self.create_local_metadata(ca, self.config.Runtime_Options__tags_read)
ct_md = self.fetch_metadata(match_set.online_results[int(i) - 1].issue_id)
md = self.create_local_metadata(ca)
ct_md = self.actual_issue_data_fetch(match_set.online_results[int(i) - 1].issue_id)
match_set.md = prepare_metadata(md, ct_md, self.config)
self.write_tags(ca, md)
self.actual_metadata_save(ca, md)
def post_process_matches(self, match_results: OnlineMatchResults) -> None:
def print_header(header: str) -> None:
@ -231,14 +231,12 @@ class CLI:
self.display_match_set_for_choice(label, match_set)
def create_local_metadata(
self, ca: ComicArchive, tags_to_read: list[str], /, tags_only: bool = False
) -> tuple[GenericMetadata, list[str]]:
def create_local_metadata(self, ca: ComicArchive) -> GenericMetadata:
md = GenericMetadata()
md.apply_default_page_list(ca.get_page_name_list())
# now, overlay the parsed filename info
if self.config.Auto_Tag__parse_filename and not tags_only:
if self.config.Auto_Tag__parse_filename:
f_md = ca.metadata_from_filename(
self.config.Filename_Parsing__filename_parser,
self.config.Filename_Parsing__remove_c2c,
@ -251,29 +249,24 @@ class CLI:
md.overlay(f_md)
tags_used = []
for tag_id in tags_to_read:
if ca.has_tags(tag_id):
for style in self.config.Runtime_Options__type_read:
if ca.has_metadata(style):
try:
t_md = ca.read_tags(tag_id)
if not t_md.is_empty:
md.overlay(
t_md,
self.config.Metadata_Options__tag_merge,
self.config.Metadata_Options__tag_merge_lists,
)
tags_used.append(tag_id)
t_md = ca.read_metadata(style)
md.overlay(
t_md, self.config.Metadata_Options__comic_merge, self.config.Metadata_Options__comic_merge_lists
)
break
except Exception as e:
logger.error("Failed to read tags from %s: %s", ca.path, e)
logger.error("Failed to load metadata for %s: %s", ca.path, e)
if not tags_only:
# finally, use explicit stuff (always 'overlay' mode)
md.overlay(self.config.Auto_Tag__metadata, mode=merge.Mode.OVERLAY, merge_lists=True)
# finally, use explicit stuff (always 'overlay' mode)
md.overlay(self.config.Auto_Tag__metadata, mode=merge.Mode.OVERLAY, merge_lists=True)
return (md, tags_used)
return md
def print(self, ca: ComicArchive) -> Result:
if not self.config.Runtime_Options__tags_read:
if not self.config.Runtime_Options__type_read:
page_count = ca.get_number_of_pages()
brief = ""
@ -286,8 +279,8 @@ class CLI:
brief += f"({page_count: >3} pages)"
brief += " tags:[ "
tag_names = [tags[tag_id].name() for tag_id in tags if ca.has_tags(tag_id)]
brief += " ".join(tag_names)
metadata_styles = [md_styles[style].name() for style in md_styles if ca.has_metadata(style)]
brief += " ".join(metadata_styles)
brief += " ]"
self.output(brief)
@ -298,110 +291,105 @@ class CLI:
self.output()
md = None
for tag_id, tag in tags.items():
if not self.config.Runtime_Options__tags_read or tag_id in self.config.Runtime_Options__tags_read:
if ca.has_tags(tag_id):
self.output(f"--------- {tag.name()} tags ---------")
for style, style_obj in md_styles.items():
if not self.config.Runtime_Options__type_read or style in self.config.Runtime_Options__type_read:
if ca.has_metadata(style):
self.output(f"--------- {style_obj.name()} tags ---------")
try:
if self.config.Runtime_Options__raw:
self.output(ca.read_raw_tags(tag_id))
self.output(ca.read_metadata_string(style))
else:
md = ca.read_tags(tag_id)
md = ca.read_metadata(style)
self.output(md)
except Exception as e:
logger.error("Failed to read tags from %s: %s", ca.path, 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_tags(self, ca: ComicArchive, tag_id: str) -> Status:
tag_name = tags[tag_id].name()
def delete_style(self, ca: ComicArchive, style: str) -> Status:
style_name = md_styles[style].name()
if ca.has_tags(tag_id):
if ca.has_metadata(style):
if not self.config.Runtime_Options__dryrun:
if ca.remove_tags(tag_id):
self.output(f"{ca.path}: Removed {tag_name} tags.")
if ca.remove_metadata(style):
self.output(f"{ca.path}: Removed {style_name} tags.")
return Status.success
else:
self.output(f"{ca.path}: Tag removal seemed to fail!")
return Status.write_failure
else:
self.output(f"{ca.path}: dry-run. {tag_name} tags not removed")
self.output(f"{ca.path}: dry-run. {style_name} tags not removed")
return Status.success
self.output(f"{ca.path}: This archive doesn't have {tag_name} tags to remove.")
self.output(f"{ca.path}: This archive doesn't have {style_name} tags to remove.")
return Status.success
def delete(self, ca: ComicArchive) -> Result:
res = Result(Action.delete, Status.success, ca.path)
for tag_id in self.config.Runtime_Options__tags_write:
status = self.delete_tags(ca, tag_id)
for style in self.config.Runtime_Options__type_modify:
status = self.delete_style(ca, style)
if status == Status.success:
res.tags_deleted.append(tag_id)
res.tags_deleted.append(style)
else:
res.status = status
return res
def _copy_tags(self, ca: ComicArchive, md: GenericMetadata, source_names: str, dst_tag_id: str) -> Status:
dst_tag_name = tags[dst_tag_id].name()
if not self.config.Runtime_Options__skip_existing_tags and ca.has_tags(dst_tag_id):
self.output(f"{ca.path}: Already has {dst_tag_name} tags. Not overwriting.")
return Status.existing_tags
if len(self.config.Commands__copy) == 1 and dst_tag_id in self.config.Commands__copy:
self.output(f"{ca.path}: Destination and source are same: {dst_tag_name}. Nothing to do.")
def copy_style(self, ca: ComicArchive, md: GenericMetadata, style: str) -> Status:
dst_style_name = md_styles[style].name()
if not self.config.Runtime_Options__skip_existing_metadata and ca.has_metadata(style):
self.output(f"{ca.path}: Already has {dst_style_name} tags. Not overwriting.")
return Status.existing_tags
if self.config.Commands__copy == style:
self.output(f"{ca.path}: Destination and source are same: {dst_style_name}. Nothing to do.")
return Status.existing_tags
src_style_name = md_styles[self.config.Commands__copy].name()
if not self.config.Runtime_Options__dryrun:
if self.config.Metadata_Options__apply_transform_on_bulk_operation and dst_tag_id == "cbi":
if self.config.Metadata_Options__apply_transform_on_bulk_operation and style == "cbi":
md = CBLTransformer(md, self.config).apply()
if ca.write_tags(md, dst_tag_id):
self.output(f"{ca.path}: Copied {source_names} tags to {dst_tag_name}.")
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. {source_names} tags not copied")
return Status.success
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)
src_tag_names = []
for src_tag_id in self.config.Commands__copy:
src_tag_names.append(tags[src_tag_id].name())
if ca.has_tags(src_tag_id):
res.tags_read.append(src_tag_id)
if not res.tags_read:
self.output(f"{ca.path}: This archive doesn't have any {', '.join(src_tag_names)} tags to copy.")
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, res.tags_read = self.create_local_metadata(ca, res.tags_read, tags_only=True)
res.md = ca.read_metadata(self.config.Commands__copy)
except Exception as e:
logger.error("Failed to read tags from %s: %s", ca.path, e)
logger.error("Failed to load metadata for %s: %s", ca.path, e)
return res
for dst_tag_id in self.config.Runtime_Options__tags_write:
if dst_tag_id in self.config.Commands__copy:
for style in self.config.Runtime_Options__type_modify:
if style == src_style_name:
continue
status = self._copy_tags(ca, res.md, ", ".join(src_tag_names), dst_tag_id)
status = self.copy_style(ca, res.md, style)
if status == Status.success:
res.tags_written.append(dst_tag_id)
res.tags_written.append(style)
else:
res.status = status
return res
def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> tuple[Result, OnlineMatchResults]:
if not self.config.Runtime_Options__skip_existing_tags:
for tag_id in self.config.Runtime_Options__tags_write:
if ca.has_tags(tag_id):
self.output(f"{ca.path}: Already has {tags[tag_id].name()} tags. Not overwriting.")
if not self.config.Runtime_Options__skip_existing_metadata:
for style in self.config.Runtime_Options__type_modify:
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,
status=Status.existing_tags,
tags_written=self.config.Runtime_Options__tags_write,
tags_written=self.config.Runtime_Options__type_modify,
),
match_results,
)
@ -409,7 +397,7 @@ class CLI:
if self.batch_mode:
self.output(f"Processing {utils.path_to_short_str(ca.path)}...")
md, tags_read = self.create_local_metadata(ca, self.config.Runtime_Options__tags_read)
md = self.create_local_metadata(ca)
if md.issue is None or md.issue == "":
if self.config.Auto_Tag__assume_issue_one:
md.issue = "1"
@ -429,8 +417,7 @@ class CLI:
Action.save,
original_path=ca.path,
status=Status.fetch_data_failure,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.fetch_data_failures.append(res)
return res, match_results
@ -442,8 +429,7 @@ class CLI:
status=Status.match_failure,
original_path=ca.path,
match_status=MatchStatus.no_match,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.no_matches.append(res)
return res, match_results
@ -456,8 +442,7 @@ class CLI:
status=Status.match_failure,
original_path=ca.path,
match_status=MatchStatus.no_match,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.no_matches.append(res)
return res, match_results
@ -469,10 +454,10 @@ class CLI:
self.output(text)
ii.set_output_function(functools.partial(self.output, already_logged=True))
# use our overlaid MD to search
if not self.config.Auto_Tag__use_year_when_identifying:
md.year = None
if self.config.Auto_Tag__ignore_leading_numbers_in_filename and md.series is not None:
md.series = re.sub(r"^([\d.]+)(.*)", r"\2", md.series)
result, matches = ii.identify(ca, md)
found_match = False
@ -503,8 +488,7 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.low_confidence_match,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.low_confidence_matches.append(res)
return res, match_results
@ -516,8 +500,7 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.multiple_match,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.multiple_matches.append(res)
return res, match_results
@ -529,8 +512,7 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.low_confidence_match,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.low_confidence_matches.append(res)
return res, match_results
@ -542,8 +524,7 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.no_match,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.no_matches.append(res)
return res, match_results
@ -551,7 +532,7 @@ class CLI:
# we got here, so we have a single match
# now get the particular issue data
ct_md = self.fetch_metadata(matches[0].issue_id)
ct_md = self.actual_issue_data_fetch(matches[0].issue_id)
if ct_md.is_empty:
res = Result(
Action.save,
@ -559,8 +540,7 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.good_match,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
match_results.fetch_data_failures.append(res)
return res, match_results
@ -572,12 +552,11 @@ class CLI:
online_results=matches,
match_status=MatchStatus.good_match,
md=prepare_metadata(md, ct_md, self.config),
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
tags_written=self.config.Runtime_Options__type_modify,
)
assert res.md
# ok, done building our metadata. time to save
if self.write_tags(ca, res.md):
if self.actual_metadata_save(ca, res.md):
match_results.good_matches.append(res)
else:
res.status = Status.write_failure
@ -590,7 +569,7 @@ class CLI:
if self.batch_mode:
msg_hdr = f"{ca.path}: "
md, tags_read = self.create_local_metadata(ca, self.config.Runtime_Options__tags_read)
md = self.create_local_metadata(ca)
if md.series is None:
logger.error(msg_hdr + "Can't rename without series name")
@ -650,7 +629,7 @@ class CLI:
suffix = " (dry-run, no change)"
self.output(f"renamed '{original_path.name}' -> '{new_name}' {suffix}")
return Result(Action.rename, Status.success, original_path, tags_read=tags_read, md=md)
return Result(Action.rename, Status.success, original_path, md=md)
def export(self, ca: ComicArchive) -> Result:
msg_hdr = ""

View File

@ -26,6 +26,7 @@ import pathlib
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib.graphics import graphics_path
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.imagepopup import ImagePopup
from comictaggerlib.pageloader import PageLoader
@ -101,8 +102,8 @@ class CoverImageWidget(QtWidgets.QWidget):
self.imageCount = 1
self.imageData = b""
self.btnLeft.setIcon(QtGui.QIcon(":/graphics/left.png"))
self.btnRight.setIcon(QtGui.QIcon(":/graphics/right.png"))
self.btnLeft.setIcon(QtGui.QIcon(str(graphics_path / "left.png")))
self.btnRight.setIcon(QtGui.QIcon(str(graphics_path / "right.png")))
self.btnLeft.clicked.connect(self.decrement_image)
self.btnRight.clicked.connect(self.increment_image)
@ -262,7 +263,7 @@ class CoverImageWidget(QtWidgets.QWidget):
self.page_loader = None
def load_default(self) -> None:
self.current_pixmap = QtGui.QPixmap(":/graphics/nocover.png")
self.current_pixmap = QtGui.QPixmap(str(graphics_path / "nocover.png"))
self.set_display_pixmap()
def resizeEvent(self, resize_event: QtGui.QResizeEvent) -> None:

View File

@ -26,10 +26,10 @@ import subprocess
import settngs
from comicapi import utils
from comicapi.comicarchive import tags
from comicapi.comicarchive import metadata_styles
from comictaggerlib import ctversion
from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns
from comictaggerlib.ctsettings.types import ComicTaggerPaths, tag
from comictaggerlib.ctsettings.types import ComicTaggerPaths, metadata_type, metadata_type_single
from comictaggerlib.resulttypes import Action
logger = logging.getLogger(__name__)
@ -138,26 +138,26 @@ def register_runtime(parser: settngs.Manager) -> None:
)
parser.add_setting(
"-t",
"--tags-read",
metavar=f"{{{','.join(tags).upper()}}}",
"--type-read",
metavar=f"{{{','.join(metadata_styles).upper()}}}",
default=[],
type=tag,
help="""Specify the tags to read.\nUse commas for multiple tags.\nSee --list-plugins for the available tags.\nThe tags used will be 'overlaid' in order:\ne.g. '-t cbl,cr' with no CBL tags, CR will be used if they exist and CR will overwrite any shared CBL tags.\n\n""",
type=metadata_type,
help="""Specify the type of tags to read.\nUse commas for multiple types.\nSee --list-plugins for the available types.\nThe tag use will be 'overlayed' in order:\ne.g. '-t cbl,cr' with no CBL tags, CR will be used if they exist and CR will overwrite any shared CBL tags.\n\n""",
file=False,
)
parser.add_setting(
"--tags-write",
metavar=f"{{{','.join(tags).upper()}}}",
"--type-modify",
metavar=f"{{{','.join(metadata_styles).upper()}}}",
default=[],
type=tag,
help="""Specify the tags to write.\nUse commas for multiple tags.\nRead tags will be used if unspecified\nSee --list-plugins for the available tags.\n\n""",
type=metadata_type,
help="""Specify the type of tags to write.\nUse commas for multiple types.\nRead types will be used if unspecified\nSee --list-plugins for the available types.\n\n""",
file=False,
)
parser.add_setting(
"--skip-existing-tags",
"--skip-existing-metadata",
action=argparse.BooleanOptionalAction,
default=True,
help="""Skip archives that already have tags specified with -t,\notherwise merges new tags with existing tags (relevant for -s or -c).\ndefault: %(default)s""",
help="""Skip archives that already have tags specified with -t,\notherwise merges new metadata with existing metadata (relevant for -s or -c).\ndefault: %(default)s""",
file=False,
)
parser.add_setting("files", nargs="*", default=[], file=False)
@ -173,7 +173,7 @@ def register_commands(parser: settngs.Manager) -> None:
action="store_const",
const=Action.print,
default=Action.gui,
help="""Print out tag info from file. Specify via -t to only print specific tags.\n\n""",
help="""Print out tag info from file. Specify type\n(via --type-read) to get only info of that tag type.\n\n""",
file=False,
)
parser.add_setting(
@ -182,16 +182,16 @@ def register_commands(parser: settngs.Manager) -> None:
dest="command",
action="store_const",
const=Action.delete,
help="Deletes the tags specified via -t.",
help="Deletes the tag block of specified type (via -t).",
file=False,
)
parser.add_setting(
"-c",
"--copy",
type=tag,
type=metadata_type_single,
default=[],
metavar=f"{{{','.join(tags).upper()}}}",
help="Copy the specified source tags to\ndestination tags specified via --tags-write\n(potentially lossy operation).\n\n",
metavar=f"{{{','.join(metadata_styles).upper()}}}",
help="Copy the specified source tag block to\ndestination style specified via --type-modify\n(potentially lossy operation).\n\n",
file=False,
)
parser.add_setting(
@ -200,7 +200,7 @@ def register_commands(parser: settngs.Manager) -> None:
dest="command",
action="store_const",
const=Action.save,
help="Save out tags as specified tags (via --tags-write).\nMust specify also at least -o, -f, or -m.\n\n",
help="Save out tags as specified type (via --type-modify).\nMust specify also at least -o, -f, or -m.\n\n",
file=False,
)
parser.add_setting(
@ -209,7 +209,7 @@ def register_commands(parser: settngs.Manager) -> None:
dest="command",
action="store_const",
const=Action.rename,
help="Rename the file based on specified tags.",
help="Rename the file based on specified tag style.",
file=False,
)
parser.add_setting(
@ -269,8 +269,8 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs
if config[0].Runtime_Options__json and config[0].Runtime_Options__interactive:
config[0].Runtime_Options__json = False
if config[0].Runtime_Options__tags_read and not config[0].Runtime_Options__tags_write:
config[0].Runtime_Options__tags_write = config[0].Runtime_Options__tags_read
if config[0].Runtime_Options__type_read and not config[0].Runtime_Options__type_modify:
config[0].Runtime_Options__type_modify = config[0].Runtime_Options__type_read
if (
config[0].Commands__command not in (Action.save_config, Action.list_plugins)
@ -279,16 +279,16 @@ def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs
):
parser.exit(message="Command requires at least one filename!\n", status=1)
if config[0].Commands__command == Action.delete and not config[0].Runtime_Options__tags_write:
parser.exit(message="Please specify the tags to delete with --tags-write\n", status=1)
if config[0].Commands__command == Action.delete and not config[0].Runtime_Options__type_modify:
parser.exit(message="Please specify the type to delete with --type-modify\n", status=1)
if config[0].Commands__command == Action.save and not config[0].Runtime_Options__tags_write:
parser.exit(message="Please specify the tags to save with --tags-write\n", status=1)
if config[0].Commands__command == Action.save and not config[0].Runtime_Options__type_modify:
parser.exit(message="Please specify the type to save with --type-modify\n", status=1)
if config[0].Commands__copy:
config[0].Commands__command = Action.copy
if not config[0].Runtime_Options__tags_write:
parser.exit(message="Please specify the tags to copy to with --tags-write\n", status=1)
if not config[0].Runtime_Options__type_modify:
parser.exit(message="Please specify the type to copy to with --type-modify\n", status=1)
if config[0].Runtime_Options__recursive:
config[0].Runtime_Options__files = utils.get_recursive_filelist(config[0].Runtime_Options__files)

View File

@ -26,8 +26,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("write_tags", default=["cbi"], cmdline=False)
parser.add_setting("read_tags", default=["cbi"], 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)
@ -154,20 +154,20 @@ def md_options(parser: settngs.Manager) -> None:
parser.add_setting("--apply-transform-on-import", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--apply-transform-on-bulk-operation", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("use_short_tag_names", default=False, action=argparse.BooleanOptionalAction, cmdline=False)
parser.add_setting("use_short_metadata_names", default=False, action=argparse.BooleanOptionalAction, cmdline=False)
parser.add_setting(
"--cr",
default=True,
action=argparse.BooleanOptionalAction,
help="Enable ComicRack tags. Turn off to only use CIX tags.\ndefault: %(default)s",
help="Enable the ComicRack metadata type. Turn off to only use the CIX metadata type.\ndefault: %(default)s",
)
parser.add_setting(
"--tag-merge",
"--comic-merge",
metavar=f"{{{','.join(merge.Mode)}}}",
default=merge.Mode.OVERLAY,
choices=merge.Mode,
type=merge.Mode,
help="How to merge fields when reading enabled tags (CR, CBL, etc.) See -t, --tags-read default: %(default)s",
help="How to merge additional metadata for enabled read styles (CR, CBL, etc.) See -t, --type-read default: %(default)s",
)
parser.add_setting(
"--metadata-merge",
@ -175,19 +175,19 @@ def md_options(parser: settngs.Manager) -> None:
default=merge.Mode.OVERLAY,
choices=merge.Mode,
type=merge.Mode,
help="How to merge fields when downloading new metadata (CV, Metron, GCD, etc.) default: %(default)s",
help="How to merge new metadata from a data source (CV, Metron, GCD, etc.) default: %(default)s",
)
parser.add_setting(
"--tag-merge-lists",
"--comic-merge-lists",
action=argparse.BooleanOptionalAction,
default=True,
help="Merge lists when reading enabled tags (genres, characters, etc.) default: %(default)s",
help="Merge all items of lists when merging new metadata (genres, characters, etc.) default: %(default)s",
)
parser.add_setting(
"--metadata-merge-lists",
action=argparse.BooleanOptionalAction,
default=True,
help="Merge lists when downloading new metadata (genres, characters, etc.) default: %(default)s",
help="Merge all items of lists when merging new metadata (genres, characters, etc.) default: %(default)s",
)
@ -242,15 +242,14 @@ def autotag(parser: settngs.Manager) -> None:
"-o",
"--online",
action="store_true",
help="""Search online and attempt to identify file\nusing existing tags and images in archive.\nMay be used in conjunction with -f and -m.\n\n""",
help="""Search online and attempt to identify file\nusing existing metadata and images in archive.\nMay be used in conjunction with -f and -m.\n\n""",
file=False,
)
parser.add_setting(
"--save-on-low-confidence",
default=False,
action=argparse.BooleanOptionalAction,
help="Automatically save tags on low-confidence matches.\ndefault: %(default)s",
cmdline=False,
help="Automatically save metadata on low-confidence matches.\ndefault: %(default)s",
)
parser.add_setting(
"--use-year-when-identifying",
@ -291,14 +290,14 @@ def autotag(parser: settngs.Manager) -> None:
"--metadata",
default=GenericMetadata(),
type=parse_metadata_from_string,
help="""Explicitly define some metadata to be used in YAML syntax. Use @file.yaml to read from a file. e.g.:\n"series: Plastic Man, publisher: Quality Comics, year: "\n"series: 'Kickers, Inc.', issue: '1', year: 1986"\nIf you want to erase a tag leave the value blank.\nSome names that can be used: series, issue, issue_count, year,\npublisher, title\n\n""",
help="""Explicitly define some tags to be used in YAML syntax. Use @file.yaml to read from a file. e.g.:\n"series: Plastic Man, publisher: Quality Comics, year: "\n"series: 'Kickers, Inc.', issue: '1', year: 1986"\nIf you want to erase a tag leave the value blank.\nSome names that can be used: series, issue, issue_count, year,\npublisher, title\n\n""",
file=False,
)
parser.add_setting(
"--clear-tags",
"--clear-metadata",
default=False,
action=argparse.BooleanOptionalAction,
help="Clears all existing tags during import, default is to merge tags.\nMay be used in conjunction with -o, -f and -m.\ndefault: %(default)s\n\n",
help="Clears all existing metadata during import, default is to merge metadata.\nMay be used in conjunction with -o, -f and -m.\ndefault: %(default)s\n\n",
)
parser.add_setting(
"--publisher-filter",
@ -342,23 +341,23 @@ def parse_filter(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]:
def migrate_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]:
original_types = ("cbi", "cr", "comet")
write_Tags = config[0].internal__write_tags
if not isinstance(write_Tags, list):
if isinstance(write_Tags, int) and write_Tags in (0, 1, 2):
config[0].internal__write_tags = [original_types[write_Tags]]
elif isinstance(write_Tags, str):
config[0].internal__write_tags = [write_Tags]
save_style = config[0].internal__save_data_style
if not isinstance(save_style, list):
if isinstance(save_style, int) and save_style in (0, 1, 2):
config[0].internal__save_data_style = [original_types[save_style]]
elif isinstance(save_style, str):
config[0].internal__save_data_style = [save_style]
else:
config[0].internal__write_tags = ["cbi"]
config[0].internal__save_data_style = ["cbi"]
read_tags = config[0].internal__read_tags
if not isinstance(read_tags, list):
if isinstance(read_tags, int) and read_tags in (0, 1, 2):
config[0].internal__read_tags = [original_types[read_tags]]
elif isinstance(read_tags, str):
config[0].internal__read_tags = [read_tags]
load_style = config[0].internal__load_data_style
if not isinstance(load_style, list):
if isinstance(load_style, int) and load_style in (0, 1, 2):
config[0].internal__load_data_style = [original_types[load_style]]
elif isinstance(load_style, str):
config[0].internal__load_data_style = [load_style]
else:
config[0].internal__read_tags = ["cbi"]
config[0].internal__load_data_style = ["cbi"]
return config

View File

@ -15,7 +15,7 @@ from typing import Any, NamedTuple
logger = logging.getLogger(__name__)
NORMALIZE_PACKAGE_NAME_RE = re.compile(r"[-_.]+")
PLUGIN_GROUPS = frozenset(("comictagger.talker", "comicapi.archiver", "comicapi.tags"))
PLUGIN_GROUPS = frozenset(("comictagger.talker", "comicapi.archiver", "comicapi.metadata"))
class FailedToLoadPlugin(Exception):
@ -72,13 +72,13 @@ class Plugins(NamedTuple):
"""Classified plugins."""
archivers: list[Plugin]
tags: list[Plugin]
metadata: list[Plugin]
talkers: list[Plugin]
def all_plugins(self) -> Generator[Plugin, None, None]:
"""Return an iterator over all :class:`LoadedPlugin`s."""
yield from self.archivers
yield from self.tags
yield from self.metadata
yield from self.talkers
def versions_str(self) -> str:
@ -133,21 +133,21 @@ def find_plugins(plugin_folder: pathlib.Path) -> Plugins:
def _classify_plugins(plugins: list[Plugin]) -> Plugins:
archivers = []
tags = []
metadata = []
talkers = []
for p in plugins:
if p.entry_point.group == "comictagger.talker":
talkers.append(p)
elif p.entry_point.group == "comicapi.tags":
tags.append(p)
elif p.entry_point.group == "comicapi.metadata":
metadata.append(p)
elif p.entry_point.group == "comicapi.archiver":
archivers.append(p)
else:
logger.warning(NotImplementedError(f"what plugin type? {p}"))
return Plugins(
tags=tags,
metadata=metadata,
archivers=archivers,
talkers=talkers,
)

View File

@ -15,7 +15,7 @@ import comictaggerlib.resulttypes
class SettngsNS(settngs.TypedNS):
Commands__version: bool
Commands__command: comictaggerlib.resulttypes.Action
Commands__copy: list[str]
Commands__copy: str
Runtime_Options__config: comictaggerlib.ctsettings.types.ComicTaggerPaths
Runtime_Options__verbose: int
@ -32,14 +32,14 @@ class SettngsNS(settngs.TypedNS):
Runtime_Options__no_gui: bool
Runtime_Options__abort_on_conflict: bool
Runtime_Options__delete_original: bool
Runtime_Options__tags_read: list[str]
Runtime_Options__tags_write: list[str]
Runtime_Options__skip_existing_tags: bool
Runtime_Options__type_read: list[str]
Runtime_Options__type_modify: list[str]
Runtime_Options__skip_existing_metadata: bool
Runtime_Options__files: list[str]
internal__install_id: str
internal__write_tags: list[str]
internal__read_tags: list[str]
internal__save_data_style: list[str]
internal__load_data_style: list[str]
internal__last_opened_folder: str
internal__window_width: int
internal__window_height: int
@ -76,11 +76,11 @@ class SettngsNS(settngs.TypedNS):
Metadata_Options__copy_weblink_to_comments: bool
Metadata_Options__apply_transform_on_import: bool
Metadata_Options__apply_transform_on_bulk_operation: bool
Metadata_Options__use_short_tag_names: bool
Metadata_Options__use_short_metadata_names: bool
Metadata_Options__cr: bool
Metadata_Options__tag_merge: comicapi.merge.Mode
Metadata_Options__comic_merge: comicapi.merge.Mode
Metadata_Options__metadata_merge: comicapi.merge.Mode
Metadata_Options__tag_merge_lists: bool
Metadata_Options__comic_merge_lists: bool
Metadata_Options__metadata_merge_lists: bool
File_Rename__template: str
@ -102,7 +102,7 @@ class SettngsNS(settngs.TypedNS):
Auto_Tag__parse_filename: bool
Auto_Tag__issue_id: str | None
Auto_Tag__metadata: comicapi.genericmetadata.GenericMetadata
Auto_Tag__clear_tags: bool
Auto_Tag__clear_metadata: bool
Auto_Tag__publisher_filter: list[str]
Auto_Tag__use_publisher_filter: bool
Auto_Tag__auto_imprint: bool
@ -124,7 +124,7 @@ class SettngsNS(settngs.TypedNS):
class Commands(typing.TypedDict):
version: bool
command: comictaggerlib.resulttypes.Action
copy: list[str]
copy: str
class Runtime_Options(typing.TypedDict):
@ -143,16 +143,16 @@ class Runtime_Options(typing.TypedDict):
no_gui: bool
abort_on_conflict: bool
delete_original: bool
tags_read: list[str]
tags_write: list[str]
skip_existing_tags: bool
type_read: list[str]
type_modify: list[str]
skip_existing_metadata: bool
files: list[str]
class internal(typing.TypedDict):
install_id: str
write_tags: list[str]
read_tags: list[str]
save_data_style: list[str]
load_data_style: list[str]
last_opened_folder: str
window_width: int
window_height: int
@ -197,11 +197,11 @@ class Metadata_Options(typing.TypedDict):
copy_weblink_to_comments: bool
apply_transform_on_import: bool
apply_transform_on_bulk_operation: bool
use_short_tag_names: bool
use_short_metadata_names: bool
cr: bool
tag_merge: comicapi.merge.Mode
comic_merge: comicapi.merge.Mode
metadata_merge: comicapi.merge.Mode
tag_merge_lists: bool
comic_merge_lists: bool
metadata_merge_lists: bool
@ -227,7 +227,7 @@ class Auto_Tag(typing.TypedDict):
parse_filename: bool
issue_id: str | None
metadata: comicapi.genericmetadata.GenericMetadata
clear_tags: bool
clear_metadata: bool
publisher_filter: list[str]
use_publisher_filter: bool
auto_imprint: bool

View File

@ -12,7 +12,7 @@ import yaml
from appdirs import AppDirs
from comicapi import utils
from comicapi.comicarchive import tags
from comicapi.comicarchive import metadata_styles
from comicapi.genericmetadata import REMOVE, GenericMetadata
if sys.version_info < (3, 10):
@ -151,14 +151,21 @@ class ComicTaggerPaths(AppDirs):
return f"logs: {self.user_log_dir}, config: {self.user_config_dir}, cache: {self.user_cache_dir}"
def tag(types: str) -> list[str]:
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[str]:
result = []
types = types.casefold()
for typ in utils.split(types, ","):
if typ not in tags:
choices = ", ".join(tags)
if typ not in metadata_styles:
choices = ", ".join(metadata_styles)
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
result.append(tags[typ].id)
result.append(metadata_styles[typ].short_name)
return result

View File

@ -71,7 +71,7 @@ class FileSelectionList(QtWidgets.QWidget):
self.separator.setSeparator(True)
select_all_action.setShortcut("Ctrl+A")
remove_action.setShortcut("Backspace" if platform.system() == "Darwin" else "Delete")
remove_action.setShortcut("Ctrl+X")
select_all_action.triggered.connect(self.select_all)
remove_action.triggered.connect(self.remove_selection)
@ -246,7 +246,7 @@ class FileSelectionList(QtWidgets.QWidget):
self,
"RAR Files are Read-Only",
"It looks like you have opened a RAR/CBR archive,\n"
"however ComicTagger cannot write to them without the rar program and are marked read only!\n\n"
"however ComicTagger cannot currently write to them without the rar program and are marked read only!\n\n"
f"{rar_help}",
)
self.rar_ro_shown = True
@ -326,7 +326,8 @@ class FileSelectionList(QtWidgets.QWidget):
type_item.setText(item_text)
type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
md_item.setText(", ".join(x for x in ca.get_supported_tags() if ca.has_tags(x)))
styles = ", ".join(x for x in ca.get_supported_metadata() if ca.has_metadata(x))
md_item.setText(styles)
if not ca.is_writable():
readonly_item.setCheckState(QtCore.Qt.CheckState.Checked)

View File

@ -1,24 +0,0 @@
<RCC>
<qresource prefix="graphics">
<file>about.png</file>
<file>app.png</file>
<file>auto.png</file>
<file>autotag.png</file>
<file>browse.png</file>
<file>clear.png</file>
<file>down.png</file>
<file>eye.svg</file>
<file>hidden.svg</file>
<file>left.png</file>
<file>longbox.png</file>
<file>nocover.png</file>
<file>open.png</file>
<file>parse.png</file>
<file>popup_bg.png</file>
<file>right.png</file>
<file>save.png</file>
<file>search.png</file>
<file>tags.png</file>
<file>up.png</file>
</qresource>
</RCC>

File diff suppressed because it is too large Load Diff

View File

@ -111,8 +111,10 @@ def open_tagger_window(
# needed to catch initial open file events (macOS)
app.openFileRequest.connect(lambda x: config[0].Runtime_Options__files.append(x.toLocalFile()))
# The window Icon needs to be set here. It's also set in taggerwindow.ui but it doesn't seem to matter
app.setWindowIcon(QtGui.QIcon(":/graphics/app.png"))
if platform.system() == "Darwin":
# Set the MacOS dock icon
app.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
if platform.system() == "Windows":
# For pure python, tell windows that we're not python,
@ -137,6 +139,7 @@ def open_tagger_window(
try:
tagger_window = TaggerWindow(config[0].Runtime_Options__files, config, talkers)
tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
tagger_window.show()
# Catch open file events (macOS)

View File

@ -20,6 +20,7 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets, sip, uic
from comictaggerlib.graphics import graphics_path
from comictaggerlib.ui import ui_path
logger = logging.getLogger(__name__)
@ -49,7 +50,7 @@ class ImagePopup(QtWidgets.QDialog):
# TODO: macOS denies this
screen = QtWidgets.QApplication.primaryScreen()
self.desktopBg = screen.grabWindow(sip.voidptr(0), 0, 0, screen_size.width(), screen_size.height())
bg = QtGui.QPixmap(":/graphics/popup_bg.png")
bg = QtGui.QPixmap(str(graphics_path / "popup_bg.png"))
self.clientBgPixmap = bg.scaled(
screen_size.width(),
screen_size.height(),

View File

@ -127,8 +127,8 @@ class App:
self._extend_plugin_paths(local_plugins)
comicapi.comicarchive.load_archive_plugins(local_plugins=[p.entry_point for p in local_plugins.archivers])
comicapi.comicarchive.load_tag_plugins(
version=version, local_plugins=[p.entry_point for p in local_plugins.tags]
comicapi.comicarchive.load_metadata_plugins(
version=version, local_plugins=[p.entry_point for p in local_plugins.metadata]
)
self.talkers = comictalker.get_talkers(
version, opts.config.user_cache_dir, local_plugins=[p.entry_point for p in local_plugins.talkers]
@ -141,7 +141,7 @@ class App:
self,
talkers: Collection[comictalker.ComicTalker],
archivers: Collection[type[comicapi.comicarchive.Archiver]],
tags: Collection[comicapi.comicarchive.Tag],
metadata_styles: Collection[comicapi.comicarchive.Metadata],
) -> None:
if self.config[0].Runtime_Options__json:
for talker in talkers:
@ -183,14 +183,14 @@ class App:
)
)
for tag in tags:
for style in metadata_styles:
print( # noqa: T201
json.dumps(
{
"type": "tag",
"enabled": tag.enabled,
"name": tag.name(),
"id": tag.id,
"type": "metadata",
"enabled": style.enabled,
"name": style.name(),
"short_name": style.short_name,
}
)
)
@ -199,14 +199,14 @@ class App:
for talker in talkers:
print(f"{talker.id:<10}: {talker.name:<21}, {talker.website}") # noqa: T201
print("\nComic Archive: (Enabled, Name: extension, exe)") # noqa: T201
print("\nComic Archive: (Name: extension, exe)") # noqa: T201
for archiver in archivers:
a = archiver()
print(f"{a.enabled!s:<5}, {a.name():<10}: {a.extension():<5}, {a.exe}") # noqa: T201
print(f"{a.name():<10}: {a.extension():<5}, {a.exe}") # noqa: T201
print("\nTags: (Enabled, ID: Name)") # noqa: T201
for tag in tags:
print(f"{tag.enabled!s:<5}, {tag.id:<10}: {tag.name()}") # noqa: T201
print("\nMetadata Style: (Short Name: Name)") # noqa: T201
for style in metadata_styles:
print(f"{style.short_name:<10}: {style.name()}") # noqa: T201
def initialize(self) -> argparse.Namespace:
conf, _ = self.initial_arg_parser.parse_known_intermixed_args()
@ -252,12 +252,9 @@ class App:
# config already loaded
error = None
if (
not self.config[0].Metadata_Options__cr
and "cr" in comicapi.comicarchive.tags
and comicapi.comicarchive.tags["cr"].enabled
):
comicapi.comicarchive.tags["cr"].enabled = False
if not self.config[0].Metadata_Options__cr:
if "cr" in comicapi.comicarchive.metadata_styles:
del comicapi.comicarchive.metadata_styles["cr"]
if len(self.talkers) < 1:
error = error = (
@ -280,7 +277,7 @@ class App:
self.list_plugins(
list(self.talkers.values()),
comicapi.comicarchive.archivers,
comicapi.comicarchive.tags.values(),
comicapi.comicarchive.metadata_styles.values(),
)
return

View File

@ -10,18 +10,18 @@ from comictaggerlib.ctsettings.settngs_namespace import SettngsNS
from comictalker.talker_utils import cleanup_html
def prepare_metadata(md: GenericMetadata, new_md: GenericMetadata, config: SettngsNS) -> GenericMetadata:
if config.Metadata_Options__apply_transform_on_import:
new_md = CBLTransformer(new_md, config).apply()
def prepare_metadata(md: GenericMetadata, new_md: GenericMetadata, opts: SettngsNS) -> GenericMetadata:
if opts.Metadata_Options__apply_transform_on_import:
new_md = CBLTransformer(new_md, opts).apply()
final_md = md.copy()
if config.Auto_Tag__clear_tags:
if opts.Auto_Tag__clear_metadata:
final_md = GenericMetadata()
final_md.overlay(new_md, config.Metadata_Options__metadata_merge, config.Metadata_Options__metadata_merge_lists)
if final_md.data_origin is not None:
final_md.overlay(new_md, opts.Metadata_Options__metadata_merge, opts.Metadata_Options__metadata_merge_lists)
if final_md.tag_origin is not None:
notes = (
f"Tagged with ComicTagger {ctversion.version} using info from {final_md.data_origin.name} on"
f"Tagged with ComicTagger {ctversion.version} using info from {final_md.tag_origin.name} on"
+ f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {final_md.issue_id}]"
)
else:
@ -31,11 +31,11 @@ def prepare_metadata(md: GenericMetadata, new_md: GenericMetadata, config: Settn
+ (f"[Issue ID {final_md.issue_id}]" if final_md.issue_id else "")
)
if config.Auto_Tag__auto_imprint:
if opts.Auto_Tag__auto_imprint:
final_md.fix_publisher()
return final_md.replace(
is_empty=False,
notes=utils.combine_notes(final_md.notes, notes, "Tagged with ComicTagger"),
description=cleanup_html(final_md.description, config.Sources__remove_html_tables) or None,
description=cleanup_html(final_md.description, opts.Sources__remove_html_tables) or None,
)

View File

@ -17,12 +17,14 @@
from __future__ import annotations
import logging
import platform
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.graphics import graphics_path
from comictaggerlib.ui import ui_path
logger = logging.getLogger(__name__)
@ -55,8 +57,12 @@ class PageBrowserWindow(QtWidgets.QDialog):
self.metadata = metadata
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Close).setDefault(True)
self.btnPrev.setIcon(QtGui.QIcon(":/graphics/left.png"))
self.btnNext.setIcon(QtGui.QIcon(":/graphics/right.png"))
if platform.system() == "Darwin":
self.btnPrev.setText("<<")
self.btnNext.setText(">>")
else:
self.btnPrev.setIcon(QtGui.QIcon(str(graphics_path / "left.png")))
self.btnNext.setIcon(QtGui.QIcon(str(graphics_path / "right.png")))
self.btnNext.clicked.connect(self.next_page)
self.btnPrev.clicked.connect(self.prev_page)

View File

@ -20,7 +20,7 @@ import logging
from PyQt5 import QtCore, QtWidgets, uic
from comicapi.comicarchive import ComicArchive, tags
from comicapi.comicarchive import ComicArchive, metadata_styles
from comicapi.genericmetadata import ImageMetadata, PageType
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ui import ui_path
@ -116,7 +116,7 @@ class PageListEditor(QtWidgets.QWidget):
self.comic_archive: ComicArchive | None = None
self.pages_list: list[ImageMetadata] = []
self.tag_ids: list[str] = []
self.data_styles: list[str] = []
def reset_page(self) -> None:
self.pageWidget.clear()
@ -343,7 +343,7 @@ class PageListEditor(QtWidgets.QWidget):
self.comic_archive = comic_archive
self.pages_list = pages_list
if pages_list:
self.select_read_tags(self.tag_ids)
self.set_metadata_style(self.data_styles)
else:
self.cbPageType.setEnabled(False)
self.chkDoublePage.setEnabled(False)
@ -386,16 +386,15 @@ class PageListEditor(QtWidgets.QWidget):
self.first_front_page = self.get_first_front_cover()
self.firstFrontCoverChanged.emit(self.first_front_page)
def select_read_tags(self, tag_ids: list[str]) -> None:
# depending on the current tags, certain fields are disabled
if not tag_ids:
return
def set_metadata_style(self, data_styles: list[str]) -> None:
# depending on the current data style, certain fields are disabled
if data_styles:
styles = [metadata_styles[style] for style in data_styles]
enabled_widgets = set()
for style in styles:
enabled_widgets.update(style.supported_attributes)
enabled_widgets = set()
for tag_id in tag_ids:
enabled_widgets.update(tags[tag_id].supported_attributes)
self.data_styles = data_styles
self.tag_ids = tag_ids
for md_field, widget in self.md_attributes.items():
enable_widget(widget, md_field in enabled_widgets)
for metadata, widget in self.md_attributes.items():
enable_widget(widget, metadata in enabled_widgets)

View File

@ -22,7 +22,7 @@ import settngs
from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive, tags
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
@ -39,7 +39,7 @@ class RenameWindow(QtWidgets.QDialog):
self,
parent: QtWidgets.QWidget,
comic_archive_list: list[ComicArchive],
read_tag_ids: list[str],
load_data_styles: list[str],
config: settngs.Config[ct_ns],
talkers: dict[str, ComicTalker],
) -> None:
@ -48,7 +48,9 @@ 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 {', '.join(tags[tag].name() for tag in read_tag_ids)} tags):")
self.label.setText(
f"Preview (based on {', '.join(metadata_styles[style].name() for style in load_data_styles)} tags):"
)
self.setWindowFlags(
QtCore.Qt.WindowType(
@ -61,7 +63,7 @@ class RenameWindow(QtWidgets.QDialog):
self.config = config
self.talkers = talkers
self.comic_archive_list = comic_archive_list
self.read_tag_ids = read_tag_ids
self.load_data_styles = load_data_styles
self.rename_list: list[str] = []
self.btnSettings.clicked.connect(self.modify_settings)
@ -82,13 +84,13 @@ class RenameWindow(QtWidgets.QDialog):
new_ext = ca.extension()
if md is None or md.is_empty:
md, error = self.parent().read_all_tags(self.read_tag_ids, ca)
md, error = self.parent().overlay_ca_read_style(self.load_data_styles, ca)
if error is not None:
logger.error("Failed to load tags from %s: %s", ca.path, error)
logger.error("Failed to load metadata for %s: %s", ca.path, error)
QtWidgets.QMessageBox.warning(
self,
"Read Failed!",
f"One or more of the read tags failed to load for {ca.path}, check log for details",
f"One or more of the read styles failed to load for {ca.path}, check log for details",
)
if md.is_empty:

View File

@ -82,7 +82,6 @@ class Result:
md: GenericMetadata | None = None
tags_read: list[str] = dataclasses.field(default_factory=list)
tags_deleted: list[str] = dataclasses.field(default_factory=list)
tags_written: list[str] = dataclasses.field(default_factory=list)

View File

@ -71,7 +71,7 @@ template_tooltip = """
The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
Accepts the following variables:
{is_empty} (boolean)
{data_origin} (string)
{tag_origin} (string)
{series} (string)
{issue} (string)
{title} (string)
@ -195,8 +195,8 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbFilenameParser.clear()
self.cbFilenameParser.addItems(utils.Parser)
for mode in merge.Mode:
self.cbTagsMergeMode.addItem(mode.name.capitalize().replace("_", " "), mode)
self.cbDownloadMergeMode.addItem(mode.name.capitalize().replace("_", " "), mode)
self.cbMergeModeComic.addItem(mode.name.capitalize().replace("_", " "), mode)
self.cbMergeModeMetadata.addItem(mode.name.capitalize().replace("_", " "), mode)
self.connect_signals()
self.settings_to_form()
self.rename_test()
@ -418,7 +418,7 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxUseFilter.setChecked(self.config[0].Auto_Tag__use_publisher_filter)
self.cbxSortByYear.setChecked(self.config[0].Issue_Identifier__sort_series_by_year)
self.cbxExactMatches.setChecked(self.config[0].Issue_Identifier__exact_series_matches_first)
self.cbxClearFormBeforePopulating.setChecked(self.config[0].Auto_Tag__clear_tags)
self.cbxClearFormBeforePopulating.setChecked(self.config[0].Auto_Tag__clear_metadata)
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].Metadata_Options__assume_lone_credit_is_primary)
self.cbxCopyCharactersToTags.setChecked(self.config[0].Metadata_Options__copy_characters_to_tags)
@ -432,13 +432,15 @@ class SettingsWindow(QtWidgets.QDialog):
self.config[0].Metadata_Options__apply_transform_on_bulk_operation
)
self.cbTagsMergeMode.setCurrentIndex(self.cbTagsMergeMode.findData(self.config[0].Metadata_Options__tag_merge))
self.cbDownloadMergeMode.setCurrentIndex(
self.cbDownloadMergeMode.findData(self.config[0].Metadata_Options__metadata_merge)
self.cbMergeModeComic.setCurrentIndex(
self.cbMergeModeComic.findData(self.config[0].Metadata_Options__comic_merge)
)
self.cbxTagsMergeLists.setChecked(self.config[0].Metadata_Options__tag_merge_lists)
self.cbMergeModeMetadata.setCurrentIndex(
self.cbMergeModeMetadata.findData(self.config[0].Metadata_Options__metadata_merge)
)
self.cbxMergeListsComic.setChecked(self.config[0].Metadata_Options__comic_merge_lists)
self.cbxMergeListsMetadata.setChecked(self.config[0].Metadata_Options__metadata_merge_lists)
self.cbxShortTagNames.setChecked(self.config[0].Metadata_Options__use_short_tag_names)
self.cbxShortMetadataNames.setChecked(self.config[0].Metadata_Options__use_short_metadata_names)
self.cbxEnableCR.setChecked(self.config[0].Metadata_Options__cr)
self.leRenameTemplate.setText(self.config[0].File_Rename__template)
@ -548,7 +550,7 @@ class SettingsWindow(QtWidgets.QDialog):
self.config[0].Auto_Tag__use_publisher_filter = self.cbxUseFilter.isChecked()
self.config[0].Issue_Identifier__sort_series_by_year = self.cbxSortByYear.isChecked()
self.config[0].Issue_Identifier__exact_series_matches_first = self.cbxExactMatches.isChecked()
self.config[0].Auto_Tag__clear_tags = self.cbxClearFormBeforePopulating.isChecked()
self.config[0].Auto_Tag__clear_metadata = self.cbxClearFormBeforePopulating.isChecked()
self.config[0].Metadata_Options__assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.config[0].Metadata_Options__copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
@ -562,17 +564,17 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxApplyCBLTransformOnBatchOperation.isChecked()
)
self.config[0].Metadata_Options__tag_merge = merge.Mode(self.cbTagsMergeMode.currentData())
self.config[0].Metadata_Options__metadata_merge = merge.Mode(self.cbDownloadMergeMode.currentData())
self.config[0].Metadata_Options__tag_merge_lists = self.cbxTagsMergeLists.isChecked()
self.config[0].Metadata_Options__comic_merge = merge.Mode(self.cbMergeModeComic.currentData())
self.config[0].Metadata_Options__metadata_merge = merge.Mode(self.cbMergeModeMetadata.currentData())
self.config[0].Metadata_Options__comic_merge_lists = self.cbxMergeListsComic.isChecked()
self.config[0].Metadata_Options__metadata_merge_lists = self.cbxMergeListsMetadata.isChecked()
self.config[0].Metadata_Options__cr = self.cbxEnableCR.isChecked()
# Update tag names if required
if self.config[0].Metadata_Options__use_short_tag_names != self.cbxShortTagNames.isChecked():
self.config[0].Metadata_Options__use_short_tag_names = self.cbxShortTagNames.isChecked()
self.parent().populate_tag_names()
self.parent().adjust_tags_combo()
# Update metadata style names if required
if self.config[0].Metadata_Options__use_short_metadata_names != self.cbxShortMetadataNames.isChecked():
self.config[0].Metadata_Options__use_short_metadata_names = self.cbxShortMetadataNames.isChecked()
self.parent().populate_style_names()
self.parent().adjust_save_style_combo()
self.config[0].File_Rename__template = str(self.leRenameTemplate.text())
self.config[0].File_Rename__issue_number_padding = int(self.leIssueNumPadding.text())

View File

@ -32,10 +32,9 @@ import settngs
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic
import comicapi.merge
import comictaggerlib.graphics.resources
import comictaggerlib.ui
from comicapi import utils
from comicapi.comicarchive import ComicArchive, tags
from comicapi.comicarchive import ComicArchive, metadata_styles
from comicapi.filenameparser import FileNameParser
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
@ -90,7 +89,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
uic.loadUi(uifile, self)
self.md_attributes = {
"data_origin": None,
"tag_origin": None,
"issue_id": None,
"series": self.leSeries,
"issue": self.leIssueNum,
@ -213,23 +212,21 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
# respect the command line selected tags
if config[0].Runtime_Options__tags_write:
config[0].internal__write_tags = config[0].Runtime_Options__tags_write
# respect the command line option tag type
if config[0].Runtime_Options__type_modify:
config[0].internal__save_data_style = config[0].Runtime_Options__type_modify
if config[0].Runtime_Options__type_read:
config[0].internal__load_data_style = config[0].Runtime_Options__type_read
if config[0].Runtime_Options__tags_read:
config[0].internal__read_tags = config[0].Runtime_Options__tags_read
for style in config[0].internal__save_data_style:
if style not in metadata_styles:
config[0].internal__save_data_style.remove(style)
for style in config[0].internal__load_data_style:
if style not in metadata_styles:
config[0].internal__load_data_style.remove(style)
for tag_id in config[0].internal__write_tags:
if tag_id not in tags:
config[0].internal__write_tags.remove(tag_id)
for tag_id in config[0].internal__read_tags:
if tag_id not in tags:
config[0].internal__read_tags.remove(tag_id)
self.selected_write_tags: list[str] = config[0].internal__write_tags
self.selected_read_tags: list[str] = config[0].internal__read_tags
self.save_data_styles: list[str] = config[0].internal__save_data_style
self.load_data_styles: list[str] = config[0].internal__load_data_style
self.setAcceptDrops(True)
self.view_tag_actions, self.remove_tag_actions = self.tag_actions()
@ -278,9 +275,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
# hook up the callbacks
self.cbSelectedReadTags.dropdownClosed.connect(self.select_read_tags)
self.cbSelectedWriteTags.itemChecked.connect(self.select_write_tags)
self.cbx_sources.currentIndexChanged.connect(self.select_source)
self.cbLoadDataStyle.dropdownClosed.connect(self.set_load_data_style)
self.cbSaveDataStyle.itemChecked.connect(self.set_save_data_style)
self.cbx_sources.currentIndexChanged.connect(self.set_source)
self.btnEditCredit.clicked.connect(self.edit_credit)
self.btnAddCredit.clicked.connect(self.add_credit)
self.btnRemoveCredit.clicked.connect(self.remove_credit)
@ -292,7 +289,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_tag_tweaks()
self.update_metadata_style_tweaks()
self.show()
self.set_app_position()
@ -341,14 +338,14 @@ class TaggerWindow(QtWidgets.QMainWindow):
def tag_actions(self) -> tuple[dict[str, QtGui.QAction], dict[str, QtGui.QAction]]:
view_raw_tags = {}
remove_raw_tags = {}
for tag in tags.values():
view_raw_tags[tag.id] = self.menuViewRawTags.addAction(f"View Raw {tag.name()} Tags")
view_raw_tags[tag.id].setStatusTip(f"View raw {tag.name()} tag block from file")
view_raw_tags[tag.id].triggered.connect(functools.partial(self.view_raw_tags, tag.id))
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[tag.id] = self.menuRemove.addAction(f"Remove Raw {tag.name()} Tags")
remove_raw_tags[tag.id].setStatusTip(f"Remove {tag.name()} tags from comic archive")
remove_raw_tags[tag.id].triggered.connect(functools.partial(self.remove_tags, tag.id))
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
@ -415,42 +412,134 @@ class TaggerWindow(QtWidgets.QMainWindow):
def config_menus(self) -> None:
# File Menu
self.actionAutoTag.triggered.connect(self.auto_tag)
self.actionCopyTags.triggered.connect(self.copy_tags)
self.actionExit.setShortcut("Ctrl+Q")
self.actionExit.setStatusTip("Exit application")
self.actionExit.triggered.connect(self.close)
self.actionLoad.triggered.connect(self.select_file)
self.actionLoadFolder.triggered.connect(self.select_folder)
self.actionOpenFolderAsComic.triggered.connect(self.select_folder_archive)
self.actionRemoveAuto.triggered.connect(self.remove_auto)
self.actionRename.triggered.connect(self.rename_archive)
self.actionRepackage.triggered.connect(self.repackage_archive)
self.actionSettings.triggered.connect(self.show_settings)
self.actionWrite_Tags.triggered.connect(self.write_tags)
# Tag Menu
self.actionApplyCBLTransform.triggered.connect(self.apply_cbl_transform)
self.actionAutoIdentify.triggered.connect(self.auto_identify_search)
self.actionAutoImprint.triggered.connect(self.auto_imprint)
self.actionClearEntryForm.triggered.connect(self.clear_form)
self.actionLiteralSearch.triggered.connect(self.literal_search)
self.actionParse_Filename.triggered.connect(self.use_filename)
self.actionParse_Filename_split_words.triggered.connect(self.use_filename_split)
self.actionReCalcPageDims.triggered.connect(self.recalc_page_dimensions)
self.actionSearchOnline.triggered.connect(self.query_online)
# Window Menu
self.actionLogWindow.triggered.connect(self.log_window.show)
self.actionPageBrowser.triggered.connect(self.show_page_browser)
# Help Menu
self.actionAbout.triggered.connect(self.about_app)
self.actionComicTaggerForum.triggered.connect(self.show_forum)
self.actionReportBug.triggered.connect(self.report_bug)
self.actionWiki.triggered.connect(self.show_wiki)
self.actionAddWebLink.triggered.connect(self.add_weblink_item)
self.actionRemoveWebLink.triggered.connect(self.remove_weblink_item)
self.actionLoad.setShortcut("Ctrl+O")
self.actionLoad.setStatusTip("Load comic archive")
self.actionLoad.triggered.connect(self.select_file)
self.actionLoadFolder.setShortcut("Ctrl+Shift+O")
self.actionLoadFolder.setStatusTip("Load folder with comic archives")
self.actionLoadFolder.triggered.connect(self.select_folder)
self.actionOpenFolderAsComic.setShortcut("Ctrl+Shift+Alt+O")
self.actionOpenFolderAsComic.setStatusTip("Load folder as a comic archives")
self.actionOpenFolderAsComic.triggered.connect(self.select_folder_archive)
self.actionWrite_Tags.setShortcut("Ctrl+S")
self.actionWrite_Tags.setStatusTip("Save tags to comic archive")
self.actionWrite_Tags.triggered.connect(self.commit_metadata)
self.actionAutoTag.setShortcut("Ctrl+T")
self.actionAutoTag.setStatusTip("Auto-tag multiple archives")
self.actionAutoTag.triggered.connect(self.auto_tag)
self.actionCopyTags.setShortcut("Ctrl+C")
self.actionCopyTags.setStatusTip("Copy one tag style tags to enabled modify style(s)")
self.actionCopyTags.triggered.connect(self.copy_tags)
self.actionRemoveAuto.setShortcut("Ctrl+D")
self.actionRemoveAuto.setStatusTip("Remove currently selected modify tag style(s) from the archive")
self.actionRemoveAuto.triggered.connect(self.remove_auto)
self.actionRepackage.setShortcut("Ctrl+E")
self.actionRepackage.setStatusTip("Re-create archive as CBZ")
self.actionRepackage.triggered.connect(self.repackage_archive)
self.actionRename.setShortcut("Ctrl+N")
self.actionRename.setStatusTip("Rename archive based on tags")
self.actionRename.triggered.connect(self.rename_archive)
self.actionSettings.setShortcut("Ctrl+Shift+S")
self.actionSettings.setStatusTip("Configure ComicTagger")
self.actionSettings.triggered.connect(self.show_settings)
# Tag Menu
self.actionParse_Filename.setShortcut("Ctrl+F")
self.actionParse_Filename.setStatusTip("Try to extract tags from filename")
self.actionParse_Filename.triggered.connect(self.use_filename)
self.actionParse_Filename_split_words.setShortcut("Ctrl+Shift+F")
self.actionParse_Filename_split_words.setStatusTip("Try to extract tags from filename and split words")
self.actionParse_Filename_split_words.triggered.connect(self.use_filename_split)
self.actionSearchOnline.setShortcut("Ctrl+W")
self.actionSearchOnline.setStatusTip("Search online for tags")
self.actionSearchOnline.triggered.connect(self.query_online)
self.actionAutoImprint.triggered.connect(self.auto_imprint)
self.actionAutoIdentify.setShortcut("Ctrl+I")
self.actionAutoIdentify.triggered.connect(self.auto_identify_search)
self.actionLiteralSearch.triggered.connect(self.literal_search)
self.actionApplyCBLTransform.setShortcut("Ctrl+L")
self.actionApplyCBLTransform.setStatusTip("Modify tags specifically for CBL format")
self.actionApplyCBLTransform.triggered.connect(self.apply_cbl_transform)
self.actionReCalcPageDims.setShortcut("Ctrl+R")
self.actionReCalcPageDims.setStatusTip(
"Trigger re-calculating image size, height and width for all pages on the next save"
)
self.actionReCalcPageDims.triggered.connect(self.recalc_page_dimensions)
self.actionClearEntryForm.setShortcut("Ctrl+Shift+C")
self.actionClearEntryForm.setStatusTip("Clear all the data on the screen")
self.actionClearEntryForm.triggered.connect(self.clear_form)
# Window Menu
self.actionPageBrowser.setShortcut("Ctrl+P")
self.actionPageBrowser.setStatusTip("Show the page browser")
self.actionPageBrowser.triggered.connect(self.show_page_browser)
self.actionLogWindow.setShortcut("Ctrl+Shift+L")
self.actionLogWindow.setStatusTip("Show the log window")
self.actionLogWindow.triggered.connect(self.log_window.show)
# Help Menu
self.actionAbout.setStatusTip("Show the " + self.appName + " info")
self.actionAbout.triggered.connect(self.about_app)
self.actionWiki.triggered.connect(self.show_wiki)
self.actionReportBug.triggered.connect(self.report_bug)
self.actionComicTaggerForum.triggered.connect(self.show_forum)
# Notes Menu
self.btnOpenWebLink.setIcon(QtGui.QIcon(str(graphics_path / "open.png")))
# ToolBar
self.actionLoad.setIcon(QtGui.QIcon(str(graphics_path / "open.png")))
self.actionLoadFolder.setIcon(QtGui.QIcon(str(graphics_path / "longbox.png")))
self.actionOpenFolderAsComic.setIcon(QtGui.QIcon(str(graphics_path / "open.png")))
self.actionWrite_Tags.setIcon(QtGui.QIcon(str(graphics_path / "save.png")))
self.actionParse_Filename.setIcon(QtGui.QIcon(str(graphics_path / "parse.png")))
self.actionParse_Filename_split_words.setIcon(QtGui.QIcon(str(graphics_path / "parse.png")))
self.actionSearchOnline.setIcon(QtGui.QIcon(str(graphics_path / "search.png")))
self.actionLiteralSearch.setIcon(QtGui.QIcon(str(graphics_path / "search.png")))
self.actionAutoIdentify.setIcon(QtGui.QIcon(str(graphics_path / "auto.png")))
self.actionAutoTag.setIcon(QtGui.QIcon(str(graphics_path / "autotag.png")))
self.actionAutoImprint.setIcon(QtGui.QIcon(str(graphics_path / "autotag.png")))
self.actionClearEntryForm.setIcon(QtGui.QIcon(str(graphics_path / "clear.png")))
self.actionPageBrowser.setIcon(QtGui.QIcon(str(graphics_path / "browse.png")))
self.toolBar.addAction(self.actionLoad)
self.toolBar.addAction(self.actionLoadFolder)
self.toolBar.addAction(self.actionWrite_Tags)
self.toolBar.addAction(self.actionSearchOnline)
self.toolBar.addAction(self.actionLiteralSearch)
self.toolBar.addAction(self.actionAutoIdentify)
self.toolBar.addAction(self.actionAutoTag)
self.toolBar.addAction(self.actionClearEntryForm)
self.toolBar.addAction(self.actionPageBrowser)
self.toolBar.addAction(self.actionAutoImprint)
self.leWebLink.addAction(self.actionAddWebLink)
self.leWebLink.addAction(self.actionRemoveWebLink)
self.actionAddWebLink.triggered.connect(self.add_weblink_item)
self.actionRemoveWebLink.triggered.connect(self.remove_weblink_item)
def add_weblink_item(self, url: str = "") -> None:
item = ""
if isinstance(url, str):
@ -589,7 +678,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
msg_box = QtWidgets.QMessageBox()
msg_box.setWindowTitle("About " + self.appName)
msg_box.setTextFormat(QtCore.Qt.TextFormat.RichText)
msg_box.setIconPixmap(QtGui.QPixmap(":/graphics/about.png"))
msg_box.setIconPixmap(QtGui.QPixmap(str(graphics_path / "about.png")))
msg_box.setText(
"<br><br><br>"
+ self.appName
@ -662,9 +751,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.menuRemove.setEnabled(enabled)
self.menuViewRawTags.setEnabled(enabled)
if self.comic_archive is not None:
for tag_id in tags:
self.view_tag_actions[tag_id].setEnabled(self.comic_archive.has_tags(tag_id))
self.remove_tag_actions[tag_id].setEnabled(self.comic_archive.has_tags(tag_id))
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.actionWrite_Tags.setEnabled(writeable)
@ -689,11 +778,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
page_count = f" ({ca.get_number_of_pages()} pages)"
self.lblPageCount.setText(page_count)
supported_md = ca.get_supported_tags()
supported_md = ca.get_supported_metadata()
tag_info = ""
for md in supported_md:
if ca.has_tags(md):
tag_info += "" + tags[md].name() + "\n"
if ca.has_metadata(md):
tag_info += "" + metadata_styles[md].name() + "\n"
self.lblTagList.setText(tag_info)
@ -852,7 +941,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.add_new_credit_entry(row, credit.role.title(), credit.person, credit.primary)
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)
@ -1075,13 +1164,13 @@ class TaggerWindow(QtWidgets.QMainWindow):
# Now push the new combined data into the edit controls
self.metadata_to_form()
def write_tags(self) -> None:
def commit_metadata(self) -> None:
if self.metadata is not None and self.comic_archive is not None:
if self.config[0].General__prompt_on_save:
reply = QtWidgets.QMessageBox.question(
self,
"Save Tags",
f"Are you sure you wish to save {', '.join([tags[tag_id].name() for tag_id in self.selected_write_tags])} tags to this archive?",
f"Are you sure you wish to save {', '.join([metadata_styles[style].name() for style in self.save_data_styles])} tags to this archive?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
@ -1093,22 +1182,22 @@ class TaggerWindow(QtWidgets.QMainWindow):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
self.form_to_metadata()
failed_tag: str = ""
# Save each tag
for tag_id in self.selected_write_tags:
success = self.comic_archive.write_tags(self.metadata, tag_id)
failed_style: str = ""
# Save each style
for style in self.save_data_styles:
success = self.comic_archive.write_metadata(self.metadata, style)
if not success:
failed_tag = tags[tag_id].name()
failed_style = metadata_styles[style].name()
break
self.comic_archive.load_cache(set(tags))
self.comic_archive.load_cache(set(metadata_styles))
QtWidgets.QApplication.restoreOverrideCursor()
if failed_tag:
if failed_style:
QtWidgets.QMessageBox.warning(
self,
"Save failed",
f"The tag save operation seemed to fail for: {failed_tag}",
f"The tag save operation seemed to fail for: {failed_style}",
)
else:
self.clear_dirty_flag()
@ -1116,56 +1205,56 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.update_menus()
# Only try to read if write was successful
self.metadata, error = self.read_selected_tags(self.selected_read_tags, self.comic_archive)
self.metadata, error = self.overlay_ca_read_style(self.load_data_styles, self.comic_archive)
if error is not None:
QtWidgets.QMessageBox.warning(
self,
"Read Failed!",
f"One or more of the selected read tags failed to load for {self.comic_archive.path}, check log for details",
f"One or more of the read styles failed to load for {self.comic_archive.path}, check log for details",
)
logger.error("Failed to load metadata for %s: %s", self.ca.path, error)
self.fileSelectionList.update_current_row()
self.update_ui_for_archive()
else:
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to write!")
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to commit!")
def select_read_tags(self, tag_ids: list[str]) -> None:
def set_load_data_style(self, load_data_styles: list[str]) -> None:
"""Should only be called from the combobox signal"""
if self.dirty_flag_verification(
"Change Read Tags",
"If you change read tag(s) now, data in the form will be lost. Are you sure?",
"Change Tag Read Style",
"If you change read tag style(s) now, data in the form will be lost. Are you sure?",
):
self.selected_read_tags = list(reversed(tag_ids))
self.config[0].internal__read_tags = self.selected_read_tags
self.load_data_styles = list(reversed(load_data_styles))
self.config[0].internal__load_data_style = self.load_data_styles
self.update_menus()
if self.comic_archive is not None:
self.load_archive(self.comic_archive)
else:
self.cbSelectedReadTags.itemChanged.disconnect()
self.adjust_tags_combo()
self.cbSelectedReadTags.itemChanged.connect(self.select_read_tags)
self.cbLoadDataStyle.itemChanged.disconnect()
self.adjust_load_style_combo()
self.cbLoadDataStyle.itemChanged.connect(self.set_load_data_style)
def select_write_tags(self) -> None:
self.selected_write_tags = self.cbSelectedWriteTags.currentData()
self.config[0].internal__write_tags = self.selected_write_tags
self.update_tag_tweaks()
def set_save_data_style(self) -> None:
self.save_data_styles = self.cbSaveDataStyle.currentData()
self.config[0].internal__save_data_style = self.save_data_styles
self.update_metadata_style_tweaks()
self.update_menus()
def select_source(self, s: int) -> None:
def set_source(self, s: int) -> None:
self.config[0].Sources__source = self.cbx_sources.itemData(s)
def update_credit_colors(self) -> None:
selected_tags = [tags[tag_id] for tag_id in self.selected_write_tags]
def update_metadata_credit_colors(self) -> None:
styles = [metadata_styles[style] for style in self.save_data_styles]
enabled = set()
for tag in selected_tags:
enabled.update(tag.supported_attributes)
for style in styles:
enabled.update(style.supported_attributes)
credit_attributes = [x for x in self.md_attributes.items() if "credits." in x[0]]
for r in range(self.twCredits.rowCount()):
w = self.twCredits.item(r, 1)
supports_role = any(tag.supports_credit_role(str(w.text())) for tag in selected_tags)
supports_role = any(style.supports_credit_role(str(w.text())) for style in styles)
for credit in credit_attributes:
widget_enabled = credit[0] in enabled
widget = self.twCredits.item(r, credit[1])
@ -1173,18 +1262,18 @@ class TaggerWindow(QtWidgets.QMainWindow):
widget_enabled = widget_enabled and supports_role
enable_widget(widget, widget_enabled)
def update_tag_tweaks(self) -> None:
# depending on the current data tag, certain fields are disabled
def update_metadata_style_tweaks(self) -> None:
# depending on the current data style, certain fields are disabled
enabled_widgets = set()
for tag_id in self.selected_write_tags:
enabled_widgets.update(tags[tag_id].supported_attributes)
for style in self.save_data_styles:
enabled_widgets.update(metadata_styles[style].supported_attributes)
for md_field, widget in self.md_attributes.items():
for metadata, widget in self.md_attributes.items():
if widget is not None and not isinstance(widget, (int)):
enable_widget(widget, md_field in enabled_widgets)
enable_widget(widget, metadata in enabled_widgets)
self.update_credit_colors()
self.page_list_editor.select_read_tags(self.selected_write_tags)
self.update_metadata_credit_colors()
self.page_list_editor.set_metadata_style(self.save_data_styles)
def cell_double_clicked(self, r: int, c: int) -> None:
self.edit_credit()
@ -1274,7 +1363,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:
@ -1315,43 +1404,45 @@ class TaggerWindow(QtWidgets.QMainWindow):
def adjust_source_combo(self) -> None:
self.cbx_sources.setCurrentIndex(self.cbx_sources.findData(self.config[0].Sources__source))
def adjust_tags_combo(self) -> None:
"""Select the enabled tags. Since tags are merged in an overlay fashion the last item in the list takes priority. We reverse the order for display to the user"""
unchecked = set(tags.keys()) - set(self.selected_read_tags)
for i, tag_id in enumerate(reversed(self.selected_read_tags)):
item_idx = self.cbSelectedReadTags.findData(tag_id)
self.cbSelectedReadTags.setItemChecked(item_idx, True)
def adjust_load_style_combo(self) -> None:
"""Select the enabled styles. Since metadata is merged in an overlay fashion the last item in the list takes priority. We reverse the order for display to the user"""
unchecked = set(metadata_styles.keys()) - set(self.load_data_styles)
for i, style in enumerate(reversed(self.load_data_styles)):
item_idx = self.cbLoadDataStyle.findData(style)
self.cbLoadDataStyle.setItemChecked(item_idx, True)
# Order matters, move items to list order
if item_idx != i:
self.cbSelectedReadTags.moveItem(item_idx, row=i)
for tag_id in unchecked:
self.cbSelectedReadTags.setItemChecked(self.cbSelectedReadTags.findData(tag_id), False)
self.cbLoadDataStyle.moveItem(item_idx, row=i)
for style in unchecked:
self.cbLoadDataStyle.setItemChecked(self.cbLoadDataStyle.findData(style), False)
# select the current tag_id
unchecked = set(tags.keys()) - set(self.selected_write_tags)
for tag_id in self.selected_write_tags:
self.cbSelectedWriteTags.setItemChecked(self.cbSelectedWriteTags.findData(tag_id), True)
for tag_id in unchecked:
self.cbSelectedWriteTags.setItemChecked(self.cbSelectedWriteTags.findData(tag_id), False)
self.update_tag_tweaks()
def adjust_save_style_combo(self) -> None:
# select the current style
unchecked = set(metadata_styles.keys()) - set(self.save_data_styles)
for style in self.save_data_styles:
self.cbSaveDataStyle.setItemChecked(self.cbSaveDataStyle.findData(style), True)
for style in unchecked:
self.cbSaveDataStyle.setItemChecked(self.cbSaveDataStyle.findData(style), False)
self.update_metadata_style_tweaks()
def populate_tag_names(self) -> None:
def populate_style_names(self) -> None:
# First clear all entries (called from settingswindow.py)
self.cbSelectedWriteTags.clear()
self.cbSelectedReadTags.clear()
# Add the entries to the tag comboboxes
for tag in tags.values():
if self.config[0].Metadata_Options__use_short_tag_names:
self.cbSelectedWriteTags.addItem(tag.id.upper(), tag.id)
self.cbSelectedReadTags.addItem(tag.id.upper(), tag.id)
self.cbSaveDataStyle.clear()
self.cbLoadDataStyle.clear()
# Add the entries to the tag style combobox
for style in metadata_styles.values():
if self.config[0].Metadata_Options__use_short_metadata_names:
self.cbSaveDataStyle.addItem(style.short_name.upper(), style.short_name)
self.cbLoadDataStyle.addItem(style.short_name.upper(), style.short_name)
else:
self.cbSelectedWriteTags.addItem(tag.name(), tag.id)
self.cbSelectedReadTags.addItem(tag.name(), tag.id)
self.cbSaveDataStyle.addItem(style.name(), style.short_name)
self.cbLoadDataStyle.addItem(style.name(), style.short_name)
def populate_combo_boxes(self) -> None:
self.populate_tag_names()
self.populate_style_names()
self.adjust_tags_combo()
self.adjust_load_style_combo()
self.adjust_save_style_combo()
# Add talker entries
for t_id, talker in self.talkers.items():
@ -1449,26 +1540,26 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.cbFormat.addItem("Year One")
def remove_auto(self) -> None:
self.remove_tags(self.selected_write_tags)
self.remove_tags(self.save_data_styles)
def remove_tags(self, tag_ids: list[str]) -> None:
# remove the indicated tag_ids from the archive
def remove_tags(self, styles: list[str]) -> None:
# remove the indicated tags from the archive
ca_list = self.fileSelectionList.get_selected_archive_list()
has_md_count = 0
file_md_count = {}
for tag_id in tag_ids:
file_md_count[tag_id] = 0
for style in styles:
file_md_count[style] = 0
for ca in ca_list:
for tag_id in tag_ids:
if ca.has_tags(tag_id):
for style in styles:
if ca.has_metadata(style):
has_md_count += 1
file_md_count[tag_id] += 1
file_md_count[style] += 1
if has_md_count == 0:
QtWidgets.QMessageBox.information(
self,
"Remove Tags",
f"No archives with {', '.join([tags[tag_id].name() for tag_id in tag_ids])} tags selected!",
f"No archives with {', '.join([metadata_styles[style].name() for style in styles])} tags selected!",
)
return
@ -1481,7 +1572,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
reply = QtWidgets.QMessageBox.question(
self,
"Remove Tags",
f"Are you sure you wish to remove {', '.join([f'{tags[tag_id].name()} tags from {count} files' for tag_id, count in file_md_count.items()])} removing a total of {has_md_count} tag(s)?",
f"Are you sure you wish to remove {', '.join([f'{metadata_styles[style].name()} tags from {count} files' for style, count in file_md_count.items()])} removing a total of {has_md_count} tag(s)?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
@ -1503,16 +1594,16 @@ class TaggerWindow(QtWidgets.QMainWindow):
progdialog.setValue(prog_idx)
progdialog.setLabelText(str(ca.path))
QtCore.QCoreApplication.processEvents()
for tag_id in tag_ids:
if ca.has_tags(tag_id) and ca.is_writable():
if ca.remove_tags(tag_id):
for style in styles:
if ca.has_metadata(style) and ca.is_writable():
if ca.remove_metadata(style):
success_count += 1
else:
failed_list.append(ca.path)
# Abandon any further tag removals to prevent any greater damage to archive
break
ca.reset_cache()
ca.load_cache(set(tags))
ca.load_cache(set(metadata_styles))
progdialog.hide()
QtCore.QCoreApplication.processEvents()
@ -1536,22 +1627,22 @@ class TaggerWindow(QtWidgets.QMainWindow):
ca_list = self.fileSelectionList.get_selected_archive_list()
has_src_count = 0
src_tag_ids: list[str] = self.selected_read_tags
dest_tag_ids: list[str] = self.selected_write_tags
src_styles: list[str] = self.load_data_styles
dest_styles: list[str] = self.save_data_styles
if len(src_tag_ids) == 1 and src_tag_ids[0] in dest_tag_ids:
# Remove the read tag from the write tag
dest_tag_ids.remove(src_tag_ids[0])
if len(src_styles) == 1 and src_styles[0] in dest_styles:
# Remove the read style from the write style
dest_styles.remove(src_styles[0])
if not dest_tag_ids:
if not dest_styles:
QtWidgets.QMessageBox.information(
self, "Copy Tags", "Can't copy tag tag onto itself. Read tag and modify tag must be different."
self, "Copy Tags", "Can't copy tag style onto itself. Read style and modify style must be different."
)
return
for ca in ca_list:
for tag_id in src_tag_ids:
if ca.has_tags(tag_id):
for style in src_styles:
if ca.has_metadata(style):
has_src_count += 1
continue
@ -1559,7 +1650,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
QtWidgets.QMessageBox.information(
self,
"Copy Tags",
f"No archives with {', '.join([tags[tag_id].name() for tag_id in src_tag_ids])} tags selected!",
f"No archives with {', '.join([metadata_styles[style].name() for style in src_styles])} tags selected!",
)
return
@ -1573,8 +1664,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
self,
"Copy Tags",
f"Are you sure you wish to copy the combined (with overlay order) tags of "
f"{', '.join([tags[tag_id].name() for tag_id in src_tag_ids])} "
f"to {', '.join([tags[tag_id].name() for tag_id in dest_tag_ids])} tags in "
f"{', '.join([metadata_styles[style].name() for style in src_styles])} "
f"to {', '.join([metadata_styles[style].name() for style in dest_styles])} tags in "
f"{has_src_count} archive(s)?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
@ -1592,15 +1683,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
success_count = 0
for prog_idx, ca in enumerate(ca_list, 1):
ca_saved = False
md, error = self.read_selected_tags(src_tag_ids, ca)
md, error = self.overlay_ca_read_style(src_styles, ca)
if error is not None:
failed_list.append(ca.path)
continue
if md.is_empty:
continue
for tag_id in dest_tag_ids:
if ca.has_tags(tag_id):
for style in dest_styles:
if ca.has_metadata(style):
QtCore.QCoreApplication.processEvents()
if prog_dialog.wasCanceled():
break
@ -1610,10 +1701,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
if tag_id == "cbi" and self.config[0].Metadata_Options__apply_transform_on_bulk_operation:
if style == "cbi" and self.config[0].Metadata_Options__apply_transform_on_bulk_operation:
md = CBLTransformer(md, self.config[0]).apply()
if ca.write_tags(md, tag_id):
if ca.write_metadata(md, style):
if not ca_saved:
success_count += 1
ca_saved = True
@ -1621,7 +1712,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
failed_list.append(ca.path)
ca.reset_cache()
ca.load_cache({*self.selected_read_tags, *self.selected_write_tags})
ca.load_cache({*self.load_data_styles, *self.save_data_styles})
prog_dialog.hide()
QtCore.QCoreApplication.processEvents()
@ -1651,19 +1742,28 @@ class TaggerWindow(QtWidgets.QMainWindow):
def identify_and_tag_single_archive(
self, ca: ComicArchive, match_results: OnlineMatchResults, dlg: AutoTagStartWindow
) -> tuple[bool, OnlineMatchResults]:
def metadata_save() -> bool:
for style in self.save_data_styles:
# write out the new data
if not ca.write_metadata(md, style):
self.auto_tag_log(
f"{metadata_styles[style].name()} save failed! Aborting any additional style saves.\n"
)
return False
return True
success = False
ii = IssueIdentifier(ca, self.config[0], self.current_talker())
# read in tags, and parse file name if not there
md, error = self.read_selected_tags(self.selected_read_tags, ca)
# read in metadata, and parse file name if not there
md, error = self.overlay_ca_read_style(self.load_data_styles, ca)
if error is not None:
QtWidgets.QMessageBox.warning(
self,
"Aborting...",
f"One or more of the read tags failed to load for {ca.path}. Aborting to prevent any possible further damage. Check log for details.",
f"One or more of the read styles failed to load for {ca.path}. Aborting to prevent any possible further damage. Check log for details.",
)
logger.error("Failed to load tags from %s: %s", self.ca.path, error)
logger.error("Failed to load metadata for %s: %s", self.ca.path, error)
return False, match_results
if md.is_empty:
@ -1678,7 +1778,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
)
if dlg.ignore_leading_digits_in_filename and md.series is not None:
# remove all leading numbers
md.series = re.sub(r"(^[\d.]*)(.*)", r"\2", md.series)
md.series = re.sub(r"([\d.]*)(.*)", r"\2", md.series)
# use the dialog specified search string
if dlg.search_string:
@ -1797,8 +1897,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
if ct_md is not None:
temp_opts = cast(ct_ns, settngs.get_namespace(self.config, True, True, True, False)[0])
if dlg.cbxClearMetadata.isChecked():
temp_opts.Auto_Tag__clear_tags
if dlg.cbxRemoveMetadata.isChecked():
temp_opts.Auto_Tag__clear_metadata
md = prepare_metadata(md, ct_md, temp_opts)
@ -1809,21 +1909,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
online_results=matches,
match_status=MatchStatus.good_match,
md=md,
tags_written=self.selected_write_tags,
tags_written=self.save_data_styles,
)
def write_Tags() -> bool:
for tag_id in self.selected_write_tags:
# write out the new data
if not ca.write_tags(md, tag_id):
self.auto_tag_log(
f"{tags[tag_id].name()} save failed! Aborting any additional tag saves.\n"
)
return False
return True
# Save tags
if write_Tags():
# Save styles
if metadata_save():
match_results.good_matches.append(res)
success = True
self.auto_tag_log("Save complete!\n")
@ -1832,13 +1922,13 @@ class TaggerWindow(QtWidgets.QMainWindow):
match_results.write_failures.append(res)
ca.reset_cache()
ca.load_cache({*self.selected_read_tags, *self.selected_write_tags})
ca.load_cache({*self.load_data_styles, *self.save_data_styles})
return success, match_results
def auto_tag(self) -> None:
ca_list = self.fileSelectionList.get_selected_archive_list()
tag_names = ", ".join([tags[tag_id].name() for tag_id in self.selected_write_tags])
styles = self.save_data_styles
if len(ca_list) == 0:
QtWidgets.QMessageBox.information(self, "Auto-Tag", "No archives selected!")
@ -1854,7 +1944,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.config[0],
(
f"You have selected {len(ca_list)} archive(s) to automatically identify and write "
+ f"{tag_names} tags to.\n\nPlease choose config below, and select OK to Auto-Tag."
+ ", ".join([metadata_styles[style].name() for style in styles])
+ " tags to.\n\nPlease choose config below, and select OK to Auto-Tag."
),
)
@ -1880,7 +1971,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.auto_tag_log(f"Auto-Tagging {prog_idx} of {len(ca_list)}\n")
self.auto_tag_log(f"{ca.path}\n")
try:
cover_idx = ca.read_tags(self.selected_read_tags[0]).get_cover_page_index_list()[0]
cover_idx = ca.read_metadata(self.load_data_styles[0]).get_cover_page_index_list()[0]
except Exception as e:
cover_idx = 0
logger.error("Failed to load metadata for %s: %s", ca.path, e)
@ -1914,7 +2005,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.atprogdialog = None
summary = ""
summary += f"Successfully added {tag_names} tags to {len(match_results.good_matches)} archive(s)\n"
summary += f"Successfully added {', '.join([metadata_styles[style].name() for style in self.save_data_styles])} tags to {len(match_results.good_matches)} archive(s)\n"
if len(match_results.multiple_matches) > 0:
summary += f"Archives with multiple matches: {len(match_results.multiple_matches)}\n"
@ -1950,7 +2041,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
matchdlg = AutoTagMatchWindow(
self,
match_results.multiple_matches,
self.selected_write_tags,
styles,
lambda match: self.current_talker().fetch_comic_data(match.issue_id),
self.config[0],
self.current_talker(),
@ -1988,7 +2079,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
if reply == QtWidgets.QMessageBox.StandardButton.Discard:
return True
if reply == QtWidgets.QMessageBox.StandardButton.Save:
self.write_tags()
self.commit_metadata()
return True
return False
return True
@ -2024,12 +2115,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
def page_browser_closed(self) -> None:
self.page_browser = None
def view_raw_tags(self, tag_id: str) -> None:
tag = tags[tag_id]
if self.comic_archive is not None and self.comic_archive.has_tags(tag.id):
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_tags(tag.id))
dlg.setWindowTitle(f"Raw {tag.name()} 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:
@ -2075,7 +2166,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
if self.dirty_flag_verification(
"File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?"
):
dlg = RenameWindow(self, ca_list, self.selected_write_tags, self.config, self.talkers)
dlg = RenameWindow(self, ca_list, self.load_data_styles, self.config, self.talkers)
dlg.setModal(True)
if dlg.exec() and self.comic_archive is not None:
self.fileSelectionList.update_selected_rows()
@ -2095,23 +2186,25 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.config[0].internal__last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
self.comic_archive = comic_archive
self.metadata, error = self.read_selected_tags(self.selected_write_tags, self.comic_archive)
self.metadata, error = self.overlay_ca_read_style(self.load_data_styles, self.comic_archive)
if error is not None:
logger.error("Failed to load tags from %s: %s", self.comic_archive.path, error)
self.exception(f"Failed to load tags from {self.comic_archive.path}, see log for details\n\n")
logger.error("Failed to load metadata for %s: %s", self.comic_archive.path, error)
self.exception(f"Failed to load metadata for {self.comic_archive.path}, see log for details\n\n")
self.update_ui_for_archive()
def read_selected_tags(self, tag_ids: list[str], ca: ComicArchive) -> tuple[GenericMetadata, Exception | None]:
def overlay_ca_read_style(
self, load_data_styles: list[str], ca: ComicArchive
) -> tuple[GenericMetadata, Exception | None]:
md = GenericMetadata()
error = None
try:
for tag_id in tag_ids:
metadata = ca.read_tags(tag_id)
for style in load_data_styles:
metadata = ca.read_metadata(style)
md.overlay(
metadata,
mode=self.config[0].Metadata_Options__tag_merge,
merge_lists=self.config[0].Metadata_Options__tag_merge_lists,
mode=self.config[0].Metadata_Options__comic_merge,
merge_lists=self.config[0].Metadata_Options__comic_merge_lists,
)
except Exception as e:
error = e

View File

@ -78,12 +78,12 @@
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxClearMetadata">
<widget class="QCheckBox" name="cbxRemoveMetadata">
<property name="toolTip">
<string>Removes existing tags before applying downloaded metadata</string>
<string>Removes existing metadata before applying retrieved metadata</string>
</property>
<property name="text">
<string>Clear Existing tags when downloading metadata</string>
<string>Clear Existing Metadata during import</string>
</property>
</widget>
</item>
@ -129,7 +129,7 @@
</sizepolicy>
</property>
<property name="text">
<string>Specify series search for all selected archives:</string>
<string>Specify series search string for all selected archives:</string>
</property>
</widget>
</item>

View File

@ -10,6 +10,7 @@ from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import QEvent, QModelIndex, QPoint, QRect, QSize, Qt, pyqtSignal
from comicapi.utils import StrEnum
from comictaggerlib.graphics import graphics_path
class ClickedButtonEnum(StrEnum):
@ -73,7 +74,7 @@ class CheckableComboBox(QtWidgets.QComboBox):
def addItem(self, text: str, data: Any = None) -> None:
super().addItem(text, data)
# Need to enable the checkboxes and require one checked item
# Expected that state of *all* checkboxes will be set ('adjust_tags_combo' in taggerwindow.py)
# Expected that state of *all* checkboxes will be set ('adjust_save_style_combo' in taggerwindow.py)
if self.count() == 1:
self.model().item(0).setCheckState(Qt.CheckState.Checked)
@ -132,8 +133,8 @@ class ReadStyleItemDelegate(QtWidgets.QStyledItemDelegate):
super().__init__()
self.combobox = parent
self.down_icon = QtGui.QImage(":/graphics/down.png")
self.up_icon = QtGui.QImage(":/graphics/up.png")
self.down_icon = QtGui.QImage(str(graphics_path / "down.png"))
self.up_icon = QtGui.QImage(str(graphics_path / "up.png"))
self.button_width = self.down_icon.width()
self.button_padding = 5
@ -276,7 +277,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
super().__init__(*args, **kwargs)
itemDelegate = ReadStyleItemDelegate(self)
itemDelegate.setToolTip(
"Select which read tag(s) to use", "Move item up in priority", "Move item down in priority"
"Select which read style(s) to use", "Move item up in priority", "Move item down in priority"
)
self.setItemDelegate(itemDelegate)
@ -344,7 +345,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
def addItem(self, text: str, data: Any = None) -> None:
super().addItem(text, data)
# Need to enable the checkboxes and require one checked item
# Expected that state of *all* checkboxes will be set ('adjust_tags_combo' in taggerwindow.py)
# Expected that state of *all* checkboxes will be set ('adjust_save_style_combo' in taggerwindow.py)
if self.count() == 1:
self.model().item(0).setCheckState(Qt.CheckState.Checked)

View File

@ -50,10 +50,10 @@
</column>
<column>
<property name="text">
<string>Tags</string>
<string>MD</string>
</property>
<property name="toolTip">
<string>Tags</string>
<string>Metadata</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>

View File

@ -11,6 +11,8 @@ from collections.abc import Sequence
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QWidget
from comictaggerlib.graphics import graphics_path
logger = logging.getLogger(__name__)
try:
@ -141,7 +143,7 @@ if qt_available:
pass
# if still nothing, go with default image
if not success:
img.load(":/graphics/nocover.png")
img.load(str(graphics_path / "nocover.png"))
return img
def qt_error(msg: str, e: Exception | None = None) -> None:

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1095</width>
<height>642</height>
<width>703</width>
<height>615</height>
</rect>
</property>
<property name="windowTitle">
@ -190,7 +190,7 @@
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Enable the publisher filter:</string>
<string>Always use Publisher Filter on manual searches:</string>
</property>
<property name="buddy">
<cstring>cbxUseFilter</cstring>
@ -211,7 +211,7 @@
<widget class="QLabel" name="label_4">
<property name="text">
<string>Publisher Filter:
1 Publisher per line</string>
One Publisher per line</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
@ -274,8 +274,7 @@
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>These settings are for the automatic issue identifier which searches online for matches.
Hover the mouse over an option for more info.</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These settings are for the automatic issue identifier which searches online for matches. &lt;/p&gt;&lt;p&gt;Hover the mouse over an entry field for more info.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
@ -303,6 +302,13 @@ Hover the mouse over an option for more info.</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="cbxClearFormBeforePopulating">
<property name="text">
<string>Clear all existing metadata during import, default is to merge metadata.</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tFilenameParser">
@ -411,7 +417,7 @@ Hover the mouse over an option for more info.</string>
</widget>
<widget class="QWidget" name="tComicTalkers">
<attribute name="title">
<string>Metadata Download</string>
<string>Metadata Sources</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4"/>
</widget>
@ -454,7 +460,7 @@ Hover the mouse over an option for more info.</string>
<number>6</number>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="cbxShortTagNames">
<widget class="QCheckBox" name="cbxShortMetadataNames">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -462,27 +468,20 @@ Hover the mouse over an option for more info.</string>
</sizepolicy>
</property>
<property name="toolTip">
<string>Use the short name for tags (CBI, CR, etc.)</string>
<string>Use the short name for the metadata styles (CBI, CR, etc.)</string>
</property>
<property name="text">
<string>Use &quot;short&quot; names for tags</string>
<string>Use &quot;short&quot; names for metadata styles</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="cbxEnableCR">
<property name="toolTip">
<string>Turn off to only use the CIX tags</string>
<string>Turn off to only use the CIX metadata type</string>
</property>
<property name="text">
<string>Enable the ComicRack metadata tags (needs a restart)</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbxClearFormBeforePopulating">
<property name="text">
<string>Clear all existing tags during metadata download, default is to merge downloaded metadata with existing tags.</string>
<string>Enable ComicRack Metadata Type (needs a restart)</string>
</property>
</widget>
</item>
@ -525,14 +524,14 @@ Hover the mouse over an option for more info.</string>
<item>
<widget class="QCheckBox" name="cbxApplyCBLTransformOnCVIMport">
<property name="text">
<string>Apply CBL Transforms on metadata download</string>
<string>Apply CBL Transforms on ComicVine Import</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
<property name="text">
<string>Apply CBL Transforms when Copying to CBL Tags</string>
<string>Apply CBL Transforms on Batch Copy Operations to CBL Tags</string>
</property>
</widget>
</item>
@ -641,17 +640,14 @@ Hover the mouse over an option for more info.</string>
</property>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_7">
<property name="toolTip">
<string>The merge mode to use when reading multiple tags from a single comic book archive. ComicRack, ComicBookInfo, etc...</string>
</property>
<property name="title">
<string>Tags Merge</string>
<string>Comic Merge</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="lblTagsMergeMode">
<widget class="QLabel" name="lblOverlayComic">
<property name="toolTip">
<string>The merge mode to use when reading multiple tags from a single comic book archive. ComicRack, ComicBookInfo, etc...</string>
<string>The merge mode to use when reading multiple metadata styles from a single comic archive (ComicRack, ComicBookInfo, etc.)</string>
</property>
<property name="text">
<string>Merge Mode:</string>
@ -660,12 +656,12 @@ Hover the mouse over an option for more info.</string>
<number>6</number>
</property>
<property name="buddy">
<cstring>cbTagsMergeMode</cstring>
<cstring>cbxOverlayReadStyle</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cbTagsMergeMode">
<widget class="QComboBox" name="cbMergeModeComic">
<property name="enabled">
<bool>true</bool>
</property>
@ -676,14 +672,14 @@ Hover the mouse over an option for more info.</string>
</sizepolicy>
</property>
<property name="toolTip">
<string>The merge mode to use when reading multiple metadata types from a single comic book archive. ComicRack, ComicBookInfo, etc...</string>
<string>The merge mode to use when reading multiple metadata styles from a comic archive (ComicRack, ComicBookInfo, etc.)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="cbxTagsMergeLists">
<widget class="QCheckBox" name="cbxMergeListsComic">
<property name="toolTip">
<string>Merge lists (characters, tags, locations, etc.) together or follow the Merge Mode</string>
<string>Merge lists (characters, tags, locations, etc.) together or the &quot;new&quot; list replaces the old</string>
</property>
<property name="text">
<string>Merge Lists</string>
@ -695,17 +691,14 @@ Hover the mouse over an option for more info.</string>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="groupBox_8">
<property name="toolTip">
<string>The merge mode to use when downloading metadata from Comic Vine, Metron, GCD, etc...</string>
</property>
<property name="title">
<string>Download Merge</string>
<string>Metadata Merge</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="lblDownloadMergeMode">
<widget class="QLabel" name="lblOverlayMetadata">
<property name="toolTip">
<string>The merge mode to use when downloading metadata from Comic Vine, Metron, GCD, etc...</string>
<string>The merge mode to use when fetching metadata from a Metadata Source (Comic Vine, Metron, GCD, etc.)</string>
</property>
<property name="text">
<string>Merge Mode:</string>
@ -714,12 +707,12 @@ Hover the mouse over an option for more info.</string>
<number>6</number>
</property>
<property name="buddy">
<cstring>cbDownloadMergeMode</cstring>
<cstring>cbxOverlaySource</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cbDownloadMergeMode">
<widget class="QComboBox" name="cbMergeModeMetadata">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -727,14 +720,14 @@ Hover the mouse over an option for more info.</string>
</sizepolicy>
</property>
<property name="toolTip">
<string>The merge mode to use when downloading metadata from Comic Vine, Metron, GCD, etc...</string>
<string>The merge mode to use when fetching data from a Metadata Source (Comic Vine, Metron, GCD, etc.)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="cbxMergeListsMetadata">
<property name="toolTip">
<string>Merge lists (characters, tags, locations, etc.) together or follow the Merge Mode</string>
<string>Merge lists (characters, tags, locations, etc.) together or the &quot;new&quot; list replaces the old</string>
</property>
<property name="text">
<string>Merge Lists</string>
@ -802,13 +795,13 @@ Hover the mouse over an option for more info.</string>
<bool>false</bool>
</property>
<property name="plainText">
<string>Overlays all non-empty values of the new metadata (e.g. Comic Vine) on top of the existing metadata.
<string>Overlays all the non-empty values of the new metadata (e.g. Comic Vine) on top of the current metadata.
See the Lists tab for controlling how lists are handled.
Example:
existing(Series=the batman, Issue=1, Tags=[batman,joker,robin])
+ new(Series=Batman, Publisher=DC Comics, Tags=[mystery,action])
(Series=the batman, Issue=1, Tags=[batman,joker,robin])
+ (Series=Batman, Publisher=DC Comics, Tags=[mystery,action])
= (Series=Batman, Issue=1, Publisher=DC Comics, Tags=[mystery,action])</string>
</property>
@ -842,13 +835,13 @@ existing(Series=the batman, Issue=1, Tags=[batman,joker,robin])
<item>
<widget class="QPlainTextEdit" name="addTextEdit">
<property name="plainText">
<string>Adds any metadata that is is missing in the existing metadata but present in the new metadata.
<string>Adds any metadata that is is missing in the current metadata but present in the new metadata (e.g. Comic Vine).
See the Lists tab for controlling how lists are handled.
Example:
existing(Series=batman, Issue=1, Tags=[batman,joker,robin])
+ new(Series=Superman, Issue=10, Publisher=DC Comics, Tags=[mystery,action])
(Series=batman, Issue=1, Tags=[batman,joker,robin])
+ (Series=Superman, Issue=10, Publisher=DC Comics, Tags=[mystery,action])
= (Series=batman, Issue=1, Tags=[batman,joker,robin], Publisher=DC Comics)</string>
</property>
@ -887,7 +880,6 @@ existing(Series=batman, Issue=1, Tags=[batman,joker,robin])
Example Merge:
(Tags=[batman,joker,robin])
+ (Tags=[mystery,action])
= (Tags=[batman,joker,robin,mystery,action])</string>
</property>
<property name="textInteractionFlags">

View File

@ -22,10 +22,6 @@
<property name="windowTitle">
<string>ComicTagger</string>
</property>
<property name="windowIcon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/app.png</normaloff>:/graphics/app.png</iconset>
</property>
<property name="unifiedTitleAndToolBarOnMac">
<bool>true</bool>
</property>
@ -45,23 +41,12 @@
<property name="enabled">
<bool>true</bool>
</property>
<property name="styleSheet">
<string notr="true">QSplitter::handle:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ccc);
border: 1px solid #777;
width: 13px;
margin-top: 2px;
margin-bottom: 2px;
border-radius: 4px;
}
</string>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="handleWidth">
<number>8</number>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
@ -79,12 +64,11 @@ border-radius: 4px;
</property>
<item row="0" column="0">
<widget class="QLabel" name="lbl_md_source">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignVCenter</set>
</property>
<property name="text">
<string>Metadata
Source</string>
<string>Data Source</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
@ -92,32 +76,26 @@ Source</string>
<widget class="QComboBox" name="cbx_sources"/>
</item>
<item row="1" column="1">
<widget class="CheckableOrderComboBox" name="cbSelectedReadTags">
<widget class="CheckableOrderComboBox" name="cbLoadDataStyle">
<property name="toolTip">
<string>At least one read style must be selected</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="CheckableComboBox" name="cbSelectedWriteTags"/>
<widget class="CheckableComboBox" name="cbSaveDataStyle"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_read_style">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignVCenter</set>
</property>
<property name="text">
<string>Read Tags</string>
<string>Read Style</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lbl_modify_style">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignVCenter</set>
</property>
<property name="text">
<string>Write Tags</string>
<string>Modify Styles</string>
</property>
</widget>
</item>
@ -1026,10 +1004,6 @@ Source</string>
<height>16777215</height>
</size>
</property>
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/open.png</normaloff>:/graphics/open.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
@ -1307,7 +1281,7 @@ Source</string>
</widget>
<widget class="QMenu" name="menuTags">
<property name="title">
<string>Metadata</string>
<string>Tags</string>
</property>
<addaction name="actionClearEntryForm"/>
<addaction name="actionParse_Filename"/>
@ -1355,208 +1329,92 @@ Source</string>
<bool>false</bool>
</attribute>
<addaction name="separator"/>
<addaction name="actionLoad"/>
<addaction name="actionLoadFolder"/>
<addaction name="actionWrite_Tags"/>
<addaction name="actionSearchOnline"/>
<addaction name="actionLiteralSearch"/>
<addaction name="actionAutoIdentify"/>
<addaction name="actionAutoImprint"/>
<addaction name="actionClearEntryForm"/>
<addaction name="actionPageBrowser"/>
</widget>
<action name="actionAbout">
<action name="actionLoad">
<property name="text">
<string>About ComicTagger</string>
</property>
<property name="menuRole">
<enum>QAction::AboutRole</enum>
<string>Open</string>
</property>
</action>
<action name="actionAddWebLink">
<action name="actionWrite_Tags">
<property name="text">
<string>Add Item</string>
<string>Save Tags</string>
</property>
</action>
<action name="actionApplyCBLTransform">
<action name="actionRepackage">
<property name="text">
<string>Apply CBL Transform</string>
</property>
<property name="statusTip">
<string>Modify metadata specifically for the CBL tags</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+T</string>
</property>
</action>
<action name="actionAutoIdentify">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/auto.png</normaloff>:/graphics/auto.png</iconset>
</property>
<property name="text">
<string>Auto-Identify</string>
</property>
<property name="iconText">
<string>Auto-Identify</string>
</property>
<property name="statusTip">
<string>Search online for metadata and auto-identify best match for single archive</string>
</property>
<property name="shortcut">
<string>Ctrl+I</string>
</property>
</action>
<action name="actionAutoImprint">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/auto.png</normaloff>:/graphics/auto.png</iconset>
</property>
<property name="text">
<string>Auto Imprint</string>
</property>
<property name="statusTip">
<string>Normalize the publisher and map imprints to their parent publisher (e.g. Vertigo is an imprint of DC Comics)</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+I</string>
</property>
</action>
<action name="actionAutoTag">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/autotag.png</normaloff>:/graphics/autotag.png</iconset>
</property>
<property name="text">
<string>Auto-Tag</string>
</property>
<property name="toolTip">
<string>Auto-Tag multiple archives</string>
</property>
<property name="statusTip">
<string>Search online for metadata, auto-identify best match, and save to archive</string>
</property>
<property name="shortcut">
<string>Ctrl+T</string>
</property>
</action>
<action name="actionClearEntryForm">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/clear.png</normaloff>:/graphics/clear.png</iconset>
</property>
<property name="text">
<string>Clear Form</string>
</property>
<property name="toolTip">
<string>Clear all the data on the screen</string>
</property>
<property name="statusTip">
<string>Clear all the data on the screen</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+C</string>
</property>
</action>
<action name="actionComicTaggerForum">
<property name="text">
<string>ComicTagger Discussions...</string>
</property>
</action>
<action name="actionCopyTags">
<property name="text">
<string>Copy Tags</string>
</property>
<property name="toolTip">
<string>Copy the selected 'read' tags to the selected 'write' tags</string>
</property>
<property name="statusTip">
<string>Copy the selected 'read' tags to the selected 'write' tags</string>
</property>
<property name="shortcut">
<string>Ctrl+C</string>
<string>Export as Zip Archive</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>Exit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
<property name="menuRole">
<enum>QAction::QuitRole</enum>
</action>
<action name="actionAbout">
<property name="text">
<string>About ComicTagger</string>
</property>
</action>
<action name="actionLiteralSearch">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/search.png</normaloff>:/graphics/search.png</iconset>
</property>
<action name="actionParse_Filename">
<property name="text">
<string>Literal Search</string>
</property>
<property name="statusTip">
<string>perform a literal search on the series and return the first 50 results</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+L</string>
<string>Parse Filename</string>
</property>
</action>
<action name="actionLoad">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/open.png</normaloff>:/graphics/open.png</iconset>
</property>
<action name="actionSearchOnline">
<property name="text">
<string>Open</string>
</property>
<property name="statusTip">
<string>Load comic archive</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
<string>Search Online</string>
</property>
</action>
<action name="actionLoadFolder">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/longbox.png</normaloff>:/graphics/longbox.png</iconset>
</property>
<action name="actionClearEntryForm">
<property name="text">
<string>Open Folder</string>
</property>
<property name="statusTip">
<string>Load folder containing comic archives</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+O</string>
<string>Clear Form</string>
</property>
</action>
<action name="actionLogWindow">
<action name="actionRemoveCRTags">
<property name="text">
<string>Show Log Window</string>
<string>Remove ComicRack Tags</string>
</property>
</action>
<action name="actionOpenFolderAsComic">
<action name="actionRemoveCBLTags">
<property name="text">
<string>Open Folder as a Comic</string>
<string>Remove ComicBookLover Tags</string>
</property>
</action>
<action name="actionReloadCBLTags">
<property name="text">
<string>Reload ComicBookLover Tags</string>
</property>
</action>
<action name="actionReloadCRTags">
<property name="text">
<string>Reload ComicRack Tags</string>
</property>
</action>
<action name="actionReloadAuto">
<property name="text">
<string>Reload Selected Tag Style</string>
</property>
</action>
<action name="actionSettings">
<property name="text">
<string>Settings...</string>
</property>
</action>
<action name="actionAutoIdentify">
<property name="text">
<string>Auto-Identify</string>
</property>
<property name="iconText">
<string>Auto-Identify</string>
</property>
<property name="toolTip">
<string>Load images from a folder as a single comic archive</string>
<string>Search online for tags and auto-identify best match for single archive</string>
</property>
<property name="statusTip">
<string>Open Folder as a Comic</string>
</property>
<property name="shortcut">
<string>Ctrl+Alt+Shift+O</string>
<string>Search online for tags and auto-identify best match for single archive</string>
</property>
</action>
<action name="actionPageBrowser">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/browse.png</normaloff>:/graphics/browse.png</iconset>
</property>
<property name="text">
<string>Show Page Browser</string>
</property>
@ -1564,89 +1422,20 @@ Source</string>
<string>Page Browser</string>
</property>
<property name="toolTip">
<string>Show the Page Browser</string>
<string>Show the Page Browser to inspect the comic</string>
</property>
<property name="statusTip">
<string>Show the Page Browser to inspect the comic</string>
</property>
<property name="shortcut">
<string>Ctrl+P</string>
</action>
<action name="actionViewRawCRTags">
<property name="text">
<string>View Raw ComicRack Tags</string>
</property>
</action>
<action name="actionParse_Filename">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/parse.png</normaloff>:/graphics/parse.png</iconset>
</property>
<action name="actionViewRawCBLTags">
<property name="text">
<string>Parse Filename</string>
</property>
<property name="statusTip">
<string>Parse metadata from filename</string>
</property>
<property name="shortcut">
<string>Ctrl+F</string>
</property>
</action>
<action name="actionParse_Filename_split_words">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/parse.png</normaloff>:/graphics/parse.png</iconset>
</property>
<property name="text">
<string>Parse Filename and split words</string>
</property>
<property name="toolTip">
<string>Attempt to split words before parsing the filename (eg 'MonsterFunHalloweenSpooktacularSpecial.cbz' becomes 'Monster Fun Halloween Spook t acul ar Special.cbz')</string>
</property>
<property name="statusTip">
<string>Parse Filename and split words</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+F</string>
</property>
</action>
<action name="actionReCalcPageDims">
<property name="text">
<string>Re-Calculate Page Dimensions</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+R</string>
</property>
</action>
<action name="actionRemoveAuto">
<property name="text">
<string>Remove Current 'Modify' tag(s)</string>
</property>
<property name="statusTip">
<string>Remove currently selected modify tag(s) from the archive</string>
</property>
<property name="shortcut">
<string>Ctrl+D</string>
</property>
</action>
<action name="actionRemoveWebLink">
<property name="text">
<string>Remove Web Link</string>
</property>
<property name="statusTip">
<string>Remove Web Link</string>
</property>
</action>
<action name="actionRename">
<property name="text">
<string>Rename</string>
</property>
<property name="shortcut">
<string>Ctrl+R</string>
</property>
</action>
<action name="actionRepackage">
<property name="text">
<string>Export as Zip Archive</string>
</property>
<property name="shortcut">
<string>Ctrl+E</string>
<string>View Raw ComicBookLover Tags</string>
</property>
</action>
<action name="actionReportBug">
@ -1654,53 +1443,99 @@ Source</string>
<string>Report Bug...</string>
</property>
</action>
<action name="actionSearchOnline">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/search.png</normaloff>:/graphics/search.png</iconset>
</property>
<action name="actionComicTaggerForum">
<property name="text">
<string>Search Online</string>
</property>
<property name="statusTip">
<string>Search Online</string>
</property>
<property name="shortcut">
<string>Alt+S</string>
</property>
</action>
<action name="actionSettings">
<property name="text">
<string>Settings...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
<property name="menuRole">
<enum>QAction::PreferencesRole</enum>
<string>ComicTagger Discussions...</string>
</property>
</action>
<action name="actionWiki">
<property name="text">
<string>Online Docs...</string>
</property>
<property name="shortcut">
<string>F1</string>
</action>
<action name="actionRemoveAuto">
<property name="text">
<string>Remove Current 'Modify' Tag Style(s)</string>
</property>
</action>
<action name="actionWrite_Tags">
<property name="icon">
<iconset resource="../graphics/graphics.qrc">
<normaloff>:/graphics/save.png</normaloff>:/graphics/save.png</iconset>
</property>
<action name="actionRename">
<property name="text">
<string>Save tag(s)</string>
<string>Rename</string>
</property>
</action>
<action name="actionApplyCBLTransform">
<property name="text">
<string>Apply CBL Transform</string>
</property>
</action>
<action name="actionReCalcPageDims">
<property name="text">
<string>Re-Calculate Page Dimensions</string>
</property>
</action>
<action name="actionLoadFolder">
<property name="text">
<string>Open Folder</string>
</property>
</action>
<action name="actionCopyTags">
<property name="text">
<string>Copy Tags</string>
</property>
</action>
<action name="actionAutoTag">
<property name="text">
<string>Auto-Tag</string>
</property>
<property name="toolTip">
<string>Search online for tags,auto-identify best match, and save to archive</string>
</property>
<property name="statusTip">
<string>Write tag(s) to file</string>
<string>Search online for tags,auto-identify best match, and save to archive</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</action>
<action name="actionAutoImprint">
<property name="text">
<string>Auto Imprint</string>
</property>
<property name="toolTip">
<string>Normalize the publisher and map imprints to their parent publisher (e.g. Vertigo is an imprint of DC Comics)</string>
</property>
</action>
<action name="actionParse_Filename_split_words">
<property name="text">
<string>Parse Filename and split words</string>
</property>
<property name="toolTip">
<string>Parse Filename and split words</string>
</property>
</action>
<action name="actionLogWindow">
<property name="text">
<string>Show Log Window</string>
</property>
</action>
<action name="actionLiteralSearch">
<property name="text">
<string>Literal Search</string>
</property>
<property name="toolTip">
<string>perform a literal search on the series and return the first 50 results</string>
</property>
</action>
<action name="actionOpenFolderAsComic">
<property name="text">
<string>Open Folder as Comic</string>
</property>
</action>
<action name="actionAddWebLink">
<property name="text">
<string>Add Item</string>
</property>
</action>
<action name="actionRemoveWebLink">
<property name="text">
<string>Remove Web Link</string>
</property>
</action>
</widget>
@ -1717,9 +1552,7 @@ Source</string>
<header>comictaggerlib.ui.customwidgets</header>
</customwidget>
</customwidgets>
<resources>
<include location="../graphics/graphics.qrc"/>
</resources>
<resources/>
<connections>
<connection>
<sender>btnAddWebLink</sender>
@ -1728,8 +1561,8 @@ Source</string>
<slot>trigger()</slot>
<hints>
<hint type="sourcelabel">
<x>359</x>
<y>108</y>
<x>900</x>
<y>536</y>
</hint>
<hint type="destinationlabel">
<x>-1</x>
@ -1744,8 +1577,8 @@ Source</string>
<slot>trigger()</slot>
<hints>
<hint type="sourcelabel">
<x>359</x>
<y>108</y>
<x>900</x>
<y>576</y>
</hint>
<hint type="destinationlabel">
<x>-1</x>

View File

@ -10,6 +10,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.ctsettings import ct_ns, group_for_plugin
from comictaggerlib.graphics import graphics_path
from comictalker.comictalker import ComicTalker
logger = logging.getLogger(__name__)
@ -36,8 +37,8 @@ class PasswordEdit(QtWidgets.QLineEdit):
def __init__(self, show_visibility: bool = True, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.visibleIcon = QtGui.QIcon(":/graphics/eye.svg")
self.hiddenIcon = QtGui.QIcon(":/graphics/hidden.svg")
self.visibleIcon = QtGui.QIcon(str(graphics_path / "eye.svg"))
self.hiddenIcon = QtGui.QIcon(str(graphics_path / "hidden.svg"))
self.setEchoMode(QtWidgets.QLineEdit.Password)

View File

@ -31,7 +31,7 @@ from pyrate_limiter import Limiter, RequestRate
from typing_extensions import Required, TypedDict
from comicapi import utils
from comicapi.genericmetadata import ComicSeries, GenericMetadata, MetadataOrigin
from comicapi.genericmetadata import ComicSeries, GenericMetadata, TagOrigin
from comicapi.issuestring import IssueString
from comicapi.utils import LocationParseError, parse_url
from comictalker import talker_utils
@ -633,7 +633,7 @@ class ComicVineTalker(ComicTalker):
def _map_comic_issue_to_metadata(self, issue: CVIssue, series: ComicSeries) -> GenericMetadata:
md = GenericMetadata(
data_origin=MetadataOrigin(self.id, self.name),
tag_origin=TagOrigin(self.id, self.name),
issue_id=utils.xlate(issue.get("id")),
series_id=series.id,
title_aliases=set(utils.split(issue.get("aliases"), "\n")),

View File

@ -1,11 +1,10 @@
[tool.black]
line-length = 120
force-exclude = "scripts"
extend-exclude = "comictaggerlib/graphics/resources.py"
[tool.isort]
line_length = 120
extend_skip = ["scripts", "comictaggerlib/graphics/resources.py"]
extend_skip = ["scripts"]
profile = "black"
[build-system]

View File

@ -64,10 +64,10 @@ comicapi.archiver =
sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver
rar = comicapi.archivers.rar:RarArchiver
folder = comicapi.archivers.folder:FolderArchiver
comicapi.tags =
cr = comicapi.tags.comicrack:ComicRack
cbi = comicapi.tags.comicbookinfo:ComicBookInfo
comet = comicapi.tags.comet:CoMet
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 =
@ -242,7 +242,6 @@ deps =
extras =
all
commands =
pyrcc5 comictaggerlib/graphics/graphics.qrc -o comictaggerlib/graphics/resources.py
pyinstaller -y build-tools/comictagger.spec
python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu
@ -313,7 +312,6 @@ per-file-ignores =
tests/*: L
[mypy]
exclude = comictaggerlib/graphics/resources.py
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
@ -331,6 +329,3 @@ check_untyped_defs = false
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = false
[mypy-comictaggerlib.graphics.resources]
ignore_errors = True

View File

@ -271,13 +271,13 @@ metadata_prepared = (
(
(
comicapi.genericmetadata.GenericMetadata(
issue_id="123", data_origin=comicapi.genericmetadata.MetadataOrigin("SOURCE", "Source")
issue_id="123", tag_origin=comicapi.genericmetadata.TagOrigin("SOURCE", "Source")
),
comicapi.genericmetadata.GenericMetadata(),
),
comicapi.genericmetadata.GenericMetadata(
issue_id="123",
data_origin=comicapi.genericmetadata.MetadataOrigin("SOURCE", "Source"),
tag_origin=comicapi.genericmetadata.TagOrigin("SOURCE", "Source"),
notes="Tagged with ComicTagger 1.3.2a5 using info from Source on 2022-04-16 15:52:26. [Issue ID 123]",
),
),

View File

@ -170,7 +170,7 @@ comic_series_result = comicapi.genericmetadata.ComicSeries(
)
date = utils.parse_date_str(cv_issue_result["results"]["cover_date"])
comic_issue_result = comicapi.genericmetadata.GenericMetadata(
data_origin=comicapi.genericmetadata.MetadataOrigin("comicvine", "Comic Vine"),
tag_origin=comicapi.genericmetadata.TagOrigin("comicvine", "Comic Vine"),
title_aliases=set(),
month=date[1],
year=date[2],
@ -190,7 +190,7 @@ comic_issue_result = comicapi.genericmetadata.GenericMetadata(
cv_md = comicapi.genericmetadata.GenericMetadata(
is_empty=False,
data_origin=comicapi.genericmetadata.MetadataOrigin("comicvine", "Comic Vine"),
tag_origin=comicapi.genericmetadata.TagOrigin("comicvine", "Comic Vine"),
issue_id=str(cv_issue_result["results"]["id"]),
series=cv_issue_result["results"]["volume"]["name"],
series_id=str(cv_issue_result["results"]["volume"]["id"]),

View File

@ -32,32 +32,32 @@ def test_getPageNameList():
def test_page_type_read(cbz):
md = cbz.read_tags("cr")
md = cbz.read_metadata("cr")
assert isinstance(md.pages[0]["type"], str)
def test_read_tags(cbz, md_saved):
md = cbz.read_tags("cr")
def test_metadata_read(cbz, md_saved):
md = cbz.read_metadata("cr")
assert md == md_saved
def test_write_cr(tmp_comic):
md = tmp_comic.read_tags("cr")
def test_save_cr(tmp_comic):
md = tmp_comic.read_metadata("cr")
md.apply_default_page_list(tmp_comic.get_page_name_list())
assert tmp_comic.write_tags(md, "cr")
assert tmp_comic.write_metadata(md, "cr")
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
def test_write_cbi(tmp_comic):
md = tmp_comic.read_tags("cr")
def test_save_cbi(tmp_comic):
md = tmp_comic.read_metadata("cr")
md.apply_default_page_list(tmp_comic.get_page_name_list())
assert tmp_comic.write_tags(md, "cbi")
assert tmp_comic.write_metadata(md, "cbi")
md = tmp_comic.read_tags("cbi")
md = tmp_comic.read_metadata("cbi")
@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support")
@ -67,9 +67,9 @@ def test_save_cr_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_tags(comicapi.genericmetadata.md_test, "cr")
assert tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cr")
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
# This is a fake CBR we don't need to care about the pages for this test
md.pages = []
@ -84,28 +84,28 @@ def test_save_cbi_rar(tmp_path, md_saved):
tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name)
assert tmp_comic.seems_to_be_a_comic_archive()
assert tmp_comic.write_tags(comicapi.genericmetadata.md_test, "cbi")
assert tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cbi")
md = tmp_comic.read_tags("cbi")
supported_attributes = comicapi.comicarchive.tags["cbi"].supported_attributes
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_write(tmp_comic):
md = tmp_comic.read_tags("cr")
def test_page_type_save(tmp_comic):
md = tmp_comic.read_metadata("cr")
t = md.pages[0]
t["type"] = ""
assert tmp_comic.write_tags(md, "cr")
assert tmp_comic.write_metadata(md, "cr")
md = tmp_comic.read_tags("cr")
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_tags(comicapi.genericmetadata.md_test, "cr")
result = tmp_comic.write_metadata(comicapi.genericmetadata.md_test, "cr")
assert not result
@ -135,7 +135,7 @@ def test_copy_from_archive(archiver, tmp_path, cbz, md_saved):
assert comic_archive.seems_to_be_a_comic_archive()
assert set(cbz.archiver.get_filename_list()) == set(comic_archive.archiver.get_filename_list())
md = comic_archive.read_tags("cr")
md = comic_archive.read_metadata("cr")
assert md == md_saved

View File

@ -161,7 +161,7 @@ def md():
@pytest.fixture
def md_saved():
yield comicapi.genericmetadata.md_test.replace(data_origin=None, issue_id=None, series_id=None)
yield comicapi.genericmetadata.md_test.replace(tag_origin=None, issue_id=None, series_id=None)
# manually seeds publishers

View File

@ -18,14 +18,14 @@ def test_save(
mock_now,
) -> None:
# Overwrite the series so it has definitely changed
tmp_comic.write_tags(md_saved.replace(series="nothing"), "cr")
tmp_comic.write_metadata(md_saved.replace(series="nothing"), "cr")
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
# Check that it changed
assert md != md_saved
# Clear the cached tags
# Clear the cached metadata
tmp_comic.reset_cache()
# Setup the app
@ -40,15 +40,15 @@ def test_save(
# Use the temporary comic we created
config[0].Runtime_Options__files = [tmp_comic.path]
# Read and save ComicRack tags
config[0].Runtime_Options__tags_read = ["cr"]
config[0].Runtime_Options__tags_write = ["cr"]
config[0].Runtime_Options__type_read = ["cr"]
config[0].Runtime_Options__type_modify = ["cr"]
# Search using the correct series since we just put the wrong series name in the CBZ
config[0].Auto_Tag__metadata = comicapi.genericmetadata.GenericMetadata(series=md_saved.series)
# Run ComicTagger
CLI(config[0], talkers).run()
# Read the CBZ
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
# This is inserted here because otherwise several other tests
# unrelated to comicvine need to be re-worked
@ -72,7 +72,7 @@ def test_delete(
md_saved,
mock_now,
) -> None:
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
# Check that the metadata starts correct
assert md == md_saved
@ -90,14 +90,14 @@ def test_delete(
# Use the temporary comic we created
config[0].Runtime_Options__files = [tmp_comic.path]
# Delete ComicRack tags
config[0].Runtime_Options__tags_write = ["cr"]
config[0].Runtime_Options__type_modify = ["cr"]
# Run ComicTagger
CLI(config[0], talkers).run()
# Read the CBZ
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
# The default page list is set on load if the comic has the requested tags
# The default page list is set on load if the comic has the requested metadata style
empty_md = comicapi.genericmetadata.GenericMetadata()
# Validate that we got an empty metadata back
@ -111,7 +111,7 @@ def test_rename(
md_saved,
mock_now,
) -> None:
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
# Check that the metadata starts correct
assert md == md_saved
@ -140,7 +140,7 @@ def test_rename(
tmp_comic.path = tmp_comic.path.parent / (md.series + ".cbz")
# Read the CBZ
md = tmp_comic.read_tags("cr")
md = tmp_comic.read_metadata("cr")
# Validate that we got the correct metadata back
assert md == md_saved

View File

@ -57,7 +57,7 @@ def test_get_issue_cover_match_score(cbz, config, comicvine_api):
def test_search(cbz, config, comicvine_api):
config, definitions = config
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, config, comicvine_api)
result, issues = ii.identify(cbz, cbz.read_tags("cr"))
result, issues = ii.identify(cbz, cbz.read_metadata("cr"))
cv_expected = IssueResult(
series=f"{testing.comicvine.cv_volume_result['results']['name']} ({testing.comicvine.cv_volume_result['results']['start_year']})",
distance=0,

View File

@ -7,25 +7,27 @@ import comicapi.genericmetadata
import testing.comicdata
from comictaggerlib.md import prepare_metadata
tags = []
metadata_styles = []
for x in entry_points(group="comicapi.tag"):
tag = x.load()
supported = tag.enabled
for x in entry_points(group="comicapi.metadata"):
meetadata = x.load()
supported = meetadata.enabled
exe_found = True
tags.append(pytest.param(tag, marks=pytest.mark.xfail(not supported, reason="tags not enabled")))
metadata_styles.append(
pytest.param(meetadata, marks=pytest.mark.xfail(not supported, reason="metadata not enabled"))
)
@pytest.mark.parametrize("tag_type", tags)
def test_metadata(mock_version, tmp_comic, md_saved, tag_type):
tag = tag_type(mock_version[0])
supported_attributes = tag.supported_attributes
tag.write_tags(comicapi.genericmetadata.md_test, tmp_comic.archiver)
written_metadata = tag.read_tags(tmp_comic.archiver)
@pytest.mark.parametrize("metadata", metadata_styles)
def test_metadata(mock_version, tmp_comic, md_saved, metadata):
md_style = metadata(mock_version[0])
supported_attributes = md_style.supported_attributes
md_style.set_metadata(comicapi.genericmetadata.md_test, tmp_comic.archiver)
written_metadata = md_style.get_metadata(tmp_comic.archiver)
md = md_saved.get_clean_metadata(*supported_attributes)
# Hack back in the pages variable because CoMet supports identifying the cover by the filename
if tag.id == "comet":
if md_style.short_name == "comet":
md.pages = [
comicapi.genericmetadata.ImageMetadata(
image_index=0, filename="!cover.jpg", type=comicapi.genericmetadata.PageType.FrontCover