Update Settings

This commit is contained in:
Timmy Welch 2022-12-06 00:20:01 -08:00
parent 10f36e9868
commit 19112ac79b
No known key found for this signature in database
35 changed files with 1736 additions and 1462 deletions

View File

@ -234,7 +234,7 @@ def update_publishers(new_publishers: Mapping[str, Mapping[str, str]]) -> None:
publishers[publisher] = ImprintDict(publisher, new_publishers[publisher])
class ImprintDict(dict):
class ImprintDict(dict): # type: ignore
"""
ImprintDict takes a publisher and a dict or mapping of lowercased
imprint names to the proper imprint name. Retrieving a value from an
@ -242,7 +242,7 @@ class ImprintDict(dict):
if the key does not exist the key is returned as the publisher unchanged
"""
def __init__(self, publisher: str, mapping: tuple | Mapping = (), **kwargs: dict) -> None:
def __init__(self, publisher: str, mapping: tuple | Mapping = (), **kwargs: dict) -> None: # type: ignore
super().__init__(mapping, **kwargs)
self.publisher = publisher

View File

@ -2,8 +2,9 @@
from __future__ import annotations
import localefix
from comictaggerlib.main import ctmain
from comictaggerlib.main import App
if __name__ == "__main__":
localefix.configure_locale()
ctmain()
App().run()

View File

@ -23,9 +23,9 @@ from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import settings
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.resulttypes import IssueResult, MultipleMatch
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import reduce_widget_font_size
from comictalker.talkerbase import ComicTalker
@ -42,23 +42,28 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
match_set_list: list[MultipleMatch],
style: int,
fetch_func: Callable[[IssueResult], GenericMetadata],
settings: ComicTaggerSettings,
options: settings.OptionValues,
talker_api: ComicTalker,
) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "matchselectionwindow.ui", self)
self.settings = settings
self.options = options
self.current_match_set: MultipleMatch = match_set_list[0]
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, talker_api, CoverImageWidget.AltCoverMode)
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer,
CoverImageWidget.AltCoverMode,
options["runtime"]["config"].user_cache_dir,
talker_api,
)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, talker_api, CoverImageWidget.ArchiveMode)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode, None, None)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -240,10 +245,10 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
md = ca.read_metadata(self._style)
if md.is_empty:
md = ca.metadata_from_filename(
self.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options["filename"]["complicated_parser"],
self.options["filename"]["remove_c2c"],
self.options["filename"]["remove_fcbd"],
self.options["filename"]["remove_publisher"],
)
# now get the particular issue data

View File

@ -34,13 +34,13 @@ class AutoTagProgressWindow(QtWidgets.QDialog):
uic.loadUi(ui_path / "autotagprogresswindow.ui", self)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, talker_api, CoverImageWidget.DataMode, False
self.archiveCoverContainer, CoverImageWidget.DataMode, None, None, False
)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.testCoverWidget = CoverImageWidget(self.testCoverContainer, talker_api, CoverImageWidget.DataMode, False)
self.testCoverWidget = CoverImageWidget(self.testCoverContainer, CoverImageWidget.DataMode, None, None, False)
gridlayout = QtWidgets.QGridLayout(self.testCoverContainer)
gridlayout.addWidget(self.testCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)

View File

