Compare commits
10 Commits
00200334fb
...
54d733ef74
Author | SHA1 | Date | |
---|---|---|---|
|
54d733ef74 | ||
|
2c3a2566cc | ||
|
1b6307f9c2 | ||
|
548ad4a816 | ||
|
27f71833b3 | ||
|
6c07fab985 | ||
|
4151c0e113 | ||
|
3119d68ea2 | ||
|
f43f51aa2f | ||
|
19986b64d0 |
@ -10,16 +10,16 @@ repos:
|
||||
- id: name-tests-test
|
||||
- id: requirements-txt-fixer
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.2.0
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.1.1
|
||||
rev: v2.2.0
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args: [-i, --remove-all-unused-imports, --ignore-init-module-imports]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.2
|
||||
rev: v3.8.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
@ -38,7 +38,7 @@ repos:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-length, flake8-print]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.2.0
|
||||
rev: v1.4.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-setuptools, types-requests, settngs>=0.7.1]
|
||||
|
@ -19,7 +19,7 @@ class FolderArchiver(Archiver):
|
||||
|
||||
def get_comment(self) -> str:
|
||||
try:
|
||||
return self.read_file(self.comment_file_name).decode("utf-8")
|
||||
return (self.path / self.comment_file_name).read_text()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
@ -28,10 +28,12 @@ class FolderArchiver(Archiver):
|
||||
return self.write_file(self.comment_file_name, comment.encode("utf-8"))
|
||||
return True
|
||||
|
||||
def supports_comment(self) -> bool:
|
||||
return True
|
||||
|
||||
def read_file(self, archive_file: str) -> bytes:
|
||||
try:
|
||||
with open(self.path / archive_file, mode="rb") as f:
|
||||
data = f.read()
|
||||
data = (self.path / archive_file).read_bytes()
|
||||
except OSError as e:
|
||||
logger.error("Error reading folder archive [%s]: %s :: %s", e, self.path, archive_file)
|
||||
raise
|
||||
@ -89,6 +91,9 @@ class FolderArchiver(Archiver):
|
||||
else:
|
||||
return True
|
||||
|
||||
def is_writable(self) -> bool:
|
||||
return True
|
||||
|
||||
def name(self) -> str:
|
||||
return "Folder"
|
||||
|
||||
|
@ -20,7 +20,7 @@ import xml.etree.ElementTree as ET
|
||||
from typing import Any
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.genericmetadata import Date, GenericMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -64,7 +64,7 @@ class CoMet:
|
||||
assign("series", md.series)
|
||||
assign("issue", md.issue) # must be int??
|
||||
assign("volume", md.volume)
|
||||
assign("description", md.comments)
|
||||
assign("description", md.description)
|
||||
assign("publisher", md.publisher)
|
||||
assign("pages", md.page_count)
|
||||
assign("format", md.format)
|
||||
@ -75,21 +75,17 @@ class CoMet:
|
||||
assign("rights", md.rights)
|
||||
assign("identifier", md.identifier)
|
||||
assign("lastMark", md.last_mark)
|
||||
assign("genre", md.genre) # TODO repeatable
|
||||
assign("genre", ",".join(md.genres)) # TODO repeatable
|
||||
|
||||
if md.characters is not None:
|
||||
char_list = [c.strip() for c in md.characters.split(",")]
|
||||
char_list = [c.strip() for c in md.characters]
|
||||
for c in char_list:
|
||||
assign("character", c)
|
||||
|
||||
if md.manga is not None and md.manga == "YesAndRightToLeft":
|
||||
assign("readingDirection", "rtl")
|
||||
|
||||
if md.year is not None:
|
||||
date_str = f"{md.year:04}"
|
||||
if md.month is not None:
|
||||
date_str += f"-{md.month:02}"
|
||||
assign("date", date_str)
|
||||
assign("date", f"{md.cover_date.year or ''}-{md.cover_date.month or ''}".strip("-"))
|
||||
|
||||
assign("coverImage", md.cover_image)
|
||||
|
||||
@ -142,7 +138,7 @@ class CoMet:
|
||||
md.title = utils.xlate(get("title"))
|
||||
md.issue = utils.xlate(get("issue"))
|
||||
md.volume = utils.xlate_int(get("volume"))
|
||||
md.comments = utils.xlate(get("description"))
|
||||
md.description = utils.xlate(get("description"))
|
||||
md.publisher = utils.xlate(get("publisher"))
|
||||
md.language = utils.xlate(get("language"))
|
||||
md.format = utils.xlate(get("format"))
|
||||
@ -153,9 +149,8 @@ class CoMet:
|
||||
md.rights = utils.xlate(get("rights"))
|
||||
md.identifier = utils.xlate(get("identifier"))
|
||||
md.last_mark = utils.xlate(get("lastMark"))
|
||||
md.genre = utils.xlate(get("genre")) # TODO - repeatable field
|
||||
|
||||
_, md.month, md.year = utils.parse_date_str(utils.xlate(get("date")))
|
||||
md.cover_date = Date.parse_date(utils.xlate(get("date")))
|
||||
|
||||
md.cover_image = utils.xlate(get("coverImage"))
|
||||
|
||||
@ -163,12 +158,15 @@ class CoMet:
|
||||
if reading_direction is not None and reading_direction == "rtl":
|
||||
md.manga = "YesAndRightToLeft"
|
||||
|
||||
# loop for genre tags
|
||||
for n in root:
|
||||
if n.tag == "genre":
|
||||
md.genres.append((n.text or "").strip())
|
||||
|
||||
# loop for character tags
|
||||
char_list = []
|
||||
for n in root:
|
||||
if n.tag == "character":
|
||||
char_list.append((n.text or "").strip())
|
||||
md.characters = ", ".join(char_list)
|
||||
md.characters.append((n.text or "").strip())
|
||||
|
||||
# Now extract the credit info
|
||||
for n in root:
|
||||
|
@ -134,7 +134,7 @@ class ComicArchive:
|
||||
return True
|
||||
|
||||
def is_writable_for_style(self, data_style: int) -> bool:
|
||||
return not (data_style == MetaDataStyle.CBI and not self.archiver.supports_comment)
|
||||
return not (data_style == MetaDataStyle.CBI and not self.archiver.supports_comment())
|
||||
|
||||
def is_zip(self) -> bool:
|
||||
return self.archiver.name() == "ZIP"
|
||||
@ -563,7 +563,8 @@ class ComicArchive:
|
||||
metadata.title = utils.xlate(p.filename_info["title"])
|
||||
metadata.volume = utils.xlate_int(p.filename_info["volume"])
|
||||
metadata.volume_count = utils.xlate_int(p.filename_info["volume_count"])
|
||||
metadata.year = utils.xlate_int(p.filename_info["year"])
|
||||
|
||||
metadata.cover_date.year = utils.xlate_int(p.filename_info["year"])
|
||||
|
||||
metadata.scan_info = utils.xlate(p.filename_info["remainder"])
|
||||
metadata.format = "FCBD" if p.filename_info["fcbd"] else None
|
||||
@ -580,7 +581,7 @@ class ComicArchive:
|
||||
if fnp.volume:
|
||||
metadata.volume = utils.xlate_int(fnp.volume)
|
||||
if fnp.year:
|
||||
metadata.year = utils.xlate_int(fnp.year)
|
||||
metadata.cover_date.year = utils.xlate_int(fnp.year)
|
||||
if fnp.issue_count:
|
||||
metadata.issue_count = utils.xlate_int(fnp.issue_count)
|
||||
if fnp.remainder:
|
||||
|
@ -21,7 +21,7 @@ from datetime import datetime
|
||||
from typing import Any, Literal, TypedDict
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.genericmetadata import Date, GenericMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -85,11 +85,12 @@ class ComicBookInfo:
|
||||
metadata.title = utils.xlate(cbi["title"])
|
||||
metadata.issue = utils.xlate(cbi["issue"])
|
||||
metadata.publisher = utils.xlate(cbi["publisher"])
|
||||
metadata.month = utils.xlate_int(cbi["publicationMonth"])
|
||||
metadata.year = utils.xlate_int(cbi["publicationYear"])
|
||||
|
||||
metadata.cover_date = Date(utils.xlate_int(cbi["publicationYear"]), utils.xlate_int(cbi["publicationMonth"]))
|
||||
|
||||
metadata.issue_count = utils.xlate_int(cbi["numberOfIssues"])
|
||||
metadata.comments = utils.xlate(cbi["comments"])
|
||||
metadata.genre = utils.xlate(cbi["genre"])
|
||||
metadata.description = utils.xlate(cbi["comments"])
|
||||
metadata.genres = utils.split(cbi["genre"], ",")
|
||||
metadata.volume = utils.xlate_int(cbi["volume"])
|
||||
metadata.volume_count = utils.xlate_int(cbi["numberOfVolumes"])
|
||||
metadata.language = utils.xlate(cbi["language"])
|
||||
@ -104,11 +105,7 @@ class ComicBookInfo:
|
||||
)
|
||||
for x in cbi["credits"]
|
||||
]
|
||||
metadata.tags = set(cbi["tags"]) if cbi["tags"] is not None else set()
|
||||
|
||||
# make sure credits and tags are at least empty lists and not None
|
||||
if metadata.credits is None:
|
||||
metadata.credits = []
|
||||
metadata.tags.update(cbi["tags"] if cbi["tags"] is not None else set())
|
||||
|
||||
# need the language string to be ISO
|
||||
if metadata.language:
|
||||
@ -152,11 +149,11 @@ class ComicBookInfo:
|
||||
assign("title", utils.xlate(metadata.title))
|
||||
assign("issue", utils.xlate(metadata.issue))
|
||||
assign("publisher", utils.xlate(metadata.publisher))
|
||||
assign("publicationMonth", utils.xlate_int(metadata.month))
|
||||
assign("publicationYear", utils.xlate_int(metadata.year))
|
||||
assign("publicationMonth", utils.xlate_int(metadata.cover_date.month))
|
||||
assign("publicationYear", utils.xlate_int(metadata.cover_date.year))
|
||||
assign("numberOfIssues", utils.xlate_int(metadata.issue_count))
|
||||
assign("comments", utils.xlate(metadata.comments))
|
||||
assign("genre", utils.xlate(metadata.genre))
|
||||
assign("comments", utils.xlate(metadata.description))
|
||||
assign("genre", utils.xlate(",".join(metadata.genres)))
|
||||
assign("volume", utils.xlate_int(metadata.volume))
|
||||
assign("numberOfVolumes", utils.xlate_int(metadata.volume_count))
|
||||
assign("language", utils.xlate(utils.get_language_from_iso(metadata.language)))
|
||||
|
@ -21,7 +21,7 @@ from typing import Any, cast
|
||||
from xml.etree.ElementTree import ElementTree
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata, ImageMetadata
|
||||
from comicapi.genericmetadata import Date, GenericMetadata, ImageMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -69,12 +69,19 @@ class ComicInfoXml:
|
||||
# helper func
|
||||
|
||||
def assign(cix_entry: str, md_entry: Any) -> None:
|
||||
if md_entry is not None and md_entry:
|
||||
if md_entry:
|
||||
text = ""
|
||||
if isinstance(md_entry, str):
|
||||
text = md_entry
|
||||
elif isinstance(md_entry, list):
|
||||
text = ",".join(md_entry)
|
||||
else:
|
||||
text = str(md_entry)
|
||||
et_entry = root.find(cix_entry)
|
||||
if et_entry is not None:
|
||||
et_entry.text = str(md_entry)
|
||||
et_entry.text = text
|
||||
else:
|
||||
ET.SubElement(root, cix_entry).text = str(md_entry)
|
||||
ET.SubElement(root, cix_entry).text = text
|
||||
else:
|
||||
et_entry = root.find(cix_entry)
|
||||
if et_entry is not None:
|
||||
@ -87,14 +94,15 @@ class ComicInfoXml:
|
||||
assign("Volume", md.volume)
|
||||
assign("AlternateSeries", md.alternate_series)
|
||||
assign("AlternateNumber", md.alternate_number)
|
||||
assign("StoryArc", md.story_arc)
|
||||
assign("SeriesGroup", md.series_group)
|
||||
assign("StoryArc", md.story_arcs)
|
||||
assign("SeriesGroup", md.series_groups)
|
||||
assign("AlternateCount", md.alternate_count)
|
||||
assign("Summary", md.comments)
|
||||
assign("Summary", md.description)
|
||||
assign("Notes", md.notes)
|
||||
assign("Year", md.year)
|
||||
assign("Month", md.month)
|
||||
assign("Day", md.day)
|
||||
|
||||
assign("Year", md.cover_date.year)
|
||||
assign("Month", md.cover_date.month)
|
||||
assign("Day", md.cover_date.day)
|
||||
|
||||
# need to specially process the credits, since they are structured
|
||||
# differently than CIX
|
||||
@ -141,7 +149,7 @@ class ComicInfoXml:
|
||||
|
||||
assign("Publisher", md.publisher)
|
||||
assign("Imprint", md.imprint)
|
||||
assign("Genre", md.genre)
|
||||
assign("Genre", md.genres)
|
||||
assign("Web", md.web_link)
|
||||
assign("PageCount", md.page_count)
|
||||
assign("LanguageISO", md.language)
|
||||
@ -194,25 +202,25 @@ class ComicInfoXml:
|
||||
md.alternate_series = utils.xlate(get("AlternateSeries"))
|
||||
md.alternate_number = utils.xlate(get("AlternateNumber"))
|
||||
md.alternate_count = utils.xlate_int(get("AlternateCount"))
|
||||
md.comments = utils.xlate(get("Summary"))
|
||||
md.description = utils.xlate(get("Summary"))
|
||||
md.notes = utils.xlate(get("Notes"))
|
||||
md.year = utils.xlate_int(get("Year"))
|
||||
md.month = utils.xlate_int(get("Month"))
|
||||
md.day = utils.xlate_int(get("Day"))
|
||||
|
||||
md.cover_date = Date(utils.xlate_int(get("Year")), utils.xlate_int(get("Month")), utils.xlate_int(get("Day")))
|
||||
|
||||
md.publisher = utils.xlate(get("Publisher"))
|
||||
md.imprint = utils.xlate(get("Imprint"))
|
||||
md.genre = utils.xlate(get("Genre"))
|
||||
md.genres = utils.split(get("Genre"), ",")
|
||||
md.web_link = utils.xlate(get("Web"))
|
||||
md.language = utils.xlate(get("LanguageISO"))
|
||||
md.format = utils.xlate(get("Format"))
|
||||
md.manga = utils.xlate(get("Manga"))
|
||||
md.characters = utils.xlate(get("Characters"))
|
||||
md.teams = utils.xlate(get("Teams"))
|
||||
md.locations = utils.xlate(get("Locations"))
|
||||
md.characters = utils.split(get("Characters"), ",")
|
||||
md.teams = utils.split(get("Teams"), ",")
|
||||
md.locations = utils.split(get("Locations"), ",")
|
||||
md.page_count = utils.xlate_int(get("PageCount"))
|
||||
md.scan_info = utils.xlate(get("ScanInformation"))
|
||||
md.story_arc = utils.xlate(get("StoryArc"))
|
||||
md.series_group = utils.xlate(get("SeriesGroup"))
|
||||
md.story_arcs = utils.split(get("StoryArc"), ",")
|
||||
md.series_groups = utils.split(get("SeriesGroup"), ",")
|
||||
md.maturity_rating = utils.xlate(get("AgeRating"))
|
||||
md.critical_rating = utils.xlate_float(get("CommunityRating"))
|
||||
|
||||
@ -232,12 +240,12 @@ class ComicInfoXml:
|
||||
]
|
||||
):
|
||||
if n.text is not None:
|
||||
for name in n.text.split(","):
|
||||
for name in utils.split(n.text, ","):
|
||||
md.add_credit(name.strip(), n.tag)
|
||||
|
||||
if n.tag == "CoverArtist":
|
||||
if n.text is not None:
|
||||
for name in n.text.split(","):
|
||||
for name in utils.split(n.text, ","):
|
||||
md.add_credit(name.strip(), "Cover")
|
||||
|
||||
# parse page data now
|
||||
|
@ -253,9 +253,9 @@ class FileNameParser:
|
||||
remainder = ""
|
||||
|
||||
if "--" in filename:
|
||||
remainder = filename.split("--", 1)[1]
|
||||
remainder = "--".join(filename.split("--", 1)[1:])
|
||||
elif "__" in filename:
|
||||
remainder = filename.split("__", 1)[1]
|
||||
remainder = "__".join(filename.split("__", 1)[1:])
|
||||
elif issue_end != 0:
|
||||
remainder = filename[issue_end:]
|
||||
|
||||
|
@ -20,11 +20,14 @@ possible, however lossy it might be
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import copy
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from typing_extensions import NamedTuple
|
||||
|
||||
from comicapi import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -60,12 +63,91 @@ class ImageMetadata(TypedDict, total=False):
|
||||
ImageWidth: str
|
||||
|
||||
|
||||
class CreditMetadata(TypedDict):
|
||||
class Credit(TypedDict):
|
||||
person: str
|
||||
role: str
|
||||
primary: bool
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ComicSeries:
|
||||
id: str
|
||||
name: str
|
||||
aliases: list[str]
|
||||
count_of_issues: int | None
|
||||
count_of_volumes: int | None
|
||||
description: str
|
||||
image_url: str
|
||||
publisher: str
|
||||
start_year: int | None
|
||||
genres: list[str]
|
||||
format: str | None
|
||||
|
||||
def copy(self) -> ComicSeries:
|
||||
return copy.deepcopy(self)
|
||||
|
||||
|
||||
class TagOrigin(NamedTuple):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Date:
|
||||
year: int | None = None
|
||||
month: int | None = None
|
||||
day: int | None = None
|
||||
month_name: str = dataclasses.field(init=False, repr=False, default="")
|
||||
month_abbr: str = dataclasses.field(init=False, repr=False, default="")
|
||||
|
||||
@classmethod
|
||||
def parse_date(cls, date_str: str | None) -> Date:
|
||||
day = None
|
||||
month = None
|
||||
year = None
|
||||
if date_str:
|
||||
parts = date_str.split("-")
|
||||
year = utils.xlate_int(parts[0])
|
||||
if len(parts) > 1:
|
||||
month = utils.xlate_int(parts[1])
|
||||
if len(parts) > 2:
|
||||
day = utils.xlate_int(parts[2])
|
||||
return Date(year, month, day)
|
||||
|
||||
def __str__(self) -> str:
|
||||
date_str = ""
|
||||
if self.year is not None:
|
||||
date_str = f"{self.year:04}"
|
||||
if self.month is not None:
|
||||
date_str += f"-{self.month:02}"
|
||||
if self.day is not None:
|
||||
date_str += f"-{self.day:02}"
|
||||
return date_str
|
||||
|
||||
def copy(self) -> Date:
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def replace(self, /, **kwargs: Any) -> Date:
|
||||
tmp = self.copy()
|
||||
tmp.__dict__.update(kwargs)
|
||||
return tmp
|
||||
|
||||
# We hijack the month property in order to update the month_name and month_abbr attributes
|
||||
@property # type: ignore[no-redef]
|
||||
def month(self) -> int: # noqa: F811
|
||||
return self.__dict__["month"]
|
||||
|
||||
@month.setter
|
||||
def month(self, month: int | None):
|
||||
if month is None:
|
||||
self.__dict__["month_name"] = ""
|
||||
self.__dict__["month_abbr"] = ""
|
||||
else:
|
||||
self.__dict__["month_name"] = calendar.month_name[month]
|
||||
self.__dict__["month_abbr"] = calendar.month_abbr[month]
|
||||
self.__dict__["month"] = month
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GenericMetadata:
|
||||
writer_synonyms = ["writer", "plotter", "scripter"]
|
||||
@ -77,21 +159,23 @@ class GenericMetadata:
|
||||
editor_synonyms = ["editor"]
|
||||
|
||||
is_empty: bool = True
|
||||
tag_origin: str | None = None
|
||||
tag_origin: TagOrigin | None = None
|
||||
issue_id: str | None = None
|
||||
series_id: str | None = None
|
||||
|
||||
series: str | None = None
|
||||
series_aliases: list[str] = dataclasses.field(default_factory=list)
|
||||
issue: str | None = None
|
||||
title: str | None = None
|
||||
title_aliases: list[str] = dataclasses.field(default_factory=list)
|
||||
publisher: str | None = None
|
||||
month: int | None = None
|
||||
year: int | None = None
|
||||
day: int | None = None
|
||||
cover_date: Date = Date(None, None, None)
|
||||
store_date: Date = Date(None, None, None)
|
||||
issue_count: int | None = None
|
||||
volume: int | None = None
|
||||
genre: str | None = None
|
||||
genres: list[str] = dataclasses.field(default_factory=list)
|
||||
language: str | None = None # 2 letter iso code
|
||||
comments: str | None = None # use same way as Summary in CIX
|
||||
description: str | None = None # use same way as Summary in CIX
|
||||
|
||||
volume_count: int | None = None
|
||||
critical_rating: float | None = None # rating in CBL; CommunityRating in CIX
|
||||
@ -109,15 +193,16 @@ class GenericMetadata:
|
||||
page_count: int | None = None
|
||||
maturity_rating: str | None = None
|
||||
|
||||
story_arc: str | None = None
|
||||
series_group: str | None = None
|
||||
story_arcs: list[str] = dataclasses.field(default_factory=list)
|
||||
series_groups: list[str] = dataclasses.field(default_factory=list)
|
||||
scan_info: str | None = None
|
||||
|
||||
characters: str | None = None
|
||||
teams: str | None = None
|
||||
locations: str | None = None
|
||||
characters: list[str] = dataclasses.field(default_factory=list)
|
||||
teams: list[str] = dataclasses.field(default_factory=list)
|
||||
locations: list[str] = dataclasses.field(default_factory=list)
|
||||
|
||||
credits: list[CreditMetadata] = dataclasses.field(default_factory=list)
|
||||
alternate_images: list[str] = dataclasses.field(default_factory=list)
|
||||
credits: list[Credit] = dataclasses.field(default_factory=list)
|
||||
tags: set[str] = dataclasses.field(default_factory=set)
|
||||
pages: list[ImageMetadata] = dataclasses.field(default_factory=list)
|
||||
|
||||
@ -127,7 +212,7 @@ class GenericMetadata:
|
||||
rights: str | None = None
|
||||
identifier: str | None = None
|
||||
last_mark: str | None = None
|
||||
cover_image: str | None = None
|
||||
cover_image: str | None = None # url to cover image
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for key, value in self.__dict__.items():
|
||||
@ -143,6 +228,25 @@ class GenericMetadata:
|
||||
tmp.__dict__.update(kwargs)
|
||||
return tmp
|
||||
|
||||
def _assign(self, cur: str, new: Any) -> None:
|
||||
if new is not None:
|
||||
if isinstance(new, str) and not new:
|
||||
setattr(self, cur, None)
|
||||
|
||||
elif isinstance(new, list) and len(new) == 0:
|
||||
pass
|
||||
|
||||
elif isinstance(new, Date):
|
||||
date = getattr(self, cur)
|
||||
if date is None:
|
||||
date = Date(None, None, None)
|
||||
GenericMetadata._assign(date, "day", new.day)
|
||||
GenericMetadata._assign(date, "month", new.month)
|
||||
GenericMetadata._assign(date, "year", new.year)
|
||||
|
||||
else:
|
||||
setattr(self, cur, new)
|
||||
|
||||
def overlay(self, new_md: GenericMetadata) -> None:
|
||||
"""Overlay a metadata object on this one
|
||||
|
||||
@ -150,53 +254,41 @@ class GenericMetadata:
|
||||
to this one.
|
||||
"""
|
||||
|
||||
def assign(cur: str, new: Any) -> None:
|
||||
if new is not None:
|
||||
if isinstance(new, str) and len(new) == 0:
|
||||
setattr(self, cur, None)
|
||||
else:
|
||||
setattr(self, cur, new)
|
||||
|
||||
if not new_md.is_empty:
|
||||
self.is_empty = False
|
||||
|
||||
assign("series", new_md.series)
|
||||
assign("issue", new_md.issue)
|
||||
assign("issue_count", new_md.issue_count)
|
||||
assign("title", new_md.title)
|
||||
assign("publisher", new_md.publisher)
|
||||
assign("day", new_md.day)
|
||||
assign("month", new_md.month)
|
||||
assign("year", new_md.year)
|
||||
assign("volume", new_md.volume)
|
||||
assign("volume_count", new_md.volume_count)
|
||||
assign("genre", new_md.genre)
|
||||
assign("language", new_md.language)
|
||||
assign("country", new_md.country)
|
||||
assign("critical_rating", new_md.critical_rating)
|
||||
assign("alternate_series", new_md.alternate_series)
|
||||
assign("alternate_number", new_md.alternate_number)
|
||||
assign("alternate_count", new_md.alternate_count)
|
||||
assign("imprint", new_md.imprint)
|
||||
assign("web_link", new_md.web_link)
|
||||
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("story_arc", new_md.story_arc)
|
||||
assign("series_group", new_md.series_group)
|
||||
assign("scan_info", new_md.scan_info)
|
||||
assign("characters", new_md.characters)
|
||||
assign("teams", new_md.teams)
|
||||
assign("locations", new_md.locations)
|
||||
assign("comments", new_md.comments)
|
||||
assign("notes", new_md.notes)
|
||||
self._assign("series", new_md.series)
|
||||
self._assign("series_id", new_md.series_id)
|
||||
self._assign("issue", new_md.issue)
|
||||
self._assign("issue_id", new_md.issue_id)
|
||||
self._assign("issue_count", new_md.issue_count)
|
||||
self._assign("title", new_md.title)
|
||||
self._assign("publisher", new_md.publisher)
|
||||
self._assign("cover_date", new_md.cover_date)
|
||||
self._assign("store_date", new_md.store_date)
|
||||
self._assign("volume", new_md.volume)
|
||||
self._assign("volume_count", new_md.volume_count)
|
||||
self._assign("language", new_md.language)
|
||||
self._assign("country", new_md.country)
|
||||
self._assign("critical_rating", new_md.critical_rating)
|
||||
self._assign("alternate_series", new_md.alternate_series)
|
||||
self._assign("alternate_number", new_md.alternate_number)
|
||||
self._assign("alternate_count", new_md.alternate_count)
|
||||
self._assign("imprint", new_md.imprint)
|
||||
self._assign("web_link", new_md.web_link)
|
||||
self._assign("format", new_md.format)
|
||||
self._assign("manga", new_md.manga)
|
||||
self._assign("black_and_white", new_md.black_and_white)
|
||||
self._assign("maturity_rating", new_md.maturity_rating)
|
||||
self._assign("scan_info", new_md.scan_info)
|
||||
self._assign("description", new_md.description)
|
||||
self._assign("notes", new_md.notes)
|
||||
|
||||
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)
|
||||
self._assign("price", new_md.price)
|
||||
self._assign("is_version_of", new_md.is_version_of)
|
||||
self._assign("rights", new_md.rights)
|
||||
self._assign("identifier", new_md.identifier)
|
||||
self._assign("last_mark", new_md.last_mark)
|
||||
|
||||
self.overlay_credits(new_md.credits)
|
||||
# TODO
|
||||
@ -206,13 +298,18 @@ class GenericMetadata:
|
||||
|
||||
# For now, go the easy route, where any overlay
|
||||
# value wipes out the whole list
|
||||
if len(new_md.tags) > 0:
|
||||
assign("tags", new_md.tags)
|
||||
self._assign("series_aliases", new_md.series_aliases)
|
||||
self._assign("title_aliases", new_md.title_aliases)
|
||||
self._assign("genres", new_md.genres)
|
||||
self._assign("story_arcs", new_md.story_arcs)
|
||||
self._assign("series_groups", new_md.series_groups)
|
||||
self._assign("characters", new_md.characters)
|
||||
self._assign("teams", new_md.teams)
|
||||
self._assign("locations", new_md.locations)
|
||||
self._assign("tags", new_md.tags)
|
||||
self._assign("pages", new_md.pages)
|
||||
|
||||
if len(new_md.pages) > 0:
|
||||
assign("pages", new_md.pages)
|
||||
|
||||
def overlay_credits(self, new_credits: list[CreditMetadata]) -> None:
|
||||
def overlay_credits(self, new_credits: list[Credit]) -> None:
|
||||
for c in new_credits:
|
||||
primary = bool("primary" in c and c["primary"])
|
||||
|
||||
@ -253,7 +350,7 @@ class GenericMetadata:
|
||||
return coverlist
|
||||
|
||||
def add_credit(self, person: str, role: str, primary: bool = False) -> None:
|
||||
credit = CreditMetadata(person=person, role=role, primary=primary)
|
||||
credit = Credit(person=person, role=role, primary=primary)
|
||||
|
||||
# look to see if it's not already there...
|
||||
found = False
|
||||
@ -373,19 +470,19 @@ class GenericMetadata:
|
||||
|
||||
md_test: GenericMetadata = GenericMetadata(
|
||||
is_empty=False,
|
||||
tag_origin=None,
|
||||
tag_origin=TagOrigin("comicvine", "Comic Vine"),
|
||||
series="Cory Doctorow's Futuristic Tales of the Here and Now",
|
||||
series_id="23437",
|
||||
issue="1",
|
||||
issue_id="140529",
|
||||
title="Anda's Game",
|
||||
publisher="IDW Publishing",
|
||||
month=10,
|
||||
year=2007,
|
||||
day=1,
|
||||
cover_date=Date(month=10, year=2007, day=1),
|
||||
issue_count=6,
|
||||
volume=1,
|
||||
genre="Sci-Fi",
|
||||
genres=["Sci-Fi"],
|
||||
language="en",
|
||||
comments=(
|
||||
description=(
|
||||
"For 12-year-old Anda, getting paid real money to kill the characters of players who were cheating"
|
||||
" in her favorite online computer game was a win-win situation. Until she found out who was paying her,"
|
||||
" and what those characters meant to the livelihood of children around the world."
|
||||
@ -404,19 +501,19 @@ md_test: GenericMetadata = GenericMetadata(
|
||||
black_and_white=None,
|
||||
page_count=24,
|
||||
maturity_rating="Everyone 10+",
|
||||
story_arc="Here and Now",
|
||||
series_group="Futuristic Tales",
|
||||
story_arcs=["Here and Now"],
|
||||
series_groups=["Futuristic Tales"],
|
||||
scan_info="(CC BY-NC-SA 3.0)",
|
||||
characters="Anda",
|
||||
teams="Fahrenheit",
|
||||
locations="lonely cottage ",
|
||||
characters=["Anda"],
|
||||
teams=["Fahrenheit"],
|
||||
locations=utils.split("lonely cottage ", ","),
|
||||
credits=[
|
||||
CreditMetadata(primary=False, person="Dara Naraghi", role="Writer"),
|
||||
CreditMetadata(primary=False, person="Esteve Polls", role="Penciller"),
|
||||
CreditMetadata(primary=False, person="Esteve Polls", role="Inker"),
|
||||
CreditMetadata(primary=False, person="Neil Uyetake", role="Letterer"),
|
||||
CreditMetadata(primary=False, person="Sam Kieth", role="Cover"),
|
||||
CreditMetadata(primary=False, person="Ted Adams", role="Editor"),
|
||||
Credit(primary=False, person="Dara Naraghi", role="Writer"),
|
||||
Credit(primary=False, person="Esteve Polls", role="Penciller"),
|
||||
Credit(primary=False, person="Esteve Polls", role="Inker"),
|
||||
Credit(primary=False, person="Neil Uyetake", role="Letterer"),
|
||||
Credit(primary=False, person="Sam Kieth", role="Cover"),
|
||||
Credit(primary=False, person="Ted Adams", role="Editor"),
|
||||
],
|
||||
tags=set(),
|
||||
pages=[
|
||||
|
@ -91,7 +91,7 @@ def get_recursive_filelist(pathlist: list[str]) -> list[str]:
|
||||
for root, _, files in os.walk(p):
|
||||
for f in files:
|
||||
filelist.append(os.path.join(root, f))
|
||||
else:
|
||||
elif os.path.exists(p):
|
||||
filelist.append(p)
|
||||
|
||||
return filelist
|
||||
@ -100,7 +100,7 @@ def get_recursive_filelist(pathlist: list[str]) -> list[str]:
|
||||
def add_to_path(dirname: str) -> None:
|
||||
if dirname:
|
||||
dirname = os.path.abspath(dirname)
|
||||
paths = [os.path.normpath(x) for x in os.environ["PATH"].split(os.pathsep)]
|
||||
paths = [os.path.normpath(x) for x in split(os.environ["PATH"], os.pathsep)]
|
||||
|
||||
if dirname not in paths:
|
||||
paths.insert(0, dirname)
|
||||
@ -136,7 +136,14 @@ def xlate(data: Any) -> str | None:
|
||||
if data is None or isinstance(data, str) and data.strip() == "":
|
||||
return None
|
||||
|
||||
return str(data)
|
||||
return str(data).strip()
|
||||
|
||||
|
||||
def split(s: str | None, c: str) -> list[str]:
|
||||
s = xlate(s)
|
||||
if s:
|
||||
return [x.strip() for x in s.strip().split(c) if x.strip()]
|
||||
return []
|
||||
|
||||
|
||||
def remove_articles(text: str) -> str:
|
||||
|
@ -119,8 +119,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for match in self.current_match_set.matches:
|
||||
for row, match in enumerate(self.current_match_set.matches):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match["series"]
|
||||
@ -160,8 +159,6 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
|
@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from comicapi.genericmetadata import CreditMetadata, GenericMetadata
|
||||
from comicapi.genericmetadata import Credit, GenericMetadata
|
||||
from comictaggerlib.ctsettings import ct_ns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -29,21 +29,10 @@ class CBLTransformer:
|
||||
self.config = config
|
||||
|
||||
def apply(self) -> GenericMetadata:
|
||||
# helper funcs
|
||||
def append_to_tags_if_unique(item: str) -> None:
|
||||
if item.casefold() not in (tag.casefold() for tag in self.metadata.tags):
|
||||
self.metadata.tags.add(item)
|
||||
|
||||
def add_string_list_to_tags(str_list: str | None) -> None:
|
||||
if str_list:
|
||||
items = [s.strip() for s in str_list.split(",")]
|
||||
for item in items:
|
||||
append_to_tags_if_unique(item)
|
||||
|
||||
if self.config.cbl_assume_lone_credit_is_primary:
|
||||
# helper
|
||||
def set_lone_primary(role_list: list[str]) -> tuple[CreditMetadata | None, int]:
|
||||
lone_credit: CreditMetadata | None = None
|
||||
def set_lone_primary(role_list: list[str]) -> tuple[Credit | None, int]:
|
||||
lone_credit: Credit | None = None
|
||||
count = 0
|
||||
for c in self.metadata.credits:
|
||||
if c["role"].casefold() in role_list:
|
||||
@ -67,33 +56,33 @@ class CBLTransformer:
|
||||
self.metadata.add_credit(c["person"], "Artist", True)
|
||||
|
||||
if self.config.cbl_copy_characters_to_tags:
|
||||
add_string_list_to_tags(self.metadata.characters)
|
||||
self.metadata.tags.update(x for x in self.metadata.characters)
|
||||
|
||||
if self.config.cbl_copy_teams_to_tags:
|
||||
add_string_list_to_tags(self.metadata.teams)
|
||||
self.metadata.tags.update(x for x in self.metadata.teams)
|
||||
|
||||
if self.config.cbl_copy_locations_to_tags:
|
||||
add_string_list_to_tags(self.metadata.locations)
|
||||
self.metadata.tags.update(x for x in self.metadata.locations)
|
||||
|
||||
if self.config.cbl_copy_storyarcs_to_tags:
|
||||
add_string_list_to_tags(self.metadata.story_arc)
|
||||
self.metadata.tags.update(x for x in self.metadata.story_arcs)
|
||||
|
||||
if self.config.cbl_copy_notes_to_comments:
|
||||
if self.metadata.notes is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
if self.metadata.description is None:
|
||||
self.metadata.description = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.notes
|
||||
self.metadata.description += "\n\n"
|
||||
if self.metadata.notes not in self.metadata.description:
|
||||
self.metadata.description += self.metadata.notes
|
||||
|
||||
if self.config.cbl_copy_weblink_to_comments:
|
||||
if self.metadata.web_link is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
if self.metadata.description is None:
|
||||
self.metadata.description = ""
|
||||
else:
|
||||
self.metadata.comments += "\n\n"
|
||||
if self.metadata.web_link not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.web_link
|
||||
self.metadata.description += "\n\n"
|
||||
if self.metadata.web_link not in self.metadata.description:
|
||||
self.metadata.description += self.metadata.web_link
|
||||
|
||||
return self.metadata
|
||||
|
@ -34,6 +34,7 @@ from comictaggerlib.graphics import graphics_path
|
||||
from comictaggerlib.issueidentifier import IssueIdentifier
|
||||
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
|
||||
from comictalker.comictalker import ComicTalker, TalkerError
|
||||
from comictalker.talker_utils import cleanup_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -89,8 +90,7 @@ class CLI:
|
||||
# sort match list by year
|
||||
match_set.matches.sort(key=lambda k: k["year"] or 0)
|
||||
|
||||
for counter, m in enumerate(match_set.matches):
|
||||
counter += 1
|
||||
for counter, m in enumerate(match_set.matches, 1):
|
||||
print(
|
||||
" {}. {} #{} [{}] ({}/{}) - {}".format(
|
||||
counter,
|
||||
@ -435,7 +435,12 @@ class CLI:
|
||||
f"Tagged with ComicTagger {ctversion.version} using info from {self.current_talker().name} on"
|
||||
f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {ct_md.issue_id}]"
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
md.overlay(
|
||||
ct_md.replace(
|
||||
notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"),
|
||||
description=cleanup_html(ct_md.description, self.config.talker_remove_html_tables),
|
||||
)
|
||||
)
|
||||
|
||||
if self.config.identifier_auto_imprint:
|
||||
md.fix_publisher()
|
||||
|
@ -124,6 +124,13 @@ def filename(parser: settngs.Manager) -> None:
|
||||
def talker(parser: settngs.Manager) -> None:
|
||||
# General settings for talkers
|
||||
parser.add_setting("--source", default="comicvine", help="Use a specified source by source ID")
|
||||
parser.add_setting(
|
||||
"--remove-html-tables",
|
||||
default=False,
|
||||
action=argparse.BooleanOptionalAction,
|
||||
display_name="Remove HTML tables",
|
||||
help="Removes html tables instead of converting them to text",
|
||||
)
|
||||
|
||||
|
||||
def cbl(parser: settngs.Manager) -> None:
|
||||
|
@ -77,6 +77,7 @@ class settngs_namespace(settngs.TypedNS):
|
||||
filename_remove_publisher: bool
|
||||
|
||||
talker_source: str
|
||||
talker_remove_html_tables: bool
|
||||
|
||||
cbl_assume_lone_credit_is_primary: bool
|
||||
cbl_copy_characters_to_tags: bool
|
||||
|
@ -5,6 +5,7 @@ import pathlib
|
||||
|
||||
from appdirs import AppDirs
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
|
||||
@ -67,7 +68,7 @@ def metadata_type_single(types: str) -> int:
|
||||
def metadata_type(types: str) -> list[int]:
|
||||
result = []
|
||||
types = types.casefold()
|
||||
for typ in types.split(","):
|
||||
for typ in utils.split(types, ","):
|
||||
typ = typ.strip()
|
||||
if typ not in MetaDataStyle.short_name:
|
||||
choices = ", ".join(MetaDataStyle.short_name)
|
||||
@ -93,7 +94,7 @@ def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
|
||||
|
||||
# First, replace escaped commas with with a unique token (to be changed back later)
|
||||
mdstr = mdstr.replace(escaped_comma, replacement_token)
|
||||
tmp_list = mdstr.split(",")
|
||||
tmp_list = utils.split(mdstr, ",")
|
||||
md_list = []
|
||||
for item in tmp_list:
|
||||
item = item.replace(replacement_token, ",")
|
||||
@ -104,11 +105,11 @@ def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
|
||||
for item in md_list:
|
||||
# Make sure to fix any escaped equal signs
|
||||
i = item.replace(escaped_equals, replacement_token)
|
||||
key, value = i.split("=")
|
||||
key, value = utils.split(i, "=")
|
||||
value = value.replace(replacement_token, "=").strip()
|
||||
key = key.strip()
|
||||
if key.casefold() == "credit":
|
||||
cred_attribs = value.split(":")
|
||||
cred_attribs = utils.split(value, ":")
|
||||
role = cred_attribs[0]
|
||||
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
|
||||
primary = len(cred_attribs) > 2
|
||||
|
@ -15,7 +15,6 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
@ -67,6 +66,8 @@ class MetadataFormatter(string.Formatter):
|
||||
return str(value).swapcase()
|
||||
if conversion == "t":
|
||||
return str(value).title()
|
||||
if conversion == "j":
|
||||
return ", ".join(list(value))
|
||||
return cast(str, super().convert_field(value, conversion))
|
||||
|
||||
def handle_replacements(self, string: str, replacements: list[Replacement]) -> str:
|
||||
@ -219,12 +220,8 @@ class FileRenamer:
|
||||
for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]:
|
||||
md_dict[role] = md.get_primary_credit(role)
|
||||
|
||||
if (isinstance(md.month, int) or isinstance(md.month, str) and md.month.isdigit()) and 0 < int(md.month) < 13:
|
||||
md_dict["month_name"] = calendar.month_name[int(md.month)]
|
||||
md_dict["month_abbr"] = calendar.month_abbr[int(md.month)]
|
||||
else:
|
||||
md_dict["month_name"] = ""
|
||||
md_dict["month_abbr"] = ""
|
||||
date = getattr(md, "cover_date")
|
||||
md_dict.update(vars(date))
|
||||
|
||||
new_basename = ""
|
||||
for component in pathlib.PureWindowsPath(template).parts:
|
||||
|
@ -63,6 +63,14 @@ try:
|
||||
qt_exception_hook = UncaughtHook()
|
||||
from comictaggerlib.taggerwindow import TaggerWindow
|
||||
|
||||
try:
|
||||
# needed here to initialize QWebEngine
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView # noqa: F401
|
||||
|
||||
qt_webengine_available = True
|
||||
except ImportError:
|
||||
qt_webengine_available = False
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
openFileRequest = QtCore.pyqtSignal(QtCore.QUrl, name="openfileRequest")
|
||||
|
||||
|
@ -221,8 +221,8 @@ class IssueIdentifier:
|
||||
search_keys = SearchKeys(
|
||||
series=self.additional_metadata.series,
|
||||
issue_number=self.additional_metadata.issue,
|
||||
year=self.additional_metadata.year,
|
||||
month=self.additional_metadata.month,
|
||||
year=self.additional_metadata.cover_date.year,
|
||||
month=self.additional_metadata.cover_date.month,
|
||||
issue_count=self.additional_metadata.issue_count,
|
||||
)
|
||||
return search_keys
|
||||
@ -257,8 +257,8 @@ class IssueIdentifier:
|
||||
search_keys = SearchKeys(
|
||||
series=working_md.series,
|
||||
issue_number=working_md.issue,
|
||||
year=working_md.year,
|
||||
month=working_md.month,
|
||||
year=working_md.cover_date.year,
|
||||
month=working_md.cover_date.month,
|
||||
issue_count=working_md.issue_count,
|
||||
)
|
||||
|
||||
@ -277,7 +277,6 @@ class IssueIdentifier:
|
||||
|
||||
def get_issue_cover_match_score(
|
||||
self,
|
||||
issue_id: str,
|
||||
primary_img_url: str,
|
||||
alt_urls: list[str],
|
||||
local_cover_hash_list: list[int],
|
||||
@ -472,8 +471,8 @@ class IssueIdentifier:
|
||||
# now re-associate the issues and series
|
||||
# is this really needed?
|
||||
for issue in issue_list:
|
||||
if issue.series.id in series_by_id:
|
||||
shortlist.append((series_by_id[issue.series.id], issue))
|
||||
if issue.series_id in series_by_id:
|
||||
shortlist.append((series_by_id[issue.series_id], issue))
|
||||
|
||||
if keys["year"] is None:
|
||||
self.log_msg(f"Found {len(shortlist)} series that have an issue #{keys['issue_number']}")
|
||||
@ -495,9 +494,6 @@ class IssueIdentifier:
|
||||
newline=False,
|
||||
)
|
||||
|
||||
# parse out the cover date
|
||||
_, month, year = utils.parse_date_str(issue.cover_date)
|
||||
|
||||
# Now check the cover match against the primary image
|
||||
hash_list = [cover_hash]
|
||||
if narrow_cover_hash is not None:
|
||||
@ -509,11 +505,11 @@ class IssueIdentifier:
|
||||
logger.info("Adding cropped cover to the hashlist")
|
||||
|
||||
try:
|
||||
image_url = issue.image_url
|
||||
alt_urls = issue.alt_image_urls
|
||||
image_url = issue.cover_image or ""
|
||||
alt_urls = issue.alternate_images
|
||||
|
||||
score_item = self.get_issue_cover_match_score(
|
||||
issue.id, image_url, alt_urls, hash_list, use_remote_alternates=False
|
||||
image_url, alt_urls, hash_list, use_remote_alternates=False
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Scoring series failed")
|
||||
@ -526,15 +522,15 @@ class IssueIdentifier:
|
||||
"issue_number": keys["issue_number"],
|
||||
"cv_issue_count": series.count_of_issues,
|
||||
"url_image_hash": score_item["hash"],
|
||||
"issue_title": issue.name,
|
||||
"issue_id": issue.id,
|
||||
"issue_title": issue.title or "",
|
||||
"issue_id": issue.issue_id or "",
|
||||
"series_id": series.id,
|
||||
"month": month,
|
||||
"year": year,
|
||||
"month": issue.cover_date.month,
|
||||
"year": issue.cover_date.year,
|
||||
"publisher": None,
|
||||
"image_url": image_url,
|
||||
"alt_image_urls": alt_urls,
|
||||
"description": issue.description,
|
||||
"description": issue.description or "",
|
||||
}
|
||||
if series.publisher is not None:
|
||||
match["publisher"] = series.publisher
|
||||
@ -595,7 +591,6 @@ class IssueIdentifier:
|
||||
self.log_msg(f"Examining alternate covers for ID: {m['series_id']} {m['series']} ...", newline=False)
|
||||
try:
|
||||
score_item = self.get_issue_cover_match_score(
|
||||
m["issue_id"],
|
||||
m["image_url"],
|
||||
m["alt_image_urls"],
|
||||
hash_list,
|
||||
|
@ -19,13 +19,13 @@ import logging
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ctsettings import ct_ns
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
from comictaggerlib.ui.qtutils import new_web_view, reduce_widget_font_size
|
||||
from comictalker.comictalker import ComicTalker, TalkerError
|
||||
from comictalker.resulttypes import ComicIssue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -58,6 +58,19 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
gridlayout.addWidget(self.coverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.teDescription: QtWidgets.QWidget
|
||||
webengine = new_web_view(self)
|
||||
if webengine:
|
||||
self.teDescription.hide()
|
||||
self.teDescription.deleteLater()
|
||||
# I don't know how to replace teDescription, this is the result of teDescription.height() once rendered
|
||||
webengine.resize(webengine.width(), 141)
|
||||
self.splitter.addWidget(webengine)
|
||||
self.teDescription = webengine
|
||||
logger.info("successfully loaded QWebEngineView")
|
||||
else:
|
||||
logger.info("failed to open QWebEngineView")
|
||||
|
||||
reduce_widget_font_size(self.twList)
|
||||
reduce_widget_font_size(self.teDescription, 1)
|
||||
|
||||
@ -74,7 +87,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.config = config
|
||||
self.talker = talker
|
||||
self.url_fetch_thread = None
|
||||
self.issue_list: list[ComicIssue] = []
|
||||
self.issue_list: list[GenericMetadata] = []
|
||||
|
||||
# Display talker logo and set url
|
||||
self.lblIssuesSourceName.setText(talker.attribution)
|
||||
@ -130,7 +143,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
||||
|
||||
try:
|
||||
self.issue_list = self.talker.fetch_issues_by_series(self.series_id)
|
||||
self.issue_list = self.talker.fetch_issues_in_series(self.series_id)
|
||||
except TalkerError as e:
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtWidgets.QMessageBox.critical(self, f"{e.source} {e.code_name} Error", f"{e}")
|
||||
@ -140,46 +153,36 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for record in self.issue_list:
|
||||
for row, issue in enumerate(self.issue_list):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record.issue_number
|
||||
item_text = issue.issue or ""
|
||||
item = IssueNumberTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, record.id)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, issue.issue_id)
|
||||
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = record.cover_date
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
# remove the day of "YYYY-MM-DD"
|
||||
parts = item_text.split("-")
|
||||
if len(parts) > 1:
|
||||
item_text = parts[0] + "-" + parts[1]
|
||||
item_text = ""
|
||||
if issue.cover_date.year is not None:
|
||||
item_text = f"{issue.cover_date.year:04}"
|
||||
if issue.cover_date.month is not None:
|
||||
item_text = f"{issue.cover_date.month:02}"
|
||||
|
||||
qtw_item = QtWidgets.QTableWidgetItem(item_text)
|
||||
qtw_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
qtw_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, qtw_item)
|
||||
|
||||
item_text = record.name
|
||||
if item_text is None:
|
||||
item_text = ""
|
||||
item_text = issue.title or ""
|
||||
qtw_item = QtWidgets.QTableWidgetItem(item_text)
|
||||
qtw_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
qtw_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, qtw_item)
|
||||
|
||||
if (
|
||||
IssueString(record.issue_number).as_string().casefold()
|
||||
== IssueString(self.issue_number).as_string().casefold()
|
||||
):
|
||||
self.initial_id = record.id
|
||||
|
||||
row += 1
|
||||
if IssueString(issue.issue).as_string().casefold() == IssueString(self.issue_number).as_string().casefold():
|
||||
self.initial_id = issue.issue_id or ""
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
@ -189,6 +192,13 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
def cell_double_clicked(self, r: int, c: int) -> None:
|
||||
self.accept()
|
||||
|
||||
def set_description(self, widget: QtWidgets.QWidget, text: str) -> None:
|
||||
if isinstance(widget, QtWidgets.QTextEdit):
|
||||
widget.setText(text.replace("</figure>", "</div>").replace("<figure", "<div"))
|
||||
else:
|
||||
html = text
|
||||
widget.setHtml(html, QtCore.QUrl(self.talker.website))
|
||||
|
||||
def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None:
|
||||
if curr is None:
|
||||
return
|
||||
@ -198,13 +208,12 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
|
||||
# list selection was changed, update the the issue cover
|
||||
for record in self.issue_list:
|
||||
if record.id == self.issue_id:
|
||||
self.issue_number = record.issue_number
|
||||
self.coverWidget.set_issue_details(self.issue_id, [record.image_url, *record.alt_image_urls])
|
||||
if record.description is None:
|
||||
self.teDescription.setText("")
|
||||
else:
|
||||
self.teDescription.setText(record.description)
|
||||
|
||||
break
|
||||
issue = self.issue_list[curr.row()]
|
||||
if not all((issue.issue, issue.year, issue.month, issue.cover_image)): # issue.title, issue.description
|
||||
issue = self.talker.fetch_comic_data(issue_id=self.issue_id)
|
||||
self.issue_number = issue.issue or ""
|
||||
self.coverWidget.set_issue_details(self.issue_id, [issue.cover_image or "", *issue.alternate_images])
|
||||
if issue.description is None:
|
||||
self.set_description(self.teDescription, "")
|
||||
else:
|
||||
self.set_description(self.teDescription, issue.description)
|
||||
|
@ -141,6 +141,7 @@ class App:
|
||||
config_paths.user_config_dir / "settings.json", list(args) or None
|
||||
)
|
||||
config = cast(settngs.Config[ct_ns], self.manager.get_namespace(cfg, file=True, cmdline=True))
|
||||
config[0].runtime_config = config_paths
|
||||
|
||||
config = ctsettings.validate_commandline_settings(config, self.manager)
|
||||
config = ctsettings.validate_file_settings(config)
|
||||
|
@ -89,8 +89,7 @@ class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for match in self.matches:
|
||||
for row, match in enumerate(self.matches):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = match["series"]
|
||||
@ -130,8 +129,6 @@ class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.SortOrder.AscendingOrder)
|
||||
|
@ -178,11 +178,11 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
try:
|
||||
for idx, comic in enumerate(zip(self.comic_archive_list, self.rename_list)):
|
||||
for idx, comic in enumerate(zip(self.comic_archive_list, self.rename_list), 1):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
break
|
||||
idx += 1
|
||||
|
||||
prog_dialog.setValue(idx)
|
||||
prog_dialog.setLabelText(comic[1])
|
||||
center_window_on_parent(prog_dialog)
|
||||
|
@ -19,12 +19,13 @@ import itertools
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
import natsort
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtCore import QUrl, pyqtSignal
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.genericmetadata import ComicSeries, GenericMetadata
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ctsettings import ct_ns
|
||||
from comictaggerlib.issueidentifier import IssueIdentifier
|
||||
@ -32,9 +33,8 @@ from comictaggerlib.issueselectionwindow import IssueSelectionWindow
|
||||
from comictaggerlib.matchselectionwindow import MatchSelectionWindow
|
||||
from comictaggerlib.progresswindow import IDProgressWindow
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
from comictaggerlib.ui.qtutils import new_web_view, reduce_widget_font_size
|
||||
from comictalker.comictalker import ComicTalker, TalkerError
|
||||
from comictalker.resulttypes import ComicSeries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -122,6 +122,16 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
gridlayout.addWidget(self.imageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.teDetails: QtWidgets.QWidget
|
||||
webengine = new_web_view(self)
|
||||
if webengine:
|
||||
self.teDetails.hide()
|
||||
self.teDetails.deleteLater()
|
||||
# I don't know how to replace teDetails, this is the result of teDetails.height() once rendered
|
||||
webengine.resize(webengine.width(), 141)
|
||||
self.splitter.addWidget(webengine)
|
||||
self.teDetails = webengine
|
||||
|
||||
reduce_widget_font_size(self.teDetails, 1)
|
||||
reduce_widget_font_size(self.twList)
|
||||
|
||||
@ -143,7 +153,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.comic_archive = comic_archive
|
||||
self.immediate_autoselect = autoselect
|
||||
self.cover_index_list = cover_index_list
|
||||
self.ct_search_results: list[ComicSeries] = []
|
||||
self.series_list: list[ComicSeries] = []
|
||||
self.literal = literal
|
||||
self.ii: IssueIdentifier | None = None
|
||||
self.iddialog: IDProgressWindow | None = None
|
||||
@ -199,7 +209,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.twList.hideRow(r)
|
||||
|
||||
def update_buttons(self) -> None:
|
||||
enabled = bool(self.ct_search_results)
|
||||
enabled = bool(self.series_list)
|
||||
|
||||
self.btnRequery.setEnabled(enabled)
|
||||
|
||||
@ -235,7 +245,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
md = GenericMetadata()
|
||||
md.series = self.series_name
|
||||
md.issue = self.issue_number
|
||||
md.year = self.year
|
||||
md.cover_date.year = self.year
|
||||
md.issue_count = self.issue_count
|
||||
|
||||
self.ii.set_additional_metadata(md)
|
||||
@ -322,11 +332,9 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
def show_issues(self) -> None:
|
||||
selector = IssueSelectionWindow(self, self.config, self.talker, self.series_id, self.issue_number)
|
||||
title = ""
|
||||
for record in self.ct_search_results:
|
||||
if record.id == self.series_id:
|
||||
title = record.name
|
||||
title += " (" + str(record.start_year) + ")"
|
||||
title += " - "
|
||||
for series in self.series_list:
|
||||
if series.id == self.series_id:
|
||||
title = f"{series.name} ({series.start_year:04}) - "
|
||||
break
|
||||
|
||||
selector.setWindowTitle(title + "Select Issue")
|
||||
@ -341,9 +349,8 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.imageWidget.update_content()
|
||||
|
||||
def select_by_id(self) -> None:
|
||||
for r in range(0, self.twList.rowCount()):
|
||||
series_id = self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
if series_id == self.series_id:
|
||||
for r, series in enumerate(self.series_list):
|
||||
if series.id == self.series_id:
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
@ -397,16 +404,16 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
)
|
||||
return
|
||||
|
||||
self.ct_search_results = self.search_thread.ct_search_results if self.search_thread is not None else []
|
||||
self.series_list = self.search_thread.ct_search_results if self.search_thread is not None else []
|
||||
# filter the publishers if enabled set
|
||||
if self.use_filter:
|
||||
try:
|
||||
publisher_filter = {s.strip().casefold() for s in self.config.identifier_publisher_filter}
|
||||
# use '' as publisher name if None
|
||||
self.ct_search_results = list(
|
||||
self.series_list = list(
|
||||
filter(
|
||||
lambda d: ("" if d.publisher is None else str(d.publisher).casefold()) not in publisher_filter,
|
||||
self.ct_search_results,
|
||||
self.series_list,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
@ -418,8 +425,8 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
# sort by start_year if set
|
||||
if self.config.identifier_sort_series_by_year:
|
||||
try:
|
||||
self.ct_search_results = sorted(
|
||||
self.ct_search_results,
|
||||
self.series_list = natsort.natsorted(
|
||||
self.series_list,
|
||||
key=lambda i: (str(i.start_year), str(i.count_of_issues)),
|
||||
reverse=True,
|
||||
)
|
||||
@ -427,8 +434,8 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
logger.exception("bad data error sorting results by start_year,count_of_issues")
|
||||
else:
|
||||
try:
|
||||
self.ct_search_results = sorted(
|
||||
self.ct_search_results, key=lambda i: str(i.count_of_issues), reverse=True
|
||||
self.series_list = natsort.natsorted(
|
||||
self.series_list, key=lambda i: str(i.count_of_issues), reverse=True
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("bad data error sorting results by count_of_issues")
|
||||
@ -451,10 +458,10 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
return 1
|
||||
return 2
|
||||
|
||||
for comic in self.ct_search_results:
|
||||
for comic in self.series_list:
|
||||
deques[categorize(comic)].append(comic)
|
||||
logger.info("Length: %d, %d, %d", len(deques[0]), len(deques[1]), len(deques[2]))
|
||||
self.ct_search_results = list(itertools.chain.from_iterable(deques))
|
||||
self.series_list = list(itertools.chain.from_iterable(deques))
|
||||
except Exception:
|
||||
logger.exception("bad data error filtering exact/near matches")
|
||||
|
||||
@ -464,42 +471,39 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
self.twList.setRowCount(0)
|
||||
|
||||
row = 0
|
||||
for record in self.ct_search_results:
|
||||
for row, series in enumerate(self.series_list):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = record.name
|
||||
item_text = series.name
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, record.id)
|
||||
item.setData(QtCore.Qt.ItemDataRole.UserRole, series.id)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
if record.start_year is not None:
|
||||
item_text = f"{record.start_year:04}"
|
||||
if series.start_year is not None:
|
||||
item_text = f"{series.start_year:04}"
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, record.start_year)
|
||||
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, series.start_year)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 1, item)
|
||||
|
||||
if record.count_of_issues is not None:
|
||||
item_text = f"{record.count_of_issues:04}"
|
||||
if series.count_of_issues is not None:
|
||||
item_text = f"{series.count_of_issues:04}"
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, record.count_of_issues)
|
||||
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, series.count_of_issues)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 2, item)
|
||||
|
||||
if record.publisher is not None:
|
||||
item_text = record.publisher
|
||||
if series.publisher is not None:
|
||||
item_text = series.publisher
|
||||
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
|
||||
item = QtWidgets.QTableWidgetItem(item_text)
|
||||
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
|
||||
self.twList.setItem(row, 3, item)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
@ -516,7 +520,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
def showEvent(self, event: QtGui.QShowEvent) -> None:
|
||||
self.perform_query()
|
||||
if not self.ct_search_results:
|
||||
if not self.series_list:
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtWidgets.QMessageBox.information(self, "Search Result", "No matches found!")
|
||||
QtCore.QTimer.singleShot(200, self.close_me)
|
||||
@ -533,6 +537,13 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
def cell_double_clicked(self, r: int, c: int) -> None:
|
||||
self.show_issues()
|
||||
|
||||
def set_description(self, widget: QtWidgets.QWidget, text: str) -> None:
|
||||
if isinstance(widget, QtWidgets.QTextEdit):
|
||||
widget.setText(text.replace("</figure>", "</div>").replace("<figure", "<div"))
|
||||
else:
|
||||
html = text
|
||||
widget.setHtml(html, QUrl(self.talker.website))
|
||||
|
||||
def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None:
|
||||
if curr is None:
|
||||
return
|
||||
@ -542,11 +553,20 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.series_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
|
||||
# list selection was changed, update the info on the series
|
||||
for record in self.ct_search_results:
|
||||
if record.id == self.series_id:
|
||||
if record.description is None:
|
||||
self.teDetails.setText("")
|
||||
else:
|
||||
self.teDetails.setText(record.description)
|
||||
self.imageWidget.set_url(record.image_url)
|
||||
break
|
||||
series = self.series_list[curr.row()]
|
||||
if not all(
|
||||
(
|
||||
series.name,
|
||||
series.start_year,
|
||||
series.count_of_issues,
|
||||
series.publisher,
|
||||
series.description,
|
||||
series.image_url,
|
||||
)
|
||||
):
|
||||
series = self.talker.fetch_series(self.series_id)
|
||||
if series.description is None:
|
||||
self.set_description(self.teDetails, "")
|
||||
else:
|
||||
self.set_description(self.teDetails, series.description)
|
||||
self.imageWidget.set_url(series.image_url)
|
||||
|
@ -424,9 +424,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
self.config[0].identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
|
||||
self.config[0].identifier_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
|
||||
self.config[0].identifier_publisher_filter = [
|
||||
x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip()
|
||||
]
|
||||
self.config[0].identifier_publisher_filter = utils.split(self.tePublisherFilter.toPlainText(), "\n")
|
||||
|
||||
self.config[0].filename_complicated_parser = self.cbxComplicatedParser.isChecked()
|
||||
self.config[0].filename_remove_c2c = self.cbxRemoveC2C.isChecked()
|
||||
|
@ -25,7 +25,6 @@ import pprint
|
||||
import re
|
||||
import sys
|
||||
import webbrowser
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable
|
||||
from urllib.parse import urlparse
|
||||
@ -65,6 +64,7 @@ from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
|
||||
from comictaggerlib.versionchecker import VersionChecker
|
||||
from comictalker.comictalker import ComicTalker, TalkerError
|
||||
from comictalker.talker_utils import cleanup_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -343,6 +343,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.actionLoadFolder.setStatusTip("Load folder with comic archives")
|
||||
self.actionLoadFolder.triggered.connect(self.select_folder)
|
||||
|
||||
self.actionOpenFolderAsComic.setShortcut("Ctrl+Shift+Alt+O")
|
||||
self.actionOpenFolderAsComic.setStatusTip("Load folder as a comic archives")
|
||||
self.actionOpenFolderAsComic.triggered.connect(self.select_folder_archive)
|
||||
|
||||
self.actionWrite_Tags.setShortcut("Ctrl+S")
|
||||
self.actionWrite_Tags.setStatusTip("Save tags to comic archive")
|
||||
self.actionWrite_Tags.triggered.connect(self.commit_metadata)
|
||||
@ -438,6 +442,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
# ToolBar
|
||||
self.actionLoad.setIcon(QtGui.QIcon(str(graphics_path / "open.png")))
|
||||
self.actionLoadFolder.setIcon(QtGui.QIcon(str(graphics_path / "longbox.png")))
|
||||
self.actionOpenFolderAsComic.setIcon(QtGui.QIcon(str(graphics_path / "open.png")))
|
||||
self.actionWrite_Tags.setIcon(QtGui.QIcon(str(graphics_path / "save.png")))
|
||||
self.actionParse_Filename.setIcon(QtGui.QIcon(str(graphics_path / "parse.png")))
|
||||
self.actionParse_Filename_split_words.setIcon(QtGui.QIcon(str(graphics_path / "parse.png")))
|
||||
@ -463,9 +468,12 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def repackage_archive(self) -> None:
|
||||
ca_list = self.fileSelectionList.get_selected_archive_list()
|
||||
non_zip_count = 0
|
||||
zip_list = []
|
||||
for ca in ca_list:
|
||||
if not ca.is_zip():
|
||||
non_zip_count += 1
|
||||
else:
|
||||
zip_list.append(ca)
|
||||
|
||||
if non_zip_count == 0:
|
||||
QtWidgets.QMessageBox.information(
|
||||
@ -502,7 +510,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
prog_dialog.setMinimumDuration(300)
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
prog_idx = 0
|
||||
|
||||
new_archives_to_add = []
|
||||
archives_to_remove = []
|
||||
@ -510,41 +517,39 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
|
||||
for ca in ca_list:
|
||||
if not ca.is_zip():
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
break
|
||||
prog_idx += 1
|
||||
prog_dialog.setValue(prog_idx)
|
||||
prog_dialog.setLabelText(str(ca.path))
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
for prog_idx, ca in enumerate(zip_list, 1):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
break
|
||||
prog_dialog.setValue(prog_idx)
|
||||
prog_dialog.setLabelText(str(ca.path))
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
export_name = ca.path.with_suffix(".cbz")
|
||||
export = True
|
||||
export_name = ca.path.with_suffix(".cbz")
|
||||
export = True
|
||||
|
||||
if export_name.exists():
|
||||
if EW.fileConflictBehavior == ExportConflictOpts.dontCreate:
|
||||
export = False
|
||||
skipped_list.append(ca.path)
|
||||
elif EW.fileConflictBehavior == ExportConflictOpts.createUnique:
|
||||
export_name = utils.unique_file(export_name)
|
||||
if export_name.exists():
|
||||
if EW.fileConflictBehavior == ExportConflictOpts.dontCreate:
|
||||
export = False
|
||||
skipped_list.append(ca.path)
|
||||
elif EW.fileConflictBehavior == ExportConflictOpts.createUnique:
|
||||
export_name = utils.unique_file(export_name)
|
||||
|
||||
if export:
|
||||
if ca.export_as_zip(export_name):
|
||||
success_count += 1
|
||||
if EW.addToList:
|
||||
new_archives_to_add.append(str(export_name))
|
||||
if EW.deleteOriginal:
|
||||
archives_to_remove.append(ca)
|
||||
ca.path.unlink(missing_ok=True)
|
||||
if export:
|
||||
if ca.export_as_zip(export_name):
|
||||
success_count += 1
|
||||
if EW.addToList:
|
||||
new_archives_to_add.append(str(export_name))
|
||||
if EW.deleteOriginal:
|
||||
archives_to_remove.append(ca)
|
||||
ca.path.unlink(missing_ok=True)
|
||||
|
||||
else:
|
||||
# last export failed, so remove the zip, if it exists
|
||||
failed_list.append(ca.path)
|
||||
if export_name.exists():
|
||||
export_name.unlink(missing_ok=True)
|
||||
else:
|
||||
# last export failed, so remove the zip, if it exists
|
||||
failed_list.append(ca.path)
|
||||
if export_name.exists():
|
||||
export_name.unlink(missing_ok=True)
|
||||
|
||||
prog_dialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -789,23 +794,25 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
assign_text(self.leVolumeCount, md.volume_count)
|
||||
assign_text(self.leTitle, md.title)
|
||||
assign_text(self.lePublisher, md.publisher)
|
||||
assign_text(self.lePubMonth, md.month)
|
||||
assign_text(self.lePubYear, md.year)
|
||||
assign_text(self.lePubDay, md.day)
|
||||
assign_text(self.leGenre, md.genre)
|
||||
|
||||
assign_text(self.lePubMonth, md.cover_date.month)
|
||||
assign_text(self.lePubYear, md.cover_date.year)
|
||||
assign_text(self.lePubDay, md.cover_date.day)
|
||||
|
||||
assign_text(self.leGenre, ",".join(md.genres))
|
||||
assign_text(self.leImprint, md.imprint)
|
||||
assign_text(self.teComments, md.comments)
|
||||
assign_text(self.teComments, md.description)
|
||||
assign_text(self.teNotes, md.notes)
|
||||
assign_text(self.leStoryArc, md.story_arc)
|
||||
assign_text(self.leStoryArc, ",".join(md.story_arcs))
|
||||
assign_text(self.leScanInfo, md.scan_info)
|
||||
assign_text(self.leSeriesGroup, md.series_group)
|
||||
assign_text(self.leSeriesGroup, ",".join(md.series_groups))
|
||||
assign_text(self.leAltSeries, md.alternate_series)
|
||||
assign_text(self.leAltIssueNum, md.alternate_number)
|
||||
assign_text(self.leAltIssueCount, md.alternate_count)
|
||||
assign_text(self.leWebLink, md.web_link)
|
||||
assign_text(self.teCharacters, md.characters)
|
||||
assign_text(self.teTeams, md.teams)
|
||||
assign_text(self.teLocations, md.locations)
|
||||
assign_text(self.teCharacters, "\n".join(md.characters))
|
||||
assign_text(self.teTeams, "\n".join(md.teams))
|
||||
assign_text(self.teLocations, "\n".join(md.locations))
|
||||
|
||||
self.dsbCriticalRating.setValue(md.critical_rating or 0.0)
|
||||
|
||||
@ -855,8 +862,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if md.credits is not None and len(md.credits) != 0:
|
||||
self.twCredits.setSortingEnabled(False)
|
||||
|
||||
row = 0
|
||||
for credit in md.credits:
|
||||
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"]):
|
||||
continue
|
||||
@ -865,8 +871,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
row, credit["role"].title(), credit["person"], (credit["primary"] if "primary" in credit else False)
|
||||
)
|
||||
|
||||
row += 1
|
||||
|
||||
self.twCredits.setSortingEnabled(True)
|
||||
self.update_credit_colors()
|
||||
|
||||
@ -906,17 +910,17 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
md.issue_count = utils.xlate_int(self.leIssueCount.text())
|
||||
md.volume = utils.xlate_int(self.leVolumeNum.text())
|
||||
md.volume_count = utils.xlate_int(self.leVolumeCount.text())
|
||||
md.month = utils.xlate_int(self.lePubMonth.text())
|
||||
md.year = utils.xlate_int(self.lePubYear.text())
|
||||
md.day = utils.xlate_int(self.lePubDay.text())
|
||||
md.cover_date.month = utils.xlate_int(self.lePubMonth.text())
|
||||
md.cover_date.year = utils.xlate_int(self.lePubYear.text())
|
||||
md.cover_date.day = utils.xlate_int(self.lePubDay.text())
|
||||
md.alternate_count = utils.xlate_int(self.leAltIssueCount.text())
|
||||
|
||||
md.series = utils.xlate(self.leSeries.text())
|
||||
md.title = utils.xlate(self.leTitle.text())
|
||||
md.publisher = utils.xlate(self.lePublisher.text())
|
||||
md.genre = utils.xlate(self.leGenre.text())
|
||||
md.genres = utils.split(self.leGenre.text(), ",")
|
||||
md.imprint = utils.xlate(self.leImprint.text())
|
||||
md.comments = utils.xlate(self.teComments.toPlainText())
|
||||
md.description = utils.xlate(self.teComments.toPlainText())
|
||||
md.notes = utils.xlate(self.teNotes.toPlainText())
|
||||
md.maturity_rating = self.cbMaturityRating.currentText()
|
||||
|
||||
@ -924,14 +928,14 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if md.critical_rating == 0.0:
|
||||
md.critical_rating = None
|
||||
|
||||
md.story_arc = utils.xlate(self.leStoryArc.text())
|
||||
md.story_arcs = utils.split(self.leStoryArc.text(), ",")
|
||||
md.scan_info = utils.xlate(self.leScanInfo.text())
|
||||
md.series_group = utils.xlate(self.leSeriesGroup.text())
|
||||
md.alternate_series = utils.xlate(self.leAltSeries.text())
|
||||
md.series_groups = utils.split(self.leSeriesGroup.text(), ",")
|
||||
md.alternate_series = self.leAltSeries.text()
|
||||
md.web_link = utils.xlate(self.leWebLink.text())
|
||||
md.characters = utils.xlate(self.teCharacters.toPlainText())
|
||||
md.teams = utils.xlate(self.teTeams.toPlainText())
|
||||
md.locations = utils.xlate(self.teLocations.toPlainText())
|
||||
md.characters = utils.split(self.teCharacters.toPlainText(), "\n")
|
||||
md.teams = utils.split(self.teTeams.toPlainText(), "\n")
|
||||
md.locations = utils.split(self.teLocations.toPlainText(), "\n")
|
||||
|
||||
md.format = utils.xlate(self.cbFormat.currentText())
|
||||
md.country = utils.xlate(self.cbCountry.currentText())
|
||||
@ -941,13 +945,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
md.manga = utils.xlate(self.cbManga.itemData(self.cbManga.currentIndex()))
|
||||
|
||||
# Make a list from the comma delimited tags string
|
||||
tmp = self.teTags.toPlainText()
|
||||
if tmp is not None:
|
||||
|
||||
def strip_list(i: Iterable[str]) -> set[str]:
|
||||
return {x.strip() for x in i}
|
||||
|
||||
md.tags = strip_list(tmp.split(","))
|
||||
md.tags = set(utils.split(self.teTags.toPlainText(), ","))
|
||||
|
||||
md.black_and_white = self.cbBW.isChecked()
|
||||
|
||||
@ -987,24 +985,32 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def select_folder(self) -> None:
|
||||
self.select_file(folder_mode=True)
|
||||
|
||||
def select_folder_archive(self) -> None:
|
||||
dialog = self.file_dialog(folder_mode=True)
|
||||
if dialog.exec():
|
||||
file_list = dialog.selectedFiles()
|
||||
if file_list:
|
||||
self.fileSelectionList.twList.selectRow(self.fileSelectionList.add_path_item(file_list[0]))
|
||||
|
||||
def select_file(self, folder_mode: bool = False) -> None:
|
||||
dialog = self.file_dialog(folder_mode=folder_mode)
|
||||
if dialog.exec():
|
||||
file_list = dialog.selectedFiles()
|
||||
self.fileSelectionList.add_path_list(file_list)
|
||||
|
||||
def file_dialog(self, folder_mode: bool = False) -> QtWidgets.QFileDialog:
|
||||
dialog = QtWidgets.QFileDialog(self)
|
||||
if folder_mode:
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.Directory)
|
||||
else:
|
||||
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar *.cb7 *.7z)"
|
||||
filters = [archive_filter, "Any files (*)"]
|
||||
dialog.setNameFilters(filters)
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles)
|
||||
|
||||
if self.config[0].internal_last_opened_folder is not None:
|
||||
dialog.setDirectory(self.config[0].internal_last_opened_folder)
|
||||
|
||||
if not folder_mode:
|
||||
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar *.cb7 *.7z)"
|
||||
filters = [archive_filter, "Any files (*)"]
|
||||
dialog.setNameFilters(filters)
|
||||
|
||||
if dialog.exec():
|
||||
file_list = dialog.selectedFiles()
|
||||
self.fileSelectionList.add_path_list(file_list)
|
||||
return dialog
|
||||
|
||||
def auto_identify_search(self) -> None:
|
||||
if self.comic_archive is None:
|
||||
@ -1087,7 +1093,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
)
|
||||
self.metadata.overlay(
|
||||
new_metadata.replace(
|
||||
notes=utils.combine_notes(self.metadata.notes, notes, "Tagged with ComicTagger")
|
||||
notes=utils.combine_notes(self.metadata.notes, notes, "Tagged with ComicTagger"),
|
||||
description=cleanup_html(
|
||||
new_metadata.description, self.config[0].talker_remove_html_tables
|
||||
),
|
||||
)
|
||||
)
|
||||
# Now push the new combined data into the edit controls
|
||||
@ -1535,27 +1544,23 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
center_window_on_parent(progdialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
prog_idx = 0
|
||||
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
for ca in ca_list:
|
||||
for prog_idx, ca in enumerate(ca_list, 1):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
progdialog.setValue(prog_idx)
|
||||
progdialog.setLabelText(str(ca.path))
|
||||
center_window_on_parent(progdialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if ca.has_metadata(style):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
break
|
||||
prog_idx += 1
|
||||
progdialog.setValue(prog_idx)
|
||||
progdialog.setLabelText(str(ca.path))
|
||||
center_window_on_parent(progdialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.has_metadata(style) and ca.is_writable():
|
||||
if not ca.remove_metadata(style):
|
||||
failed_list.append(ca.path)
|
||||
else:
|
||||
success_count += 1
|
||||
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
if ca.is_writable():
|
||||
if not ca.remove_metadata(style):
|
||||
failed_list.append(ca.path)
|
||||
else:
|
||||
success_count += 1
|
||||
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
|
||||
|
||||
progdialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -1617,20 +1622,18 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
prog_dialog.setMinimumDuration(300)
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
prog_idx = 0
|
||||
|
||||
failed_list = []
|
||||
success_count = 0
|
||||
for ca in ca_list:
|
||||
if ca.has_metadata(src_style):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
break
|
||||
prog_idx += 1
|
||||
prog_dialog.setValue(prog_idx)
|
||||
prog_dialog.setLabelText(str(ca.path))
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
for prog_idx, ca in enumerate(ca_list, 1):
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
break
|
||||
|
||||
prog_dialog.setValue(prog_idx)
|
||||
prog_dialog.setLabelText(str(ca.path))
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.has_metadata(src_style) and ca.is_writable():
|
||||
md = ca.read_metadata(src_style)
|
||||
@ -1722,7 +1725,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
return False, match_results
|
||||
|
||||
if dlg.dont_use_year:
|
||||
md.year = None
|
||||
md.cover_date.year = None
|
||||
if md.issue is None or md.issue == "":
|
||||
if dlg.assume_issue_one:
|
||||
md.issue = "1"
|
||||
@ -1842,13 +1845,11 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.auto_tag_log("==========================================================================\n")
|
||||
self.auto_tag_log(f"Auto-Tagging Started for {len(ca_list)} items\n")
|
||||
|
||||
prog_idx = 0
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
archives_to_remove = []
|
||||
for ca in ca_list:
|
||||
for prog_idx, ca in enumerate(ca_list):
|
||||
self.auto_tag_log("==========================================================================\n")
|
||||
self.auto_tag_log(f"Auto-Tagging {prog_idx + 1} of {len(ca_list)}\n")
|
||||
self.auto_tag_log(f"Auto-Tagging {prog_idx} of {len(ca_list)}\n")
|
||||
self.auto_tag_log(f"{ca.path}\n")
|
||||
try:
|
||||
cover_idx = ca.read_metadata(style).get_cover_page_index_list()[0]
|
||||
@ -1863,7 +1864,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if self.atprogdialog.isdone:
|
||||
break
|
||||
self.atprogdialog.progressBar.setValue(prog_idx)
|
||||
prog_idx += 1
|
||||
|
||||
self.atprogdialog.label.setText(str(ca.path))
|
||||
center_window_on_parent(self.atprogdialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
@ -5,6 +5,10 @@ from __future__ import annotations
|
||||
import io
|
||||
import logging
|
||||
import traceback
|
||||
import webbrowser
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
|
||||
@ -12,6 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
qt_available = True
|
||||
except ImportError:
|
||||
@ -25,6 +30,68 @@ if qt_available:
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
try:
|
||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
|
||||
|
||||
class WebPage(QWebEnginePage):
|
||||
def acceptNavigationRequest(
|
||||
self, url: QUrl, n_type: QWebEnginePage.NavigationType, isMainFrame: bool
|
||||
) -> bool:
|
||||
if n_type in (
|
||||
QWebEnginePage.NavigationType.NavigationTypeOther,
|
||||
QWebEnginePage.NavigationType.NavigationTypeTyped,
|
||||
):
|
||||
return True
|
||||
if n_type in (QWebEnginePage.NavigationType.NavigationTypeLinkClicked,) and url.scheme() in (
|
||||
"http",
|
||||
"https",
|
||||
):
|
||||
webbrowser.open(url.toString())
|
||||
return False
|
||||
|
||||
def new_web_view(parent: QWidget) -> QWebEngineView:
|
||||
webengine = QWebEngineView(parent)
|
||||
webengine.setPage(WebPage(parent))
|
||||
webengine.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
|
||||
settings = webengine.settings()
|
||||
settings.setAttribute(settings.WebAttribute.AutoLoadImages, True)
|
||||
settings.setAttribute(settings.WebAttribute.JavascriptEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.JavascriptCanOpenWindows, False)
|
||||
settings.setAttribute(settings.WebAttribute.JavascriptCanAccessClipboard, False)
|
||||
settings.setAttribute(settings.WebAttribute.LinksIncludedInFocusChain, False)
|
||||
settings.setAttribute(settings.WebAttribute.LocalStorageEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.LocalContentCanAccessRemoteUrls, False)
|
||||
settings.setAttribute(settings.WebAttribute.XSSAuditingEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.SpatialNavigationEnabled, True)
|
||||
settings.setAttribute(settings.WebAttribute.LocalContentCanAccessFileUrls, False)
|
||||
settings.setAttribute(settings.WebAttribute.HyperlinkAuditingEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.ScrollAnimatorEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.ErrorPageEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.PluginsEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.FullScreenSupportEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.ScreenCaptureEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.WebGLEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.Accelerated2dCanvasEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.AutoLoadIconsForPage, False)
|
||||
settings.setAttribute(settings.WebAttribute.TouchIconsEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.FocusOnNavigationEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.PrintElementBackgrounds, False)
|
||||
settings.setAttribute(settings.WebAttribute.AllowRunningInsecureContent, False)
|
||||
settings.setAttribute(settings.WebAttribute.AllowGeolocationOnInsecureOrigins, False)
|
||||
settings.setAttribute(settings.WebAttribute.AllowWindowActivationFromJavaScript, False)
|
||||
settings.setAttribute(settings.WebAttribute.ShowScrollBars, True)
|
||||
settings.setAttribute(settings.WebAttribute.PlaybackRequiresUserGesture, True)
|
||||
settings.setAttribute(settings.WebAttribute.JavascriptCanPaste, False)
|
||||
settings.setAttribute(settings.WebAttribute.WebRTCPublicInterfacesOnly, False)
|
||||
settings.setAttribute(settings.WebAttribute.DnsPrefetchEnabled, False)
|
||||
settings.setAttribute(settings.WebAttribute.PdfViewerEnabled, False)
|
||||
return webengine
|
||||
|
||||
except ImportError:
|
||||
|
||||
def new_web_view(parent: QWidget) -> QWebEngineView:
|
||||
...
|
||||
|
||||
def reduce_widget_font_size(widget: QtWidgets.QWidget, delta: int = 2) -> None:
|
||||
f = widget.font()
|
||||
if f.pointSize() > 10:
|
||||
|
@ -1156,7 +1156,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1096</width>
|
||||
<height>30</height>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuComicTagger">
|
||||
@ -1180,6 +1180,7 @@
|
||||
</widget>
|
||||
<addaction name="actionLoad"/>
|
||||
<addaction name="actionLoadFolder"/>
|
||||
<addaction name="actionOpenFolderAsComic"/>
|
||||
<addaction name="actionWrite_Tags"/>
|
||||
<addaction name="actionAutoTag"/>
|
||||
<addaction name="actionCopyTags"/>
|
||||
@ -1444,6 +1445,11 @@
|
||||
<string>perform a literal search on the series and return the first 50 results</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOpenFolderAsComic">
|
||||
<property name="text">
|
||||
<string>Open Folder as Comic</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<resources/>
|
||||
|
@ -10,15 +10,12 @@ else:
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
from comictalker.comictalker import ComicTalker, TalkerError
|
||||
from comictalker.resulttypes import ComicIssue, ComicSeries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"ComicTalker",
|
||||
"TalkerError",
|
||||
"ComicIssue",
|
||||
"ComicSeries",
|
||||
]
|
||||
|
||||
|
||||
|
@ -15,16 +15,16 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sqlite3 as lite
|
||||
from typing import Any
|
||||
import sqlite3
|
||||
from typing import Any, cast
|
||||
|
||||
from comictalker.resulttypes import ComicIssue, ComicSeries, Credit
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import ComicSeries, Credit, Date, GenericMetadata, TagOrigin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -68,23 +68,28 @@ class ComicCacher:
|
||||
# this will wipe out any existing version
|
||||
open(self.db_file, "wb").close()
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
con = sqlite3.connect(self.db_file)
|
||||
con.row_factory = sqlite3.Row
|
||||
|
||||
# create tables
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
# source_name,name,id,start_year,publisher,image,description,count_of_issues
|
||||
# source,name,id,start_year,publisher,image,description,count_of_issues
|
||||
cur.execute(
|
||||
"CREATE TABLE SeriesSearchCache("
|
||||
+ "search_term TEXT,"
|
||||
+ "id TEXT NOT NULL,"
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')),"
|
||||
+ "source_name TEXT NOT NULL)"
|
||||
+ "id TEXT NOT NULL,"
|
||||
+ "source TEXT NOT NULL,"
|
||||
+ "search_term TEXT,"
|
||||
+ "PRIMARY KEY (id, source, search_term))"
|
||||
)
|
||||
cur.execute("CREATE TABLE Source(" + "id TEXT NOT NULL," + "name TEXT NOT NULL," + "PRIMARY KEY (id))")
|
||||
|
||||
cur.execute(
|
||||
"CREATE TABLE Series("
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
|
||||
+ "id TEXT NOT NULL,"
|
||||
+ "source TEXT NOT NULL,"
|
||||
+ "name TEXT,"
|
||||
+ "publisher TEXT,"
|
||||
+ "count_of_issues INT,"
|
||||
@ -95,24 +100,23 @@ class ComicCacher:
|
||||
+ "description TEXT,"
|
||||
+ "genres TEXT," # Newline separated. For filtering etc.
|
||||
+ "format TEXT,"
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
|
||||
+ "source_name TEXT NOT NULL,"
|
||||
+ "PRIMARY KEY (id, source_name))"
|
||||
+ "PRIMARY KEY (id, source))"
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"CREATE TABLE Issues("
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
|
||||
+ "id TEXT NOT NULL,"
|
||||
+ "source TEXT NOT NULL,"
|
||||
+ "series_id TEXT,"
|
||||
+ "name TEXT,"
|
||||
+ "issue_number TEXT,"
|
||||
+ "image_url TEXT,"
|
||||
+ "thumb_url TEXT,"
|
||||
+ "cover_date TEXT,"
|
||||
+ "store_date TEXT,"
|
||||
+ "site_detail_url TEXT,"
|
||||
+ "description TEXT,"
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
|
||||
+ "source_name TEXT NOT NULL,"
|
||||
+ "aliases TEXT," # Newline separated
|
||||
+ "alt_image_urls TEXT," # Newline separated URLs
|
||||
+ "characters TEXT," # Newline separated
|
||||
@ -129,32 +133,33 @@ class ComicCacher:
|
||||
+ "country TEXT,"
|
||||
+ "volume TEXT,"
|
||||
+ "complete BOOL," # Is the data complete? Includes characters, locations, credits.
|
||||
+ "PRIMARY KEY (id, source_name))"
|
||||
+ "PRIMARY KEY (id, source))"
|
||||
)
|
||||
|
||||
def add_search_results(self, source_name: str, search_term: str, ct_search_results: list[ComicSeries]) -> None:
|
||||
con = lite.connect(self.db_file)
|
||||
def add_search_results(self, source: TagOrigin, search_term: str, series_list: list[ComicSeries]) -> None:
|
||||
self.add_source(source)
|
||||
|
||||
with con:
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
con.text_factory = str
|
||||
cur = con.cursor()
|
||||
|
||||
# remove all previous entries with this search term
|
||||
cur.execute(
|
||||
"DELETE FROM SeriesSearchCache WHERE search_term = ? AND source_name = ?",
|
||||
[search_term.casefold(), source_name],
|
||||
"DELETE FROM SeriesSearchCache WHERE search_term = ? AND source = ?",
|
||||
[search_term.casefold(), source.id],
|
||||
)
|
||||
|
||||
# now add in new results
|
||||
for record in ct_search_results:
|
||||
for record in series_list:
|
||||
cur.execute(
|
||||
"INSERT INTO SeriesSearchCache " + "(source_name, search_term, id) " + "VALUES(?, ?, ?)",
|
||||
(source_name, search_term.casefold(), record.id),
|
||||
"INSERT INTO SeriesSearchCache (source, search_term, id) VALUES(?, ?, ?)",
|
||||
(source.id, search_term.casefold(), record.id),
|
||||
)
|
||||
|
||||
data = {
|
||||
"id": record.id,
|
||||
"source_name": source_name,
|
||||
"source": source.id,
|
||||
"name": record.name,
|
||||
"publisher": record.publisher,
|
||||
"count_of_issues": record.count_of_issues,
|
||||
@ -169,91 +174,59 @@ class ComicCacher:
|
||||
}
|
||||
self.upsert(cur, "series", data)
|
||||
|
||||
def get_search_results(self, source_name: str, search_term: str) -> list[ComicSeries]:
|
||||
results = []
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
con.text_factory = str
|
||||
cur = con.cursor()
|
||||
def add_series_info(self, source: TagOrigin, series: ComicSeries) -> None:
|
||||
self.add_source(source)
|
||||
|
||||
cur.execute(
|
||||
"SELECT * FROM SeriesSearchCache INNER JOIN Series on"
|
||||
" SeriesSearchCache.id=Series.id AND SeriesSearchCache.source_name=Series.source_name"
|
||||
" WHERE search_term=? AND SeriesSearchCache.source_name=?",
|
||||
[search_term.casefold(), source_name],
|
||||
)
|
||||
|
||||
rows = cur.fetchall()
|
||||
# now process the results
|
||||
for record in rows:
|
||||
result = ComicSeries(
|
||||
id=record[4],
|
||||
name=record[5],
|
||||
publisher=record[6],
|
||||
count_of_issues=record[7],
|
||||
count_of_volumes=record[8],
|
||||
start_year=record[9],
|
||||
image_url=record[10],
|
||||
aliases=record[11].strip().splitlines(),
|
||||
description=record[12],
|
||||
genres=record[13].strip().splitlines(),
|
||||
format=record[14],
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def add_series_info(self, source_name: str, series_record: ComicSeries) -> None:
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
with con:
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
"id": series_record.id,
|
||||
"source_name": source_name,
|
||||
"name": series_record.name,
|
||||
"publisher": series_record.publisher,
|
||||
"count_of_issues": series_record.count_of_issues,
|
||||
"count_of_volumes": series_record.count_of_volumes,
|
||||
"start_year": series_record.start_year,
|
||||
"image_url": series_record.image_url,
|
||||
"description": series_record.description,
|
||||
"genres": "\n".join(series_record.genres),
|
||||
"format": series_record.format,
|
||||
"id": series.id,
|
||||
"source": source.id,
|
||||
"name": series.name,
|
||||
"publisher": series.publisher,
|
||||
"count_of_issues": series.count_of_issues,
|
||||
"count_of_volumes": series.count_of_volumes,
|
||||
"start_year": series.start_year,
|
||||
"image_url": series.image_url,
|
||||
"description": series.description,
|
||||
"genres": "\n".join(series.genres),
|
||||
"format": series.format,
|
||||
"timestamp": timestamp,
|
||||
"aliases": "\n".join(series_record.aliases),
|
||||
"aliases": "\n".join(series.aliases),
|
||||
}
|
||||
self.upsert(cur, "series", data)
|
||||
|
||||
def add_series_issues_info(self, source_name: str, series_issues: list[ComicIssue]) -> None:
|
||||
con = lite.connect(self.db_file)
|
||||
def add_series_issues_info(self, source: TagOrigin, issues: list[GenericMetadata], complete: bool) -> None:
|
||||
self.add_source(source)
|
||||
|
||||
with con:
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
# add in issues
|
||||
|
||||
for issue in series_issues:
|
||||
for issue in issues:
|
||||
data = {
|
||||
"id": issue.id,
|
||||
"series_id": issue.series.id,
|
||||
"source_name": source_name,
|
||||
"name": issue.name,
|
||||
"issue_number": issue.issue_number,
|
||||
"id": issue.issue_id,
|
||||
"series_id": issue.series_id,
|
||||
"source": source.id,
|
||||
"name": issue.title,
|
||||
"issue_number": issue.issue,
|
||||
"volume": issue.volume,
|
||||
"site_detail_url": issue.site_detail_url,
|
||||
"cover_date": issue.cover_date,
|
||||
"image_url": issue.image_url,
|
||||
"site_detail_url": issue.web_link,
|
||||
"cover_date": str(issue.cover_date),
|
||||
"store_date": str(issue.store_date),
|
||||
"image_url": issue.cover_image,
|
||||
"description": issue.description,
|
||||
"timestamp": timestamp,
|
||||
"aliases": "\n".join(issue.aliases),
|
||||
"alt_image_urls": "\n".join(issue.alt_image_urls),
|
||||
"aliases": "\n".join(issue.title_aliases),
|
||||
"alt_image_urls": "\n".join(issue.alternate_images),
|
||||
"characters": "\n".join(issue.characters),
|
||||
"locations": "\n".join(issue.locations),
|
||||
"teams": "\n".join(issue.teams),
|
||||
@ -265,26 +238,76 @@ class ComicCacher:
|
||||
"maturity_rating": issue.maturity_rating,
|
||||
"language": issue.language,
|
||||
"country": issue.country,
|
||||
"credits": json.dumps([dataclasses.asdict(x) for x in issue.credits]),
|
||||
"complete": issue.complete,
|
||||
"credits": json.dumps(issue.credits),
|
||||
"complete": complete,
|
||||
}
|
||||
self.upsert(cur, "issues", data)
|
||||
|
||||
def get_series_info(self, series_id: str, source_name: str, purge: bool = True) -> ComicSeries | None:
|
||||
result: ComicSeries | None = None
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
def add_source(self, source: TagOrigin) -> None:
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
|
||||
if purge:
|
||||
self.upsert(
|
||||
cur,
|
||||
"source",
|
||||
{
|
||||
"id": source.id,
|
||||
"name": source.name,
|
||||
},
|
||||
)
|
||||
|
||||
def get_search_results(self, source: TagOrigin, search_term: str) -> list[ComicSeries]:
|
||||
results = []
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
con.text_factory = str
|
||||
cur = con.cursor()
|
||||
|
||||
cur.execute(
|
||||
"SELECT * FROM SeriesSearchCache INNER JOIN Series on"
|
||||
+ " SeriesSearchCache.id=Series.id AND SeriesSearchCache.source=Series.source"
|
||||
+ " WHERE search_term=? AND SeriesSearchCache.source=?",
|
||||
[search_term.casefold(), source.id],
|
||||
)
|
||||
|
||||
rows = cur.fetchall()
|
||||
# now process the results
|
||||
for record in rows:
|
||||
result = ComicSeries(
|
||||
id=record["id"],
|
||||
name=record["name"],
|
||||
publisher=record["publisher"],
|
||||
count_of_issues=record["count_of_issues"],
|
||||
count_of_volumes=record["count_of_volumes"],
|
||||
start_year=record["start_year"],
|
||||
image_url=record["image_url"],
|
||||
aliases=utils.split(record["aliases"], "\n"),
|
||||
description=record["description"],
|
||||
genres=utils.split(record["genres"], "\n"),
|
||||
format=record["format"],
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def get_series_info(self, series_id: str, source: TagOrigin, expire_stale: bool = True) -> ComicSeries | None:
|
||||
result: ComicSeries | None = None
|
||||
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
|
||||
if expire_stale:
|
||||
# purge stale series info
|
||||
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
|
||||
cur.execute("DELETE FROM Series WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
# fetch
|
||||
cur.execute("SELECT * FROM Series" " WHERE id=? AND source_name=?", [series_id, source_name])
|
||||
cur.execute("SELECT * FROM Series WHERE id=? AND source=?", [series_id, source.id])
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
@ -293,24 +316,24 @@ class ComicCacher:
|
||||
|
||||
# since ID is primary key, there is only one row
|
||||
result = ComicSeries(
|
||||
id=row[0],
|
||||
name=row[1],
|
||||
publisher=row[2],
|
||||
count_of_issues=row[3],
|
||||
count_of_volumes=row[4],
|
||||
start_year=row[5],
|
||||
image_url=row[6],
|
||||
aliases=row[7].strip().splitlines(),
|
||||
description=row[8],
|
||||
genres=row[9].strip().splitlines(),
|
||||
format=row[10],
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
publisher=row["publisher"],
|
||||
count_of_issues=row["count_of_issues"],
|
||||
count_of_volumes=row["count_of_volumes"],
|
||||
start_year=row["start_year"],
|
||||
image_url=row["image_url"],
|
||||
aliases=utils.split(row["aliases"], "\n"),
|
||||
description=row["description"],
|
||||
genres=utils.split(row["genres"], "\n"),
|
||||
format=row["format"],
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def get_series_issues_info(self, series_id: str, source_name: str) -> list[ComicIssue]:
|
||||
def get_series_issues_info(self, series_id: str, source: TagOrigin) -> list[tuple[GenericMetadata, bool]]:
|
||||
# get_series_info should only fail if someone is doing something weird
|
||||
series = self.get_series_info(series_id, source_name, False) or ComicSeries(
|
||||
series = self.get_series_info(series_id, source, False) or ComicSeries(
|
||||
id=series_id,
|
||||
name="",
|
||||
description="",
|
||||
@ -323,8 +346,9 @@ class ComicCacher:
|
||||
count_of_volumes=None,
|
||||
format=None,
|
||||
)
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
|
||||
@ -334,53 +358,22 @@ class ComicCacher:
|
||||
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
# fetch
|
||||
results: list[ComicIssue] = []
|
||||
results: list[tuple[GenericMetadata, bool]] = []
|
||||
|
||||
cur.execute("SELECT * FROM Issues WHERE series_id=? AND source_name=?", [series_id, source_name])
|
||||
cur.execute("SELECT * FROM Issues WHERE series_id=? AND source=?", [series_id, source.id])
|
||||
rows = cur.fetchall()
|
||||
|
||||
# now process the results
|
||||
for row in rows:
|
||||
credits = []
|
||||
try:
|
||||
for credit in json.loads(row[15]):
|
||||
credits.append(Credit(**credit))
|
||||
except Exception:
|
||||
logger.exception("credits failed")
|
||||
record = ComicIssue(
|
||||
id=row[0],
|
||||
name=row[2],
|
||||
issue_number=row[3],
|
||||
volume=row[25],
|
||||
site_detail_url=row[7],
|
||||
cover_date=row[6],
|
||||
image_url=row[4],
|
||||
description=row[8],
|
||||
series=series,
|
||||
aliases=row[11].strip().splitlines(),
|
||||
alt_image_urls=row[12].strip().splitlines(),
|
||||
characters=row[13].strip().splitlines(),
|
||||
locations=row[14].strip().splitlines(),
|
||||
credits=credits,
|
||||
teams=row[16].strip().splitlines(),
|
||||
story_arcs=row[17].strip().splitlines(),
|
||||
genres=row[18].strip().splitlines(),
|
||||
tags=row[19].strip().splitlines(),
|
||||
critical_rating=row[20],
|
||||
manga=row[21],
|
||||
maturity_rating=row[22],
|
||||
language=row[23],
|
||||
country=row[24],
|
||||
complete=bool(row[26]),
|
||||
)
|
||||
record = self.map_row_metadata(row, series, source)
|
||||
|
||||
results.append(record)
|
||||
|
||||
return results
|
||||
|
||||
def get_issue_info(self, issue_id: int, source_name: str) -> ComicIssue | None:
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
def get_issue_info(self, issue_id: int, source: TagOrigin) -> tuple[GenericMetadata, bool] | None:
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
|
||||
@ -389,15 +382,15 @@ class ComicCacher:
|
||||
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
|
||||
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
cur.execute("SELECT * FROM Issues WHERE id=? AND source_name=?", [issue_id, source_name])
|
||||
cur.execute("SELECT * FROM Issues WHERE id=? AND source=?", [issue_id, source.id])
|
||||
row = cur.fetchone()
|
||||
|
||||
record = None
|
||||
|
||||
if row:
|
||||
# get_series_info should only fail if someone is doing something weird
|
||||
series = self.get_series_info(row[1], source_name, False) or ComicSeries(
|
||||
id=row[1],
|
||||
series = self.get_series_info(row["id"], source, False) or ComicSeries(
|
||||
id=row["id"],
|
||||
name="",
|
||||
description="",
|
||||
genres=[],
|
||||
@ -410,43 +403,69 @@ class ComicCacher:
|
||||
format=None,
|
||||
)
|
||||
|
||||
# now process the results
|
||||
credits = []
|
||||
try:
|
||||
for credit in json.loads(row[15]):
|
||||
credits.append(Credit(**credit))
|
||||
except Exception:
|
||||
logger.exception("credits failed")
|
||||
record = ComicIssue(
|
||||
id=row[0],
|
||||
name=row[2],
|
||||
issue_number=row[3],
|
||||
volume=row[25],
|
||||
site_detail_url=row[7],
|
||||
cover_date=row[6],
|
||||
image_url=row[4],
|
||||
description=row[8],
|
||||
series=series,
|
||||
aliases=row[11].strip().splitlines(),
|
||||
alt_image_urls=row[12].strip().splitlines(),
|
||||
characters=row[13].strip().splitlines(),
|
||||
locations=row[14].strip().splitlines(),
|
||||
credits=credits,
|
||||
teams=row[16].strip().splitlines(),
|
||||
story_arcs=row[17].strip().splitlines(),
|
||||
genres=row[18].strip().splitlines(),
|
||||
tags=row[19].strip().splitlines(),
|
||||
critical_rating=row[20],
|
||||
manga=row[21],
|
||||
maturity_rating=row[22],
|
||||
language=row[23],
|
||||
country=row[24],
|
||||
complete=bool(row[26]),
|
||||
)
|
||||
record = self.map_row_metadata(row, series, source)
|
||||
|
||||
return record
|
||||
|
||||
def upsert(self, cur: lite.Cursor, tablename: str, data: dict[str, Any]) -> None:
|
||||
def get_source(self, source_id: str) -> TagOrigin:
|
||||
con = sqlite3.connect(self.db_file)
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
|
||||
cur.execute("SELECT * FROM Source WHERE id=?", [source_id])
|
||||
row = cur.fetchone()
|
||||
|
||||
return TagOrigin(row["id"], row["name"])
|
||||
|
||||
def map_row_metadata(
|
||||
self, row: sqlite3.Row, series: ComicSeries, source: TagOrigin
|
||||
) -> tuple[GenericMetadata, bool]:
|
||||
day, month, year = utils.parse_date_str(row["cover_date"])
|
||||
credits = []
|
||||
try:
|
||||
for credit in json.loads(row["credits"]):
|
||||
credits.append(cast(Credit, credit))
|
||||
except Exception:
|
||||
logger.exception("credits failed")
|
||||
return (
|
||||
GenericMetadata(
|
||||
tag_origin=source,
|
||||
alternate_images=utils.split(row["alt_image_urls"], "\n"),
|
||||
characters=utils.split(row["characters"], "\n"),
|
||||
country=row["country"],
|
||||
cover_image=row["image_url"],
|
||||
credits=credits,
|
||||
critical_rating=row["critical_rating"],
|
||||
cover_date=Date.parse_date(row["cover_date"]),
|
||||
store_date=Date.parse_date(row["store_date"]),
|
||||
description=row["description"],
|
||||
genres=utils.split(row["genres"], "\n"),
|
||||
issue=row["issue_number"],
|
||||
issue_count=series.count_of_issues,
|
||||
issue_id=row["id"],
|
||||
language=row["language"],
|
||||
locations=utils.split(row["locations"], "\n"),
|
||||
manga=row["manga"],
|
||||
maturity_rating=row["maturity_rating"],
|
||||
publisher=series.publisher,
|
||||
series=series.name,
|
||||
series_aliases=series.aliases,
|
||||
series_id=series.id,
|
||||
story_arcs=utils.split(row["story_arcs"], "\n"),
|
||||
tags=set(utils.split(row["tags"], "\n")),
|
||||
teams=utils.split(row["teams"], "\n"),
|
||||
title=row["name"],
|
||||
title_aliases=utils.split(row["aliases"], "\n"),
|
||||
volume=row["volume"],
|
||||
volume_count=series.count_of_volumes,
|
||||
web_link=row["site_detail_url"],
|
||||
),
|
||||
row["complete"],
|
||||
)
|
||||
|
||||
def upsert(self, cur: sqlite3.Cursor, tablename: str, data: dict[str, Any]) -> None:
|
||||
"""This does an insert if the given PK doesn't exist, and an
|
||||
update it if does
|
||||
|
||||
|
@ -19,8 +19,7 @@ from typing import Any, Callable
|
||||
|
||||
import settngs
|
||||
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictalker.resulttypes import ComicIssue, ComicSeries
|
||||
from comicapi.genericmetadata import ComicSeries, GenericMetadata, TagOrigin
|
||||
from comictalker.talker_utils import fix_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -108,6 +107,7 @@ class ComicTalker:
|
||||
|
||||
name: str = "Example"
|
||||
id: str = "example"
|
||||
origin: TagOrigin = TagOrigin(id, name)
|
||||
website: str = "https://example.com"
|
||||
logo_url: str = f"{website}/logo.png"
|
||||
attribution: str = f"Metadata provided by <a href='{website}'>{name}</a>"
|
||||
@ -153,6 +153,8 @@ class ComicTalker:
|
||||
|
||||
If the Talker does not use an API key it should validate that the URL works.
|
||||
If the Talker does not use an API key or URL it should check that the source is available.
|
||||
|
||||
Caching MUST NOT be implemented on this function.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -175,6 +177,8 @@ class ComicTalker:
|
||||
|
||||
A sensible amount of results should be returned.
|
||||
|
||||
Caching SHOULD be implemented on this function.
|
||||
|
||||
For example the `ComicVineTalker` stops requesting new pages after the results
|
||||
become too different from the `series_name` by use of the `titles_match` function
|
||||
provided by the `comicapi.utils` module, and only allows a maximum of 5 pages
|
||||
@ -187,6 +191,9 @@ class ComicTalker:
|
||||
"""
|
||||
This function should return an instance of GenericMetadata for a single issue.
|
||||
It is guaranteed that either `issue_id` or (`series_id` and `issue_number` is set).
|
||||
|
||||
Caching MUST be implemented on this function.
|
||||
|
||||
Below is an example of how this function might be implemented:
|
||||
|
||||
if issue_number and series_id:
|
||||
@ -198,13 +205,20 @@ class ComicTalker:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def fetch_issues_by_series(self, series_id: str) -> list[ComicIssue]:
|
||||
def fetch_series(self, series_id: str) -> ComicSeries:
|
||||
"""
|
||||
This function should return an instance of ComicSeries from the given series ID.
|
||||
Caching MUST be implemented on this function.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def fetch_issues_in_series(self, series_id: str) -> list[GenericMetadata]:
|
||||
"""Expected to return a list of issues with a given series ID"""
|
||||
raise NotImplementedError
|
||||
|
||||
def fetch_issues_by_series_issue_num_and_year(
|
||||
self, series_id_list: list[str], issue_number: str, year: int | None
|
||||
) -> list[ComicIssue]:
|
||||
) -> list[GenericMetadata]:
|
||||
"""
|
||||
This function should return a single issue for each series id in
|
||||
the `series_id_list` and it should match the issue_number.
|
||||
@ -213,5 +227,7 @@ class ComicTalker:
|
||||
|
||||
If there is no year given (`year` == None) or the Talker does not have issue publication info
|
||||
return the results unfiltered.
|
||||
|
||||
Caching SHOULD be implemented on this function.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
@ -1,59 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Credit:
|
||||
name: str
|
||||
role: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ComicSeries:
|
||||
aliases: list[str]
|
||||
count_of_issues: int | None
|
||||
count_of_volumes: int | None
|
||||
description: str
|
||||
id: str
|
||||
image_url: str
|
||||
name: str
|
||||
publisher: str
|
||||
start_year: int | None
|
||||
genres: list[str]
|
||||
format: str | None
|
||||
|
||||
def copy(self) -> ComicSeries:
|
||||
return copy.deepcopy(self)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ComicIssue:
|
||||
aliases: list[str]
|
||||
cover_date: str
|
||||
description: str
|
||||
id: str
|
||||
image_url: str
|
||||
issue_number: str
|
||||
volume: str | None
|
||||
critical_rating: float
|
||||
maturity_rating: str
|
||||
manga: str
|
||||
genres: list[str]
|
||||
tags: list[str]
|
||||
name: str
|
||||
language: str
|
||||
country: str
|
||||
site_detail_url: str
|
||||
series: ComicSeries
|
||||
alt_image_urls: list[str]
|
||||
characters: list[str]
|
||||
locations: list[str]
|
||||
credits: list[Credit]
|
||||
teams: list[str]
|
||||
story_arcs: list[str]
|
||||
complete: bool # Is this a complete ComicIssue? or is there more data to fetch
|
||||
|
||||
def copy(self) -> ComicIssue:
|
||||
return copy.deepcopy(self)
|
@ -18,11 +18,6 @@ import posixpath
|
||||
import re
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictalker.resulttypes import ComicIssue
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -38,100 +33,7 @@ def fix_url(url: str) -> str:
|
||||
return tmp_url.geturl()
|
||||
|
||||
|
||||
def map_comic_issue_to_metadata(
|
||||
issue_results: ComicIssue, source: str, remove_html_tables: bool = False, use_year_volume: bool = False
|
||||
) -> GenericMetadata:
|
||||
"""Maps ComicIssue to generic metadata"""
|
||||
metadata = GenericMetadata()
|
||||
metadata.is_empty = False
|
||||
|
||||
metadata.series = utils.xlate(issue_results.series.name)
|
||||
metadata.issue = utils.xlate(IssueString(issue_results.issue_number).as_string())
|
||||
|
||||
# Rely on comic talker to validate this number
|
||||
metadata.issue_count = utils.xlate_int(issue_results.series.count_of_issues)
|
||||
|
||||
if issue_results.series.format:
|
||||
metadata.format = issue_results.series.format
|
||||
|
||||
metadata.volume = utils.xlate_int(issue_results.volume)
|
||||
metadata.volume_count = utils.xlate_int(issue_results.series.count_of_volumes)
|
||||
|
||||
if issue_results.name:
|
||||
metadata.title = utils.xlate(issue_results.name)
|
||||
if issue_results.image_url:
|
||||
metadata.cover_image = issue_results.image_url
|
||||
|
||||
if issue_results.series.publisher:
|
||||
metadata.publisher = utils.xlate(issue_results.series.publisher)
|
||||
|
||||
if issue_results.cover_date:
|
||||
metadata.day, metadata.month, metadata.year = utils.parse_date_str(issue_results.cover_date)
|
||||
elif issue_results.series.start_year:
|
||||
metadata.year = utils.xlate_int(issue_results.series.start_year)
|
||||
|
||||
metadata.comments = cleanup_html(issue_results.description, remove_html_tables)
|
||||
if use_year_volume:
|
||||
metadata.volume = issue_results.series.start_year
|
||||
|
||||
metadata.tag_origin = source
|
||||
metadata.issue_id = issue_results.id
|
||||
metadata.web_link = issue_results.site_detail_url
|
||||
|
||||
for person in issue_results.credits:
|
||||
if person.role:
|
||||
roles = person.role.split(",")
|
||||
for role in roles:
|
||||
# can we determine 'primary' from CV??
|
||||
metadata.add_credit(person.name, role.title().strip(), False)
|
||||
|
||||
if issue_results.characters:
|
||||
metadata.characters = ", ".join(issue_results.characters)
|
||||
if issue_results.teams:
|
||||
metadata.teams = ", ".join(issue_results.teams)
|
||||
if issue_results.locations:
|
||||
metadata.locations = ", ".join(issue_results.locations)
|
||||
if issue_results.story_arcs:
|
||||
metadata.story_arc = ", ".join(issue_results.story_arcs)
|
||||
if issue_results.genres:
|
||||
metadata.genre = ", ".join(issue_results.genres)
|
||||
|
||||
if issue_results.tags:
|
||||
metadata.tags = set(issue_results.tags)
|
||||
|
||||
if issue_results.manga:
|
||||
metadata.manga = issue_results.manga
|
||||
|
||||
if issue_results.critical_rating:
|
||||
metadata.critical_rating = utils.xlate_float(issue_results.critical_rating)
|
||||
|
||||
if issue_results.maturity_rating:
|
||||
metadata.maturity_rating = issue_results.maturity_rating
|
||||
|
||||
if issue_results.language:
|
||||
metadata.language = issue_results.language
|
||||
|
||||
if issue_results.country:
|
||||
metadata.country = issue_results.country
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def parse_date_str(date_str: str) -> tuple[int | None, int | None, int | None]:
|
||||
day = None
|
||||
month = None
|
||||
year = None
|
||||
if date_str:
|
||||
parts = date_str.split("-")
|
||||
year = utils.xlate_int(parts[0])
|
||||
if len(parts) > 1:
|
||||
month = utils.xlate_int(parts[1])
|
||||
if len(parts) > 2:
|
||||
day = utils.xlate_int(parts[2])
|
||||
return day, month, year
|
||||
|
||||
|
||||
def cleanup_html(string: str, remove_html_tables: bool = False) -> str:
|
||||
def cleanup_html(string: str | None, remove_html_tables: bool = False) -> str:
|
||||
"""Cleans HTML code from any text. Will remove any HTML tables with remove_html_tables"""
|
||||
if string is None:
|
||||
return ""
|
||||
@ -195,13 +97,13 @@ def cleanup_html(string: str, remove_html_tables: bool = False) -> str:
|
||||
for row in table.findAll("tr"):
|
||||
cols = []
|
||||
col = row.findAll("td")
|
||||
i = 0
|
||||
for c in col:
|
||||
|
||||
for i, c in enumerate(col):
|
||||
item = c.string.strip()
|
||||
cols.append(item)
|
||||
if len(item) > col_widths[i]:
|
||||
col_widths[i] = len(item)
|
||||
i += 1
|
||||
|
||||
if len(cols) != 0:
|
||||
rows.append(cols)
|
||||
# now we have the data, make it into text
|
||||
@ -209,15 +111,14 @@ def cleanup_html(string: str, remove_html_tables: bool = False) -> str:
|
||||
for w in col_widths:
|
||||
fmtstr += f" {{:{w + 1}}}|"
|
||||
table_text = ""
|
||||
counter = 0
|
||||
for row in rows:
|
||||
|
||||
for counter, row in enumerate(rows):
|
||||
table_text += fmtstr.format(*row) + "\n"
|
||||
if counter == 0 and len(hdrs) != 0:
|
||||
table_text += "|"
|
||||
for w in col_widths:
|
||||
table_text += "-" * (w + 2) + "|"
|
||||
table_text += "\n"
|
||||
counter += 1
|
||||
|
||||
table_strings.append(table_text + "\n")
|
||||
|
||||
|
@ -29,13 +29,12 @@ import settngs
|
||||
from pyrate_limiter import Limiter, RequestRate
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
||||
import comictalker.talker_utils as talker_utils
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.genericmetadata import ComicSeries, Date, GenericMetadata, TagOrigin
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictalker import talker_utils
|
||||
from comictalker.comiccacher import ComicCacher
|
||||
from comictalker.comictalker import ComicTalker, TalkerDataError, TalkerNetworkError
|
||||
from comictalker.resulttypes import ComicIssue, ComicSeries, Credit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -60,27 +59,27 @@ class CVImage(TypedDict, total=False):
|
||||
|
||||
class CVAltImage(TypedDict):
|
||||
original_url: str
|
||||
id: int
|
||||
id: Required[int]
|
||||
caption: str
|
||||
image_tags: str
|
||||
|
||||
|
||||
class CVPublisher(TypedDict, total=False):
|
||||
api_detail_url: str
|
||||
id: int
|
||||
id: Required[int]
|
||||
name: Required[str]
|
||||
|
||||
|
||||
class CVCredit(TypedDict):
|
||||
api_detail_url: str
|
||||
id: int
|
||||
id: Required[int]
|
||||
name: str
|
||||
site_detail_url: str
|
||||
|
||||
|
||||
class CVPersonCredit(TypedDict):
|
||||
api_detail_url: str
|
||||
id: int
|
||||
id: Required[int]
|
||||
name: str
|
||||
site_detail_url: str
|
||||
role: str
|
||||
@ -92,7 +91,7 @@ class CVSeries(TypedDict):
|
||||
aliases: str
|
||||
count_of_issues: int
|
||||
description: str
|
||||
id: int
|
||||
id: Required[int]
|
||||
image: CVImage
|
||||
name: str
|
||||
publisher: CVPublisher
|
||||
@ -111,6 +110,7 @@ class CVIssue(TypedDict, total=False):
|
||||
character_died_in: None
|
||||
concept_credits: list[CVCredit]
|
||||
cover_date: str
|
||||
store_date: str
|
||||
date_added: str
|
||||
date_last_updated: str
|
||||
deck: None
|
||||
@ -122,7 +122,7 @@ class CVIssue(TypedDict, total=False):
|
||||
first_appearance_storyarcs: None
|
||||
first_appearance_teams: None
|
||||
has_staff_review: bool
|
||||
id: int
|
||||
id: Required[int]
|
||||
image: CVImage
|
||||
issue_number: str
|
||||
location_credits: list[CVCredit]
|
||||
@ -130,11 +130,10 @@ class CVIssue(TypedDict, total=False):
|
||||
object_credits: list[CVCredit]
|
||||
person_credits: list[CVPersonCredit]
|
||||
site_detail_url: str
|
||||
store_date: str
|
||||
story_arc_credits: list[CVCredit]
|
||||
team_credits: list[CVCredit]
|
||||
team_disbanded_in: None
|
||||
volume: CVSeries # CV uses volume to mean series
|
||||
volume: Required[CVSeries] # CV uses volume to mean series
|
||||
|
||||
|
||||
T = TypeVar("T", CVIssue, CVSeries, list[CVSeries], list[CVIssue])
|
||||
@ -160,6 +159,7 @@ default_limiter = Limiter(RequestRate(1, 5))
|
||||
class ComicVineTalker(ComicTalker):
|
||||
name: str = "Comic Vine"
|
||||
id: str = "comicvine"
|
||||
origin: TagOrigin = TagOrigin(id, name)
|
||||
website: str = "https://comicvine.gamespot.com"
|
||||
logo_url: str = f"{website}/a/bundles/comicvinesite/images/logo.png"
|
||||
attribution: str = f"Metadata provided by <a href='{website}'>{name}</a>"
|
||||
@ -170,7 +170,6 @@ class ComicVineTalker(ComicTalker):
|
||||
# Default settings
|
||||
self.default_api_url = self.api_url = f"{self.website}/api/"
|
||||
self.default_api_key = self.api_key = "27431e6787042105bd3e47e169a624521f89f3a4"
|
||||
self.remove_html_tables: bool = False
|
||||
self.use_series_start_as_volume: bool = False
|
||||
|
||||
def register_settings(self, parser: settngs.Manager) -> None:
|
||||
@ -181,13 +180,6 @@ class ComicVineTalker(ComicTalker):
|
||||
display_name="Use series start as volume",
|
||||
help="Use the series start year as the volume number",
|
||||
)
|
||||
parser.add_setting(
|
||||
"--cv-remove-html-tables",
|
||||
default=False,
|
||||
action=argparse.BooleanOptionalAction,
|
||||
display_name="Remove HTML tables",
|
||||
help="Removes html tables instead of converting them to text",
|
||||
)
|
||||
|
||||
# The default needs to be unset or None.
|
||||
# This allows this setting to be unset with the empty string, allowing the default to change
|
||||
@ -206,7 +198,6 @@ class ComicVineTalker(ComicTalker):
|
||||
settings = super().parse_settings(settings)
|
||||
|
||||
self.use_series_start_as_volume = settings["cv_use_series_start_as_volume"]
|
||||
self.remove_html_tables = settings["cv_remove_html_tables"]
|
||||
|
||||
# Set a different limit if using the default API key
|
||||
if self.api_key == self.default_api_key:
|
||||
@ -253,7 +244,7 @@ class ComicVineTalker(ComicTalker):
|
||||
# For literal searches always retrieve from online
|
||||
cvc = ComicCacher(self.cache_folder, self.version)
|
||||
if not refresh_cache and not literal:
|
||||
cached_search_results = cvc.get_search_results(self.id, series_name)
|
||||
cached_search_results = cvc.get_search_results(self.origin, series_name)
|
||||
|
||||
if len(cached_search_results) > 0:
|
||||
return cached_search_results
|
||||
@ -321,12 +312,12 @@ class ComicVineTalker(ComicTalker):
|
||||
if callback is not None:
|
||||
callback(current_result_count, total_result_count)
|
||||
|
||||
# Format result to ComicIssue
|
||||
# Format result to GenericMetadata
|
||||
formatted_search_results = self._format_search_results(search_results)
|
||||
|
||||
# Cache these search results, even if it's literal we cache the results
|
||||
# The most it will cause is extra processing time
|
||||
cvc.add_search_results(self.id, series_name, formatted_search_results)
|
||||
cvc.add_search_results(self.origin, series_name, formatted_search_results)
|
||||
|
||||
return formatted_search_results
|
||||
|
||||
@ -341,53 +332,15 @@ class ComicVineTalker(ComicTalker):
|
||||
|
||||
return comic_data
|
||||
|
||||
def fetch_issues_by_series(self, series_id: str) -> list[ComicIssue]:
|
||||
# before we search online, look in our cache, since we might already have this info
|
||||
cvc = ComicCacher(self.cache_folder, self.version)
|
||||
cached_series_issues_result = cvc.get_series_issues_info(series_id, self.id)
|
||||
def fetch_series(self, series_id: str) -> ComicSeries:
|
||||
return self._fetch_series_data(int(series_id))
|
||||
|
||||
series_data = self._fetch_series_data(int(series_id))
|
||||
|
||||
if len(cached_series_issues_result) == series_data.count_of_issues:
|
||||
return cached_series_issues_result
|
||||
|
||||
params = { # CV uses volume to mean series
|
||||
"api_key": self.api_key,
|
||||
"filter": f"volume:{series_id}",
|
||||
"format": "json",
|
||||
"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description,aliases,associated_images",
|
||||
"offset": 0,
|
||||
}
|
||||
cv_response: CVResult[list[CVIssue]] = self._get_cv_content(urljoin(self.api_url, "issues/"), params)
|
||||
|
||||
current_result_count = cv_response["number_of_page_results"]
|
||||
total_result_count = cv_response["number_of_total_results"]
|
||||
|
||||
series_issues_result = cv_response["results"]
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
# see if we need to keep asking for more pages...
|
||||
while current_result_count < total_result_count:
|
||||
page += 1
|
||||
offset += cv_response["number_of_page_results"]
|
||||
|
||||
params["offset"] = offset
|
||||
cv_response = self._get_cv_content(urljoin(self.api_url, "issues/"), params)
|
||||
|
||||
series_issues_result.extend(cv_response["results"])
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
|
||||
# Format to expected output
|
||||
formatted_series_issues_result = self._format_issue_results(series_issues_result)
|
||||
|
||||
cvc.add_series_issues_info(self.id, formatted_series_issues_result)
|
||||
|
||||
return formatted_series_issues_result
|
||||
def fetch_issues_in_series(self, series_id: str) -> list[GenericMetadata]:
|
||||
return [x[0] for x in self._fetch_issues_in_series(series_id)]
|
||||
|
||||
def fetch_issues_by_series_issue_num_and_year(
|
||||
self, series_id_list: list[str], issue_number: str, year: str | int | None
|
||||
) -> list[ComicIssue]:
|
||||
) -> list[GenericMetadata]:
|
||||
series_filter = ""
|
||||
for vid in series_id_list:
|
||||
series_filter += str(vid) + "|"
|
||||
@ -400,7 +353,7 @@ class ComicVineTalker(ComicTalker):
|
||||
params: dict[str, str | int] = { # CV uses volume to mean series
|
||||
"api_key": self.api_key,
|
||||
"format": "json",
|
||||
"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description,aliases,associated_images",
|
||||
"field_list": "id,volume,issue_number,name,image,cover_date,store_date,site_detail_url,description,aliases,associated_images",
|
||||
"filter": flt,
|
||||
}
|
||||
|
||||
@ -424,7 +377,10 @@ class ComicVineTalker(ComicTalker):
|
||||
filtered_issues_result.extend(cv_response["results"])
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
|
||||
formatted_filtered_issues_result = self._format_issue_results(filtered_issues_result)
|
||||
formatted_filtered_issues_result = [
|
||||
self.map_comic_issue_to_metadata(x, self._fetch_series_data(x["volume"]["id"]))
|
||||
for x in filtered_issues_result
|
||||
]
|
||||
|
||||
return formatted_filtered_issues_result
|
||||
|
||||
@ -446,17 +402,17 @@ class ComicVineTalker(ComicTalker):
|
||||
def _get_url_content(self, url: str, params: dict[str, Any]) -> Any:
|
||||
# if there is a 500 error, try a few more times before giving up
|
||||
limit_counter = 0
|
||||
tries = 0
|
||||
while tries < 4:
|
||||
|
||||
for tries in range(1, 5):
|
||||
try:
|
||||
resp = requests.get(url, params=params, headers={"user-agent": "comictagger/" + self.version})
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
if resp.status_code == 500:
|
||||
logger.debug(f"Try #{tries + 1}: ")
|
||||
logger.debug(f"Try #{tries}: ")
|
||||
time.sleep(1)
|
||||
logger.debug(str(resp.status_code))
|
||||
tries += 1
|
||||
|
||||
if resp.status_code == requests.status_codes.codes.TOO_MANY_REQUESTS:
|
||||
logger.info(f"{self.name} rate limit encountered. Waiting for 10 seconds\n")
|
||||
time.sleep(10)
|
||||
@ -504,7 +460,7 @@ class ComicVineTalker(ComicTalker):
|
||||
|
||||
formatted_results.append(
|
||||
ComicSeries(
|
||||
aliases=aliases.splitlines(),
|
||||
aliases=utils.split(aliases, "\n"),
|
||||
count_of_issues=record.get("count_of_issues", 0),
|
||||
count_of_volumes=None,
|
||||
description=record.get("description", ""),
|
||||
@ -520,81 +476,55 @@ class ComicVineTalker(ComicTalker):
|
||||
|
||||
return formatted_results
|
||||
|
||||
def _format_issue_results(self, issue_results: list[CVIssue], complete: bool = False) -> list[ComicIssue]:
|
||||
formatted_results = []
|
||||
for record in issue_results:
|
||||
# Extract image super
|
||||
if record.get("image") is None:
|
||||
image_url = ""
|
||||
else:
|
||||
image_url = record["image"].get("super_url", "")
|
||||
def _fetch_issues_in_series(self, series_id: str) -> list[tuple[GenericMetadata, bool]]:
|
||||
# before we search online, look in our cache, since we might already have this info
|
||||
cvc = ComicCacher(self.cache_folder, self.version)
|
||||
cached_series_issues_result = cvc.get_series_issues_info(series_id, self.origin)
|
||||
|
||||
alt_images_list = []
|
||||
for alt in record["associated_images"]:
|
||||
alt_images_list.append(alt["original_url"])
|
||||
series = self._fetch_series_data(int(series_id))
|
||||
|
||||
character_list = []
|
||||
if record.get("character_credits"):
|
||||
for char in record["character_credits"]:
|
||||
character_list.append(char["name"])
|
||||
if len(cached_series_issues_result) == series.count_of_issues:
|
||||
# Remove internal "complete" bool
|
||||
return cached_series_issues_result
|
||||
|
||||
location_list = []
|
||||
if record.get("location_credits"):
|
||||
for loc in record["location_credits"]:
|
||||
location_list.append(loc["name"])
|
||||
params = { # CV uses volume to mean series
|
||||
"api_key": self.api_key,
|
||||
"filter": f"volume:{series_id}",
|
||||
"format": "json",
|
||||
"offset": 0,
|
||||
}
|
||||
cv_response: CVResult[list[CVIssue]] = self._get_cv_content(urljoin(self.api_url, "issues/"), params)
|
||||
|
||||
teams_list = []
|
||||
if record.get("team_credits"):
|
||||
for loc in record["team_credits"]:
|
||||
teams_list.append(loc["name"])
|
||||
current_result_count = cv_response["number_of_page_results"]
|
||||
total_result_count = cv_response["number_of_total_results"]
|
||||
|
||||
story_list = []
|
||||
if record.get("story_arc_credits"):
|
||||
for loc in record["story_arc_credits"]:
|
||||
story_list.append(loc["name"])
|
||||
series_issues_result = cv_response["results"]
|
||||
page = 1
|
||||
offset = 0
|
||||
|
||||
persons_list = []
|
||||
if record.get("person_credits"):
|
||||
for person in record["person_credits"]:
|
||||
persons_list.append(Credit(name=person["name"], role=person["role"]))
|
||||
# see if we need to keep asking for more pages...
|
||||
while current_result_count < total_result_count:
|
||||
page += 1
|
||||
offset += cv_response["number_of_page_results"]
|
||||
|
||||
series = self._fetch_series_data(record["volume"]["id"])
|
||||
params["offset"] = offset
|
||||
cv_response = self._get_cv_content(urljoin(self.api_url, "issues/"), params)
|
||||
|
||||
formatted_results.append(
|
||||
ComicIssue(
|
||||
aliases=record["aliases"].split("\n") if record["aliases"] else [],
|
||||
cover_date=record.get("cover_date", ""),
|
||||
description=record.get("description", ""),
|
||||
id=str(record["id"]),
|
||||
image_url=image_url,
|
||||
issue_number=record["issue_number"],
|
||||
volume=None,
|
||||
name=record["name"],
|
||||
site_detail_url=record.get("site_detail_url", ""),
|
||||
series=series, # CV uses volume to mean series
|
||||
alt_image_urls=alt_images_list,
|
||||
characters=character_list,
|
||||
locations=location_list,
|
||||
teams=teams_list,
|
||||
story_arcs=story_list,
|
||||
critical_rating=0,
|
||||
maturity_rating="",
|
||||
manga="",
|
||||
language="",
|
||||
country="",
|
||||
genres=[],
|
||||
tags=[],
|
||||
credits=persons_list,
|
||||
complete=complete,
|
||||
)
|
||||
)
|
||||
series_issues_result.extend(cv_response["results"])
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
# Format to expected output
|
||||
formatted_series_issues_result = [
|
||||
self.map_comic_issue_to_metadata(x, self._fetch_series_data(x["volume"]["id"]))
|
||||
for x in series_issues_result
|
||||
]
|
||||
|
||||
return formatted_results
|
||||
cvc.add_series_issues_info(self.origin, formatted_series_issues_result, False)
|
||||
return [(x, False) for x in formatted_series_issues_result]
|
||||
|
||||
def _fetch_series_data(self, series_id: int) -> ComicSeries:
|
||||
# before we search online, look in our cache, since we might already have this info
|
||||
cvc = ComicCacher(self.cache_folder, self.version)
|
||||
cached_series_result = cvc.get_series_info(str(series_id), self.id)
|
||||
cached_series_result = cvc.get_series_info(str(series_id), self.origin)
|
||||
|
||||
if cached_series_result is not None:
|
||||
return cached_series_result
|
||||
@ -611,47 +541,37 @@ class ComicVineTalker(ComicTalker):
|
||||
formatted_series_results = self._format_search_results([series_results])
|
||||
|
||||
if series_results:
|
||||
cvc.add_series_info(self.id, formatted_series_results[0])
|
||||
cvc.add_series_info(self.origin, formatted_series_results[0])
|
||||
|
||||
return formatted_series_results[0]
|
||||
|
||||
def _fetch_issue_data(self, series_id: int, issue_number: str) -> GenericMetadata:
|
||||
issues_list_results = self.fetch_issues_by_series(str(series_id))
|
||||
issues_list_results = self._fetch_issues_in_series(str(series_id))
|
||||
|
||||
# Loop through issue list to find the required issue info
|
||||
f_record = None
|
||||
f_record = (GenericMetadata(), False)
|
||||
for record in issues_list_results:
|
||||
if not IssueString(issue_number).as_string():
|
||||
issue_number = "1"
|
||||
if (
|
||||
IssueString(record.issue_number).as_string().casefold()
|
||||
== IssueString(issue_number).as_string().casefold()
|
||||
):
|
||||
if IssueString(record[0].issue).as_string().casefold() == IssueString(issue_number).as_string().casefold():
|
||||
f_record = record
|
||||
break
|
||||
|
||||
if f_record and f_record.complete:
|
||||
if not f_record[0].is_empty and f_record[1]:
|
||||
# Cache had full record
|
||||
return talker_utils.map_comic_issue_to_metadata(
|
||||
f_record, self.name, self.remove_html_tables, self.use_series_start_as_volume
|
||||
)
|
||||
return f_record[0]
|
||||
|
||||
if f_record is not None:
|
||||
return self._fetch_issue_data_by_issue_id(f_record.id)
|
||||
if f_record[0].issue_id is not None:
|
||||
return self._fetch_issue_data_by_issue_id(f_record[0].issue_id)
|
||||
return GenericMetadata()
|
||||
|
||||
def _fetch_issue_data_by_issue_id(self, issue_id: str) -> GenericMetadata:
|
||||
# before we search online, look in our cache, since we might already have this info
|
||||
cvc = ComicCacher(self.cache_folder, self.version)
|
||||
cached_issues_result = cvc.get_issue_info(int(issue_id), self.id)
|
||||
cached_issues_result = cvc.get_issue_info(int(issue_id), self.origin)
|
||||
|
||||
if cached_issues_result and cached_issues_result.complete:
|
||||
return talker_utils.map_comic_issue_to_metadata(
|
||||
cached_issues_result,
|
||||
self.name,
|
||||
self.remove_html_tables,
|
||||
self.use_series_start_as_volume,
|
||||
)
|
||||
if cached_issues_result and cached_issues_result[1]:
|
||||
return cached_issues_result[0]
|
||||
|
||||
issue_url = urljoin(self.api_url, f"issue/{CVTypeID.Issue}-{issue_id}")
|
||||
params = {"api_key": self.api_key, "format": "json"}
|
||||
@ -660,17 +580,70 @@ class ComicVineTalker(ComicTalker):
|
||||
issue_results = cv_response["results"]
|
||||
|
||||
# Format to expected output
|
||||
cv_issues = self._format_issue_results([issue_results], True)
|
||||
|
||||
# Due to issue not returning publisher, fetch the series.
|
||||
cv_issues[0].series = self._fetch_series_data(int(cv_issues[0].series.id))
|
||||
|
||||
cvc.add_series_issues_info(self.id, cv_issues)
|
||||
|
||||
# Now, map the ComicIssue data to generic metadata
|
||||
return talker_utils.map_comic_issue_to_metadata(
|
||||
cv_issues[0],
|
||||
self.name,
|
||||
self.remove_html_tables,
|
||||
self.use_series_start_as_volume,
|
||||
cv_issues = self.map_comic_issue_to_metadata(
|
||||
issue_results, self._fetch_series_data(int(issue_results["volume"]["id"]))
|
||||
)
|
||||
|
||||
cvc.add_series_issues_info(self.origin, [cv_issues], True)
|
||||
|
||||
# Now, map the GenericMetadata data to generic metadata
|
||||
return cv_issues
|
||||
|
||||
def map_comic_issue_to_metadata(self, issue: CVIssue, series: ComicSeries) -> GenericMetadata:
|
||||
md = GenericMetadata(
|
||||
tag_origin=self.origin,
|
||||
issue_id=utils.xlate(issue.get("id")),
|
||||
series_id=series.id,
|
||||
title_aliases=utils.split(issue.get("aliases"), "\n"),
|
||||
publisher=utils.xlate(series.publisher),
|
||||
description=issue.get("description"),
|
||||
issue=utils.xlate(IssueString(issue.get("issue_number")).as_string()),
|
||||
issue_count=utils.xlate_int(series.count_of_issues),
|
||||
format=utils.xlate(series.format),
|
||||
volume_count=utils.xlate_int(series.count_of_volumes),
|
||||
title=utils.xlate(issue.get("name")),
|
||||
web_link=utils.xlate(issue.get("site_detail_url")),
|
||||
series=utils.xlate(series.name),
|
||||
series_aliases=series.aliases,
|
||||
cover_date=Date.parse_date(issue.get("cover_date", "")),
|
||||
store_date=Date.parse_date(issue.get("store_date", "")),
|
||||
)
|
||||
if issue.get("image") is None:
|
||||
md.cover_image = ""
|
||||
else:
|
||||
md.cover_image = issue.get("image", {}).get("super_url", "")
|
||||
|
||||
md.alternate_images = []
|
||||
for alt in issue.get("associated_images", []):
|
||||
md.alternate_images.append(alt["original_url"])
|
||||
|
||||
md.characters = []
|
||||
for character in issue.get("character_credits", []):
|
||||
md.characters.append(character["name"])
|
||||
|
||||
md.locations = []
|
||||
for location in issue.get("location_credits", []):
|
||||
md.locations.append(location["name"])
|
||||
|
||||
md.teams = []
|
||||
for team in issue.get("team_credits", []):
|
||||
md.teams.append(team["name"])
|
||||
|
||||
md.story_arcs = []
|
||||
for arc in issue.get("story_arc_credits", []):
|
||||
md.story_arcs.append(arc["name"])
|
||||
|
||||
for person in issue.get("person_credits", []):
|
||||
md.add_credit(person["name"], person["role"].title().strip(), False)
|
||||
|
||||
md.volume = utils.xlate_int(issue.get("volume"))
|
||||
if self.use_series_start_as_volume:
|
||||
md.volume = series.start_year
|
||||
|
||||
series = self._fetch_series_data(issue["volume"]["id"])
|
||||
if issue.get("cover_date"):
|
||||
md.cover_date.day, md.cover_date.month, md.cover_date.year = utils.parse_date_str(issue.get("cover_date"))
|
||||
elif series.start_year:
|
||||
md.cover_date.year = utils.xlate_int(series.start_year)
|
||||
|
||||
return md
|
||||
|
15
setup.cfg
15
setup.cfg
@ -6,7 +6,8 @@ long_description_content_type = text/markdown
|
||||
url = https://github.com/comictagger/comictagger
|
||||
author = ComicTagger team
|
||||
author_email = comictagger@gmail.com
|
||||
license = Apache License 2.0
|
||||
license = Apache-2.0
|
||||
license_files = LICENSE
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: Console
|
||||
@ -73,8 +74,12 @@ GUI =
|
||||
PyQt5
|
||||
ICU =
|
||||
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
|
||||
QTW =
|
||||
PyQt5
|
||||
PyQtWebEngine
|
||||
all =
|
||||
PyQt5
|
||||
PyQtWebEngine
|
||||
py7zr
|
||||
rarfile>=4.0
|
||||
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
|
||||
@ -96,7 +101,6 @@ basepython = {env:tox_python:python3.9}
|
||||
[testenv]
|
||||
description = run the tests with pytest
|
||||
package = wheel
|
||||
wheel_build_env = .pkg
|
||||
deps =
|
||||
pytest>=7
|
||||
extras =
|
||||
@ -112,7 +116,6 @@ commands =
|
||||
[m1env]
|
||||
description = run the tests with pytest
|
||||
package = wheel
|
||||
wheel_build_env = .pkg
|
||||
deps =
|
||||
pytest>=7
|
||||
icu,all: pyicu-binary
|
||||
@ -121,6 +124,8 @@ extras =
|
||||
cbr: CBR
|
||||
gui: GUI
|
||||
all: 7Z,CBR,GUI
|
||||
commands =
|
||||
python -m pytest {tty:--color=yes} {posargs}
|
||||
|
||||
[testenv:py3.9-{icu,all}]
|
||||
base = {env:tox_env:testenv}
|
||||
@ -129,7 +134,6 @@ base = {env:tox_env:testenv}
|
||||
labels =
|
||||
release
|
||||
build
|
||||
skip_install = true
|
||||
deps =
|
||||
black>=22
|
||||
isort>=5.10
|
||||
@ -137,8 +141,9 @@ deps =
|
||||
autoflake
|
||||
pyupgrade
|
||||
commands =
|
||||
-python ./build-tools/generate_settngs.py
|
||||
-setup-cfg-fmt setup.cfg
|
||||
-python -m autoflake -i --remove-all-unused-imports --ignore-init-module-imports .
|
||||
-python -m autoflake -i --remove-all-unused-imports --ignore-init-module-imports -r comictaggerlib comicapi comictalker testing tests build-tools
|
||||
-python -m isort --af --add-import 'from __future__ import annotations' .
|
||||
-python -m black .
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import comicapi.genericmetadata
|
||||
import comictalker.resulttypes
|
||||
from comicapi import utils
|
||||
|
||||
search_results = [
|
||||
comictalker.resulttypes.ComicSeries(
|
||||
comicapi.genericmetadata.ComicSeries(
|
||||
count_of_issues=1,
|
||||
count_of_volumes=1,
|
||||
description="this is a description",
|
||||
@ -18,7 +17,7 @@ search_results = [
|
||||
genres=[],
|
||||
format=None,
|
||||
),
|
||||
comictalker.resulttypes.ComicSeries(
|
||||
comicapi.genericmetadata.ComicSeries(
|
||||
count_of_issues=1,
|
||||
count_of_volumes=1,
|
||||
description="this is a description",
|
||||
|
@ -4,8 +4,6 @@ from typing import Any
|
||||
|
||||
import comicapi.genericmetadata
|
||||
from comicapi import utils
|
||||
from comictalker.resulttypes import ComicIssue, ComicSeries
|
||||
from comictalker.talker_utils import cleanup_html
|
||||
|
||||
|
||||
def filter_field_list(cv_result, kwargs):
|
||||
@ -158,62 +156,53 @@ cv_not_found = {
|
||||
"status_code": 101,
|
||||
"results": [],
|
||||
}
|
||||
comic_issue_result = ComicIssue(
|
||||
aliases=cv_issue_result["results"]["aliases"] or [],
|
||||
cover_date=cv_issue_result["results"]["cover_date"],
|
||||
description=cv_issue_result["results"]["description"],
|
||||
id=str(cv_issue_result["results"]["id"]),
|
||||
image_url=cv_issue_result["results"]["image"]["super_url"],
|
||||
issue_number=cv_issue_result["results"]["issue_number"],
|
||||
volume=None,
|
||||
name=cv_issue_result["results"]["name"],
|
||||
site_detail_url=cv_issue_result["results"]["site_detail_url"],
|
||||
series=ComicSeries(
|
||||
id=str(cv_issue_result["results"]["volume"]["id"]),
|
||||
name=cv_issue_result["results"]["volume"]["name"],
|
||||
aliases=[],
|
||||
count_of_issues=cv_volume_result["results"]["count_of_issues"],
|
||||
count_of_volumes=None,
|
||||
description=cv_volume_result["results"]["description"],
|
||||
image_url=cv_volume_result["results"]["image"]["super_url"],
|
||||
publisher=cv_volume_result["results"]["publisher"]["name"],
|
||||
start_year=int(cv_volume_result["results"]["start_year"]),
|
||||
genres=[],
|
||||
format=None,
|
||||
),
|
||||
characters=[],
|
||||
alt_image_urls=[],
|
||||
complete=False,
|
||||
credits=[],
|
||||
locations=[],
|
||||
story_arcs=[],
|
||||
critical_rating=0,
|
||||
maturity_rating="",
|
||||
manga="",
|
||||
language="",
|
||||
country="",
|
||||
comic_series_result = comicapi.genericmetadata.ComicSeries(
|
||||
id=str(cv_issue_result["results"]["volume"]["id"]),
|
||||
name=cv_issue_result["results"]["volume"]["name"],
|
||||
aliases=[],
|
||||
count_of_issues=cv_volume_result["results"]["count_of_issues"],
|
||||
count_of_volumes=None,
|
||||
description=cv_volume_result["results"]["description"],
|
||||
image_url=cv_volume_result["results"]["image"]["super_url"],
|
||||
publisher=cv_volume_result["results"]["publisher"]["name"],
|
||||
start_year=int(cv_volume_result["results"]["start_year"]),
|
||||
genres=[],
|
||||
tags=[],
|
||||
teams=[],
|
||||
format=None,
|
||||
)
|
||||
date = utils.parse_date_str(cv_issue_result["results"]["cover_date"])
|
||||
comic_issue_result = comicapi.genericmetadata.GenericMetadata(
|
||||
tag_origin=comicapi.genericmetadata.TagOrigin("comicvine", "Comic Vine"),
|
||||
title_aliases=cv_issue_result["results"]["aliases"] or [],
|
||||
cover_date=comicapi.genericmetadata.Date.parse_date(cv_issue_result["results"]["cover_date"]),
|
||||
store_date=comicapi.genericmetadata.Date.parse_date(cv_issue_result["results"]["store_date"]),
|
||||
description=cv_issue_result["results"]["description"],
|
||||
publisher=cv_volume_result["results"]["publisher"]["name"],
|
||||
issue_count=cv_volume_result["results"]["count_of_issues"],
|
||||
issue_id=str(cv_issue_result["results"]["id"]),
|
||||
series=cv_issue_result["results"]["volume"]["name"],
|
||||
series_id=str(cv_issue_result["results"]["volume"]["id"]),
|
||||
cover_image=cv_issue_result["results"]["image"]["super_url"],
|
||||
issue=cv_issue_result["results"]["issue_number"],
|
||||
volume=None,
|
||||
title=cv_issue_result["results"]["name"],
|
||||
web_link=cv_issue_result["results"]["site_detail_url"],
|
||||
)
|
||||
|
||||
cv_md = comicapi.genericmetadata.GenericMetadata(
|
||||
is_empty=False,
|
||||
tag_origin="Comic Vine",
|
||||
tag_origin=comicapi.genericmetadata.TagOrigin("comicvine", "Comic Vine"),
|
||||
issue_id=str(cv_issue_result["results"]["id"]),
|
||||
series=cv_issue_result["results"]["volume"]["name"],
|
||||
series_id=str(cv_issue_result["results"]["volume"]["id"]),
|
||||
issue=cv_issue_result["results"]["issue_number"],
|
||||
title=cv_issue_result["results"]["name"],
|
||||
publisher=cv_volume_result["results"]["publisher"]["name"],
|
||||
month=date[1],
|
||||
year=date[2],
|
||||
day=date[0],
|
||||
issue_count=6,
|
||||
cover_date=comicapi.genericmetadata.Date.parse_date(cv_issue_result["results"]["cover_date"]),
|
||||
issue_count=cv_volume_result["results"]["count_of_issues"],
|
||||
volume=None,
|
||||
genre=None,
|
||||
genres=[],
|
||||
language=None,
|
||||
comments=cleanup_html(cv_issue_result["results"]["description"], False),
|
||||
description=cv_issue_result["results"]["description"],
|
||||
volume_count=None,
|
||||
critical_rating=None,
|
||||
country=None,
|
||||
@ -228,14 +217,14 @@ cv_md = comicapi.genericmetadata.GenericMetadata(
|
||||
black_and_white=None,
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arc=None,
|
||||
series_group=None,
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
scan_info=None,
|
||||
characters=None,
|
||||
teams=None,
|
||||
locations=None,
|
||||
characters=[],
|
||||
teams=[],
|
||||
locations=[],
|
||||
credits=[
|
||||
comicapi.genericmetadata.CreditMetadata(person=x["name"], role=x["role"].title(), primary=False)
|
||||
comicapi.genericmetadata.Credit(person=x["name"], role=x["role"].title(), primary=False)
|
||||
for x in cv_issue_result["results"]["person_credits"]
|
||||
],
|
||||
tags=set(),
|
||||
|
@ -703,6 +703,20 @@ for p in names:
|
||||
fnames.append(tuple(pp))
|
||||
|
||||
rnames = [
|
||||
(
|
||||
"{series} {month_name}",
|
||||
False,
|
||||
"universal",
|
||||
"Cory Doctorow's Futuristic Tales of the Here and Now October.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series} {month_abbr}",
|
||||
False,
|
||||
"universal",
|
||||
"Cory Doctorow's Futuristic Tales of the Here and Now Oct.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series!c} {price} {year}", # Capitalize
|
||||
False,
|
||||
@ -872,7 +886,7 @@ rnames = [
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series} #{issue} - {locations} ({year})",
|
||||
"{series} #{issue} - {locations!j} ({year})",
|
||||
False,
|
||||
"universal",
|
||||
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - lonely cottage (2007).cbz",
|
||||
|
@ -36,9 +36,9 @@ def test_page_type_read(cbz):
|
||||
assert isinstance(md.pages[0]["Type"], str)
|
||||
|
||||
|
||||
def test_metadata_read(cbz):
|
||||
def test_metadata_read(cbz, md_saved):
|
||||
md = cbz.read_cix()
|
||||
assert md == comicapi.genericmetadata.md_test
|
||||
assert md == md_saved
|
||||
|
||||
|
||||
def test_save_cix(tmp_comic):
|
||||
@ -73,7 +73,7 @@ def test_save_cix_rar(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support")
|
||||
def test_save_cbi_rar(tmp_path):
|
||||
def test_save_cbi_rar(tmp_path, md_saved):
|
||||
cbr_path = datadir / "fake_cbr.cbr"
|
||||
shutil.copy(cbr_path, tmp_path)
|
||||
|
||||
@ -82,7 +82,7 @@ def test_save_cbi_rar(tmp_path):
|
||||
assert tmp_comic.write_cbi(comicapi.genericmetadata.md_test)
|
||||
|
||||
md = tmp_comic.read_cbi()
|
||||
assert md.replace(pages=[]) == comicapi.genericmetadata.md_test.replace(
|
||||
assert md.replace(pages=[]) == md_saved.replace(
|
||||
pages=[],
|
||||
day=None,
|
||||
alternate_series=None,
|
||||
@ -136,7 +136,7 @@ for x in entry_points(group="comicapi.archiver"):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("archiver", archivers)
|
||||
def test_copy_from_archive(archiver, tmp_path, cbz):
|
||||
def test_copy_from_archive(archiver, tmp_path, cbz, md_saved):
|
||||
comic_path = tmp_path / cbz.path.with_suffix("").name
|
||||
|
||||
archive = archiver.open(comic_path)
|
||||
@ -149,7 +149,7 @@ def test_copy_from_archive(archiver, tmp_path, cbz):
|
||||
assert set(cbz.archiver.get_filename_list()) == set(comic_archive.archiver.get_filename_list())
|
||||
|
||||
md = comic_archive.read_cix()
|
||||
assert md == comicapi.genericmetadata.md_test
|
||||
assert md == md_saved
|
||||
|
||||
|
||||
def test_rename(tmp_comic, tmp_path):
|
||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
import comictalker.comiccacher
|
||||
from comicapi.genericmetadata import TagOrigin
|
||||
from testing.comicdata import search_results
|
||||
|
||||
|
||||
@ -13,13 +14,13 @@ def test_create_cache(config, mock_version):
|
||||
|
||||
|
||||
def test_search_results(comic_cache):
|
||||
comic_cache.add_search_results("test", "test search", search_results)
|
||||
assert search_results == comic_cache.get_search_results("test", "test search")
|
||||
comic_cache.add_search_results(TagOrigin("test", "test"), "test search", search_results)
|
||||
assert search_results == comic_cache.get_search_results(TagOrigin("test", "test"), "test search")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("series_info", search_results)
|
||||
def test_series_info(comic_cache, series_info):
|
||||
comic_cache.add_series_info(series_record=series_info, source_name="test")
|
||||
comic_cache.add_series_info(series=series_info, source=TagOrigin("test", "test"))
|
||||
vi = series_info.copy()
|
||||
cache_result = comic_cache.get_series_info(series_id=series_info.id, source_name="test")
|
||||
cache_result = comic_cache.get_series_info(series_id=series_info.id, source=TagOrigin("test", "test"))
|
||||
assert vi == cache_result
|
||||
|
@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
import comicapi.genericmetadata
|
||||
@ -11,7 +9,7 @@ import testing.comicvine
|
||||
def test_search_for_series(comicvine_api, comic_cache):
|
||||
results = comicvine_api.search_for_series("cory doctorows futuristic tales of the here and now")
|
||||
cache_issues = comic_cache.get_search_results(
|
||||
comicvine_api.id, "cory doctorows futuristic tales of the here and now"
|
||||
comicvine_api.origin, "cory doctorows futuristic tales of the here and now"
|
||||
)
|
||||
assert results == cache_issues
|
||||
|
||||
@ -20,16 +18,16 @@ def test_fetch_series_data(comicvine_api, comic_cache):
|
||||
result = comicvine_api._fetch_series_data(23437)
|
||||
# del result["description"]
|
||||
# del result["image_url"]
|
||||
cache_result = comic_cache.get_series_info(23437, comicvine_api.id)
|
||||
cache_result = comic_cache.get_series_info(23437, comicvine_api.origin)
|
||||
# del cache_result["description"]
|
||||
# del cache_result["image_url"]
|
||||
assert result == cache_result
|
||||
|
||||
|
||||
def test_fetch_issues_by_series(comicvine_api, comic_cache):
|
||||
results = comicvine_api.fetch_issues_by_series(23437)
|
||||
cache_issues = comic_cache.get_series_issues_info(23437, comicvine_api.id)
|
||||
assert dataclasses.asdict(results[0])["series"] == dataclasses.asdict(cache_issues[0])["series"]
|
||||
def test_fetch_issues_in_series(comicvine_api, comic_cache):
|
||||
results = comicvine_api.fetch_issues_in_series(23437)
|
||||
cache_issues = comic_cache.get_series_issues_info(23437, comicvine_api.origin)
|
||||
assert results[0] == cache_issues[0][0]
|
||||
|
||||
|
||||
def test_fetch_issue_data_by_issue_id(comicvine_api):
|
||||
@ -38,7 +36,7 @@ def test_fetch_issue_data_by_issue_id(comicvine_api):
|
||||
assert result == testing.comicvine.cv_md
|
||||
|
||||
|
||||
def test_fetch_issues_by_series_issue_num_and_year(comicvine_api):
|
||||
def test_fetch_issues_in_series_issue_num_and_year(comicvine_api):
|
||||
results = comicvine_api.fetch_issues_by_series_issue_num_and_year([23437], "1", None)
|
||||
cv_expected = testing.comicvine.comic_issue_result.copy()
|
||||
|
||||
|
@ -9,7 +9,9 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import settngs
|
||||
from PIL import Image
|
||||
from pyrate_limiter import Limiter, RequestRate
|
||||
|
||||
import comicapi.comicarchive
|
||||
import comicapi.genericmetadata
|
||||
@ -110,11 +112,18 @@ def comicvine_api(monkeypatch, cbz, comic_cache, mock_version, config) -> comict
|
||||
|
||||
# apply the monkeypatch for requests.get to mock_get
|
||||
monkeypatch.setattr(requests, "get", m_get)
|
||||
monkeypatch.setattr(comictalker.talkers.comicvine, "custom_limiter", Limiter(RequestRate(100, 1)))
|
||||
monkeypatch.setattr(comictalker.talkers.comicvine, "default_limiter", Limiter(RequestRate(100, 1)))
|
||||
|
||||
cv = comictalker.talkers.comicvine.ComicVineTalker(
|
||||
version=mock_version[0],
|
||||
cache_folder=config[0].runtime_config.user_cache_dir,
|
||||
)
|
||||
manager = settngs.Manager()
|
||||
manager.add_persistent_group("comicvine", cv.register_settings)
|
||||
cfg, _ = manager.defaults()
|
||||
cfg["comicvine"]["comicvine_key"] = "testing"
|
||||
cv.parse_settings(cfg["comicvine"])
|
||||
return cv
|
||||
|
||||
|
||||
@ -135,6 +144,11 @@ def md():
|
||||
yield comicapi.genericmetadata.md_test.copy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def md_saved(md):
|
||||
yield md.replace(tag_origin=None, issue_id=None, series_id=None)
|
||||
|
||||
|
||||
# manually seeds publishers
|
||||
@pytest.fixture
|
||||
def seed_publishers(monkeypatch):
|
||||
|
@ -26,7 +26,7 @@ def test_add_credit():
|
||||
md = comicapi.genericmetadata.GenericMetadata()
|
||||
|
||||
md.add_credit(person="test", role="writer", primary=False)
|
||||
assert md.credits == [comicapi.genericmetadata.CreditMetadata(person="test", role="writer", primary=False)]
|
||||
assert md.credits == [comicapi.genericmetadata.Credit(person="test", role="writer", primary=False)]
|
||||
|
||||
|
||||
def test_add_credit_primary():
|
||||
@ -34,7 +34,7 @@ def test_add_credit_primary():
|
||||
|
||||
md.add_credit(person="test", role="writer", primary=False)
|
||||
md.add_credit(person="test", role="writer", primary=True)
|
||||
assert md.credits == [comicapi.genericmetadata.CreditMetadata(person="test", role="writer", primary=True)]
|
||||
assert md.credits == [comicapi.genericmetadata.Credit(person="test", role="writer", primary=True)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("md, role, expected", credits)
|
||||
|
@ -6,6 +6,7 @@ import pytest
|
||||
from PIL import Image
|
||||
|
||||
import comicapi.comicarchive
|
||||
import comicapi.genericmetadata
|
||||
import comicapi.issuestring
|
||||
import comictaggerlib.issueidentifier
|
||||
import testing.comicdata
|
||||
@ -37,13 +38,8 @@ def test_get_issue_cover_match_score(cbz, config, comicvine_api):
|
||||
config, definitions = config
|
||||
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, config, comicvine_api)
|
||||
score = ii.get_issue_cover_match_score(
|
||||
int(
|
||||
comicapi.issuestring.IssueString(
|
||||
cbz.read_metadata(comicapi.comicarchive.MetaDataStyle.CIX).issue
|
||||
).as_float()
|
||||
),
|
||||
"https://comicvine.gamespot.com/a/uploads/scale_large/0/574/585444-109004_20080707014047_large.jpg",
|
||||
"https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-140529/",
|
||||
["https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-140529/"],
|
||||
[ii.calculate_hash(cbz.get_page(0))],
|
||||
)
|
||||
expected = {
|
||||
@ -67,8 +63,12 @@ def test_search(cbz, config, comicvine_api):
|
||||
"issue_title": testing.comicvine.cv_issue_result["results"]["name"],
|
||||
"issue_id": str(testing.comicvine.cv_issue_result["results"]["id"]),
|
||||
"series_id": str(testing.comicvine.cv_volume_result["results"]["id"]),
|
||||
"month": testing.comicvine.date[1],
|
||||
"year": testing.comicvine.date[2],
|
||||
"month": comicapi.genericmetadata.Date.parse_date(
|
||||
testing.comicvine.cv_issue_result["results"]["cover_date"]
|
||||
).month,
|
||||
"year": comicapi.genericmetadata.Date.parse_date(
|
||||
testing.comicvine.cv_issue_result["results"]["cover_date"]
|
||||
).year,
|
||||
"publisher": testing.comicvine.cv_volume_result["results"]["publisher"]["name"],
|
||||
"image_url": testing.comicvine.cv_issue_result["results"]["image"]["super_url"],
|
||||
"description": testing.comicvine.cv_issue_result["results"]["description"],
|
||||
|
@ -5,27 +5,27 @@ import comicapi.comicinfoxml
|
||||
import comicapi.genericmetadata
|
||||
|
||||
|
||||
def test_cix():
|
||||
def test_cix(md_saved):
|
||||
CIX = comicapi.comicinfoxml.ComicInfoXml()
|
||||
string = CIX.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CIX.metadata_from_string(string)
|
||||
assert md == comicapi.genericmetadata.md_test
|
||||
assert md == md_saved
|
||||
|
||||
|
||||
def test_cbi():
|
||||
def test_cbi(md_saved):
|
||||
CBI = comicapi.comicbookinfo.ComicBookInfo()
|
||||
string = CBI.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CBI.metadata_from_string(string)
|
||||
md_test = comicapi.genericmetadata.md_test.replace(
|
||||
day=None,
|
||||
md_test = md_saved.replace(
|
||||
cover_date=md_saved.cover_date.replace(day=None),
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arc=None,
|
||||
series_group=None,
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
scan_info=None,
|
||||
characters=None,
|
||||
teams=None,
|
||||
locations=None,
|
||||
characters=[],
|
||||
teams=[],
|
||||
locations=[],
|
||||
pages=[],
|
||||
alternate_series=None,
|
||||
alternate_number=None,
|
||||
@ -39,17 +39,17 @@ def test_cbi():
|
||||
assert md == md_test
|
||||
|
||||
|
||||
def test_comet():
|
||||
def test_comet(md_saved):
|
||||
CBI = comicapi.comet.CoMet()
|
||||
string = CBI.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CBI.metadata_from_string(string)
|
||||
md_test = comicapi.genericmetadata.md_test.replace(
|
||||
day=None,
|
||||
story_arc=None,
|
||||
series_group=None,
|
||||
md_test = md_saved.replace(
|
||||
cover_date=md_saved.cover_date.replace(day=None),
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
scan_info=None,
|
||||
teams=None,
|
||||
locations=None,
|
||||
teams=[],
|
||||
locations=[],
|
||||
pages=[],
|
||||
alternate_series=None,
|
||||
alternate_number=None,
|
||||
|
Loading…
x
Reference in New Issue
Block a user