Update setting group names

Make group names presentable to users and add builtin plugins during namespace generation.
Revamp talkeruigenerator.py to use generated group and setting names and remove as much hard-coded strings as possible
Add a --list-plugins commandline option
This commit is contained in:
Timmy Welch 2023-09-05 03:55:12 -04:00
parent 90eb1c3980
commit 05e6eaf88e
23 changed files with 532 additions and 450 deletions

View File

@ -9,6 +9,7 @@ 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

@ -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_config.user_cache_dir, talker
self.altCoverContainer, CoverImageWidget.AltCoverMode, config.Runtime_Options_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_complicated_parser,
self.config.filename_remove_c2c,
self.config.filename_remove_fcbd,
self.config.filename_remove_publisher,
self.config.Filename_Parsing_complicated_parser,
self.config.Filename_Parsing_remove_c2c,
self.config.Filename_Parsing_remove_fcbd,
self.config.Filename_Parsing_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.identifier_series_match_identify_thresh)
self.sbNameMatchSearchThresh.setValue(self.config.Issue_Identifier_series_match_identify_thresh)
self.leSearchString.setEnabled(False)
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)
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)
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.identifier_series_match_search_thresh
self.name_length_match_tolerance = self.config.Issue_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.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
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
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.cbl_assume_lone_credit_is_primary:
if self.config.Comic_Book_Lover_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.cbl_copy_characters_to_tags:
if self.config.Comic_Book_Lover_copy_characters_to_tags:
self.metadata.tags.update(x for x in self.metadata.characters)
if self.config.cbl_copy_teams_to_tags:
if self.config.Comic_Book_Lover_copy_teams_to_tags:
self.metadata.tags.update(x for x in self.metadata.teams)
if self.config.cbl_copy_locations_to_tags:
if self.config.Comic_Book_Lover_copy_locations_to_tags:
self.metadata.tags.update(x for x in self.metadata.locations)
if self.config.cbl_copy_storyarcs_to_tags:
if self.config.Comic_Book_Lover_copy_storyarcs_to_tags:
self.metadata.tags.update(x for x in self.metadata.story_arcs)
if self.config.cbl_copy_notes_to_comments:
if self.config.Comic_Book_Lover_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.cbl_copy_weblink_to_comments:
if self.config.Comic_Book_Lover_copy_weblink_to_comments:
if self.metadata.web_link is not None:
if self.metadata.description is None:
self.metadata.description = ""

View File

