diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py
index cf8c4cc..9aec972 100644
--- a/comicapi/genericmetadata.py
+++ b/comicapi/genericmetadata.py
@@ -24,14 +24,12 @@ 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
+from typing import TYPE_CHECKING, Any, TypedDict, Union, overload
from typing_extensions import NamedTuple, Required
-from comicapi import utils
+from comicapi import merge, utils
from comicapi._url import Url, parse_url
from comicapi.utils import norm_fold
@@ -41,50 +39,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
-class __remove(Enum):
- REMOVE = auto()
-
-
-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
+REMOVE = object()
class PageType:
@@ -117,10 +72,7 @@ class ImageMetadata(TypedDict, total=False):
width: str
-class Credit(TypedDict):
- person: str
- role: str
- primary: bool
+Credit = merge.Credit
@dataclasses.dataclass
@@ -145,12 +97,6 @@ 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")
@@ -261,93 +207,19 @@ class GenericMetadata:
new_md.__post_init__()
return new_md
- def credit_dedupe(self, cur: list[Credit], new: list[Credit]) -> list[Credit]:
- if len(new) == 0:
- return cur
- if len(cur) == 0:
- return new
-
- # Create dict for deduplication
- new_dict: dict[str, Credit] = {norm_fold(f"{n['person']}_{n['role']}"): n for n in new}
- cur_dict: dict[str, Credit] = {norm_fold(f"{c['person']}_{c['role']}"): c for c in cur}
-
- # Any duplicates use the 'new' value
- cur_dict.update(new_dict)
- return list(cur_dict.values())
-
- 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:
+ def overlay(self, new_md: GenericMetadata, mode: merge.Mode = merge.Mode.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,
- }
+ merge_function = merge.function[mode]
def assign(cur: Any, new: Any) -> Any:
if new is REMOVE:
if isinstance(cur, (list, set)):
cur.clear()
return cur
- else:
- return None
+ return None
- assign_func = assign_funcs.get(mode, self.assign_overlay)
- return assign_func(cur, new)
+ return merge_function(cur, new)
if not new_md.is_empty:
self.is_empty = False
@@ -390,13 +262,11 @@ class GenericMetadata:
self.scan_info = assign(self.scan_info, new_md.scan_info)
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)
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)
+ [self.add_credit(c) for c in assign(self.credits, new_md.credits)]
self.price = assign(self.price, new_md.price)
self.is_version_of = assign(self.is_version_of, new_md.is_version_of)
@@ -406,27 +276,8 @@ class GenericMetadata:
self._cover_image = assign(self._cover_image, new_md._cover_image)
self._alternate_images = assign(self._alternate_images, new_md._alternate_images)
- 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:
- return []
-
- 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)
+ self.pages = assign(self.pages, new_md.pages)
+ self.page_count = assign(self.page_count, new_md.page_count)
def apply_default_page_list(self, page_list: Sequence[str]) -> None:
# generate a default page list, with the first page marked as the cover
@@ -471,21 +322,35 @@ class GenericMetadata:
return coverlist
- def add_credit(self, person: str, role: str, primary: bool = False) -> None:
- if person == "":
+ @overload
+ def add_credit(self, person: Credit) -> None: ...
+
+ @overload
+ def add_credit(self, person: str, role: str, primary: bool = False) -> None: ...
+
+ def add_credit(self, person: str | Credit, role: str | None = None, primary: bool = False) -> None:
+
+ credit: Credit
+ if isinstance(person, Credit):
+ credit = person
+ else:
+ assert role is not None
+ credit = Credit(person=person, role=role, primary=primary)
+
+ if credit.role is None:
+ raise TypeError("GenericMetadata.add_credit takes either a Credit object or a person name and role")
+ if credit.person == "":
return
- credit = Credit(person=person, role=role, primary=primary)
-
- person = norm_fold(person)
- role = norm_fold(role)
+ person = norm_fold(credit.person)
+ role = norm_fold(credit.role)
# look to see if it's not already there...
found = False
for c in self.credits:
- if norm_fold(c["person"]) == person and norm_fold(c["role"]) == role:
+ if norm_fold(c.person) == person and norm_fold(c.role) == role:
# no need to add it. just adjust the "primary" flag as needed
- c["primary"] = primary
+ c.primary = c.primary or primary
found = True
break
@@ -495,12 +360,10 @@ class GenericMetadata:
def get_primary_credit(self, role: str) -> str:
primary = ""
for credit in self.credits:
- if "role" not in credit or "person" not in credit:
- continue
- if (primary == "" and credit["role"].casefold() == role.casefold()) or (
- credit["role"].casefold() == role.casefold() and "primary" in credit and credit["primary"]
+ if (primary == "" and credit.role.casefold() == role.casefold()) or (
+ credit.role.casefold() == role.casefold() and credit.primary
):
- primary = credit["person"]
+ primary = credit.person
return primary
def __str__(self) -> str:
@@ -559,9 +422,9 @@ class GenericMetadata:
for c in self.credits:
primary = ""
- if "primary" in c and c["primary"]:
+ if c.primary:
primary = " [P]"
- add_string("credit", c["role"] + ": " + c["person"] + primary)
+ add_string("credit", c.role + ": " + c.person + primary)
# find the longest field name
flen = 0
diff --git a/comicapi/merge.py b/comicapi/merge.py
new file mode 100644
index 0000000..16e773d
--- /dev/null
+++ b/comicapi/merge.py
@@ -0,0 +1,128 @@
+from __future__ import annotations
+
+import dataclasses
+import sys
+from collections import defaultdict
+from collections.abc import Collection
+from enum import Enum, auto
+from typing import Any
+
+from comicapi.utils import norm_fold
+
+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
+
+
+@dataclasses.dataclass
+class Credit:
+ person: str = ""
+ role: str = ""
+ primary: bool = False
+
+
+class Mode(StrEnum):
+ OVERLAY = auto()
+ ADD_MISSING = auto()
+ COMBINE = auto()
+
+
+def merge_lists(old: Collection[Any], new: Collection[Any]) -> list[Any] | set[Any]:
+ """Dedupes normalised (NFKD), casefolded values using 'new' values on collisions"""
+ if len(new) == 0:
+ return old if isinstance(old, set) else list(old)
+ if len(old) == 0:
+ return new if isinstance(new, set) else list(new)
+
+ # Create dict to preserve case
+ new_dict = {norm_fold(str(n)): n for n in new}
+ old_dict = {norm_fold(str(c)): c for c in old}
+
+ old_dict.update(new_dict)
+
+ if isinstance(old, set):
+ return set(old_dict.values())
+
+ return list(old_dict.values())
+
+
+def combine(old: Any, new: Any) -> Any:
+ """combine - Same as `overlay` except lists are merged"""
+ if new is None:
+ return old
+
+ if (
+ not (isinstance(new, str) or isinstance(old, str))
+ and isinstance(new, Collection)
+ and isinstance(old, Collection)
+ ):
+ return merge_lists(old, new)
+ if isinstance(new, str) and len(new) == 0:
+ return old
+ return new
+
+
+def overlay(old: Any, new: Any) -> Any:
+ """overlay - When the `new` object is not empty, replace `old` with `new`."""
+ if new is None:
+ return old
+ if (
+ not (isinstance(new, str) or isinstance(old, str))
+ and isinstance(new, Collection)
+ and isinstance(old, Collection)
+ ):
+ if isinstance(new, list) and len(new) > 0 and isinstance(new[0], Credit):
+ return merge_lists(old, new)
+ if len(new) == 0:
+ return old
+
+ return new
+
+
+def add_missing(old: Any, new: Any) -> Any:
+ """add_missing - When the `old` object is empty, replace `old` with `new`"""
+ return overlay(new, old)
+
+
+function = defaultdict(
+ lambda: overlay,
+ {
+ Mode.OVERLAY: overlay,
+ Mode.ADD_MISSING: add_missing,
+ Mode.COMBINE: combine,
+ },
+)
diff --git a/comicapi/metadata/comet.py b/comicapi/metadata/comet.py
index a521784..aa58a12 100644
--- a/comicapi/metadata/comet.py
+++ b/comicapi/metadata/comet.py
@@ -199,26 +199,26 @@ class CoMet(Metadata):
# loop thru credits, and build a list for each role that CoMet supports
for credit in metadata.credits:
- if credit["role"].casefold() in set(GenericMetadata.writer_synonyms):
- ET.SubElement(root, "writer").text = str(credit["person"])
+ if credit.role.casefold() in set(GenericMetadata.writer_synonyms):
+ ET.SubElement(root, "writer").text = str(credit.person)
- if credit["role"].casefold() in set(GenericMetadata.penciller_synonyms):
- ET.SubElement(root, "penciller").text = str(credit["person"])
+ if credit.role.casefold() in set(GenericMetadata.penciller_synonyms):
+ ET.SubElement(root, "penciller").text = str(credit.person)
- if credit["role"].casefold() in set(GenericMetadata.inker_synonyms):
- ET.SubElement(root, "inker").text = str(credit["person"])
+ if credit.role.casefold() in set(GenericMetadata.inker_synonyms):
+ ET.SubElement(root, "inker").text = str(credit.person)
- if credit["role"].casefold() in set(GenericMetadata.colorist_synonyms):
- ET.SubElement(root, "colorist").text = str(credit["person"])
+ if credit.role.casefold() in set(GenericMetadata.colorist_synonyms):
+ ET.SubElement(root, "colorist").text = str(credit.person)
- if credit["role"].casefold() in set(GenericMetadata.letterer_synonyms):
- ET.SubElement(root, "letterer").text = str(credit["person"])
+ if credit.role.casefold() in set(GenericMetadata.letterer_synonyms):
+ ET.SubElement(root, "letterer").text = str(credit.person)
- if credit["role"].casefold() in set(GenericMetadata.cover_synonyms):
- ET.SubElement(root, "coverDesigner").text = str(credit["person"])
+ if credit.role.casefold() in set(GenericMetadata.cover_synonyms):
+ ET.SubElement(root, "coverDesigner").text = str(credit.person)
- if credit["role"].casefold() in set(GenericMetadata.editor_synonyms):
- ET.SubElement(root, "editor").text = str(credit["person"])
+ if credit.role.casefold() in set(GenericMetadata.editor_synonyms):
+ ET.SubElement(root, "editor").text = str(credit.person)
ET.indent(root)
diff --git a/comicapi/metadata/comicbookinfo.py b/comicapi/metadata/comicbookinfo.py
index 3c49d82..9e62273 100644
--- a/comicapi/metadata/comicbookinfo.py
+++ b/comicapi/metadata/comicbookinfo.py
@@ -47,6 +47,12 @@ _CBILiteralType = Literal[
]
+class credit(TypedDict):
+ person: str
+ role: str
+ primary: bool
+
+
class _ComicBookInfoJson(TypedDict, total=False):
series: str
title: str
@@ -61,7 +67,7 @@ class _ComicBookInfoJson(TypedDict, total=False):
genre: str
language: str
country: str
- credits: list[Credit]
+ credits: list[credit]
tags: list[str]
comments: str
@@ -217,7 +223,7 @@ class ComicBookInfo(Metadata):
assign("language", utils.xlate(utils.get_language_from_iso(metadata.language)))
assign("country", utils.xlate(metadata.country))
assign("rating", utils.xlate_int(metadata.critical_rating))
- assign("credits", metadata.credits)
+ assign("credits", [credit(person=c.person, role=c.role, primary=c.primary) for c in metadata.credits])
assign("tags", list(metadata.tags))
return cbi_container
diff --git a/comicapi/metadata/comicrack.py b/comicapi/metadata/comicrack.py
index 1b02826..b6a7b78 100644
--- a/comicapi/metadata/comicrack.py
+++ b/comicapi/metadata/comicrack.py
@@ -187,26 +187,26 @@ class ComicRack(Metadata):
# first, loop thru credits, and build a list for each role that CIX
# supports
for credit in metadata.credits:
- if credit["role"].casefold() in set(GenericMetadata.writer_synonyms):
- credit_writer_list.append(credit["person"].replace(",", ""))
+ if credit.role.casefold() in set(GenericMetadata.writer_synonyms):
+ credit_writer_list.append(credit.person.replace(",", ""))
- if credit["role"].casefold() in set(GenericMetadata.penciller_synonyms):
- credit_penciller_list.append(credit["person"].replace(",", ""))
+ if credit.role.casefold() in set(GenericMetadata.penciller_synonyms):
+ credit_penciller_list.append(credit.person.replace(",", ""))
- if credit["role"].casefold() in set(GenericMetadata.inker_synonyms):
- credit_inker_list.append(credit["person"].replace(",", ""))
+ if credit.role.casefold() in set(GenericMetadata.inker_synonyms):
+ credit_inker_list.append(credit.person.replace(",", ""))
- if credit["role"].casefold() in set(GenericMetadata.colorist_synonyms):
- credit_colorist_list.append(credit["person"].replace(",", ""))
+ if credit.role.casefold() in set(GenericMetadata.colorist_synonyms):
+ credit_colorist_list.append(credit.person.replace(",", ""))
- if credit["role"].casefold() in set(GenericMetadata.letterer_synonyms):
- credit_letterer_list.append(credit["person"].replace(",", ""))
+ if credit.role.casefold() in set(GenericMetadata.letterer_synonyms):
+ credit_letterer_list.append(credit.person.replace(",", ""))
- if credit["role"].casefold() in set(GenericMetadata.cover_synonyms):
- credit_cover_list.append(credit["person"].replace(",", ""))
+ if credit.role.casefold() in set(GenericMetadata.cover_synonyms):
+ credit_cover_list.append(credit.person.replace(",", ""))
- if credit["role"].casefold() in set(GenericMetadata.editor_synonyms):
- credit_editor_list.append(credit["person"].replace(",", ""))
+ if credit.role.casefold() in set(GenericMetadata.editor_synonyms):
+ credit_editor_list.append(credit.person.replace(",", ""))
assign("Series", md.series)
assign("Number", md.issue)
diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py
index d195273..3b38232 100644
--- a/comictaggerlib/cbltransformer.py
+++ b/comictaggerlib/cbltransformer.py
@@ -36,14 +36,14 @@ class CBLTransformer:
lone_credit: Credit | None = None
count = 0
for c in self.metadata.credits:
- if c["role"].casefold() in role_list:
+ if c.role.casefold() in role_list:
count += 1
lone_credit = c
if count > 1:
lone_credit = None
break
if lone_credit is not None:
- lone_credit["primary"] = True
+ lone_credit.primary = True
return lone_credit, count
# need to loop three times, once for 'writer', 'artist', and then
@@ -53,8 +53,8 @@ class CBLTransformer:
if c is None and count == 0:
c, count = set_lone_primary(["penciler", "penciller"])
if c is not None:
- c["primary"] = False
- self.metadata.add_credit(c["person"], "Artist", True)
+ c.primary = False
+ self.metadata.add_credit(c.person, "Artist", True)
if self.config.Metadata_Options__cbl_copy_characters_to_tags:
self.metadata.tags.update(x for x in self.metadata.characters)
diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py
index 6e7aa2c..b449780 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -26,10 +26,10 @@ import sys
from collections.abc import Collection
from typing import Any, TextIO
-from comicapi import utils
+from comicapi import merge, utils
from comicapi.comicarchive import ComicArchive
from comicapi.comicarchive import metadata_styles as md_styles
-from comicapi.genericmetadata import GenericMetadata, OverlayMode
+from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.cbltransformer import CBLTransformer
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
@@ -259,7 +259,7 @@ class CLI:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
# finally, use explicit stuff (always 'overlay' mode)
- md.overlay(self.config.Runtime_Options__metadata, mode=OverlayMode.overlay)
+ md.overlay(self.config.Runtime_Options__metadata, mode=merge.Mode.OVERLAY)
return md
diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py
index 8d8a04c..02a4747 100644
--- a/comictaggerlib/ctsettings/commandline.py
+++ b/comictaggerlib/ctsettings/commandline.py
@@ -25,9 +25,9 @@ import subprocess
import settngs
-from comicapi import utils
+from comicapi import merge, utils
from comicapi.comicarchive import metadata_styles
-from comicapi.genericmetadata import GenericMetadata, OverlayMode
+from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import ctversion
from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns
from comictaggerlib.ctsettings.types import (
@@ -179,13 +179,13 @@ def register_runtime(parser: settngs.Manager) -> None:
)
parser.add_setting(
"--read-style-overlay",
- type=OverlayMode,
+ type=merge.Mode,
help="How to overlay new metadata on the current for enabled read styles (CR, CBL, etc.)",
file=False,
)
parser.add_setting(
"--source-overlay",
- type=OverlayMode,
+ type=merge.Mode,
help="How to overlay new metadata from a data source (CV, Metron, GCD, etc.) on to the current",
file=False,
)
diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py
index 83e534a..b10adf5 100644
--- a/comictaggerlib/ctsettings/file.py
+++ b/comictaggerlib/ctsettings/file.py
@@ -5,8 +5,7 @@ import uuid
import settngs
-from comicapi import utils
-from comicapi.genericmetadata import OverlayMode
+from comicapi import merge, utils
from comictaggerlib.ctsettings.settngs_namespace import SettngsNS as ct_ns
from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
@@ -27,8 +26,8 @@ def internal(parser: settngs.Manager) -> None:
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("load_data_overlay", default=OverlayMode.overlay, cmdline=False, type=OverlayMode)
- parser.add_setting("source_data_overlay", default=OverlayMode.overlay, cmdline=False, type=OverlayMode)
+ parser.add_setting("load_data_overlay", default=merge.Mode.OVERLAY, cmdline=False, type=merge.Mode)
+ parser.add_setting("source_data_overlay", default=merge.Mode.OVERLAY, cmdline=False, type=merge.Mode)
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)
diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py
index 6f85208..b03dc0b 100644
--- a/comictaggerlib/ctsettings/settngs_namespace.py
+++ b/comictaggerlib/ctsettings/settngs_namespace.py
@@ -5,11 +5,11 @@ import typing
import settngs
import comicapi.genericmetadata
+import comicapi.merge
import comicapi.utils
import comictaggerlib.ctsettings.types
import comictaggerlib.defaults
import comictaggerlib.resulttypes
-from comicapi.genericmetadata import OverlayMode
class SettngsNS(settngs.TypedNS):
@@ -37,8 +37,8 @@ class SettngsNS(settngs.TypedNS):
Runtime_Options__json: bool
Runtime_Options__type_modify: list[str]
Runtime_Options__type_read: list[str]
- Runtime_Options__read_style_overlay: OverlayMode
- Runtime_Options__source_overlay: OverlayMode
+ Runtime_Options__read_style_overlay: comicapi.merge.Mode
+ Runtime_Options__source_overlay: comicapi.merge.Mode
Runtime_Options__overwrite: bool
Runtime_Options__no_gui: bool
Runtime_Options__files: list[str]
@@ -46,8 +46,8 @@ class SettngsNS(settngs.TypedNS):
internal__install_id: str
internal__save_data_style: list[str]
internal__load_data_style: list[str]
- internal__load_data_overlay: OverlayMode
- internal__source_data_overlay: OverlayMode
+ internal__load_data_overlay: comicapi.merge.Mode
+ internal__source_data_overlay: comicapi.merge.Mode
internal__last_opened_folder: str
internal__window_width: int
internal__window_height: int
@@ -147,6 +147,8 @@ class Runtime_Options(typing.TypedDict):
quiet: bool
json: bool
type: list[str]
+ read_style_overlay: comicapi.merge.Mode
+ source_overlay: comicapi.merge.Mode
overwrite: bool
no_gui: bool
files: list[str]
@@ -156,6 +158,8 @@ class internal(typing.TypedDict):
install_id: str
save_data_style: list[str]
load_data_style: list[str]
+ load_data_overlay: comicapi.merge.Mode
+ source_data_overlay: comicapi.merge.Mode
last_opened_folder: str
window_width: int
window_height: int
diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py
index 0b60b15..0169ad3 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -28,10 +28,9 @@ import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
import comictaggerlib.ui.talkeruigenerator
-from comicapi import utils
+from comicapi import merge, utils
from comicapi.archivers.archiver import Archiver
-# TODO OverlayMode changed to merge
-from comicapi.genericmetadata import OverlayMode, md_test
+from comicapi.genericmetadata import md_test
from comictaggerlib import ctsettings
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.ctsettings.plugin import group_for_plugin
@@ -195,8 +194,7 @@ class SettingsWindow(QtWidgets.QDialog):
)
self.cbFilenameParser.clear()
self.cbFilenameParser.addItems(utils.Parser)
- # TODO check
- for mode in OverlayMode:
+ for mode in merge.Mode:
self.cbxOverlayReadStyle.addItem(mode.name.capitalize().replace("_", " "), mode.value)
self.cbxOverlaySource.addItem(mode.name.capitalize().replace("_", " "), mode.value)
self.connect_signals()
@@ -570,8 +568,8 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxApplyCBLTransformOnBatchOperation.isChecked()
)
- self.config[0].internal__load_data_overlay = OverlayMode[self.cbxOverlayReadStyle.currentData()]
- self.config[0].internal__source_data_overlay = OverlayMode[self.cbxOverlaySource.currentData()]
+ self.config[0].internal__load_data_overlay = merge.Mode[self.cbxOverlayReadStyle.currentData()]
+ self.config[0].internal__source_data_overlay = merge.Mode[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():
diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py
index a69044a..108e539 100644
--- a/comictaggerlib/taggerwindow.py
+++ b/comictaggerlib/taggerwindow.py
@@ -939,12 +939,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
for row, credit in enumerate(md.credits):
# if the role-person pair already exists, just skip adding it to the list
- if self.is_dupe_credit(credit["role"].title(), credit["person"]):
+ if self.is_dupe_credit(credit.role.title(), credit.person):
continue
- self.add_new_credit_entry(
- row, credit["role"].title(), credit["person"], (credit["primary"] if "primary" in credit else False)
- )
+ self.add_new_credit_entry(row, credit.role.title(), credit.person, credit.primary)
self.twCredits.setSortingEnabled(True)
self.update_metadata_credit_colors()
diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui
index ca5d2a5..a3af0f7 100644
--- a/comictaggerlib/ui/settingswindow.ui
+++ b/comictaggerlib/ui/settingswindow.ui
@@ -755,7 +755,7 @@
false
- Overlays all the none empty values of the read style/data source (e.g. Comic Vine) metadata on top of the current metadata (i.e. file name parsing, other read style(s)).
+ Overlays all the non-empty values of the new metadata (e.g. Comic Vine) on top of the current metadata.
Example:
(Series=batman, Issue=1, Tags=[batman,joker,robin])
@@ -792,7 +792,7 @@ Example:
-
- Adds any metadata that is present in the read style/data source (e.g. Comic Vine) but is missing in the current metadata (i.e. file name parsing, other read style(s)).
+ Adds any metadata that is present in the new metadata (e.g. Comic Vine) but is missing in the current metadata.
Example:
(Series=batman, Issue=1, Tags=[batman,joker,robin])
@@ -829,7 +829,7 @@ Example:
-
- Combine lists (tags, characters, genres, credits, etc.) replacing duplicates with the "new" data and all other non-empty values are replaced by the "new" values.
+ Same as Overlay except lists (tags, characters, genres, credits, etc.) are combined, replacing duplicates with the "new" data and all other non-empty values are replaced by the "new" values.
Example:
(Series=batman, Issue=1, Tags=[batman,joker,robin])
diff --git a/testing/comicdata.py b/testing/comicdata.py
index fb50eaa..40602dd 100644
--- a/testing/comicdata.py
+++ b/testing/comicdata.py
@@ -55,23 +55,86 @@ select_details = [
# Used to test GenericMetadata.overlay
metadata = [
(
+ comicapi.genericmetadata.md_test.copy(),
comicapi.genericmetadata.GenericMetadata(series="test", issue="2", title="never"),
comicapi.genericmetadata.md_test.replace(series="test", issue="2", title="never"),
),
(
+ comicapi.genericmetadata.md_test.copy(),
comicapi.genericmetadata.GenericMetadata(),
comicapi.genericmetadata.md_test.copy(),
),
+ (
+ comicapi.genericmetadata.GenericMetadata(
+ credits=[
+ comicapi.genericmetadata.Credit(person="test", role="writer", primary=False),
+ comicapi.genericmetadata.Credit(person="test", role="artist", primary=True),
+ ],
+ ),
+ comicapi.genericmetadata.GenericMetadata(
+ credits=[
+ comicapi.genericmetadata.Credit(person="", role="writer", primary=False),
+ comicapi.genericmetadata.Credit(person="test2", role="inker", primary=False),
+ ]
+ ),
+ comicapi.genericmetadata.GenericMetadata(
+ credits=[
+ comicapi.genericmetadata.Credit(person="test", role="writer", primary=False),
+ comicapi.genericmetadata.Credit(person="test", role="artist", primary=True),
+ comicapi.genericmetadata.Credit(person="test2", role="inker", primary=False),
+ ]
+ ),
+ ),
]
metadata_add = [
(
- comicapi.genericmetadata.GenericMetadata(series="test", issue="1", title="test", genres={"test", "test2"}),
comicapi.genericmetadata.GenericMetadata(
- series="test2", issue="2", title="test2", genres={"test3", "test4"}, issue_count=5
+ series="test",
+ title="test",
+ issue="1",
+ volume_count=1,
+ page_count=3,
+ day=3,
+ genres={"test", "test2"},
+ story_arcs=["arc"],
+ characters=set(),
+ credits=[
+ comicapi.genericmetadata.Credit(person="test", role="writer", primary=False),
+ comicapi.genericmetadata.Credit(person="test", role="artist", primary=True),
+ ],
),
comicapi.genericmetadata.GenericMetadata(
- series="test", issue="1", title="test", genres={"test", "test2"}, issue_count=5
+ series="test2",
+ title="",
+ issue_count=2,
+ page_count=2,
+ day=0,
+ genres={"fake"},
+ characters={"bob", "fred"},
+ scan_info="nothing",
+ credits=[
+ comicapi.genericmetadata.Credit(person="Bob", role="writer", primary=False),
+ comicapi.genericmetadata.Credit(person="test", role="artist", primary=True),
+ ],
+ ),
+ comicapi.genericmetadata.GenericMetadata(
+ series="test",
+ title="test",
+ issue="1",
+ issue_count=2,
+ volume_count=1,
+ day=3,
+ page_count=3,
+ genres={"test", "test2"},
+ story_arcs=["arc"],
+ characters={"bob", "fred"},
+ scan_info="nothing",
+ credits=[
+ comicapi.genericmetadata.Credit(person="test", role="writer", primary=False),
+ comicapi.genericmetadata.Credit(person="test", role="artist", primary=True),
+ comicapi.genericmetadata.Credit(person="Bob", role="writer", primary=False),
+ ],
),
),
]
@@ -80,33 +143,42 @@ metadata_combine = [
(
comicapi.genericmetadata.GenericMetadata(
series="test",
- issue="1",
title="test",
+ issue="1",
+ volume_count=1,
+ page_count=3,
+ day=3,
genres={"test", "test2"},
- story_arcs=["arc1"],
- characters={"Bob", "fred"},
+ story_arcs=["arc"],
+ characters=set(),
web_links=[comicapi.genericmetadata.parse_url("https://my.comics.here.com")],
),
comicapi.genericmetadata.GenericMetadata(
series="test2",
- title="test2",
- genres={"test2", "test3", "test4"},
- story_arcs=["arc1", "arc2"],
+ title="",
+ issue_count=2,
+ page_count=2,
+ day=0,
+ genres={"fake"},
characters={"bob", "fred"},
+ scan_info="nothing",
+ web_links=[comicapi.genericmetadata.parse_url("https://my.comics.here.com")],
),
comicapi.genericmetadata.GenericMetadata(
series="test2",
+ title="test",
issue="1",
- title="test2",
- genres={"test", "test2", "test3", "test4"},
- story_arcs=["arc1", "arc2"],
+ issue_count=2,
+ volume_count=1,
+ day=0,
+ page_count=2,
+ genres={"fake", "test", "test2"},
+ story_arcs=["arc"],
characters={"bob", "fred"},
+ scan_info="nothing",
web_links=[comicapi.genericmetadata.parse_url("https://my.comics.here.com")],
),
),
-]
-
-metadata_dedupe_set = [
(
comicapi.genericmetadata.GenericMetadata(characters={"Macintosh", "Søren Kierkegaard", "Barry"}),
comicapi.genericmetadata.GenericMetadata(characters={"MacIntosh", "Soren Kierkegaard"}),
@@ -114,9 +186,6 @@ metadata_dedupe_set = [
characters={"MacIntosh", "Soren Kierkegaard", "Søren Kierkegaard", "Barry"}
),
),
-]
-
-metadata_dedupe_list = [
(
comicapi.genericmetadata.GenericMetadata(story_arcs=["arc 1", "arc2", "arc 3"]),
comicapi.genericmetadata.GenericMetadata(story_arcs=["Arc 1", "Arc2"]),
@@ -146,7 +215,10 @@ credits = [
(comicapi.genericmetadata.md_test, "writeR", "Dara Naraghi"),
(
comicapi.genericmetadata.md_test.replace(
- credits=[{"person": "Dara Naraghi", "role": "writer"}, {"person": "Dara Naraghi", "role": "writer"}]
+ credits=[
+ comicapi.genericmetadata.Credit(person="Dara Naraghi", role="writer"),
+ comicapi.genericmetadata.Credit(person="Dara Naraghi", role="writer"),
+ ]
),
"writeR",
"Dara Naraghi",
diff --git a/tests/genericmetadata_test.py b/tests/genericmetadata_test.py
index 1d64918..7b090ee 100644
--- a/tests/genericmetadata_test.py
+++ b/tests/genericmetadata_test.py
@@ -5,8 +5,8 @@ import textwrap
import pytest
import comicapi.genericmetadata
-import testing.comicdata as cd
-from comicapi.genericmetadata import OverlayMode
+import comicapi.merge
+import testing.comicdata
def test_apply_default_page_list(tmp_path):
@@ -18,71 +18,25 @@ def test_apply_default_page_list(tmp_path):
assert isinstance(md.pages[0]["image_index"], int)
-@pytest.mark.parametrize("replaced, expected", cd.metadata)
-def test_metadata_overlay(md: comicapi.genericmetadata.GenericMetadata, replaced, expected):
- md.overlay(replaced)
+@pytest.mark.parametrize("md, new, expected", testing.comicdata.metadata)
+def test_metadata_overlay(md, new, expected):
+ md.overlay(new, comicapi.merge.Mode.OVERLAY)
assert md == expected
-@pytest.mark.parametrize("md, new, expected", cd.metadata_add)
+@pytest.mark.parametrize("md, new, expected", testing.comicdata.metadata_add)
def test_metadata_overlay_add_missing(md, new, expected):
- md.overlay(new, OverlayMode.add_missing)
+ md.overlay(new, comicapi.merge.Mode.ADD_MISSING)
assert md == expected
-@pytest.mark.parametrize("md, new, expected", cd.metadata_combine)
+@pytest.mark.parametrize("md, new, expected", testing.comicdata.metadata_combine)
def test_metadata_overlay_combine(md, new, expected):
- md.overlay(new, OverlayMode.combine)
+ md.overlay(new, comicapi.merge.Mode.COMBINE)
assert md == expected
-@pytest.mark.parametrize("md, new, expected", cd.metadata_dedupe_set)
-def test_assign_dedupe_set(md, new, expected):
- md.overlay(new, OverlayMode.combine)
- assert md == expected
-
-
-@pytest.mark.parametrize("md, new, expected", cd.metadata_dedupe_list)
-def test_assign_dedupe_list(md, new, expected):
- md.overlay(new, OverlayMode.combine)
- assert md == 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()
@@ -98,7 +52,7 @@ def test_add_credit_primary():
assert md.credits == [comicapi.genericmetadata.Credit(person="test", role="writer", primary=True)]
-@pytest.mark.parametrize("md, role, expected", cd.credits)
+@pytest.mark.parametrize("md, role, expected", testing.comicdata.credits)
def test_get_primary_credit(md, role, expected):
assert md.get_primary_credit(role) == expected
diff --git a/tests/integration_test.py b/tests/integration_test.py
index 57ff9c1..bf5bf9f 100644
--- a/tests/integration_test.py
+++ b/tests/integration_test.py
@@ -54,11 +54,11 @@ def test_save(
# unrelated to comicvine need to be re-worked
md_saved.credits.insert(
1,
- {
- "person": "Esteve Polls",
- "primary": False,
- "role": "Writer",
- },
+ comicapi.genericmetadata.Credit(
+ person="Esteve Polls",
+ primary=False,
+ role="Writer",
+ ),
)
# Validate that we got the correct metadata back