Compare commits
1 Commits
1.6.0-alph
...
store_date
Author | SHA1 | Date | |
---|---|---|---|
54d733ef74 |
@ -14,12 +14,12 @@ repos:
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.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.10.1
|
||||
rev: v3.8.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
@ -29,16 +29,16 @@ repos:
|
||||
- id: isort
|
||||
args: [--af,--add-import, 'from __future__ import annotations']
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.7.0
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-length, flake8-print]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.5.1
|
||||
rev: v1.4.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-setuptools, types-requests, settngs>=0.7.1]
|
||||
|
@ -9,7 +9,6 @@ import comictaggerlib.main
|
||||
|
||||
def generate() -> str:
|
||||
app = comictaggerlib.main.App()
|
||||
app.load_plugins(app.initial_arg_parser.parse_known_args()[0])
|
||||
app.register_settings()
|
||||
return settngs.generate_ns(app.manager.definitions)
|
||||
|
||||
|
@ -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__)
|
||||
|
||||
@ -85,11 +85,7 @@ class CoMet:
|
||||
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)
|
||||
|
||||
@ -154,7 +150,7 @@ class CoMet:
|
||||
md.identifier = utils.xlate(get("identifier"))
|
||||
md.last_mark = utils.xlate(get("lastMark"))
|
||||
|
||||
_, 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"))
|
||||
|
||||
|
@ -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,8 +85,9 @@ 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.description = utils.xlate(cbi["comments"])
|
||||
metadata.genres = utils.split(cbi["genre"], ",")
|
||||
@ -148,8 +149,8 @@ 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.description))
|
||||
assign("genre", utils.xlate(",".join(metadata.genres)))
|
||||
|
@ -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__)
|
||||
|
||||
@ -99,9 +99,10 @@ class ComicInfoXml:
|
||||
assign("AlternateCount", md.alternate_count)
|
||||
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
|
||||
@ -203,9 +204,9 @@ class ComicInfoXml:
|
||||
md.alternate_count = utils.xlate_int(get("AlternateCount"))
|
||||
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.genres = utils.split(get("Genre"), ",")
|
||||
|
@ -20,6 +20,7 @@ possible, however lossy it might be
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import copy
|
||||
import dataclasses
|
||||
import logging
|
||||
@ -91,6 +92,62 @@ class TagOrigin(NamedTuple):
|
||||
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"]
|
||||
@ -112,9 +169,8 @@ class GenericMetadata:
|
||||
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
|
||||
genres: list[str] = dataclasses.field(default_factory=list)
|
||||
@ -172,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
|
||||
|
||||
@ -179,51 +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)
|
||||
elif isinstance(new, list) and len(new) == 0:
|
||||
pass
|
||||
else:
|
||||
setattr(self, cur, new)
|
||||
|
||||
if not new_md.is_empty:
|
||||
self.is_empty = False
|
||||
|
||||
assign("series", new_md.series)
|
||||
assign("series_id", new_md.series_id)
|
||||
assign("issue", new_md.issue)
|
||||
assign("issue_id", new_md.issue_id)
|
||||
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("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("scan_info", new_md.scan_info)
|
||||
assign("description", new_md.description)
|
||||
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
|
||||
@ -233,16 +298,16 @@ class GenericMetadata:
|
||||
|
||||
# For now, go the easy route, where any overlay
|
||||
# value wipes out the whole list
|
||||
assign("series_aliases", new_md.series_aliases)
|
||||
assign("title_aliases", new_md.title_aliases)
|
||||
assign("genres", new_md.genres)
|
||||
assign("story_arcs", new_md.story_arcs)
|
||||
assign("series_groups", new_md.series_groups)
|
||||
assign("characters", new_md.characters)
|
||||
assign("teams", new_md.teams)
|
||||
assign("locations", new_md.locations)
|
||||
assign("tags", new_md.tags)
|
||||
assign("pages", new_md.pages)
|
||||
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)
|
||||
|
||||
def overlay_credits(self, new_credits: list[Credit]) -> None:
|
||||
for c in new_credits:
|
||||
@ -332,7 +397,7 @@ class GenericMetadata:
|
||||
add_attr_string("day")
|
||||
add_attr_string("volume")
|
||||
add_attr_string("volume_count")
|
||||
add_attr_string("genres")
|
||||
add_attr_string("genre")
|
||||
add_attr_string("language")
|
||||
add_attr_string("country")
|
||||
add_attr_string("critical_rating")
|
||||
@ -353,13 +418,13 @@ class GenericMetadata:
|
||||
if self.black_and_white:
|
||||
add_attr_string("black_and_white")
|
||||
add_attr_string("maturity_rating")
|
||||
add_attr_string("story_arcs")
|
||||
add_attr_string("series_groups")
|
||||
add_attr_string("story_arc")
|
||||
add_attr_string("series_group")
|
||||
add_attr_string("scan_info")
|
||||
add_attr_string("characters")
|
||||
add_attr_string("teams")
|
||||
add_attr_string("locations")
|
||||
add_attr_string("description")
|
||||
add_attr_string("comments")
|
||||
add_attr_string("notes")
|
||||
|
||||
add_string("tags", ", ".join(self.tags))
|
||||
@ -412,9 +477,7 @@ md_test: GenericMetadata = GenericMetadata(
|
||||
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,
|
||||
genres=["Sci-Fi"],
|
||||
|
@ -52,7 +52,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
self.current_match_set: MultipleMatch = match_set_list[0]
|
||||
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode, config.Runtime_Options_config.user_cache_dir, talker
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
@ -233,10 +233,10 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
md = ca.read_metadata(self._style)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.config.Filename_Parsing_complicated_parser,
|
||||
self.config.Filename_Parsing_remove_c2c,
|
||||
self.config.Filename_Parsing_remove_fcbd,
|
||||
self.config.Filename_Parsing_remove_publisher,
|
||||
self.config.filename_complicated_parser,
|
||||
self.config.filename_remove_c2c,
|
||||
self.config.filename_remove_fcbd,
|
||||
self.config.filename_remove_publisher,
|
||||
)
|
||||
|
||||
# now get the particular issue data
|
||||
|
@ -40,15 +40,15 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
|
||||
self.cbxSpecifySearchString.setChecked(False)
|
||||
self.cbxSplitWords.setChecked(False)
|
||||
self.sbNameMatchSearchThresh.setValue(self.config.Issue_Identifier_series_match_identify_thresh)
|
||||
self.sbNameMatchSearchThresh.setValue(self.config.identifier_series_match_identify_thresh)
|
||||
self.leSearchString.setEnabled(False)
|
||||
|
||||
self.cbxSaveOnLowConfidence.setChecked(self.config.Auto_Tag_save_on_low_confidence)
|
||||
self.cbxDontUseYear.setChecked(self.config.Auto_Tag_dont_use_year_when_identifying)
|
||||
self.cbxAssumeIssueOne.setChecked(self.config.Auto_Tag_assume_1_if_no_issue_num)
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.Auto_Tag_ignore_leading_numbers_in_filename)
|
||||
self.cbxRemoveAfterSuccess.setChecked(self.config.Auto_Tag_remove_archive_after_successful_match)
|
||||
self.cbxAutoImprint.setChecked(self.config.Issue_Identifier_auto_imprint)
|
||||
self.cbxSaveOnLowConfidence.setChecked(self.config.autotag_save_on_low_confidence)
|
||||
self.cbxDontUseYear.setChecked(self.config.autotag_dont_use_year_when_identifying)
|
||||
self.cbxAssumeIssueOne.setChecked(self.config.autotag_assume_1_if_no_issue_num)
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.autotag_ignore_leading_numbers_in_filename)
|
||||
self.cbxRemoveAfterSuccess.setChecked(self.config.autotag_remove_archive_after_successful_match)
|
||||
self.cbxAutoImprint.setChecked(self.config.identifier_auto_imprint)
|
||||
|
||||
nlmt_tip = """<html>The <b>Name Match Ratio Threshold: Auto-Identify</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The lower
|
||||
@ -73,7 +73,7 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.ignore_leading_digits_in_filename = False
|
||||
self.remove_after_success = False
|
||||
self.search_string = ""
|
||||
self.name_length_match_tolerance = self.config.Issue_Identifier_series_match_search_thresh
|
||||
self.name_length_match_tolerance = self.config.identifier_series_match_search_thresh
|
||||
self.split_words = self.cbxSplitWords.isChecked()
|
||||
|
||||
def search_string_toggle(self) -> None:
|
||||
@ -92,11 +92,11 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.split_words = self.cbxSplitWords.isChecked()
|
||||
|
||||
# persist some settings
|
||||
self.config.Auto_Tag_save_on_low_confidence = self.auto_save_on_low
|
||||
self.config.Auto_Tag_dont_use_year_when_identifying = self.dont_use_year
|
||||
self.config.Auto_Tag_assume_1_if_no_issue_num = self.assume_issue_one
|
||||
self.config.Auto_Tag_ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
|
||||
self.config.Auto_Tag_remove_archive_after_successful_match = self.remove_after_success
|
||||
self.config.autotag_save_on_low_confidence = self.auto_save_on_low
|
||||
self.config.autotag_dont_use_year_when_identifying = self.dont_use_year
|
||||
self.config.autotag_assume_1_if_no_issue_num = self.assume_issue_one
|
||||
self.config.autotag_ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
|
||||
self.config.autotag_remove_archive_after_successful_match = self.remove_after_success
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.search_string = self.leSearchString.text()
|
||||
|
@ -29,7 +29,7 @@ class CBLTransformer:
|
||||
self.config = config
|
||||
|
||||
def apply(self) -> GenericMetadata:
|
||||
if self.config.Comic_Book_Lover_assume_lone_credit_is_primary:
|
||||
if self.config.cbl_assume_lone_credit_is_primary:
|
||||
# helper
|
||||
def set_lone_primary(role_list: list[str]) -> tuple[Credit | None, int]:
|
||||
lone_credit: Credit | None = None
|
||||
@ -55,19 +55,19 @@ class CBLTransformer:
|
||||
c["primary"] = False
|
||||
self.metadata.add_credit(c["person"], "Artist", True)
|
||||
|
||||
if self.config.Comic_Book_Lover_copy_characters_to_tags:
|
||||
if self.config.cbl_copy_characters_to_tags:
|
||||
self.metadata.tags.update(x for x in self.metadata.characters)
|
||||
|
||||
if self.config.Comic_Book_Lover_copy_teams_to_tags:
|
||||
if self.config.cbl_copy_teams_to_tags:
|
||||
self.metadata.tags.update(x for x in self.metadata.teams)
|
||||
|
||||
if self.config.Comic_Book_Lover_copy_locations_to_tags:
|
||||
if self.config.cbl_copy_locations_to_tags:
|
||||
self.metadata.tags.update(x for x in self.metadata.locations)
|
||||
|
||||
if self.config.Comic_Book_Lover_copy_storyarcs_to_tags:
|
||||
if self.config.cbl_copy_storyarcs_to_tags:
|
||||
self.metadata.tags.update(x for x in self.metadata.story_arcs)
|
||||
|
||||
if self.config.Comic_Book_Lover_copy_notes_to_comments:
|
||||
if self.config.cbl_copy_notes_to_comments:
|
||||
if self.metadata.notes is not None:
|
||||
if self.metadata.description is None:
|
||||
self.metadata.description = ""
|
||||
@ -76,7 +76,7 @@ class CBLTransformer:
|
||||
if self.metadata.notes not in self.metadata.description:
|
||||
self.metadata.description += self.metadata.notes
|
||||
|
||||
if self.config.Comic_Book_Lover_copy_weblink_to_comments:
|
||||
if self.config.cbl_copy_weblink_to_comments:
|
||||
if self.metadata.web_link is not None:
|
||||
if self.metadata.description is None:
|
||||
self.metadata.description = ""
|
||||
|
@ -16,10 +16,12 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pprint import pprint
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
@ -44,9 +46,9 @@ class CLI:
|
||||
self.batch_mode = False
|
||||
|
||||
def current_talker(self) -> ComicTalker:
|
||||
if self.config.Sources_source in self.talkers:
|
||||
return self.talkers[self.config.Sources_source]
|
||||
logger.error("Could not find the '%s' talker", self.config.Sources_source)
|
||||
if self.config.talker_source in self.talkers:
|
||||
return self.talkers[self.config.talker_source]
|
||||
logger.error("Could not find the '%s' talker", self.config.talker_source)
|
||||
raise SystemExit(2)
|
||||
|
||||
def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata:
|
||||
@ -57,14 +59,14 @@ class CLI:
|
||||
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
|
||||
return GenericMetadata()
|
||||
|
||||
if self.config.Comic_Book_Lover_apply_transform_on_import:
|
||||
if self.config.cbl_apply_transform_on_import:
|
||||
ct_md = CBLTransformer(ct_md, self.config).apply()
|
||||
|
||||
return ct_md
|
||||
|
||||
def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool:
|
||||
if not self.config.Runtime_Options_dryrun:
|
||||
for metadata_style in self.config.Runtime_Options_type:
|
||||
if not self.config.runtime_dryrun:
|
||||
for metadata_style in self.config.runtime_type:
|
||||
# write out the new data
|
||||
if not ca.write_metadata(md, metadata_style):
|
||||
logger.error("The tag save seemed to fail for style: %s!", MetaDataStyle.name[metadata_style])
|
||||
@ -73,7 +75,7 @@ class CLI:
|
||||
print("Save complete.")
|
||||
logger.info("Save complete.")
|
||||
else:
|
||||
if self.config.Runtime_Options_quiet:
|
||||
if self.config.runtime_quiet:
|
||||
logger.info("dry-run option was set, so nothing was written")
|
||||
print("dry-run option was set, so nothing was written")
|
||||
else:
|
||||
@ -100,7 +102,7 @@ class CLI:
|
||||
m["issue_title"],
|
||||
)
|
||||
)
|
||||
if self.config.Runtime_Options_interactive:
|
||||
if self.config.runtime_interactive:
|
||||
while True:
|
||||
i = input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
|
||||
@ -111,7 +113,7 @@ class CLI:
|
||||
ca = match_set.ca
|
||||
md = self.create_local_metadata(ca)
|
||||
ct_md = self.actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"])
|
||||
if self.config.Issue_Identifier_clear_metadata_on_import:
|
||||
if self.config.identifier_clear_metadata_on_import:
|
||||
md = ct_md
|
||||
else:
|
||||
notes = (
|
||||
@ -120,14 +122,14 @@ class CLI:
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if self.config.Issue_Identifier_auto_imprint:
|
||||
if self.config.identifier_auto_imprint:
|
||||
md.fix_publisher()
|
||||
|
||||
self.actual_metadata_save(ca, md)
|
||||
|
||||
def post_process_matches(self, match_results: OnlineMatchResults) -> None:
|
||||
# now go through the match results
|
||||
if self.config.Runtime_Options_summary:
|
||||
if self.config.runtime_summary:
|
||||
if len(match_results.good_matches) > 0:
|
||||
print("\nSuccessful matches:\n------------------")
|
||||
for f in match_results.good_matches:
|
||||
@ -148,7 +150,7 @@ class CLI:
|
||||
for f in match_results.fetch_data_failures:
|
||||
print(f)
|
||||
|
||||
if not self.config.Runtime_Options_summary and not self.config.Runtime_Options_interactive:
|
||||
if not self.config.runtime_summary and not self.config.runtime_interactive:
|
||||
# just quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
@ -168,41 +170,38 @@ class CLI:
|
||||
self.display_match_set_for_choice(label, match_set)
|
||||
|
||||
def run(self) -> None:
|
||||
if len(self.config.Runtime_Options_files) < 1:
|
||||
if len(self.config.runtime_files) < 1:
|
||||
logger.error("You must specify at least one filename. Use the -h option for more info")
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
self.batch_mode = len(self.config.Runtime_Options_files) > 1
|
||||
self.batch_mode = len(self.config.runtime_files) > 1
|
||||
|
||||
for f in self.config.Runtime_Options_files:
|
||||
for f in self.config.runtime_files:
|
||||
self.process_file_cli(f, match_results)
|
||||
sys.stdout.flush()
|
||||
|
||||
self.post_process_matches(match_results)
|
||||
|
||||
if self.config.Runtime_Options_online:
|
||||
print(
|
||||
f"\nFiles tagged with metadata provided by {self.current_talker().name} {self.current_talker().website}"
|
||||
)
|
||||
print(f"\nFiles tagged with metadata provided by {self.current_talker().name} {self.current_talker().website}")
|
||||
|
||||
def create_local_metadata(self, ca: ComicArchive) -> GenericMetadata:
|
||||
md = GenericMetadata()
|
||||
md.set_default_page_list(ca.get_number_of_pages())
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if self.config.Runtime_Options_parse_filename:
|
||||
if self.config.runtime_parse_filename:
|
||||
f_md = ca.metadata_from_filename(
|
||||
self.config.Filename_Parsing_complicated_parser,
|
||||
self.config.Filename_Parsing_remove_c2c,
|
||||
self.config.Filename_Parsing_remove_fcbd,
|
||||
self.config.Filename_Parsing_remove_publisher,
|
||||
self.config.Runtime_Options_split_words,
|
||||
self.config.filename_complicated_parser,
|
||||
self.config.filename_remove_c2c,
|
||||
self.config.filename_remove_fcbd,
|
||||
self.config.filename_remove_publisher,
|
||||
self.config.runtime_split_words,
|
||||
)
|
||||
|
||||
md.overlay(f_md)
|
||||
|
||||
for metadata_style in self.config.Runtime_Options_type:
|
||||
for metadata_style in self.config.runtime_type:
|
||||
if ca.has_metadata(metadata_style):
|
||||
try:
|
||||
t_md = ca.read_metadata(metadata_style)
|
||||
@ -212,12 +211,12 @@ class CLI:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
# finally, use explicit stuff
|
||||
md.overlay(self.config.Runtime_Options_metadata)
|
||||
md.overlay(self.config.runtime_metadata)
|
||||
|
||||
return md
|
||||
|
||||
def print(self, ca: ComicArchive) -> None:
|
||||
if not self.config.Runtime_Options_type:
|
||||
if not self.config.runtime_type:
|
||||
page_count = ca.get_number_of_pages()
|
||||
|
||||
brief = ""
|
||||
@ -247,59 +246,49 @@ class CLI:
|
||||
|
||||
print(brief)
|
||||
|
||||
if self.config.Runtime_Options_quiet:
|
||||
if self.config.runtime_quiet:
|
||||
return
|
||||
|
||||
print()
|
||||
|
||||
raw: str | bytes = ""
|
||||
if not self.config.Runtime_Options_type or MetaDataStyle.CIX in self.config.Runtime_Options_type:
|
||||
if not self.config.runtime_type or MetaDataStyle.CIX in self.config.runtime_type:
|
||||
if ca.has_metadata(MetaDataStyle.CIX):
|
||||
print("--------- ComicRack tags ---------")
|
||||
try:
|
||||
if self.config.Runtime_Options_raw:
|
||||
raw = ca.read_raw_cix()
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
print(raw)
|
||||
if self.config.runtime_raw:
|
||||
print(ca.read_raw_cix())
|
||||
else:
|
||||
print(ca.read_cix())
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if not self.config.Runtime_Options_type or MetaDataStyle.CBI in self.config.Runtime_Options_type:
|
||||
if not self.config.runtime_type or MetaDataStyle.CBI in self.config.runtime_type:
|
||||
if ca.has_metadata(MetaDataStyle.CBI):
|
||||
print("------- ComicBookLover tags -------")
|
||||
try:
|
||||
if self.config.Runtime_Options_raw:
|
||||
raw = ca.read_raw_cbi()
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
print(raw)
|
||||
if self.config.runtime_raw:
|
||||
pprint(json.loads(ca.read_raw_cbi()))
|
||||
else:
|
||||
print(ca.read_cbi())
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if not self.config.Runtime_Options_type or MetaDataStyle.COMET in self.config.Runtime_Options_type:
|
||||
if not self.config.runtime_type or MetaDataStyle.COMET in self.config.runtime_type:
|
||||
if ca.has_metadata(MetaDataStyle.COMET):
|
||||
print("----------- CoMet tags -----------")
|
||||
try:
|
||||
if self.config.Runtime_Options_raw:
|
||||
raw = ca.read_raw_comet()
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode("utf-8")
|
||||
print(raw)
|
||||
if self.config.runtime_raw:
|
||||
print(ca.read_raw_comet())
|
||||
else:
|
||||
print(ca.read_comet())
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
def delete(self, ca: ComicArchive) -> None:
|
||||
for metadata_style in self.config.Runtime_Options_type:
|
||||
for metadata_style in self.config.runtime_type:
|
||||
style_name = MetaDataStyle.name[metadata_style]
|
||||
if ca.has_metadata(metadata_style):
|
||||
if not self.config.Runtime_Options_dryrun:
|
||||
if not self.config.runtime_dryrun:
|
||||
if not ca.remove_metadata(metadata_style):
|
||||
print(f"{ca.path}: Tag removal seemed to fail!")
|
||||
else:
|
||||
@ -310,25 +299,25 @@ class CLI:
|
||||
print(f"{ca.path}: This archive doesn't have {style_name} tags to remove.")
|
||||
|
||||
def copy(self, ca: ComicArchive) -> None:
|
||||
for metadata_style in self.config.Runtime_Options_type:
|
||||
for metadata_style in self.config.runtime_type:
|
||||
dst_style_name = MetaDataStyle.name[metadata_style]
|
||||
if not self.config.Runtime_Options_overwrite and ca.has_metadata(metadata_style):
|
||||
if not self.config.runtime_overwrite and ca.has_metadata(metadata_style):
|
||||
print(f"{ca.path}: Already has {dst_style_name} tags. Not overwriting.")
|
||||
return
|
||||
if self.config.Commands_copy == metadata_style:
|
||||
if self.config.commands_copy == metadata_style:
|
||||
print(f"{ca.path}: Destination and source are same: {dst_style_name}. Nothing to do.")
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[self.config.Commands_copy]
|
||||
if ca.has_metadata(self.config.Commands_copy):
|
||||
if not self.config.Runtime_Options_dryrun:
|
||||
src_style_name = MetaDataStyle.name[self.config.commands_copy]
|
||||
if ca.has_metadata(self.config.commands_copy):
|
||||
if not self.config.runtime_dryrun:
|
||||
try:
|
||||
md = ca.read_metadata(self.config.Commands_copy)
|
||||
md = ca.read_metadata(self.config.commands_copy)
|
||||
except Exception as e:
|
||||
md = GenericMetadata()
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if self.config.Comic_Book_Lover_apply_transform_on_bulk_operation == MetaDataStyle.CBI:
|
||||
if self.config.cbl_apply_transform_on_bulk_operation == MetaDataStyle.CBI:
|
||||
md = CBLTransformer(md, self.config).apply()
|
||||
|
||||
if not ca.write_metadata(md, metadata_style):
|
||||
@ -341,8 +330,8 @@ class CLI:
|
||||
print(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.")
|
||||
|
||||
def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> None:
|
||||
if not self.config.Runtime_Options_overwrite:
|
||||
for metadata_style in self.config.Runtime_Options_type:
|
||||
if not self.config.runtime_overwrite:
|
||||
for metadata_style in self.config.runtime_type:
|
||||
if ca.has_metadata(metadata_style):
|
||||
print(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
|
||||
return
|
||||
@ -352,26 +341,26 @@ class CLI:
|
||||
|
||||
md = self.create_local_metadata(ca)
|
||||
if md.issue is None or md.issue == "":
|
||||
if self.config.Auto_Tag_assume_1_if_no_issue_num:
|
||||
if self.config.autotag_assume_1_if_no_issue_num:
|
||||
md.issue = "1"
|
||||
|
||||
# now, search online
|
||||
if self.config.Runtime_Options_online:
|
||||
if self.config.Runtime_Options_issue_id is not None:
|
||||
if self.config.runtime_online:
|
||||
if self.config.runtime_issue_id is not None:
|
||||
# we were given the actual issue ID to search with
|
||||
try:
|
||||
ct_md = self.current_talker().fetch_comic_data(self.config.Runtime_Options_issue_id)
|
||||
ct_md = self.current_talker().fetch_comic_data(self.config.runtime_issue_id)
|
||||
except TalkerError as e:
|
||||
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
|
||||
match_results.fetch_data_failures.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
if ct_md is None:
|
||||
logger.error("No match for ID %s was found.", self.config.Runtime_Options_issue_id)
|
||||
logger.error("No match for ID %s was found.", self.config.runtime_issue_id)
|
||||
match_results.no_matches.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
if self.config.Comic_Book_Lover_apply_transform_on_import:
|
||||
if self.config.cbl_apply_transform_on_import:
|
||||
ct_md = CBLTransformer(ct_md, self.config).apply()
|
||||
else:
|
||||
if md is None or md.is_empty:
|
||||
@ -382,7 +371,7 @@ class CLI:
|
||||
ii = IssueIdentifier(ca, self.config, self.current_talker())
|
||||
|
||||
def myoutput(text: str) -> None:
|
||||
if self.config.Runtime_Options_verbose:
|
||||
if self.config.runtime_verbose:
|
||||
IssueIdentifier.default_write_output(text)
|
||||
|
||||
# use our overlaid MD struct to search
|
||||
@ -422,7 +411,7 @@ class CLI:
|
||||
logger.error("Online search: Multiple good matches. Save aborted")
|
||||
match_results.multiple_matches.append(MultipleMatch(ca, matches))
|
||||
return
|
||||
if low_confidence and self.config.Runtime_Options_abort_on_low_confidence:
|
||||
if low_confidence and self.config.runtime_abort_on_low_confidence:
|
||||
logger.error("Online search: Low confidence match. Save aborted")
|
||||
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
|
||||
return
|
||||
@ -439,7 +428,7 @@ class CLI:
|
||||
match_results.fetch_data_failures.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
if self.config.Issue_Identifier_clear_metadata_on_import:
|
||||
if self.config.identifier_clear_metadata_on_import:
|
||||
md = GenericMetadata()
|
||||
|
||||
notes = (
|
||||
@ -449,11 +438,11 @@ class CLI:
|
||||
md.overlay(
|
||||
ct_md.replace(
|
||||
notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"),
|
||||
description=cleanup_html(ct_md.description, self.config.Sources_remove_html_tables),
|
||||
description=cleanup_html(ct_md.description, self.config.talker_remove_html_tables),
|
||||
)
|
||||
)
|
||||
|
||||
if self.config.Issue_Identifier_auto_imprint:
|
||||
if self.config.identifier_auto_imprint:
|
||||
md.fix_publisher()
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
@ -475,18 +464,18 @@ class CLI:
|
||||
return
|
||||
|
||||
new_ext = "" # default
|
||||
if self.config.File_Rename_set_extension_based_on_archive:
|
||||
if self.config.rename_set_extension_based_on_archive:
|
||||
new_ext = ca.extension()
|
||||
|
||||
renamer = FileRenamer(
|
||||
md,
|
||||
platform="universal" if self.config.File_Rename_strict else "auto",
|
||||
replacements=self.config.File_Rename_replacements,
|
||||
platform="universal" if self.config.rename_strict else "auto",
|
||||
replacements=self.config.rename_replacements,
|
||||
)
|
||||
renamer.set_template(self.config.File_Rename_template)
|
||||
renamer.set_issue_zero_padding(self.config.File_Rename_issue_number_padding)
|
||||
renamer.set_smart_cleanup(self.config.File_Rename_use_smart_string_cleanup)
|
||||
renamer.move = self.config.File_Rename_move_to_dir
|
||||
renamer.set_template(self.config.rename_template)
|
||||
renamer.set_issue_zero_padding(self.config.rename_issue_number_padding)
|
||||
renamer.set_smart_cleanup(self.config.rename_use_smart_string_cleanup)
|
||||
renamer.move = self.config.rename_move_to_dir
|
||||
|
||||
try:
|
||||
new_name = renamer.determine_name(ext=new_ext)
|
||||
@ -498,14 +487,14 @@ class CLI:
|
||||
"Please consult the template help in the settings "
|
||||
"and the documentation on the format at "
|
||||
"https://docs.python.org/3/library/string.html#format-string-syntax",
|
||||
self.config.File_Rename_template,
|
||||
self.config.rename_template,
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("Formatter failure: %s metadata: %s", self.config.File_Rename_template, renamer.metadata)
|
||||
logger.exception("Formatter failure: %s metadata: %s", self.config.rename_template, renamer.metadata)
|
||||
return
|
||||
|
||||
folder = get_rename_dir(ca, self.config.File_Rename_dir if self.config.File_Rename_move_to_dir else None)
|
||||
folder = get_rename_dir(ca, self.config.rename_dir if self.config.rename_move_to_dir else None)
|
||||
|
||||
full_path = folder / new_name
|
||||
|
||||
@ -514,7 +503,7 @@ class CLI:
|
||||
return
|
||||
|
||||
suffix = ""
|
||||
if not self.config.Runtime_Options_dryrun:
|
||||
if not self.config.runtime_dryrun:
|
||||
# rename the file
|
||||
try:
|
||||
ca.rename(utils.unique_file(full_path))
|
||||
@ -537,7 +526,7 @@ class CLI:
|
||||
filename_path = ca.path
|
||||
new_file = filename_path.with_suffix(".cbz")
|
||||
|
||||
if self.config.Runtime_Options_abort_on_conflict and new_file.exists():
|
||||
if self.config.runtime_abort_on_conflict and new_file.exists():
|
||||
print(msg_hdr + f"{new_file.name} already exists in the that folder.")
|
||||
return
|
||||
|
||||
@ -545,10 +534,10 @@ class CLI:
|
||||
|
||||
delete_success = False
|
||||
export_success = False
|
||||
if not self.config.Runtime_Options_dryrun:
|
||||
if not self.config.runtime_dryrun:
|
||||
if ca.export_as_zip(new_file):
|
||||
export_success = True
|
||||
if self.config.Runtime_Options_delete_after_zip_export:
|
||||
if self.config.runtime_delete_after_zip_export:
|
||||
try:
|
||||
filename_path.unlink(missing_ok=True)
|
||||
delete_success = True
|
||||
@ -560,7 +549,7 @@ class CLI:
|
||||
new_file.unlink(missing_ok=True)
|
||||
else:
|
||||
msg = msg_hdr + f"Dry-run: Would try to create {os.path.split(new_file)[1]}"
|
||||
if self.config.Runtime_Options_delete_after_zip_export:
|
||||
if self.config.runtime_delete_after_zip_export:
|
||||
msg += " and delete original."
|
||||
print(msg)
|
||||
return
|
||||
@ -568,7 +557,7 @@ class CLI:
|
||||
msg = msg_hdr
|
||||
if export_success:
|
||||
msg += f"Archive exported successfully to: {os.path.split(new_file)[1]}"
|
||||
if self.config.Runtime_Options_delete_after_zip_export and delete_success:
|
||||
if self.config.runtime_delete_after_zip_export and delete_success:
|
||||
msg += " (Original deleted) "
|
||||
else:
|
||||
msg += "Archive failed to export!"
|
||||
@ -587,28 +576,28 @@ class CLI:
|
||||
return
|
||||
|
||||
if not ca.is_writable() and (
|
||||
self.config.Commands_delete
|
||||
or self.config.Commands_copy
|
||||
or self.config.Commands_save
|
||||
or self.config.Commands_rename
|
||||
self.config.commands_delete
|
||||
or self.config.commands_copy
|
||||
or self.config.commands_save
|
||||
or self.config.commands_rename
|
||||
):
|
||||
logger.error("This archive is not writable")
|
||||
return
|
||||
|
||||
if self.config.Commands_print:
|
||||
if self.config.commands_print:
|
||||
self.print(ca)
|
||||
|
||||
elif self.config.Commands_delete:
|
||||
elif self.config.commands_delete:
|
||||
self.delete(ca)
|
||||
|
||||
elif self.config.Commands_copy is not None:
|
||||
elif self.config.commands_copy is not None:
|
||||
self.copy(ca)
|
||||
|
||||
elif self.config.Commands_save:
|
||||
elif self.config.commands_save:
|
||||
self.save(ca, match_results)
|
||||
|
||||
elif self.config.Commands_rename:
|
||||
elif self.config.commands_rename:
|
||||
self.rename(ca)
|
||||
|
||||
elif self.config.Commands_export_to_zip:
|
||||
elif self.config.commands_export_to_zip:
|
||||
self.export(ca)
|
||||
|
@ -6,7 +6,7 @@ from comictaggerlib.ctsettings.commandline import (
|
||||
validate_commandline_settings,
|
||||
)
|
||||
from comictaggerlib.ctsettings.file import register_file_settings, validate_file_settings
|
||||
from comictaggerlib.ctsettings.plugin import group_for_plugin, register_plugin_settings, validate_plugin_settings
|
||||
from comictaggerlib.ctsettings.plugin import register_plugin_settings, validate_plugin_settings
|
||||
from comictaggerlib.ctsettings.settngs_namespace import settngs_namespace as ct_ns
|
||||
from comictaggerlib.ctsettings.types import ComicTaggerPaths
|
||||
from comictalker import ComicTalker
|
||||
@ -23,5 +23,4 @@ __all__ = [
|
||||
"validate_plugin_settings",
|
||||
"ComicTaggerPaths",
|
||||
"ct_ns",
|
||||
"group_for_plugin",
|
||||
]
|
||||
|
@ -234,74 +234,63 @@ def register_commands(parser: settngs.Manager) -> None:
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--only-save-config",
|
||||
"--only-set-cv-key",
|
||||
action="store_true",
|
||||
help="Only save the configuration (eg, Comic Vine API key) and quit.",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--list-plugins",
|
||||
action="store_true",
|
||||
help="List the available plugins.\n\n",
|
||||
help="Only set the Comic Vine API key and quit.\n\n",
|
||||
file=False,
|
||||
)
|
||||
|
||||
|
||||
def register_commandline_settings(parser: settngs.Manager) -> None:
|
||||
parser.add_group("Commands", register_commands, True)
|
||||
parser.add_persistent_group("Runtime Options", register_runtime)
|
||||
parser.add_group("commands", register_commands, True)
|
||||
parser.add_persistent_group("runtime", register_runtime)
|
||||
|
||||
|
||||
def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs.Manager) -> settngs.Config[ct_ns]:
|
||||
if config[0].Commands_version:
|
||||
if config[0].commands_version:
|
||||
parser.exit(
|
||||
status=1,
|
||||
message=f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team\n"
|
||||
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n",
|
||||
)
|
||||
|
||||
config[0].Runtime_Options_no_gui = any(
|
||||
config[0].runtime_no_gui = any(
|
||||
[
|
||||
config[0].Commands_print,
|
||||
config[0].Commands_delete,
|
||||
config[0].Commands_save,
|
||||
config[0].Commands_copy,
|
||||
config[0].Commands_rename,
|
||||
config[0].Commands_export_to_zip,
|
||||
config[0].Commands_only_save_config,
|
||||
config[0].Commands_list_plugins,
|
||||
config[0].Runtime_Options_no_gui,
|
||||
config[0].commands_print,
|
||||
config[0].commands_delete,
|
||||
config[0].commands_save,
|
||||
config[0].commands_copy,
|
||||
config[0].commands_rename,
|
||||
config[0].commands_export_to_zip,
|
||||
config[0].commands_only_set_cv_key,
|
||||
config[0].runtime_no_gui,
|
||||
]
|
||||
)
|
||||
|
||||
if platform.system() == "Windows" and config[0].Runtime_Options_glob:
|
||||
if platform.system() == "Windows" and config[0].runtime_glob:
|
||||
# no globbing on windows shell, so do it for them
|
||||
import glob
|
||||
|
||||
globs = config[0].Runtime_Options_files
|
||||
config[0].Runtime_Options_files = []
|
||||
globs = config[0].runtime_files
|
||||
config[0].runtime_files = []
|
||||
for item in globs:
|
||||
config[0].Runtime_Options_files.extend(glob.glob(item))
|
||||
config[0].runtime_files.extend(glob.glob(item))
|
||||
|
||||
if (
|
||||
not config[0].Commands_only_save_config
|
||||
and config[0].Runtime_Options_no_gui
|
||||
and not config[0].Runtime_Options_files
|
||||
):
|
||||
if not config[0].commands_only_set_cv_key and config[0].runtime_no_gui and not config[0].runtime_files:
|
||||
parser.exit(message="Command requires at least one filename!\n", status=1)
|
||||
|
||||
if config[0].Commands_delete and not config[0].Runtime_Options_type:
|
||||
if config[0].commands_delete and not config[0].runtime_type:
|
||||
parser.exit(message="Please specify the type to delete with -t\n", status=1)
|
||||
|
||||
if config[0].Commands_save and not config[0].Runtime_Options_type:
|
||||
if config[0].commands_save and not config[0].runtime_type:
|
||||
parser.exit(message="Please specify the type to save with -t\n", status=1)
|
||||
|
||||
if config[0].Commands_copy:
|
||||
if not config[0].Runtime_Options_type:
|
||||
if config[0].commands_copy:
|
||||
if not config[0].runtime_type:
|
||||
parser.exit(message="Please specify the type to copy to with -t\n", status=1)
|
||||
|
||||
if config[0].Runtime_Options_recursive:
|
||||
config[0].Runtime_Options_files = utils.get_recursive_filelist(config[0].Runtime_Options_files)
|
||||
if config[0].runtime_recursive:
|
||||
config[0].runtime_files = utils.get_recursive_filelist(config[0].runtime_files)
|
||||
|
||||
# take a crack at finding rar exe if it's not in the path
|
||||
if not utils.which("rar"):
|
||||
|
@ -123,11 +123,7 @@ 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 (use --list-plugins to list all sources)",
|
||||
)
|
||||
parser.add_setting("--source", default="comicvine", help="Use a specified source by source ID")
|
||||
parser.add_setting(
|
||||
"--remove-html-tables",
|
||||
default=False,
|
||||
@ -223,7 +219,7 @@ def autotag(parser: settngs.Manager) -> None:
|
||||
def validate_file_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]:
|
||||
new_filter = []
|
||||
remove = []
|
||||
for x in config[0].Issue_Identifier_publisher_filter:
|
||||
for x in config[0].identifier_publisher_filter:
|
||||
x = x.strip()
|
||||
if x: # ignore empty arguments
|
||||
if x[-1] == "-": # this publisher needs to be removed. We remove after all publishers have been enumerated
|
||||
@ -234,22 +230,22 @@ def validate_file_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_n
|
||||
for x in remove: # remove publishers
|
||||
if x in new_filter:
|
||||
new_filter.remove(x)
|
||||
config[0].Issue_Identifier_publisher_filter = new_filter
|
||||
config[0].identifier_publisher_filter = new_filter
|
||||
|
||||
config[0].File_Rename_replacements = Replacements(
|
||||
[Replacement(x[0], x[1], x[2]) for x in config[0].File_Rename_replacements[0]],
|
||||
[Replacement(x[0], x[1], x[2]) for x in config[0].File_Rename_replacements[1]],
|
||||
config[0].rename_replacements = Replacements(
|
||||
[Replacement(x[0], x[1], x[2]) for x in config[0].rename_replacements[0]],
|
||||
[Replacement(x[0], x[1], x[2]) for x in config[0].rename_replacements[1]],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def register_file_settings(parser: settngs.Manager) -> None:
|
||||
parser.add_group("general", general, False)
|
||||
parser.add_group("internal", internal, False)
|
||||
parser.add_group("Issue Identifier", identifier, False)
|
||||
parser.add_group("Filename Parsing", filename, False)
|
||||
parser.add_group("Sources", talker, False)
|
||||
parser.add_group("Comic Book Lover", cbl, False)
|
||||
parser.add_group("File Rename", rename, False)
|
||||
parser.add_group("Auto-Tag", autotag, False)
|
||||
parser.add_group("General", general, False)
|
||||
parser.add_group("Dialog Flags", dialog, False)
|
||||
parser.add_group("identifier", identifier, False)
|
||||
parser.add_group("dialog", dialog, False)
|
||||
parser.add_group("filename", filename, False)
|
||||
parser.add_group("talker", talker, False)
|
||||
parser.add_group("cbl", cbl, False)
|
||||
parser.add_group("rename", rename, False)
|
||||
parser.add_group("autotag", autotag, False)
|
||||
|
@ -7,23 +7,12 @@ from typing import cast
|
||||
import settngs
|
||||
|
||||
import comicapi.comicarchive
|
||||
import comicapi.utils
|
||||
import comictaggerlib.ctsettings
|
||||
from comicapi.comicarchive import Archiver
|
||||
from comictaggerlib.ctsettings.settngs_namespace import settngs_namespace as ct_ns
|
||||
from comictalker.comictalker import ComicTalker
|
||||
|
||||
logger = logging.getLogger("comictagger")
|
||||
|
||||
|
||||
def group_for_plugin(plugin: Archiver | ComicTalker) -> str:
|
||||
if isinstance(plugin, ComicTalker):
|
||||
return f"Source {plugin.id}"
|
||||
if isinstance(plugin, Archiver):
|
||||
return "Archive"
|
||||
raise NotImplementedError(f"Invalid plugin received: {plugin=}")
|
||||
|
||||
|
||||
def archiver(manager: settngs.Manager) -> None:
|
||||
for archiver in comicapi.comicarchive.archivers:
|
||||
if archiver.exe:
|
||||
@ -37,28 +26,27 @@ def archiver(manager: settngs.Manager) -> None:
|
||||
|
||||
|
||||
def register_talker_settings(manager: settngs.Manager) -> None:
|
||||
for talker in comictaggerlib.ctsettings.talkers.values():
|
||||
for talker_id, talker in comictaggerlib.ctsettings.talkers.items():
|
||||
|
||||
def api_options(manager: settngs.Manager) -> None:
|
||||
# The default needs to be unset or None.
|
||||
# This allows this setting to be unset with the empty string, allowing the default to change
|
||||
manager.add_setting(
|
||||
f"--{talker.id}-key",
|
||||
f"--{talker_id}-key",
|
||||
display_name="API Key",
|
||||
help=f"API Key for {talker.name} (default: {talker.default_api_key})",
|
||||
)
|
||||
manager.add_setting(
|
||||
f"--{talker.id}-url",
|
||||
f"--{talker_id}-url",
|
||||
display_name="URL",
|
||||
help=f"URL for {talker.name} (default: {talker.default_api_url})",
|
||||
)
|
||||
|
||||
try:
|
||||
manager.add_persistent_group(group_for_plugin(talker), api_options, False)
|
||||
if hasattr(talker, "register_settings"):
|
||||
manager.add_persistent_group(group_for_plugin(talker), talker.register_settings, False)
|
||||
manager.add_persistent_group("talker_" + talker_id, api_options, False)
|
||||
manager.add_persistent_group("talker_" + talker_id, talker.register_settings, False)
|
||||
except Exception:
|
||||
logger.exception("Failed to register settings for %s", talker.id)
|
||||
logger.exception("Failed to register settings for %s", talker_id)
|
||||
|
||||
|
||||
def validate_archive_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]:
|
||||
@ -67,11 +55,11 @@ def validate_archive_settings(config: settngs.Config[ct_ns]) -> settngs.Config[c
|
||||
cfg = settngs.normalize_config(config, file=True, cmdline=True, default=False)
|
||||
for archiver in comicapi.comicarchive.archivers:
|
||||
exe_name = settngs.sanitize_name(archiver.exe)
|
||||
if exe_name in cfg[0][group_for_plugin(archiver())] and cfg[0][group_for_plugin(archiver())][exe_name]:
|
||||
if os.path.basename(cfg[0][group_for_plugin(archiver())][exe_name]) == archiver.exe:
|
||||
comicapi.utils.add_to_path(os.path.dirname(cfg[0][group_for_plugin(archiver())][exe_name]))
|
||||
if exe_name in cfg[0]["archiver"] and cfg[0]["archiver"][exe_name]:
|
||||
if os.path.basename(cfg[0]["archiver"][exe_name]) == archiver.exe:
|
||||
comicapi.utils.add_to_path(os.path.dirname(cfg[0]["archiver"][exe_name]))
|
||||
else:
|
||||
archiver.exe = cfg[0][group_for_plugin(archiver())][exe_name]
|
||||
archiver.exe = cfg[0]["archiver"][exe_name]
|
||||
|
||||
return config
|
||||
|
||||
@ -79,12 +67,12 @@ def validate_archive_settings(config: settngs.Config[ct_ns]) -> settngs.Config[c
|
||||
def validate_talker_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]:
|
||||
# Apply talker settings from config file
|
||||
cfg = settngs.normalize_config(config, True, True)
|
||||
for talker in list(comictaggerlib.ctsettings.talkers.values()):
|
||||
for talker_id, talker in list(comictaggerlib.ctsettings.talkers.items()):
|
||||
try:
|
||||
cfg[0][group_for_plugin(talker)] = talker.parse_settings(cfg[0][group_for_plugin(talker)])
|
||||
cfg[0]["talker_" + talker_id] = talker.parse_settings(cfg[0]["talker_" + talker_id])
|
||||
except Exception as e:
|
||||
# Remove talker as we failed to apply the settings
|
||||
del comictaggerlib.ctsettings.talkers[talker.id]
|
||||
del comictaggerlib.ctsettings.talkers[talker_id]
|
||||
logger.exception("Failed to initialize talker settings: %s", e)
|
||||
|
||||
return cast(settngs.Config[ct_ns], settngs.get_namespace(cfg, file=True, cmdline=True))
|
||||
@ -97,5 +85,5 @@ def validate_plugin_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct
|
||||
|
||||
|
||||
def register_plugin_settings(manager: settngs.Manager) -> None:
|
||||
manager.add_persistent_group("Archive", archiver, False)
|
||||
manager.add_persistent_group("archiver", archiver, False)
|
||||
register_talker_settings(manager)
|
||||
|
@ -8,39 +8,40 @@ import comictaggerlib.defaults
|
||||
|
||||
|
||||
class settngs_namespace(settngs.TypedNS):
|
||||
Commands_version: bool
|
||||
Commands_print: bool
|
||||
Commands_delete: bool
|
||||
Commands_copy: int
|
||||
Commands_save: bool
|
||||
Commands_rename: bool
|
||||
Commands_export_to_zip: bool
|
||||
Commands_only_save_config: bool
|
||||
Commands_list_plugins: bool
|
||||
commands_version: bool
|
||||
commands_print: bool
|
||||
commands_delete: bool
|
||||
commands_copy: int
|
||||
commands_save: bool
|
||||
commands_rename: bool
|
||||
commands_export_to_zip: bool
|
||||
commands_only_set_cv_key: bool
|
||||
|
||||
Runtime_Options_config: comictaggerlib.ctsettings.types.ComicTaggerPaths
|
||||
Runtime_Options_verbose: int
|
||||
Runtime_Options_abort_on_conflict: bool
|
||||
Runtime_Options_delete_after_zip_export: bool
|
||||
Runtime_Options_parse_filename: bool
|
||||
Runtime_Options_issue_id: str
|
||||
Runtime_Options_online: bool
|
||||
Runtime_Options_metadata: comicapi.genericmetadata.GenericMetadata
|
||||
Runtime_Options_interactive: bool
|
||||
Runtime_Options_abort_on_low_confidence: bool
|
||||
Runtime_Options_summary: bool
|
||||
Runtime_Options_raw: bool
|
||||
Runtime_Options_recursive: bool
|
||||
Runtime_Options_script: str
|
||||
Runtime_Options_split_words: bool
|
||||
Runtime_Options_dryrun: bool
|
||||
Runtime_Options_darkmode: bool
|
||||
Runtime_Options_glob: bool
|
||||
Runtime_Options_quiet: bool
|
||||
Runtime_Options_type: list[int]
|
||||
Runtime_Options_overwrite: bool
|
||||
Runtime_Options_no_gui: bool
|
||||
Runtime_Options_files: list[str]
|
||||
runtime_config: comictaggerlib.ctsettings.types.ComicTaggerPaths
|
||||
runtime_verbose: int
|
||||
runtime_abort_on_conflict: bool
|
||||
runtime_delete_after_zip_export: bool
|
||||
runtime_parse_filename: bool
|
||||
runtime_issue_id: str
|
||||
runtime_online: bool
|
||||
runtime_metadata: comicapi.genericmetadata.GenericMetadata
|
||||
runtime_interactive: bool
|
||||
runtime_abort_on_low_confidence: bool
|
||||
runtime_summary: bool
|
||||
runtime_raw: bool
|
||||
runtime_recursive: bool
|
||||
runtime_script: str
|
||||
runtime_split_words: bool
|
||||
runtime_dryrun: bool
|
||||
runtime_darkmode: bool
|
||||
runtime_glob: bool
|
||||
runtime_quiet: bool
|
||||
runtime_type: list[int]
|
||||
runtime_overwrite: bool
|
||||
runtime_no_gui: bool
|
||||
runtime_files: list[str]
|
||||
|
||||
general_check_for_new_version: bool
|
||||
|
||||
internal_install_id: str
|
||||
internal_save_data_style: int
|
||||
@ -55,56 +56,50 @@ class settngs_namespace(settngs.TypedNS):
|
||||
internal_sort_column: int
|
||||
internal_sort_direction: int
|
||||
|
||||
Issue_Identifier_series_match_identify_thresh: int
|
||||
Issue_Identifier_border_crop_percent: int
|
||||
Issue_Identifier_publisher_filter: list[str]
|
||||
Issue_Identifier_series_match_search_thresh: int
|
||||
Issue_Identifier_clear_metadata_on_import: bool
|
||||
Issue_Identifier_auto_imprint: bool
|
||||
Issue_Identifier_sort_series_by_year: bool
|
||||
Issue_Identifier_exact_series_matches_first: bool
|
||||
Issue_Identifier_always_use_publisher_filter: bool
|
||||
Issue_Identifier_clear_form_before_populating: bool
|
||||
identifier_series_match_identify_thresh: int
|
||||
identifier_border_crop_percent: int
|
||||
identifier_publisher_filter: list[str]
|
||||
identifier_series_match_search_thresh: int
|
||||
identifier_clear_metadata_on_import: bool
|
||||
identifier_auto_imprint: bool
|
||||
identifier_sort_series_by_year: bool
|
||||
identifier_exact_series_matches_first: bool
|
||||
identifier_always_use_publisher_filter: bool
|
||||
identifier_clear_form_before_populating: bool
|
||||
|
||||
Filename_Parsing_complicated_parser: bool
|
||||
Filename_Parsing_remove_c2c: bool
|
||||
Filename_Parsing_remove_fcbd: bool
|
||||
Filename_Parsing_remove_publisher: bool
|
||||
dialog_show_disclaimer: bool
|
||||
dialog_dont_notify_about_this_version: str
|
||||
dialog_ask_about_usage_stats: bool
|
||||
|
||||
Sources_source: str
|
||||
Sources_remove_html_tables: bool
|
||||
filename_complicated_parser: bool
|
||||
filename_remove_c2c: bool
|
||||
filename_remove_fcbd: bool
|
||||
filename_remove_publisher: bool
|
||||
|
||||
Comic_Book_Lover_assume_lone_credit_is_primary: bool
|
||||
Comic_Book_Lover_copy_characters_to_tags: bool
|
||||
Comic_Book_Lover_copy_teams_to_tags: bool
|
||||
Comic_Book_Lover_copy_locations_to_tags: bool
|
||||
Comic_Book_Lover_copy_storyarcs_to_tags: bool
|
||||
Comic_Book_Lover_copy_notes_to_comments: bool
|
||||
Comic_Book_Lover_copy_weblink_to_comments: bool
|
||||
Comic_Book_Lover_apply_transform_on_import: bool
|
||||
Comic_Book_Lover_apply_transform_on_bulk_operation: bool
|
||||
talker_source: str
|
||||
talker_remove_html_tables: bool
|
||||
|
||||
File_Rename_template: str
|
||||
File_Rename_issue_number_padding: int
|
||||
File_Rename_use_smart_string_cleanup: bool
|
||||
File_Rename_set_extension_based_on_archive: bool
|
||||
File_Rename_dir: str
|
||||
File_Rename_move_to_dir: bool
|
||||
File_Rename_strict: bool
|
||||
File_Rename_replacements: comictaggerlib.defaults.Replacements
|
||||
cbl_assume_lone_credit_is_primary: bool
|
||||
cbl_copy_characters_to_tags: bool
|
||||
cbl_copy_teams_to_tags: bool
|
||||
cbl_copy_locations_to_tags: bool
|
||||
cbl_copy_storyarcs_to_tags: bool
|
||||
cbl_copy_notes_to_comments: bool
|
||||
cbl_copy_weblink_to_comments: bool
|
||||
cbl_apply_transform_on_import: bool
|
||||
cbl_apply_transform_on_bulk_operation: bool
|
||||
|
||||
Auto_Tag_save_on_low_confidence: bool
|
||||
Auto_Tag_dont_use_year_when_identifying: bool
|
||||
Auto_Tag_assume_1_if_no_issue_num: bool
|
||||
Auto_Tag_ignore_leading_numbers_in_filename: bool
|
||||
Auto_Tag_remove_archive_after_successful_match: bool
|
||||
rename_template: str
|
||||
rename_issue_number_padding: int
|
||||
rename_use_smart_string_cleanup: bool
|
||||
rename_set_extension_based_on_archive: bool
|
||||
rename_dir: str
|
||||
rename_move_to_dir: bool
|
||||
rename_strict: bool
|
||||
rename_replacements: comictaggerlib.defaults.Replacements
|
||||
|
||||
General_check_for_new_version: bool
|
||||
|
||||
Dialog_Flags_show_disclaimer: bool
|
||||
Dialog_Flags_dont_notify_about_this_version: str
|
||||
Dialog_Flags_ask_about_usage_stats: bool
|
||||
|
||||
Source_comicvine_comicvine_key: str
|
||||
Source_comicvine_comicvine_url: str
|
||||
Source_comicvine_cv_use_series_start_as_volume: bool
|
||||
autotag_save_on_low_confidence: bool
|
||||
autotag_dont_use_year_when_identifying: bool
|
||||
autotag_assume_1_if_no_issue_num: bool
|
||||
autotag_ignore_leading_numbers_in_filename: bool
|
||||
autotag_remove_archive_after_successful_match: bool
|
||||
|
@ -15,7 +15,6 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
@ -221,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:
|
||||
|
@ -95,9 +95,10 @@ def open_tagger_window(
|
||||
talkers: dict[str, ComicTalker], config: settngs.Config[ct_ns], error: tuple[str, bool] | None
|
||||
) -> None:
|
||||
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
|
||||
args = [sys.argv[0]]
|
||||
if config[0].Runtime_Options_darkmode:
|
||||
args = []
|
||||
if config[0].runtime_darkmode:
|
||||
args.extend(["-platform", "windows:darkmode=2"])
|
||||
args.extend(sys.argv)
|
||||
app = Application(args)
|
||||
if error is not None:
|
||||
show_exception_box(error[0])
|
||||
@ -105,7 +106,7 @@ def open_tagger_window(
|
||||
raise SystemExit(1)
|
||||
|
||||
# needed to catch initial open file events (macOS)
|
||||
app.openFileRequest.connect(lambda x: config[0].Runtime_Options_files.append(x.toLocalFile()))
|
||||
app.openFileRequest.connect(lambda x: config[0].runtime_files.append(x.toLocalFile()))
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
# Set the MacOS dock icon
|
||||
@ -133,7 +134,7 @@ def open_tagger_window(
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow(config[0].Runtime_Options_files, config, talkers)
|
||||
tagger_window = TaggerWindow(config[0].runtime_files, config, talkers)
|
||||
tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
tagger_window.show()
|
||||
|
||||
|
@ -96,10 +96,10 @@ class IssueIdentifier:
|
||||
|
||||
# used to eliminate series names that are too long based on our search
|
||||
# string
|
||||
self.series_match_thresh = config.Issue_Identifier_series_match_identify_thresh
|
||||
self.series_match_thresh = config.identifier_series_match_identify_thresh
|
||||
|
||||
# used to eliminate unlikely publishers
|
||||
self.publisher_filter = [s.strip().casefold() for s in config.Issue_Identifier_publisher_filter]
|
||||
self.publisher_filter = [s.strip().casefold() for s in config.identifier_publisher_filter]
|
||||
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output
|
||||
@ -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
|
||||
@ -239,10 +239,10 @@ class IssueIdentifier:
|
||||
|
||||
# try to get some metadata from filename
|
||||
md_from_filename = ca.metadata_from_filename(
|
||||
self.config.Filename_Parsing_complicated_parser,
|
||||
self.config.Filename_Parsing_remove_c2c,
|
||||
self.config.Filename_Parsing_remove_fcbd,
|
||||
self.config.Filename_Parsing_remove_publisher,
|
||||
self.config.filename_complicated_parser,
|
||||
self.config.filename_remove_c2c,
|
||||
self.config.filename_remove_fcbd,
|
||||
self.config.filename_remove_publisher,
|
||||
)
|
||||
|
||||
working_md = md_from_filename.copy()
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -291,7 +291,7 @@ class IssueIdentifier:
|
||||
return Score(score=0, url="", hash=0)
|
||||
|
||||
try:
|
||||
url_image_data = ImageFetcher(self.config.Runtime_Options_config.user_cache_dir).fetch(
|
||||
url_image_data = ImageFetcher(self.config.runtime_config.user_cache_dir).fetch(
|
||||
primary_img_url, blocking=True
|
||||
)
|
||||
except ImageFetcherException as e:
|
||||
@ -313,7 +313,7 @@ class IssueIdentifier:
|
||||
if use_remote_alternates:
|
||||
for alt_url in alt_urls:
|
||||
try:
|
||||
alt_url_image_data = ImageFetcher(self.config.Runtime_Options_config.user_cache_dir).fetch(
|
||||
alt_url_image_data = ImageFetcher(self.config.runtime_config.user_cache_dir).fetch(
|
||||
alt_url, blocking=True
|
||||
)
|
||||
except ImageFetcherException as e:
|
||||
@ -499,7 +499,7 @@ class IssueIdentifier:
|
||||
if narrow_cover_hash is not None:
|
||||
hash_list.append(narrow_cover_hash)
|
||||
|
||||
cropped_border = self.crop_border(cover_image_data, self.config.Issue_Identifier_border_crop_percent)
|
||||
cropped_border = self.crop_border(cover_image_data, self.config.identifier_border_crop_percent)
|
||||
if cropped_border is not None:
|
||||
hash_list.append(self.calculate_hash(cropped_border))
|
||||
logger.info("Adding cropped cover to the hashlist")
|
||||
@ -525,8 +525,8 @@ class IssueIdentifier:
|
||||
"issue_title": issue.title or "",
|
||||
"issue_id": issue.issue_id or "",
|
||||
"series_id": series.id,
|
||||
"month": issue.month,
|
||||
"year": issue.year,
|
||||
"month": issue.cover_date.month,
|
||||
"year": issue.cover_date.year,
|
||||
"publisher": None,
|
||||
"image_url": image_url,
|
||||
"alt_image_urls": alt_urls,
|
||||
|
@ -52,10 +52,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
uic.loadUi(ui_path / "issueselectionwindow.ui", self)
|
||||
|
||||
self.coverWidget = CoverImageWidget(
|
||||
self.coverImageContainer,
|
||||
CoverImageWidget.AltCoverMode,
|
||||
config.Runtime_Options_config.user_cache_dir,
|
||||
talker,
|
||||
self.coverImageContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
|
||||
gridlayout.addWidget(self.coverWidget)
|
||||
@ -90,7 +87,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.config = config
|
||||
self.talker = talker
|
||||
self.url_fetch_thread = None
|
||||
self.issue_list: dict[str, GenericMetadata] = {}
|
||||
self.issue_list: list[GenericMetadata] = []
|
||||
|
||||
# Display talker logo and set url
|
||||
self.lblIssuesSourceName.setText(talker.attribution)
|
||||
@ -98,7 +95,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.imageIssuesSourceWidget = CoverImageWidget(
|
||||
self.imageIssuesSourceLogo,
|
||||
CoverImageWidget.URLMode,
|
||||
config.Runtime_Options_config.user_cache_dir,
|
||||
config.runtime_config.user_cache_dir,
|
||||
talker,
|
||||
False,
|
||||
)
|
||||
@ -146,9 +143,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
|
||||
|
||||
try:
|
||||
self.issue_list = {
|
||||
x.issue_id: x for x in self.talker.fetch_issues_in_series(self.series_id) if x.issue_id is not None
|
||||
}
|
||||
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}")
|
||||
@ -158,7 +153,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
|
||||
for row, issue in enumerate(self.issue_list.values()):
|
||||
for row, issue in enumerate(self.issue_list):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = issue.issue or ""
|
||||
@ -170,12 +165,12 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
self.twList.setItem(row, 0, item)
|
||||
|
||||
item_text = ""
|
||||
if issue.year is not None:
|
||||
item_text += f"-{issue.year:04}"
|
||||
if issue.month is not None:
|
||||
item_text += f"-{issue.month:02}"
|
||||
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.strip("-"))
|
||||
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)
|
||||
@ -213,8 +208,8 @@ 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
|
||||
issue = self.issue_list[self.issue_id]
|
||||
if not (issue.issue and issue.year and issue.month and issue.cover_image):
|
||||
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])
|
||||
|
@ -91,7 +91,7 @@ def configure_locale() -> None:
|
||||
|
||||
|
||||
def update_publishers(config: settngs.Config[ct_ns]) -> None:
|
||||
json_file = config[0].Runtime_Options_config.user_config_dir / "publishers.json"
|
||||
json_file = config[0].runtime_config.user_config_dir / "publishers.json"
|
||||
if json_file.exists():
|
||||
try:
|
||||
comicapi.utils.update_publishers(json.loads(json_file.read_text("utf-8")))
|
||||
@ -121,18 +121,6 @@ class App:
|
||||
comicapi.comicarchive.load_archive_plugins()
|
||||
ctsettings.talkers = comictalker.get_talkers(version, opts.config.user_cache_dir)
|
||||
|
||||
def list_plugins(
|
||||
self, talkers: list[comictalker.ComicTalker], archivers: list[type[comicapi.comicarchive.Archiver]]
|
||||
) -> None:
|
||||
print("Metadata Sources: (ID: Name URL)") # noqa: T201
|
||||
for talker in talkers:
|
||||
print(f"{talker.id}: {talker.name} {talker.default_api_url}") # noqa: T201
|
||||
|
||||
print("\nComic Archive: (Name: extension, exe)") # noqa: T201
|
||||
for archiver in archivers:
|
||||
a = archiver()
|
||||
print(f"{a.name()}: {a.extension()}, {a.exe}") # noqa: T201
|
||||
|
||||
def initialize(self) -> argparse.Namespace:
|
||||
conf, _ = self.initial_arg_parser.parse_known_args()
|
||||
assert conf is not None
|
||||
@ -153,7 +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_Options_config = config_paths
|
||||
config[0].runtime_config = config_paths
|
||||
|
||||
config = ctsettings.validate_commandline_settings(config, self.manager)
|
||||
config = ctsettings.validate_file_settings(config)
|
||||
@ -182,7 +170,7 @@ class App:
|
||||
|
||||
if len(talkers) < 1:
|
||||
error = error = (
|
||||
f"Failed to load any talkers, please re-install and check the log located in '{self.config[0].Runtime_Options_config.user_log_dir}' for more details",
|
||||
f"Failed to load any talkers, please re-install and check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details",
|
||||
True,
|
||||
)
|
||||
|
||||
@ -195,31 +183,34 @@ class App:
|
||||
comicapi.utils.load_publishers()
|
||||
update_publishers(self.config)
|
||||
|
||||
if self.config[0].Commands_list_plugins:
|
||||
self.list_plugins(list(talkers.values()), comicapi.comicarchive.archivers)
|
||||
return
|
||||
|
||||
if self.config[0].Commands_only_save_config:
|
||||
# manage the CV API key
|
||||
# None comparison is used so that the empty string can unset the value
|
||||
if not error and (
|
||||
self.config[0].talker_comicvine_comicvine_key is not None # type: ignore[attr-defined]
|
||||
or self.config[0].talker_comicvine_comicvine_url is not None # type: ignore[attr-defined]
|
||||
):
|
||||
settings_path = self.config[0].runtime_config.user_config_dir / "settings.json"
|
||||
if self.config_load_success:
|
||||
self.manager.save_file(self.config[0], settings_path)
|
||||
|
||||
if self.config[0].commands_only_set_cv_key:
|
||||
if self.config_load_success:
|
||||
settings_path = self.config[0].Runtime_Options_config.user_config_dir / "settings.json"
|
||||
if self.config_load_success:
|
||||
self.manager.save_file(self.config[0], settings_path)
|
||||
print("Key set") # noqa: T201
|
||||
return
|
||||
|
||||
if not self.config_load_success:
|
||||
error = (
|
||||
f"Failed to load settings, check the log located in '{self.config[0].Runtime_Options_config.user_log_dir}' for more details",
|
||||
f"Failed to load settings, check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details",
|
||||
True,
|
||||
)
|
||||
|
||||
if not self.config[0].Runtime_Options_no_gui:
|
||||
if not self.config[0].runtime_no_gui:
|
||||
try:
|
||||
from comictaggerlib import gui
|
||||
|
||||
return gui.open_tagger_window(talkers, self.config, error)
|
||||
except ImportError:
|
||||
self.config[0].Runtime_Options_no_gui = True
|
||||
self.config[0].runtime_no_gui = True
|
||||
logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
|
||||
|
||||
# GUI mode is not available or CLI mode was requested
|
||||
|
@ -45,7 +45,7 @@ class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
uic.loadUi(ui_path / "matchselectionwindow.ui", self)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode, config.Runtime_Options_config.user_cache_dir, talker
|
||||
self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
|
@ -62,32 +62,32 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
self.rename_list: list[str] = []
|
||||
|
||||
self.btnSettings.clicked.connect(self.modify_settings)
|
||||
platform = "universal" if self.config[0].File_Rename_strict else "auto"
|
||||
self.renamer = FileRenamer(None, platform=platform, replacements=self.config[0].File_Rename_replacements)
|
||||
platform = "universal" if self.config[0].rename_strict else "auto"
|
||||
self.renamer = FileRenamer(None, platform=platform, replacements=self.config[0].rename_replacements)
|
||||
|
||||
self.do_preview()
|
||||
|
||||
def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str:
|
||||
self.renamer.set_template(self.config[0].File_Rename_template)
|
||||
self.renamer.set_issue_zero_padding(self.config[0].File_Rename_issue_number_padding)
|
||||
self.renamer.set_smart_cleanup(self.config[0].File_Rename_use_smart_string_cleanup)
|
||||
self.renamer.replacements = self.config[0].File_Rename_replacements
|
||||
self.renamer.set_template(self.config[0].rename_template)
|
||||
self.renamer.set_issue_zero_padding(self.config[0].rename_issue_number_padding)
|
||||
self.renamer.set_smart_cleanup(self.config[0].rename_use_smart_string_cleanup)
|
||||
self.renamer.replacements = self.config[0].rename_replacements
|
||||
|
||||
new_ext = ca.path.suffix # default
|
||||
if self.config[0].File_Rename_set_extension_based_on_archive:
|
||||
if self.config[0].rename_set_extension_based_on_archive:
|
||||
new_ext = ca.extension()
|
||||
|
||||
if md is None:
|
||||
md = ca.read_metadata(self.data_style)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.config[0].Filename_Parsing_complicated_parser,
|
||||
self.config[0].Filename_Parsing_remove_c2c,
|
||||
self.config[0].Filename_Parsing_remove_fcbd,
|
||||
self.config[0].Filename_Parsing_remove_publisher,
|
||||
self.config[0].filename_complicated_parser,
|
||||
self.config[0].filename_remove_c2c,
|
||||
self.config[0].filename_remove_fcbd,
|
||||
self.config[0].filename_remove_publisher,
|
||||
)
|
||||
self.renamer.set_metadata(md)
|
||||
self.renamer.move = self.config[0].File_Rename_move_to_dir
|
||||
self.renamer.move = self.config[0].rename_move_to_dir
|
||||
return new_ext
|
||||
|
||||
def do_preview(self) -> None:
|
||||
@ -100,7 +100,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
try:
|
||||
new_name = self.renamer.determine_name(new_ext)
|
||||
except ValueError as e:
|
||||
logger.exception("Invalid format string: %s", self.config[0].File_Rename_template)
|
||||
logger.exception("Invalid format string: %s", self.config[0].rename_template)
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"Invalid format string!",
|
||||
@ -114,7 +114,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"Formatter failure: %s metadata: %s", self.config[0].File_Rename_template, self.renamer.metadata
|
||||
"Formatter failure: %s metadata: %s", self.config[0].rename_template, self.renamer.metadata
|
||||
)
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
@ -190,7 +190,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
|
||||
folder = get_rename_dir(
|
||||
comic[0],
|
||||
self.config[0].File_Rename_dir if self.config[0].File_Rename_move_to_dir else None,
|
||||
self.config[0].rename_dir if self.config[0].rename_move_to_dir else None,
|
||||
)
|
||||
|
||||
full_path = folder / comic[1]
|
||||
|
@ -116,7 +116,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
uic.loadUi(ui_path / "seriesselectionwindow.ui", self)
|
||||
|
||||
self.imageWidget = CoverImageWidget(
|
||||
self.imageContainer, CoverImageWidget.URLMode, config.Runtime_Options_config.user_cache_dir, talker
|
||||
self.imageContainer, CoverImageWidget.URLMode, config.runtime_config.user_cache_dir, talker
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
|
||||
gridlayout.addWidget(self.imageWidget)
|
||||
@ -153,7 +153,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.comic_archive = comic_archive
|
||||
self.immediate_autoselect = autoselect
|
||||
self.cover_index_list = cover_index_list
|
||||
self.series_list: dict[str, ComicSeries] = {}
|
||||
self.series_list: list[ComicSeries] = []
|
||||
self.literal = literal
|
||||
self.ii: IssueIdentifier | None = None
|
||||
self.iddialog: IDProgressWindow | None = None
|
||||
@ -161,7 +161,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.progdialog: QtWidgets.QProgressDialog | None = None
|
||||
self.search_thread: SearchThread | None = None
|
||||
|
||||
self.use_filter = self.config.Issue_Identifier_always_use_publisher_filter
|
||||
self.use_filter = self.config.identifier_always_use_publisher_filter
|
||||
|
||||
# Load to retrieve settings
|
||||
self.talker = talker
|
||||
@ -172,7 +172,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.imageSourceWidget = CoverImageWidget(
|
||||
self.imageSourceLogo,
|
||||
CoverImageWidget.URLMode,
|
||||
config.Runtime_Options_config.user_cache_dir,
|
||||
config.runtime_config.user_cache_dir,
|
||||
talker,
|
||||
False,
|
||||
)
|
||||
@ -245,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)
|
||||
@ -332,7 +332,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
def show_issues(self) -> None:
|
||||
selector = IssueSelectionWindow(self, self.config, self.talker, self.series_id, self.issue_number)
|
||||
title = ""
|
||||
for series in self.series_list.values():
|
||||
for series in self.series_list:
|
||||
if series.id == self.series_id:
|
||||
title = f"{series.name} ({series.start_year:04}) - "
|
||||
break
|
||||
@ -349,18 +349,14 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
self.imageWidget.update_content()
|
||||
|
||||
def select_by_id(self) -> None:
|
||||
for r in range(self.twList.rows()):
|
||||
if self.series_id == self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole):
|
||||
for r, series in enumerate(self.series_list):
|
||||
if series.id == self.series_id:
|
||||
self.twList.selectRow(r)
|
||||
break
|
||||
|
||||
def perform_query(self, refresh: bool = False) -> None:
|
||||
self.search_thread = SearchThread(
|
||||
self.talker,
|
||||
self.series_name,
|
||||
refresh,
|
||||
self.literal,
|
||||
self.config.Issue_Identifier_series_match_search_thresh,
|
||||
self.talker, self.series_name, refresh, self.literal, self.config.identifier_series_match_search_thresh
|
||||
)
|
||||
self.search_thread.searchComplete.connect(self.search_complete)
|
||||
self.search_thread.progressUpdate.connect(self.search_progress_update)
|
||||
@ -408,18 +404,16 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
)
|
||||
return
|
||||
|
||||
tmp_list = self.search_thread.ct_search_results if self.search_thread is not None else []
|
||||
self.series_list = {x.id: x for x in tmp_list}
|
||||
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.Issue_Identifier_publisher_filter}
|
||||
publisher_filter = {s.strip().casefold() for s in self.config.identifier_publisher_filter}
|
||||
# use '' as publisher name if None
|
||||
self.series_list = dict(
|
||||
self.series_list = list(
|
||||
filter(
|
||||
lambda d: ("" if d[1].publisher is None else str(d[1].publisher).casefold())
|
||||
not in publisher_filter,
|
||||
self.series_list.items(),
|
||||
lambda d: ("" if d.publisher is None else str(d.publisher).casefold()) not in publisher_filter,
|
||||
self.series_list,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
@ -429,32 +423,30 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
# compare as str in case extra chars ie. '1976?'
|
||||
# - missing (none) values being converted to 'None' - consistent with prior behaviour in v1.2.3
|
||||
# sort by start_year if set
|
||||
if self.config.Issue_Identifier_sort_series_by_year:
|
||||
if self.config.identifier_sort_series_by_year:
|
||||
try:
|
||||
self.series_list = dict(
|
||||
natsort.natsorted(
|
||||
self.series_list.items(),
|
||||
key=lambda i: (str(i[1].start_year), str(i[1].count_of_issues)),
|
||||
reverse=True,
|
||||
)
|
||||
self.series_list = natsort.natsorted(
|
||||
self.series_list,
|
||||
key=lambda i: (str(i.start_year), str(i.count_of_issues)),
|
||||
reverse=True,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("bad data error sorting results by start_year,count_of_issues")
|
||||
else:
|
||||
try:
|
||||
self.series_list = dict(
|
||||
natsort.natsorted(self.series_list.items(), key=lambda i: str(i[1].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")
|
||||
|
||||
# move sanitized matches to the front
|
||||
if self.config.Issue_Identifier_exact_series_matches_first:
|
||||
if self.config.identifier_exact_series_matches_first:
|
||||
try:
|
||||
sanitized = utils.sanitize_title(self.series_name, False).casefold()
|
||||
sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold()
|
||||
|
||||
deques: list[deque[tuple[str, ComicSeries]]] = [deque(), deque(), deque()]
|
||||
deques: list[deque[ComicSeries]] = [deque(), deque(), deque()]
|
||||
|
||||
def categorize(result: ComicSeries) -> int:
|
||||
# We don't remove anything on this one so that we only get exact matches
|
||||
@ -466,10 +458,10 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
return 1
|
||||
return 2
|
||||
|
||||
for comic in self.series_list.items():
|
||||
deques[categorize(comic[1])].append(comic)
|
||||
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.series_list = dict(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")
|
||||
|
||||
@ -479,7 +471,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
self.twList.setRowCount(0)
|
||||
|
||||
for row, series in enumerate(self.series_list.values()):
|
||||
for row, series in enumerate(self.series_list):
|
||||
self.twList.insertRow(row)
|
||||
|
||||
item_text = series.name
|
||||
@ -561,14 +553,16 @@ 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
|
||||
series = self.series_list[self.series_id]
|
||||
if not (
|
||||
series.name
|
||||
and series.start_year
|
||||
and series.count_of_issues
|
||||
and series.publisher
|
||||
and series.description
|
||||
and series.image_url
|
||||
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:
|
||||
|
@ -188,7 +188,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.leRenameTemplate.setToolTip(f"<pre>{html.escape(template_tooltip)}</pre>")
|
||||
self.rename_error: Exception | None = None
|
||||
|
||||
self.sources = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs(
|
||||
self.sources: dict = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs(
|
||||
self.tComicTalkers, self.config, self.talkers
|
||||
)
|
||||
self.connect_signals()
|
||||
@ -307,45 +307,43 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.leRarExePath.setText(getattr(self.config[0], self.config[1]["archiver"].v["rar"].internal_name))
|
||||
else:
|
||||
self.leRarExePath.setEnabled(False)
|
||||
self.sbNameMatchIdentifyThresh.setValue(self.config[0].Issue_Identifier_series_match_identify_thresh)
|
||||
self.sbNameMatchSearchThresh.setValue(self.config[0].Issue_Identifier_series_match_search_thresh)
|
||||
self.tePublisherFilter.setPlainText("\n".join(self.config[0].Issue_Identifier_publisher_filter))
|
||||
self.sbNameMatchIdentifyThresh.setValue(self.config[0].identifier_series_match_identify_thresh)
|
||||
self.sbNameMatchSearchThresh.setValue(self.config[0].identifier_series_match_search_thresh)
|
||||
self.tePublisherFilter.setPlainText("\n".join(self.config[0].identifier_publisher_filter))
|
||||
|
||||
self.cbxCheckForNewVersion.setChecked(self.config[0].General_check_for_new_version)
|
||||
self.cbxCheckForNewVersion.setChecked(self.config[0].general_check_for_new_version)
|
||||
|
||||
self.cbxComplicatedParser.setChecked(self.config[0].Filename_Parsing_complicated_parser)
|
||||
self.cbxRemoveC2C.setChecked(self.config[0].Filename_Parsing_remove_c2c)
|
||||
self.cbxRemoveFCBD.setChecked(self.config[0].Filename_Parsing_remove_fcbd)
|
||||
self.cbxRemovePublisher.setChecked(self.config[0].Filename_Parsing_remove_publisher)
|
||||
self.cbxComplicatedParser.setChecked(self.config[0].filename_complicated_parser)
|
||||
self.cbxRemoveC2C.setChecked(self.config[0].filename_remove_c2c)
|
||||
self.cbxRemoveFCBD.setChecked(self.config[0].filename_remove_fcbd)
|
||||
self.cbxRemovePublisher.setChecked(self.config[0].filename_remove_publisher)
|
||||
self.switch_parser()
|
||||
|
||||
self.cbxClearFormBeforePopulating.setChecked(self.config[0].Issue_Identifier_clear_form_before_populating)
|
||||
self.cbxUseFilter.setChecked(self.config[0].Issue_Identifier_always_use_publisher_filter)
|
||||
self.cbxSortByYear.setChecked(self.config[0].Issue_Identifier_sort_series_by_year)
|
||||
self.cbxExactMatches.setChecked(self.config[0].Issue_Identifier_exact_series_matches_first)
|
||||
self.cbxClearFormBeforePopulating.setChecked(self.config[0].identifier_clear_form_before_populating)
|
||||
self.cbxUseFilter.setChecked(self.config[0].identifier_always_use_publisher_filter)
|
||||
self.cbxSortByYear.setChecked(self.config[0].identifier_sort_series_by_year)
|
||||
self.cbxExactMatches.setChecked(self.config[0].identifier_exact_series_matches_first)
|
||||
|
||||
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].Comic_Book_Lover_assume_lone_credit_is_primary)
|
||||
self.cbxCopyCharactersToTags.setChecked(self.config[0].Comic_Book_Lover_copy_characters_to_tags)
|
||||
self.cbxCopyTeamsToTags.setChecked(self.config[0].Comic_Book_Lover_copy_teams_to_tags)
|
||||
self.cbxCopyLocationsToTags.setChecked(self.config[0].Comic_Book_Lover_copy_locations_to_tags)
|
||||
self.cbxCopyStoryArcsToTags.setChecked(self.config[0].Comic_Book_Lover_copy_storyarcs_to_tags)
|
||||
self.cbxCopyNotesToComments.setChecked(self.config[0].Comic_Book_Lover_copy_notes_to_comments)
|
||||
self.cbxCopyWebLinkToComments.setChecked(self.config[0].Comic_Book_Lover_copy_weblink_to_comments)
|
||||
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.config[0].Comic_Book_Lover_apply_transform_on_import)
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setChecked(
|
||||
self.config[0].Comic_Book_Lover_apply_transform_on_bulk_operation
|
||||
)
|
||||
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].cbl_assume_lone_credit_is_primary)
|
||||
self.cbxCopyCharactersToTags.setChecked(self.config[0].cbl_copy_characters_to_tags)
|
||||
self.cbxCopyTeamsToTags.setChecked(self.config[0].cbl_copy_teams_to_tags)
|
||||
self.cbxCopyLocationsToTags.setChecked(self.config[0].cbl_copy_locations_to_tags)
|
||||
self.cbxCopyStoryArcsToTags.setChecked(self.config[0].cbl_copy_storyarcs_to_tags)
|
||||
self.cbxCopyNotesToComments.setChecked(self.config[0].cbl_copy_notes_to_comments)
|
||||
self.cbxCopyWebLinkToComments.setChecked(self.config[0].cbl_copy_weblink_to_comments)
|
||||
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.config[0].cbl_apply_transform_on_import)
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setChecked(self.config[0].cbl_apply_transform_on_bulk_operation)
|
||||
|
||||
self.leRenameTemplate.setText(self.config[0].File_Rename_template)
|
||||
self.leIssueNumPadding.setText(str(self.config[0].File_Rename_issue_number_padding))
|
||||
self.cbxSmartCleanup.setChecked(self.config[0].File_Rename_use_smart_string_cleanup)
|
||||
self.cbxChangeExtension.setChecked(self.config[0].File_Rename_set_extension_based_on_archive)
|
||||
self.cbxMoveFiles.setChecked(self.config[0].File_Rename_move_to_dir)
|
||||
self.leDirectory.setText(self.config[0].File_Rename_dir)
|
||||
self.cbxRenameStrict.setChecked(self.config[0].File_Rename_strict)
|
||||
self.leRenameTemplate.setText(self.config[0].rename_template)
|
||||
self.leIssueNumPadding.setText(str(self.config[0].rename_issue_number_padding))
|
||||
self.cbxSmartCleanup.setChecked(self.config[0].rename_use_smart_string_cleanup)
|
||||
self.cbxChangeExtension.setChecked(self.config[0].rename_set_extension_based_on_archive)
|
||||
self.cbxMoveFiles.setChecked(self.config[0].rename_move_to_dir)
|
||||
self.leDirectory.setText(self.config[0].rename_dir)
|
||||
self.cbxRenameStrict.setChecked(self.config[0].rename_strict)
|
||||
|
||||
for table, replacments in zip(
|
||||
(self.twLiteralReplacements, self.twValueReplacements), self.config[0].File_Rename_replacements
|
||||
(self.twLiteralReplacements, self.twValueReplacements), self.config[0].rename_replacements
|
||||
):
|
||||
table.clearContents()
|
||||
for i in reversed(range(table.rowCount())):
|
||||
@ -385,7 +383,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.rename_test()
|
||||
if self.rename_error is not None:
|
||||
if isinstance(self.rename_error, ValueError):
|
||||
logger.exception("Invalid format string: %s", self.config[0].File_Rename_template)
|
||||
logger.exception("Invalid format string: %s", self.config[0].rename_template)
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"Invalid format string!",
|
||||
@ -399,7 +397,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
return
|
||||
else:
|
||||
logger.exception(
|
||||
"Formatter failure: %s metadata: %s", self.config[0].File_Rename_template, self.renamer.metadata
|
||||
"Formatter failure: %s metadata: %s", self.config[0].rename_template, self.renamer.metadata
|
||||
)
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
@ -422,50 +420,48 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
if not str(self.leIssueNumPadding.text()).isdigit():
|
||||
self.leIssueNumPadding.setText("0")
|
||||
|
||||
self.config[0].General_check_for_new_version = self.cbxCheckForNewVersion.isChecked()
|
||||
self.config[0].general_check_for_new_version = self.cbxCheckForNewVersion.isChecked()
|
||||
|
||||
self.config[0].Issue_Identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
|
||||
self.config[0].Issue_Identifier_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
|
||||
self.config[0].Issue_Identifier_publisher_filter = utils.split(self.tePublisherFilter.toPlainText(), "\n")
|
||||
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 = utils.split(self.tePublisherFilter.toPlainText(), "\n")
|
||||
|
||||
self.config[0].Filename_Parsing_complicated_parser = self.cbxComplicatedParser.isChecked()
|
||||
self.config[0].Filename_Parsing_remove_c2c = self.cbxRemoveC2C.isChecked()
|
||||
self.config[0].Filename_Parsing_remove_fcbd = self.cbxRemoveFCBD.isChecked()
|
||||
self.config[0].Filename_Parsing_remove_publisher = self.cbxRemovePublisher.isChecked()
|
||||
self.config[0].filename_complicated_parser = self.cbxComplicatedParser.isChecked()
|
||||
self.config[0].filename_remove_c2c = self.cbxRemoveC2C.isChecked()
|
||||
self.config[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked()
|
||||
self.config[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked()
|
||||
|
||||
self.config[0].Issue_Identifier_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.config[0].Issue_Identifier_always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.config[0].Issue_Identifier_sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.config[0].Issue_Identifier_exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
self.config[0].identifier_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.config[0].identifier_always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.config[0].identifier_sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.config[0].identifier_exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
|
||||
self.config[0].Comic_Book_Lover_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.config[0].Comic_Book_Lover_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.config[0].Comic_Book_Lover_copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
|
||||
self.config[0].Comic_Book_Lover_copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.config[0].Comic_Book_Lover_copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
|
||||
self.config[0].Comic_Book_Lover_copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
|
||||
self.config[0].Comic_Book_Lover_copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.config[0].Comic_Book_Lover_apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
self.config.values.Comic_Book_Lover_apply_transform_on_bulk_operation = (
|
||||
self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
)
|
||||
self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.config[0].cbl_copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
|
||||
self.config[0].cbl_copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.config[0].cbl_copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
|
||||
self.config[0].cbl_copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
|
||||
self.config[0].cbl_copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.config[0].cbl_apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
self.config[0].cbl_apply_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
|
||||
self.config[0].File_Rename_template = str(self.leRenameTemplate.text())
|
||||
self.config[0].File_Rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.config[0].File_Rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.config[0].File_Rename_set_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
self.config[0].File_Rename_move_to_dir = self.cbxMoveFiles.isChecked()
|
||||
self.config[0].File_Rename_dir = self.leDirectory.text()
|
||||
self.config[0].rename_template = str(self.leRenameTemplate.text())
|
||||
self.config[0].rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.config[0].rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.config[0].rename_set_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
self.config[0].rename_move_to_dir = self.cbxMoveFiles.isChecked()
|
||||
self.config[0].rename_dir = self.leDirectory.text()
|
||||
|
||||
self.config[0].File_Rename_strict = self.cbxRenameStrict.isChecked()
|
||||
self.config[0].File_Rename_replacements = self.get_replacements()
|
||||
self.config[0].rename_strict = self.cbxRenameStrict.isChecked()
|
||||
self.config[0].rename_replacements = self.get_replacements()
|
||||
|
||||
# Read settings from talker tabs
|
||||
comictaggerlib.ui.talkeruigenerator.form_settings_to_config(self.sources, self.config)
|
||||
|
||||
self.update_talkers_config()
|
||||
|
||||
settngs.save_file(self.config, self.config[0].Runtime_Options_config.user_config_dir / "settings.json")
|
||||
settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json")
|
||||
self.parent().config = self.config
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
@ -478,8 +474,8 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.select_file(self.leRarExePath, "RAR")
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
ImageFetcher(self.config[0].Runtime_Options_config.user_cache_dir).clear_cache()
|
||||
ComicCacher(self.config[0].Runtime_Options_config.user_cache_dir, version).clear_cache()
|
||||
ImageFetcher(self.config[0].runtime_config.user_cache_dir).clear_cache()
|
||||
ComicCacher(self.config[0].runtime_config.user_cache_dir, version).clear_cache()
|
||||
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
|
||||
|
||||
def reset_settings(self) -> None:
|
||||
|
@ -156,10 +156,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
|
||||
if config[0].Runtime_Options_type and isinstance(config[0].Runtime_Options_type[0], int):
|
||||
if config[0].runtime_type and isinstance(config[0].runtime_type[0], int):
|
||||
# respect the command line option tag type
|
||||
config[0].internal_save_data_style = config[0].Runtime_Options_type[0]
|
||||
config[0].internal_load_data_style = config[0].Runtime_Options_type[0]
|
||||
config[0].internal_save_data_style = config[0].runtime_type[0]
|
||||
config[0].internal_load_data_style = config[0].runtime_type[0]
|
||||
|
||||
self.save_data_style = config[0].internal_save_data_style
|
||||
self.load_data_style = config[0].internal_load_data_style
|
||||
@ -212,7 +212,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
# hook up the callbacks
|
||||
self.cbLoadDataStyle.currentIndexChanged.connect(self.set_load_data_style)
|
||||
self.cbSaveDataStyle.currentIndexChanged.connect(self.set_save_data_style)
|
||||
self.cbx_sources.currentIndexChanged.connect(self.set_source)
|
||||
self.btnEditCredit.clicked.connect(self.edit_credit)
|
||||
self.btnAddCredit.clicked.connect(self.add_credit)
|
||||
self.btnRemoveCredit.clicked.connect(self.remove_credit)
|
||||
@ -246,7 +245,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if len(file_list) != 0:
|
||||
self.fileSelectionList.add_path_list(file_list)
|
||||
|
||||
if self.config[0].Dialog_Flags_show_disclaimer:
|
||||
if self.config[0].dialog_show_disclaimer:
|
||||
checked = OptionalMessageDialog.msg(
|
||||
self,
|
||||
"Welcome!",
|
||||
@ -265,15 +264,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
Have fun!
|
||||
""",
|
||||
)
|
||||
self.config[0].Dialog_Flags_show_disclaimer = not checked
|
||||
self.config[0].dialog_show_disclaimer = not checked
|
||||
|
||||
if self.config[0].General_check_for_new_version:
|
||||
if self.config[0].general_check_for_new_version:
|
||||
self.check_latest_version_online()
|
||||
|
||||
def current_talker(self) -> ComicTalker:
|
||||
if self.config[0].Sources_source in self.talkers:
|
||||
return self.talkers[self.config[0].Sources_source]
|
||||
logger.error("Could not find the '%s' talker", self.config[0].Sources_source)
|
||||
if self.config[0].talker_source in self.talkers:
|
||||
return self.talkers[self.config[0].talker_source]
|
||||
logger.error("Could not find the '%s' talker", self.config[0].talker_source)
|
||||
raise SystemExit(2)
|
||||
|
||||
def open_file_event(self, url: QtCore.QUrl) -> None:
|
||||
@ -286,7 +285,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
def setup_logger(self) -> ApplicationLogWindow:
|
||||
try:
|
||||
current_logs = (self.config[0].Runtime_Options_config.user_log_dir / "ComicTagger.log").read_text("utf-8")
|
||||
current_logs = (self.config[0].runtime_config.user_log_dir / "ComicTagger.log").read_text("utf-8")
|
||||
except Exception:
|
||||
current_logs = ""
|
||||
root_logger = logging.getLogger()
|
||||
@ -619,10 +618,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def actual_load_current_archive(self) -> None:
|
||||
if self.metadata.is_empty and self.comic_archive is not None:
|
||||
self.metadata = self.comic_archive.metadata_from_filename(
|
||||
self.config[0].Filename_Parsing_complicated_parser,
|
||||
self.config[0].Filename_Parsing_remove_c2c,
|
||||
self.config[0].Filename_Parsing_remove_fcbd,
|
||||
self.config[0].Filename_Parsing_remove_publisher,
|
||||
self.config[0].filename_complicated_parser,
|
||||
self.config[0].filename_remove_c2c,
|
||||
self.config[0].filename_remove_fcbd,
|
||||
self.config[0].filename_remove_publisher,
|
||||
)
|
||||
if len(self.metadata.pages) == 0 and self.comic_archive is not None:
|
||||
self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages())
|
||||
@ -795,9 +794,11 @@ 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.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.description)
|
||||
@ -909,9 +910,9 @@ 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())
|
||||
@ -968,10 +969,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
# copy the form onto metadata object
|
||||
self.form_to_metadata()
|
||||
new_metadata = self.comic_archive.metadata_from_filename(
|
||||
self.config[0].Filename_Parsing_complicated_parser,
|
||||
self.config[0].Filename_Parsing_remove_c2c,
|
||||
self.config[0].Filename_Parsing_remove_fcbd,
|
||||
self.config[0].Filename_Parsing_remove_publisher,
|
||||
self.config[0].filename_complicated_parser,
|
||||
self.config[0].filename_remove_c2c,
|
||||
self.config[0].filename_remove_fcbd,
|
||||
self.config[0].filename_remove_publisher,
|
||||
split_words,
|
||||
)
|
||||
if new_metadata is not None:
|
||||
@ -1080,10 +1081,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
else:
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
if new_metadata is not None:
|
||||
if self.config[0].Comic_Book_Lover_apply_transform_on_import:
|
||||
if self.config[0].cbl_apply_transform_on_import:
|
||||
new_metadata = CBLTransformer(new_metadata, self.config[0]).apply()
|
||||
|
||||
if self.config[0].Issue_Identifier_clear_form_before_populating:
|
||||
if self.config[0].identifier_clear_form_before_populating:
|
||||
self.clear_form()
|
||||
|
||||
notes = (
|
||||
@ -1094,7 +1095,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
new_metadata.replace(
|
||||
notes=utils.combine_notes(self.metadata.notes, notes, "Tagged with ComicTagger"),
|
||||
description=cleanup_html(
|
||||
new_metadata.description, self.config[0].Sources_remove_html_tables
|
||||
new_metadata.description, self.config[0].talker_remove_html_tables
|
||||
),
|
||||
)
|
||||
)
|
||||
@ -1156,9 +1157,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.update_style_tweaks()
|
||||
self.update_menus()
|
||||
|
||||
def set_source(self, s: int) -> None:
|
||||
self.config[0].Sources_source = self.cbx_sources.itemData(s)
|
||||
|
||||
def update_credit_colors(self) -> None:
|
||||
# !!!ATB qt5 porting TODO
|
||||
inactive_color = QtGui.QColor(255, 170, 150)
|
||||
@ -1376,7 +1374,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
settingswin.setModal(True)
|
||||
settingswin.exec()
|
||||
settingswin.result()
|
||||
self.adjust_source_combo()
|
||||
|
||||
def set_app_position(self) -> None:
|
||||
if self.config[0].internal_window_width != 0:
|
||||
@ -1387,9 +1384,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
size = self.frameGeometry()
|
||||
self.move(int((screen.width() - size.width()) / 2), int((screen.height() - size.height()) / 2))
|
||||
|
||||
def adjust_source_combo(self) -> None:
|
||||
self.cbx_sources.setCurrentIndex(self.cbx_sources.findData(self.config[0].Sources_source))
|
||||
|
||||
def adjust_load_style_combo(self) -> None:
|
||||
# select the current style
|
||||
if self.load_data_style == MetaDataStyle.CBI:
|
||||
@ -1415,11 +1409,6 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.cbSaveDataStyle.addItem("ComicRack", MetaDataStyle.CIX)
|
||||
self.adjust_save_style_combo()
|
||||
|
||||
# Add talker entries
|
||||
for t_id, talker in self.talkers.items():
|
||||
self.cbx_sources.addItem(talker.name, t_id)
|
||||
self.adjust_source_combo()
|
||||
|
||||
# Add the entries to the country combobox
|
||||
self.cbCountry.addItem("", "")
|
||||
for f in natsort.humansorted(utils.countries().items(), operator.itemgetter(1)):
|
||||
@ -1649,10 +1638,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if ca.has_metadata(src_style) and ca.is_writable():
|
||||
md = ca.read_metadata(src_style)
|
||||
|
||||
if (
|
||||
dest_style == MetaDataStyle.CBI
|
||||
and self.config[0].Comic_Book_Lover_apply_transform_on_bulk_operation
|
||||
):
|
||||
if dest_style == MetaDataStyle.CBI and self.config[0].cbl_apply_transform_on_bulk_operation:
|
||||
md = CBLTransformer(md, self.config[0]).apply()
|
||||
|
||||
if not ca.write_metadata(md, dest_style):
|
||||
@ -1690,7 +1676,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
logger.exception("Save aborted.")
|
||||
|
||||
if not ct_md.is_empty:
|
||||
if self.config[0].Comic_Book_Lover_apply_transform_on_import:
|
||||
if self.config[0].cbl_apply_transform_on_import:
|
||||
ct_md = CBLTransformer(ct_md, self.config[0]).apply()
|
||||
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
@ -1720,10 +1706,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.config[0].Filename_Parsing_complicated_parser,
|
||||
self.config[0].Filename_Parsing_remove_c2c,
|
||||
self.config[0].Filename_Parsing_remove_fcbd,
|
||||
self.config[0].Filename_Parsing_remove_publisher,
|
||||
self.config[0].filename_complicated_parser,
|
||||
self.config[0].filename_remove_c2c,
|
||||
self.config[0].filename_remove_fcbd,
|
||||
self.config[0].filename_remove_publisher,
|
||||
dlg.split_words,
|
||||
)
|
||||
if dlg.ignore_leading_digits_in_filename and md.series is not None:
|
||||
@ -1739,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"
|
||||
@ -1809,7 +1795,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if self.config[0].Issue_Identifier_auto_imprint:
|
||||
if self.config[0].identifier_auto_imprint:
|
||||
md.fix_publisher()
|
||||
|
||||
if not ca.write_metadata(md, self.save_data_style):
|
||||
@ -1995,7 +1981,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.config[0].internal_sort_column,
|
||||
self.config[0].internal_sort_direction,
|
||||
) = self.fileSelectionList.get_sorting()
|
||||
settngs.save_file(self.config, self.config[0].Runtime_Options_config.user_config_dir / "settings.json")
|
||||
settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json")
|
||||
|
||||
event.accept()
|
||||
else:
|
||||
@ -2122,7 +2108,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.version_check_complete(version_checker.get_latest_version(self.config[0].internal_install_id))
|
||||
|
||||
def version_check_complete(self, new_version: tuple[str, str]) -> None:
|
||||
if new_version[0] not in (self.version, self.config[0].Dialog_Flags_dont_notify_about_this_version):
|
||||
if new_version[0] not in (self.version, self.config[0].dialog_dont_notify_about_this_version):
|
||||
website = "https://github.com/comictagger/comictagger"
|
||||
checked = OptionalMessageDialog.msg(
|
||||
self,
|
||||
@ -2133,7 +2119,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
"Don't tell me about this version again",
|
||||
)
|
||||
if checked:
|
||||
self.config[0].Dialog_Flags_dont_notify_about_this_version = new_version[0]
|
||||
self.config[0].dialog_dont_notify_about_this_version = new_version[0]
|
||||
|
||||
def on_incoming_socket_connection(self) -> None:
|
||||
# Accept connection from other instance.
|
||||
|
@ -59,36 +59,26 @@
|
||||
<property name="formAlignment">
|
||||
<set>Qt::AlignHCenter|Qt::AlignTop</set>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Read Style</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="cbLoadDataStyle"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="saveStyleLabel">
|
||||
<property name="text">
|
||||
<string>Modify Style</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="cbSaveDataStyle"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lbl_md_source">
|
||||
<property name="text">
|
||||
<string>Metadata Source</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="cbx_sources"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -2,12 +2,12 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import Any, NamedTuple, cast
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import settngs
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from comictaggerlib.ctsettings import ct_ns, group_for_plugin
|
||||
from comictaggerlib.ctsettings import ct_ns
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
from comictalker.comictalker import ComicTalker
|
||||
|
||||
@ -16,15 +16,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class TalkerTab(NamedTuple):
|
||||
tab: QtWidgets.QWidget
|
||||
# dict[option.dest] = QWidget
|
||||
widgets: dict[str, QtWidgets.QWidget]
|
||||
|
||||
|
||||
class Sources(NamedTuple):
|
||||
cbx_sources: QtWidgets.QComboBox
|
||||
tabs: list[tuple[ComicTalker, TalkerTab]]
|
||||
|
||||
|
||||
class PasswordEdit(QtWidgets.QLineEdit):
|
||||
"""
|
||||
Password LineEdit with icons to show/hide password entries.
|
||||
@ -44,11 +38,11 @@ class PasswordEdit(QtWidgets.QLineEdit):
|
||||
# Add the password hide/shown toggle at the end of the edit box.
|
||||
self.togglepasswordAction = self.addAction(self.visibleIcon, QtWidgets.QLineEdit.TrailingPosition)
|
||||
self.togglepasswordAction.setToolTip("Show password")
|
||||
self.togglepasswordAction.triggered.connect(self.on_toggle_password_action)
|
||||
self.togglepasswordAction.triggered.connect(self.on_toggle_password_Action)
|
||||
|
||||
self.password_shown = False
|
||||
|
||||
def on_toggle_password_action(self) -> None:
|
||||
def on_toggle_password_Action(self) -> None:
|
||||
if not self.password_shown:
|
||||
self.setEchoMode(QtWidgets.QLineEdit.Normal)
|
||||
self.password_shown = True
|
||||
@ -62,16 +56,14 @@ class PasswordEdit(QtWidgets.QLineEdit):
|
||||
|
||||
|
||||
def generate_api_widgets(
|
||||
talker: ComicTalker,
|
||||
widgets: TalkerTab,
|
||||
key_option: settngs.Setting,
|
||||
url_option: settngs.Setting,
|
||||
talker_id: str,
|
||||
sources: dict[str, QtWidgets.QWidget],
|
||||
config: settngs.Config[ct_ns],
|
||||
layout: QtWidgets.QGridLayout,
|
||||
talkers: dict[str, ComicTalker],
|
||||
) -> None:
|
||||
# *args enforces keyword arguments and allows position arguments to be ignored
|
||||
def call_check_api(
|
||||
*args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker: ComicTalker
|
||||
) -> None:
|
||||
def call_check_api(*args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker_id: str) -> None:
|
||||
url = ""
|
||||
key = ""
|
||||
if le_key is not None:
|
||||
@ -79,43 +71,46 @@ def generate_api_widgets(
|
||||
if le_url is not None:
|
||||
url = le_url.text().strip()
|
||||
|
||||
check_text, check_bool = talker.check_api_key(url, key)
|
||||
check_text, check_bool = talkers[talker_id].check_api_key(url, key)
|
||||
if check_bool:
|
||||
QtWidgets.QMessageBox.information(None, "API Test Success", check_text)
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(None, "API Test Failed", check_text)
|
||||
|
||||
# get the actual config objects in case they have overwritten the default
|
||||
talker_key = config[1][f"talker_{talker_id}"][1][f"{talker_id}_key"]
|
||||
talker_url = config[1][f"talker_{talker_id}"][1][f"{talker_id}_url"]
|
||||
btn_test_row = None
|
||||
le_key = None
|
||||
le_url = None
|
||||
|
||||
# only file settings are saved
|
||||
if key_option.file:
|
||||
# record the current row, so we know where to add the button
|
||||
if talker_key.file:
|
||||
# record the current row so we know where to add the button
|
||||
btn_test_row = layout.rowCount()
|
||||
le_key = generate_password_textbox(key_option, layout)
|
||||
le_key = generate_password_textbox(talker_key, layout)
|
||||
|
||||
# To enable setting and getting
|
||||
widgets.widgets[key_option.dest] = le_key
|
||||
sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_key"] = le_key
|
||||
|
||||
# only file settings are saved
|
||||
if url_option.file:
|
||||
# record the current row, so we know where to add the button
|
||||
if talker_url.file:
|
||||
# record the current row so we know where to add the button
|
||||
# We overwrite so that the default will be next to the url text box
|
||||
btn_test_row = layout.rowCount()
|
||||
le_url = generate_textbox(url_option, layout)
|
||||
# We insert the default url here so that people don't think it's unset
|
||||
le_url.setText(talker.default_api_url)
|
||||
le_url = generate_textbox(talker_url, layout)
|
||||
value, _ = settngs.get_option(config[0], talker_url)
|
||||
if not value:
|
||||
le_url.setText(talkers[talker_id].default_api_url)
|
||||
# To enable setting and getting
|
||||
widgets.widgets[url_option.dest] = le_url
|
||||
sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_url"] = le_url
|
||||
|
||||
# The button row was recorded so we add it
|
||||
if btn_test_row is not None:
|
||||
btn = QtWidgets.QPushButton("Test API")
|
||||
layout.addWidget(btn, btn_test_row, 2)
|
||||
# partial is used as connect will pass in event information
|
||||
btn.clicked.connect(partial(call_check_api, le_url=le_url, le_key=le_key, talker=talker))
|
||||
btn.clicked.connect(partial(call_check_api, le_url=le_url, le_key=le_key, talker_id=talker_id))
|
||||
|
||||
|
||||
def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox:
|
||||
@ -176,39 +171,31 @@ def generate_password_textbox(option: settngs.Setting, layout: QtWidgets.QGridLa
|
||||
return widget
|
||||
|
||||
|
||||
def settings_to_talker_form(sources: Sources, config: settngs.Config[ct_ns]) -> None:
|
||||
def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[ct_ns]) -> None:
|
||||
# Set the active talker via id in sources combo box
|
||||
sources[0].setCurrentIndex(sources[0].findData(config[0].Sources_source))
|
||||
sources["cbx_select_talker"].setCurrentIndex(sources["cbx_select_talker"].findData(config[0].talker_source))
|
||||
|
||||
# Iterate over the tabs, the talker is included in the tab so no extra lookup is needed
|
||||
for talker, tab in sources.tabs:
|
||||
# dest is guaranteed to be unique within a talker
|
||||
# and refer to the correct item in config.definitions.v['group name']
|
||||
for dest, widget in tab.widgets.items():
|
||||
value, default = settngs.get_option(config.values, config.definitions[group_for_plugin(talker)].v[dest])
|
||||
for talker in sources["tabs"].items():
|
||||
for name, widget in talker[1].widgets.items():
|
||||
value = getattr(config[0], name)
|
||||
value_type = type(value)
|
||||
try:
|
||||
if isinstance(value, str) and value and isinstance(widget, QtWidgets.QLineEdit) and not default:
|
||||
if value_type is str and value:
|
||||
widget.setText(value)
|
||||
if isinstance(value, (float, int)) and isinstance(
|
||||
widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)
|
||||
):
|
||||
if value_type is int or value_type is float:
|
||||
widget.setValue(value)
|
||||
if isinstance(value, bool) and isinstance(widget, QtWidgets.QCheckBox):
|
||||
if value_type is bool:
|
||||
widget.setChecked(value)
|
||||
except Exception:
|
||||
logger.debug("Failed to set value of %s for %s(%s)", dest, talker.name, talker.id)
|
||||
logger.debug("Failed to set value of %s", name)
|
||||
|
||||
|
||||
def form_settings_to_config(sources: Sources, config: settngs.Config) -> settngs.Config[ct_ns]:
|
||||
# Update the currently selected talker
|
||||
config.values.Sources_source = sources.cbx_sources.currentData()
|
||||
cfg = settngs.normalize_config(config, True, True)
|
||||
def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[ct_ns]) -> None:
|
||||
# Source combo box value
|
||||
config[0].talker_source = sources["cbx_select_talker"].currentData()
|
||||
|
||||
# Iterate over the tabs, the talker is included in the tab so no extra lookup is needed
|
||||
for talker, tab in sources.tabs:
|
||||
talker_options = cfg.values[group_for_plugin(talker)]
|
||||
# dest is guaranteed to be unique within a talker and refer to the correct item in config.values['group name']
|
||||
for dest, widget in tab.widgets.items():
|
||||
for tab in sources["tabs"].items():
|
||||
for name, widget in tab[1].widgets.items():
|
||||
widget_value = None
|
||||
if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)):
|
||||
widget_value = widget.value()
|
||||
@ -217,88 +204,83 @@ def form_settings_to_config(sources: Sources, config: settngs.Config) -> settngs
|
||||
elif isinstance(widget, QtWidgets.QCheckBox):
|
||||
widget_value = widget.isChecked()
|
||||
|
||||
talker_options[dest] = widget_value
|
||||
return cast(settngs.Config[ct_ns], settngs.get_namespace(cfg, True, True))
|
||||
setattr(config[0], name, widget_value)
|
||||
|
||||
|
||||
def generate_source_option_tabs(
|
||||
comic_talker_tab: QtWidgets.QWidget,
|
||||
config: settngs.Config[ct_ns],
|
||||
talkers: dict[str, ComicTalker],
|
||||
) -> Sources:
|
||||
) -> dict[str, QtWidgets.QWidget]:
|
||||
"""
|
||||
Generate GUI tabs and settings for talkers
|
||||
"""
|
||||
|
||||
# Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget
|
||||
sources: dict = {"tabs": {}}
|
||||
|
||||
# Tab comes with a QVBoxLayout
|
||||
comic_talker_tab_layout = comic_talker_tab.layout()
|
||||
|
||||
talker_layout = QtWidgets.QGridLayout()
|
||||
lbl_select_talker = QtWidgets.QLabel("Metadata Source:")
|
||||
cbx_select_talker = QtWidgets.QComboBox()
|
||||
line = QtWidgets.QFrame()
|
||||
line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
line.setFrameShadow(QtWidgets.QFrame.Sunken)
|
||||
talker_tabs = QtWidgets.QTabWidget()
|
||||
|
||||
# Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget
|
||||
sources: Sources = Sources(QtWidgets.QComboBox(), [])
|
||||
|
||||
talker_layout.addWidget(lbl_select_talker, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
|
||||
talker_layout.addWidget(sources[0], 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
|
||||
talker_layout.addWidget(cbx_select_talker, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
|
||||
talker_layout.addWidget(line, 1, 0, 1, -1)
|
||||
talker_layout.addWidget(talker_tabs, 2, 0, 1, -1)
|
||||
|
||||
comic_talker_tab_layout.addLayout(talker_layout)
|
||||
|
||||
# Add source sub tabs to Comic Sources tab
|
||||
for t_id, talker in talkers.items():
|
||||
# Add source to general tab dropdown list
|
||||
sources.cbx_sources.addItem(talker.name, t_id)
|
||||
tab = TalkerTab(tab=QtWidgets.QWidget(), widgets={})
|
||||
# Add combobox to sources for getting and setting talker
|
||||
sources["cbx_select_talker"] = cbx_select_talker
|
||||
|
||||
# Add source sub tabs to Comic Sources tab
|
||||
for talker_id, talker_obj in talkers.items():
|
||||
# Add source to general tab dropdown list
|
||||
cbx_select_talker.addItem(talker_obj.name, talker_id)
|
||||
|
||||
tab_name = talker_id
|
||||
sources["tabs"][tab_name] = TalkerTab(tab=QtWidgets.QWidget(), widgets={})
|
||||
layout_grid = QtWidgets.QGridLayout()
|
||||
url_option: settngs.Setting | None = None
|
||||
key_option: settngs.Setting | None = None
|
||||
for option in config.definitions[group_for_plugin(talker)].v.values():
|
||||
|
||||
for option in config[1][f"talker_{talker_id}"][1].values():
|
||||
if not option.file:
|
||||
continue
|
||||
elif option.dest == f"{t_id}_key":
|
||||
key_option = option
|
||||
elif option.dest == f"{t_id}_url":
|
||||
url_option = option
|
||||
elif option._guess_type() is bool:
|
||||
if option.dest in (f"{talker_id}_url", f"{talker_id}_key"):
|
||||
continue
|
||||
current_widget = None
|
||||
if option._guess_type() is bool:
|
||||
current_widget = generate_checkbox(option, layout_grid)
|
||||
tab.widgets[option.dest] = current_widget
|
||||
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
|
||||
elif option._guess_type() is int:
|
||||
current_widget = generate_spinbox(option, layout_grid)
|
||||
tab.widgets[option.dest] = current_widget
|
||||
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
|
||||
elif option._guess_type() is float:
|
||||
current_widget = generate_doublespinbox(option, layout_grid)
|
||||
tab.widgets[option.dest] = current_widget
|
||||
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
|
||||
|
||||
elif option._guess_type() is str:
|
||||
current_widget = generate_textbox(option, layout_grid)
|
||||
tab.widgets[option.dest] = current_widget
|
||||
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
|
||||
else:
|
||||
logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}")
|
||||
|
||||
# The key and url options are always defined.
|
||||
# If they aren't something has gone wrong with the talker, remove it
|
||||
if key_option is None or url_option is None:
|
||||
del talkers[t_id]
|
||||
continue
|
||||
|
||||
# Add talker URL and API key fields
|
||||
generate_api_widgets(talker, tab, key_option, url_option, layout_grid)
|
||||
generate_api_widgets(talker_id, sources, config, layout_grid, talkers)
|
||||
|
||||
# Add vertical spacer
|
||||
vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0)
|
||||
# Display the new widgets
|
||||
tab.tab.setLayout(layout_grid)
|
||||
sources["tabs"][tab_name].tab.setLayout(layout_grid)
|
||||
|
||||
# Add new sub tab to Comic Source tab
|
||||
talker_tabs.addTab(tab.tab, talker.name)
|
||||
sources.tabs.append((talker, tab))
|
||||
talker_tabs.addTab(sources["tabs"][tab_name].tab, talker_obj.name)
|
||||
|
||||
return sources
|
||||
|
@ -16,28 +16,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sqlite3
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from typing_extensions import NamedTuple
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import ComicSeries, Credit, Date, GenericMetadata, TagOrigin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Series(NamedTuple):
|
||||
id: str
|
||||
data: bytes
|
||||
|
||||
|
||||
class Issue(NamedTuple):
|
||||
id: str
|
||||
series_id: str
|
||||
data: bytes
|
||||
|
||||
|
||||
class ComicCacher:
|
||||
def __init__(self, cache_folder: pathlib.Path, version: str) -> None:
|
||||
self.cache_folder = cache_folder
|
||||
@ -83,43 +74,71 @@ class ComicCacher:
|
||||
# create tables
|
||||
with con:
|
||||
cur = con.cursor()
|
||||
# source,name,id,start_year,publisher,image,description,count_of_issues
|
||||
cur.execute(
|
||||
"""CREATE TABLE SeriesSearchCache(
|
||||
timestamp DATE DEFAULT (datetime('now','localtime')),
|
||||
id TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
search_term TEXT,
|
||||
PRIMARY KEY (id, source, search_term))"""
|
||||
"CREATE TABLE SeriesSearchCache("
|
||||
+ "timestamp DATE DEFAULT (datetime('now','localtime')),"
|
||||
+ "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 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,
|
||||
data BLOB,
|
||||
complete BOOL,
|
||||
PRIMARY KEY (id, source))"""
|
||||
"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,"
|
||||
+ "count_of_volumes INT,"
|
||||
+ "start_year INT,"
|
||||
+ "image_url TEXT,"
|
||||
+ "aliases TEXT," # Newline separated
|
||||
+ "description TEXT,"
|
||||
+ "genres TEXT," # Newline separated. For filtering etc.
|
||||
+ "format TEXT,"
|
||||
+ "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,
|
||||
data BLOB,
|
||||
complete BOOL,
|
||||
PRIMARY KEY (id, source))"""
|
||||
"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,"
|
||||
+ "aliases TEXT," # Newline separated
|
||||
+ "alt_image_urls TEXT," # Newline separated URLs
|
||||
+ "characters TEXT," # Newline separated
|
||||
+ "locations TEXT," # Newline separated
|
||||
+ "credits TEXT," # JSON: "{"name": "Bob Shakespeare", "role": "Writer"}"
|
||||
+ "teams TEXT," # Newline separated
|
||||
+ "story_arcs TEXT," # Newline separated
|
||||
+ "genres TEXT," # Newline separated
|
||||
+ "tags TEXT," # Newline separated
|
||||
+ "critical_rating FLOAT,"
|
||||
+ "manga TEXT," # Yes/YesAndRightToLeft/No
|
||||
+ "maturity_rating TEXT,"
|
||||
+ "language TEXT,"
|
||||
+ "country TEXT,"
|
||||
+ "volume TEXT,"
|
||||
+ "complete BOOL," # Is the data complete? Includes characters, locations, credits.
|
||||
+ "PRIMARY KEY (id, source))"
|
||||
)
|
||||
|
||||
def expire_stale_records(self, cur: sqlite3.Cursor, table: str) -> None:
|
||||
# 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)])
|
||||
def add_search_results(self, source: TagOrigin, search_term: str, series_list: list[ComicSeries]) -> None:
|
||||
self.add_source(source)
|
||||
|
||||
def add_search_results(self, source: str, search_term: str, series_list: list[Series], complete: bool) -> None:
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
con.text_factory = str
|
||||
@ -128,80 +147,154 @@ class ComicCacher:
|
||||
# remove all previous entries with this search term
|
||||
cur.execute(
|
||||
"DELETE FROM SeriesSearchCache WHERE search_term = ? AND source = ?",
|
||||
[search_term.casefold(), source],
|
||||
[search_term.casefold(), source.id],
|
||||
)
|
||||
|
||||
# now add in new results
|
||||
for series in series_list:
|
||||
for record in series_list:
|
||||
cur.execute(
|
||||
"INSERT INTO SeriesSearchCache (source, search_term, id) VALUES(?, ?, ?)",
|
||||
(source, search_term.casefold(), series.id),
|
||||
(source.id, search_term.casefold(), record.id),
|
||||
)
|
||||
|
||||
data = {
|
||||
"id": series.id,
|
||||
"source": source,
|
||||
"data": series.data,
|
||||
"complete": complete,
|
||||
"id": record.id,
|
||||
"source": source.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,
|
||||
"description": record.description,
|
||||
"genres": "\n".join(record.genres),
|
||||
"format": record.format,
|
||||
"timestamp": datetime.datetime.now(),
|
||||
"aliases": "\n".join(record.aliases),
|
||||
}
|
||||
self.upsert(cur, "series", data)
|
||||
|
||||
def add_series_info(self, source: str, series: Series, complete: bool) -> None:
|
||||
def add_series_info(self, source: TagOrigin, series: ComicSeries) -> None:
|
||||
self.add_source(source)
|
||||
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
|
||||
timestamp = datetime.datetime.now()
|
||||
|
||||
data = {
|
||||
"id": series.id,
|
||||
"source": source,
|
||||
"data": series.data,
|
||||
"complete": complete,
|
||||
"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.aliases),
|
||||
}
|
||||
self.upsert(cur, "series", data)
|
||||
|
||||
def add_issues_info(self, source: str, issues: list[Issue], complete: bool) -> None:
|
||||
def add_series_issues_info(self, source: TagOrigin, issues: list[GenericMetadata], complete: bool) -> None:
|
||||
self.add_source(source)
|
||||
|
||||
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 issues:
|
||||
data = {
|
||||
"id": issue.id,
|
||||
"id": issue.issue_id,
|
||||
"series_id": issue.series_id,
|
||||
"data": issue.data,
|
||||
"source": source,
|
||||
"source": source.id,
|
||||
"name": issue.title,
|
||||
"issue_number": issue.issue,
|
||||
"volume": issue.volume,
|
||||
"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.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),
|
||||
"story_arcs": "\n".join(issue.story_arcs),
|
||||
"genres": "\n".join(issue.genres),
|
||||
"tags": "\n".join(issue.tags),
|
||||
"critical_rating": issue.critical_rating,
|
||||
"manga": issue.manga,
|
||||
"maturity_rating": issue.maturity_rating,
|
||||
"language": issue.language,
|
||||
"country": issue.country,
|
||||
"credits": json.dumps(issue.credits),
|
||||
"complete": complete,
|
||||
}
|
||||
self.upsert(cur, "issues", data)
|
||||
|
||||
def get_search_results(self, source: str, search_term: str, expire_stale: bool = True) -> list[tuple[Series, bool]]:
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
if expire_stale:
|
||||
self.expire_stale_records(cur, "SeriesSearchCache")
|
||||
self.expire_stale_records(cur, "Series")
|
||||
|
||||
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],
|
||||
"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 = Series(id=record["id"], data=record["data"])
|
||||
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, record["complete"]))
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def get_series_info(self, series_id: str, source: str, expire_stale: bool = True) -> tuple[Series, bool] | None:
|
||||
result: Series | None = None
|
||||
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
|
||||
@ -209,64 +302,169 @@ class ComicCacher:
|
||||
con.text_factory = str
|
||||
|
||||
if expire_stale:
|
||||
self.expire_stale_records(cur, "Series")
|
||||
# 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=?", [series_id, source])
|
||||
cur.execute("SELECT * FROM Series WHERE id=? AND source=?", [series_id, source.id])
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
return result
|
||||
|
||||
result = Series(id=row["id"], data=row["data"])
|
||||
# since ID is primary key, there is only one row
|
||||
result = ComicSeries(
|
||||
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, row["complete"])
|
||||
return result
|
||||
|
||||
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, False) or ComicSeries(
|
||||
id=series_id,
|
||||
name="",
|
||||
description="",
|
||||
genres=[],
|
||||
image_url="",
|
||||
publisher="",
|
||||
start_year=None,
|
||||
aliases=[],
|
||||
count_of_issues=None,
|
||||
count_of_volumes=None,
|
||||
format=None,
|
||||
)
|
||||
|
||||
def get_series_issues_info(
|
||||
self, series_id: str, source: str, expire_stale: bool = True
|
||||
) -> list[tuple[Issue, bool]]:
|
||||
with sqlite3.connect(self.db_file) as con:
|
||||
con.row_factory = sqlite3.Row
|
||||
cur = con.cursor()
|
||||
con.text_factory = str
|
||||
|
||||
if expire_stale:
|
||||
self.expire_stale_records(cur, "Issues")
|
||||
# purge stale issue info - probably issue data won't change
|
||||
# much....
|
||||
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
|
||||
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
|
||||
|
||||
# fetch
|
||||
results: list[tuple[Issue, bool]] = []
|
||||
results: list[tuple[GenericMetadata, bool]] = []
|
||||
|
||||
cur.execute("SELECT * FROM Issues WHERE series_id=? AND source=?", [series_id, source])
|
||||
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:
|
||||
record = (Issue(id=row["id"], series_id=row["series_id"], data=row["data"]), row["complete"])
|
||||
record = self.map_row_metadata(row, series, source)
|
||||
|
||||
results.append(record)
|
||||
|
||||
return results
|
||||
|
||||
def get_issue_info(self, issue_id: int, source: str, expire_stale: bool = True) -> tuple[Issue, bool] | None:
|
||||
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
|
||||
|
||||
if expire_stale:
|
||||
self.expire_stale_records(cur, "Issues")
|
||||
# purge stale issue info - probably issue data won't change
|
||||
# much....
|
||||
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=?", [issue_id, source])
|
||||
cur.execute("SELECT * FROM Issues WHERE id=? AND source=?", [issue_id, source.id])
|
||||
row = cur.fetchone()
|
||||
|
||||
record = None
|
||||
|
||||
if row:
|
||||
record = (Issue(id=row["id"], series_id=row["series_id"], data=row["data"]), row["complete"])
|
||||
# get_series_info should only fail if someone is doing something weird
|
||||
series = self.get_series_info(row["id"], source, False) or ComicSeries(
|
||||
id=row["id"],
|
||||
name="",
|
||||
description="",
|
||||
genres=[],
|
||||
image_url="",
|
||||
publisher="",
|
||||
start_year=None,
|
||||
aliases=[],
|
||||
count_of_issues=None,
|
||||
count_of_volumes=None,
|
||||
format=None,
|
||||
)
|
||||
|
||||
record = self.map_row_metadata(row, series, source)
|
||||
|
||||
return record
|
||||
|
||||
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,7 +19,7 @@ from typing import Any, Callable
|
||||
|
||||
import settngs
|
||||
|
||||
from comicapi.genericmetadata import ComicSeries, GenericMetadata
|
||||
from comicapi.genericmetadata import ComicSeries, GenericMetadata, TagOrigin
|
||||
from comictalker.talker_utils import fix_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -107,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>"
|
||||
|
@ -30,10 +30,10 @@ from pyrate_limiter import Limiter, RequestRate
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import ComicSeries, GenericMetadata, TagOrigin
|
||||
from comicapi.genericmetadata import ComicSeries, Date, GenericMetadata, TagOrigin
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictalker import talker_utils
|
||||
from comictalker.comiccacher import ComicCacher, Issue, Series
|
||||
from comictalker.comiccacher import ComicCacher
|
||||
from comictalker.comictalker import ComicTalker, TalkerDataError, TalkerNetworkError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -110,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
|
||||
@ -129,7 +130,6 @@ 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
|
||||
@ -159,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>"
|
||||
@ -243,10 +244,10 @@ 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 self._format_search_results([json.loads(x[0].data) for x in cached_search_results])
|
||||
return cached_search_results
|
||||
|
||||
params = { # CV uses volume to mean series
|
||||
"api_key": self.api_key,
|
||||
@ -316,12 +317,7 @@ class ComicVineTalker(ComicTalker):
|
||||
|
||||
# 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,
|
||||
[Series(id=str(x["id"]), data=json.dumps(x).encode("utf-8")) for x in search_results],
|
||||
False,
|
||||
)
|
||||
cvc.add_search_results(self.origin, series_name, formatted_search_results)
|
||||
|
||||
return formatted_search_results
|
||||
|
||||
@ -337,7 +333,7 @@ class ComicVineTalker(ComicTalker):
|
||||
return comic_data
|
||||
|
||||
def fetch_series(self, series_id: str) -> ComicSeries:
|
||||
return self._fetch_series_data(int(series_id))[0]
|
||||
return self._fetch_series_data(int(series_id))
|
||||
|
||||
def fetch_issues_in_series(self, series_id: str) -> list[GenericMetadata]:
|
||||
return [x[0] for x in self._fetch_issues_in_series(series_id)]
|
||||
@ -357,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,
|
||||
}
|
||||
|
||||
@ -382,7 +378,7 @@ class ComicVineTalker(ComicTalker):
|
||||
current_result_count += cv_response["number_of_page_results"]
|
||||
|
||||
formatted_filtered_issues_result = [
|
||||
self._map_comic_issue_to_metadata(x, self._fetch_series_data(x["volume"]["id"])[0])
|
||||
self.map_comic_issue_to_metadata(x, self._fetch_series_data(x["volume"]["id"]))
|
||||
for x in filtered_issues_result
|
||||
]
|
||||
|
||||
@ -447,52 +443,49 @@ class ComicVineTalker(ComicTalker):
|
||||
def _format_search_results(self, search_results: list[CVSeries]) -> list[ComicSeries]:
|
||||
formatted_results = []
|
||||
for record in search_results:
|
||||
formatted_results.append(self._format_series(record))
|
||||
# Flatten publisher to name only
|
||||
if record.get("publisher") is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = record["publisher"].get("name", "")
|
||||
|
||||
if record.get("image") is None:
|
||||
image_url = ""
|
||||
else:
|
||||
image_url = record["image"].get("super_url", "")
|
||||
|
||||
start_year = utils.xlate_int(record.get("start_year", ""))
|
||||
|
||||
aliases = record.get("aliases") or ""
|
||||
|
||||
formatted_results.append(
|
||||
ComicSeries(
|
||||
aliases=utils.split(aliases, "\n"),
|
||||
count_of_issues=record.get("count_of_issues", 0),
|
||||
count_of_volumes=None,
|
||||
description=record.get("description", ""),
|
||||
id=str(record["id"]),
|
||||
image_url=image_url,
|
||||
name=record["name"],
|
||||
publisher=pub_name,
|
||||
start_year=start_year,
|
||||
genres=[],
|
||||
format=None,
|
||||
)
|
||||
)
|
||||
|
||||
return formatted_results
|
||||
|
||||
def _format_series(self, record) -> ComicSeries:
|
||||
# Flatten publisher to name only
|
||||
if record.get("publisher") is None:
|
||||
pub_name = ""
|
||||
else:
|
||||
pub_name = record["publisher"].get("name", "")
|
||||
|
||||
if record.get("image") is None:
|
||||
image_url = ""
|
||||
else:
|
||||
image_url = record["image"].get("super_url", "")
|
||||
|
||||
start_year = utils.xlate_int(record.get("start_year", ""))
|
||||
|
||||
aliases = record.get("aliases") or ""
|
||||
|
||||
return ComicSeries(
|
||||
aliases=utils.split(aliases, "\n"),
|
||||
count_of_issues=record.get("count_of_issues", 0),
|
||||
count_of_volumes=None,
|
||||
description=record.get("description", ""),
|
||||
id=str(record["id"]),
|
||||
image_url=image_url,
|
||||
name=record["name"],
|
||||
publisher=pub_name,
|
||||
start_year=start_year,
|
||||
genres=[],
|
||||
format=None,
|
||||
)
|
||||
|
||||
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.id)
|
||||
cached_series_issues_result = cvc.get_series_issues_info(series_id, self.origin)
|
||||
|
||||
series = self._fetch_series_data(int(series_id))[0]
|
||||
series = self._fetch_series_data(int(series_id))
|
||||
|
||||
if len(cached_series_issues_result) == series.count_of_issues:
|
||||
return [
|
||||
(self._map_comic_issue_to_metadata(json.loads(x[0].data), series), x[1])
|
||||
for x in cached_series_issues_result
|
||||
]
|
||||
# Remove internal "complete" bool
|
||||
return cached_series_issues_result
|
||||
|
||||
params = { # CV uses volume to mean series
|
||||
"api_key": self.api_key,
|
||||
@ -521,27 +514,20 @@ class ComicVineTalker(ComicTalker):
|
||||
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"])[0])
|
||||
self.map_comic_issue_to_metadata(x, self._fetch_series_data(x["volume"]["id"]))
|
||||
for x in series_issues_result
|
||||
]
|
||||
|
||||
cvc.add_issues_info(
|
||||
self.id,
|
||||
[
|
||||
Issue(id=str(x["id"]), series_id=series_id, data=json.dumps(x).encode("utf-8"))
|
||||
for x in series_issues_result
|
||||
],
|
||||
False,
|
||||
)
|
||||
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) -> tuple[ComicSeries, bool]:
|
||||
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 = cvc.get_series_info(str(series_id), self.id)
|
||||
cached_series_result = cvc.get_series_info(str(series_id), self.origin)
|
||||
|
||||
if cached_series is not None:
|
||||
return (self._format_series(json.loads(cached_series[0].data)), cached_series[1])
|
||||
if cached_series_result is not None:
|
||||
return cached_series_result
|
||||
|
||||
series_url = urljoin(self.api_url, f"volume/{CVTypeID.Volume}-{series_id}") # CV uses volume to mean series
|
||||
|
||||
@ -552,13 +538,12 @@ class ComicVineTalker(ComicTalker):
|
||||
cv_response: CVResult[CVSeries] = self._get_cv_content(series_url, params)
|
||||
|
||||
series_results = cv_response["results"]
|
||||
formatted_series_results = self._format_search_results([series_results])
|
||||
|
||||
if series_results:
|
||||
cvc.add_series_info(
|
||||
self.id, Series(id=str(series_results["id"]), data=json.dumps(series_results).encode("utf-8")), True
|
||||
)
|
||||
cvc.add_series_info(self.origin, formatted_series_results[0])
|
||||
|
||||
return self._format_series(series_results), True
|
||||
return formatted_series_results[0]
|
||||
|
||||
def _fetch_issue_data(self, series_id: int, issue_number: str) -> GenericMetadata:
|
||||
issues_list_results = self._fetch_issues_in_series(str(series_id))
|
||||
@ -583,12 +568,10 @@ class ComicVineTalker(ComicTalker):
|
||||
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_issue = cvc.get_issue_info(int(issue_id), self.id)
|
||||
cached_issues_result = cvc.get_issue_info(int(issue_id), self.origin)
|
||||
|
||||
if cached_issue and cached_issue[1]:
|
||||
return self._map_comic_issue_to_metadata(
|
||||
json.loads(cached_issue[0].data), self._fetch_series_data(int(cached_issue[0].series_id))[0]
|
||||
)
|
||||
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"}
|
||||
@ -596,26 +579,19 @@ class ComicVineTalker(ComicTalker):
|
||||
|
||||
issue_results = cv_response["results"]
|
||||
|
||||
cvc.add_issues_info(
|
||||
self.id,
|
||||
[
|
||||
Issue(
|
||||
id=str(issue_results["id"]),
|
||||
series_id=str(issue_results["volume"]["id"]),
|
||||
data=json.dumps(issue_results).encode("utf-8"),
|
||||
)
|
||||
],
|
||||
True,
|
||||
# Format to expected output
|
||||
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 self._map_comic_issue_to_metadata(
|
||||
issue_results, self._fetch_series_data(int(issue_results["volume"]["id"]))[0]
|
||||
)
|
||||
return cv_issues
|
||||
|
||||
def _map_comic_issue_to_metadata(self, issue: CVIssue, series: ComicSeries) -> GenericMetadata:
|
||||
def map_comic_issue_to_metadata(self, issue: CVIssue, series: ComicSeries) -> GenericMetadata:
|
||||
md = GenericMetadata(
|
||||
tag_origin=TagOrigin(self.id, self.name),
|
||||
tag_origin=self.origin,
|
||||
issue_id=utils.xlate(issue.get("id")),
|
||||
series_id=series.id,
|
||||
title_aliases=utils.split(issue.get("aliases"), "\n"),
|
||||
@ -629,6 +605,8 @@ class ComicVineTalker(ComicTalker):
|
||||
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 = ""
|
||||
@ -662,9 +640,10 @@ class ComicVineTalker(ComicTalker):
|
||||
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.day, md.month, md.year = utils.parse_date_str(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.year = utils.xlate_int(series.start_year)
|
||||
md.cover_date.year = utils.xlate_int(series.start_year)
|
||||
|
||||
return md
|
||||
|
@ -41,10 +41,10 @@ install_requires =
|
||||
pathvalidate
|
||||
pillow>=9.1.0,<10
|
||||
pycountry
|
||||
pyrate-limiter>=2.6,<3
|
||||
pyrate-limiter
|
||||
rapidfuzz>=2.12.0
|
||||
requests==2.*
|
||||
settngs==0.7.2
|
||||
settngs==0.7.1
|
||||
text2digits
|
||||
typing-extensions>=4.3.0
|
||||
wordninja
|
||||
|
@ -4,7 +4,7 @@ import comicapi.genericmetadata
|
||||
from comicapi import utils
|
||||
|
||||
search_results = [
|
||||
dict(
|
||||
comicapi.genericmetadata.ComicSeries(
|
||||
count_of_issues=1,
|
||||
count_of_volumes=1,
|
||||
description="this is a description",
|
||||
@ -17,7 +17,7 @@ search_results = [
|
||||
genres=[],
|
||||
format=None,
|
||||
),
|
||||
dict(
|
||||
comicapi.genericmetadata.ComicSeries(
|
||||
count_of_issues=1,
|
||||
count_of_volumes=1,
|
||||
description="this is a description",
|
||||
|
@ -173,9 +173,8 @@ 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 [],
|
||||
month=date[1],
|
||||
year=date[2],
|
||||
day=date[0],
|
||||
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"],
|
||||
@ -198,9 +197,7 @@ cv_md = comicapi.genericmetadata.GenericMetadata(
|
||||
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],
|
||||
cover_date=comicapi.genericmetadata.Date.parse_date(cv_issue_result["results"]["cover_date"]),
|
||||
issue_count=cv_volume_result["results"]["count_of_issues"],
|
||||
volume=None,
|
||||
genres=[],
|
||||
|
@ -87,38 +87,6 @@ names = [
|
||||
},
|
||||
(False, False),
|
||||
),
|
||||
(
|
||||
"action comics 1024.cbz",
|
||||
"issue number is current year (digits == 4)",
|
||||
{
|
||||
"issue": "1024",
|
||||
"series": "action comics",
|
||||
"title": "",
|
||||
"publisher": "",
|
||||
"volume": "",
|
||||
"year": "",
|
||||
"remainder": "",
|
||||
"issue_count": "",
|
||||
"alternate": "",
|
||||
},
|
||||
(False, False),
|
||||
),
|
||||
(
|
||||
"Action Comics 1001 (2018).cbz",
|
||||
"issue number is current year (digits == 4)",
|
||||
{
|
||||
"issue": "1001",
|
||||
"series": "Action Comics",
|
||||
"title": "",
|
||||
"publisher": "",
|
||||
"volume": "",
|
||||
"year": "2018",
|
||||
"remainder": "",
|
||||
"issue_count": "",
|
||||
"alternate": "",
|
||||
},
|
||||
(False, False),
|
||||
),
|
||||
(
|
||||
"january jones #2.cbz",
|
||||
"month in series",
|
||||
@ -735,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,
|
||||
|
@ -60,7 +60,7 @@ def test_save_cbi(tmp_comic):
|
||||
|
||||
|
||||
@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support")
|
||||
def test_save_cix_rar(tmp_path, md_saved):
|
||||
def test_save_cix_rar(tmp_path):
|
||||
cbr_path = datadir / "fake_cbr.cbr"
|
||||
shutil.copy(cbr_path, tmp_path)
|
||||
|
||||
@ -69,7 +69,7 @@ def test_save_cix_rar(tmp_path, md_saved):
|
||||
assert tmp_comic.write_cix(comicapi.genericmetadata.md_test)
|
||||
|
||||
md = tmp_comic.read_cix()
|
||||
assert md.replace(pages=[], page_count=0) == md_saved.replace(pages=[], page_count=0)
|
||||
assert md.replace(pages=[]) == comicapi.genericmetadata.md_test.replace(pages=[])
|
||||
|
||||
|
||||
@pytest.mark.xfail(not (comicapi.archivers.rar.rar_support and shutil.which("rar")), reason="rar support")
|
||||
@ -95,12 +95,12 @@ def test_save_cbi_rar(tmp_path, md_saved):
|
||||
manga=None,
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
story_arc=None,
|
||||
series_group=None,
|
||||
scan_info=None,
|
||||
characters=[],
|
||||
teams=[],
|
||||
locations=[],
|
||||
characters=None,
|
||||
teams=None,
|
||||
locations=None,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,37 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
import comictalker.comiccacher
|
||||
from comicapi.genericmetadata import TagOrigin
|
||||
from testing.comicdata import search_results
|
||||
|
||||
|
||||
def test_create_cache(config, mock_version):
|
||||
config, definitions = config
|
||||
comictalker.comiccacher.ComicCacher(config.Runtime_Options_config.user_cache_dir, mock_version[0])
|
||||
assert config.Runtime_Options_config.user_cache_dir.exists()
|
||||
comictalker.comiccacher.ComicCacher(config.runtime_config.user_cache_dir, mock_version[0])
|
||||
assert config.runtime_config.user_cache_dir.exists()
|
||||
|
||||
|
||||
def test_search_results(comic_cache):
|
||||
comic_cache.add_search_results(
|
||||
"test",
|
||||
"test search",
|
||||
[comictalker.comiccacher.Series(id=x["id"], data=json.dumps(x)) for x in search_results],
|
||||
True,
|
||||
)
|
||||
cached_results = [json.loads(x[0].data) for x in comic_cache.get_search_results("test", "test search")]
|
||||
assert search_results == cached_results
|
||||
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=comictalker.comiccacher.Series(id=series_info["id"], data=json.dumps(series_info)),
|
||||
source="test",
|
||||
complete=True,
|
||||
)
|
||||
comic_cache.add_series_info(series=series_info, source=TagOrigin("test", "test"))
|
||||
vi = series_info.copy()
|
||||
cache_result = json.loads(comic_cache.get_series_info(series_id=series_info["id"], source="test")[0].data)
|
||||
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 json
|
||||
|
||||
import pytest
|
||||
|
||||
import comicapi.genericmetadata
|
||||
@ -9,34 +7,27 @@ 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")[0]
|
||||
cache_series = comic_cache.get_search_results(
|
||||
comicvine_api.id, "cory doctorows futuristic tales of the here and now"
|
||||
)[0][0]
|
||||
series_results = comicvine_api._format_series(json.loads(cache_series.data))
|
||||
assert results == series_results
|
||||
results = comicvine_api.search_for_series("cory doctorows futuristic tales of the here and now")
|
||||
cache_issues = comic_cache.get_search_results(
|
||||
comicvine_api.origin, "cory doctorows futuristic tales of the here and now"
|
||||
)
|
||||
assert results == cache_issues
|
||||
|
||||
|
||||
def test_fetch_series(comicvine_api, comic_cache):
|
||||
result = comicvine_api.fetch_series(23437)
|
||||
cache_series = comic_cache.get_series_info(23437, comicvine_api.id)[0]
|
||||
series_result = comicvine_api._format_series(json.loads(cache_series.data))
|
||||
assert result == series_result
|
||||
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.origin)
|
||||
# del cache_result["description"]
|
||||
# del cache_result["image_url"]
|
||||
assert result == cache_result
|
||||
|
||||
|
||||
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.id)
|
||||
issues_results = [
|
||||
comicvine_api._map_comic_issue_to_metadata(
|
||||
json.loads(x[0].data),
|
||||
comicvine_api._format_series(
|
||||
json.loads(comic_cache.get_series_info(x[0].series_id, comicvine_api.id)[0].data)
|
||||
),
|
||||
)
|
||||
for x in cache_issues
|
||||
]
|
||||
assert results == issues_results
|
||||
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):
|
||||
|
@ -117,7 +117,7 @@ def comicvine_api(monkeypatch, cbz, comic_cache, mock_version, config) -> comict
|
||||
|
||||
cv = comictalker.talkers.comicvine.ComicVineTalker(
|
||||
version=mock_version[0],
|
||||
cache_folder=config[0].Runtime_Options_config.user_cache_dir,
|
||||
cache_folder=config[0].runtime_config.user_cache_dir,
|
||||
)
|
||||
manager = settngs.Manager()
|
||||
manager.add_persistent_group("comicvine", cv.register_settings)
|
||||
@ -145,8 +145,8 @@ def md():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def md_saved():
|
||||
yield comicapi.genericmetadata.md_test.replace(tag_origin=None, issue_id=None, series_id=None)
|
||||
def md_saved(md):
|
||||
yield md.replace(tag_origin=None, issue_id=None, series_id=None)
|
||||
|
||||
|
||||
# manually seeds publishers
|
||||
@ -174,14 +174,14 @@ def config(tmp_path):
|
||||
app.register_settings()
|
||||
|
||||
defaults = app.parse_settings(comictaggerlib.ctsettings.ComicTaggerPaths(tmp_path / "config"), "")
|
||||
defaults[0].Runtime_Options_config.user_data_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].Runtime_Options_config.user_config_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].Runtime_Options_config.user_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].Runtime_Options_config.user_state_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].Runtime_Options_config.user_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].runtime_config.user_data_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].runtime_config.user_config_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].runtime_config.user_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].runtime_config.user_state_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults[0].runtime_config.user_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
yield defaults
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def comic_cache(config, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]:
|
||||
yield comictalker.comiccacher.ComicCacher(config[0].Runtime_Options_config.user_cache_dir, mock_version[0])
|
||||
yield comictalker.comiccacher.ComicCacher(config[0].runtime_config.user_cache_dir, mock_version[0])
|
||||
|
@ -5,6 +5,9 @@ import io
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
import comicapi.comicarchive
|
||||
import comicapi.genericmetadata
|
||||
import comicapi.issuestring
|
||||
import comictaggerlib.issueidentifier
|
||||
import testing.comicdata
|
||||
import testing.comicvine
|
||||
@ -60,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"],
|
||||
|
@ -17,7 +17,7 @@ def test_cbi(md_saved):
|
||||
string = CBI.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CBI.metadata_from_string(string)
|
||||
md_test = md_saved.replace(
|
||||
day=None,
|
||||
cover_date=md_saved.cover_date.replace(day=None),
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arcs=[],
|
||||
@ -44,7 +44,7 @@ def test_comet(md_saved):
|
||||
string = CBI.string_from_metadata(comicapi.genericmetadata.md_test)
|
||||
md = CBI.metadata_from_string(string)
|
||||
md_test = md_saved.replace(
|
||||
day=None,
|
||||
cover_date=md_saved.cover_date.replace(day=None),
|
||||
story_arcs=[],
|
||||
series_groups=[],
|
||||
scan_info=None,
|
||||
|
@ -218,18 +218,3 @@ urls = [
|
||||
@pytest.mark.parametrize("value, result", urls)
|
||||
def test_fix_url(value, result):
|
||||
assert comictalker.talker_utils.fix_url(value) == result
|
||||
|
||||
|
||||
split = [
|
||||
(("1,2,,3", ","), ["1", "2", "3"]),
|
||||
(("1 ,2,,3", ","), ["1", "2", "3"]),
|
||||
(("1 ,2,,3 ", ","), ["1", "2", "3"]),
|
||||
(("\n1 \n2\n\n3 ", ","), ["1 \n2\n\n3"]),
|
||||
(("\n1 \n2\n\n3 ", "\n"), ["1", "2", "3"]),
|
||||
((None, ","), []),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value, result", split)
|
||||
def test_split(value, result):
|
||||
assert comicapi.utils.split(*value) == result
|
||||
|
Reference in New Issue
Block a user