@ -46,9 +46,9 @@ class CLI:
self.batch_mode = False
def current_talker(self) -> ComicTalker:
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)
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)
raise SystemExit(2)
def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata:
@ -59,14 +59,14 @@ class CLI:
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
return GenericMetadata()
if self.config.cbl_apply_transform_on_import:
if self.config.Comic_Book_Lover_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_dryrun:
for metadata_style in self.config.runtime_type:
if not self.config.Runtime_Options_dryrun:
for metadata_style in self.config.Runtime_Options_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])
@ -75,7 +75,7 @@ class CLI:
print("Save complete.")
logger.info("Save complete.")
else:
if self.config.runtime_quiet:
if self.config.Runtime_Options_quiet:
logger.info("dry-run option was set, so nothing was written")
print("dry-run option was set, so nothing was written")
else:
@ -102,7 +102,7 @@ class CLI:
m["issue_title"],
)
)
if self.config.runtime_interactive:
if self.config.Runtime_Options_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":
@ -113,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.identifier_clear_metadata_on_import:
if self.config.Issue_Identifier_clear_metadata_on_import:
md = ct_md
else:
notes = (
@ -122,14 +122,14 @@ class CLI:
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
if self.config.identifier_auto_imprint:
if self.config.Issue_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_summary:
if self.config.Runtime_Options_summary:
if len(match_results.good_matches) > 0:
print("\nSuccessful matches:\n------------------")
for f in match_results.good_matches:
@ -150,7 +150,7 @@ class CLI:
for f in match_results.fetch_data_failures:
print(f)
if not self.config.runtime_summary and not self.config.runtime_interactive:
if not self.config.Runtime_Options_summary and not self.config.Runtime_Options_interactive:
# just quit if we're not interactive or showing the summary
return
@ -170,14 +170,14 @@ class CLI:
self.display_match_set_for_choice(label, match_set)
def run(self) -> None:
if len(self.config.runtime_files) < 1:
if len(self.config.Runtime_Options_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_files) > 1
self.batch_mode = len(self.config.Runtime_Options_files) > 1
for f in self.config.runtime_files:
for f in self.config.Runtime_Options_files:
self.process_file_cli(f, match_results)
sys.stdout.flush()
@ -190,18 +190,18 @@ class CLI:
md.set_default_page_list(ca.get_number_of_pages())
# now, overlay the parsed filename info
if self.config.runtime_parse_filename:
if self.config.Runtime_Options_parse_filename:
f_md = ca.metadata_from_filename(
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,
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,
)
md.overlay(f_md)
for metadata_style in self.config.runtime_type:
for metadata_style in self.config.Runtime_Options_type:
if ca.has_metadata(metadata_style):
try:
t_md = ca.read_metadata(metadata_style)
@ -211,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_metadata)
md.overlay(self.config.Runtime_Options_metadata)
return md
def print(self, ca: ComicArchive) -> None:
if not self.config.runtime_type:
if not self.config.Runtime_Options_type:
page_count = ca.get_number_of_pages()
brief = ""
@ -246,38 +246,38 @@ class CLI:
print(brief)
if self.config.runtime_quiet:
if self.config.Runtime_Options_quiet:
return
print()
if not self.config.runtime_type or MetaDataStyle.CIX in self.config.runtime_type:
if not self.config.Runtime_Options_type or MetaDataStyle.CIX in self.config.Runtime_Options_type:
if ca.has_metadata(MetaDataStyle.CIX):
print("--------- ComicRack tags ---------")
try:
if self.config.runtime_raw:
if self.config.Runtime_Options_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_type or MetaDataStyle.CBI in self.config.runtime_type:
if not self.config.Runtime_Options_type or MetaDataStyle.CBI in self.config.Runtime_Options_type:
if ca.has_metadata(MetaDataStyle.CBI):
print("------- ComicBookLover tags -------")
try:
if self.config.runtime_raw:
if self.config.Runtime_Options_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_type or MetaDataStyle.COMET in self.config.runtime_type:
if not self.config.Runtime_Options_type or MetaDataStyle.COMET in self.config.Runtime_Options_type:
if ca.has_metadata(MetaDataStyle.COMET):
print("----------- CoMet tags -----------")
try:
if self.config.runtime_raw:
if self.config.Runtime_Options_raw:
print(ca.read_raw_comet())
else:
print(ca.read_comet())
@ -285,10 +285,10 @@ class CLI:
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_type:
for metadata_style in self.config.Runtime_Options_type:
style_name = MetaDataStyle.name[metadata_style]
if ca.has_metadata(metadata_style):
if not self.config.runtime_dryrun:
if not self.config.Runtime_Options_dryrun:
if not ca.remove_metadata(metadata_style):
print(f"{ca.path}: Tag removal seemed to fail!")
else:
@ -299,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_type:
for metadata_style in self.config.Runtime_Options_type:
dst_style_name = MetaDataStyle.name[metadata_style]
if not self.config.runtime_overwrite and ca.has_metadata(metadata_style):
if not self.config.Runtime_Options_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_dryrun:
src_style_name = MetaDataStyle.name[self.config.Commands_copy]
if ca.has_metadata(self.config.Commands_copy):
if not self.config.Runtime_Options_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.cbl_apply_transform_on_bulk_operation == MetaDataStyle.CBI:
if self.config.Comic_Book_Lover_apply_transform_on_bulk_operation == MetaDataStyle.CBI:
md = CBLTransformer(md, self.config).apply()
if not ca.write_metadata(md, metadata_style):
@ -330,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_overwrite:
for metadata_style in self.config.runtime_type:
if not self.config.Runtime_Options_overwrite:
for metadata_style in self.config.Runtime_Options_type:
if ca.has_metadata(metadata_style):
print(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
return
@ -341,26 +341,26 @@ class CLI:
md = self.create_local_metadata(ca)
if md.issue is None or md.issue == "":
if self.config.autotag_assume_1_if_no_issue_num:
if self.config.Auto_Tag_assume_1_if_no_issue_num:
md.issue = "1"
# now, search online
if self.config.runtime_online:
if self.config.runtime_issue_id is not None:
if self.config.Runtime_Options_online:
if self.config.Runtime_Options_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_issue_id)
ct_md = self.current_talker().fetch_comic_data(self.config.Runtime_Options_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_issue_id)
logger.error("No match for ID %s was found.", self.config.Runtime_Options_issue_id)
match_results.no_matches.append(str(ca.path.absolute()))
return
if self.config.cbl_apply_transform_on_import:
if self.config.Comic_Book_Lover_apply_transform_on_import:
ct_md = CBLTransformer(ct_md, self.config).apply()
else:
if md is None or md.is_empty:
@ -371,7 +371,7 @@ class CLI:
ii = IssueIdentifier(ca, self.config, self.current_talker())
def myoutput(text: str) -> None:
if self.config.runtime_verbose:
if self.config.Runtime_Options_verbose:
IssueIdentifier.default_write_output(text)
# use our overlaid MD struct to search
@ -411,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_abort_on_low_confidence:
if low_confidence and self.config.Runtime_Options_abort_on_low_confidence:
logger.error("Online search: Low confidence match. Save aborted")
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
return
@ -428,7 +428,7 @@ class CLI:
match_results.fetch_data_failures.append(str(ca.path.absolute()))
return
if self.config.identifier_clear_metadata_on_import:
if self.config.Issue_Identifier_clear_metadata_on_import:
md = GenericMetadata()
notes = (
@ -438,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.talker_remove_html_tables),
description=cleanup_html(ct_md.description, self.config.Sources_remove_html_tables),
)
)
if self.config.identifier_auto_imprint:
if self.config.Issue_Identifier_auto_imprint:
md.fix_publisher()
# ok, done building our metadata. time to save
@ -464,18 +464,18 @@ class CLI:
return
new_ext = "" # default
if self.config.rename_set_extension_based_on_archive:
if self.config.File_Rename_set_extension_based_on_archive:
new_ext = ca.extension()
renamer = FileRenamer(
md,
platform="universal" if self.config.rename_strict else "auto",
replacements=self.config.rename_replacements,
platform="universal" if self.config.File_Rename_strict else "auto",
replacements=self.config.File_Rename_replacements,
)
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
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
try:
new_name = renamer.determine_name(ext=new_ext)
@ -487,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.rename_template,
self.config.File_Rename_template,
)
return
except Exception:
logger.exception("Formatter failure: %s metadata: %s", self.config.rename_template, renamer.metadata)
logger.exception("Formatter failure: %s metadata: %s", self.config.File_Rename_template, renamer.metadata)
return
folder = get_rename_dir(ca, self.config.rename_dir if self.config.rename_move_to_dir else None)
folder = get_rename_dir(ca, self.config.File_Rename_dir if self.config.File_Rename_move_to_dir else None)
full_path = folder / new_name
@ -503,7 +503,7 @@ class CLI:
return
suffix = ""
if not self.config.runtime_dryrun:
if not self.config.Runtime_Options_dryrun:
# rename the file
try:
ca.rename(utils.unique_file(full_path))
@ -526,7 +526,7 @@ class CLI:
filename_path = ca.path
new_file = filename_path.with_suffix(".cbz")
if self.config.runtime_abort_on_conflict and new_file.exists():
if self.config.Runtime_Options_abort_on_conflict and new_file.exists():
print(msg_hdr + f"{new_file.name} already exists in the that folder.")
return
@ -534,10 +534,10 @@ class CLI:
delete_success = False
export_success = False
if not self.config.runtime_dryrun:
if not self.config.Runtime_Options_dryrun:
if ca.export_as_zip(new_file):
export_success = True
if self.config.runtime_delete_after_zip_export:
if self.config.Runtime_Options_delete_after_zip_export:
try:
filename_path.unlink(missing_ok=True)
delete_success = True
@ -549,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_delete_after_zip_export:
if self.config.Runtime_Options_delete_after_zip_export:
msg += " and delete original."
print(msg)
return
@ -557,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_delete_after_zip_export and delete_success:
if self.config.Runtime_Options_delete_after_zip_export and delete_success:
msg += " (Original deleted) "
else:
msg += "Archive failed to export!"
@ -576,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 register_plugin_settings, validate_plugin_settings
from comictaggerlib.ctsettings.plugin import group_for_plugin, 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,4 +23,5 @@ __all__ = [
"validate_plugin_settings",
"ComicTaggerPaths",
"ct_ns",
"group_for_plugin",
]

