Compare commits

..

1 Commits

Author SHA1 Message Date
54d733ef74 Support separate cover and store dates in ComicAPI 2023-08-04 22:39:30 -07:00
42 changed files with 1094 additions and 1018 deletions

View File

@ -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]

View File

@ -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)

View File

@ -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"))

View File

@ -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:

View File

@ -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)))

View File

@ -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"), ",")

View File

@ -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"],

View File

@ -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

View File

@ -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()

View File

@ -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 = ""

View File

@ -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)

View File

@ -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",
]

View File

@ -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"):

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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,

View File

@ -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])

View File

@ -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

View File

@ -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)

View File

@ -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]

View File

@ -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:

View File

@ -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:

View File

@ -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.

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>"

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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=[],

View File

@ -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,

View File

@ -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,
)

View File

@ -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

View File

@ -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):

View File

@ -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])

View File

@ -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"],

View File

@ -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,

View File

@ -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