Update all references of saved 'matadata' to 'tags'

This commit is contained in:
Timmy Welch 2024-06-20 16:47:10 -07:00
parent 24002c66e7
commit 69a9566f42
41 changed files with 20466 additions and 755 deletions

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.metadata")
mdatas, mhiddenimports = collect_entry_point("comicapi.tags")
hiddenimports += mhiddenimports
datas += mdatas

View File

@ -30,97 +30,94 @@ from typing import TYPE_CHECKING
from comicapi import utils
from comicapi.archivers import Archiver, UnknownArchiver, ZipArchiver
from comicapi.genericmetadata import GenericMetadata
from comicapi.metadata import Metadata
from comicapi.tags import Tag
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]] = []
metadata_styles: dict[str, Metadata] = {}
tags: dict[str, Tag] = {}
def load_archive_plugins(local_plugins: Iterable[EntryPoint] = tuple()) -> None:
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)
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
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()
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.warning(
"Builtin metadata for %s tags are being overridden by a plugin from %s", style_name, spec.origin
)
if ep.module.startswith("comicapi"):
builtin.append(archiver)
else:
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()})
archive_plugins.append(archiver)
except Exception:
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.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()})
class ComicArchive:
@ -145,7 +142,7 @@ class ComicArchive:
self.archiver = UnknownArchiver.open(self.path)
load_archive_plugins()
load_metadata_plugins()
load_tag_plugins()
for archiver in archivers:
if archiver.enabled and archiver.is_valid(self.path):
self.archiver = archiver.open(self.path)
@ -162,15 +159,19 @@ class ComicArchive:
self.page_list.clear()
self.md.clear()
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 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 get_supported_metadata(self) -> list[str]:
return [style[0] for style in metadata_styles.items() if style[1].supports_metadata(self.archiver)]
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 rename(self, path: pathlib.Path | str) -> None:
new_path = pathlib.Path(path).absolute()
@ -193,11 +194,6 @@ 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"
@ -210,33 +206,42 @@ class ComicArchive:
def extension(self) -> str:
return self.archiver.extension()
def read_metadata(self, style: str) -> GenericMetadata:
if style in self.md:
return self.md[style]
def read_tags(self, tag_id: str) -> GenericMetadata:
if tag_id in self.md:
return self.md[tag_id]
md = GenericMetadata()
if metadata_styles[style].has_metadata(self.archiver):
md = metadata_styles[style].get_metadata(self.archiver)
tag = tags[tag_id]
if tag.enabled and tag.has_tags(self.archiver):
md = tag.read_tags(self.archiver)
md.apply_default_page_list(self.get_page_name_list())
return md
def read_metadata_string(self, style: str) -> str:
return metadata_styles[style].get_metadata_string(self.archiver)
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 write_metadata(self, metadata: GenericMetadata, style: str) -> bool:
if style in self.md:
del self.md[style]
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
metadata.apply_default_page_list(self.get_page_name_list())
return metadata_styles[style].set_metadata(metadata, self.archiver)
return tags[tag_id].write_tags(metadata, self.archiver)
def has_metadata(self, style: str) -> bool:
if style in self.md:
def has_tags(self, tag_id: str) -> bool:
if tag_id in self.md:
return True
return metadata_styles[style].has_metadata(self.archiver)
if not tags[tag_id].enabled:
return False
return tags[tag_id].has_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 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 get_page(self, index: int) -> bytes:
image_data = b""

View File