View File

@ -236,61 +236,72 @@ def register_commands(parser: settngs.Manager) -> None:
parser.add_setting(
"--only-set-cv-key",
action="store_true",
help="Only set the Comic Vine API key and quit.\n\n",
help="Only set the Comic Vine API key and quit.",
file=False,
)
parser.add_setting(
"--list-plugins",
action="store_true",
help="List the available plugins.\n\n",
file=False,
)
def register_commandline_settings(parser: settngs.Manager) -> None:
parser.add_group("commands", register_commands, True)
parser.add_persistent_group("runtime", register_runtime)
parser.add_group("Commands", register_commands, True)
parser.add_persistent_group("Runtime Options", 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_no_gui = any(
config[0].Runtime_Options_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_set_cv_key,
config[0].runtime_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].Commands_list_plugins,
config[0].Runtime_Options_no_gui,
]
)
if platform.system() == "Windows" and config[0].runtime_glob:
if platform.system() == "Windows" and config[0].Runtime_Options_glob:
# no globbing on windows shell, so do it for them
import glob
globs = config[0].runtime_files
config[0].runtime_files = []
globs = config[0].Runtime_Options_files
config[0].Runtime_Options_files = []
for item in globs:
config[0].runtime_files.extend(glob.glob(item))
config[0].Runtime_Options_files.extend(glob.glob(item))
if not config[0].commands_only_set_cv_key and config[0].runtime_no_gui and not config[0].runtime_files:
if (
not config[0].Commands_only_set_cv_key
and config[0].Runtime_Options_no_gui
and not config[0].Runtime_Options_files
):
parser.exit(message="Command requires at least one filename!\n", status=1)
if config[0].commands_delete and not config[0].runtime_type:
if config[0].Commands_delete and not config[0].Runtime_Options_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_type:
if config[0].Commands_save and not config[0].Runtime_Options_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_type:
if config[0].Commands_copy:
if not config[0].Runtime_Options_type:
parser.exit(message="Please specify the type to copy to with -t\n", status=1)
if config[0].runtime_recursive:
config[0].runtime_files = utils.get_recursive_filelist(config[0].runtime_files)
if config[0].Runtime_Options_recursive:
config[0].Runtime_Options_files = utils.get_recursive_filelist(config[0].Runtime_Options_files)
# take a crack at finding rar exe if it's not in the path
if not utils.which("rar"):

