lordwelch rewrite
This commit is contained in:
parent
b761763c4c
commit
3d443e0908
@ -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
|
||||
|
128
comicapi/merge.py
Normal file
128
comicapi/merge.py
Normal file
@ -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,
|
||||
},
|
||||
)
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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()
|
||||
|
@ -755,7 +755,7 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="plainText">
|
||||
<string>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)).
|
||||
<string>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:
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="addTextEdit">
|
||||
<property name="plainText">
|
||||
<string>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)).
|
||||
<string>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:
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="combineTextEdit">
|
||||
<property name="plainText">
|
||||
<string>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.
|
||||
<string>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])
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user