@ -92,7 +92,7 @@ class ComicSeries:
return copy.deepcopy(self)
class TagOrigin(NamedTuple):
class MetadataOrigin(NamedTuple):
id: str
name: str
@ -109,7 +109,7 @@ class GenericMetadata:
translator_synonyms = ("translator", "translation")
is_empty: bool = True
tag_origin: TagOrigin | None = None
data_origin: MetadataOrigin | 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.tag_origin = assign(self.tag_origin, new_md.tag_origin) # TODO use and purpose now?
self.data_origin = assign(self.data_origin, new_md.data_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,
tag_origin=TagOrigin("comicvine", "Comic Vine"),
data_origin=MetadataOrigin("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",
"TagOrigin",
"MetadataOrigin",
"GenericMetadata",
)

View File

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

View File

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

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.metadata import Metadata
from comicapi.tags import Tag
logger = logging.getLogger(__name__)
class CoMet(Metadata):
class CoMet(Tag):
enabled = True
short_name = "comet"
id = "comet"
def __init__(self, version: str) -> None:
super().__init__(version)
@ -71,13 +71,13 @@ class CoMet(Metadata):
def supports_credit_role(self, role: str) -> bool:
return role.casefold() in self._get_parseable_credits()
def supports_metadata(self, archive: Archiver) -> bool:
def supports_tags(self, archive: Archiver) -> bool:
return archive.supports_files()
def has_metadata(self, archive: Archiver) -> bool:
if not self.supports_metadata(archive):
def has_tags(self, archive: Archiver) -> bool:
if not self.supports_tags(archive):
return False
has_metadata = False
has_tags = 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(Metadata):
if self._validate_bytes(data):
# since we found it, save it!
self.file = n
has_metadata = True
has_tags = True
break
return has_metadata
return has_tags
def remove_metadata(self, archive: Archiver) -> bool:
return self.has_metadata(archive) and archive.remove_file(self.file)
def remove_tags(self, archive: Archiver) -> bool:
return self.has_tags(archive) and archive.remove_file(self.file)
def get_metadata(self, archive: Archiver) -> GenericMetadata:
if self.has_metadata(archive):
def read_tags(self, archive: Archiver) -> GenericMetadata:
if self.has_tags(archive):
metadata = archive.read_file(self.file) or b""
if self._validate_bytes(metadata):
return self._metadata_from_bytes(metadata, archive)
return GenericMetadata()
def get_metadata_string(self, archive: Archiver) -> str:
if self.has_metadata(archive):
def read_raw_tags(self, archive: Archiver) -> str:
if self.has_tags(archive):
return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding="unicode", xml_declaration=True)
return ""
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_metadata(archive):
def write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_tags(archive):
success = True
xml = b""
if self.has_metadata(archive):
if self.has_tags(archive):
xml = archive.read_file(self.file)
if self.file != self.comet_filename:
success = self.remove_metadata(archive)
success = self.remove_tags(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.metadata import Metadata
from comicapi.tags import Tag
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(Metadata):
class ComicBookInfo(Tag):
enabled = True
short_name = "cbi"
id = "cbi"
def __init__(self, version: str) -> None:
super().__init__(version)
@ -108,29 +108,29 @@ class ComicBookInfo(Metadata):
def supports_credit_role(self, role: str) -> bool:
return True
def supports_metadata(self, archive: Archiver) -> bool:
def supports_tags(self, archive: Archiver) -> bool:
return archive.supports_comment()
def has_metadata(self, archive: Archiver) -> bool:
return self.supports_metadata(archive) and self._validate_string(archive.get_comment())
def has_tags(self, archive: Archiver) -> bool:
return self.supports_tags(archive) and self._validate_string(archive.get_comment())
def remove_metadata(self, archive: Archiver) -> bool:
def remove_tags(self, archive: Archiver) -> bool:
return archive.set_comment("")
def get_metadata(self, archive: Archiver) -> GenericMetadata:
if self.has_metadata(archive):
def read_tags(self, archive: Archiver) -> GenericMetadata:
if self.has_tags(archive):
comment = archive.get_comment()
if self._validate_string(comment):
return self._metadata_from_string(comment)
return GenericMetadata()
def get_metadata_string(self, archive: Archiver) -> str:
if self.has_metadata(archive):
def read_raw_tags(self, archive: Archiver) -> str:
if self.has_tags(archive):
return json.dumps(json.loads(archive.get_comment()), indent=2)
return ""
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_metadata(archive):
def write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_tags(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.metadata import Metadata
from comicapi.tags import Tag
logger = logging.getLogger(__name__)
class ComicRack(Metadata):
class ComicRack(Tag):
enabled = True
short_name = "cr"
id = "cr"
def __init__(self, version: str) -> None:
super().__init__(version)
@ -84,35 +84,35 @@ class ComicRack(Metadata):
def supports_credit_role(self, role: str) -> bool:
return role.casefold() in self._get_parseable_credits()
def supports_metadata(self, archive: Archiver) -> bool:
def supports_tags(self, archive: Archiver) -> bool:
return archive.supports_files()
def has_metadata(self, archive: Archiver) -> bool:
def has_tags(self, archive: Archiver) -> bool:
return (
self.supports_metadata(archive)
self.supports_tags(archive)
and self.file in archive.get_filename_list()
and self._validate_bytes(archive.read_file(self.file))
)
def remove_metadata(self, archive: Archiver) -> bool:
return self.has_metadata(archive) and archive.remove_file(self.file)
def remove_tags(self, archive: Archiver) -> bool:
return self.has_tags(archive) and archive.remove_file(self.file)
def get_metadata(self, archive: Archiver) -> GenericMetadata:
if self.has_metadata(archive):
def read_tags(self, archive: Archiver) -> GenericMetadata:
if self.has_tags(archive):
metadata = archive.read_file(self.file) or b""
if self._validate_bytes(metadata):
return self._metadata_from_bytes(metadata)
return GenericMetadata()
def get_metadata_string(self, archive: Archiver) -> str:
if self.has_metadata(archive):
def read_raw_tags(self, archive: Archiver) -> str:
if self.has_tags(archive):
return ET.tostring(ET.fromstring(archive.read_file(self.file)), encoding="unicode", xml_declaration=True)
return ""
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_metadata(archive):
def write_tags(self, metadata: GenericMetadata, archive: Archiver) -> bool:
if self.supports_tags(archive):
xml = b""
if self.has_metadata(archive):
if self.has_tags(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 Metadata:
class Tag:
enabled: bool = False
short_name: str = ""
id: str = ""
def __init__(self, version: str) -> None:
self.version: str = version
self.supported_attributes = {
"tag_origin",
"data_origin",
"issue_id",
"series_id",
"series",
@ -71,44 +71,44 @@ class Metadata:
def supports_credit_role(self, role: str) -> bool:
return False
def supports_metadata(self, archive: Archiver) -> bool:
def supports_tags(self, archive: Archiver) -> bool:
"""
Checks the given archive for the ability to save this metadata style.
Checks the given archive for the ability to save these tags.
Should always return a bool. Failures should return False.
Typically consists of a call to either `archive.supports_comment` or `archive.supports_file`
"""
return False
def has_metadata(self, archive: Archiver) -> bool:
def has_tags(self, archive: Archiver) -> bool:
"""
Checks the given archive for metadata.
Checks the given archive for tags.
Should always return a bool. Failures should return False.
"""
return False
def remove_metadata(self, archive: Archiver) -> bool:
def remove_tags(self, archive: Archiver) -> bool:
"""
Removes the metadata from the given archive.
Removes the tags from the given archive.
Should always return a bool. Failures should return False.
"""
return False
def get_metadata(self, archive: Archiver) -> GenericMetadata:
def read_tags(self, archive: Archiver) -> GenericMetadata:
"""
Returns a GenericMetadata representing the data saved in the given archive.
Returns a GenericMetadata representing the tags saved in the given archive.
Should always return a GenericMetadata. Failures should return an empty metadata object.
"""
return GenericMetadata()
def get_metadata_string(self, archive: Archiver) -> str:
def read_raw_tags(self, archive: Archiver) -> str:
"""
Returns the raw metadata as a string.
If the metadata is a binary format a roughly similar text format should be used.
Returns the raw tags as a string.
If the tags are a binary format a roughly similar text format should be used.
Should always return a string. Failures should return the empty string.
"""
return ""
def set_metadata(self, metadata: GenericMetadata, archive: Archiver) -> bool:
def write_tags(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 Metadata:
def name(self) -> str:
"""
Returns the name of this metadata for display purposes eg "Comic Rack".
Returns the name of these tags for display purposes eg "Comic Rack".
Should always return a string. Failures should return the empty string.
"""
return ""

View File

@ -32,10 +32,9 @@ from comicfn2dict import comicfn2dict
import comicapi.data
from comicapi import filenamelexer, filenameparser
from ._url import LocationParseError as LocationParseError # noqa: F401
from ._url import Url as Url
from ._url import parse_url as parse_url
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
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, metadata_styles
from comicapi.comicarchive import ComicArchive, tags
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],
load_styles: list[str],
read_tags: 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._styles = load_styles
self._tags = read_tags
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().overlay_ca_read_style(self.load_data_styles, ca)
md, error = self.parent().read_all_tags(self._tags, ca)
if error is not None:
logger.error("Failed to load metadata for %s: %s", ca.path, error)
logger.error("Failed to load tags for %s: %s", ca.path, error)
QtWidgets.QApplication.restoreOverrideCursor()
QtWidgets.QMessageBox.critical(
self,
"Read Failed!",
f"One or more of the read styles failed to load for {ca.path}, check log for details",
f"One or more of the read tags 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 style in self._styles:
success = ca.write_metadata(md, style)
for tag_id in self._tags:
success = ca.write_tags(md, tag_id)
QtWidgets.QApplication.restoreOverrideCursor()
if not success:
QtWidgets.QMessageBox.warning(
self,
"Write Error",
f"Saving {metadata_styles[style].name()} the tags to the archive seemed to fail!",
f"Saving {tags[tag_id].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
from comicapi.comicarchive import metadata_styles as md_styles
from comicapi.comicarchive import ComicArchive, tags
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 actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata:
def fetch_metadata(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 actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool:
def write_tags(self, ca: ComicArchive, md: GenericMetadata) -> bool:
if not self.config.Runtime_Options__dryrun:
for style in self.config.Runtime_Options__type_modify:
for tag_id in self.config.Runtime_Options__tags_write:
# write out the new data
if not ca.write_metadata(md, style):
logger.error("The tag save seemed to fail for style: %s!", md_styles[style].name())
if not ca.write_tags(md, tag_id):
logger.error("The tag save seemed to fail for: %s!", tags[tag_id].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 = self.create_local_metadata(ca)
ct_md = self.actual_issue_data_fetch(match_set.online_results[int(i) - 1].issue_id)
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)
match_set.md = prepare_metadata(md, ct_md, self.config)
self.actual_metadata_save(ca, md)
self.write_tags(ca, md)
def post_process_matches(self, match_results: OnlineMatchResults) -> None:
def print_header(header: str) -> None:
@ -231,12 +231,14 @@ class CLI:
self.display_match_set_for_choice(label, match_set)
def create_local_metadata(self, ca: ComicArchive) -> GenericMetadata:
def create_local_metadata(
self, ca: ComicArchive, tags_to_read: list[str], /, tags_only: bool = False
) -> tuple[GenericMetadata, list[str]]:
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:
if self.config.Auto_Tag__parse_filename and not tags_only:
f_md = ca.metadata_from_filename(
self.config.Filename_Parsing__filename_parser,
self.config.Filename_Parsing__remove_c2c,
@ -249,24 +251,29 @@ class CLI:
md.overlay(f_md)
for style in self.config.Runtime_Options__type_read:
if ca.has_metadata(style):
tags_used = []
for tag_id in tags_to_read:
if ca.has_tags(tag_id):
try:
t_md = ca.read_metadata(style)
md.overlay(
t_md, self.config.Metadata_Options__comic_merge, self.config.Metadata_Options__comic_merge_lists
)
break
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)
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
logger.error("Failed to read tags from %s: %s", ca.path, e)
# finally, use explicit stuff (always 'overlay' mode)
md.overlay(self.config.Auto_Tag__metadata, mode=merge.Mode.OVERLAY, merge_lists=True)
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)
return md
return (md, tags_used)
def print(self, ca: ComicArchive) -> Result:
if not self.config.Runtime_Options__type_read:
if not self.config.Runtime_Options__tags_read:
page_count = ca.get_number_of_pages()
brief = ""
@ -279,8 +286,8 @@ class CLI:
brief += f"({page_count: >3} pages)"
brief += " tags:[ "
metadata_styles = [md_styles[style].name() for style in md_styles if ca.has_metadata(style)]
brief += " ".join(metadata_styles)
tag_names = [tags[tag_id].name() for tag_id in tags if ca.has_tags(tag_id)]
brief += " ".join(tag_names)
brief += " ]"
self.output(brief)
@ -291,105 +298,110 @@ class CLI:
self.output()
md = None
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 ---------")
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 ---------")
try:
if self.config.Runtime_Options__raw:
self.output(ca.read_metadata_string(style))
self.output(ca.read_raw_tags(tag_id))
else:
md = ca.read_metadata(style)
md = ca.read_tags(tag_id)
self.output(md)
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
logger.error("Failed to read tags from %s: %s", ca.path, e)
return Result(Action.print, Status.success, ca.path, md=md)
def delete_style(self, ca: ComicArchive, style: str) -> Status:
style_name = md_styles[style].name()
def delete_tags(self, ca: ComicArchive, tag_id: str) -> Status:
tag_name = tags[tag_id].name()
if ca.has_metadata(style):
if ca.has_tags(tag_id):
if not self.config.Runtime_Options__dryrun:
if ca.remove_metadata(style):
self.output(f"{ca.path}: Removed {style_name} tags.")
if ca.remove_tags(tag_id):
self.output(f"{ca.path}: Removed {tag_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. {style_name} tags not removed")
self.output(f"{ca.path}: dry-run. {tag_name} tags not removed")
return Status.success
self.output(f"{ca.path}: This archive doesn't have {style_name} tags to remove.")
self.output(f"{ca.path}: This archive doesn't have {tag_name} tags to remove.")
return Status.success
def delete(self, ca: ComicArchive) -> Result:
res = Result(Action.delete, Status.success, ca.path)
for style in self.config.Runtime_Options__type_modify:
status = self.delete_style(ca, style)
for tag_id in self.config.Runtime_Options__tags_write:
status = self.delete_tags(ca, tag_id)
if status == Status.success:
res.tags_deleted.append(style)
res.tags_deleted.append(tag_id)
else:
res.status = status
return res
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.")
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.")
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 style == "cbi":
if self.config.Metadata_Options__apply_transform_on_bulk_operation and dst_tag_id == "cbi":
md = CBLTransformer(md, self.config).apply()
if ca.write_metadata(md, style):
self.output(f"{ca.path}: Copied {src_style_name} tags to {dst_style_name}.")
return Status.success
if ca.write_tags(md, dst_tag_id):
self.output(f"{ca.path}: Copied {source_names} tags to {dst_tag_name}.")
else:
self.output(f"{ca.path}: Tag copy seemed to fail!")
return Status.write_failure
else:
self.output(f"{ca.path}: dry-run. {src_style_name} tags not copied")
return Status.success
return Status.read_failure
self.output(f"{ca.path}: dry-run. {source_names} tags not copied")
return Status.success
def copy(self, ca: ComicArchive) -> Result:
src_style_name = md_styles[self.config.Commands__copy].name()
res = Result(Action.copy, Status.success, ca.path)
if not ca.has_metadata(self.config.Commands__copy):
self.output(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.")
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.")
res.status = Status.read_failure
return res
try:
res.md = ca.read_metadata(self.config.Commands__copy)
res.md, res.tags_read = self.create_local_metadata(ca, res.tags_read, tags_only=True)
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
logger.error("Failed to read tags from %s: %s", ca.path, e)
return res
for style in self.config.Runtime_Options__type_modify:
if style == src_style_name:
for dst_tag_id in self.config.Runtime_Options__tags_write:
if dst_tag_id in self.config.Commands__copy:
continue
status = self.copy_style(ca, res.md, style)
status = self._copy_tags(ca, res.md, ", ".join(src_tag_names), dst_tag_id)
if status == Status.success:
res.tags_written.append(style)
res.tags_written.append(dst_tag_id)
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_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.")
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.")
return (
Result(
Action.save,
original_path=ca.path,
status=Status.existing_tags,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
),
match_results,
)
@ -397,7 +409,7 @@ class CLI:
if self.batch_mode:
self.output(f"Processing {utils.path_to_short_str(ca.path)}...")
md = self.create_local_metadata(ca)
md, tags_read = self.create_local_metadata(ca, self.config.Runtime_Options__tags_read)
if md.issue is None or md.issue == "":
if self.config.Auto_Tag__assume_issue_one:
md.issue = "1"
@ -417,7 +429,8 @@ class CLI:
Action.save,
original_path=ca.path,
status=Status.fetch_data_failure,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.fetch_data_failures.append(res)
return res, match_results
@ -429,7 +442,8 @@ class CLI:
status=Status.match_failure,
original_path=ca.path,
match_status=MatchStatus.no_match,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.no_matches.append(res)
return res, match_results
@ -442,7 +456,8 @@ class CLI:
status=Status.match_failure,
original_path=ca.path,
match_status=MatchStatus.no_match,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.no_matches.append(res)
return res, match_results
@ -454,10 +469,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
@ -488,7 +503,8 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.low_confidence_match,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.low_confidence_matches.append(res)
return res, match_results
@ -500,7 +516,8 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.multiple_match,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.multiple_matches.append(res)
return res, match_results
@ -512,7 +529,8 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.low_confidence_match,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.low_confidence_matches.append(res)
return res, match_results
@ -524,7 +542,8 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.no_match,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.no_matches.append(res)
return res, match_results
@ -532,7 +551,7 @@ class CLI:
# we got here, so we have a single match
# now get the particular issue data
ct_md = self.actual_issue_data_fetch(matches[0].issue_id)
ct_md = self.fetch_metadata(matches[0].issue_id)
if ct_md.is_empty:
res = Result(
Action.save,
@ -540,7 +559,8 @@ class CLI:
original_path=ca.path,
online_results=matches,
match_status=MatchStatus.good_match,
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
match_results.fetch_data_failures.append(res)
return res, match_results
@ -552,11 +572,12 @@ class CLI:
online_results=matches,
match_status=MatchStatus.good_match,
md=prepare_metadata(md, ct_md, self.config),
tags_written=self.config.Runtime_Options__type_modify,
tags_written=self.config.Runtime_Options__tags_write,
tags_read=tags_read,
)
assert res.md
# ok, done building our metadata. time to save
if self.actual_metadata_save(ca, res.md):
if self.write_tags(ca, res.md):
match_results.good_matches.append(res)
else:
res.status = Status.write_failure
@ -569,7 +590,7 @@ class CLI:
if self.batch_mode:
msg_hdr = f"{ca.path}: "
md = self.create_local_metadata(ca)
md, tags_read = self.create_local_metadata(ca, self.config.Runtime_Options__tags_read)
if md.series is None:
logger.error(msg_hdr + "Can't rename without series name")
@ -629,7 +650,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, md=md)
return Result(Action.rename, Status.success, original_path, tags_read=tags_read, md=md)
def export(self, ca: ComicArchive) -> Result:
msg_hdr = ""

View File

@ -26,10 +26,10 @@ import subprocess
import settngs
from comicapi import utils
from comicapi.comicarchive import metadata_styles
from comicapi.comicarchive import tags
from comictaggerlib import ctversion
from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns
from comictaggerlib.ctsettings.types import ComicTaggerPaths, metadata_type, metadata_type_single
from comictaggerlib.ctsettings.types import ComicTaggerPaths, tag
from comictaggerlib.resulttypes import Action
logger = logging.getLogger(__name__)
@ -138,26 +138,26 @@ def register_runtime(parser: settngs.Manager) -> None:
)
parser.add_setting(
"-t",
"--type-read",
metavar=f"{{{','.join(metadata_styles).upper()}}}",
"--tags-read",
metavar=f"{{{','.join(tags).upper()}}}",
default=[],
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""",
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""",
file=False,
)
parser.add_setting(
"--type-modify",
metavar=f"{{{','.join(metadata_styles).upper()}}}",
"--tags-write",
metavar=f"{{{','.join(tags).upper()}}}",
default=[],
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""",
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""",
file=False,
)
parser.add_setting(
"--skip-existing-metadata",
"--skip-existing-tags",
action=argparse.BooleanOptionalAction,
default=True,
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""",
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""",
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 type\n(via --type-read) to get only info of that tag type.\n\n""",
help="""Print out tag info from file. Specify via -t to only print specific tags.\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 tag block of specified type (via -t).",
help="Deletes the tags specified via -t.",
file=False,
)
parser.add_setting(
"-c",
"--copy",
type=metadata_type_single,
type=tag,
default=[],
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",
metavar=f"{{{','.join(tags).upper()}}}",
help="Copy the specified source tags to\ndestination tags specified via --tags-write\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 type (via --type-modify).\nMust specify also at least -o, -f, or -m.\n\n",
help="Save out tags as specified tags (via --tags-write).\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 tag style.",
help="Rename the file based on specified tags.",
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__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].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].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__type_modify:
parser.exit(message="Please specify the type to delete with --type-modify\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.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__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__copy:
config[0].Commands__command = Action.copy
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 not config[0].Runtime_Options__tags_write:
parser.exit(message="Please specify the tags to copy to with --tags-write\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("save_data_style", default=["cbi"], cmdline=False)
parser.add_setting("load_data_style", default=["cbi"], cmdline=False)
parser.add_setting("write_tags", default=["cbi"], cmdline=False)
parser.add_setting("read_tags", 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_metadata_names", default=False, action=argparse.BooleanOptionalAction, cmdline=False)
parser.add_setting("use_short_tag_names", default=False, action=argparse.BooleanOptionalAction, cmdline=False)
parser.add_setting(
"--cr",
default=True,
action=argparse.BooleanOptionalAction,
help="Enable the ComicRack metadata type. Turn off to only use the CIX metadata type.\ndefault: %(default)s",
help="Enable ComicRack tags. Turn off to only use CIX tags.\ndefault: %(default)s",
)
parser.add_setting(
"--comic-merge",
"--tag-merge",
metavar=f"{{{','.join(merge.Mode)}}}",
default=merge.Mode.OVERLAY,
choices=merge.Mode,
type=merge.Mode,
help="How to merge additional metadata for enabled read styles (CR, CBL, etc.) See -t, --type-read default: %(default)s",
help="How to merge fields when reading enabled tags (CR, CBL, etc.) See -t, --tags-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 new metadata from a data source (CV, Metron, GCD, etc.) default: %(default)s",
help="How to merge fields when downloading new metadata (CV, Metron, GCD, etc.) default: %(default)s",
)
parser.add_setting(
"--comic-merge-lists",
"--tag-merge-lists",
action=argparse.BooleanOptionalAction,
default=True,
help="Merge all items of lists when merging new metadata (genres, characters, etc.) default: %(default)s",
help="Merge lists when reading enabled tags (genres, characters, etc.) default: %(default)s",
)
parser.add_setting(
"--metadata-merge-lists",
action=argparse.BooleanOptionalAction,
default=True,
help="Merge all items of lists when merging new metadata (genres, characters, etc.) default: %(default)s",
help="Merge lists when downloading new metadata (genres, characters, etc.) default: %(default)s",
)
@ -242,14 +242,15 @@ def autotag(parser: settngs.Manager) -> None:
"-o",
"--online",
action="store_true",
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""",
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""",
file=False,
)
parser.add_setting(
"--save-on-low-confidence",
default=False,
action=argparse.BooleanOptionalAction,
help="Automatically save metadata on low-confidence matches.\ndefault: %(default)s",
help="Automatically save tags on low-confidence matches.\ndefault: %(default)s",
cmdline=False,
)
parser.add_setting(
"--use-year-when-identifying",
@ -290,14 +291,14 @@ def autotag(parser: settngs.Manager) -> None:
"--metadata",
default=GenericMetadata(),
type=parse_metadata_from_string,
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""",
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""",
file=False,
)
parser.add_setting(
"--clear-metadata",
"--clear-tags",
default=False,
action=argparse.BooleanOptionalAction,
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",
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",
)
parser.add_setting(
"--publisher-filter",
@ -341,23 +342,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")
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]
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]
else:
config[0].internal__save_data_style = ["cbi"]
config[0].internal__write_tags = ["cbi"]
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]
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]
else:
config[0].internal__load_data_style = ["cbi"]
config[0].internal__read_tags = ["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.metadata"))
PLUGIN_GROUPS = frozenset(("comictagger.talker", "comicapi.archiver", "comicapi.tags"))
class FailedToLoadPlugin(Exception):
@ -72,13 +72,13 @@ class Plugins(NamedTuple):
"""Classified plugins."""
archivers: list[Plugin]
metadata: list[Plugin]
tags: 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.metadata
yield from self.tags
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 = []
metadata = []
tags = []
talkers = []
for p in plugins:
if p.entry_point.group == "comictagger.talker":
talkers.append(p)
elif p.entry_point.group == "comicapi.metadata":
metadata.append(p)
elif p.entry_point.group == "comicapi.tags":
tags.append(p)
elif p.entry_point.group == "comicapi.archiver":
archivers.append(p)
else:
logger.warning(NotImplementedError(f"what plugin type? {p}"))
return Plugins(
metadata=metadata,
tags=tags,
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: str
Commands__copy: list[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__type_read: list[str]
Runtime_Options__type_modify: list[str]
Runtime_Options__skip_existing_metadata: bool
Runtime_Options__tags_read: list[str]
Runtime_Options__tags_write: list[str]
Runtime_Options__skip_existing_tags: bool
Runtime_Options__files: list[str]
internal__install_id: str
internal__save_data_style: list[str]
internal__load_data_style: list[str]
internal__write_tags: list[str]
internal__read_tags: 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_metadata_names: bool
Metadata_Options__use_short_tag_names: bool
Metadata_Options__cr: bool
Metadata_Options__comic_merge: comicapi.merge.Mode
Metadata_Options__tag_merge: comicapi.merge.Mode
Metadata_Options__metadata_merge: comicapi.merge.Mode
Metadata_Options__comic_merge_lists: bool
Metadata_Options__tag_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_metadata: bool
Auto_Tag__clear_tags: 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: str
copy: list[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
type_read: list[str]
type_modify: list[str]
skip_existing_metadata: bool
tags_read: list[str]
tags_write: list[str]
skip_existing_tags: bool
files: list[str]
class internal(typing.TypedDict):
install_id: str
save_data_style: list[str]
load_data_style: list[str]
write_tags: list[str]
read_tags: 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_metadata_names: bool
use_short_tag_names: bool
cr: bool
comic_merge: comicapi.merge.Mode
tag_merge: comicapi.merge.Mode
metadata_merge: comicapi.merge.Mode
comic_merge_lists: bool
tag_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_metadata: bool
clear_tags: 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 metadata_styles
from comicapi.comicarchive import tags
from comicapi.genericmetadata import REMOVE, GenericMetadata
if sys.version_info < (3, 10):
@ -151,21 +151,14 @@ class ComicTaggerPaths(AppDirs):
return f"logs: {self.user_log_dir}, config: {self.user_config_dir}, cache: {self.user_cache_dir}"
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]:
def tag(types: str) -> list[str]:
result = []
types = types.casefold()
for typ in utils.split(types, ","):
if typ not in metadata_styles:
choices = ", ".join(metadata_styles)
if typ not in tags:
choices = ", ".join(tags)
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
result.append(metadata_styles[typ].short_name)
result.append(tags[typ].id)
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("Ctrl+X")
remove_action.setShortcut("Backspace" if platform.system() == "Darwin" else "Delete")
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 currently write to them without the rar program and are marked read only!\n\n"
"however ComicTagger cannot write to them without the rar program and are marked read only!\n\n"
f"{rar_help}",
)
self.rar_ro_shown = True
@ -326,8 +326,7 @@ class FileSelectionList(QtWidgets.QWidget):
type_item.setText(item_text)
type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
styles = ", ".join(x for x in ca.get_supported_metadata() if ca.has_metadata(x))
md_item.setText(styles)
md_item.setText(", ".join(x for x in ca.get_supported_tags() if ca.has_tags(x)))
if not ca.is_writable():
readonly_item.setCheckState(QtCore.Qt.CheckState.Checked)

View File

@ -0,0 +1,24 @@
<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

@ -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_metadata_plugins(
version=version, local_plugins=[p.entry_point for p in local_plugins.metadata]
comicapi.comicarchive.load_tag_plugins(
version=version, local_plugins=[p.entry_point for p in local_plugins.tags]
)
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]],
metadata_styles: Collection[comicapi.comicarchive.Metadata],
tags: Collection[comicapi.comicarchive.Tag],
) -> None:
if self.config[0].Runtime_Options__json:
for talker in talkers:
@ -183,14 +183,14 @@ class App:
)
)
for style in metadata_styles:
for tag in tags:
print( # noqa: T201
json.dumps(
{
"type": "metadata",
"enabled": style.enabled,
"name": style.name(),
"short_name": style.short_name,
"type": "tag",
"enabled": tag.enabled,
"name": tag.name(),
"id": tag.id,
}
)
)
@ -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: (Name: extension, exe)") # noqa: T201
print("\nComic Archive: (Enabled, Name: extension, exe)") # noqa: T201
for archiver in archivers:
a = archiver()
print(f"{a.name():<10}: {a.extension():<5}, {a.exe}") # noqa: T201
print(f"{a.enabled!s:<5}, {a.name():<10}: {a.extension():<5}, {a.exe}") # noqa: T201
print("\nMetadata Style: (Short Name: Name)") # noqa: T201
for style in metadata_styles:
print(f"{style.short_name:<10}: {style.name()}") # noqa: T201
print("\nTags: (Enabled, ID: Name)") # noqa: T201
for tag in tags:
print(f"{tag.enabled!s:<5}, {tag.id:<10}: {tag.name()}") # noqa: T201
def initialize(self) -> argparse.Namespace:
conf, _ = self.initial_arg_parser.parse_known_intermixed_args()
@ -252,9 +252,12 @@ class App:
# config already loaded
error = None
if not self.config[0].Metadata_Options__cr:
if "cr" in comicapi.comicarchive.metadata_styles:
del comicapi.comicarchive.metadata_styles["cr"]
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 len(self.talkers) < 1:
error = error = (
@ -277,7 +280,7 @@ class App:
self.list_plugins(
list(self.talkers.values()),
comicapi.comicarchive.archivers,
comicapi.comicarchive.metadata_styles.values(),
comicapi.comicarchive.tags.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, opts: SettngsNS) -> GenericMetadata:
if opts.Metadata_Options__apply_transform_on_import:
new_md = CBLTransformer(new_md, opts).apply()
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()
final_md = md.copy()
if opts.Auto_Tag__clear_metadata:
if config.Auto_Tag__clear_tags:
final_md = GenericMetadata()
final_md.overlay(new_md, opts.Metadata_Options__metadata_merge, opts.Metadata_Options__metadata_merge_lists)
if final_md.tag_origin is not None:
final_md.overlay(new_md, config.Metadata_Options__metadata_merge, config.Metadata_Options__metadata_merge_lists)
if final_md.data_origin is not None:
notes = (
f"Tagged with ComicTagger {ctversion.version} using info from {final_md.tag_origin.name} on"
f"Tagged with ComicTagger {ctversion.version} using info from {final_md.data_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, opts: Settngs
+ (f"[Issue ID {final_md.issue_id}]" if final_md.issue_id else "")
)
if opts.Auto_Tag__auto_imprint:
if config.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, opts.Sources__remove_html_tables) or None,
description=cleanup_html(final_md.description, config.Sources__remove_html_tables) or None,
)