View File

@ -123,7 +123,11 @@ def filename(parser: settngs.Manager) -> None:
def talker(parser: settngs.Manager) -> None:
# General settings for talkers
parser.add_setting("--source", default="comicvine", help="Use a specified source by source ID")
parser.add_setting(
"--source",
default="comicvine",
help="Use a specified source by source ID (use --list-plugins to list all sources)",
)
parser.add_setting(
"--remove-html-tables",
default=False,
@ -219,7 +223,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].identifier_publisher_filter:
for x in config[0].Issue_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
@ -230,22 +234,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].identifier_publisher_filter = new_filter
config[0].Issue_Identifier_publisher_filter = new_filter
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]],
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]],
)
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("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)
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)

View File

@ -7,12 +7,23 @@ 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:
@ -26,27 +37,28 @@ def archiver(manager: settngs.Manager) -> None:
def register_talker_settings(manager: settngs.Manager) -> None:
for talker_id, talker in comictaggerlib.ctsettings.talkers.items():
for talker in comictaggerlib.ctsettings.talkers.values():
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("talker_" + talker_id, api_options, False)
manager.add_persistent_group("talker_" + talker_id, talker.register_settings, False)
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)
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]:
@ -55,11 +67,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]["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]))
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]))
else:
archiver.exe = cfg[0]["archiver"][exe_name]
archiver.exe = cfg[0][group_for_plugin(archiver())][exe_name]
return config
@ -67,12 +79,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_id, talker in list(comictaggerlib.ctsettings.talkers.items()):
for talker in list(comictaggerlib.ctsettings.talkers.values()):
try:
cfg[0]["talker_" + talker_id] = talker.parse_settings(cfg[0]["talker_" + talker_id])
cfg[0][group_for_plugin(talker)] = talker.parse_settings(cfg[0][group_for_plugin(talker)])
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))
@ -85,5 +97,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("archiver", archiver, False)
manager.add_persistent_group("Archive", archiver, False)
register_talker_settings(manager)

View File

@ -8,40 +8,39 @@ 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_set_cv_key: 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
Commands_list_plugins: bool
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
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]
internal_install_id: str
internal_save_data_style: int
@ -56,50 +55,56 @@ class settngs_namespace(settngs.TypedNS):
internal_sort_column: int
internal_sort_direction: int
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
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
dialog_show_disclaimer: bool
dialog_dont_notify_about_this_version: str
dialog_ask_about_usage_stats: bool
Filename_Parsing_complicated_parser: bool
Filename_Parsing_remove_c2c: bool
Filename_Parsing_remove_fcbd: bool
Filename_Parsing_remove_publisher: bool
filename_complicated_parser: bool
filename_remove_c2c: bool
filename_remove_fcbd: bool
filename_remove_publisher: bool
Sources_source: str
Sources_remove_html_tables: bool
talker_source: str
talker_remove_html_tables: 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
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
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
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
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
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
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

View File