@ -19,14 +19,14 @@ import logging
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib import settings
from comictaggerlib.ui import ui_path
logger = logging.getLogger(__name__)
class AutoTagStartWindow(QtWidgets.QDialog):
def __init__(self, parent: QtWidgets.QWidget, settings: ComicTaggerSettings, msg: str) -> None:
def __init__(self, parent: QtWidgets.QWidget, options: settings.OptionValues, msg: str) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "autotagstartwindow.ui", self)
@ -36,19 +36,20 @@ class AutoTagStartWindow(QtWidgets.QDialog):
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.settings = settings
self.options = options
self.cbxSpecifySearchString.setChecked(False)
self.cbxSplitWords.setChecked(False)
self.sbNameMatchSearchThresh.setValue(self.settings.id_series_match_identify_thresh)
self.sbNameMatchSearchThresh.setValue(self.options["identifier"]["series_match_identify_thresh"])
self.leSearchString.setEnabled(False)
self.cbxSaveOnLowConfidence.setChecked(self.settings.save_on_low_confidence)
self.cbxDontUseYear.setChecked(self.settings.dont_use_year_when_identifying)
self.cbxAssumeIssueOne.setChecked(self.settings.assume_1_if_no_issue_num)
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.settings.ignore_leading_numbers_in_filename)
self.cbxRemoveAfterSuccess.setChecked(self.settings.remove_archive_after_successful_match)
self.cbxAutoImprint.setChecked(self.settings.auto_imprint)
self.cbxSaveOnLowConfidence.setChecked(self.options["autotag"]["save_on_low_confidence"])
self.cbxDontUseYear.setChecked(self.options["autotag"]["dont_use_year_when_identifying"])
self.cbxAssumeIssueOne.setChecked(self.options["autotag"]["assume_1_if_no_issue_num"])
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.options["autotag"]["ignore_leading_numbers_in_filename"])
self.cbxRemoveAfterSuccess.setChecked(self.options["autotag"]["remove_archive_after_successful_match"])
self.cbxWaitForRateLimit.setChecked(self.options["autotag"]["wait_and_retry_on_rate_limit"])
self.cbxAutoImprint.setChecked(self.options["comicvine"]["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
@ -72,8 +73,9 @@ class AutoTagStartWindow(QtWidgets.QDialog):
self.assume_issue_one = False
self.ignore_leading_digits_in_filename = False
self.remove_after_success = False
self.wait_and_retry_on_rate_limit = False
self.search_string = ""
self.name_length_match_tolerance = self.settings.id_series_match_search_thresh
self.name_length_match_tolerance = self.options["comicvine"]["series_match_search_thresh"]
self.split_words = self.cbxSplitWords.isChecked()
def search_string_toggle(self) -> None:
@ -89,14 +91,16 @@ class AutoTagStartWindow(QtWidgets.QDialog):
self.ignore_leading_digits_in_filename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
self.remove_after_success = self.cbxRemoveAfterSuccess.isChecked()
self.name_length_match_tolerance = self.sbNameMatchSearchThresh.value()
self.wait_and_retry_on_rate_limit = self.cbxWaitForRateLimit.isChecked()
self.split_words = self.cbxSplitWords.isChecked()
# persist some settings
self.settings.save_on_low_confidence = self.auto_save_on_low
self.settings.dont_use_year_when_identifying = self.dont_use_year
self.settings.assume_1_if_no_issue_num = self.assume_issue_one
self.settings.ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
self.settings.remove_archive_after_successful_match = self.remove_after_success
self.options["autotag"]["save_on_low_confidence"] = self.auto_save_on_low
self.options["autotag"]["dont_use_year_when_identifying"] = self.dont_use_year
self.options["autotag"]["assume_1_if_no_issue_num"] = self.assume_issue_one
self.options["autotag"]["ignore_leading_numbers_in_filename"] = self.ignore_leading_digits_in_filename
self.options["autotag"]["remove_archive_after_successful_match"] = self.remove_after_success
self.options["autotag"]["wait_and_retry_on_rate_limit"] = self.wait_and_retry_on_rate_limit
if self.cbxSpecifySearchString.isChecked():
self.search_string = self.leSearchString.text()

View File

@ -18,15 +18,15 @@ from __future__ import annotations
import logging
from comicapi.genericmetadata import CreditMetadata, GenericMetadata
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib import settings
logger = logging.getLogger(__name__)
class CBLTransformer:
def __init__(self, metadata: GenericMetadata, settings: ComicTaggerSettings) -> None:
def __init__(self, metadata: GenericMetadata, options: settings.OptionValues) -> None:
self.metadata = metadata
self.settings = settings
self.options = options
def apply(self) -> GenericMetadata:
# helper funcs
@ -40,7 +40,7 @@ class CBLTransformer:
for item in items:
append_to_tags_if_unique(item)
if self.settings.assume_lone_credit_is_primary:
if self.options["cbl"]["assume_lone_credit_is_primary"]:
# helper
def set_lone_primary(role_list: list[str]) -> tuple[CreditMetadata | None, int]:
@ -67,19 +67,19 @@ class CBLTransformer:
c["primary"] = False
self.metadata.add_credit(c["person"], "Artist", True)
if self.settings.copy_characters_to_tags:
if self.options["cbl"]["copy_characters_to_tags"]:
add_string_list_to_tags(self.metadata.characters)
if self.settings.copy_teams_to_tags:
if self.options["cbl"]["copy_teams_to_tags"]:
add_string_list_to_tags(self.metadata.teams)
if self.settings.copy_locations_to_tags:
if self.options["cbl"]["copy_locations_to_tags"]:
add_string_list_to_tags(self.metadata.locations)
if self.settings.copy_storyarcs_to_tags:
if self.options["cbl"]["copy_storyarcs_to_tags"]:
add_string_list_to_tags(self.metadata.story_arc)
if self.settings.copy_notes_to_comments:
if self.options["cbl"]["copy_notes_to_comments"]:
if self.metadata.notes is not None:
if self.metadata.comments is None:
self.metadata.comments = ""
@ -88,7 +88,7 @@ class CBLTransformer:
if self.metadata.notes not in self.metadata.comments:
self.metadata.comments += self.metadata.notes
if self.settings.copy_weblink_to_comments:
if self.options["cbl"]["copy_weblink_to_comments"]:
if self.metadata.web_link is not None:
if self.metadata.comments is None:
self.metadata.comments = ""

View File

@ -16,7 +16,6 @@
# limitations under the License.
from __future__ import annotations
import argparse
import json
import logging
import os
@ -28,21 +27,18 @@ from pprint import pprint
from comicapi import utils
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import ctversion
from comictaggerlib import ctversion, settings
from comictaggerlib.cbltransformer import CBLTransformer
from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
from comictaggerlib.graphics import graphics_path
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
from comictaggerlib.settings import ComicTaggerSettings
from comictalker.talkerbase import ComicTalker, TalkerError
logger = logging.getLogger(__name__)
def actual_issue_data_fetch(
issue_id: int, settings: ComicTaggerSettings, opts: argparse.Namespace, talker_api: ComicTalker
) -> GenericMetadata:
def actual_issue_data_fetch(issue_id: int, options: settings.OptionValues, talker_api: ComicTalker) -> GenericMetadata:
# now get the particular issue data
try:
ct_md = talker_api.fetch_comic_data(issue_id)
@ -50,15 +46,15 @@ def actual_issue_data_fetch(
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
return GenericMetadata()
if settings.apply_cbl_transform_on_cv_import:
ct_md = CBLTransformer(ct_md, settings).apply()
if options["cbl"]["apply_cbl_transform_on_cv_import"]:
ct_md = CBLTransformer(ct_md, options).apply()
return ct_md
def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: GenericMetadata) -> bool:
if not opts.dryrun:
for metadata_style in opts.type:
def actual_metadata_save(ca: ComicArchive, options: settings.OptionValues, md: GenericMetadata) -> bool:
if not options["runtime"]["dryrun"]:
for metadata_style in options["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])
@ -67,7 +63,7 @@ def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: Generic
print("Save complete.")
logger.info("Save complete.")
else:
if opts.terse:
if options["runtime"]["terse"]:
logger.info("dry-run option was set, so nothing was written")
print("dry-run option was set, so nothing was written")
else:
@ -80,8 +76,7 @@ def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: Generic
def display_match_set_for_choice(
label: str,
match_set: MultipleMatch,
opts: argparse.Namespace,
settings: ComicTaggerSettings,
options: settings.OptionValues,
talker_api: ComicTalker,
) -> None:
print(f"{match_set.ca.path} -- {label}:")
@ -102,7 +97,7 @@ def display_match_set_for_choice(
m["issue_title"],
)
)
if opts.interactive:
if options["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,9 +106,9 @@ def display_match_set_for_choice(
# save the data!
# we know at this point, that the file is all good to go
ca = match_set.ca
md = create_local_metadata(opts, ca, settings)
ct_md = actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"], settings, opts, talker_api)
if opts.overwrite:
md = create_local_metadata(ca, options)
ct_md = actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"], options, talker_api)
if options["comicvine"]["clear_metadata_on_import"]:
md = ct_md
else:
notes = (
@ -122,17 +117,17 @@ def display_match_set_for_choice(
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
if opts.auto_imprint:
if options["comicvine"]["auto_imprint"]:
md.fix_publisher()
actual_metadata_save(ca, opts, md)
actual_metadata_save(ca, options, md)
def post_process_matches(
match_results: OnlineMatchResults, opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker
match_results: OnlineMatchResults, options: settings.OptionValues, talker_api: ComicTalker
) -> None:
# now go through the match results
if opts.show_save_summary:
if options["runtime"]["show_save_summary"]:
if len(match_results.good_matches) > 0:
print("\nSuccessful matches:\n------------------")
for f in match_results.good_matches:
@ -153,14 +148,14 @@ def post_process_matches(
for f in match_results.fetch_data_failures:
print(f)
if not opts.show_save_summary and not opts.interactive:
if not options["runtime"]["show_save_summary"] and not options["runtime"]["interactive"]:
# just quit if we're not interactive or showing the summary
return
if len(match_results.multiple_matches) > 0:
print("\nArchives with multiple high-confidence matches:\n------------------")
for match_set in match_results.multiple_matches:
display_match_set_for_choice("Multiple high-confidence matches", match_set, opts, settings, talker_api)
display_match_set_for_choice("Multiple high-confidence matches", match_set, options, talker_api)
if len(match_results.low_confidence_matches) > 0:
print("\nArchives with low-confidence matches:\n------------------")
@ -170,40 +165,40 @@ def post_process_matches(
else:
label = "Multiple low-confidence matches"
display_match_set_for_choice(label, match_set, opts, settings, talker_api)
display_match_set_for_choice(label, match_set, options, talker_api)
def cli_mode(opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
if len(opts.file_list) < 1:
def cli_mode(options: settings.OptionValues, talker_api: ComicTalker) -> None:
if len(options["runtime"]["file_list"]) < 1:
logger.error("You must specify at least one filename. Use the -h option for more info")
return
match_results = OnlineMatchResults()
for f in opts.file_list:
process_file_cli(f, opts, settings, talker_api, match_results)
for f in options["runtime"]["file_list"]:
process_file_cli(f, options, talker_api, match_results)
sys.stdout.flush()
post_process_matches(match_results, opts, settings, talker_api)
post_process_matches(match_results, options, talker_api)
def create_local_metadata(opts: argparse.Namespace, ca: ComicArchive, settings: ComicTaggerSettings) -> GenericMetadata:
def create_local_metadata(ca: ComicArchive, options: settings.OptionValues) -> GenericMetadata:
md = GenericMetadata()
md.set_default_page_list(ca.get_number_of_pages())
# now, overlay the parsed filename info
if opts.parse_filename:
if options["runtime"]["parse_filename"]:
f_md = ca.metadata_from_filename(
settings.complicated_parser,
settings.remove_c2c,
settings.remove_fcbd,
settings.remove_publisher,
opts.split_words,
options["filename"]["complicated_parser"],
options["filename"]["remove_c2c"],
options["filename"]["remove_fcbd"],
options["filename"]["remove_publisher"],
options["runtime"]["split_words"],
)
md.overlay(f_md)
for metadata_style in opts.type:
for metadata_style in options["runtime"]["type"]:
if ca.has_metadata(metadata_style):
try:
t_md = ca.read_metadata(metadata_style)
@ -213,21 +208,17 @@ def create_local_metadata(opts: argparse.Namespace, ca: ComicArchive, settings:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
# finally, use explicit stuff
md.overlay(opts.metadata)
md.overlay(options["runtime"]["metadata"])
return md
def process_file_cli(
filename: str,
opts: argparse.Namespace,
settings: ComicTaggerSettings,
talker_api: ComicTalker,
match_results: OnlineMatchResults,
filename: str, options: settings.OptionValues, talker_api: ComicTalker, match_results: OnlineMatchResults
) -> None:
batch_mode = len(opts.file_list) > 1
batch_mode = len(options["runtime"]["file_list"]) > 1
ca = ComicArchive(filename, settings.rar_exe_path, str(graphics_path / "nocover.png"))
ca = ComicArchive(filename, options["general"]["rar_exe_path"], str(graphics_path / "nocover.png"))
if not os.path.lexists(filename):
logger.error("Cannot find %s", filename)
@ -237,7 +228,12 @@ def process_file_cli(
logger.error("Sorry, but %s is not a comic archive!", filename)
return
if not ca.is_writable() and (opts.delete or opts.copy or opts.save or opts.rename):
if not ca.is_writable() and (
options["commands"]["delete"]
or options["commands"]["copy"]
or options["commands"]["save"]
or options["commands"]["rename"]
):
logger.error("This archive is not writable")
return
@ -249,9 +245,9 @@ def process_file_cli(
if ca.has_comet():
has[MetaDataStyle.COMET] = True
if opts.print:
if options["commands"]["print"]:
if not opts.type:
if not options["runtime"]["type"]:
page_count = ca.get_number_of_pages()
brief = ""
@ -284,49 +280,49 @@ def process_file_cli(
print(brief)
if opts.terse:
if options["runtime"]["terse"]:
return
print()
if not opts.type or MetaDataStyle.CIX in opts.type:
if not options["runtime"]["type"] or MetaDataStyle.CIX in options["runtime"]["type"]:
if has[MetaDataStyle.CIX]:
print("--------- ComicRack tags ---------")
try:
if opts.raw:
if options["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 opts.type or MetaDataStyle.CBI in opts.type:
if not options["runtime"]["type"] or MetaDataStyle.CBI in options["runtime"]["type"]:
if has[MetaDataStyle.CBI]:
print("------- ComicBookLover tags -------")
try:
if opts.raw:
if options["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 opts.type or MetaDataStyle.COMET in opts.type:
if not options["runtime"]["type"] or MetaDataStyle.COMET in options["runtime"]["type"]:
if has[MetaDataStyle.COMET]:
print("----------- CoMet tags -----------")
try:
if opts.raw:
if options["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)
elif opts.delete:
for metadata_style in opts.type:
elif options["commands"]["delete"]:
for metadata_style in options["runtime"]["type"]:
style_name = MetaDataStyle.name[metadata_style]
if has[metadata_style]:
if not opts.dryrun:
if not options["runtime"]["dryrun"]:
if not ca.remove_metadata(metadata_style):
print(f"{filename}: Tag removal seemed to fail!")
else:
@ -336,27 +332,27 @@ def process_file_cli(
else:
print(f"{filename}: This archive doesn't have {style_name} tags to remove.")
elif opts.copy is not None:
for metadata_style in opts.type:
elif options["commands"]["copy"] is not None:
for metadata_style in options["runtime"]["type"]:
dst_style_name = MetaDataStyle.name[metadata_style]
if opts.no_overwrite and has[metadata_style]:
if options["runtime"]["no_overwrite"] and has[metadata_style]:
print(f"{filename}: Already has {dst_style_name} tags. Not overwriting.")
return
if opts.copy == metadata_style:
if options["commands"]["copy"] == metadata_style:
print(f"{filename}: Destination and source are same: {dst_style_name}. Nothing to do.")
return
src_style_name = MetaDataStyle.name[opts.copy]
if has[opts.copy]:
if not opts.dryrun:
src_style_name = MetaDataStyle.name[options["commands"]["copy"]]
if has[options["commands"]["copy"]]:
if not options["runtime"]["dryrun"]:
try:
md = ca.read_metadata(opts.copy)
md = ca.read_metadata(options["commands"]["copy"])
except Exception as e:
md = GenericMetadata()
logger.error("Failed to load metadata for %s: %s", ca.path, e)
if settings.apply_cbl_transform_on_bulk_operation and metadata_style == MetaDataStyle.CBI:
md = CBLTransformer(md, settings).apply()
if options["apply_cbl_transform_on_bulk_operation"] and metadata_style == MetaDataStyle.CBI:
md = CBLTransformer(md, options).apply()
if not ca.write_metadata(md, metadata_style):
print(f"{filename}: Tag copy seemed to fail!")
@ -367,10 +363,10 @@ def process_file_cli(
else:
print(f"{filename}: This archive doesn't have {src_style_name} tags to copy.")
elif opts.save:
elif options["commands"]["save"]:
if opts.no_overwrite:
for metadata_style in opts.type:
if options["runtime"]["no_overwrite"]:
for metadata_style in options["runtime"]["type"]:
if has[metadata_style]:
print(f"{filename}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
return
@ -378,39 +374,39 @@ def process_file_cli(
if batch_mode:
print(f"Processing {ca.path}...")
md = create_local_metadata(opts, ca, settings)
md = create_local_metadata(ca, options)
if md.issue is None or md.issue == "":
if opts.assume_issue_one:
if options["runtime"]["assume_issue_one"]:
md.issue = "1"
# now, search online
if opts.online:
if opts.issue_id is not None:
if options["runtime"]["online"]:
if options["runtime"]["issue_id"] is not None:
# we were given the actual issue ID to search with
try:
ct_md = talker_api.fetch_comic_data(opts.issue_id)
ct_md = talker_api.fetch_comic_data(options["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.", opts.issue_id)
logger.error("No match for ID %s was found.", options["runtime"]["issue_id"])
match_results.no_matches.append(str(ca.path.absolute()))
return
if settings.apply_cbl_transform_on_cv_import:
ct_md = CBLTransformer(ct_md, settings).apply()
if options["cbl"]["apply_cbl_transform_on_cv_import"]:
ct_md = CBLTransformer(ct_md, options).apply()
else:
if md is None or md.is_empty:
logger.error("No metadata given to search online with!")
match_results.no_matches.append(str(ca.path.absolute()))
return
ii = IssueIdentifier(ca, settings, talker_api)
ii = IssueIdentifier(ca, options, talker_api)
def myoutput(text: str) -> None:
if opts.verbose:
if options["runtime"]["verbose"]:
IssueIdentifier.default_write_output(text)
# use our overlaid MD struct to search
@ -450,7 +446,7 @@ def process_file_cli(
logger.error("Online search: Multiple good matches. Save aborted")
match_results.multiple_matches.append(MultipleMatch(ca, matches))
return
if low_confidence and opts.abort_on_low_confidence:
if low_confidence and options["runtime"]["abort_on_low_confidence"]:
logger.error("Online search: Low confidence match. Save aborted")
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
return
@ -462,12 +458,12 @@ def process_file_cli(
# we got here, so we have a single match
# now get the particular issue data
ct_md = actual_issue_data_fetch(matches[0]["issue_id"], settings, opts, talker_api)
ct_md = actual_issue_data_fetch(matches[0]["issue_id"], options, talker_api)
if ct_md.is_empty:
match_results.fetch_data_failures.append(str(ca.path.absolute()))
return
if opts.overwrite:
if options["comicvine"]["clear_metadata_on_import"]:
md = ct_md
else:
notes = (
@ -476,29 +472,29 @@ def process_file_cli(
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
if opts.auto_imprint:
if options["comicvine"]["auto_imprint"]:
md.fix_publisher()
# ok, done building our metadata. time to save
if not actual_metadata_save(ca, opts, md):
if not actual_metadata_save(ca, options, md):
match_results.write_failures.append(str(ca.path.absolute()))
else:
match_results.good_matches.append(str(ca.path.absolute()))
elif opts.rename:
elif options["commands"]["rename"]:
original_path = ca.path
msg_hdr = ""
if batch_mode:
msg_hdr = f"{ca.path}: "
md = create_local_metadata(opts, ca, settings)
md = create_local_metadata(ca, options)
if md.series is None:
logger.error(msg_hdr + "Can't rename without series name")
return
new_ext = "" # default
if settings.rename_extension_based_on_archive:
if options["filename"]["rename_set_extension_based_on_archive"]:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
@ -506,11 +502,11 @@ def process_file_cli(
elif ca.is_rar():
new_ext = ".cbr"
renamer = FileRenamer(md, platform="universal" if settings.rename_strict else "auto")
renamer.set_template(settings.rename_template)
renamer.set_issue_zero_padding(settings.rename_issue_number_padding)
renamer.set_smart_cleanup(settings.rename_use_smart_string_cleanup)
renamer.move = settings.rename_move_dir
renamer = FileRenamer(md, platform="universal" if options[filename]["rename_strict"] else "auto")
renamer.set_template(options[filename]["rename_template"])
renamer.set_issue_zero_padding(options[filename]["rename_issue_number_padding"])
renamer.set_smart_cleanup(options[filename]["rename_use_smart_string_cleanup"])
renamer.move = options[filename]["rename_move_to_dir"]
try:
new_name = renamer.determine_name(ext=new_ext)
@ -522,13 +518,17 @@ def process_file_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",
settings.rename_template,
options[filename]["rename_template"],
)
return
except Exception:
logger.exception("Formatter failure: %s metadata: %s", settings.rename_template, renamer.metadata)
logger.exception(
"Formatter failure: %s metadata: %s", options[filename]["rename_template"], renamer.metadata
)
folder = get_rename_dir(ca, settings.rename_dir if settings.rename_move_dir else None)
folder = get_rename_dir(
ca, options[filename]["rename_dir"] if options[filename]["rename_move_to_dir"] else None
)
full_path = folder / new_name
@ -537,7 +537,7 @@ def process_file_cli(
return
suffix = ""
if not opts.dryrun:
if not options["runtime"]["dryrun"]:
# rename the file
try:
ca.rename(utils.unique_file(full_path))
@ -548,7 +548,7 @@ def process_file_cli(
print(f"renamed '{original_path.name}' -> '{new_name}' {suffix}")
elif opts.export_to_zip:
elif options["commands"]["export_to_zip"]:
msg_hdr = ""
if batch_mode:
msg_hdr = f"{ca.path}: "
@ -560,7 +560,7 @@ def process_file_cli(
filename_path = pathlib.Path(filename).absolute()
new_file = filename_path.with_suffix(".cbz")
if opts.abort_on_conflict and new_file.exists():
if options["runtime"]["abort_on_conflict"] and new_file.exists():
print(msg_hdr + f"{new_file.name} already exists in the that folder.")
return
@ -568,10 +568,10 @@ def process_file_cli(
delete_success = False
export_success = False
if not opts.dryrun:
if not options["runtime"]["dryrun"]:
if ca.export_as_zip(new_file):
export_success = True
if opts.delete_after_zip_export:
if options["runtime"]["delete_after_zip_export"]:
try:
filename_path.unlink(missing_ok=True)
delete_success = True
@ -583,7 +583,7 @@ def process_file_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 opts.delete_after_zip_export:
if options["runtime"]["delete_after_zip_export"]:
msg += " and delete original."
print(msg)
return
@ -591,7 +591,7 @@ def process_file_cli(
msg = msg_hdr
if export_success:
msg += f"Archive exported successfully to: {os.path.split(new_file)[1]}"
if opts.delete_after_zip_export and delete_success:
if options["runtime"]["delete_after_zip_export"] and delete_success:
msg += " (Original deleted) "
else:
msg += "Archive failed to export!"

View File

@ -20,6 +20,7 @@ TODO: This should be re-factored using subclasses!
from __future__ import annotations
import logging
import pathlib
from PyQt5 import QtCore, QtGui, QtWidgets, uic
@ -63,16 +64,26 @@ class CoverImageWidget(QtWidgets.QWidget):
image_fetch_complete = QtCore.pyqtSignal(QtCore.QByteArray)
def __init__(
self, parent: QtWidgets.QWidget, talker_api: ComicTalker, mode: int, expand_on_click: bool = True
self,
parent: QtWidgets.QWidget,
mode: int,
cache_folder: pathlib.Path | None,
talker_api: ComicTalker | None,
expand_on_click: bool = True,
) -> None:
super().__init__(parent)
self.cover_fetcher = ImageFetcher()
if mode not in (self.AltCoverMode, self.URLMode) or cache_folder is None:
self.cover_fetcher = None
self.talker_api = None
else:
self.cover_fetcher = ImageFetcher(cache_folder)
self.talker_api = None
uic.loadUi(ui_path / "coverimagewidget.ui", self)
reduce_widget_font_size(self.label)
self.talker_api = talker_api
self.cache_folder = cache_folder
self.mode: int = mode
self.page_loader: PageLoader | None = None
self.showControls = True
@ -221,8 +232,9 @@ class CoverImageWidget(QtWidgets.QWidget):
self.label.setText(f"Page {self.imageIndex + 1} (of {self.imageCount})")
def load_url(self) -> None:
assert isinstance(self.cache_folder, pathlib.Path)
self.load_default()
self.cover_fetcher = ImageFetcher()
self.cover_fetcher = ImageFetcher(self.cache_folder)
ImageFetcher.image_fetch_complete = self.image_fetch_complete.emit
self.cover_fetcher.fetch(self.url_list[self.imageIndex])

View File

@ -19,7 +19,6 @@ import logging
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
logger = logging.getLogger(__name__)
@ -32,7 +31,7 @@ class ExportConflictOpts:
class ExportWindow(QtWidgets.QDialog):
def __init__(self, parent: QtWidgets.QWidget, settings: ComicTaggerSettings, msg: str) -> None:
def __init__(self, parent: QtWidgets.QWidget, msg: str) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "exportwindow.ui", self)
@ -42,8 +41,6 @@ class ExportWindow(QtWidgets.QDialog):
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.settings = settings
self.cbxDeleteOriginal.setChecked(False)
self.cbxAddToList.setChecked(True)
self.radioDontCreate.setChecked(True)

View File

@ -24,9 +24,9 @@ from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive
from comictaggerlib import settings
from comictaggerlib.graphics import graphics_path
from comictaggerlib.optionalmsgdialog import OptionalMessageDialog
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.settingswindow import linuxRarHelp, macRarHelp, windowsRarHelp
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
@ -59,14 +59,14 @@ class FileSelectionList(QtWidgets.QWidget):
def __init__(
self,
parent: QtWidgets.QWidget,
settings: ComicTaggerSettings,
options: settings.OptionValues,
dirty_flag_verification: Callable[[str, str], bool],
) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "fileselectionlist.ui", self)
self.settings = settings
self.options = options
reduce_widget_font_size(self.twList)
@ -227,7 +227,7 @@ class FileSelectionList(QtWidgets.QWidget):
else:
QtWidgets.QMessageBox.information(self, "File/Folder Open", "No readable comic archives were found.")
if rar_added and not utils.which(self.settings.rar_exe_path or "rar"):
if rar_added and not utils.which(self.options["general"]["rar_exe_path"] or "rar"):
self.rar_ro_message()
self.twList.setSortingEnabled(True)
@ -281,7 +281,7 @@ class FileSelectionList(QtWidgets.QWidget):
if self.is_list_dupe(path):
return self.get_current_list_row(path)
ca = ComicArchive(path, self.settings.rar_exe_path, str(graphics_path / "nocover.png"))
ca = ComicArchive(path, self.options["general"]["rar_exe_path"], str(graphics_path / "nocover.png"))
if ca.seems_to_be_a_comic_archive():
row: int = self.twList.rowCount()

143
comictaggerlib/gui.py Normal file
View File

@ -0,0 +1,143 @@
from __future__ import annotations
import logging.handlers
import os
import platform
import sys
import traceback
import types
from comictaggerlib import settings
from comictaggerlib.graphics import graphics_path
from comictalker.talkerbase import ComicTalker
logger = logging.getLogger("comictagger")
try:
qt_available = True
from PyQt5 import QtCore, QtGui, QtWidgets
def show_exception_box(log_msg: str) -> None:
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
If unavailable (non-console application), log an additional notice.
"""
if QtWidgets.QApplication.instance() is not None:
errorbox = QtWidgets.QMessageBox()
errorbox.setText(log_msg)
errorbox.exec()
QtWidgets.QApplication.exit(1)
else:
logger.debug("No QApplication instance available.")
class UncaughtHook(QtCore.QObject):
_exception_caught = QtCore.pyqtSignal(object)
def __init__(self) -> None:
super().__init__()
# this registers the exception_hook() function as hook with the Python interpreter
sys.excepthook = self.exception_hook
# connect signal to execute the message box function always on main thread
self._exception_caught.connect(show_exception_box)
def exception_hook(
self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: types.TracebackType | None
) -> None:
"""Function handling uncaught exceptions.
It is triggered each time an uncaught exception occurs.
"""
if issubclass(exc_type, KeyboardInterrupt):
# ignore keyboard interrupt to support console applications
sys.__excepthook__(exc_type, exc_value, exc_traceback)
else:
exc_info = (exc_type, exc_value, exc_traceback)
trace_back = "".join(traceback.format_tb(exc_traceback))
log_msg = f"{exc_type.__name__}: {exc_value}\n\n{trace_back}"
logger.critical("Uncaught exception: %s: %s", exc_type.__name__, exc_value, exc_info=exc_info)
# trigger message box show
self._exception_caught.emit(f"Oops. An unexpected error occurred:\n{log_msg}")
qt_exception_hook = UncaughtHook()
from comictaggerlib.taggerwindow import TaggerWindow
class Application(QtWidgets.QApplication):
openFileRequest = QtCore.pyqtSignal(QtCore.QUrl, name="openfileRequest")
# Handles "Open With" from Finder on macOS
def event(self, event: QtCore.QEvent) -> bool:
if event.type() == QtCore.QEvent.FileOpen:
logger.info(event.url().toLocalFile())
self.openFileRequest.emit(event.url())
return True
return super().event(event)
except ImportError as e:
def show_exception_box(log_msg: str) -> None:
...
logger.error(str(e))
qt_available = False
def open_tagger_window(
talker_api: ComicTalker, options: settings.OptionValues, gui_exception: Exception | None
) -> None:
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
args = []
if options["runtime"]["darkmode"]:
args.extend(["-platform", "windows:darkmode=2"])
args.extend(sys.argv)
app = Application(args)
if gui_exception is not None:
trace_back = "".join(traceback.format_tb(gui_exception.__traceback__))
log_msg = f"{type(gui_exception).__name__}: {gui_exception}\n\n{trace_back}"
show_exception_box(f"{log_msg}")
raise SystemExit(1)
# needed to catch initial open file events (macOS)
app.openFileRequest.connect(lambda x: options["runtime"]["files"].append(x.toLocalFile()))
if platform.system() == "Darwin":
# Set the MacOS dock icon
app.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
if platform.system() == "Windows":
# For pure python, tell windows that we're not python,
# so we can have our own taskbar icon
import ctypes
myappid = "comictagger" # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore[attr-defined]
# force close of console window
swp_hidewindow = 0x0080
console_wnd = ctypes.windll.kernel32.GetConsoleWindow() # type: ignore[attr-defined]
if console_wnd != 0:
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow) # type: ignore[attr-defined]
if platform.system() != "Linux":
img = QtGui.QPixmap(str(graphics_path / "tags.png"))
splash = QtWidgets.QSplashScreen(img)
splash.show()
splash.raise_()
QtWidgets.QApplication.processEvents()
try:
tagger_window = TaggerWindow(options["runtime"]["files"], options, talker_api)
tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
tagger_window.show()
# Catch open file events (macOS)
app.openFileRequest.connect(tagger_window.open_file_event)
if platform.system() != "Linux":
splash.finish(tagger_window)
sys.exit(app.exec())
except Exception:
logger.exception("GUI mode failed")
QtWidgets.QMessageBox.critical(
QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc()
)

View File

@ -18,6 +18,7 @@ from __future__ import annotations
import datetime
import logging
import os
import pathlib
import shutil
import sqlite3 as lite
import tempfile
@ -25,7 +26,6 @@ import tempfile
import requests
from comictaggerlib import ctversion
from comictaggerlib.settings import ComicTaggerSettings
try:
from PyQt5 import QtCore, QtNetwork
@ -49,11 +49,10 @@ class ImageFetcher:
image_fetch_complete = fetch_complete
def __init__(self) -> None:
def __init__(self, cache_folder: pathlib.Path) -> None:
self.settings_folder = ComicTaggerSettings.get_settings_folder()
self.db_file = os.path.join(self.settings_folder, "image_url_cache.db")
self.cache_folder = os.path.join(self.settings_folder, "image_cache")
self.db_file = cache_folder / "image_url_cache.db"
self.cache_folder = cache_folder / "image_cache"
self.user_data = None
self.fetched_url = ""

0
comictaggerlib/imagehasher.py Executable file → Normal file
View File

View File

@ -26,11 +26,10 @@ from comicapi import utils
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
from comictaggerlib import settings
from comictaggerlib.imagefetcher import ImageFetcher, ImageFetcherException
from comictaggerlib.imagehasher import ImageHasher
from comictaggerlib.resulttypes import IssueResult
from comictaggerlib.settings import ComicTaggerSettings
from comictalker.talker_utils import parse_date_str
from comictalker.talkerbase import ComicTalker, TalkerError
logger = logging.getLogger(__name__)
@ -73,8 +72,8 @@ class IssueIdentifier:
result_one_good_match = 4
result_multiple_good_matches = 5
def __init__(self, comic_archive: ComicArchive, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
self.settings = settings
def __init__(self, comic_archive: ComicArchive, options: settings.OptionValues, talker_api: ComicTalker) -> None:
self.options = options
self.talker_api = talker_api
self.comic_archive: ComicArchive = comic_archive
self.image_hasher = 1
@ -97,10 +96,10 @@ class IssueIdentifier:
# used to eliminate series names that are too long based on our search
# string
self.series_match_thresh = settings.id_series_match_identify_thresh
self.series_match_thresh = options["identifier"]["series_match_identify_thresh"]
# used to eliminate unlikely publishers
self.publisher_filter = [s.strip().casefold() for s in settings.id_publisher_filter.split(",")]
self.publisher_filter = [s.strip().casefold() for s in options["identifier"]["publisher_filter"]]
self.additional_metadata = GenericMetadata()
self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output
@ -202,10 +201,10 @@ class IssueIdentifier:
# try to get some metadata from filename
md_from_filename = ca.metadata_from_filename(
self.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options["filename"]["complicated_parser"],
self.options["filename"]["remove_c2c"],
self.options["filename"]["remove_fcbd"],
self.options["filename"]["remove_publisher"],
)
working_md = md_from_filename.copy()
@ -256,7 +255,9 @@ class IssueIdentifier:
return Score(score=0, url="", hash=0)
try:
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
url_image_data = ImageFetcher(self.options["runtime"]["config"].user_cache_dir).fetch(
primary_thumb_url, blocking=True
)
except ImageFetcherException as e:
self.log_msg("Network issue while fetching cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError from e
@ -276,7 +277,9 @@ class IssueIdentifier:
if use_remote_alternates:
for alt_url in alt_urls:
try:
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
alt_url_image_data = ImageFetcher(self.options["runtime"]["config"].user_cache_dir).fetch(
alt_url, blocking=True
)
except ImageFetcherException as e:
self.log_msg("Network issue while fetching alt. cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError from e
@ -466,7 +469,7 @@ class IssueIdentifier:
)
# parse out the cover date
_, month, year = parse_date_str(issue["cover_date"])
_, month, year = utils.parse_date_str(issue["cover_date"])
# Now check the cover match against the primary image
hash_list = [cover_hash]
@ -487,6 +490,7 @@ class IssueIdentifier:
use_remote_alternates=False,
)
except Exception:
logger.exception("Scoring series failed")
self.match_list = []
return self.match_list

View File

@ -20,8 +20,8 @@ import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.issuestring import IssueString
from comictaggerlib import settings
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import reduce_widget_font_size
from comictalker.resulttypes import ComicIssue
@ -44,7 +44,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
def __init__(
self,
parent: QtWidgets.QWidget,
settings: ComicTaggerSettings,
options: settings.OptionValues,
talker_api: ComicTalker,
series_id: int,
issue_number: str,
@ -53,7 +53,12 @@ class IssueSelectionWindow(QtWidgets.QDialog):
uic.loadUi(ui_path / "issueselectionwindow.ui", self)
self.coverWidget = CoverImageWidget(self.coverImageContainer, talker_api, CoverImageWidget.AltCoverMode)
self.coverWidget = CoverImageWidget(
self.coverImageContainer,
CoverImageWidget.AltCoverMode,
options["runtime"]["config"].user_cache_dir,
talker_api,
)
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
gridlayout.addWidget(self.coverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -71,7 +76,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
self.series_id = series_id
self.issue_id: int | None = None
self.settings = settings
self.options = options
self.talker_api = talker_api
self.url_fetch_thread = None
self.issue_list: list[ComicIssue] = []

42
comictaggerlib/log.py Normal file
View File

@ -0,0 +1,42 @@
from __future__ import annotations
import logging.handlers
import pathlib
logger = logging.getLogger("comictagger")
def get_file_handler(filename: pathlib.Path) -> logging.FileHandler:
file_handler = logging.handlers.RotatingFileHandler(filename, encoding="utf-8", backupCount=10)
if filename.is_file() and filename.stat().st_size > 0:
file_handler.doRollover()
return file_handler
def setup_logging(verbose: int, log_dir: pathlib.Path) -> None:
logging.getLogger("comicapi").setLevel(logging.DEBUG)
logging.getLogger("comictaggerlib").setLevel(logging.DEBUG)
log_file = log_dir / "ComicTagger.log"
log_file.parent.mkdir(parents=True, exist_ok=True)
stream_handler = logging.StreamHandler()
file_handler = get_file_handler(log_file)
if verbose > 1:
stream_handler.setLevel(logging.DEBUG)
elif verbose > 0:
stream_handler.setLevel(logging.INFO)
else:
stream_handler.setLevel(logging.WARNING)
logging.basicConfig(
handlers=[
stream_handler,
file_handler,
],
level=logging.WARNING,
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)

336
comictaggerlib/main.py Executable file → Normal file
View File

@ -15,23 +15,20 @@
# limitations under the License.
from __future__ import annotations
import argparse
import json
import logging.handlers
import os
import pathlib
import platform
import pprint
import signal
import sys
import traceback
import types
from typing import Any
import comictalker.comictalkerapi as ct_api
from comicapi import utils
from comictaggerlib import cli
from comictaggerlib import cli, settings
from comictaggerlib.ctversion import version
from comictaggerlib.graphics import graphics_path
from comictaggerlib.options import parse_cmd_line
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.log import setup_logging
from comictalker.talkerbase import TalkerError
if sys.version_info < (3, 10):
@ -39,235 +36,138 @@ if sys.version_info < (3, 10):
else:
import importlib.metadata as importlib_metadata
logger = logging.getLogger("comictagger")
logging.getLogger("comicapi").setLevel(logging.DEBUG)
logging.getLogger("comictaggerlib").setLevel(logging.DEBUG)
logging.getLogger("comictalker").setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
try:
qt_available = True
from PyQt5 import QtCore, QtGui, QtWidgets
from comictaggerlib import gui
def show_exception_box(log_msg: str) -> None:
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
If unavailable (non-console application), log an additional notice.
"""
if QtWidgets.QApplication.instance() is not None:
errorbox = QtWidgets.QMessageBox()
errorbox.setText(log_msg)
errorbox.exec()
QtWidgets.QApplication.exit(1)
else:
logger.debug("No QApplication instance available.")
class UncaughtHook(QtCore.QObject):
_exception_caught = QtCore.pyqtSignal(object)
def __init__(self) -> None:
super().__init__()
# this registers the exception_hook() function as hook with the Python interpreter
sys.excepthook = self.exception_hook
# connect signal to execute the message box function always on main thread
self._exception_caught.connect(show_exception_box)
def exception_hook(
self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: types.TracebackType | None
) -> None:
"""Function handling uncaught exceptions.
It is triggered each time an uncaught exception occurs.
"""
if issubclass(exc_type, KeyboardInterrupt):
# ignore keyboard interrupt to support console applications
sys.__excepthook__(exc_type, exc_value, exc_traceback)
else:
exc_info = (exc_type, exc_value, exc_traceback)
trace_back = "".join(traceback.format_tb(exc_traceback))
log_msg = f"{exc_type.__name__}: {exc_value}\n\n{trace_back}"
logger.critical("Uncaught exception: %s: %s", exc_type.__name__, exc_value, exc_info=exc_info)
# trigger message box show
self._exception_caught.emit(f"Oops. An unexpected error occurred:\n{log_msg}")
qt_exception_hook = UncaughtHook()
from comictaggerlib.taggerwindow import TaggerWindow
class Application(QtWidgets.QApplication):
openFileRequest = QtCore.pyqtSignal(QtCore.QUrl, name="openfileRequest")
def event(self, event: QtCore.QEvent) -> bool:
if event.type() == QtCore.QEvent.FileOpen:
logger.info(event.url().toLocalFile())
self.openFileRequest.emit(event.url())
return True
return super().event(event)
except ImportError as e:
def show_exception_box(log_msg: str) -> None:
...
logger.error(str(e))
qt_available = gui.qt_available
except Exception:
qt_available = False
def rotate(handler: logging.handlers.RotatingFileHandler, filename: pathlib.Path) -> None:
if filename.is_file() and filename.stat().st_size > 0:
handler.doRollover()
logger = logging.getLogger("comictagger")
def update_publishers() -> None:
json_file = ComicTaggerSettings.get_settings_folder() / "publishers.json"
logger.setLevel(logging.DEBUG)
def update_publishers(options: dict[str, dict[str, Any]]) -> None:
json_file = options["runtime"]["config"].user_config_dir / "publishers.json"
if json_file.exists():
try:
utils.update_publishers(json.loads(json_file.read_text("utf-8")))
except Exception as e:
except Exception:
logger.exception("Failed to load publishers from %s", json_file)
show_exception_box(str(e))
# show_exception_box(str(e))
def ctmain() -> None:
opts = parse_cmd_line()
settings = ComicTaggerSettings(opts.config_path)
class App:
"""docstring for App"""
os.makedirs(ComicTaggerSettings.get_settings_folder() / "logs", exist_ok=True)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)
file_handler = logging.handlers.RotatingFileHandler(
ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log", encoding="utf-8", backupCount=10
)
rotate(file_handler, ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log")
logging.basicConfig(
handlers=[
stream_handler,
file_handler,
],
level=logging.WARNING,
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
def __init__(self) -> None:
self.options: dict[str, dict[str, Any]] = {}
self.initial_arg_parser = settings.initial_cmd_line_parser()
if settings.settings_warning < 4:
print( # noqa: T201
"""
!!!Warning!!!
The next release will save settings in a different format
NO SETTINGS WILL BE TRANSFERED to the new version.
See https://github.com/comictagger/comictagger/releases/1.5.5 for more information.
""",
file=sys.stderr,
)
def run(self) -> None:
opts = self.initialize()
self.register_options()
self.parse_options(opts.config)
self.initialize_dirs()
# manage the CV API key
# None comparison is used so that the empty string can unset the value
if opts.cv_api_key is not None or opts.cv_url is not None:
settings.cv_api_key = opts.cv_api_key if opts.cv_api_key is not None else settings.cv_api_key
settings.cv_url = opts.cv_url if opts.cv_url is not None else settings.cv_url
settings.save()
if opts.only_set_cv_key:
print("Key set") # noqa: T201
return
self.ctmain()
signal.signal(signal.SIGINT, signal.SIG_DFL)
def initialize(self) -> argparse.Namespace:
opts, _ = self.initial_arg_parser.parse_known_args()
assert opts is not None
setup_logging(opts.verbose, opts.config.user_log_dir / "log")
return opts
logger.info(
"ComicTagger Version: %s running on: %s PyInstaller: %s",
version,
platform.system(),
"Yes" if getattr(sys, "frozen", None) else "No",
)
def register_options(self) -> None:
self.manager = settings.Manager()
settings.register_commandline(self.manager)
settings.register_settings(self.manager)
logger.debug("Installed Packages")
for pkg in sorted(importlib_metadata.distributions(), key=lambda x: x.name):
logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"])
def parse_options(self, config_paths: settings.ComicTaggerPaths) -> None:
options = self.manager.parse_options(config_paths)
self.options = settings.validate_commandline_options(options, self.manager)
self.options = settings.validate_settings(options, self.manager)
if not qt_available and not opts.no_gui:
opts.no_gui = True
logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
def initialize_dirs(self) -> None:
self.options["runtime"]["config"].user_data_dir.mkdir(parents=True, exist_ok=True)
self.options["runtime"]["config"].user_config_dir.mkdir(parents=True, exist_ok=True)
self.options["runtime"]["config"].user_cache_dir.mkdir(parents=True, exist_ok=True)
self.options["runtime"]["config"].user_state_dir.mkdir(parents=True, exist_ok=True)
self.options["runtime"]["config"].user_log_dir.mkdir(parents=True, exist_ok=True)
logger.debug("user_data_dir: %s", self.options["runtime"]["config"].user_data_dir)
logger.debug("user_config_dir: %s", self.options["runtime"]["config"].user_config_dir)
logger.debug("user_cache_dir: %s", self.options["runtime"]["config"].user_cache_dir)
logger.debug("user_state_dir: %s", self.options["runtime"]["config"].user_state_dir)
logger.debug("user_log_dir: %s", self.options["runtime"]["config"].user_log_dir)
talker_exception = None
try:
talker_api = ct_api.get_comic_talker("comicvine")( # type: ignore[call-arg]
version=version,
cache_folder=ComicTaggerSettings.get_settings_folder(),
series_match_thresh=settings.id_series_match_search_thresh,
remove_html_tables=settings.remove_html_tables,
use_series_start_as_volume=settings.use_series_start_as_volume,
wait_on_ratelimit=settings.wait_and_retry_on_rate_limit,
api_url=settings.cv_url,
api_key=settings.cv_api_key,
)
except TalkerError as e:
logger.exception("Unable to load talker")
talker_exception = e
if opts.no_gui:
raise SystemExit(1)
def ctmain(self) -> None:
assert self.options is not None
# options already loaded
utils.load_publishers()
update_publishers()
if opts.no_gui:
try:
cli.cli_mode(opts, settings, talker_api)
except Exception:
logger.exception("CLI mode failed")
else:
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
args = []
if opts.darkmode:
args.extend(["-platform", "windows:darkmode=2"])
args.extend(sys.argv)
app = Application(args)
if talker_exception is not None:
trace_back = "".join(traceback.format_tb(talker_exception.__traceback__))
log_msg = f"{type(talker_exception).__name__}: {talker_exception}\n\n{trace_back}"
show_exception_box(f"Unable to load talker: {log_msg}")
raise SystemExit(1)
# needed to catch initial open file events (macOS)
app.openFileRequest.connect(lambda x: opts.files.append(x.toLocalFile()))
if platform.system() == "Darwin":
# Set the MacOS dock icon
app.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
if platform.system() == "Windows":
# For pure python, tell windows that we're not python,
# so we can have our own taskbar icon
import ctypes
myappid = "comictagger" # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore[attr-defined]
# force close of console window
swp_hidewindow = 0x0080
console_wnd = ctypes.windll.kernel32.GetConsoleWindow() # type: ignore[attr-defined]
if console_wnd != 0:
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow) # type: ignore[attr-defined]
if platform.system() != "Linux":
img = QtGui.QPixmap(str(graphics_path / "tags.png"))
splash = QtWidgets.QSplashScreen(img)
splash.show()
splash.raise_()
QtWidgets.QApplication.processEvents()
try:
tagger_window = TaggerWindow(opts.files, settings, talker_api, opts=opts)
tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
tagger_window.show()
# Catch open file events (macOS)
app.openFileRequest.connect(tagger_window.open_file_event)
if platform.system() != "Linux":
splash.finish(tagger_window)
sys.exit(app.exec())
except Exception:
logger.exception("GUI mode failed")
QtWidgets.QMessageBox.critical(
QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc()
# manage the CV API key
# None comparison is used so that the empty string can unset the value
if self.options["comicvine"]["cv_api_key"] is not None or self.options["comicvine"]["cv_url"] is not None:
self.options["comicvine"]["cv_api_key"] = (
self.options["comicvine"]["cv_api_key"]
if self.options["comicvine"]["cv_api_key"] is not None
else self.options["comicvine"]["cv_api_key"]
)
self.options["comicvine"]["cv_url"] = (
self.options["comicvine"]["cv_url"]
if self.options["comicvine"]["cv_url"] is not None
else self.options["comicvine"]["cv_url"]
)
self.manager.save_file(self.options, self.options["runtime"]["config"].user_config_dir / "settings.json")
logger.debug(pprint.pformat(self.options))
if self.options["commands"]["only_set_cv_key"]:
print("Key set") # noqa: T201
return
signal.signal(signal.SIGINT, signal.SIG_DFL)
logger.info(
"ComicTagger Version: %s running on: %s PyInstaller: %s",
version,
platform.system(),
"Yes" if getattr(sys, "frozen", None) else "No",
)
logger.debug("Installed Packages")
for pkg in sorted(importlib_metadata.distributions(), key=lambda x: x.name):
logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"])
utils.load_publishers()
update_publishers(self.options)
if not qt_available and not self.options["runtime"]["no_gui"]:
self.options["runtime"]["no_gui"] = True
logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
gui_exception = None
try:
talker_api = ct_api.get_comic_talker("comicvine")( # type: ignore[call-arg]
version=version,
cache_folder=self.options["runtime"]["config"].user_cache_dir,
series_match_thresh=self.options["comicvine"]["series_match_search_thresh"],
remove_html_tables=self.options["comicvine"]["remove_html_tables"],
use_series_start_as_volume=self.options["comicvine"]["use_series_start_as_volume"],
wait_on_ratelimit=self.options["comicvine"]["wait_and_retry_on_rate_limit"],
api_url=self.options["comicvine"]["cv_url"],
api_key=self.options["comicvine"]["cv_api_key"],
)
except TalkerError as e:
logger.exception("Unable to load talker")
gui_exception = e
if self.options["runtime"]["no_gui"]:
raise SystemExit(1)
if self.options["runtime"]["no_gui"]:
try:
cli.cli_mode(self.options, talker_api)
except Exception:
logger.exception("CLI mode failed")
else:
gui.open_tagger_window(talker_api, self.options, gui_exception)

View File

@ -21,6 +21,7 @@ import os
from PyQt5 import QtCore, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib import settings
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.resulttypes import IssueResult
from comictaggerlib.ui import ui_path
@ -38,18 +39,24 @@ class MatchSelectionWindow(QtWidgets.QDialog):
parent: QtWidgets.QWidget,
matches: list[IssueResult],
comic_archive: ComicArchive,
options: settings.OptionValues,
talker_api: ComicTalker,
) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "matchselectionwindow.ui", self)
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, talker_api, CoverImageWidget.AltCoverMode)
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer,
CoverImageWidget.AltCoverMode,
options["runtime"]["config"].user_cache_dir,
talker_api,
)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, talker_api, CoverImageWidget.ArchiveMode)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode, None, None)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)

View File

@ -1,418 +0,0 @@
"""CLI options class for ComicTagger app"""
#
# Copyright 2012-2014 Anthony Beville
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import logging
import os
import platform
import sys
from comicapi import utils
from comicapi.comicarchive import MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import ctversion
logger = logging.getLogger(__name__)
def define_args() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="""A utility for reading and writing metadata to comic archives.
If no options are given, %(prog)s will run in windowed mode.""",
epilog="For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki",
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--version",
action="store_true",
help="Display version.",
)
commands = parser.add_mutually_exclusive_group()
commands.add_argument(
"-p",
"--print",
action="store_true",
help="""Print out tag info from file. Specify type\n(via -t) to get only info of that tag type.\n\n""",
)
commands.add_argument(
"-d",
"--delete",
action="store_true",
help="Deletes the tag block of specified type (via -t).\n",
)
commands.add_argument(
"-c",
"--copy",
type=metadata_type,
metavar="{CR,CBL,COMET}",
help="Copy the specified source tag block to\ndestination style specified via -t\n(potentially lossy operation).\n\n",
)
commands.add_argument(
"-s",
"--save",
action="store_true",
help="Save out tags as specified type (via -t).\nMust specify also at least -o, -f, or -m.\n\n",
)
commands.add_argument(
"-r",
"--rename",
action="store_true",
help="Rename the file based on specified tag style.",
)
commands.add_argument(
"-e",
"--export-to-zip",
action="store_true",
help="Export RAR archive to Zip format.",
)
# TODO: update for new api
commands.add_argument(
"--only-set-cv-key",
action="store_true",
help="Only set the Comic Vine API key and quit.\n\n",
)
parser.add_argument(
"-1",
"--assume-issue-one",
action="store_true",
help="""Assume issue number is 1 if not found (relevant for -s).\n\n""",
)
parser.add_argument(
"--abort-on-conflict",
action="store_true",
help="""Don't export to zip if intended new filename\nexists (otherwise, creates a new unique filename).\n\n""",
)
parser.add_argument(
"-a",
"--auto-imprint",
action="store_true",
help="""Enables the auto imprint functionality.\ne.g. if the publisher is set to 'vertigo' it\nwill be updated to 'DC Comics' and the imprint\nproperty will be set to 'Vertigo'.\n\n""",
)
parser.add_argument(
"--config",
dest="config_path",
help="""Config directory defaults to ~/.ComicTagger\non Linux/Mac and %%APPDATA%% on Windows\n""",
)
# TODO: update for new api
parser.add_argument(
"--cv-api-key",
help="Use the given Comic Vine API Key (persisted in settings).",
)
# TODO: update for new api
parser.add_argument(
"--cv-url",
help="Use the given Comic Vine URL (persisted in settings).",
)
parser.add_argument(
"--delete-rar",
action="store_true",
dest="delete_after_zip_export",
help="""Delete original RAR archive after successful\nexport to Zip.""",
)
parser.add_argument(
"-f",
"--parse-filename",
"--parsefilename",
action="store_true",
help="""Parse the filename to get some info,\nspecifically series name, issue number,\nvolume, and publication year.\n\n""",
)
parser.add_argument(
"--id",
dest="issue_id",
type=int,
help="""Use the issue ID when searching online.\nOverrides all other metadata.\n\n""",
)
parser.add_argument(
"-t",
"--type",
metavar="{CR,CBL,COMET}",
default=[],
type=metadata_type,
help="""Specify TYPE as either CR, CBL or COMET\n(as either ComicRack, ComicBookLover,\nor CoMet style tags, respectively).\nUse commas for multiple types.\nFor searching the metadata will use the first listed:\neg '-t cbl,cr' with no CBL tags, CR will be used if they exist\n\n""",
)
parser.add_argument(
"-o",
"--online",
action="store_true",
help="""Search online and attempt to identify file\nusing existing metadata and images in archive.\nMay be used in conjunction with -f and -m.\n\n""",
)
parser.add_argument(
"-m",
"--metadata",
default=GenericMetadata(),
type=parse_metadata_from_string,
help="""Explicitly define, as a list, some tags to be used. e.g.:\n"series=Plastic Man, publisher=Quality Comics"\n"series=Kickers^, Inc., issue=1, year=1986"\nName-Value pairs are comma separated. Use a\n"^" to escape an "=" or a ",", as shown in\nthe example above. Some names that can be\nused: series, issue, issue_count, year,\npublisher, title\n\n""",
)
parser.add_argument(
"-i",
"--interactive",
action="store_true",
help="""Interactively query the user when there are\nmultiple matches for an online search.\n\n""",
)
parser.add_argument(
"--no-overwrite",
"--nooverwrite",
action="store_true",
help="""Don't modify tag block if it already exists (relevant for -s or -c).""",
)
parser.add_argument(
"--noabort",
dest="abort_on_low_confidence",
action="store_false",
help="""Don't abort save operation when online match\nis of low confidence.\n\n""",
)
parser.add_argument(
"--nosummary",
dest="show_save_summary",
action="store_false",
help="Suppress the default summary after a save operation.\n\n",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="""Overwrite all existing metadata.\nMay be used in conjunction with -o, -f and -m.\n\n""",
)
parser.add_argument(
"--raw", action="store_true", help="""With -p, will print out the raw tag block(s)\nfrom the file.\n"""
)
parser.add_argument(
"-R",
"--recursive",
action="store_true",
help="Recursively include files in sub-folders.",
)
parser.add_argument(
"-S",
"--script",
help="""Run an "add-on" python script that uses the\nComicTagger library for custom processing.\nScript arguments can follow the script name.\n\n""",
)
parser.add_argument(
"--split-words",
action="store_true",
help="""Splits words before parsing the filename.\ne.g. 'judgedredd' to 'judge dredd'\n\n""",
)
parser.add_argument(
"--terse",
action="store_true",
help="Don't say much (for print mode).",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Be noisy when doing what it does.",
)
# TODO: update for new api
parser.add_argument(
"-w",
"--wait-on-cv-rate-limit",
action="store_true",
help="""When encountering a Comic Vine rate limit\nerror, wait and retry query.\n\n""",
)
parser.add_argument(
"-n", "--dryrun", action="store_true", help="Don't actually modify file (only relevant for -d, -s, or -r).\n\n"
)
parser.add_argument(
"--darkmode",
action="store_true",
help="Windows only. Force a dark pallet",
)
parser.add_argument(
"-g",
"--glob",
action="store_true",
help="Windows only. Enable globbing",
)
parser.add_argument("files", nargs="*")
return parser
def metadata_type(types: str) -> list[int]:
result = []
types = types.casefold()
for typ in types.split(","):
typ = typ.strip()
if typ not in MetaDataStyle.short_name:
choices = ", ".join(MetaDataStyle.short_name)
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
result.append(MetaDataStyle.short_name.index(typ))
return result
def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
"""The metadata string is a comma separated list of name-value pairs
The names match the attributes of the internal metadata struct (for now)
The caret is the special "escape character", since it's not common in
natural language text
example = "series=Kickers^, Inc. ,issue=1, year=1986"
"""
escaped_comma = "^,"
escaped_equals = "^="
replacement_token = "<_~_>"
md = GenericMetadata()
# First, replace escaped commas with with a unique token (to be changed back later)
mdstr = mdstr.replace(escaped_comma, replacement_token)
tmp_list = mdstr.split(",")
md_list = []
for item in tmp_list:
item = item.replace(replacement_token, ",")
md_list.append(item)
# Now build a nice dict from the list
md_dict = {}
for item in md_list:
# Make sure to fix any escaped equal signs
i = item.replace(escaped_equals, replacement_token)
key, value = i.split("=")
value = value.replace(replacement_token, "=").strip()
key = key.strip()
if key.casefold() == "credit":
cred_attribs = value.split(":")
role = cred_attribs[0]
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
primary = len(cred_attribs) > 2
md.add_credit(person.strip(), role.strip(), primary)
else:
md_dict[key] = value
# Map the dict to the metadata object
for key, value in md_dict.items():
if not hasattr(md, key):
raise argparse.ArgumentTypeError(f"'{key}' is not a valid tag name")
else:
md.is_empty = False
setattr(md, key, value)
return md
def launch_script(scriptfile: str, args: list[str]) -> None:
# we were given a script. special case for the args:
# 1. ignore everything before the -S,
# 2. pass all the ones that follow (including script name) to the script
if not os.path.exists(scriptfile):
logger.error("Can't find %s", scriptfile)
else:
# I *think* this makes sense:
# assume the base name of the file is the module name
# add the folder of the given file to the python path import module
dirname = os.path.dirname(scriptfile)
module_name = os.path.splitext(os.path.basename(scriptfile))[0]
sys.path = [dirname] + sys.path
try:
script = __import__(module_name)
# Determine if the entry point exists before trying to run it
if "main" in dir(script):
script.main(args)
else:
logger.error("Can't find entry point 'main()' in module '%s'", module_name)
except Exception:
logger.exception("Script: %s raised an unhandled exception: ", module_name)
sys.exit(0)
def parse_cmd_line() -> argparse.Namespace:
if platform.system() == "Darwin" and getattr(sys, "frozen", False):
# remove the PSN (process serial number) argument from OS/X
input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a]
else:
input_args = sys.argv[1:]
script_args = []
# first check if we're launching a script and split off script args
for n, _ in enumerate(input_args):
if input_args[n] == "--":
break
if input_args[n] in ["-S", "--script"] and n + 1 < len(input_args):
# insert a "--" which will cause getopt to ignore the remaining args
# so they will be passed to the script
script_args = input_args[n + 2 :]
input_args = input_args[: n + 2]
break
parser = define_args()
opts = parser.parse_args(input_args)
if opts.config_path:
opts.config_path = os.path.abspath(opts.config_path)
if opts.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",
)
opts.no_gui = any(
[
opts.print,
opts.delete,
opts.save,
opts.copy,
opts.rename,
opts.export_to_zip,
opts.only_set_cv_key, # TODO: update for new api
]
)
if opts.script is not None:
launch_script(opts.script, script_args)
if platform.system() == "Windows" and opts.glob:
# no globbing on windows shell, so do it for them
import glob
globs = opts.files
opts.files = []
for item in globs:
opts.files.extend(glob.glob(item))
# TODO: update for new api
if opts.only_set_cv_key and opts.cv_api_key is None and opts.cv_url is None:
parser.exit(message="Key not given!\n", status=1)
# TODO: update for new api
if not opts.only_set_cv_key and opts.no_gui and not opts.files:
parser.exit(message="Command requires at least one filename!\n", status=1)
if opts.delete and not opts.type:
parser.exit(message="Please specify the type to delete with -t\n", status=1)
if opts.save and not opts.type:
parser.exit(message="Please specify the type to save with -t\n", status=1)
if opts.copy:
if not opts.type:
parser.exit(message="Please specify the type to copy to with -t\n", status=1)
if len(opts.copy) > 1:
parser.exit(message="Please specify only one type to copy to with -c\n", status=1)
opts.copy = opts.copy[0]
if opts.recursive:
opts.file_list = utils.get_recursive_filelist(opts.files)
else:
opts.file_list = opts.files
return opts

View File

@ -36,7 +36,7 @@ class PageBrowserWindow(QtWidgets.QDialog):
uic.loadUi(ui_path / "pagebrowser.ui", self)
self.pageWidget = CoverImageWidget(self.pageContainer, talker_api, CoverImageWidget.ArchiveMode)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode, None, None)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)

View File

@ -73,7 +73,7 @@ class PageListEditor(QtWidgets.QWidget):
uic.loadUi(ui_path / "pagelisteditor.ui", self)
self.pageWidget = CoverImageWidget(self.pageContainer, talker_api, CoverImageWidget.ArchiveMode)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode, None, None)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)

View File

@ -22,8 +22,8 @@ from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import settings
from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.settingswindow import SettingsWindow
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import center_window_on_parent
@ -38,7 +38,7 @@ class RenameWindow(QtWidgets.QDialog):
parent: QtWidgets.QWidget,
comic_archive_list: list[ComicArchive],
data_style: int,
settings: ComicTaggerSettings,
options: settings.OptionValues,
talker_api: ComicTalker,
) -> None:
super().__init__(parent)
@ -54,25 +54,25 @@ class RenameWindow(QtWidgets.QDialog):
)
)
self.settings = settings
self.options = options
self.talker_api = talker_api
self.comic_archive_list = comic_archive_list
self.data_style = data_style
self.rename_list: list[str] = []
self.btnSettings.clicked.connect(self.modify_settings)
platform = "universal" if self.settings.rename_strict else "auto"
platform = "universal" if self.options["filename"]["rename_strict"] else "auto"
self.renamer = FileRenamer(None, platform=platform)
self.do_preview()
def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str:
self.renamer.set_template(self.settings.rename_template)
self.renamer.set_issue_zero_padding(self.settings.rename_issue_number_padding)
self.renamer.set_smart_cleanup(self.settings.rename_use_smart_string_cleanup)
self.renamer.set_template(self.options["filename"]["rename_template"])
self.renamer.set_issue_zero_padding(self.options["filename"]["rename_issue_number_padding"])
self.renamer.set_smart_cleanup(self.options["filename"]["rename_use_smart_string_cleanup"])
new_ext = ca.path.suffix # default
if self.settings.rename_extension_based_on_archive:
if self.options["filename"]["rename_set_extension_based_on_archive"]:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
@ -84,13 +84,13 @@ class RenameWindow(QtWidgets.QDialog):
md = ca.read_metadata(self.data_style)
if md.is_empty:
md = ca.metadata_from_filename(
self.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options["filename"]["complicated_parser"],
self.options["filename"]["remove_c2c"],
self.options["filename"]["remove_fcbd"],
self.options["filename"]["remove_publisher"],
)
self.renamer.set_metadata(md)
self.renamer.move = self.settings.rename_move_dir
self.renamer.move = self.options["filename"]["rename_move_to_dir"]
return new_ext
def do_preview(self) -> None:
@ -103,7 +103,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.settings.rename_template)
logger.exception("Invalid format string: %s", self.options["filename"]["rename_template"])
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
@ -117,7 +117,9 @@ class RenameWindow(QtWidgets.QDialog):
return
except Exception as e:
logger.exception(
"Formatter failure: %s metadata: %s", self.settings.rename_template, self.renamer.metadata
"Formatter failure: %s metadata: %s",
self.options["filename"]["rename_template"],
self.renamer.metadata,
)
QtWidgets.QMessageBox.critical(
self,
@ -164,7 +166,7 @@ class RenameWindow(QtWidgets.QDialog):
self.twList.setSortingEnabled(True)
def modify_settings(self) -> None:
settingswin = SettingsWindow(self, self.settings, self.talker_api)
settingswin = SettingsWindow(self, self.options, self.talker_api)
settingswin.setModal(True)
settingswin.show_rename_tab()
settingswin.exec()
@ -192,7 +194,10 @@ class RenameWindow(QtWidgets.QDialog):
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
folder = get_rename_dir(comic[0], self.settings.rename_dir if self.settings.rename_move_dir else None)
folder = get_rename_dir(
comic[0],
self.options["filename"]["rename_dir"] if self.options["filename"]["rename_move_to_dir"] else None,
)
full_path = folder / comic[1]

View File

@ -1,420 +0,0 @@
"""Settings class for ComicTagger app"""
#
# Copyright 2012-2014 Anthony Beville
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import configparser
import logging
import os
import pathlib
import platform
import uuid
from collections.abc import Iterator
from typing import TextIO, no_type_check
from comicapi import utils
logger = logging.getLogger(__name__)
class ComicTaggerSettings:
folder: pathlib.Path | str = ""
@staticmethod
def get_settings_folder() -> pathlib.Path:
if not ComicTaggerSettings.folder:
if platform.system() == "Windows":
ComicTaggerSettings.folder = pathlib.Path(os.environ["APPDATA"]) / "ComicTagger"
else:
ComicTaggerSettings.folder = pathlib.Path(os.path.expanduser("~")) / ".ComicTagger"
return pathlib.Path(ComicTaggerSettings.folder)
def __init__(self, folder: str | pathlib.Path | None) -> None:
# General Settings
self.rar_exe_path = ""
self.allow_cbi_in_rar = True
self.check_for_new_version = False
self.send_usage_stats = False
# automatic settings
self.install_id = uuid.uuid4().hex
self.last_selected_save_data_style = 0
self.last_selected_load_data_style = 0
self.last_opened_folder = ""
self.last_main_window_width = 0
self.last_main_window_height = 0
self.last_main_window_x = 0
self.last_main_window_y = 0
self.last_form_side_width = -1
self.last_list_side_width = -1
self.last_filelist_sorted_column = -1
self.last_filelist_sorted_order = 0
# identifier settings
self.id_series_match_search_thresh = 90
self.id_series_match_identify_thresh = 91
self.id_publisher_filter = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
# Show/ask dialog flags
self.ask_about_cbi_in_rar = True
self.show_disclaimer = True
self.settings_warning = 0
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
# filename parsing settings
self.complicated_parser = False
self.remove_c2c = False
self.remove_fcbd = False
self.remove_publisher = False
# Comic Vine settings
self.use_series_start_as_volume = False
self.clear_form_before_populating_from_cv = False
self.remove_html_tables = False
self.cv_api_key = ""
self.cv_url = ""
self.auto_imprint = False
self.sort_series_by_year = True
self.exact_series_matches_first = True
self.always_use_publisher_filter = False
# CBL Transform settings
self.assume_lone_credit_is_primary = False
self.copy_characters_to_tags = False
self.copy_teams_to_tags = False
self.copy_locations_to_tags = False
self.copy_storyarcs_to_tags = False
self.copy_notes_to_comments = False
self.copy_weblink_to_comments = False
self.apply_cbl_transform_on_cv_import = False
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
self.rename_template = "{series} #{issue} ({year})"
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
self.rename_dir = ""
self.rename_move_dir = False
self.rename_strict = False
# Auto-tag stickies
self.save_on_low_confidence = False
self.dont_use_year_when_identifying = False
self.assume_1_if_no_issue_num = False
self.ignore_leading_numbers_in_filename = False
self.remove_archive_after_successful_match = False
self.wait_and_retry_on_rate_limit = False
self.config = configparser.RawConfigParser()
if folder:
ComicTaggerSettings.folder = pathlib.Path(folder)
else:
ComicTaggerSettings.folder = ComicTaggerSettings.get_settings_folder()
if not os.path.exists(ComicTaggerSettings.folder):
os.makedirs(ComicTaggerSettings.folder)
self.settings_file = os.path.join(ComicTaggerSettings.folder, "settings")
# if config file doesn't exist, write one out
if not os.path.exists(self.settings_file):
self.save()
else:
self.load()
# take a crack at finding rar exe, if not set already
if self.rar_exe_path == "":
if platform.system() == "Windows":
# look in some likely places for Windows machines
if os.path.exists(r"C:\Program Files\WinRAR\Rar.exe"):
self.rar_exe_path = r"C:\Program Files\WinRAR\Rar.exe"
elif os.path.exists(r"C:\Program Files (x86)\WinRAR\Rar.exe"):
self.rar_exe_path = r"C:\Program Files (x86)\WinRAR\Rar.exe"
else:
if os.path.exists("/opt/homebrew/bin"):
utils.add_to_path("/opt/homebrew/bin")
# see if it's in the path of unix user
rarpath = utils.which("rar")
if rarpath is not None:
self.rar_exe_path = "rar"
if self.rar_exe_path != "":
self.save()
if self.rar_exe_path != "":
# make sure rar program is now in the path for the rar class
utils.add_to_path(os.path.dirname(self.rar_exe_path))
def reset(self) -> None:
os.unlink(self.settings_file)
self.__init__(ComicTaggerSettings.folder) # type: ignore[misc]
def load(self) -> None:
def readline_generator(f: TextIO) -> Iterator[str]:
line = f.readline()
while line:
yield line
line = f.readline()
with open(self.settings_file, encoding="utf-8") as f:
self.config.read_file(readline_generator(f))
self.rar_exe_path = self.config.get("settings", "rar_exe_path")
if self.config.has_option("settings", "check_for_new_version"):
self.check_for_new_version = self.config.getboolean("settings", "check_for_new_version")
if self.config.has_option("settings", "send_usage_stats"):
self.send_usage_stats = self.config.getboolean("settings", "send_usage_stats")
if self.config.has_option("auto", "install_id"):
self.install_id = self.config.get("auto", "install_id")
if self.config.has_option("auto", "last_selected_load_data_style"):
self.last_selected_load_data_style = self.config.getint("auto", "last_selected_load_data_style")
if self.config.has_option("auto", "last_selected_save_data_style"):
self.last_selected_save_data_style = self.config.getint("auto", "last_selected_save_data_style")
if self.config.has_option("auto", "last_opened_folder"):
self.last_opened_folder = self.config.get("auto", "last_opened_folder")
if self.config.has_option("auto", "last_main_window_width"):
self.last_main_window_width = self.config.getint("auto", "last_main_window_width")
if self.config.has_option("auto", "last_main_window_height"):
self.last_main_window_height = self.config.getint("auto", "last_main_window_height")
if self.config.has_option("auto", "last_main_window_x"):
self.last_main_window_x = self.config.getint("auto", "last_main_window_x")
if self.config.has_option("auto", "last_main_window_y"):
self.last_main_window_y = self.config.getint("auto", "last_main_window_y")
if self.config.has_option("auto", "last_form_side_width"):
self.last_form_side_width = self.config.getint("auto", "last_form_side_width")
if self.config.has_option("auto", "last_list_side_width"):
self.last_list_side_width = self.config.getint("auto", "last_list_side_width")
if self.config.has_option("auto", "last_filelist_sorted_column"):
self.last_filelist_sorted_column = self.config.getint("auto", "last_filelist_sorted_column")
if self.config.has_option("auto", "last_filelist_sorted_order"):
self.last_filelist_sorted_order = self.config.getint("auto", "last_filelist_sorted_order")
if self.config.has_option("identifier", "id_series_match_search_thresh"):
self.id_series_match_search_thresh = self.config.getint("identifier", "id_series_match_search_thresh")
if self.config.has_option("identifier", "id_series_match_identify_thresh"):
self.id_series_match_identify_thresh = self.config.getint("identifier", "id_series_match_identify_thresh")
if self.config.has_option("identifier", "id_publisher_filter"):
self.id_publisher_filter = self.config.get("identifier", "id_publisher_filter")
if self.config.has_option("filenameparser", "complicated_parser"):
self.complicated_parser = self.config.getboolean("filenameparser", "complicated_parser")
if self.config.has_option("filenameparser", "remove_c2c"):
self.remove_c2c = self.config.getboolean("filenameparser", "remove_c2c")
if self.config.has_option("filenameparser", "remove_fcbd"):
self.remove_fcbd = self.config.getboolean("filenameparser", "remove_fcbd")
if self.config.has_option("filenameparser", "remove_publisher"):
self.remove_publisher = self.config.getboolean("filenameparser", "remove_publisher")
if self.config.has_option("dialogflags", "ask_about_cbi_in_rar"):
self.ask_about_cbi_in_rar = self.config.getboolean("dialogflags", "ask_about_cbi_in_rar")
if self.config.has_option("dialogflags", "show_disclaimer"):
self.show_disclaimer = self.config.getboolean("dialogflags", "show_disclaimer")
if self.config.has_option("dialogflags", "settings_warning"):
self.settings_warning = self.config.getint("dialogflags", "settings_warning")
if self.config.has_option("dialogflags", "dont_notify_about_this_version"):
self.dont_notify_about_this_version = self.config.get("dialogflags", "dont_notify_about_this_version")
if self.config.has_option("dialogflags", "ask_about_usage_stats"):
self.ask_about_usage_stats = self.config.getboolean("dialogflags", "ask_about_usage_stats")
if self.config.has_option("comicvine", "use_series_start_as_volume"):
self.use_series_start_as_volume = self.config.getboolean("comicvine", "use_series_start_as_volume")
if self.config.has_option("comicvine", "clear_form_before_populating_from_cv"):
self.clear_form_before_populating_from_cv = self.config.getboolean(
"comicvine", "clear_form_before_populating_from_cv"
)
if self.config.has_option("comicvine", "remove_html_tables"):
self.remove_html_tables = self.config.getboolean("comicvine", "remove_html_tables")
if self.config.has_option("comicvine", "sort_series_by_year"):
self.sort_series_by_year = self.config.getboolean("comicvine", "sort_series_by_year")
if self.config.has_option("comicvine", "exact_series_matches_first"):
self.exact_series_matches_first = self.config.getboolean("comicvine", "exact_series_matches_first")
if self.config.has_option("comicvine", "always_use_publisher_filter"):
self.always_use_publisher_filter = self.config.getboolean("comicvine", "always_use_publisher_filter")
if self.config.has_option("comicvine", "cv_api_key"):
self.cv_api_key = self.config.get("comicvine", "cv_api_key")
if self.config.has_option("comicvine", "cv_url"):
self.cv_url = self.config.get("comicvine", "cv_url")
if self.config.has_option("cbl_transform", "assume_lone_credit_is_primary"):
self.assume_lone_credit_is_primary = self.config.getboolean(
"cbl_transform", "assume_lone_credit_is_primary"
)
if self.config.has_option("cbl_transform", "copy_characters_to_tags"):
self.copy_characters_to_tags = self.config.getboolean("cbl_transform", "copy_characters_to_tags")
if self.config.has_option("cbl_transform", "copy_teams_to_tags"):
self.copy_teams_to_tags = self.config.getboolean("cbl_transform", "copy_teams_to_tags")
if self.config.has_option("cbl_transform", "copy_locations_to_tags"):
self.copy_locations_to_tags = self.config.getboolean("cbl_transform", "copy_locations_to_tags")
if self.config.has_option("cbl_transform", "copy_notes_to_comments"):
self.copy_notes_to_comments = self.config.getboolean("cbl_transform", "copy_notes_to_comments")
if self.config.has_option("cbl_transform", "copy_storyarcs_to_tags"):
self.copy_storyarcs_to_tags = self.config.getboolean("cbl_transform", "copy_storyarcs_to_tags")
if self.config.has_option("cbl_transform", "copy_weblink_to_comments"):
self.copy_weblink_to_comments = self.config.getboolean("cbl_transform", "copy_weblink_to_comments")
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_cv_import"):
self.apply_cbl_transform_on_cv_import = self.config.getboolean(
"cbl_transform", "apply_cbl_transform_on_cv_import"
)
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_bulk_operation"):
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean(
"cbl_transform", "apply_cbl_transform_on_bulk_operation"
)
if self.config.has_option("rename", "rename_template"):
self.rename_template = self.config.get("rename", "rename_template")
if self.config.has_option("rename", "rename_issue_number_padding"):
self.rename_issue_number_padding = self.config.getint("rename", "rename_issue_number_padding")
if self.config.has_option("rename", "rename_use_smart_string_cleanup"):
self.rename_use_smart_string_cleanup = self.config.getboolean("rename", "rename_use_smart_string_cleanup")
if self.config.has_option("rename", "rename_extension_based_on_archive"):
self.rename_extension_based_on_archive = self.config.getboolean(
"rename", "rename_extension_based_on_archive"
)
if self.config.has_option("rename", "rename_dir"):
self.rename_dir = self.config.get("rename", "rename_dir")
if self.config.has_option("rename", "rename_move_dir"):
self.rename_move_dir = self.config.getboolean("rename", "rename_move_dir")
if self.config.has_option("rename", "rename_strict"):
self.rename_strict = self.config.getboolean("rename", "rename_strict")
if self.config.has_option("autotag", "save_on_low_confidence"):
self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence")
if self.config.has_option("autotag", "dont_use_year_when_identifying"):
self.dont_use_year_when_identifying = self.config.getboolean("autotag", "dont_use_year_when_identifying")
if self.config.has_option("autotag", "assume_1_if_no_issue_num"):
self.assume_1_if_no_issue_num = self.config.getboolean("autotag", "assume_1_if_no_issue_num")
if self.config.has_option("autotag", "ignore_leading_numbers_in_filename"):
self.ignore_leading_numbers_in_filename = self.config.getboolean(
"autotag", "ignore_leading_numbers_in_filename"
)
if self.config.has_option("autotag", "remove_archive_after_successful_match"):
self.remove_archive_after_successful_match = self.config.getboolean(
"autotag", "remove_archive_after_successful_match"
)
if self.config.has_option("autotag", "wait_and_retry_on_rate_limit"):
self.wait_and_retry_on_rate_limit = self.config.getboolean("autotag", "wait_and_retry_on_rate_limit")
if self.config.has_option("autotag", "auto_imprint"):
self.auto_imprint = self.config.getboolean("autotag", "auto_imprint")
@no_type_check
def save(self) -> None:
if not self.config.has_section("settings"):
self.config.add_section("settings")
self.config.set("settings", "check_for_new_version", self.check_for_new_version)
self.config.set("settings", "rar_exe_path", self.rar_exe_path)
self.config.set("settings", "send_usage_stats", self.send_usage_stats)
if not self.config.has_section("auto"):
self.config.add_section("auto")
self.config.set("auto", "install_id", self.install_id)
self.config.set("auto", "last_selected_load_data_style", self.last_selected_load_data_style)
self.config.set("auto", "last_selected_save_data_style", self.last_selected_save_data_style)
self.config.set("auto", "last_opened_folder", self.last_opened_folder)
self.config.set("auto", "last_main_window_width", self.last_main_window_width)
self.config.set("auto", "last_main_window_height", self.last_main_window_height)
self.config.set("auto", "last_main_window_x", self.last_main_window_x)
self.config.set("auto", "last_main_window_y", self.last_main_window_y)
self.config.set("auto", "last_form_side_width", self.last_form_side_width)
self.config.set("auto", "last_list_side_width", self.last_list_side_width)
self.config.set("auto", "last_filelist_sorted_column", self.last_filelist_sorted_column)
self.config.set("auto", "last_filelist_sorted_order", self.last_filelist_sorted_order)
if not self.config.has_section("identifier"):
self.config.add_section("identifier")
self.config.set("identifier", "id_series_match_search_thresh", self.id_series_match_search_thresh)
self.config.set("identifier", "id_series_match_identify_thresh", self.id_series_match_identify_thresh)
self.config.set("identifier", "id_publisher_filter", self.id_publisher_filter)
if not self.config.has_section("dialogflags"):
self.config.add_section("dialogflags")
self.config.set("dialogflags", "ask_about_cbi_in_rar", self.ask_about_cbi_in_rar)
self.config.set("dialogflags", "show_disclaimer", self.show_disclaimer)
self.config.set("dialogflags", "settings_warning", self.settings_warning)
self.config.set("dialogflags", "dont_notify_about_this_version", self.dont_notify_about_this_version)
self.config.set("dialogflags", "ask_about_usage_stats", self.ask_about_usage_stats)
if not self.config.has_section("filenameparser"):
self.config.add_section("filenameparser")
self.config.set("filenameparser", "complicated_parser", self.complicated_parser)
self.config.set("filenameparser", "remove_c2c", self.remove_c2c)
self.config.set("filenameparser", "remove_fcbd", self.remove_fcbd)
self.config.set("filenameparser", "remove_publisher", self.remove_publisher)
if not self.config.has_section("comicvine"):
self.config.add_section("comicvine")
self.config.set("comicvine", "use_series_start_as_volume", self.use_series_start_as_volume)
self.config.set("comicvine", "clear_form_before_populating_from_cv", self.clear_form_before_populating_from_cv)
self.config.set("comicvine", "remove_html_tables", self.remove_html_tables)
self.config.set("comicvine", "sort_series_by_year", self.sort_series_by_year)
self.config.set("comicvine", "exact_series_matches_first", self.exact_series_matches_first)
self.config.set("comicvine", "always_use_publisher_filter", self.always_use_publisher_filter)
self.config.set("comicvine", "cv_api_key", self.cv_api_key)
self.config.set("comicvine", "cv_url", self.cv_url)
if not self.config.has_section("cbl_transform"):
self.config.add_section("cbl_transform")
self.config.set("cbl_transform", "assume_lone_credit_is_primary", self.assume_lone_credit_is_primary)
self.config.set("cbl_transform", "copy_characters_to_tags", self.copy_characters_to_tags)
self.config.set("cbl_transform", "copy_teams_to_tags", self.copy_teams_to_tags)
self.config.set("cbl_transform", "copy_locations_to_tags", self.copy_locations_to_tags)
self.config.set("cbl_transform", "copy_storyarcs_to_tags", self.copy_storyarcs_to_tags)
self.config.set("cbl_transform", "copy_notes_to_comments", self.copy_notes_to_comments)
self.config.set("cbl_transform", "copy_weblink_to_comments", self.copy_weblink_to_comments)
self.config.set("cbl_transform", "apply_cbl_transform_on_cv_import", self.apply_cbl_transform_on_cv_import)
self.config.set(
"cbl_transform",
"apply_cbl_transform_on_bulk_operation",
self.apply_cbl_transform_on_bulk_operation,
)
if not self.config.has_section("rename"):
self.config.add_section("rename")
self.config.set("rename", "rename_template", self.rename_template)
self.config.set("rename", "rename_issue_number_padding", self.rename_issue_number_padding)
self.config.set("rename", "rename_use_smart_string_cleanup", self.rename_use_smart_string_cleanup)
self.config.set("rename", "rename_extension_based_on_archive", self.rename_extension_based_on_archive)
self.config.set("rename", "rename_dir", self.rename_dir)
self.config.set("rename", "rename_move_dir", self.rename_move_dir)
self.config.set("rename", "rename_strict", self.rename_strict)
if not self.config.has_section("autotag"):
self.config.add_section("autotag")
self.config.set("autotag", "save_on_low_confidence", self.save_on_low_confidence)
self.config.set("autotag", "dont_use_year_when_identifying", self.dont_use_year_when_identifying)
self.config.set("autotag", "assume_1_if_no_issue_num", self.assume_1_if_no_issue_num)
self.config.set("autotag", "ignore_leading_numbers_in_filename", self.ignore_leading_numbers_in_filename)
self.config.set("autotag", "remove_archive_after_successful_match", self.remove_archive_after_successful_match)
self.config.set("autotag", "wait_and_retry_on_rate_limit", self.wait_and_retry_on_rate_limit)
self.config.set("autotag", "auto_imprint", self.auto_imprint)
with open(self.settings_file, "w", encoding="utf-8") as configfile:
self.config.write(configfile)

View File

@ -0,0 +1,17 @@
from __future__ import annotations
from comictaggerlib.settings.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options
from comictaggerlib.settings.file import register_settings, validate_settings
from comictaggerlib.settings.manager import Manager
from comictaggerlib.settings.types import ComicTaggerPaths, OptionValues
__all__ = [
"initial_cmd_line_parser",
"register_commandline",
"register_settings",
"validate_commandline_options",
"validate_settings",
"Manager",
"ComicTaggerPaths",
"OptionValues",
]

View File

@ -0,0 +1,343 @@
"""CLI options class for ComicTagger app"""
#
# Copyright 2012-2014 Anthony Beville
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import argparse
import logging
import os
import platform
from typing import Any
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import ctversion
from comictaggerlib.settings.manager import Manager
from comictaggerlib.settings.types import ComicTaggerPaths, metadata_type, parse_metadata_from_string
logger = logging.getLogger(__name__)
def initial_cmd_line_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(add_help=False)
# Ensure this stays up to date with register_options
parser.add_argument(
"--config",
help="Config directory defaults to ~/.ComicTagger\non Linux/Mac and %%APPDATA%% on Windows\n",
type=ComicTaggerPaths,
default=ComicTaggerPaths(),
)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Be noisy when doing what it does.",
)
return parser
def register_options(parser: Manager) -> None:
parser.add_setting(
"--config",
help="Config directory defaults to ~/.ComicTagger\non Linux/Mac and %%APPDATA%% on Windows\n",
type=ComicTaggerPaths,
default=ComicTaggerPaths(),
file=False,
)
parser.add_setting(
"-v",
"--verbose",
action="count",
default=0,
help="Be noisy when doing what it does.",
file=False,
)
parser.add_setting(
"--abort-on-conflict",
action="store_true",
help="""Don't export to zip if intended new filename\nexists (otherwise, creates a new unique filename).\n\n""",
file=False,
)
parser.add_setting(
"--delete-original",
action="store_true",
dest="delete_after_zip_export",
help="""Delete original archive after successful\nexport to Zip. (only relevant for -e)""",
file=False,
)
parser.add_setting(
"-f",
"--parse-filename",
"--parsefilename",
action="store_true",
help="""Parse the filename to get some info,\nspecifically series name, issue number,\nvolume, and publication year.\n\n""",
file=False,
)
parser.add_setting(
"--id",
dest="issue_id",
type=int,
help="""Use the issue ID when searching online.\nOverrides all other metadata.\n\n""",
file=False,
)
parser.add_setting(
"-o",
"--online",
action="store_true",
help="""Search online and attempt to identify file\nusing existing metadata and images in archive.\nMay be used in conjunction with -f and -m.\n\n""",
file=False,
)
parser.add_setting(
"-m",
"--metadata",
default=GenericMetadata(),
type=parse_metadata_from_string,
help="""Explicitly define, as a list, some tags to be used. e.g.:\n"series=Plastic Man, publisher=Quality Comics"\n"series=Kickers^, Inc., issue=1, year=1986"\nName-Value pairs are comma separated. Use a\n"^" to escape an "=" or a ",", as shown in\nthe example above. Some names that can be\nused: series, issue, issue_count, year,\npublisher, title\n\n""",
file=False,
)
parser.add_setting(
"-i",
"--interactive",
action="store_true",
help="""Interactively query the user when there are\nmultiple matches for an online search.\n\n""",
file=False,
)
parser.add_setting(
"--noabort",
dest="abort_on_low_confidence",
action="store_false",
help="""Don't abort save operation when online match\nis of low confidence.\n\n""",
file=False,
)
parser.add_setting(
"--nosummary",
dest="show_save_summary",
action="store_false",
help="Suppress the default summary after a save operation.\n\n",
file=False,
)
parser.add_setting(
"--raw",
action="store_true",
help="""With -p, will print out the raw tag block(s)\nfrom the file.\n""",
file=False,
)
parser.add_setting(
"-R",
"--recursive",
action="store_true",
help="Recursively include files in sub-folders.",
file=False,
)
parser.add_setting(
"-S",
"--script",
help="""Run an "add-on" python script that uses the\nComicTagger library for custom processing.\nScript arguments can follow the script name.\n\n""",
file=False,
)
parser.add_setting(
"--split-words",
action="store_true",
help="""Splits words before parsing the filename.\ne.g. 'judgedredd' to 'judge dredd'\n\n""",
file=False,
)
parser.add_setting(
"-n",
"--dryrun",
action="store_true",
help="Don't actually modify file (only relevant for -d, -s, or -r).\n\n",
file=False,
)
parser.add_setting(
"--darkmode",
action="store_true",
help="Windows only. Force a dark pallet",
file=False,
)
parser.add_setting(
"-g",
"--glob",
action="store_true",
help="Windows only. Enable globbing",
file=False,
)
parser.add_setting(
"--terse",
action="store_true",
help="Don't say much (for print mode).",
file=False,
)
parser.add_setting(
"-t",
"--type",
metavar="{CR,CBL,COMET}",
default=[],
type=metadata_type,
help="""Specify TYPE as either CR, CBL or COMET\n(as either ComicRack, ComicBookLover,\nor CoMet style tags, respectively).\nUse commas for multiple types.\nFor searching the metadata will use the first listed:\neg '-t cbl,cr' with no CBL tags, CR will be used if they exist\n\n""",
file=False,
)
parser.add_setting(
# "--no-overwrite",
# "--nooverwrite",
"--temporary",
dest="no_overwrite",
action="store_true",
help="""Don't modify tag block if it already exists (relevant for -s or -c).""",
file=False,
)
parser.add_setting("files", nargs="*", file=False)
def register_commands(parser: Manager) -> None:
parser.add_setting(
"--version",
action="store_true",
help="Display version.",
file=False,
)
parser.add_setting(
"-p",
"--print",
action="store_true",
help="""Print out tag info from file. Specify type\n(via -t) to get only info of that tag type.\n\n""",
file=False,
)
parser.add_setting(
"-d",
"--delete",
action="store_true",
help="Deletes the tag block of specified type (via -t).\n",
file=False,
)
parser.add_setting(
"-c",
"--copy",
type=metadata_type,
metavar="{CR,CBL,COMET}",
help="Copy the specified source tag block to\ndestination style specified via -t\n(potentially lossy operation).\n\n",
file=False,
)
parser.add_setting(
"-s",
"--save",
action="store_true",
help="Save out tags as specified type (via -t).\nMust specify also at least -o, -f, or -m.\n\n",
file=False,
)
parser.add_setting(
"-r",
"--rename",
action="store_true",
help="Rename the file based on specified tag style.",
file=False,
)
parser.add_setting(
"-e",
"--export-to-zip",
action="store_true",
help="Export RAR archive to Zip format.",
file=False,
)
parser.add_setting(
"--only-set-cv-key",
action="store_true",
help="Only set the Comic Vine API key and quit.\n\n",
file=False,
)
def register_commandline(parser: Manager) -> None:
parser.add_group("commands", register_commands, True)
parser.add_group("runtime", register_options)
def validate_commandline_options(options: dict[str, dict[str, Any]], parser: Manager) -> dict[str, dict[str, Any]]:
if options["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",
)
options["runtime"]["no_gui"] = any(
[
options["commands"]["print"],
options["commands"]["delete"],
options["commands"]["save"],
options["commands"]["copy"],
options["commands"]["rename"],
options["commands"]["export_to_zip"],
options["commands"]["only_set_cv_key"],
]
)
if platform.system() == "Windows" and options["runtime"]["glob"]:
# no globbing on windows shell, so do it for them
import glob
globs = options["runtime"]["files"]
options["runtime"]["files"] = []
for item in globs:
options["runtime"]["files"].extend(glob.glob(item))
if (
options["commands"]["only_set_cv_key"]
and options["comicvine"]["cv_api_key"] is None
and options["comicvine"]["cv_url"] is None
):
parser.exit(message="Key not given!\n", status=1)
if not options["commands"]["only_set_cv_key"] and options["runtime"]["no_gui"] and not options["runtime"]["files"]:
parser.exit(message="Command requires at least one filename!\n", status=1)
if options["commands"]["delete"] and not options["runtime"]["type"]:
parser.exit(message="Please specify the type to delete with -t\n", status=1)
if options["commands"]["save"] and not options["runtime"]["type"]:
parser.exit(message="Please specify the type to save with -t\n", status=1)
if options["commands"]["copy"]:
if not options["runtime"]["type"]:
parser.exit(message="Please specify the type to copy to with -t\n", status=1)
if len(options["commands"]["copy"]) > 1:
parser.exit(message="Please specify only one type to copy to with -c\n", status=1)
options["commands"]["copy"] = options["commands"]["copy"][0]
if options["runtime"]["recursive"]:
options["runtime"]["file_list"] = utils.get_recursive_filelist(options["runtime"]["files"])
else:
options["runtime"]["file_list"] = options["runtime"]["files"]
# take a crack at finding rar exe, if not set already
if options["general"]["rar_exe_path"].strip() in ("", "rar"):
if platform.system() == "Windows":
# look in some likely places for Windows machines
if os.path.exists(r"C:\Program Files\WinRAR\Rar.exe"):
options["general"]["rar_exe_path"] = r"C:\Program Files\WinRAR\Rar.exe"
elif os.path.exists(r"C:\Program Files (x86)\WinRAR\Rar.exe"):
options["general"]["rar_exe_path"] = r"C:\Program Files (x86)\WinRAR\Rar.exe"
else:
if os.path.exists("/opt/homebrew/bin"):
utils.add_to_path("/opt/homebrew/bin")
# see if it's in the path of unix user
rarpath = utils.which("rar")
if rarpath is not None:
options["general"]["rar_exe_path"] = "rar"
return options

View File

@ -0,0 +1,229 @@
from __future__ import annotations
import argparse
import uuid
from collections.abc import Sequence
from typing import Any, Callable
from comictaggerlib.settings.manager import Manager
def general(parser: Manager) -> None:
# General Settings
parser.add_setting("--rar-exe-path", default="rar")
parser.add_setting("--allow-cbi-in-rar", default=True, action=argparse.BooleanOptionalAction)
parser.add_setting("check_for_new_version", default=False, cmdline=False)
parser.add_setting("send_usage_stats", default=False, cmdline=False)
def internal(parser: Manager) -> None:
# automatic settings
parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False)
parser.add_setting("last_selected_save_data_style", default=0, cmdline=False)
parser.add_setting("last_selected_load_data_style", default=0, cmdline=False)
parser.add_setting("last_opened_folder", default="", cmdline=False)
parser.add_setting("last_main_window_width", default=0, cmdline=False)
parser.add_setting("last_main_window_height", default=0, cmdline=False)
parser.add_setting("last_main_window_x", default=0, cmdline=False)
parser.add_setting("last_main_window_y", default=0, cmdline=False)
parser.add_setting("last_form_side_width", default=-1, cmdline=False)
parser.add_setting("last_list_side_width", default=-1, cmdline=False)
parser.add_setting("last_filelist_sorted_column", default=-1, cmdline=False)
parser.add_setting("last_filelist_sorted_order", default=0, cmdline=False)
def identifier(parser: Manager) -> None:
# identifier settings
parser.add_setting("--series-match-identify-thresh", default=91, type=int)
parser.add_setting(
"--publisher-filter",
default=["Panini Comics", "Abril", "Planeta DeAgostini", "Editorial Televisa", "Dino Comics"],
action=_AppendAction,
)
def _copy_items(items: Sequence[Any] | None) -> Sequence[Any]:
if items is None:
return []
# The copy module is used only in the 'append' and 'append_const'
# actions, and it is needed only when the default value isn't a list.
# Delay its import for speeding up the common case.
if type(items) is list:
return items[:]
import copy
return copy.copy(items)
class _AppendAction(argparse.Action):
def __init__(
self,
option_strings: list[str],
dest: str,
nargs: str | None = None,
const: Any = None,
default: Any = None,
type: Callable[[str], Any] | None = None, # noqa: A002
choices: list[Any] | None = None,
required: bool = False,
help: str | None = None, # noqa: A002
metavar: str | None = None,
):
self.called = False
if nargs == 0:
raise ValueError(
"nargs for append actions must be != 0; if arg "
"strings are not supplying the value to append, "
"the append const action may be more appropriate"
)
if const is not None and nargs != argparse.OPTIONAL:
raise ValueError("nargs must be %r to supply const" % argparse.OPTIONAL)
super().__init__(
option_strings=option_strings,
dest=dest,
nargs=nargs,
const=const,
default=default,
type=type,
choices=choices,
required=required,
help=help,
metavar=metavar,
)
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None,
) -> None:
if values:
if not self.called:
setattr(namespace, self.dest, [])
items = getattr(namespace, self.dest, None)
items = _copy_items(items)
items.append(values) # type: ignore
setattr(namespace, self.dest, items)
def dialog(parser: Manager) -> None:
# Show/ask dialog flags
parser.add_setting("ask_about_cbi_in_rar", default=True, cmdline=False)
parser.add_setting("show_disclaimer", default=True, cmdline=False)
parser.add_setting("dont_notify_about_this_version", default="", cmdline=False)
parser.add_setting("ask_about_usage_stats", default=True, cmdline=False)
def filename(parser: Manager) -> None:
# filename parsing settings
parser.add_setting("--complicated-parser", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--remove-c2c", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--remove-fcbd", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--remove-publisher", default=False, action=argparse.BooleanOptionalAction)
def comicvine(parser: Manager) -> None:
# Comic Vine settings
parser.add_setting(
"--series-match-search-thresh",
default=90,
)
parser.add_setting("--use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting(
"--overwrite",
default=True,
help="Overwrite all existing metadata.\nMay be used in conjunction with -o, -f and -m.\n\n",
dest="clear_metadata_on_import",
action=argparse.BooleanOptionalAction,
)
parser.add_setting("--remove-html-tables", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting(
"--cv-api-key",
default="",
help="Use the given Comic Vine API Key (persisted in settings).",
)
parser.add_setting(
"--cv-url",
default="",
help="Use the given Comic Vine URL (persisted in settings).",
)
parser.add_setting(
"-a",
"--auto-imprint",
action=argparse.BooleanOptionalAction,
default=False,
help="""Enables the auto imprint functionality.\ne.g. if the publisher is set to 'vertigo' it\nwill be updated to 'DC Comics' and the imprint\nproperty will be set to 'Vertigo'.\n\n""",
)
parser.add_setting("--sort-series-by-year", default=True, action=argparse.BooleanOptionalAction)
parser.add_setting("--exact-series-matches-first", default=True, action=argparse.BooleanOptionalAction)
parser.add_setting("--always-use-publisher-filter", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--clear-form-before-populating-from-cv", default=False, action=argparse.BooleanOptionalAction)
def cbl(parser: Manager) -> None:
# CBL Transform settings
parser.add_setting("--assume-lone-credit-is-primary", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-characters-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-teams-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-locations-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-storyarcs-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-notes-to-comments", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-weblink-to-comments", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--apply-cbl-transform-on-cv-import", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--apply-cbl-transform-on-bulk-operation", default=False, action=argparse.BooleanOptionalAction)
def rename(parser: Manager) -> None:
# Rename settings
parser.add_setting("--template", default="{series} #{issue} ({year})")
parser.add_setting("--issue-number-padding", default=3, type=int)
parser.add_setting("--use-smart-string-cleanup", default=True, action=argparse.BooleanOptionalAction)
parser.add_setting("--set-extension-based-on-archive", default=True, action=argparse.BooleanOptionalAction)
parser.add_setting("--dir", default="")
parser.add_setting("--move-to-dir", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--strict", default=False, action=argparse.BooleanOptionalAction)
def autotag(parser: Manager) -> None:
# Auto-tag stickies
parser.add_setting("--save-on-low-confidence", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--dont-use-year-when-identifying", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting(
"-1",
"--assume-issue-one",
dest="assume_1_if_no_issue_num",
action=argparse.BooleanOptionalAction,
help="""Assume issue number is 1 if not found (relevant for -s).\n\n""",
default=False,
)
parser.add_setting("--ignore-leading-numbers-in-filename", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--remove-archive-after-successful-match", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting(
"-w",
"--wait-on-cv-rate-limit",
dest="wait_and_retry_on_rate_limit",
action=argparse.BooleanOptionalAction,
default=True,
help="""When encountering a Comic Vine rate limit\nerror, wait and retry query.\n\n""",
)
def validate_settings(options: dict[str, dict[str, Any]], parser: Manager) -> dict[str, dict[str, Any]]:
options["identifier"]["publisher_filter"] = [
x.strip() for x in options["identifier"]["publisher_filter"] if x.strip()
]
return options
def register_settings(parser: 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("comicvine", comicvine, False)
parser.add_group("cbl", cbl, False)
parser.add_group("rename", rename, False)
parser.add_group("autotag", autotag, False)

View File

@ -0,0 +1,260 @@
from __future__ import annotations
import argparse
import json
import logging
import pathlib
from collections import defaultdict
from collections.abc import Sequence
from typing import Any, Callable, NoReturn, Union
from comictaggerlib.settings.types import ComicTaggerPaths, OptionValues
logger = logging.getLogger(__name__)
class Setting:
def __init__(
self,
# From argparse
*names: str,
action: type[argparse.Action] | None = None,
nargs: str | int | None = None,
const: str | None = None,
default: str | None = None,
type: Callable[..., Any] | None = None, # noqa: A002
choices: Sequence[Any] | None = None,
required: bool | None = None,
help: str | None = None, # noqa: A002
metavar: str | None = None,
dest: str | None = None,
# ComicTagger
cmdline: bool = True,
file: bool = True,
group: str = "",
exclusive: bool = False,
):
if not names:
raise ValueError("names must be specified")
self.internal_name, dest, flag = self.get_dest(group, names, dest)
args: Sequence[str] = names
if not metavar and action not in ("store_true", "store_false", "count"):
metavar = dest.upper()
if not flag:
args = [f"{group}_{names[0]}".lstrip("_"), *names[1:]]
self.action = action
self.nargs = nargs
self.const = const
self.default = default
self.type = type
self.choices = choices
self.required = required
self.help = help
self.metavar = metavar
self.dest = dest
self.cmdline = cmdline
self.file = file
self.argparse_args = args
self.group = group
self.exclusive = exclusive
self.argparse_kwargs = {
"action": action,
"nargs": nargs,
"const": const,
"default": default,
"type": type,
"choices": choices,
"required": required,
"help": help,
"metavar": metavar,
"dest": self.internal_name if flag else None,
}
def __str__(self) -> str:
return f"Setting({self.argparse_args}, type={self.type}, file={self.file}, cmdline={self.cmdline}, kwargs={self.argparse_kwargs})"
def __repr__(self) -> str:
return self.__str__()
def get_dest(self, prefix: str, names: Sequence[str], dest: str | None) -> tuple[str, str, bool]:
dest_name = None
flag = False
for n in names:
if n.startswith("--"):
flag = True
dest_name = n.lstrip("-").replace("-", "_")
break
if n.startswith("-"):
flag = True
if dest_name is None:
dest_name = names[0]
if dest:
dest_name = dest
if dest_name is None:
raise Exception("Something failed, try again")
internal_name = f"{prefix}_{dest_name}".lstrip("_")
return internal_name, dest_name, flag
def filter_argparse_kwargs(self) -> dict[str, Any]:
return {k: v for k, v in self.argparse_kwargs.items() if v is not None}
def to_argparse(self) -> tuple[Sequence[str], dict[str, Any]]:
return self.argparse_args, self.filter_argparse_kwargs()
ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser]
class Manager:
"""docstring for SettingManager"""
def __init__(self, options: dict[str, dict[str, Setting]] | None = None):
self.argparser = argparse.ArgumentParser()
self.options: dict[str, dict[str, Setting]] = defaultdict(lambda: dict())
if options:
self.options = options
self.current_group: ArgParser | None = None
self.current_group_name = ""
def defaults(self) -> OptionValues:
return self.normalize_options({}, file=True, cmdline=True)
def get_namespace_for_args(self, options: OptionValues) -> argparse.Namespace:
namespace = argparse.Namespace()
for group_name, group in self.options.items():
for setting_name, setting in group.items():
if not setting.cmdline:
if hasattr(namespace, setting.internal_name):
raise Exception(f"Duplicate internal name: {setting.internal_name}")
setattr(
namespace, setting.internal_name, options.get(group_name, {}).get(setting.dest, setting.default)
)
return namespace
def add_setting(self, *args: Any, **kwargs: Any) -> None:
exclusive = isinstance(self.current_group, argparse._MutuallyExclusiveGroup)
setting = Setting(*args, group=self.current_group_name, exclusive=exclusive, **kwargs)
self.options[self.current_group_name][setting.dest] = setting
def create_argparser(self) -> None:
groups: dict[str, ArgParser] = {}
self.argparser = argparse.ArgumentParser(
description="""A utility for reading and writing metadata to comic archives.
If no options are given, %(prog)s will run in windowed mode.""",
epilog="For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki",
formatter_class=argparse.RawTextHelpFormatter,
)
for group_name, group in self.options.items():
for setting_name, setting in group.items():
if setting.cmdline:
argparse_args, argparse_kwargs = setting.to_argparse()
current_group: ArgParser = self.argparser
if setting.group:
if setting.group not in groups:
if setting.exclusive:
groups[setting.group] = self.argparser.add_argument_group(
setting.group
).add_mutually_exclusive_group()
else:
groups[setting.group] = self.argparser.add_argument_group(setting.group)
# hardcoded exception for files
if not (setting.group == "runtime" and setting.nargs == "*"):
current_group = groups[setting.group]
current_group.add_argument(*argparse_args, **argparse_kwargs)
def add_group(self, name: str, add_settings: Callable[[Manager], None], exclusive_group: bool = False) -> None:
self.current_group_name = name
if exclusive_group:
self.current_group = self.argparser.add_mutually_exclusive_group()
add_settings(self)
self.current_group_name = ""
self.current_group = None
def exit(self, *args: Any, **kwargs: Any) -> NoReturn:
self.argparser.exit(*args, **kwargs)
raise SystemExit(99)
def save_file(self, options: OptionValues, filename: pathlib.Path) -> bool:
self.options = options["option_definitions"]
file_options = self.normalize_options(options, file=True)
del file_options["option_definitions"]
for group in list(file_options.keys()):
if not file_options[group]:
del file_options[group]
if not filename.exists():
filename.touch()
try:
json_str = json.dumps(file_options, indent=2)
with filename.open(mode="w") as file:
file.write(json_str)
except Exception:
logger.exception("Failed to save config file: %s", filename)
return False
return True
def parse_file(self, filename: pathlib.Path) -> OptionValues:
options: OptionValues = {}
if filename.exists():
try:
with filename.open() as file:
opts = json.load(file)
if isinstance(opts, dict):
options = opts
except Exception:
logger.exception("Failed to load config file: %s", filename)
else:
logger.info("No config file found")
return self.normalize_options(options, file=True)
def normalize_options(
self, raw_options: OptionValues | argparse.Namespace, file: bool = False, cmdline: bool = False
) -> OptionValues:
options: OptionValues = {}
for group_name, group in self.options.items():
group_options = {}
for setting_name, setting in group.items():
if (setting.cmdline and cmdline) or (setting.file and file):
group_options[setting_name] = self.get_option(raw_options, setting, group_name)
options[group_name] = group_options
options["option_definitions"] = self.options
return options
def get_option(self, options: OptionValues | argparse.Namespace, setting: Setting, group_name: str) -> Any:
if isinstance(options, dict):
return options.get(group_name, {}).get(setting.dest, setting.default)
return getattr(options, setting.internal_name)
def parse_args(self, args: list[str] | None = None, namespace: argparse.Namespace | None = None) -> OptionValues:
"""Must handle a namespace with both argparse values and file values"""
self.create_argparser()
ns = self.argparser.parse_args(args, namespace=namespace)
return self.normalize_options(ns, cmdline=True, file=True)
def parse_options(self, config_paths: ComicTaggerPaths, args: list[str] | None = None) -> OptionValues:
file_options = self.parse_file(config_paths.user_config_dir / "settings.json")
cli_options = self.parse_args(args, namespace=self.get_namespace_for_args(file_options))
# for group, group_options in cli_options.items():
# if group in cli_options:
# file_options[group].update(group_options)
# else:
# file_options[group] = group_options
# Just in case something weird happens with the commandline options
file_options["runtime"]["config"] = config_paths
# Normalize a final time for fun
return self.normalize_options(cli_options, file=True, cmdline=True)

View File

@ -0,0 +1,122 @@
from __future__ import annotations
import argparse
import pathlib
from typing import Any
from appdirs import AppDirs
from comicapi.comicarchive import MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
OptionValues = dict[str, dict[str, Any]]
class ComicTaggerPaths(AppDirs):
def __init__(self, config_path: pathlib.Path | str | None = None) -> None:
super().__init__("ComicTagger", None, None, False, False)
self.path: pathlib.Path | None = None
if config_path:
self.path = pathlib.Path(config_path).absolute()
@property
def user_data_dir(self) -> pathlib.Path:
if self.path:
return self.path
return pathlib.Path(super().user_data_dir)
@property
def user_config_dir(self) -> pathlib.Path:
if self.path:
return self.path
return pathlib.Path(super().user_config_dir)
@property
def user_cache_dir(self) -> pathlib.Path:
if self.path:
path = self.path / "cache"
return path
return pathlib.Path(super().user_cache_dir)
@property
def user_state_dir(self) -> pathlib.Path:
if self.path:
return self.path
return pathlib.Path(super().user_state_dir)
@property
def user_log_dir(self) -> pathlib.Path:
if self.path:
path = self.path / "log"
return path
return pathlib.Path(super().user_log_dir)
@property
def site_data_dir(self) -> pathlib.Path:
return pathlib.Path(super().site_data_dir)
@property
def site_config_dir(self) -> pathlib.Path:
return pathlib.Path(super().site_config_dir)
def metadata_type(types: str) -> list[int]:
result = []
types = types.casefold()
for typ in types.split(","):
typ = typ.strip()
if typ not in MetaDataStyle.short_name:
choices = ", ".join(MetaDataStyle.short_name)
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
result.append(MetaDataStyle.short_name.index(typ))
return result
def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
"""The metadata string is a comma separated list of name-value pairs
The names match the attributes of the internal metadata struct (for now)
The caret is the special "escape character", since it's not common in
natural language text
example = "series=Kickers^, Inc. ,issue=1, year=1986"
"""
escaped_comma = "^,"
escaped_equals = "^="
replacement_token = "<_~_>"
md = GenericMetadata()
# First, replace escaped commas with with a unique token (to be changed back later)
mdstr = mdstr.replace(escaped_comma, replacement_token)
tmp_list = mdstr.split(",")
md_list = []
for item in tmp_list:
item = item.replace(replacement_token, ",")
md_list.append(item)
# Now build a nice dict from the list
md_dict = {}
for item in md_list:
# Make sure to fix any escaped equal signs
i = item.replace(escaped_equals, replacement_token)
key, value = i.split("=")
value = value.replace(replacement_token, "=").strip()
key = key.strip()
if key.casefold() == "credit":
cred_attribs = value.split(":")
role = cred_attribs[0]
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
primary = len(cred_attribs) > 2
md.add_credit(person.strip(), role.strip(), primary)
else:
md_dict[key] = value
# Map the dict to the metadata object
for key, value in md_dict.items():
if not hasattr(md, key):
raise argparse.ArgumentTypeError(f"'{key}' is not a valid tag name")
else:
md.is_empty = False
setattr(md, key, value)
return md

View File

@ -25,10 +25,10 @@ from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import md_test
from comictaggerlib import settings
from comictaggerlib.ctversion import version
from comictaggerlib.filerenamer import FileRenamer
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
from comictalker.comiccacher import ComicCacher
from comictalker.talkerbase import ComicTalker
@ -129,7 +129,7 @@ Spider-Geddon #1 - New Players; Check In
class SettingsWindow(QtWidgets.QDialog):
def __init__(self, parent: QtWidgets.QWidget, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
def __init__(self, parent: QtWidgets.QWidget, options: settings.OptionValues, talker_api: ComicTalker) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "settingswindow.ui", self)
@ -138,7 +138,7 @@ class SettingsWindow(QtWidgets.QDialog):
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.settings = settings
self.options = options
self.talker_api = talker_api
self.name = "Settings"
@ -227,53 +227,55 @@ class SettingsWindow(QtWidgets.QDialog):
def settings_to_form(self) -> None:
# Copy values from settings to form
self.leRarExePath.setText(self.settings.rar_exe_path)
self.sbNameMatchIdentifyThresh.setValue(self.settings.id_series_match_identify_thresh)
self.sbNameMatchSearchThresh.setValue(self.settings.id_series_match_search_thresh)
self.tePublisherFilter.setPlainText(self.settings.id_publisher_filter)
self.leRarExePath.setText(self.options["general"]["rar_exe_path"])
self.sbNameMatchIdentifyThresh.setValue(self.options["identifier"]["series_match_identify_thresh"])
self.sbNameMatchSearchThresh.setValue(self.options["comicvine"]["series_match_search_thresh"])
self.tePublisherFilter.setPlainText("\n".join(self.options["identifier"]["publisher_filter"]))
self.cbxCheckForNewVersion.setChecked(self.settings.check_for_new_version)
self.cbxCheckForNewVersion.setChecked(self.options["general"]["check_for_new_version"])
self.cbxComplicatedParser.setChecked(self.settings.complicated_parser)
self.cbxRemoveC2C.setChecked(self.settings.remove_c2c)
self.cbxRemoveFCBD.setChecked(self.settings.remove_fcbd)
self.cbxRemovePublisher.setChecked(self.settings.remove_publisher)
self.cbxComplicatedParser.setChecked(self.options["filename"]["complicated_parser"])
self.cbxRemoveC2C.setChecked(self.options["filename"]["remove_c2c"])
self.cbxRemoveFCBD.setChecked(self.options["filename"]["remove_fcbd"])
self.cbxRemovePublisher.setChecked(self.options["filename"]["remove_publisher"])
self.switch_parser()
self.cbxUseSeriesStartAsVolume.setChecked(self.settings.use_series_start_as_volume)
self.cbxClearFormBeforePopulating.setChecked(self.settings.clear_form_before_populating_from_cv)
self.cbxRemoveHtmlTables.setChecked(self.settings.remove_html_tables)
self.cbxUseSeriesStartAsVolume.setChecked(self.options["comicvine"]["use_series_start_as_volume"])
self.cbxClearFormBeforePopulating.setChecked(self.options["comicvine"]["clear_form_before_populating_from_cv"])
self.cbxRemoveHtmlTables.setChecked(self.options["comicvine"]["remove_html_tables"])
self.cbxUseFilter.setChecked(self.settings.always_use_publisher_filter)
self.cbxSortByYear.setChecked(self.settings.sort_series_by_year)
self.cbxExactMatches.setChecked(self.settings.exact_series_matches_first)
self.cbxUseFilter.setChecked(self.options["comicvine"]["always_use_publisher_filter"])
self.cbxSortByYear.setChecked(self.options["comicvine"]["sort_series_by_year"])
self.cbxExactMatches.setChecked(self.options["comicvine"]["exact_series_matches_first"])
self.leKey.setText(self.settings.cv_api_key)
self.leURL.setText(self.settings.cv_url)
self.leKey.setText(self.options["comicvine"]["cv_api_key"])
self.leURL.setText(self.options["comicvine"]["cv_url"])
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.settings.assume_lone_credit_is_primary)
self.cbxCopyCharactersToTags.setChecked(self.settings.copy_characters_to_tags)
self.cbxCopyTeamsToTags.setChecked(self.settings.copy_teams_to_tags)
self.cbxCopyLocationsToTags.setChecked(self.settings.copy_locations_to_tags)
self.cbxCopyStoryArcsToTags.setChecked(self.settings.copy_storyarcs_to_tags)
self.cbxCopyNotesToComments.setChecked(self.settings.copy_notes_to_comments)
self.cbxCopyWebLinkToComments.setChecked(self.settings.copy_weblink_to_comments)
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.settings.apply_cbl_transform_on_cv_import)
self.cbxApplyCBLTransformOnBatchOperation.setChecked(self.settings.apply_cbl_transform_on_bulk_operation)
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.options["cbl"]["assume_lone_credit_is_primary"])
self.cbxCopyCharactersToTags.setChecked(self.options["cbl"]["copy_characters_to_tags"])
self.cbxCopyTeamsToTags.setChecked(self.options["cbl"]["copy_teams_to_tags"])
self.cbxCopyLocationsToTags.setChecked(self.options["cbl"]["copy_locations_to_tags"])
self.cbxCopyStoryArcsToTags.setChecked(self.options["cbl"]["copy_storyarcs_to_tags"])
self.cbxCopyNotesToComments.setChecked(self.options["cbl"]["copy_notes_to_comments"])
self.cbxCopyWebLinkToComments.setChecked(self.options["cbl"]["copy_weblink_to_comments"])
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.options["cbl"]["apply_cbl_transform_on_cv_import"])
self.cbxApplyCBLTransformOnBatchOperation.setChecked(
self.options["cbl"]["apply_cbl_transform_on_bulk_operation"]
)
self.leRenameTemplate.setText(self.settings.rename_template)
self.leIssueNumPadding.setText(str(self.settings.rename_issue_number_padding))
self.cbxSmartCleanup.setChecked(self.settings.rename_use_smart_string_cleanup)
self.cbxChangeExtension.setChecked(self.settings.rename_extension_based_on_archive)
self.cbxMoveFiles.setChecked(self.settings.rename_move_dir)
self.leDirectory.setText(self.settings.rename_dir)
self.cbxRenameStrict.setChecked(self.settings.rename_strict)
self.leRenameTemplate.setText(self.options["rename"]["template"])
self.leIssueNumPadding.setText(str(self.options["rename"]["issue_number_padding"]))
self.cbxSmartCleanup.setChecked(self.options["rename"]["use_smart_string_cleanup"])
self.cbxChangeExtension.setChecked(self.options["rename"]["set_extension_based_on_archive"])
self.cbxMoveFiles.setChecked(self.options["rename"]["move_to_dir"])
self.leDirectory.setText(self.options["rename"]["dir"])
self.cbxRenameStrict.setChecked(self.options["rename"]["strict"])
def accept(self) -> None:
self.rename_test()
if self.rename_error is not None:
if isinstance(self.rename_error, ValueError):
logger.exception("Invalid format string: %s", self.settings.rename_template)
logger.exception("Invalid format string: %s", self.options["rename"]["template"])
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
@ -287,7 +289,7 @@ class SettingsWindow(QtWidgets.QDialog):
return
else:
logger.exception(
"Formatter failure: %s metadata: %s", self.settings.rename_template, self.renamer.metadata
"Formatter failure: %s metadata: %s", self.options["rename"]["template"], self.renamer.metadata
)
QtWidgets.QMessageBox.critical(
self,
@ -300,69 +302,77 @@ class SettingsWindow(QtWidgets.QDialog):
)
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
self.options["general"]["rar_exe_path"] = str(self.leRarExePath.text())
# make sure rar program is now in the path for the rar class
if self.settings.rar_exe_path:
utils.add_to_path(os.path.dirname(self.settings.rar_exe_path))
if self.options["general"]["rar_exe_path"]:
utils.add_to_path(os.path.dirname(self.options["general"]["rar_exe_path"]))
if not str(self.leIssueNumPadding.text()).isdigit():
self.leIssueNumPadding.setText("0")
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
self.options["general"]["check_for_new_version"] = self.cbxCheckForNewVersion.isChecked()
self.settings.id_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
self.settings.id_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
self.settings.id_publisher_filter = str(self.tePublisherFilter.toPlainText())
self.options["identifier"]["series_match_identify_thresh"] = self.sbNameMatchIdentifyThresh.value()
self.options["comicvine"]["series_match_search_thresh"] = self.sbNameMatchSearchThresh.value()
self.options["identifier"]["publisher_filter"] = [
x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip()
]
self.settings.complicated_parser = self.cbxComplicatedParser.isChecked()
self.settings.remove_c2c = self.cbxRemoveC2C.isChecked()
self.settings.remove_fcbd = self.cbxRemoveFCBD.isChecked()
self.settings.remove_publisher = self.cbxRemovePublisher.isChecked()
self.options["filename"]["complicated_parser"] = self.cbxComplicatedParser.isChecked()
self.options["filename"]["remove_c2c"] = self.cbxRemoveC2C.isChecked()
self.options["filename"]["remove_fcbd"] = self.cbxRemoveFCBD.isChecked()
self.options["filename"]["remove_publisher"] = self.cbxRemovePublisher.isChecked()
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
self.options["comicvine"]["use_series_start_as_volume"] = self.cbxUseSeriesStartAsVolume.isChecked()
self.options["comicvine"][
"clear_form_before_populating_from_cv"
] = self.cbxClearFormBeforePopulating.isChecked()
self.options["comicvine"]["remove_html_tables"] = self.cbxRemoveHtmlTables.isChecked()
self.settings.always_use_publisher_filter = self.cbxUseFilter.isChecked()
self.settings.sort_series_by_year = self.cbxSortByYear.isChecked()
self.settings.exact_series_matches_first = self.cbxExactMatches.isChecked()
self.options["comicvine"]["always_use_publisher_filter"] = self.cbxUseFilter.isChecked()
self.options["comicvine"]["sort_series_by_year"] = self.cbxSortByYear.isChecked()
self.options["comicvine"]["exact_series_matches_first"] = self.cbxExactMatches.isChecked()
# Ignore empty field
if self.leKey.text().strip():
self.settings.cv_api_key = self.leKey.text().strip()
self.talker_api.api_key = self.settings.cv_api_key
self.options["comicvine"]["cv_api_key"] = self.leKey.text().strip()
self.talker_api.api_key = self.options["comicvine"]["cv_api_key"]
if self.leURL.text().strip():
self.settings.cv_url = self.leURL.text().strip()
self.talker_api.api_url = self.settings.cv_url
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
self.settings.copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.options["comicvine"]["cv_url"] = self.leURL.text().strip()
self.talker_api.api_url = self.options["comicvine"]["cv_url"]
self.settings.rename_template = str(self.leRenameTemplate.text())
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
self.settings.rename_dir = self.leDirectory.text()
self.options["cbl"]["assume_lone_credit_is_primary"] = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.options["cbl"]["copy_characters_to_tags"] = self.cbxCopyCharactersToTags.isChecked()
self.options["cbl"]["copy_teams_to_tags"] = self.cbxCopyTeamsToTags.isChecked()
self.options["cbl"]["copy_locations_to_tags"] = self.cbxCopyLocationsToTags.isChecked()
self.options["cbl"]["copy_storyarcs_to_tags"] = self.cbxCopyStoryArcsToTags.isChecked()
self.options["cbl"]["copy_notes_to_comments"] = self.cbxCopyNotesToComments.isChecked()
self.options["cbl"]["copy_weblink_to_comments"] = self.cbxCopyWebLinkToComments.isChecked()
self.options["cbl"]["apply_cbl_transform_on_cv_import"] = self.cbxApplyCBLTransformOnCVIMport.isChecked()
self.options["cbl"][
"apply_cbl_transform_on_bulk_operation"
] = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.settings.rename_strict = self.cbxRenameStrict.isChecked()
self.options["rename"]["template"] = str(self.leRenameTemplate.text())
self.options["rename"]["issue_number_padding"] = int(self.leIssueNumPadding.text())
self.options["rename"]["use_smart_string_cleanup"] = self.cbxSmartCleanup.isChecked()
self.options["rename"]["set_extension_based_on_archive"] = self.cbxChangeExtension.isChecked()
self.options["rename"]["move_to_dir"] = self.cbxMoveFiles.isChecked()
self.options["rename"]["dir"] = self.leDirectory.text()
self.settings.save()
self.options["rename"]["strict"] = self.cbxRenameStrict.isChecked()
settings.Manager().save_file(self.options, self.options["runtime"]["config"].user_config_dir / "settings.json")
self.parent().options = self.options
QtWidgets.QDialog.accept(self)
def select_rar(self) -> None:
self.select_file(self.leRarExePath, "RAR")
def clear_cache(self) -> None:
ImageFetcher().clear_cache()
ComicCacher(ComicTaggerSettings.get_settings_folder(), version).clear_cache()
ImageFetcher(self.options["runtime"]["config"].cache_folder).clear_cache()
ComicCacher(self.options["runtime"]["config"].cache_folder, version).clear_cache()
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
def test_api_key(self) -> None:
@ -372,7 +382,7 @@ class SettingsWindow(QtWidgets.QDialog):
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
def reset_settings(self) -> None:
self.settings.reset()
self.options = settings.Manager(self.options["option_definitions"]).defaults()
self.settings_to_form()
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")

View File

@ -15,7 +15,6 @@
# limitations under the License.
from __future__ import annotations
import argparse
import json
import logging
import operator
@ -40,7 +39,7 @@ from comicapi.comicinfoxml import ComicInfoXml
from comicapi.filenameparser import FileNameParser
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
from comictaggerlib import ctversion
from comictaggerlib import ctversion, settings
from comictaggerlib.applicationlogwindow import ApplicationLogWindow, QTextEditLogger
from comictaggerlib.autotagmatchwindow import AutoTagMatchWindow
from comictaggerlib.autotagprogresswindow import AutoTagProgressWindow
@ -58,7 +57,6 @@ from comictaggerlib.pagebrowser import PageBrowserWindow
from comictaggerlib.pagelisteditor import PageListEditor
from comictaggerlib.renamewindow import RenameWindow
from comictaggerlib.resulttypes import IssueResult, MultipleMatch, OnlineMatchResults
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.settingswindow import SettingsWindow
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
@ -80,25 +78,26 @@ class TaggerWindow(QtWidgets.QMainWindow):
def __init__(
self,
file_list: list[str],
settings: ComicTaggerSettings,
options: settings.OptionValues,
talker_api: ComicTalker,
parent: QtWidgets.QWidget | None = None,
opts: argparse.Namespace | None = None,
) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "taggerwindow.ui", self)
self.settings = settings
self.options = options
if not options:
self.options = {}
self.talker_api = talker_api
self.log_window = self.setup_logger()
# prevent multiple instances
socket = QtNetwork.QLocalSocket(self)
socket.connectToServer(settings.install_id)
socket.connectToServer(options["internal"]["install_id"])
alive = socket.waitForConnected(3000)
if alive:
logger.setLevel(logging.INFO)
logger.info("Another application with key [%s] is already running", settings.install_id)
logger.info("Another application with key [%s] is already running", options["internal"]["install_id"])
# send file list to other instance
if file_list:
socket.write(pickle.dumps(file_list))
@ -110,20 +109,20 @@ class TaggerWindow(QtWidgets.QMainWindow):
# listen on a socket to prevent multiple instances
self.socketServer = QtNetwork.QLocalServer(self)
self.socketServer.newConnection.connect(self.on_incoming_socket_connection)
ok = self.socketServer.listen(settings.install_id)
ok = self.socketServer.listen(options["internal"]["install_id"])
if not ok:
if self.socketServer.serverError() == QtNetwork.QAbstractSocket.SocketError.AddressInUseError:
self.socketServer.removeServer(settings.install_id)
ok = self.socketServer.listen(settings.install_id)
self.socketServer.removeServer(options["internal"]["install_id"])
ok = self.socketServer.listen(options["internal"]["install_id"])
if not ok:
logger.error(
"Cannot start local socket with key [%s]. Reason: %s",
settings.install_id,
options["internal"]["install_id"],
self.socketServer.errorString(),
)
sys.exit()
self.archiveCoverWidget = CoverImageWidget(self.coverImageContainer, talker_api, CoverImageWidget.ArchiveMode)
self.archiveCoverWidget = CoverImageWidget(self.coverImageContainer, CoverImageWidget.ArchiveMode, None, None)
grid_layout = QtWidgets.QGridLayout(self.coverImageContainer)
grid_layout.addWidget(self.archiveCoverWidget)
grid_layout.setContentsMargins(0, 0, 0, 0)
@ -132,14 +131,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
grid_layout = QtWidgets.QGridLayout(self.tabPages)
grid_layout.addWidget(self.page_list_editor)
self.fileSelectionList = FileSelectionList(self.widgetListHolder, self.settings, self.dirty_flag_verification)
self.fileSelectionList = FileSelectionList(self.widgetListHolder, self.options, self.dirty_flag_verification)
grid_layout = QtWidgets.QGridLayout(self.widgetListHolder)
grid_layout.addWidget(self.fileSelectionList)
self.fileSelectionList.selectionChanged.connect(self.file_list_selection_changed)
self.fileSelectionList.listCleared.connect(self.file_list_cleared)
self.fileSelectionList.set_sorting(
self.settings.last_filelist_sorted_column, QtCore.Qt.SortOrder(self.settings.last_filelist_sorted_order)
self.options["internal"]["last_filelist_sorted_column"],
QtCore.Qt.SortOrder(self.options["internal"]["last_filelist_sorted_order"]),
)
# we can't specify relative font sizes in the UI designer, so
@ -156,14 +156,14 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.scrollAreaWidgetContents.adjustSize()
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
# TODO: this needs to be looked at
if opts is not None and opts.type:
# respect the command line option tag type
settings.last_selected_save_data_style = opts.type[0]
settings.last_selected_load_data_style = opts.type[0]
self.save_data_style = settings.last_selected_save_data_style
self.load_data_style = settings.last_selected_load_data_style
if options["runtime"]["type"] and isinstance(options["runtime"]["type"][0], int):
# respect the command line option tag type
options["internal"]["last_selected_save_data_style"] = options["runtime"]["type"][0]
options["internal"]["last_selected_load_data_style"] = options["runtime"]["type"][0]
self.save_data_style = options["internal"]["last_selected_save_data_style"]
self.load_data_style = options["internal"]["last_selected_load_data_style"]
self.setAcceptDrops(True)
self.config_menus()
@ -228,8 +228,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.show()
self.set_app_position()
if self.settings.last_form_side_width != -1:
self.splitter.setSizes([self.settings.last_form_side_width, self.settings.last_list_side_width])
if self.options["internal"]["last_form_side_width"] != -1:
self.splitter.setSizes(
[self.options["internal"]["last_form_side_width"], self.options["internal"]["last_list_side_width"]]
)
self.raise_()
QtCore.QCoreApplication.processEvents()
self.resizeEvent(None)
@ -246,7 +248,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
if len(file_list) != 0:
self.fileSelectionList.add_path_list(file_list)
if self.settings.show_disclaimer:
if self.options["dialog"]["show_disclaimer"]:
checked = OptionalMessageDialog.msg(
self,
"Welcome!",
@ -261,26 +263,9 @@ use ComicTagger on local copies of your comics.<br><br>
Have fun!
""",
)
self.settings.show_disclaimer = not checked
self.options["dialog"]["show_disclaimer"] = not checked
if self.settings.settings_warning < 4:
checked = OptionalMessageDialog.msg(
self,
"Warning!",
f"""<span style="font-size:15px">
{"&nbsp;"*100}
The next release will save settings in a different format
<span style="font-weight: bold;font-size:19px">no settings will be transfered</span> to the new version.<br/>
See <a href="https://github.com/comictagger/comictagger/releases/1.5.5">https://github.com/comictagger/comictagger/releases/1.5.5</a>
for more information
<br/><br/>
You have {4-self.settings.settings_warning} warnings left.
</span>""",
)
if checked:
self.settings.settings_warning += 1
if self.settings.check_for_new_version:
if self.options["general"]["check_for_new_version"]:
self.check_latest_version_online()
def open_file_event(self, url: QtCore.QUrl) -> None:
@ -293,7 +278,7 @@ You have {4-self.settings.settings_warning} warnings left.
def setup_logger(self) -> ApplicationLogWindow:
try:
current_logs = (ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log").read_text("utf-8")
current_logs = (self.options["runtime"]["config"].user_log_dir / "ComicTagger.log").read_text("utf-8")
except Exception:
current_logs = ""
root_logger = logging.getLogger()
@ -494,7 +479,6 @@ You have {4-self.settings.settings_warning} warnings left.
if non_zip_count != 0:
EW = ExportWindow(
self,
self.settings,
(
f"You have selected {non_zip_count} archive(s) to export to Zip format. "
""" New archives will be created in the same folder as the original.
@ -627,10 +611,10 @@ You have {4-self.settings.settings_warning} warnings left.
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.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options["filename"]["complicated_parser"],
self.options["filename"]["remove_c2c"],
self.options["filename"]["remove_fcbd"],
self.options["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())
@ -998,10 +982,10 @@ You have {4-self.settings.settings_warning} warnings left.
# copy the form onto metadata object
self.form_to_metadata()
new_metadata = self.comic_archive.metadata_from_filename(
self.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options["filename"]["complicated_parser"],
self.options["filename"]["remove_c2c"],
self.options["filename"]["remove_fcbd"],
self.options["filename"]["remove_publisher"],
split_words,
)
if new_metadata is not None:
@ -1022,8 +1006,8 @@ You have {4-self.settings.settings_warning} warnings left.
else:
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles)
if self.settings.last_opened_folder is not None:
dialog.setDirectory(self.settings.last_opened_folder)
if self.options["internal"]["last_opened_folder"] is not None:
dialog.setDirectory(self.options["internal"]["last_opened_folder"])
if not folder_mode:
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar *.cb7 *.7z)"
@ -1073,7 +1057,7 @@ You have {4-self.settings.settings_warning} warnings left.
issue_count,
cover_index_list,
self.comic_archive,
self.settings,
self.options,
self.talker_api,
autoselect,
literal,
@ -1092,16 +1076,9 @@ You have {4-self.settings.settings_warning} warnings left.
self.form_to_metadata()
try:
if selector.issue_id:
new_metadata = self.talker_api.fetch_comic_data(selector.issue_id)
elif selector.volume_id and selector.issue_number:
# Would this ever be needed?
new_metadata = self.talker_api.fetch_comic_data(
series_id=selector.volume_id, issue_number=selector.issue_number
)
else:
# Only left with series? Isn't series only handled elsewhere?
new_metadata = self.talker_api.fetch_comic_data(series_id=selector.volume_id)
new_metadata = self.talker_api.fetch_comic_data(
issue_id=selector.issue_id or 0, series_id=selector.volume_id, issue_number=selector.issue_number
)
except TalkerError as e:
QtWidgets.QApplication.restoreOverrideCursor()
QtWidgets.QMessageBox.critical(
@ -1112,10 +1089,10 @@ You have {4-self.settings.settings_warning} warnings left.
else:
QtWidgets.QApplication.restoreOverrideCursor()
if new_metadata is not None:
if self.settings.apply_cbl_transform_on_cv_import:
new_metadata = CBLTransformer(new_metadata, self.settings).apply()
if self.options["cbl"]["apply_cbl_transform_on_cv_import"]:
new_metadata = CBLTransformer(new_metadata, self.options).apply()
if self.settings.clear_form_before_populating_from_cv:
if self.options["comicvine"]["clear_form_before_populating_from_cv"]:
self.clear_form()
notes = (
@ -1170,7 +1147,7 @@ You have {4-self.settings.settings_warning} warnings left.
"Change Tag Read Style", "If you change read tag style now, data in the form will be lost. Are you sure?"
):
self.load_data_style = self.cbLoadDataStyle.itemData(s)
self.settings.last_selected_load_data_style = self.load_data_style
self.options["internal"]["last_selected_load_data_style"] = self.load_data_style
self.update_menus()
if self.comic_archive is not None:
self.load_archive(self.comic_archive)
@ -1181,7 +1158,7 @@ You have {4-self.settings.settings_warning} warnings left.
def set_save_data_style(self, s: int) -> None:
self.save_data_style = self.cbSaveDataStyle.itemData(s)
self.settings.last_selected_save_data_style = self.save_data_style
self.options["internal"]["last_selected_save_data_style"] = self.save_data_style
self.update_style_tweaks()
self.update_menus()
@ -1400,15 +1377,17 @@ You have {4-self.settings.settings_warning} warnings left.
def show_settings(self) -> None:
settingswin = SettingsWindow(self, self.settings, self.talker_api)
settingswin = SettingsWindow(self, self.options, self.talker_api)
settingswin.setModal(True)
settingswin.exec()
settingswin.result()
def set_app_position(self) -> None:
if self.settings.last_main_window_width != 0:
self.move(self.settings.last_main_window_x, self.settings.last_main_window_y)
self.resize(self.settings.last_main_window_width, self.settings.last_main_window_height)
if self.options["internal"]["last_main_window_width"] != 0:
self.move(self.options["internal"]["last_main_window_x"], self.options["internal"]["last_main_window_y"])
self.resize(
self.options["internal"]["last_main_window_width"], self.options["internal"]["last_main_window_height"]
)
else:
screen = QtGui.QGuiApplication.primaryScreen().geometry()
size = self.frameGeometry()
@ -1675,8 +1654,11 @@ You have {4-self.settings.settings_warning} warnings left.
if ca.has_metadata(src_style) and ca.is_writable():
md = ca.read_metadata(src_style)
if dest_style == MetaDataStyle.CBI and self.settings.apply_cbl_transform_on_bulk_operation:
md = CBLTransformer(md, self.settings).apply()
if (
dest_style == MetaDataStyle.CBI
and self.options["cbl"]["apply_cbl_transform_on_bulk_operation"]
):
md = CBLTransformer(md, self.options).apply()
if not ca.write_metadata(md, dest_style):
failed_list.append(ca.path)
@ -1710,12 +1692,12 @@ You have {4-self.settings.settings_warning} warnings left.
try:
ct_md = self.talker_api.fetch_comic_data(match["issue_id"])
except TalkerError as e:
logger.exception(f"Save aborted.\n{e}")
except TalkerError:
logger.exception("Save aborted.")
if not ct_md.is_empty:
if self.settings.apply_cbl_transform_on_cv_import:
ct_md = CBLTransformer(ct_md, self.settings).apply()
if self.options["cbl"]["apply_cbl_transform_on_cv_import"]:
ct_md = CBLTransformer(ct_md, self.options).apply()
QtWidgets.QApplication.restoreOverrideCursor()
@ -1734,7 +1716,7 @@ You have {4-self.settings.settings_warning} warnings left.
self, ca: ComicArchive, match_results: OnlineMatchResults, dlg: AutoTagStartWindow
) -> tuple[bool, OnlineMatchResults]:
success = False
ii = IssueIdentifier(ca, self.settings, self.talker_api)
ii = IssueIdentifier(ca, self.options, self.talker_api)
# read in metadata, and parse file name if not there
try:
@ -1744,10 +1726,10 @@ You have {4-self.settings.settings_warning} warnings left.
logger.error("Failed to load metadata for %s: %s", ca.path, e)
if md.is_empty:
md = ca.metadata_from_filename(
self.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options["filename"]["complicated_parser"],
self.options["filename"]["remove_c2c"],
self.options["filename"]["remove_fcbd"],
self.options["filename"]["remove_publisher"],
dlg.split_words,
)
if dlg.ignore_leading_digits_in_filename and md.series is not None:
@ -1833,7 +1815,7 @@ You have {4-self.settings.settings_warning} warnings left.
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
if self.settings.auto_imprint:
if self.options["comicvine"]["auto_imprint"]:
md.fix_publisher()
if not ca.write_metadata(md, self.save_data_style):
@ -1862,7 +1844,7 @@ You have {4-self.settings.settings_warning} warnings left.
atstartdlg = AutoTagStartWindow(
self,
self.settings,
self.options,
(
f"You have selected {len(ca_list)} archive(s) to automatically identify and write {MetaDataStyle.name[style]} tags to."
"\n\nPlease choose options below, and select OK to Auto-Tag."
@ -1965,7 +1947,7 @@ You have {4-self.settings.settings_warning} warnings left.
match_results.multiple_matches,
style,
self.actual_issue_data_fetch,
self.settings,
self.options,
self.talker_api,
)
matchdlg.setModal(True)
@ -2011,17 +1993,19 @@ You have {4-self.settings.settings_warning} warnings left.
f"Exit {self.appName}", "If you quit now, data in the form will be lost. Are you sure?"
):
appsize = self.size()
self.settings.last_main_window_width = appsize.width()
self.settings.last_main_window_height = appsize.height()
self.settings.last_main_window_x = self.x()
self.settings.last_main_window_y = self.y()
self.settings.last_form_side_width = self.splitter.sizes()[0]
self.settings.last_list_side_width = self.splitter.sizes()[1]
self.options["internal"]["last_main_window_width"] = appsize.width()
self.options["internal"]["last_main_window_height"] = appsize.height()
self.options["internal"]["last_main_window_x"] = self.x()
self.options["internal"]["last_main_window_y"] = self.y()
self.options["internal"]["last_form_side_width"] = self.splitter.sizes()[0]
self.options["internal"]["last_list_side_width"] = self.splitter.sizes()[1]
(
self.settings.last_filelist_sorted_column,
self.settings.last_filelist_sorted_order,
self.options["internal"]["last_filelist_sorted_column"],
self.options["internal"]["last_filelist_sorted_order"],
) = self.fileSelectionList.get_sorting()
self.settings.save()
settings.Manager().save_file(
self.options, self.options["runtime"]["config"].user_config_dir / "settings.json"
)
event.accept()
else:
@ -2070,7 +2054,7 @@ You have {4-self.settings.settings_warning} warnings left.
def apply_cbl_transform(self) -> None:
self.form_to_metadata()
self.metadata = CBLTransformer(self.metadata, self.settings).apply()
self.metadata = CBLTransformer(self.metadata, self.options).apply()
self.metadata_to_form()
def recalc_page_dimensions(self) -> None:
@ -2096,7 +2080,7 @@ You have {4-self.settings.settings_warning} warnings left.
"File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?"
):
dlg = RenameWindow(self, ca_list, self.load_data_style, self.settings, self.talker_api)
dlg = RenameWindow(self, ca_list, self.load_data_style, self.options, self.talker_api)
dlg.setModal(True)
if dlg.exec() and self.comic_archive is not None:
self.fileSelectionList.update_selected_rows()
@ -2115,7 +2099,7 @@ You have {4-self.settings.settings_warning} warnings left.
QtCore.QTimer.singleShot(1, self.fileSelectionList.revert_selection)
return
self.settings.last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
self.options["internal"]["last_opened_folder"] = os.path.abspath(os.path.split(comic_archive.path)[0])
self.comic_archive = comic_archive
try:
self.metadata = self.comic_archive.read_metadata(self.load_data_style)
@ -2147,11 +2131,13 @@ You have {4-self.settings.settings_warning} warnings left.
def check_latest_version_online(self) -> None:
version_checker = VersionChecker()
self.version_check_complete(
version_checker.get_latest_version(self.settings.install_id, self.settings.send_usage_stats)
version_checker.get_latest_version(
self.options["internal"]["install_id"], self.options["general"]["send_usage_stats"]
)
)
def version_check_complete(self, new_version: tuple[str, str]) -> None:
if new_version[0] not in (self.version, self.settings.dont_notify_about_this_version):
if new_version[0] not in (self.version, self.options["dialog"]["dont_notify_about_this_version"]):
website = "https://github.com/comictagger/comictagger"
checked = OptionalMessageDialog.msg(
self,
@ -2162,7 +2148,7 @@ You have {4-self.settings.settings_warning} warnings left.
"Don't tell me about this version again",
)
if checked:
self.settings.dont_notify_about_this_version = new_version[0]
self.options["dialog"]["dont_notify_about_this_version"] = new_version[0]
def on_incoming_socket_connection(self) -> None:
# Accept connection from other instance.

View File

@ -25,12 +25,12 @@ from PyQt5.QtCore import pyqtSignal
from comicapi import utils
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import settings
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.issueselectionwindow import IssueSelectionWindow
from comictaggerlib.matchselectionwindow import MatchSelectionWindow
from comictaggerlib.progresswindow import IDProgressWindow
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import reduce_widget_font_size
from comictalker.resulttypes import ComicVolume
@ -111,7 +111,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
issue_count: int,
cover_index_list: list[int],
comic_archive: ComicArchive | None,
settings: ComicTaggerSettings,
options: settings.OptionValues,
talker_api: ComicTalker,
autoselect: bool = False,
literal: bool = False,
@ -120,7 +120,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
uic.loadUi(ui_path / "volumeselectionwindow.ui", self)
self.imageWidget = CoverImageWidget(self.imageContainer, talker_api, CoverImageWidget.URLMode)
self.imageWidget = CoverImageWidget(
self.imageContainer, CoverImageWidget.URLMode, options["runtime"]["config"].user_cache_dir, talker_api
)
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
gridlayout.addWidget(self.imageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -136,7 +138,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
)
)
self.settings = settings
self.options = options
self.series_name = series_name
self.issue_number = issue_number
self.issue_id: int | None = None
@ -154,7 +156,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.progdialog: QtWidgets.QProgressDialog | None = None
self.search_thread: SearchThread | None = None
self.use_filter = self.settings.always_use_publisher_filter
self.use_filter = self.options["comicvine"]["always_use_publisher_filter"]
# Load to retrieve settings
self.talker_api = talker_api
@ -205,7 +207,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.iddialog.rejected.connect(self.identify_cancel)
self.iddialog.show()
self.ii = IssueIdentifier(self.comic_archive, self.settings, self.talker_api)
self.ii = IssueIdentifier(self.comic_archive, self.options, self.talker_api)
md = GenericMetadata()
md.series = self.series_name
@ -278,7 +280,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
choices = True
if choices:
selector = MatchSelectionWindow(self, matches, self.comic_archive, self.talker_api)
selector = MatchSelectionWindow(
self, matches, self.comic_archive, talker_api=self.talker_api, options=self.options
)
selector.setModal(True)
selector.exec()
if selector.result():
@ -294,7 +298,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.show_issues()
def show_issues(self) -> None:
selector = IssueSelectionWindow(self, self.settings, self.talker_api, self.volume_id, self.issue_number)
selector = IssueSelectionWindow(self, self.options, self.talker_api, self.volume_id, self.issue_number)
title = ""
for record in self.ct_search_results:
if record["id"] == self.volume_id:
@ -322,7 +326,11 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
def perform_query(self, refresh: bool = False) -> None:
self.search_thread = SearchThread(
self.talker_api, self.series_name, refresh, self.literal, self.settings.id_series_match_search_thresh
self.talker_api,
self.series_name,
refresh,
self.literal,
self.options["comicvine"]["series_match_search_thresh"],
)
self.search_thread.searchComplete.connect(self.search_complete)
self.search_thread.progressUpdate.connect(self.search_progress_update)
@ -360,7 +368,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
def search_complete(self) -> None:
if self.progdialog is not None:
self.progdialog.accept()
self.progdialog = None
del self.progdialog
if self.search_thread is not None and self.search_thread.ct_error:
# TODO Currently still opens the window
QtWidgets.QMessageBox.critical(
@ -374,7 +382,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
# filter the publishers if enabled set
if self.use_filter:
try:
publisher_filter = {s.strip().casefold() for s in self.settings.id_publisher_filter.split(",")}
publisher_filter = {s.strip().casefold() for s in self.options["identifier"]["publisher_filter"]}
# use '' as publisher name if None
self.ct_search_results = list(
filter(
@ -390,7 +398,7 @@ class VolumeSelectionWindow(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.settings.sort_series_by_year:
if self.options["comicvine"]["sort_series_by_year"]:
try:
self.ct_search_results = sorted(
self.ct_search_results,
@ -408,7 +416,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
logger.exception("bad data error sorting results by count_of_issues")
# move sanitized matches to the front
if self.settings.exact_series_matches_first:
if self.options["comicvine"]["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

@ -1,3 +1,4 @@
appdirs==1.4.4
beautifulsoup4>=4.1
importlib_metadata>=3.3.0
natsort>=8.1.0

View File

@ -14,7 +14,7 @@ import os
from setuptools import setup
def read(fname):
def read(fname: str) -> str:
"""
Read the contents of a file.
Parameters

View File

@ -7,8 +7,8 @@ from testing.comicdata import search_results
def test_create_cache(settings, mock_version):
comictalker.comiccacher.ComicCacher(settings.get_settings_folder(), mock_version[0])
assert (settings.get_settings_folder() / "settings").exists()
comictalker.comiccacher.ComicCacher(settings["runtime"]["config"].user_cache_dir, mock_version[0])
assert (settings["runtime"]["config"].user_cache_dir).exists()
def test_search_results(comic_cache):

View File

@ -17,6 +17,7 @@ import comictaggerlib.settings
import comictalker.comiccacher
import comictalker.talkers.comicvine
from comicapi import utils
from comictaggerlib import settings as ctsettings
from testing import comicvine, filenames
from testing.comicdata import all_seed_imprints, seed_imprints
@ -116,7 +117,7 @@ def comicvine_api(
cv = comictalker.talkers.comicvine.ComicVineTalker(
version=mock_version[0],
cache_folder=settings.get_settings_folder(),
cache_folder=settings["runtime"]["config"].user_cache_dir,
api_url="",
api_key="",
series_match_thresh=90,
@ -163,9 +164,20 @@ def seed_all_publishers(monkeypatch):
@pytest.fixture
def settings(tmp_path):
yield comictaggerlib.settings.ComicTaggerSettings(tmp_path / "settings")
manager = ctsettings.Manager()
ctsettings.register_commandline(manager)
ctsettings.register_settings(manager)
defaults = manager.defaults()
defaults["runtime"]["config"] = ctsettings.ComicTaggerPaths(tmp_path / "config")
defaults["runtime"]["config"].user_data_dir.mkdir(parents=True, exist_ok=True)
defaults["runtime"]["config"].user_config_dir.mkdir(parents=True, exist_ok=True)
defaults["runtime"]["config"].user_cache_dir.mkdir(parents=True, exist_ok=True)
defaults["runtime"]["config"].user_state_dir.mkdir(parents=True, exist_ok=True)
defaults["runtime"]["config"].user_log_dir.mkdir(parents=True, exist_ok=True)
yield defaults
@pytest.fixture
def comic_cache(settings, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]:
yield comictalker.comiccacher.ComicCacher(settings.get_settings_folder(), mock_version[0])
yield comictalker.comiccacher.ComicCacher(settings["runtime"]["config"].user_cache_dir, mock_version[0])