View File

@ -20,7 +20,7 @@ import logging
from PyQt5 import QtCore, QtWidgets, uic
from comicapi.comicarchive import ComicArchive, metadata_styles
from comicapi.comicarchive import ComicArchive, tags
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.data_styles: list[str] = []
self.tag_ids: 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.set_metadata_style(self.data_styles)
self.select_read_tags(self.tag_ids)
else:
self.cbPageType.setEnabled(False)
self.chkDoublePage.setEnabled(False)
@ -386,15 +386,16 @@ class PageListEditor(QtWidgets.QWidget):
self.first_front_page = self.get_first_front_cover()
self.firstFrontCoverChanged.emit(self.first_front_page)
def set_metadata_style(self, data_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)
def select_read_tags(self, tag_ids: list[str]) -> None:
# depending on the current tags, certain fields are disabled
if not tag_ids:
return
self.data_styles = data_styles
enabled_widgets = set()
for tag_id in tag_ids:
enabled_widgets.update(tags[tag_id].supported_attributes)
for metadata, widget in self.md_attributes.items():
enable_widget(widget, metadata in enabled_widgets)
self.tag_ids = tag_ids
for md_field, widget in self.md_attributes.items():
enable_widget(widget, md_field 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, metadata_styles
from comicapi.comicarchive import ComicArchive, tags
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],
load_data_styles: list[str],
read_tag_ids: list[str],
config: settngs.Config[ct_ns],
talkers: dict[str, ComicTalker],
) -> None:
@ -48,9 +48,7 @@ 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(metadata_styles[style].name() for style in load_data_styles)} tags):"
)
self.label.setText(f"Preview (based on {', '.join(tags[tag].name() for tag in read_tag_ids)} tags):")
self.setWindowFlags(
QtCore.Qt.WindowType(
@ -63,7 +61,7 @@ class RenameWindow(QtWidgets.QDialog):
self.config = config
self.talkers = talkers
self.comic_archive_list = comic_archive_list
self.load_data_styles = load_data_styles
self.read_tag_ids = read_tag_ids
self.rename_list: list[str] = []
self.btnSettings.clicked.connect(self.modify_settings)
@ -84,13 +82,13 @@ class RenameWindow(QtWidgets.QDialog):
new_ext = ca.extension()
if md is None or md.is_empty:
md, error = self.parent().overlay_ca_read_style(self.load_data_styles, ca)
md, error = self.parent().read_all_tags(self.read_tag_ids, ca)
if error is not None:
logger.error("Failed to load metadata for %s: %s", ca.path, error)
logger.error("Failed to load tags from %s: %s", ca.path, error)
QtWidgets.QMessageBox.warning(
self,
"Read Failed!",
f"One or more of the read styles failed to load for {ca.path}, check log for details",
f"One or more of the read tags failed to load for {ca.path}, check log for details",
)
if md.is_empty:

View File

@ -82,6 +82,7 @@ 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)
{tag_origin} (string)
{data_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.cbMergeModeComic.addItem(mode.name.capitalize().replace("_", " "), mode)
self.cbMergeModeMetadata.addItem(mode.name.capitalize().replace("_", " "), mode)
self.cbTagsMergeMode.addItem(mode.name.capitalize().replace("_", " "), mode)
self.cbDownloadMergeMode.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_metadata)
self.cbxClearFormBeforePopulating.setChecked(self.config[0].Auto_Tag__clear_tags)
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,15 +432,13 @@ class SettingsWindow(QtWidgets.QDialog):
self.config[0].Metadata_Options__apply_transform_on_bulk_operation
)
self.cbMergeModeComic.setCurrentIndex(
self.cbMergeModeComic.findData(self.config[0].Metadata_Options__comic_merge)
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.cbMergeModeMetadata.setCurrentIndex(
self.cbMergeModeMetadata.findData(self.config[0].Metadata_Options__metadata_merge)
)
self.cbxMergeListsComic.setChecked(self.config[0].Metadata_Options__comic_merge_lists)
self.cbxTagsMergeLists.setChecked(self.config[0].Metadata_Options__tag_merge_lists)
self.cbxMergeListsMetadata.setChecked(self.config[0].Metadata_Options__metadata_merge_lists)
self.cbxShortMetadataNames.setChecked(self.config[0].Metadata_Options__use_short_metadata_names)
self.cbxShortTagNames.setChecked(self.config[0].Metadata_Options__use_short_tag_names)
self.cbxEnableCR.setChecked(self.config[0].Metadata_Options__cr)
self.leRenameTemplate.setText(self.config[0].File_Rename__template)
@ -550,7 +548,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_metadata = self.cbxClearFormBeforePopulating.isChecked()
self.config[0].Auto_Tag__clear_tags = 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()
@ -564,17 +562,17 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxApplyCBLTransformOnBatchOperation.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__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__metadata_merge_lists = self.cbxMergeListsMetadata.isChecked()
self.config[0].Metadata_Options__cr = self.cbxEnableCR.isChecked()
# 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()
# 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()
self.config[0].File_Rename__template = str(self.leRenameTemplate.text())
self.config[0].File_Rename__issue_number_padding = int(self.leIssueNumPadding.text())

View File

@ -35,7 +35,7 @@ import comicapi.merge
import comictaggerlib.graphics.resources
import comictaggerlib.ui
from comicapi import utils
from comicapi.comicarchive import ComicArchive, metadata_styles
from comicapi.comicarchive import ComicArchive, tags
from comicapi.filenameparser import FileNameParser
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
@ -90,7 +90,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
uic.loadUi(uifile, self)
self.md_attributes = {
"tag_origin": None,
"data_origin": None,
"issue_id": None,
"series": self.leSeries,
"issue": self.leIssueNum,
@ -213,21 +213,23 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
# 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
# respect the command line selected tags
if config[0].Runtime_Options__tags_write:
config[0].internal__write_tags = config[0].Runtime_Options__tags_write
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)
if config[0].Runtime_Options__tags_read:
config[0].internal__read_tags = config[0].Runtime_Options__tags_read
self.save_data_styles: list[str] = config[0].internal__save_data_style
self.load_data_styles: list[str] = config[0].internal__load_data_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.setAcceptDrops(True)
self.view_tag_actions, self.remove_tag_actions = self.tag_actions()
@ -276,9 +278,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.cbMaturityRating.lineEdit().setAcceptDrops(False)
# hook up the callbacks
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.cbSelectedReadTags.dropdownClosed.connect(self.select_read_tags)
self.cbSelectedWriteTags.itemChecked.connect(self.select_write_tags)
self.cbx_sources.currentIndexChanged.connect(self.select_source)
self.btnEditCredit.clicked.connect(self.edit_credit)
self.btnAddCredit.clicked.connect(self.add_credit)
self.btnRemoveCredit.clicked.connect(self.remove_credit)
@ -290,7 +292,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_metadata_style_tweaks()
self.update_tag_tweaks()
self.show()
self.set_app_position()
@ -339,14 +341,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 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))
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))
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))
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))
return view_raw_tags, remove_raw_tags
@ -423,7 +425,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
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.commit_metadata)
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)
@ -660,9 +662,9 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.menuRemove.setEnabled(enabled)
self.menuViewRawTags.setEnabled(enabled)
if self.comic_archive is not None:
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))
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))
self.actionWrite_Tags.setEnabled(writeable)
@ -687,11 +689,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
page_count = f" ({ca.get_number_of_pages()} pages)"
self.lblPageCount.setText(page_count)
supported_md = ca.get_supported_metadata()
supported_md = ca.get_supported_tags()
tag_info = ""
for md in supported_md:
if ca.has_metadata(md):
tag_info += "" + metadata_styles[md].name() + "\n"
if ca.has_tags(md):
tag_info += "" + tags[md].name() + "\n"
self.lblTagList.setText(tag_info)
@ -850,7 +852,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.add_new_credit_entry(row, credit.role.title(), credit.person, credit.primary)
self.twCredits.setSortingEnabled(True)
self.update_metadata_credit_colors()
self.update_credit_colors()
def add_new_credit_entry(self, row: int, role: str, name: str, primary_flag: bool = False) -> None:
self.twCredits.insertRow(row)
@ -1073,13 +1075,13 @@ class TaggerWindow(QtWidgets.QMainWindow):
# Now push the new combined data into the edit controls
self.metadata_to_form()
def commit_metadata(self) -> None:
def write_tags(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([metadata_styles[style].name() for style in self.save_data_styles])} tags to this archive?",
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?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
@ -1091,22 +1093,22 @@ class TaggerWindow(QtWidgets.QMainWindow):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
self.form_to_metadata()
failed_style: str = ""
# Save each style
for style in self.save_data_styles:
success = self.comic_archive.write_metadata(self.metadata, style)
failed_tag: str = ""
# Save each tag
for tag_id in self.selected_write_tags:
success = self.comic_archive.write_tags(self.metadata, tag_id)
if not success:
failed_style = metadata_styles[style].name()
failed_tag = tags[tag_id].name()
break
self.comic_archive.load_cache(set(metadata_styles))
self.comic_archive.load_cache(set(tags))
QtWidgets.QApplication.restoreOverrideCursor()
if failed_style:
if failed_tag:
QtWidgets.QMessageBox.warning(
self,
"Save failed",
f"The tag save operation seemed to fail for: {failed_style}",
f"The tag save operation seemed to fail for: {failed_tag}",
)
else:
self.clear_dirty_flag()
@ -1114,56 +1116,56 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.update_menus()
# Only try to read if write was successful
self.metadata, error = self.overlay_ca_read_style(self.load_data_styles, self.comic_archive)
self.metadata, error = self.read_selected_tags(self.selected_read_tags, self.comic_archive)
if error is not None:
QtWidgets.QMessageBox.warning(
self,
"Read Failed!",
f"One or more of the read styles failed to load for {self.comic_archive.path}, check log for details",
f"One or more of the selected read tags 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 commit!")
QtWidgets.QMessageBox.information(self, "Whoops!", "No data to write!")
def set_load_data_style(self, load_data_styles: list[str]) -> None:
def select_read_tags(self, tag_ids: list[str]) -> None:
"""Should only be called from the combobox signal"""
if self.dirty_flag_verification(
"Change Tag Read Style",
"If you change read tag style(s) now, data in the form will be lost. Are you sure?",
"Change Read Tags",
"If you change read tag(s) now, data in the form will be lost. Are you sure?",
):
self.load_data_styles = list(reversed(load_data_styles))
self.config[0].internal__load_data_style = self.load_data_styles
self.selected_read_tags = list(reversed(tag_ids))
self.config[0].internal__read_tags = self.selected_read_tags
self.update_menus()
if self.comic_archive is not None:
self.load_archive(self.comic_archive)
else:
self.cbLoadDataStyle.itemChanged.disconnect()
self.adjust_load_style_combo()
self.cbLoadDataStyle.itemChanged.connect(self.set_load_data_style)
self.cbSelectedReadTags.itemChanged.disconnect()
self.adjust_tags_combo()
self.cbSelectedReadTags.itemChanged.connect(self.select_read_tags)
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()
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()
self.update_menus()
def set_source(self, s: int) -> None:
def select_source(self, s: int) -> None:
self.config[0].Sources__source = self.cbx_sources.itemData(s)
def update_metadata_credit_colors(self) -> None:
styles = [metadata_styles[style] for style in self.save_data_styles]
def update_credit_colors(self) -> None:
selected_tags = [tags[tag_id] for tag_id in self.selected_write_tags]
enabled = set()
for style in styles:
enabled.update(style.supported_attributes)
for tag in selected_tags:
enabled.update(tag.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(style.supports_credit_role(str(w.text())) for style in styles)
supports_role = any(tag.supports_credit_role(str(w.text())) for tag in selected_tags)
for credit in credit_attributes:
widget_enabled = credit[0] in enabled
widget = self.twCredits.item(r, credit[1])
@ -1171,18 +1173,18 @@ class TaggerWindow(QtWidgets.QMainWindow):
widget_enabled = widget_enabled and supports_role
enable_widget(widget, widget_enabled)
def update_metadata_style_tweaks(self) -> None:
# depending on the current data style, certain fields are disabled
def update_tag_tweaks(self) -> None:
# depending on the current data tag, certain fields are disabled
enabled_widgets = set()
for style in self.save_data_styles:
enabled_widgets.update(metadata_styles[style].supported_attributes)
for tag_id in self.selected_write_tags:
enabled_widgets.update(tags[tag_id].supported_attributes)
for metadata, widget in self.md_attributes.items():
for md_field, widget in self.md_attributes.items():
if widget is not None and not isinstance(widget, (int)):
enable_widget(widget, metadata in enabled_widgets)
enable_widget(widget, md_field in enabled_widgets)
self.update_metadata_credit_colors()
self.page_list_editor.set_metadata_style(self.save_data_styles)
self.update_credit_colors()
self.page_list_editor.select_read_tags(self.selected_write_tags)
def cell_double_clicked(self, r: int, c: int) -> None:
self.edit_credit()
@ -1272,7 +1274,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
row = self.twCredits.rowCount()
self.add_new_credit_entry(row, new_role, new_name, new_primary)
self.update_metadata_credit_colors()
self.update_credit_colors()
self.set_dirty_flag()
def remove_credit(self) -> None:
@ -1313,45 +1315,43 @@ 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_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)
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)
# Order matters, move items to list order
if item_idx != i:
self.cbLoadDataStyle.moveItem(item_idx, row=i)
for style in unchecked:
self.cbLoadDataStyle.setItemChecked(self.cbLoadDataStyle.findData(style), False)
self.cbSelectedReadTags.moveItem(item_idx, row=i)
for tag_id in unchecked:
self.cbSelectedReadTags.setItemChecked(self.cbSelectedReadTags.findData(tag_id), False)
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()
# 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 populate_style_names(self) -> None:
def populate_tag_names(self) -> None:
# First clear all entries (called from settingswindow.py)
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)
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)
else:
self.cbSaveDataStyle.addItem(style.name(), style.short_name)
self.cbLoadDataStyle.addItem(style.name(), style.short_name)
self.cbSelectedWriteTags.addItem(tag.name(), tag.id)
self.cbSelectedReadTags.addItem(tag.name(), tag.id)
def populate_combo_boxes(self) -> None:
self.populate_style_names()
self.populate_tag_names()
self.adjust_load_style_combo()
self.adjust_save_style_combo()
self.adjust_tags_combo()
# Add talker entries
for t_id, talker in self.talkers.items():
@ -1449,26 +1449,26 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.cbFormat.addItem("Year One")
def remove_auto(self) -> None:
self.remove_tags(self.save_data_styles)
self.remove_tags(self.selected_write_tags)
def remove_tags(self, styles: list[str]) -> None:
# remove the indicated tags from the archive
def remove_tags(self, tag_ids: list[str]) -> None:
# remove the indicated tag_ids from the archive
ca_list = self.fileSelectionList.get_selected_archive_list()
has_md_count = 0
file_md_count = {}
for style in styles:
file_md_count[style] = 0
for tag_id in tag_ids:
file_md_count[tag_id] = 0
for ca in ca_list:
for style in styles:
if ca.has_metadata(style):
for tag_id in tag_ids:
if ca.has_tags(tag_id):
has_md_count += 1
file_md_count[style] += 1
file_md_count[tag_id] += 1
if has_md_count == 0:
QtWidgets.QMessageBox.information(
self,
"Remove Tags",
f"No archives with {', '.join([metadata_styles[style].name() for style in styles])} tags selected!",
f"No archives with {', '.join([tags[tag_id].name() for tag_id in tag_ids])} tags selected!",
)
return
@ -1481,7 +1481,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
reply = QtWidgets.QMessageBox.question(
self,
"Remove Tags",
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)?",
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)?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
@ -1503,16 +1503,16 @@ class TaggerWindow(QtWidgets.QMainWindow):
progdialog.setValue(prog_idx)
progdialog.setLabelText(str(ca.path))
QtCore.QCoreApplication.processEvents()
for style in styles:
if ca.has_metadata(style) and ca.is_writable():
if ca.remove_metadata(style):
for tag_id in tag_ids:
if ca.has_tags(tag_id) and ca.is_writable():
if ca.remove_tags(tag_id):
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(metadata_styles))
ca.load_cache(set(tags))
progdialog.hide()
QtCore.QCoreApplication.processEvents()
@ -1536,22 +1536,22 @@ class TaggerWindow(QtWidgets.QMainWindow):
ca_list = self.fileSelectionList.get_selected_archive_list()
has_src_count = 0
src_styles: list[str] = self.load_data_styles
dest_styles: list[str] = self.save_data_styles
src_tag_ids: list[str] = self.selected_read_tags
dest_tag_ids: list[str] = self.selected_write_tags
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 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 not dest_styles:
if not dest_tag_ids:
QtWidgets.QMessageBox.information(
self, "Copy Tags", "Can't copy tag style onto itself. Read style and modify style must be different."
self, "Copy Tags", "Can't copy tag tag onto itself. Read tag and modify tag must be different."
)
return
for ca in ca_list:
for style in src_styles:
if ca.has_metadata(style):
for tag_id in src_tag_ids:
if ca.has_tags(tag_id):
has_src_count += 1
continue
@ -1559,7 +1559,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
QtWidgets.QMessageBox.information(
self,
"Copy Tags",
f"No archives with {', '.join([metadata_styles[style].name() for style in src_styles])} tags selected!",
f"No archives with {', '.join([tags[tag_id].name() for tag_id in src_tag_ids])} tags selected!",
)
return
@ -1573,8 +1573,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([metadata_styles[style].name() for style in src_styles])} "
f"to {', '.join([metadata_styles[style].name() for style in dest_styles])} tags in "
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"{has_src_count} archive(s)?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
@ -1592,15 +1592,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
success_count = 0
for prog_idx, ca in enumerate(ca_list, 1):
ca_saved = False
md, error = self.overlay_ca_read_style(src_styles, ca)
md, error = self.read_selected_tags(src_tag_ids, ca)
if error is not None:
failed_list.append(ca.path)
continue
if md.is_empty:
continue
for style in dest_styles:
if ca.has_metadata(style):
for tag_id in dest_tag_ids:
if ca.has_tags(tag_id):
QtCore.QCoreApplication.processEvents()
if prog_dialog.wasCanceled():
break
@ -1610,10 +1610,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
if style == "cbi" and self.config[0].Metadata_Options__apply_transform_on_bulk_operation:
if tag_id == "cbi" and self.config[0].Metadata_Options__apply_transform_on_bulk_operation:
md = CBLTransformer(md, self.config[0]).apply()
if ca.write_metadata(md, style):
if ca.write_tags(md, tag_id):
if not ca_saved:
success_count += 1
ca_saved = True
@ -1621,7 +1621,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
failed_list.append(ca.path)
ca.reset_cache()
ca.load_cache({*self.load_data_styles, *self.save_data_styles})
ca.load_cache({*self.selected_read_tags, *self.selected_write_tags})
prog_dialog.hide()
QtCore.QCoreApplication.processEvents()
@ -1651,28 +1651,19 @@ 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 metadata, and parse file name if not there
md, error = self.overlay_ca_read_style(self.load_data_styles, ca)
# read in tags, and parse file name if not there
md, error = self.read_selected_tags(self.selected_read_tags, ca)
if error is not None:
QtWidgets.QMessageBox.warning(
self,
"Aborting...",
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.",
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.",
)
logger.error("Failed to load metadata for %s: %s", self.ca.path, error)
logger.error("Failed to load tags from %s: %s", self.ca.path, error)
return False, match_results
if md.is_empty:
@ -1687,7 +1678,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:
@ -1806,8 +1797,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.cbxRemoveMetadata.isChecked():
temp_opts.Auto_Tag__clear_metadata
if dlg.cbxClearMetadata.isChecked():
temp_opts.Auto_Tag__clear_tags
md = prepare_metadata(md, ct_md, temp_opts)
@ -1818,11 +1809,21 @@ class TaggerWindow(QtWidgets.QMainWindow):
online_results=matches,
match_status=MatchStatus.good_match,
md=md,
tags_written=self.save_data_styles,
tags_written=self.selected_write_tags,
)
# Save styles
if metadata_save():
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():
match_results.good_matches.append(res)
success = True
self.auto_tag_log("Save complete!\n")
@ -1831,13 +1832,13 @@ class TaggerWindow(QtWidgets.QMainWindow):
match_results.write_failures.append(res)
ca.reset_cache()
ca.load_cache({*self.load_data_styles, *self.save_data_styles})
ca.load_cache({*self.selected_read_tags, *self.selected_write_tags})
return success, match_results
def auto_tag(self) -> None:
ca_list = self.fileSelectionList.get_selected_archive_list()
styles = self.save_data_styles
tag_names = ", ".join([tags[tag_id].name() for tag_id in self.selected_write_tags])
if len(ca_list) == 0:
QtWidgets.QMessageBox.information(self, "Auto-Tag", "No archives selected!")
@ -1853,8 +1854,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.config[0],
(
f"You have selected {len(ca_list)} archive(s) to automatically identify and write "
+ ", ".join([metadata_styles[style].name() for style in styles])
+ " tags to.\n\nPlease choose config below, and select OK to Auto-Tag."
+ f"{tag_names} tags to.\n\nPlease choose config below, and select OK to Auto-Tag."
),
)
@ -1880,7 +1880,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_metadata(self.load_data_styles[0]).get_cover_page_index_list()[0]
cover_idx = ca.read_tags(self.selected_read_tags[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 +1914,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.atprogdialog = None
summary = ""
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"
summary += f"Successfully added {tag_names} 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 +1950,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
matchdlg = AutoTagMatchWindow(
self,
match_results.multiple_matches,
styles,
self.selected_write_tags,
lambda match: self.current_talker().fetch_comic_data(match.issue_id),
self.config[0],
self.current_talker(),
@ -1988,7 +1988,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
if reply == QtWidgets.QMessageBox.StandardButton.Discard:
return True
if reply == QtWidgets.QMessageBox.StandardButton.Save:
self.commit_metadata()
self.write_tags()
return True
return False
return True
@ -2024,12 +2024,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
def page_browser_closed(self) -> None:
self.page_browser = None
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]
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):
dlg = LogWindow(self)
dlg.set_text(self.comic_archive.read_metadata_string(style))
dlg.setWindowTitle(f"Raw {md_style.name()} Tag View")
dlg.set_text(self.comic_archive.read_raw_tags(tag.id))
dlg.setWindowTitle(f"Raw {tag.name()} Tag View")
dlg.exec()
def show_wiki(self) -> None:
@ -2075,7 +2075,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.load_data_styles, self.config, self.talkers)
dlg = RenameWindow(self, ca_list, self.selected_write_tags, self.config, self.talkers)
dlg.setModal(True)
if dlg.exec() and self.comic_archive is not None:
self.fileSelectionList.update_selected_rows()
@ -2095,25 +2095,23 @@ 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.overlay_ca_read_style(self.load_data_styles, self.comic_archive)
self.metadata, error = self.read_selected_tags(self.selected_write_tags, self.comic_archive)
if error is not None:
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")
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")
self.update_ui_for_archive()
def overlay_ca_read_style(
self, load_data_styles: list[str], ca: ComicArchive
) -> tuple[GenericMetadata, Exception | None]:
def read_selected_tags(self, tag_ids: list[str], ca: ComicArchive) -> tuple[GenericMetadata, Exception | None]:
md = GenericMetadata()
error = None
try:
for style in load_data_styles:
metadata = ca.read_metadata(style)
for tag_id in tag_ids:
metadata = ca.read_tags(tag_id)
md.overlay(
metadata,
mode=self.config[0].Metadata_Options__comic_merge,
merge_lists=self.config[0].Metadata_Options__comic_merge_lists,
mode=self.config[0].Metadata_Options__tag_merge,
merge_lists=self.config[0].Metadata_Options__tag_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="cbxRemoveMetadata">
<widget class="QCheckBox" name="cbxClearMetadata">
<property name="toolTip">
<string>Removes existing metadata before applying retrieved metadata</string>
<string>Removes existing tags before applying downloaded metadata</string>
</property>
<property name="text">
<string>Clear Existing Metadata during import</string>
<string>Clear Existing tags when downloading metadata</string>
</property>
</widget>
</item>
@ -129,7 +129,7 @@
</sizepolicy>
</property>
<property name="text">
<string>Specify series search string for all selected archives:</string>
<string>Specify series search for all selected archives:</string>
</property>
</widget>
</item>

View File

@ -73,7 +73,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_save_style_combo' in taggerwindow.py)
# Expected that state of *all* checkboxes will be set ('adjust_tags_combo' in taggerwindow.py)
if self.count() == 1:
self.model().item(0).setCheckState(Qt.CheckState.Checked)
@ -276,7 +276,7 @@ class CheckableOrderComboBox(QtWidgets.QComboBox):
super().__init__(*args, **kwargs)
itemDelegate = ReadStyleItemDelegate(self)
itemDelegate.setToolTip(
"Select which read style(s) to use", "Move item up in priority", "Move item down in priority"
"Select which read tag(s) to use", "Move item up in priority", "Move item down in priority"
)
self.setItemDelegate(itemDelegate)
@ -344,7 +344,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_save_style_combo' in taggerwindow.py)
# Expected that state of *all* checkboxes will be set ('adjust_tags_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>MD</string>
<string>Tags</string>
</property>
<property name="toolTip">
<string>Metadata</string>
<string>Tags</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>

View File

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

View File

@ -79,11 +79,12 @@ border-radius: 4px;
</property>
<item row="0" column="0">
<widget class="QLabel" name="lbl_md_source">
<property name="text">
<string>Data Source</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
<set>Qt::AlignRight|Qt::AlignVCenter</set>
</property>
<property name="text">
<string>Metadata
Source</string>
</property>
</widget>
</item>
@ -91,26 +92,32 @@ border-radius: 4px;
<widget class="QComboBox" name="cbx_sources"/>
</item>
<item row="1" column="1">
<widget class="CheckableOrderComboBox" name="cbLoadDataStyle">
<widget class="CheckableOrderComboBox" name="cbSelectedReadTags">
<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="cbSaveDataStyle"/>
<widget class="CheckableComboBox" name="cbSelectedWriteTags"/>
</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 Style</string>
<string>Read Tags</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>Modify Styles</string>
<string>Write Tags</string>
</property>
</widget>
</item>
@ -1300,7 +1307,7 @@ border-radius: 4px;
</widget>
<widget class="QMenu" name="menuTags">
<property name="title">
<string>Tags</string>
<string>Metadata</string>
</property>
<addaction name="actionClearEntryForm"/>
<addaction name="actionParse_Filename"/>
@ -1442,7 +1449,7 @@ border-radius: 4px;
<string>Clear Form</string>
</property>
<property name="toolTip">
<string>Clear all metadata for the current comic</string>
<string>Clear all the data on the screen</string>
</property>
<property name="statusTip">
<string>Clear all the data on the screen</string>
@ -1461,10 +1468,10 @@ border-radius: 4px;
<string>Copy Tags</string>
</property>
<property name="toolTip">
<string>Copy metadata from the selected 'read' tags to the selected 'modify' tags</string>
<string>Copy the selected 'read' tags to the selected 'write' tags</string>
</property>
<property name="statusTip">
<string>Copy metadata from one type of tag to another</string>
<string>Copy the selected 'read' tags to the selected 'write' tags</string>
</property>
<property name="shortcut">
<string>Ctrl+C</string>
@ -1721,8 +1728,8 @@ border-radius: 4px;
<slot>trigger()</slot>
<hints>
<hint type="sourcelabel">
<x>900</x>
<y>536</y>
<x>359</x>
<y>108</y>
</hint>
<hint type="destinationlabel">
<x>-1</x>
@ -1737,8 +1744,8 @@ border-radius: 4px;
<slot>trigger()</slot>
<hints>
<hint type="sourcelabel">
<x>900</x>
<y>576</y>
<x>359</x>
<y>108</y>
</hint>
<hint type="destinationlabel">
<x>-1</x>

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, TagOrigin
from comicapi.genericmetadata import ComicSeries, GenericMetadata, MetadataOrigin
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(
tag_origin=TagOrigin(self.id, self.name),
data_origin=MetadataOrigin(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

@ -64,10 +64,10 @@ comicapi.archiver =
sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver
rar = comicapi.archivers.rar:RarArchiver
folder = comicapi.archivers.folder:FolderArchiver
comicapi.metadata =
cr = comicapi.metadata.comicrack:ComicRack
cbi = comicapi.metadata.comicbookinfo:ComicBookInfo
comet = comicapi.metadata.comet:CoMet
comicapi.tags =
cr = comicapi.tags.comicrack:ComicRack
cbi = comicapi.tags.comicbookinfo:ComicBookInfo
comet = comicapi.tags.comet:CoMet
comictagger.talker =
comicvine = comictalker.talkers.comicvine:ComicVineTalker
pyinstaller40 =

View File

@ -271,13 +271,13 @@ metadata_prepared = (
(
(
comicapi.genericmetadata.GenericMetadata(
issue_id="123", tag_origin=comicapi.genericmetadata.TagOrigin("SOURCE", "Source")
issue_id="123", data_origin=comicapi.genericmetadata.MetadataOrigin("SOURCE", "Source")
),
comicapi.genericmetadata.GenericMetadata(),
),
comicapi.genericmetadata.GenericMetadata(
issue_id="123",
tag_origin=comicapi.genericmetadata.TagOrigin("SOURCE", "Source"),
data_origin=comicapi.genericmetadata.MetadataOrigin("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(
tag_origin=comicapi.genericmetadata.TagOrigin("comicvine", "Comic Vine"),
data_origin=comicapi.genericmetadata.MetadataOrigin("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,
tag_origin=comicapi.genericmetadata.TagOrigin("comicvine", "Comic Vine"),
data_origin=comicapi.genericmetadata.MetadataOrigin("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_metadata("cr")
md = cbz.read_tags("cr")
assert isinstance(md.pages[0]["type"], str)
def test_metadata_read(cbz, md_saved):
md = cbz.read_metadata("cr")
def test_read_tags(cbz, md_saved):
md = cbz.read_tags("cr")
assert md == md_saved
def test_save_cr(tmp_comic):
md = tmp_comic.read_metadata("cr")
def test_write_cr(tmp_comic):
md = tmp_comic.read_tags("cr")
md.apply_default_page_list(tmp_comic.get_page_name_list())
assert tmp_comic.write_metadata(md, "cr")
assert tmp_comic.write_tags(md, "cr")
md = tmp_comic.read_metadata("cr")
md = tmp_comic.read_tags("cr")
def test_save_cbi(tmp_comic):
md = tmp_comic.read_metadata("cr")
def test_write_cbi(tmp_comic):
md = tmp_comic.read_tags("cr")
md.apply_default_page_list(tmp_comic.get_page_name_list())
assert tmp_comic.write_metadata(md, "cbi")
assert tmp_comic.write_tags(md, "cbi")
md = tmp_comic.read_metadata("cbi")
md = tmp_comic.read_tags("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_metadata(comicapi.genericmetadata.md_test, "cr")
assert tmp_comic.write_tags(comicapi.genericmetadata.md_test, "cr")
md = tmp_comic.read_metadata("cr")
md = tmp_comic.read_tags("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_metadata(comicapi.genericmetadata.md_test, "cbi")
assert tmp_comic.write_tags(comicapi.genericmetadata.md_test, "cbi")
md = tmp_comic.read_metadata("cbi")
supported_attributes = comicapi.comicarchive.metadata_styles["cbi"].supported_attributes
md = tmp_comic.read_tags("cbi")
supported_attributes = comicapi.comicarchive.tags["cbi"].supported_attributes
assert md.get_clean_metadata(*supported_attributes) == md_saved.get_clean_metadata(*supported_attributes)
def test_page_type_save(tmp_comic):
md = tmp_comic.read_metadata("cr")
def test_page_type_write(tmp_comic):
md = tmp_comic.read_tags("cr")
t = md.pages[0]
t["type"] = ""
assert tmp_comic.write_metadata(md, "cr")
assert tmp_comic.write_tags(md, "cr")
md = tmp_comic.read_metadata("cr")
md = tmp_comic.read_tags("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_metadata(comicapi.genericmetadata.md_test, "cr")
result = tmp_comic.write_tags(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_metadata("cr")
md = comic_archive.read_tags("cr")
assert md == md_saved

View File

@ -161,7 +161,7 @@ def md():
@pytest.fixture
def md_saved():
yield comicapi.genericmetadata.md_test.replace(tag_origin=None, issue_id=None, series_id=None)
yield comicapi.genericmetadata.md_test.replace(data_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_metadata(md_saved.replace(series="nothing"), "cr")
tmp_comic.write_tags(md_saved.replace(series="nothing"), "cr")
md = tmp_comic.read_metadata("cr")
md = tmp_comic.read_tags("cr")
# Check that it changed
assert md != md_saved
# Clear the cached metadata
# Clear the cached tags
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__type_read = ["cr"]
config[0].Runtime_Options__type_modify = ["cr"]
config[0].Runtime_Options__tags_read = ["cr"]
config[0].Runtime_Options__tags_write = ["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_metadata("cr")
md = tmp_comic.read_tags("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_metadata("cr")
md = tmp_comic.read_tags("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__type_modify = ["cr"]
config[0].Runtime_Options__tags_write = ["cr"]
# Run ComicTagger
CLI(config[0], talkers).run()
# Read the CBZ
md = tmp_comic.read_metadata("cr")
md = tmp_comic.read_tags("cr")
# The default page list is set on load if the comic has the requested metadata style
# The default page list is set on load if the comic has the requested tags
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_metadata("cr")
md = tmp_comic.read_tags("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_metadata("cr")
md = tmp_comic.read_tags("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_metadata("cr"))
result, issues = ii.identify(cbz, cbz.read_tags("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,27 +7,25 @@ import comicapi.genericmetadata
import testing.comicdata
from comictaggerlib.md import prepare_metadata
metadata_styles = []
tags = []
for x in entry_points(group="comicapi.metadata"):
meetadata = x.load()
supported = meetadata.enabled
for x in entry_points(group="comicapi.tag"):
tag = x.load()
supported = tag.enabled
exe_found = True
metadata_styles.append(
pytest.param(meetadata, marks=pytest.mark.xfail(not supported, reason="metadata not enabled"))
)
tags.append(pytest.param(tag, marks=pytest.mark.xfail(not supported, reason="tags not enabled")))
@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)
@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)
md = md_saved.get_clean_metadata(*supported_attributes)
# Hack back in the pages variable because CoMet supports identifying the cover by the filename
if md_style.short_name == "comet":
if tag.id == "comet":
md.pages = [
comicapi.genericmetadata.ImageMetadata(
image_index=0, filename="!cover.jpg", type=comicapi.genericmetadata.PageType.FrontCover