diff --git a/comicapi/utils.py b/comicapi/utils.py
index 42cf19f..497e336 100644
--- a/comicapi/utils.py
+++ b/comicapi/utils.py
@@ -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
diff --git a/comictagger.py b/comictagger.py
index 947f5a4..17c515d 100755
--- a/comictagger.py
+++ b/comictagger.py
@@ -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()
diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py
index 516dc04..4baf51d 100644
--- a/comictaggerlib/autotagmatchwindow.py
+++ b/comictaggerlib/autotagmatchwindow.py
@@ -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
diff --git a/comictaggerlib/autotagprogresswindow.py b/comictaggerlib/autotagprogresswindow.py
index 477381b..b8362d1 100644
--- a/comictaggerlib/autotagprogresswindow.py
+++ b/comictaggerlib/autotagprogresswindow.py
@@ -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)
diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py
index bae9f19..4ee1c5f 100644
--- a/comictaggerlib/autotagstartwindow.py
+++ b/comictaggerlib/autotagstartwindow.py
@@ -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 = """The Name Match Ratio Threshold: Auto-Identify 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()
diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py
index 34194c3..c6d9726 100644
--- a/comictaggerlib/cbltransformer.py
+++ b/comictaggerlib/cbltransformer.py
@@ -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 = ""
diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py
index 3a13e2c..f2424fe 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -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!"
diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py
index ecd19a4..c7c6a51 100644
--- a/comictaggerlib/coverimagewidget.py
+++ b/comictaggerlib/coverimagewidget.py
@@ -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])
diff --git a/comictaggerlib/exportwindow.py b/comictaggerlib/exportwindow.py
index e962bbd..a3f8f73 100644
--- a/comictaggerlib/exportwindow.py
+++ b/comictaggerlib/exportwindow.py
@@ -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)
diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py
index f883d38..7eb0116 100644
--- a/comictaggerlib/fileselectionlist.py
+++ b/comictaggerlib/fileselectionlist.py
@@ -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()
diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py
new file mode 100644
index 0000000..1212744
--- /dev/null
+++ b/comictaggerlib/gui.py
@@ -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()
+ )
diff --git a/comictaggerlib/imagefetcher.py b/comictaggerlib/imagefetcher.py
index 214f2b8..4f07c4f 100644
--- a/comictaggerlib/imagefetcher.py
+++ b/comictaggerlib/imagefetcher.py
@@ -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 = ""
diff --git a/comictaggerlib/imagehasher.py b/comictaggerlib/imagehasher.py
old mode 100755
new mode 100644
diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py
index 3ed87c3..7b30fb9 100644
--- a/comictaggerlib/issueidentifier.py
+++ b/comictaggerlib/issueidentifier.py
@@ -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
diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py
index e1b6e0c..0c5694d 100644
--- a/comictaggerlib/issueselectionwindow.py
+++ b/comictaggerlib/issueselectionwindow.py
@@ -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] = []
diff --git a/comictaggerlib/log.py b/comictaggerlib/log.py
new file mode 100644
index 0000000..e022f67
--- /dev/null
+++ b/comictaggerlib/log.py
@@ -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",
+ )
diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py
old mode 100755
new mode 100644
index 5bd46a2..7c0ea9a
--- a/comictaggerlib/main.py
+++ b/comictaggerlib/main.py
@@ -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)
diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py
index 330318c..588fd2c 100644
--- a/comictaggerlib/matchselectionwindow.py
+++ b/comictaggerlib/matchselectionwindow.py
@@ -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)
diff --git a/comictaggerlib/options.py b/comictaggerlib/options.py
deleted file mode 100644
index be9aecf..0000000
--- a/comictaggerlib/options.py
+++ /dev/null
@@ -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
diff --git a/comictaggerlib/pagebrowser.py b/comictaggerlib/pagebrowser.py
index f2e2387..79e7968 100644
--- a/comictaggerlib/pagebrowser.py
+++ b/comictaggerlib/pagebrowser.py
@@ -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)
diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py
index 39b71a9..136cb39 100644
--- a/comictaggerlib/pagelisteditor.py
+++ b/comictaggerlib/pagelisteditor.py
@@ -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)
diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py
index b732579..f231618 100644
--- a/comictaggerlib/renamewindow.py
+++ b/comictaggerlib/renamewindow.py
@@ -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]
diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py
deleted file mode 100644
index ac17af0..0000000
--- a/comictaggerlib/settings.py
+++ /dev/null
@@ -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)
diff --git a/comictaggerlib/settings/__init__.py b/comictaggerlib/settings/__init__.py
new file mode 100644
index 0000000..5219e80
--- /dev/null
+++ b/comictaggerlib/settings/__init__.py
@@ -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",
+]
diff --git a/comictaggerlib/settings/cmdline.py b/comictaggerlib/settings/cmdline.py
new file mode 100644
index 0000000..d84f403
--- /dev/null
+++ b/comictaggerlib/settings/cmdline.py
@@ -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
diff --git a/comictaggerlib/settings/file.py b/comictaggerlib/settings/file.py
new file mode 100644
index 0000000..36931f5
--- /dev/null
+++ b/comictaggerlib/settings/file.py
@@ -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)
diff --git a/comictaggerlib/settings/manager.py b/comictaggerlib/settings/manager.py
new file mode 100644
index 0000000..5dfb1e9
--- /dev/null
+++ b/comictaggerlib/settings/manager.py
@@ -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)
diff --git a/comictaggerlib/settings/types.py b/comictaggerlib/settings/types.py
new file mode 100644
index 0000000..774fbdb
--- /dev/null
+++ b/comictaggerlib/settings/types.py
@@ -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
diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py
index 6152e26..b786869 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -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.")
diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py
index 38d2e91..fb54d41 100644
--- a/comictaggerlib/taggerwindow.py
+++ b/comictaggerlib/taggerwindow.py
@@ -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.
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"""
-{" "*100}
-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
-
-You have {4-self.settings.settings_warning} warnings left.
-""",
- )
- 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.
diff --git a/comictaggerlib/volumeselectionwindow.py b/comictaggerlib/volumeselectionwindow.py
index a7f2e60..03ff093 100644
--- a/comictaggerlib/volumeselectionwindow.py
+++ b/comictaggerlib/volumeselectionwindow.py
@@ -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()
diff --git a/requirements.txt b/requirements.txt
index 844c274..972345f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
+appdirs==1.4.4
beautifulsoup4>=4.1
importlib_metadata>=3.3.0
natsort>=8.1.0
diff --git a/setup.py b/setup.py
index 3da2031..a2b00ab 100644
--- a/setup.py
+++ b/setup.py
@@ -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
diff --git a/tests/comiccacher_test.py b/tests/comiccacher_test.py
index 7eeb314..547f699 100644
--- a/tests/comiccacher_test.py
+++ b/tests/comiccacher_test.py
@@ -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):
diff --git a/tests/conftest.py b/tests/conftest.py
index 2eaa39c..a15df4a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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])