@ -96,7 +96,7 @@ def open_tagger_window(
) -> None:
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
args = []
if config[0].runtime_darkmode:
if config[0].Runtime_Options_darkmode:
args.extend(["-platform", "windows:darkmode=2"])
args.extend(sys.argv)
app = Application(args)
@ -106,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_files.append(x.toLocalFile()))
app.openFileRequest.connect(lambda x: config[0].Runtime_Options_files.append(x.toLocalFile()))
if platform.system() == "Darwin":
# Set the MacOS dock icon
@ -134,7 +134,7 @@ def open_tagger_window(
QtWidgets.QApplication.processEvents()
try:
tagger_window = TaggerWindow(config[0].runtime_files, config, talkers)
tagger_window = TaggerWindow(config[0].Runtime_Options_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.identifier_series_match_identify_thresh
self.series_match_thresh = config.Issue_Identifier_series_match_identify_thresh
# used to eliminate unlikely publishers
self.publisher_filter = [s.strip().casefold() for s in config.identifier_publisher_filter]
self.publisher_filter = [s.strip().casefold() for s in config.Issue_Identifier_publisher_filter]
self.additional_metadata = GenericMetadata()
self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output
@ -239,10 +239,10 @@ class IssueIdentifier:
# try to get some metadata from filename
md_from_filename = ca.metadata_from_filename(
self.config.filename_complicated_parser,
self.config.filename_remove_c2c,
self.config.filename_remove_fcbd,
self.config.filename_remove_publisher,
self.config.Filename_Parsing_complicated_parser,
self.config.Filename_Parsing_remove_c2c,
self.config.Filename_Parsing_remove_fcbd,
self.config.Filename_Parsing_remove_publisher,
)
working_md = md_from_filename.copy()
@ -291,7 +291,7 @@ class IssueIdentifier:
return Score(score=0, url="", hash=0)
try:
url_image_data = ImageFetcher(self.config.runtime_config.user_cache_dir).fetch(
url_image_data = ImageFetcher(self.config.Runtime_Options_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_config.user_cache_dir).fetch(
alt_url_image_data = ImageFetcher(self.config.Runtime_Options_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.identifier_border_crop_percent)
cropped_border = self.crop_border(cover_image_data, self.config.Issue_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")

View File

@ -52,7 +52,10 @@ class IssueSelectionWindow(QtWidgets.QDialog):
uic.loadUi(ui_path / "issueselectionwindow.ui", self)
self.coverWidget = CoverImageWidget(
self.coverImageContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker
self.coverImageContainer,
CoverImageWidget.AltCoverMode,
config.Runtime_Options_config.user_cache_dir,
talker,
)
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
gridlayout.addWidget(self.coverWidget)
@ -95,7 +98,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
self.imageIssuesSourceWidget = CoverImageWidget(
self.imageIssuesSourceLogo,
CoverImageWidget.URLMode,
config.runtime_config.user_cache_dir,
config.Runtime_Options_config.user_cache_dir,
talker,
False,
)

View File

@ -91,7 +91,7 @@ def configure_locale() -> None:
def update_publishers(config: settngs.Config[ct_ns]) -> None:
json_file = config[0].runtime_config.user_config_dir / "publishers.json"
json_file = config[0].Runtime_Options_config.user_config_dir / "publishers.json"
if json_file.exists():
try:
comicapi.utils.update_publishers(json.loads(json_file.read_text("utf-8")))
@ -121,6 +121,18 @@ 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
@ -141,7 +153,7 @@ class App:
config_paths.user_config_dir / "settings.json", list(args) or None
)
config = cast(settngs.Config[ct_ns], self.manager.get_namespace(cfg, file=True, cmdline=True))
config[0].runtime_config = config_paths
config[0].Runtime_Options_config = config_paths
config = ctsettings.validate_commandline_settings(config, self.manager)
config = ctsettings.validate_file_settings(config)
@ -170,7 +182,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_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_Options_config.user_log_dir}' for more details",
True,
)
@ -183,34 +195,38 @@ 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
# 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]
self.config[0].Source_comicvine_comicvine_key is not None
or self.config[0].Source_comicvine_comicvine_url is not None
):
settings_path = self.config[0].runtime_config.user_config_dir / "settings.json"
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)
if self.config[0].commands_only_set_cv_key:
if self.config[0].Commands_only_set_cv_key:
if self.config_load_success:
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_config.user_log_dir}' for more details",
f"Failed to load settings, check the log located in '{self.config[0].Runtime_Options_config.user_log_dir}' for more details",
True,
)
if not self.config[0].runtime_no_gui:
if not self.config[0].Runtime_Options_no_gui:
try:
from comictaggerlib import gui
return gui.open_tagger_window(talkers, self.config, error)
except ImportError:
self.config[0].runtime_no_gui = True
self.config[0].Runtime_Options_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_config.user_cache_dir, talker
self.altCoverContainer, CoverImageWidget.AltCoverMode, config.Runtime_Options_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].rename_strict else "auto"
self.renamer = FileRenamer(None, platform=platform, replacements=self.config[0].rename_replacements)
platform = "universal" if self.config[0].File_Rename_strict else "auto"
self.renamer = FileRenamer(None, platform=platform, replacements=self.config[0].File_Rename_replacements)
self.do_preview()
def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str:
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
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
new_ext = ca.path.suffix # default
if self.config[0].rename_set_extension_based_on_archive:
if self.config[0].File_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_complicated_parser,
self.config[0].filename_remove_c2c,
self.config[0].filename_remove_fcbd,
self.config[0].filename_remove_publisher,
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.renamer.set_metadata(md)
self.renamer.move = self.config[0].rename_move_to_dir
self.renamer.move = self.config[0].File_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].rename_template)
logger.exception("Invalid format string: %s", self.config[0].File_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].rename_template, self.renamer.metadata
"Formatter failure: %s metadata: %s", self.config[0].File_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].rename_dir if self.config[0].rename_move_to_dir else None,
self.config[0].File_Rename_dir if self.config[0].File_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_config.user_cache_dir, talker
self.imageContainer, CoverImageWidget.URLMode, config.Runtime_Options_config.user_cache_dir, talker
)
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
gridlayout.addWidget(self.imageWidget)
@ -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.identifier_always_use_publisher_filter
self.use_filter = self.config.Issue_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_config.user_cache_dir,
config.Runtime_Options_config.user_cache_dir,
talker,
False,
)
@ -356,7 +356,11 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
def perform_query(self, refresh: bool = False) -> None:
self.search_thread = SearchThread(
self.talker, self.series_name, refresh, self.literal, self.config.identifier_series_match_search_thresh
self.talker,
self.series_name,
refresh,
self.literal,
self.config.Issue_Identifier_series_match_search_thresh,
)
self.search_thread.searchComplete.connect(self.search_complete)
self.search_thread.progressUpdate.connect(self.search_progress_update)
@ -409,7 +413,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
# filter the publishers if enabled set
if self.use_filter:
try:
publisher_filter = {s.strip().casefold() for s in self.config.identifier_publisher_filter}
publisher_filter = {s.strip().casefold() for s in self.config.Issue_Identifier_publisher_filter}
# use '' as publisher name if None
self.series_list = dict(
filter(
@ -425,7 +429,7 @@ 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.identifier_sort_series_by_year:
if self.config.Issue_Identifier_sort_series_by_year:
try:
self.series_list = dict(
natsort.natsorted(
@ -445,7 +449,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
logger.exception("bad data error sorting results by count_of_issues")
# move sanitized matches to the front
if self.config.identifier_exact_series_matches_first:
if self.config.Issue_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()

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: dict = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs(
self.sources = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs(
self.tComicTalkers, self.config, self.talkers
)
self.connect_signals()
@ -307,43 +307,45 @@ 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].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.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.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_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.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.switch_parser()
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.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.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.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.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)
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)
for table, replacments in zip(
(self.twLiteralReplacements, self.twValueReplacements), self.config[0].rename_replacements
(self.twLiteralReplacements, self.twValueReplacements), self.config[0].File_Rename_replacements
):
table.clearContents()
for i in reversed(range(table.rowCount())):
@ -383,7 +385,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].rename_template)
logger.exception("Invalid format string: %s", self.config[0].File_Rename_template)
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
@ -397,7 +399,7 @@ class SettingsWindow(QtWidgets.QDialog):
return
else:
logger.exception(
"Formatter failure: %s metadata: %s", self.config[0].rename_template, self.renamer.metadata
"Formatter failure: %s metadata: %s", self.config[0].File_Rename_template, self.renamer.metadata
)
QtWidgets.QMessageBox.critical(
self,
@ -420,48 +422,50 @@ 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].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].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].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].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].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].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].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].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].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_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_strict = self.cbxRenameStrict.isChecked()
self.config[0].rename_replacements = self.get_replacements()
self.config[0].File_Rename_strict = self.cbxRenameStrict.isChecked()
self.config[0].File_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_config.user_config_dir / "settings.json")
settngs.save_file(self.config, self.config[0].Runtime_Options_config.user_config_dir / "settings.json")
self.parent().config = self.config
QtWidgets.QDialog.accept(self)
@ -474,8 +478,8 @@ class SettingsWindow(QtWidgets.QDialog):
self.select_file(self.leRarExePath, "RAR")
def clear_cache(self) -> None:
ImageFetcher(self.config[0].runtime_config.user_cache_dir).clear_cache()
ComicCacher(self.config[0].runtime_config.user_cache_dir, version).clear_cache()
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()
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_type and isinstance(config[0].runtime_type[0], int):
if config[0].Runtime_Options_type and isinstance(config[0].Runtime_Options_type[0], int):
# respect the command line option tag type
config[0].internal_save_data_style = config[0].runtime_type[0]
config[0].internal_load_data_style = config[0].runtime_type[0]
config[0].internal_save_data_style = config[0].Runtime_Options_type[0]
config[0].internal_load_data_style = config[0].Runtime_Options_type[0]
self.save_data_style = config[0].internal_save_data_style
self.load_data_style = config[0].internal_load_data_style
@ -245,7 +245,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
if len(file_list) != 0:
self.fileSelectionList.add_path_list(file_list)
if self.config[0].dialog_show_disclaimer:
if self.config[0].Dialog_Flags_show_disclaimer:
checked = OptionalMessageDialog.msg(
self,
"Welcome!",
@ -264,15 +264,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
Have fun!
""",
)
self.config[0].dialog_show_disclaimer = not checked
self.config[0].Dialog_Flags_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].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)
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)
raise SystemExit(2)
def open_file_event(self, url: QtCore.QUrl) -> None:
@ -285,7 +285,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
def setup_logger(self) -> ApplicationLogWindow:
try:
current_logs = (self.config[0].runtime_config.user_log_dir / "ComicTagger.log").read_text("utf-8")
current_logs = (self.config[0].Runtime_Options_config.user_log_dir / "ComicTagger.log").read_text("utf-8")
except Exception:
current_logs = ""
root_logger = logging.getLogger()
@ -618,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_complicated_parser,
self.config[0].filename_remove_c2c,
self.config[0].filename_remove_fcbd,
self.config[0].filename_remove_publisher,
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,
)
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())
@ -967,10 +967,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_complicated_parser,
self.config[0].filename_remove_c2c,
self.config[0].filename_remove_fcbd,
self.config[0].filename_remove_publisher,
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,
split_words,
)
if new_metadata is not None:
@ -1079,10 +1079,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
else:
QtWidgets.QApplication.restoreOverrideCursor()
if new_metadata is not None:
if self.config[0].cbl_apply_transform_on_import:
if self.config[0].Comic_Book_Lover_apply_transform_on_import:
new_metadata = CBLTransformer(new_metadata, self.config[0]).apply()
if self.config[0].identifier_clear_form_before_populating:
if self.config[0].Issue_Identifier_clear_form_before_populating:
self.clear_form()
notes = (
@ -1093,7 +1093,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].talker_remove_html_tables
new_metadata.description, self.config[0].Sources_remove_html_tables
),
)
)
@ -1636,7 +1636,10 @@ 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].cbl_apply_transform_on_bulk_operation:
if (
dest_style == MetaDataStyle.CBI
and self.config[0].Comic_Book_Lover_apply_transform_on_bulk_operation
):
md = CBLTransformer(md, self.config[0]).apply()
if not ca.write_metadata(md, dest_style):
@ -1674,7 +1677,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
logger.exception("Save aborted.")
if not ct_md.is_empty:
if self.config[0].cbl_apply_transform_on_import:
if self.config[0].Comic_Book_Lover_apply_transform_on_import:
ct_md = CBLTransformer(ct_md, self.config[0]).apply()
QtWidgets.QApplication.restoreOverrideCursor()
@ -1704,10 +1707,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_complicated_parser,
self.config[0].filename_remove_c2c,
self.config[0].filename_remove_fcbd,
self.config[0].filename_remove_publisher,
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,
dlg.split_words,
)
if dlg.ignore_leading_digits_in_filename and md.series is not None:
@ -1793,7 +1796,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
if self.config[0].identifier_auto_imprint:
if self.config[0].Issue_Identifier_auto_imprint:
md.fix_publisher()
if not ca.write_metadata(md, self.save_data_style):
@ -1979,7 +1982,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_config.user_config_dir / "settings.json")
settngs.save_file(self.config, self.config[0].Runtime_Options_config.user_config_dir / "settings.json")
event.accept()
else:
@ -2106,7 +2109,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_dont_notify_about_this_version):
if new_version[0] not in (self.version, self.config[0].Dialog_Flags_dont_notify_about_this_version):
website = "https://github.com/comictagger/comictagger"
checked = OptionalMessageDialog.msg(
self,
@ -2117,7 +2120,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
"Don't tell me about this version again",
)
if checked:
self.config[0].dialog_dont_notify_about_this_version = new_version[0]
self.config[0].Dialog_Flags_dont_notify_about_this_version = new_version[0]
def on_incoming_socket_connection(self) -> None:
# Accept connection from other instance.

View File

@ -2,12 +2,12 @@ from __future__ import annotations
import logging
from functools import partial
from typing import Any, NamedTuple
from typing import Any, NamedTuple, cast
import settngs
from PyQt5 import QtCore, QtGui, QtWidgets
from comictaggerlib.ctsettings import ct_ns
from comictaggerlib.ctsettings import ct_ns, group_for_plugin
from comictaggerlib.graphics import graphics_path
from comictalker.comictalker import ComicTalker
@ -16,9 +16,15 @@ 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.
@ -38,11 +44,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
@ -56,14 +62,16 @@ class PasswordEdit(QtWidgets.QLineEdit):
def generate_api_widgets(
talker_id: str,
sources: dict[str, QtWidgets.QWidget],
config: settngs.Config[ct_ns],
talker: ComicTalker,
widgets: TalkerTab,
key_option: settngs.Setting,
url_option: settngs.Setting,
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_id: str) -> None:
def call_check_api(
*args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker: ComicTalker
) -> None:
url = ""
key = ""
if le_key is not None:
@ -71,46 +79,43 @@ def generate_api_widgets(
if le_url is not None:
url = le_url.text().strip()
check_text, check_bool = talkers[talker_id].check_api_key(url, key)
check_text, check_bool = talker.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 talker_key.file:
# record the current row so we know where to add the button
if key_option.file:
# record the current row, so we know where to add the button
btn_test_row = layout.rowCount()
le_key = generate_password_textbox(talker_key, layout)
le_key = generate_password_textbox(key_option, layout)
# To enable setting and getting
sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_key"] = le_key
widgets.widgets[key_option.dest] = le_key
# only file settings are saved
if talker_url.file:
# record the current row so we know where to add the button
if url_option.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(talker_url, layout)
value, _ = settngs.get_option(config[0], talker_url)
if not value:
le_url.setText(talkers[talker_id].default_api_url)
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)
# To enable setting and getting
sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_url"] = le_url
widgets.widgets[url_option.dest] = 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_id=talker_id))
btn.clicked.connect(partial(call_check_api, le_url=le_url, le_key=le_key, talker=talker))
def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox:
@ -171,31 +176,39 @@ def generate_password_textbox(option: settngs.Setting, layout: QtWidgets.QGridLa
return widget
def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[ct_ns]) -> None:
def settings_to_talker_form(sources: Sources, config: settngs.Config[ct_ns]) -> None:
# Set the active talker via id in sources combo box
sources["cbx_select_talker"].setCurrentIndex(sources["cbx_select_talker"].findData(config[0].talker_source))
sources[0].setCurrentIndex(sources[0].findData(config[0].Sources_source))
for talker in sources["tabs"].items():
for name, widget in talker[1].widgets.items():
value = getattr(config[0], name)
value_type = type(value)
# 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])
try:
if value_type is str and value:
if isinstance(value, str) and value and isinstance(widget, QtWidgets.QLineEdit) and not default:
widget.setText(value)
if value_type is int or value_type is float:
if isinstance(value, (float, int)) and isinstance(
widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)
):
widget.setValue(value)
if value_type is bool:
if isinstance(value, bool) and isinstance(widget, QtWidgets.QCheckBox):
widget.setChecked(value)
except Exception:
logger.debug("Failed to set value of %s", name)
logger.debug("Failed to set value of %s for %s(%s)", dest, talker.name, talker.id)
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()
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)
for tab in sources["tabs"].items():
for name, widget in tab[1].widgets.items():
# 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():
widget_value = None
if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)):
widget_value = widget.value()
@ -204,83 +217,88 @@ def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settn
elif isinstance(widget, QtWidgets.QCheckBox):
widget_value = widget.isChecked()
setattr(config[0], name, widget_value)
talker_options[dest] = widget_value
return cast(settngs.Config[ct_ns], settngs.get_namespace(cfg, True, True))
def generate_source_option_tabs(
comic_talker_tab: QtWidgets.QWidget,
config: settngs.Config[ct_ns],
talkers: dict[str, ComicTalker],
) -> dict[str, QtWidgets.QWidget]:
) -> Sources:
"""
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(cbx_select_talker, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum)
talker_layout.addWidget(sources[0], 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 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():
for t_id, talker in talkers.items():
# Add source to general tab dropdown list
cbx_select_talker.addItem(talker_obj.name, talker_id)
sources.cbx_sources.addItem(talker.name, t_id)
tab = TalkerTab(tab=QtWidgets.QWidget(), widgets={})
tab_name = talker_id
sources["tabs"][tab_name] = TalkerTab(tab=QtWidgets.QWidget(), widgets={})
layout_grid = QtWidgets.QGridLayout()
for option in config[1][f"talker_{talker_id}"][1].values():
url_option: settngs.Setting | None = None
key_option: settngs.Setting | None = None
for option in config.definitions[group_for_plugin(talker)].v.values():
if not option.file:
continue
if option.dest in (f"{talker_id}_url", f"{talker_id}_key"):
continue
current_widget = None
if option._guess_type() is bool:
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:
current_widget = generate_checkbox(option, layout_grid)
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
tab.widgets[option.dest] = current_widget
elif option._guess_type() is int:
current_widget = generate_spinbox(option, layout_grid)
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
tab.widgets[option.dest] = current_widget
elif option._guess_type() is float:
current_widget = generate_doublespinbox(option, layout_grid)
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
tab.widgets[option.dest] = current_widget
elif option._guess_type() is str:
current_widget = generate_textbox(option, layout_grid)
sources["tabs"][tab_name].widgets[option.internal_name] = current_widget
tab.widgets[option.dest] = 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_id, sources, config, layout_grid, talkers)
generate_api_widgets(talker, tab, key_option, url_option, layout_grid)
# 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
sources["tabs"][tab_name].tab.setLayout(layout_grid)
tab.tab.setLayout(layout_grid)
# Add new sub tab to Comic Source tab
talker_tabs.addTab(sources["tabs"][tab_name].tab, talker_obj.name)
talker_tabs.addTab(tab.tab, talker.name)
sources.tabs.append((talker, tab))
return sources

View File

@ -44,7 +44,7 @@ install_requires =
pyrate-limiter>=2.6,<3
rapidfuzz>=2.12.0
requests==2.*
settngs==0.7.1
settngs==0.7.2
text2digits
typing-extensions>=4.3.0
wordninja

View File

@ -10,8 +10,8 @@ from testing.comicdata import search_results
def test_create_cache(config, mock_version):
config, definitions = config
comictalker.comiccacher.ComicCacher(config.runtime_config.user_cache_dir, mock_version[0])
assert config.runtime_config.user_cache_dir.exists()
comictalker.comiccacher.ComicCacher(config.Runtime_Options_config.user_cache_dir, mock_version[0])
assert config.Runtime_Options_config.user_cache_dir.exists()
def test_search_results(comic_cache):

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_config.user_cache_dir,
cache_folder=config[0].Runtime_Options_config.user_cache_dir,
)
manager = settngs.Manager()
manager.add_persistent_group("comicvine", cv.register_settings)
@ -174,14 +174,14 @@ def config(tmp_path):
app.register_settings()
defaults = app.parse_settings(comictaggerlib.ctsettings.ComicTaggerPaths(tmp_path / "config"), "")
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)
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)
yield defaults
@pytest.fixture
def comic_cache(config, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]:
yield comictalker.comiccacher.ComicCacher(config[0].runtime_config.user_cache_dir, mock_version[0])
yield comictalker.comiccacher.ComicCacher(config[0].Runtime_Options_config.user_cache_dir, mock_version[0])