diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py
index cc6760b..060c461 100644
--- a/comicapi/genericmetadata.py
+++ b/comicapi/genericmetadata.py
@@ -24,6 +24,7 @@ from __future__ import annotations
import copy
import dataclasses
import logging
+import sys
from collections.abc import Sequence
from enum import Enum, auto
from typing import TYPE_CHECKING, Any, TypedDict, Union
@@ -32,6 +33,7 @@ from typing_extensions import NamedTuple, Required
from comicapi import utils
from comicapi._url import Url, parse_url
+from comicapi.utils import norm_fold
if TYPE_CHECKING:
Union
@@ -46,6 +48,45 @@ class __remove(Enum):
REMOVE = __remove.REMOVE
+if sys.version_info < (3, 11):
+
+ class StrEnum(str, Enum):
+ """
+ Enum where members are also (and must be) strings
+ """
+
+ def __new__(cls, *values: Any) -> Any:
+ "values must already be of type `str`"
+ if len(values) > 3:
+ raise TypeError(f"too many arguments for str(): {values!r}")
+ if len(values) == 1:
+ # it must be a string
+ if not isinstance(values[0], str):
+ raise TypeError(f"{values[0]!r} is not a string")
+ if len(values) >= 2:
+ # check that encoding argument is a string
+ if not isinstance(values[1], str):
+ raise TypeError(f"encoding must be a string, not {values[1]!r}")
+ if len(values) == 3:
+ # check that errors argument is a string
+ if not isinstance(values[2], str):
+ raise TypeError("errors must be a string, not %r" % (values[2]))
+ value = str(*values)
+ member = str.__new__(cls, value)
+ member._value_ = value
+ return member
+
+ @staticmethod
+ def _generate_next_value_(name: str, start: int, count: int, last_values: Any) -> str:
+ """
+ Return the lower-cased version of the member name.
+ """
+ return name.lower()
+
+else:
+ from enum import StrEnum
+
+
class PageType:
"""
These page info classes are exactly the same as the CIX scheme, since
@@ -104,6 +145,12 @@ class TagOrigin(NamedTuple):
name: str
+class OverlayMode(StrEnum):
+ overlay = auto()
+ add_missing = auto()
+ combine = auto()
+
+
@dataclasses.dataclass
class GenericMetadata:
writer_synonyms = ("writer", "plotter", "scripter", "script")
@@ -214,101 +261,172 @@ class GenericMetadata:
new_md.__post_init__()
return new_md
- def overlay(self, new_md: GenericMetadata) -> None:
- """Overlay a metadata object on this one
+ def credit_dedupe(self, cur: list[Credit], new: list[Credit]) -> list[Credit]:
+ if len(new) == 0:
+ return cur
+ if len(cur) == 0:
+ return new
- That is, when the new object has non-None values, over-write them
- to this one.
- """
+ # Create dict for deduplication
+ new_dict: dict[str, Credit] = {f"{norm_fold(n['person'])}_{n['role'].casefold()}": n for n in new}
+ cur_dict: dict[str, Credit] = {f"{norm_fold(c['person'])}_{c['role'].casefold()}": c for c in cur}
- def assign(cur: str, new: Any) -> None:
- if new is not None:
- if new is REMOVE:
- if isinstance(getattr(self, cur), (list, set)):
- getattr(self, cur).clear()
- else:
- setattr(self, cur, None)
- return
+ # Any duplicates use the 'new' value
+ cur_dict.update(new_dict)
+ return list(cur_dict.values())
- if isinstance(new, str) and len(new) == 0:
- setattr(self, cur, None)
- elif isinstance(new, (list, set)) and len(new) == 0:
- pass
+ def assign_dedupe(self, new: list[str] | set[str], cur: list[str] | set[str]) -> list[str] | set[str]:
+ """Dedupes normalised (NFKD), casefolded values using 'new' values on collisions"""
+ if len(new) == 0:
+ return cur
+ if len(cur) == 0:
+ return new
+
+ # Create dict values for deduplication
+ new_dict: dict[str, str] = {norm_fold(n): n for n in new}
+ cur_dict: dict[str, str] = {norm_fold(c): c for c in cur}
+
+ if isinstance(cur, list):
+ cur_dict.update(new_dict)
+ return list(cur_dict.values())
+
+ if isinstance(cur, set):
+ cur_dict.update(new_dict)
+ return set(cur_dict.values())
+
+ # All else fails
+ return cur
+
+ def assign_overlay(self, cur: Any, new: Any) -> Any:
+ """Overlay - When the 'new' object has non-None values, overwrite 'cur'(rent) with 'new'."""
+ if new is None:
+ return cur
+ if isinstance(new, (list, set)) and len(new) == 0:
+ return cur
+ else:
+ return new
+
+ def assign_add_missing(self, cur: Any, new: Any) -> Any:
+ """Add Missing - Any 'cur(rent)' values that are None or an empty list/set, add 'new' non-None values"""
+ if new is None:
+ return cur
+ if cur is None:
+ return new
+ elif isinstance(cur, (list, set)) and len(cur) == 0:
+ return new
+ else:
+ return cur
+
+ def assign_combine(self, cur: Any, new: Any) -> Any:
+ """Combine - Combine lists and sets (checking for dupes). All non-None values from new replace cur(rent)"""
+ if new is None:
+ return cur
+ if isinstance(new, (list, set)) and isinstance(cur, (list, set)):
+ # Check for weblinks (Url is a tuple)
+ if len(new) > 0 and isinstance(new, list) and isinstance(new[0], Url):
+ return list(set(new).union(cur))
+ return self.assign_dedupe(new, cur)
+ else:
+ return new
+
+ def overlay(self, new_md: GenericMetadata, mode: OverlayMode = OverlayMode.overlay) -> None:
+ """Overlay a new metadata object on this one"""
+
+ assign_funcs = {
+ OverlayMode.overlay: self.assign_overlay,
+ OverlayMode.add_missing: self.assign_add_missing,
+ OverlayMode.combine: self.assign_combine,
+ }
+
+ def assign(cur: Any, new: Any) -> Any:
+ if new is REMOVE:
+ if isinstance(cur, (list, set)):
+ cur.clear()
+ return cur
else:
- setattr(self, cur, new)
+ return None
+
+ assign_func = assign_funcs.get(mode, self.assign_overlay)
+ return assign_func(cur, new)
if not new_md.is_empty:
self.is_empty = False
- assign("tag_origin", new_md.tag_origin)
- assign("issue_id", new_md.issue_id)
- assign("series_id", new_md.series_id)
+ self.tag_origin = assign(self.tag_origin, new_md.tag_origin) # TODO use and purpose now?
+ self.issue_id = assign(self.issue_id, new_md.issue_id)
+ self.series_id = assign(self.series_id, new_md.series_id)
- assign("series", new_md.series)
- assign("series_aliases", new_md.series_aliases)
- assign("issue", new_md.issue)
- assign("issue_count", new_md.issue_count)
- assign("title", new_md.title)
- assign("title_aliases", new_md.title_aliases)
- assign("volume", new_md.volume)
- assign("volume_count", new_md.volume_count)
- assign("genres", new_md.genres)
- assign("description", new_md.description)
- assign("notes", new_md.notes)
+ self.series = assign(self.series, new_md.series)
+ self.series_aliases = assign(self.series_aliases, new_md.series_aliases)
+ self.issue = assign(self.issue, new_md.issue)
+ self.issue_count = assign(self.issue_count, new_md.issue_count)
+ self.title = assign(self.title, new_md.title)
+ self.title_aliases = assign(self.title_aliases, new_md.title_aliases)
+ self.volume = assign(self.volume, new_md.volume)
+ self.volume_count = assign(self.volume_count, new_md.volume_count)
+ self.genres = assign(self.genres, new_md.genres)
+ self.description = assign(self.description, new_md.description)
+ self.notes = assign(self.notes, new_md.notes)
- assign("alternate_series", new_md.alternate_series)
- assign("alternate_number", new_md.alternate_number)
- assign("alternate_count", new_md.alternate_count)
- assign("story_arcs", new_md.story_arcs)
- assign("series_groups", new_md.series_groups)
+ self.alternate_series = assign(self.alternate_series, new_md.alternate_series)
+ self.alternate_number = assign(self.alternate_number, new_md.alternate_number)
+ self.alternate_count = assign(self.alternate_count, new_md.alternate_count)
+ self.story_arcs = assign(self.story_arcs, new_md.story_arcs)
+ self.series_groups = assign(self.series_groups, new_md.series_groups)
- assign("publisher", new_md.publisher)
- assign("imprint", new_md.imprint)
- assign("day", new_md.day)
- assign("month", new_md.month)
- assign("year", new_md.year)
- assign("language", new_md.language)
- assign("country", new_md.country)
- assign("web_links", new_md.web_links)
- assign("format", new_md.format)
- assign("manga", new_md.manga)
- assign("black_and_white", new_md.black_and_white)
- assign("maturity_rating", new_md.maturity_rating)
- assign("critical_rating", new_md.critical_rating)
- assign("scan_info", new_md.scan_info)
+ self.publisher = assign(self.publisher, new_md.publisher)
+ self.imprint = assign(self.imprint, new_md.imprint)
+ self.day = assign(self.day, new_md.day)
+ self.month = assign(self.month, new_md.month)
+ self.year = assign(self.year, new_md.year)
+ self.language = assign(self.language, new_md.language)
+ self.country = assign(self.country, new_md.country)
+ self.web_links = assign(self.web_links, new_md.web_links)
+ self.format = assign(self.format, new_md.format)
+ self.manga = assign(self.manga, new_md.manga)
+ self.black_and_white = assign(self.black_and_white, new_md.black_and_white)
+ self.maturity_rating = assign(self.maturity_rating, new_md.maturity_rating)
+ self.critical_rating = assign(self.critical_rating, new_md.critical_rating)
+ self.scan_info = assign(self.scan_info, new_md.scan_info)
- assign("tags", new_md.tags)
- assign("pages", new_md.pages)
- assign("page_count", new_md.page_count)
+ self.tags = assign(self.tags, new_md.tags)
+ self.pages = assign(self.pages, new_md.pages)
+ self.page_count = assign(self.page_count, new_md.page_count)
- assign("characters", new_md.characters)
- assign("teams", new_md.teams)
- assign("locations", new_md.locations)
- self.overlay_credits(new_md.credits)
+ self.characters = assign(self.characters, new_md.characters)
+ self.teams = assign(self.teams, new_md.teams)
+ self.locations = assign(self.locations, new_md.locations)
+ self.credits = self.assign_credits(self.credits, new_md.credits)
- assign("price", new_md.price)
- assign("is_version_of", new_md.is_version_of)
- assign("rights", new_md.rights)
- assign("identifier", new_md.identifier)
- assign("last_mark", new_md.last_mark)
- assign("_cover_image", new_md._cover_image)
- assign("_alternate_images", new_md._alternate_images)
+ self.price = assign(self.price, new_md.price)
+ self.is_version_of = assign(self.is_version_of, new_md.is_version_of)
+ self.rights = assign(self.rights, new_md.rights)
+ self.identifier = assign(self.identifier, new_md.identifier)
+ self.last_mark = assign(self.last_mark, new_md.last_mark)
+ self._cover_image = assign(self._cover_image, new_md._cover_image)
+ self._alternate_images = assign(self._alternate_images, new_md._alternate_images)
- def overlay_credits(self, new_credits: list[Credit]) -> None:
+ def assign_credits_overlay(self, cur_credits: list[Credit], new_credits: list[Credit]) -> list[Credit]:
+ return self.credit_dedupe(cur_credits, new_credits)
+
+ def assign_credits_add_missing(self, cur_credits: list[Credit], new_credits: list[Credit]) -> list[Credit]:
+ # Send new_credits as cur_credits and vis-versa to keep cur_credit on duplication
+ return self.credit_dedupe(new_credits, cur_credits)
+
+ def assign_credits(
+ self, cur_credits: list[Credit], new_credits: list[Credit], mode: OverlayMode = OverlayMode.overlay
+ ) -> list[Credit]:
if new_credits is REMOVE:
- self.credits = []
- return
- for c in new_credits:
- primary = bool("primary" in c and c["primary"])
+ return []
- # Remove credit role if person is blank
- if c["person"] == "":
- for r in reversed(self.credits):
- if r["role"].casefold() == c["role"].casefold():
- self.credits.remove(r)
- # otherwise, add it!
- else:
- self.add_credit(c["person"], c["role"], primary)
+ assign_credit_funcs = {
+ OverlayMode.overlay: self.assign_credits_overlay,
+ OverlayMode.add_missing: self.assign_credits_add_missing,
+ OverlayMode.combine: self.assign_credits_overlay,
+ }
+
+ assign_credit_func = assign_credit_funcs.get(mode, self.assign_credits_overlay)
+ return assign_credit_func(cur_credits, new_credits)
def apply_default_page_list(self, page_list: Sequence[str]) -> None:
# generate a default page list, with the first page marked as the cover
@@ -354,12 +472,15 @@ class GenericMetadata:
return coverlist
def add_credit(self, person: str, role: str, primary: bool = False) -> None:
+ if person == "":
+ return
+
credit = Credit(person=person, role=role, primary=primary)
# look to see if it's not already there...
found = False
for c in self.credits:
- if c["person"].casefold() == person.casefold() and c["role"].casefold() == role.casefold():
+ if norm_fold(c["person"]) == norm_fold(person) and norm_fold(c["role"]) == norm_fold(role):
# no need to add it. just adjust the "primary" flag as needed
c["primary"] = primary
found = True
diff --git a/comicapi/utils.py b/comicapi/utils.py
index 91239a5..ccfdac7 100644
--- a/comicapi/utils.py
+++ b/comicapi/utils.py
@@ -223,6 +223,11 @@ def parse_filename(
return fni
+def norm_fold(string: str) -> str:
+ """Normalise and casefold string"""
+ return unicodedata.normalize("NFKD", string).casefold()
+
+
def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) -> str:
split_notes, split_str, untouched_notes = (existing_notes or "").rpartition(split)
if split_notes or split_str:
diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py
index 0b0acb7..163f597 100644
--- a/comictaggerlib/autotagmatchwindow.py
+++ b/comictaggerlib/autotagmatchwindow.py
@@ -81,7 +81,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.load_data_styles = load_styles
+ self._styles = styles
self.fetch_func = fetch_func
self.current_match_set_idx = 0
@@ -262,8 +262,8 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
return
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
- md.overlay(ct_md)
- for style in self.load_data_styles:
+ md.overlay(ct_md, self.config.Metadata_Options__source_overlay)
+ for style in self._styles:
success = ca.write_metadata(md, style)
QtWidgets.QApplication.restoreOverrideCursor()
if not success:
diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py
index 79cbe4e..d195273 100644
--- a/comictaggerlib/cbltransformer.py
+++ b/comictaggerlib/cbltransformer.py
@@ -30,7 +30,7 @@ class CBLTransformer:
self.config = config
def apply(self) -> GenericMetadata:
- if self.config.Comic_Book_Lover__assume_lone_credit_is_primary:
+ if self.config.Metadata_Options__cbl_assume_lone_credit_is_primary:
# helper
def set_lone_primary(role_list: list[str]) -> tuple[Credit | None, int]:
lone_credit: Credit | None = None
@@ -56,19 +56,19 @@ class CBLTransformer:
c["primary"] = False
self.metadata.add_credit(c["person"], "Artist", True)
- if self.config.Comic_Book_Lover__copy_characters_to_tags:
+ if self.config.Metadata_Options__cbl_copy_characters_to_tags:
self.metadata.tags.update(x for x in self.metadata.characters)
- if self.config.Comic_Book_Lover__copy_teams_to_tags:
+ if self.config.Metadata_Options__cbl_copy_teams_to_tags:
self.metadata.tags.update(x for x in self.metadata.teams)
- if self.config.Comic_Book_Lover__copy_locations_to_tags:
+ if self.config.Metadata_Options__cbl_copy_locations_to_tags:
self.metadata.tags.update(x for x in self.metadata.locations)
- if self.config.Comic_Book_Lover__copy_storyarcs_to_tags:
+ if self.config.Metadata_Options__cbl_copy_storyarcs_to_tags:
self.metadata.tags.update(x for x in self.metadata.story_arcs)
- if self.config.Comic_Book_Lover__copy_notes_to_comments:
+ if self.config.Metadata_Options__cbl_copy_notes_to_comments:
if self.metadata.notes is not None:
if self.metadata.description is None:
self.metadata.description = ""
@@ -77,7 +77,7 @@ class CBLTransformer:
if self.metadata.notes not in self.metadata.description:
self.metadata.description += self.metadata.notes
- if self.config.Comic_Book_Lover__copy_weblink_to_comments:
+ if self.config.Metadata_Options__cbl_copy_weblink_to_comments:
for web_link in self.metadata.web_links:
temp_desc = self.metadata.description
if temp_desc is None:
diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py
index 7b7dee4..13dc94a 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -127,7 +127,7 @@ class CLI:
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
return GenericMetadata()
- if self.config.Comic_Book_Lover__apply_transform_on_import:
+ if self.config.Metadata_Options__cbl_apply_transform_on_import:
ct_md = CBLTransformer(ct_md, self.config).apply()
return ct_md
@@ -253,11 +253,12 @@ class CLI:
if ca.has_metadata(style):
try:
t_md = ca.read_metadata(style)
- md.overlay(t_md)
+ md.overlay(t_md, self.config.Metadata_Options__read_style_overlay)
+ break
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
- # finally, use explicit stuff
+ # finally, use explicit stuff (always 'overlay' mode)
md.overlay(self.config.Runtime_Options__metadata)
return md
@@ -340,7 +341,7 @@ class CLI:
src_style_name = md_styles[self.config.Commands__copy].name()
if not self.config.Runtime_Options__dryrun:
- if self.config.Comic_Book_Lover__apply_transform_on_bulk_operation == "cbi":
+ if self.config.Metadata_Options__cbl_apply_transform_on_bulk_operation == "cbi":
md = CBLTransformer(md, self.config).apply()
if ca.write_metadata(md, style):
diff --git a/comictaggerlib/ctsettings/__init__.py b/comictaggerlib/ctsettings/__init__.py
index 4029bd4..5e68f71 100644
--- a/comictaggerlib/ctsettings/__init__.py
+++ b/comictaggerlib/ctsettings/__init__.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import json
import logging
import pathlib
+from enum import Enum
from typing import Any
import settngs
@@ -55,7 +56,11 @@ def validate_types(config: settngs.Config[settngs.Values]) -> settngs.Config[set
if setting.type is not None:
# If it is not the default and the type attribute is not None
# use it to convert the loaded string into the expected value
- if isinstance(value, str):
+ if (
+ isinstance(value, str)
+ or isinstance(default, Enum)
+ or (isinstance(setting.type, type) and issubclass(setting.type, Enum))
+ ):
config.values[setting.group][setting.dest] = setting.type(value)
return config
diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py
index a6066e6..6f67c98 100644
--- a/comictaggerlib/ctsettings/file.py
+++ b/comictaggerlib/ctsettings/file.py
@@ -6,6 +6,7 @@ import uuid
import settngs
from comicapi import utils
+from comicapi.genericmetadata import OverlayMode
from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns
from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
@@ -13,13 +14,6 @@ from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replaceme
def general(parser: settngs.Manager) -> None:
# General Settings
parser.add_setting("check_for_new_version", default=False, cmdline=False)
- parser.add_setting(
- "--disable-cr",
- default=False,
- action=argparse.BooleanOptionalAction,
- help="Disable the ComicRack metadata type",
- )
- parser.add_setting("use_short_metadata_names", default=False, action=argparse.BooleanOptionalAction, cmdline=False)
parser.add_setting(
"--prompt-on-save",
default=True,
@@ -164,17 +158,38 @@ def talker(parser: settngs.Manager) -> None:
)
-def cbl(parser: settngs.Manager) -> None:
+def md_options(parser: settngs.Manager) -> None:
# CBL Transform settings
- parser.add_setting("--assume-lone-credit-is-primary", default=False, action=argparse.BooleanOptionalAction)
- parser.add_setting("--copy-characters-to-tags", default=False, action=argparse.BooleanOptionalAction)
- parser.add_setting("--copy-teams-to-tags", default=False, action=argparse.BooleanOptionalAction)
- parser.add_setting("--copy-locations-to-tags", default=False, action=argparse.BooleanOptionalAction)
- parser.add_setting("--copy-storyarcs-to-tags", default=False, action=argparse.BooleanOptionalAction)
- parser.add_setting("--copy-notes-to-comments", default=False, action=argparse.BooleanOptionalAction)
- parser.add_setting("--copy-weblink-to-comments", default=False, action=argparse.BooleanOptionalAction)
- 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("--cbl-assume-lone-credit-is-primary", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-copy-characters-to-tags", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-copy-teams-to-tags", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-copy-locations-to-tags", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-copy-storyarcs-to-tags", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-copy-notes-to-comments", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-copy-weblink-to-comments", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-apply-transform-on-import", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--cbl-apply-transform-on-bulk-operation", default=False, action=argparse.BooleanOptionalAction)
+
+ parser.add_setting(
+ "--read-style-overlay",
+ default=OverlayMode.overlay,
+ type=OverlayMode,
+ help="How to overlay new metadata on the current for enabled read styles (CR, CBL, etc.)",
+ )
+ parser.add_setting(
+ "--source-overlay",
+ default=OverlayMode.overlay,
+ type=OverlayMode,
+ help="How to overlay the new metadata from a source (CV, Metron, GCD, etc.) on to the current",
+ )
+
+ parser.add_setting("use_short_metadata_names", default=False, action=argparse.BooleanOptionalAction, cmdline=False)
+ parser.add_setting(
+ "--disable-cr",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Disable the ComicRack metadata type",
+ )
def rename(parser: settngs.Manager) -> None:
@@ -311,7 +326,7 @@ def register_file_settings(parser: settngs.Manager) -> None:
parser.add_group("Issue Identifier", identifier, False)
parser.add_group("Filename Parsing", filename, False)
parser.add_group("Sources", talker, False)
- parser.add_group("Comic Book Lover", cbl, False)
+ parser.add_group("Metadata Options", md_options, False)
parser.add_group("File Rename", rename, False)
parser.add_group("Auto-Tag", autotag, False)
parser.add_group("General", general, False)
diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py
index a0677fb..089ff84 100644
--- a/comictaggerlib/ctsettings/settngs_namespace.py
+++ b/comictaggerlib/ctsettings/settngs_namespace.py
@@ -9,6 +9,7 @@ import comicapi.utils
import comictaggerlib.ctsettings.types
import comictaggerlib.defaults
import comictaggerlib.resulttypes
+from comicapi.genericmetadata import OverlayMode
class SettngsNS(settngs.TypedNS):
@@ -74,15 +75,19 @@ class SettngsNS(settngs.TypedNS):
Sources__source: str
Sources__remove_html_tables: bool
- Comic_Book_Lover__assume_lone_credit_is_primary: bool
- Comic_Book_Lover__copy_characters_to_tags: bool
- Comic_Book_Lover__copy_teams_to_tags: bool
- Comic_Book_Lover__copy_locations_to_tags: bool
- Comic_Book_Lover__copy_storyarcs_to_tags: bool
- Comic_Book_Lover__copy_notes_to_comments: bool
- Comic_Book_Lover__copy_weblink_to_comments: bool
- Comic_Book_Lover__apply_transform_on_import: bool
- Comic_Book_Lover__apply_transform_on_bulk_operation: bool
+ Metadata_Options__cbl_assume_lone_credit_is_primary: bool
+ Metadata_Options__cbl_copy_characters_to_tags: bool
+ Metadata_Options__cbl_copy_teams_to_tags: bool
+ Metadata_Options__cbl_copy_locations_to_tags: bool
+ Metadata_Options__cbl_copy_storyarcs_to_tags: bool
+ Metadata_Options__cbl_copy_notes_to_comments: bool
+ Metadata_Options__cbl_copy_weblink_to_comments: bool
+ Metadata_Options__cbl_apply_transform_on_import: bool
+ Metadata_Options__cbl_apply_transform_on_bulk_operation: bool
+ Metadata_Options__read_style_overlay: OverlayMode
+ Metadata_Options__source_overlay: OverlayMode
+ Metadata_Options__use_short_metadata_names: bool
+ Metadata_Options__disable_cr: bool
File_Rename__template: str
File_Rename__issue_number_padding: int
@@ -101,8 +106,6 @@ class SettngsNS(settngs.TypedNS):
Auto_Tag__remove_archive_after_successful_match: bool
General__check_for_new_version: bool
- General__disable_cr: bool
- General__use_short_metadata_names: bool
General__prompt_on_save: bool
Dialog_Flags__show_disclaimer: bool
@@ -189,16 +192,19 @@ class Sources(typing.TypedDict):
remove_html_tables: bool
-class Comic_Book_Lover(typing.TypedDict):
- assume_lone_credit_is_primary: bool
- copy_characters_to_tags: bool
- copy_teams_to_tags: bool
- copy_locations_to_tags: bool
- copy_storyarcs_to_tags: bool
- copy_notes_to_comments: bool
- copy_weblink_to_comments: bool
- apply_transform_on_import: bool
- apply_transform_on_bulk_operation: bool
+class Metadata_Options(typing.TypedDict):
+ cbl_assume_lone_credit_is_primary: bool
+ cbl_copy_characters_to_tags: bool
+ cbl_copy_teams_to_tags: bool
+ cbl_copy_locations_to_tags: bool
+ cbl_copy_storyarcs_to_tags: bool
+ cbl_copy_notes_to_comments: bool
+ cbl_copy_weblink_to_comments: bool
+ cbl_apply_transform_on_import: bool
+ cbl_apply_transform_on_bulk_operation: bool
+ metadata_overlay: OverlayMode
+ use_short_metadata_names: bool
+ disable_cr: bool
class File_Rename(typing.TypedDict):
@@ -223,8 +229,6 @@ class Auto_Tag(typing.TypedDict):
class General(typing.TypedDict):
check_for_new_version: bool
- disable_cr: bool
- use_short_metadata_names: bool
prompt_on_save: bool
@@ -253,7 +257,7 @@ SettngsDict = typing.TypedDict(
"Issue Identifier": Issue_Identifier,
"Filename Parsing": Filename_Parsing,
"Sources": Sources,
- "Comic Book Lover": Comic_Book_Lover,
+ "Metadata Options": Metadata_Options,
"File Rename": File_Rename,
"Auto-Tag": Auto_Tag,
"General": General,
diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py
index ffc7101..3534384 100644
--- a/comictaggerlib/main.py
+++ b/comictaggerlib/main.py
@@ -252,7 +252,7 @@ class App:
# config already loaded
error = None
- if self.config[0].General__disable_cr:
+ if self.config[0].Metadata_Options__disable_cr:
if "cr" in comicapi.comicarchive.metadata_styles:
del comicapi.comicarchive.metadata_styles["cr"]
diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py
index 7c8d136..5cd9bf7 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -30,7 +30,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets, uic
import comictaggerlib.ui.talkeruigenerator
from comicapi import utils
from comicapi.archivers.archiver import Archiver
-from comicapi.genericmetadata import md_test
+# TODO OverlayMode changed to merge
+from comicapi.genericmetadata import OverlayMode, md_test
from comictaggerlib import ctsettings
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.ctsettings.plugin import group_for_plugin
@@ -194,6 +195,10 @@ class SettingsWindow(QtWidgets.QDialog):
)
self.cbFilenameParser.clear()
self.cbFilenameParser.addItems(utils.Parser)
+ # TODO check
+ for mode in OverlayMode:
+ self.cbxOverlayReadStyle.addItem(mode.name.capitalize().replace("_", " "), mode.value)
+ self.cbxOverlaySource.addItem(mode.name.capitalize().replace("_", " "), mode.value)
self.connect_signals()
self.settings_to_form()
self.rename_test()
@@ -399,7 +404,6 @@ class SettingsWindow(QtWidgets.QDialog):
self.tePublisherFilter.setPlainText("\n".join(self.config[0].Issue_Identifier__publisher_filter))
self.cbxCheckForNewVersion.setChecked(self.config[0].General__check_for_new_version)
- self.cbxShortMetadataNames.setChecked(self.config[0].General__use_short_metadata_names)
self.cbFilenameParser.setCurrentText(self.config[0].Filename_Parsing__filename_parser)
self.cbxRemoveC2C.setChecked(self.config[0].Filename_Parsing__remove_c2c)
@@ -417,17 +421,25 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxExactMatches.setChecked(self.config[0].Issue_Identifier__exact_series_matches_first)
self.cbxClearFormBeforePopulating.setChecked(self.config[0].Issue_Identifier__clear_metadata)
- self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].Comic_Book_Lover__assume_lone_credit_is_primary)
- self.cbxCopyCharactersToTags.setChecked(self.config[0].Comic_Book_Lover__copy_characters_to_tags)
- self.cbxCopyTeamsToTags.setChecked(self.config[0].Comic_Book_Lover__copy_teams_to_tags)
- self.cbxCopyLocationsToTags.setChecked(self.config[0].Comic_Book_Lover__copy_locations_to_tags)
- self.cbxCopyStoryArcsToTags.setChecked(self.config[0].Comic_Book_Lover__copy_storyarcs_to_tags)
- self.cbxCopyNotesToComments.setChecked(self.config[0].Comic_Book_Lover__copy_notes_to_comments)
- self.cbxCopyWebLinkToComments.setChecked(self.config[0].Comic_Book_Lover__copy_weblink_to_comments)
- self.cbxApplyCBLTransformOnCVIMport.setChecked(self.config[0].Comic_Book_Lover__apply_transform_on_import)
+ self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].Metadata_Options__cbl_assume_lone_credit_is_primary)
+ self.cbxCopyCharactersToTags.setChecked(self.config[0].Metadata_Options__cbl_copy_characters_to_tags)
+ self.cbxCopyTeamsToTags.setChecked(self.config[0].Metadata_Options__cbl_copy_teams_to_tags)
+ self.cbxCopyLocationsToTags.setChecked(self.config[0].Metadata_Options__cbl_copy_locations_to_tags)
+ self.cbxCopyStoryArcsToTags.setChecked(self.config[0].Metadata_Options__cbl_copy_storyarcs_to_tags)
+ self.cbxCopyNotesToComments.setChecked(self.config[0].Metadata_Options__cbl_copy_notes_to_comments)
+ self.cbxCopyWebLinkToComments.setChecked(self.config[0].Metadata_Options__cbl_copy_weblink_to_comments)
+ self.cbxApplyCBLTransformOnCVIMport.setChecked(self.config[0].Metadata_Options__cbl_apply_transform_on_import)
self.cbxApplyCBLTransformOnBatchOperation.setChecked(
- self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation
+ self.config[0].Metadata_Options__cbl_apply_transform_on_bulk_operation
)
+ self.cbxOverlayReadStyle.setCurrentIndex(
+ self.cbxOverlayReadStyle.findData(self.config[0].Metadata_Options__read_style_overlay.value)
+ )
+ self.cbxOverlaySource.setCurrentIndex(
+ self.cbxOverlaySource.findData(self.config[0].Metadata_Options__source_overlay.value)
+ )
+ self.cbxShortMetadataNames.setChecked(self.config[0].Metadata_Options__use_short_metadata_names)
+ self.cbxDisableCR.setChecked(self.config[0].Metadata_Options__disable_cr)
self.leRenameTemplate.setText(self.config[0].File_Rename__template)
self.leIssueNumPadding.setText(str(self.config[0].File_Rename__issue_number_padding))
@@ -544,18 +556,29 @@ class SettingsWindow(QtWidgets.QDialog):
self.config[0].Issue_Identifier__exact_series_matches_first = self.cbxExactMatches.isChecked()
self.config[0].Issue_Identifier__clear_metadata = self.cbxClearFormBeforePopulating.isChecked()
- self.config[0].Comic_Book_Lover__assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
- self.config[0].Comic_Book_Lover__copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
- self.config[0].Comic_Book_Lover__copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
- self.config[0].Comic_Book_Lover__copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
- self.config[0].Comic_Book_Lover__copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
- self.config[0].Comic_Book_Lover__copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
- self.config[0].Comic_Book_Lover__copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
- self.config[0].Comic_Book_Lover__apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
- self.config.values.Comic_Book_Lover__apply_transform_on_bulk_operation = (
+ self.config[0].Metadata_Options__cbl_assume_lone_credit_is_primary = (
+ self.cbxAssumeLoneCreditIsPrimary.isChecked()
+ )
+ self.config[0].Metadata_Options__cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
+ self.config[0].Metadata_Options__cbl_copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
+ self.config[0].Metadata_Options__cbl_copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
+ self.config[0].Metadata_Options__cbl_copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
+ self.config[0].Metadata_Options__cbl_copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
+ self.config[0].Metadata_Options__cbl_copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
+ self.config[0].Metadata_Options__cbl_apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
+ self.config.values.Metadata_Options__cbl_apply_transform_on_bulk_operation = (
self.cbxApplyCBLTransformOnBatchOperation.isChecked()
)
+ self.config[0].Metadata_Options__read_style_overlay = OverlayMode[self.cbxOverlayReadStyle.currentData()]
+ self.config[0].Metadata_Options__source_overlay = OverlayMode[self.cbxOverlaySource.currentData()]
+ self.config[0].Metadata_Options__disable_cr = self.cbxDisableCR.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()
+
self.config[0].File_Rename__template = str(self.leRenameTemplate.text())
self.config[0].File_Rename__issue_number_padding = int(self.leIssueNumPadding.text())
self.config[0].File_Rename__use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py
index 790eba4..7651552 100644
--- a/comictaggerlib/taggerwindow.py
+++ b/comictaggerlib/taggerwindow.py
@@ -1431,7 +1431,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.cbLoadDataStyle.clear()
# Add the entries to the tag style combobox
for style in metadata_styles.values():
- if self.config[0].General__use_short_metadata_names:
+ if self.config[0].Metadata_Options__use_short_metadata_names:
self.cbSaveDataStyle.addItem(style.short_name.upper(), style.short_name)
self.cbLoadDataStyle.addItem(style.short_name.upper(), style.short_name)
else:
@@ -1701,7 +1701,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
- if style == "cbi" and self.config[0].Comic_Book_Lover__apply_transform_on_bulk_operation:
+ if style == "cbi" and self.config[0].Metadata_Options__cbl_apply_transform_on_bulk_operation:
md = CBLTransformer(md, self.config[0]).apply()
if ca.write_metadata(md, style):
diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui
index d5a4af6..7423156 100644
--- a/comictaggerlib/ui/settingswindow.ui
+++ b/comictaggerlib/ui/settingswindow.ui
@@ -7,7 +7,7 @@
0
0
703
- 574
+ 597
@@ -28,7 +28,7 @@
- 0
+ 4
@@ -48,43 +48,7 @@
- -
-
-
- Use the short name for the metadata styles (CBI, CR, etc.)
-
-
- Use "short" names for metadata styles
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Default Settings
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Clear Cache
-
-
-
- -
+
-
@@ -103,7 +67,7 @@
- -
+
-
@@ -122,13 +86,39 @@
- -
+
-
Prompts the user to confirm saving tags
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Default Settings
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Clear Cache
+
+
+
@@ -427,19 +417,124 @@
-
+
- Metadata Types
+ Metadata Options
- -
-
-
- Apply CBL Transforms on Batch Copy Operations to CBL Tags
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+ 0
+
+
+
+ General
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Use the short name for the metadata styles (CBI, CR, etc.)
+
+
+ Use "short" names for metadata styles
+
+
+
+ -
+
+
+ Useful if you use the CIX metadata type
+
+
+ Disable ComicRack Metadata Type
+
+
+
+
- -
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 80
+
+
+
+ CBL Options
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+ Apply CBL Transforms on ComicVine Import
+
+
+
+ -
+
+
+ Apply CBL Transforms on Batch Copy Operations to CBL Tags
+
+
+
+
+
+
+ -
@@ -450,99 +545,347 @@
CBL Transforms
-
-
-
- 11
- 21
- 251
- 206
-
+
+
+ 6
-
-
-
-
-
- Copy Locations to Generic Tags
-
-
-
- -
-
-
- Assume Lone Credit Is Primary
-
-
-
- -
-
-
- Copy Characters to Generic Tags
-
-
-
- -
-
-
- Copy Teams to Generic Tags
-
-
-
- -
-
-
- Copy Notes to Comments
-
-
-
- -
-
-
- Copy Web Link to Comments
-
-
-
- -
-
-
- Copy Story Arcs to Generic Tags
-
-
-
-
-
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+ -
+
+
+ Copy Notes to Comments
+
+
+
+ -
+
+
+ Copy Web Link to Comments
+
+
+
+ -
+
+
+ Copy Locations to Generic Tags
+
+
+
+ -
+
+
+ Assume Lone Credit Is Primary
+
+
+
+ -
+
+
+ Copy Story Arcs to Generic Tags
+
+
+
+ -
+
+
+ Copy Characters to Generic Tags
+
+
+
+ -
+
+
+ Copy Teams to Generic Tags
+
+
+
+
- -
+
-
+
+
+
+ 0
+ 165
+
+
+
+ Overlay
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
-
+
+
+ The operation to perform when overlaying source data (Comic Vine, Metron, GCD, etc.)
+
+
+ Data Source
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 6
+
+
+
+ -
+
+
+ The operation to perform when overlaying read styles (ComicRack, ComicBookInfo, etc.)
+
+
+ Read Style
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+ 6
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ The operation to perform when overlaying source data (Comic Vine, Metron, GCD, etc.)
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 200
+
+
+
+ QTabWidget::North
+
+
+ 0
+
+
+ true
+
+
+ false
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Overlay
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 1
+ 1
+
+
+
+ false
+
+
+ Read Style: Overlays all the none empty values of the read style metadata on top of the current metadata (i.e. file name parsing, other read style(s)).
+Data Source: Overlays all the none empty values from the data source (e.g. Comic Vine) on top of the current metadata (i.e. file name parsing, read style(s)).
+
+Example:
+(Series=batman, Issue=1, Tags=[batman,joker,robin])
++ (Series=Batman, Publisher=DC Comics, Tags=[mystery,action])
+= (Series=Batman, Issue=1, Publisher=DC Comics, Tags=[mystery,action])
+
+
+ Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
+
+
+
+
+ Add Missing
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Read Style: Adds any metadata that is present in the read style but is missing in the current metadata (i.e. file name parsing, other read style(s)).
+Data Source: Add any metadata that is present in the data source (e.g. Comic Vine) but is missing in the current metadata (i.e. file name parsing, read style(s)).
+
+Example:
+(Series=batman, Issue=1, Tags=[batman,joker,robin])
++ (Series=Superman, Issue=10, Publisher=DC Comics, Tags=[superman,lois lane,lex luthor])
+= (Series=batman, Issue=1, Publisher=DC Comics, Tags=[batman,joker,robin])
+
+
+ Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
+
+
+
+
+ Combine
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Read Style: Same as Overlay except lists (tags, characters, genres, credits, etc.) are combined with the current metadata (i.e. file name parsing, other read style(s)).
+Data Source: Same as Overlay except lists (tags, characters, genres, credits, etc.) are combined with the current metadata (i.e. file name parsing, read style(s)).
+
+Example:
+(Series=batman, Issue=1, Tags=[batman,joker,robin])
++ (Series=Batman, Publisher=DC Comics, Tags=[mystery,action])
+= (Series=Batman, Issue=1, Publisher=DC Comics, Tags=[batman,joker,robin,mystery,action])
+
+
+ Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ The operation to perform when overlaying read styles (ComicRack, ComicBookInfo, etc.)
+
+
+
+
+
+
+ -
Qt::Vertical
-
- QSizePolicy::Fixed
-
20
- 10
+ 40
- -
-
-
- Apply CBL Transforms on ComicVine Import
-
-
-
- -
-
-
- Disable ComicRack Metadata Type (useful if you use the CIX metadata type)
-
-
-
diff --git a/tests/genericmetadata_test.py b/tests/genericmetadata_test.py
index 38602ad..cbf32ff 100644
--- a/tests/genericmetadata_test.py
+++ b/tests/genericmetadata_test.py
@@ -5,6 +5,7 @@ import textwrap
import pytest
import comicapi.genericmetadata
+from comicapi.genericmetadata import OverlayMode, parse_url
from testing.comicdata import credits, metadata
@@ -24,6 +25,107 @@ def test_metadata_overlay(md: comicapi.genericmetadata.GenericMetadata, replaced
assert md == expected
+def test_metadata_overlay_add_missing():
+ md = comicapi.genericmetadata.GenericMetadata(series="test", issue="1", title="test", genres={"test", "test2"})
+ add = comicapi.genericmetadata.GenericMetadata(
+ series="test2", issue="2", title="test2", genres={"test3", "test4"}, issue_count=5
+ )
+ expected = comicapi.genericmetadata.GenericMetadata(
+ series="test", issue="1", title="test", genres={"test", "test2"}, issue_count=5
+ )
+ md.overlay(add, OverlayMode.add_missing)
+
+ assert md == expected
+
+
+def test_metadata_overlay_combine():
+ md = comicapi.genericmetadata.GenericMetadata(
+ series="test",
+ issue="1",
+ title="test",
+ genres={"test", "test2"},
+ story_arcs=["arc1"],
+ characters={"Bob", "fred"},
+ web_links=[parse_url("https://my.comics.here.com")],
+ )
+ combine_new = comicapi.genericmetadata.GenericMetadata(
+ series="test2",
+ title="test2",
+ genres={"test2", "test3", "test4"},
+ story_arcs=["arc1", "arc2"],
+ characters={"bob", "fred"},
+ )
+ expected = comicapi.genericmetadata.GenericMetadata(
+ series="test2",
+ issue="1",
+ title="test2",
+ genres={"test", "test2", "test3", "test4"},
+ story_arcs=["arc1", "arc2"],
+ characters={"bob", "fred"},
+ web_links=[parse_url("https://my.comics.here.com")],
+ )
+ md.overlay(combine_new, OverlayMode.combine)
+
+ assert md == expected
+
+
+def test_assign_dedupe_set():
+ md_cur = comicapi.genericmetadata.GenericMetadata(characters={"Macintosh", "Søren Kierkegaard", "Barry"})
+ md_new = comicapi.genericmetadata.GenericMetadata(characters={"MacIntosh", "Soren Kierkegaard"})
+ # Expect a failure to normalise with NFKD and 'ø'
+ expected = comicapi.genericmetadata.GenericMetadata(
+ characters={"MacIntosh", "Soren Kierkegaard", "Søren Kierkegaard", "Barry"}
+ )
+
+ md_cur.overlay(md_new, OverlayMode.combine)
+
+ assert md_cur == expected
+
+
+def test_assign_dedupe_list():
+ md_cur = comicapi.genericmetadata.GenericMetadata(story_arcs=["arc 1", "arc2", "arc 3"])
+ md_new = comicapi.genericmetadata.GenericMetadata(story_arcs=["Arc 1", "Arc2"])
+ expected = comicapi.genericmetadata.GenericMetadata(story_arcs=["Arc 1", "Arc2", "arc 3"])
+
+ md_cur.overlay(md_new, OverlayMode.combine)
+
+ assert md_cur == expected
+
+
+def test_assign_credits_overlay():
+ md = comicapi.genericmetadata.GenericMetadata()
+ md.add_credit(person="test", role="writer", primary=False)
+ md.add_credit(person="test", role="artist", primary=True)
+
+ md_new = comicapi.genericmetadata.GenericMetadata()
+ md_new.add_credit(person="", role="writer")
+ md_new.add_credit(person="test2", role="inker")
+
+ expected = comicapi.genericmetadata.GenericMetadata()
+ expected.add_credit(person="test", role="writer", primary=False)
+ expected.add_credit(person="test", role="artist", primary=True)
+ expected.add_credit(person="test2", role="inker")
+
+ assert md.assign_credits_overlay(md.credits, md_new.credits) == expected.credits
+
+
+def test_assign_credits_add_missing():
+ md = comicapi.genericmetadata.GenericMetadata()
+ md.add_credit(person="test", role="writer", primary=False)
+ md.add_credit(person="test", role="artist", primary=True)
+
+ md_new = comicapi.genericmetadata.GenericMetadata()
+ md_new.add_credit(person="Bob", role="writer")
+ md_new.add_credit(person="test", role="artist", primary=True)
+
+ expected = comicapi.genericmetadata.GenericMetadata()
+ expected.add_credit(person="Bob", role="writer")
+ expected.add_credit(person="test", role="artist", primary=True)
+ expected.add_credit(person="test", role="writer", primary=False)
+
+ assert md.assign_credits_add_missing(md.credits, md_new.credits) == expected.credits
+
+
def test_add_credit():
md = comicapi.genericmetadata.GenericMetadata()