diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d3992ce..fea6f61 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -31,7 +31,7 @@ repos:
rev: v1.7.7
hooks:
- id: autoflake
- args: [-i]
+ args: [-i, --remove-all-unused-imports, --ignore-init-module-imports]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
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..ea7aaf3 100644
--- a/comictaggerlib/autotagmatchwindow.py
+++ b/comictaggerlib/autotagmatchwindow.py
@@ -19,13 +19,13 @@ import logging
import os
from typing import Callable
+import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
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,25 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
match_set_list: list[MultipleMatch],
style: int,
fetch_func: Callable[[IssueResult], GenericMetadata],
- settings: ComicTaggerSettings,
+ options: settngs.Namespace,
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 +242,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..b0ff6b2 100644
--- a/comictaggerlib/autotagstartwindow.py
+++ b/comictaggerlib/autotagstartwindow.py
@@ -17,16 +17,16 @@ from __future__ import annotations
import logging
+import settngs
from PyQt5 import QtCore, QtWidgets, uic
-from comictaggerlib.settings import ComicTaggerSettings
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: settngs.Namespace, 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..16d5c6f 100644
--- a/comictaggerlib/cbltransformer.py
+++ b/comictaggerlib/cbltransformer.py
@@ -17,16 +17,17 @@ from __future__ import annotations
import logging
+import settngs
+
from comicapi.genericmetadata import CreditMetadata, GenericMetadata
-from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
class CBLTransformer:
- def __init__(self, metadata: GenericMetadata, settings: ComicTaggerSettings) -> None:
+ def __init__(self, metadata: GenericMetadata, options: settngs.Namespace) -> None:
self.metadata = metadata
- self.settings = settings
+ self.options = options
def apply(self) -> GenericMetadata:
# helper funcs
@@ -40,7 +41,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 +68,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 +89,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..8229dee 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -16,15 +16,15 @@
# limitations under the License.
from __future__ import annotations
-import argparse
import json
import logging
import os
-import pathlib
import sys
from datetime import datetime
from pprint import pprint
+import settngs
+
from comicapi import utils
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
@@ -34,229 +34,187 @@ 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:
- # now get the particular issue data
- try:
- ct_md = talker_api.fetch_comic_data(issue_id)
- except TalkerError as e:
- logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
- return GenericMetadata()
+class CLI:
+ def __init__(self, options: settngs.Values, talker_api: ComicTalker):
+ self.options = options
+ self.talker_api = talker_api
+ self.batch_mode = False
- if settings.apply_cbl_transform_on_cv_import:
- ct_md = CBLTransformer(ct_md, settings).apply()
+ def actual_issue_data_fetch(self, issue_id: int) -> GenericMetadata:
+ # now get the particular issue data
+ try:
+ ct_md = self.talker_api.fetch_comic_data(issue_id)
+ except TalkerError as e:
+ logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
+ return GenericMetadata()
- return ct_md
+ if self.options.cbl_apply_transform_on_import:
+ ct_md = CBLTransformer(ct_md, self.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:
- # 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])
- return False
+ def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool:
+ if not self.options.runtime_dryrun:
+ for metadata_style in self.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])
+ return False
- print("Save complete.")
- logger.info("Save complete.")
- else:
- if opts.terse:
- logger.info("dry-run option was set, so nothing was written")
- print("dry-run option was set, so nothing was written")
+ print("Save complete.")
+ logger.info("Save complete.")
else:
- logger.info("dry-run option was set, so nothing was written, but here is the final set of tags:")
- print("dry-run option was set, so nothing was written, but here is the final set of tags:")
- print(f"{md}")
- return True
-
-
-def display_match_set_for_choice(
- label: str,
- match_set: MultipleMatch,
- opts: argparse.Namespace,
- settings: ComicTaggerSettings,
- talker_api: ComicTalker,
-) -> None:
- print(f"{match_set.ca.path} -- {label}:")
-
- # sort match list by year
- match_set.matches.sort(key=lambda k: k["year"] or 0)
-
- for (counter, m) in enumerate(match_set.matches):
- counter += 1
- print(
- " {}. {} #{} [{}] ({}/{}) - {}".format(
- counter,
- m["series"],
- m["issue_number"],
- m["publisher"],
- m["month"],
- m["year"],
- m["issue_title"],
- )
- )
- if opts.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":
- break
- if i != "s":
- # 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 = ct_md
+ if self.options.runtime_quiet:
+ logger.info("dry-run option was set, so nothing was written")
+ print("dry-run option was set, so nothing was written")
else:
- notes = (
- f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on"
- f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {ct_md.issue_id}]"
+ logger.info("dry-run option was set, so nothing was written, but here is the final set of tags:")
+ print("dry-run option was set, so nothing was written, but here is the final set of tags:")
+ print(f"{md}")
+ return True
+
+ def display_match_set_for_choice(self, label: str, match_set: MultipleMatch) -> None:
+ print(f"{match_set.ca.path} -- {label}:")
+
+ # sort match list by year
+ match_set.matches.sort(key=lambda k: k["year"] or 0)
+
+ for (counter, m) in enumerate(match_set.matches):
+ counter += 1
+ print(
+ " {}. {} #{} [{}] ({}/{}) - {}".format(
+ counter,
+ m["series"],
+ m["issue_number"],
+ m["publisher"],
+ m["month"],
+ m["year"],
+ m["issue_title"],
)
- md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
+ )
+ if self.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":
+ break
+ if i != "s":
+ # save the data!
+ # we know at this point, that the file is all good to go
+ ca = match_set.ca
+ md = self.create_local_metadata(ca)
+ ct_md = self.actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"])
+ if self.options.comicvine_clear_metadata_on_import:
+ md = ct_md
+ else:
+ notes = (
+ f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on"
+ f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {ct_md.issue_id}]"
+ )
+ md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
- if opts.auto_imprint:
- md.fix_publisher()
+ if self.options.comicvine_auto_imprint:
+ md.fix_publisher()
- actual_metadata_save(ca, opts, md)
+ self.actual_metadata_save(ca, md)
+ def post_process_matches(self, match_results: OnlineMatchResults) -> None:
+ # now go through the match results
+ if self.options.runtime_summary:
+ if len(match_results.good_matches) > 0:
+ print("\nSuccessful matches:\n------------------")
+ for f in match_results.good_matches:
+ print(f)
-def post_process_matches(
- match_results: OnlineMatchResults, opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker
-) -> None:
- # now go through the match results
- if opts.show_save_summary:
- if len(match_results.good_matches) > 0:
- print("\nSuccessful matches:\n------------------")
- for f in match_results.good_matches:
- print(f)
+ if len(match_results.no_matches) > 0:
+ print("\nNo matches:\n------------------")
+ for f in match_results.no_matches:
+ print(f)
- if len(match_results.no_matches) > 0:
- print("\nNo matches:\n------------------")
- for f in match_results.no_matches:
- print(f)
+ if len(match_results.write_failures) > 0:
+ print("\nFile Write Failures:\n------------------")
+ for f in match_results.write_failures:
+ print(f)
- if len(match_results.write_failures) > 0:
- print("\nFile Write Failures:\n------------------")
- for f in match_results.write_failures:
- print(f)
+ if len(match_results.fetch_data_failures) > 0:
+ print("\nNetwork Data Fetch Failures:\n------------------")
+ for f in match_results.fetch_data_failures:
+ print(f)
- if len(match_results.fetch_data_failures) > 0:
- print("\nNetwork Data Fetch Failures:\n------------------")
- for f in match_results.fetch_data_failures:
- print(f)
+ if not self.options.runtime_summary and not self.options.runtime_interactive:
+ # just quit if we're not interactive or showing the summary
+ return
- if not opts.show_save_summary and not opts.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:
+ self.display_match_set_for_choice("Multiple high-confidence matches", match_set)
- 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)
+ if len(match_results.low_confidence_matches) > 0:
+ print("\nArchives with low-confidence matches:\n------------------")
+ for match_set in match_results.low_confidence_matches:
+ if len(match_set.matches) == 1:
+ label = "Single low-confidence match"
+ else:
+ label = "Multiple low-confidence matches"
- if len(match_results.low_confidence_matches) > 0:
- print("\nArchives with low-confidence matches:\n------------------")
- for match_set in match_results.low_confidence_matches:
- if len(match_set.matches) == 1:
- label = "Single low-confidence match"
- else:
- label = "Multiple low-confidence matches"
+ self.display_match_set_for_choice(label, match_set)
- display_match_set_for_choice(label, match_set, opts, settings, talker_api)
+ def run(self) -> None:
+ if len(self.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()
+ self.batch_mode = len(self.options.runtime_file_list) > 1
-def cli_mode(opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
- if len(opts.file_list) < 1:
- logger.error("You must specify at least one filename. Use the -h option for more info")
- return
+ for f in self.options.runtime_file_list:
+ self.process_file_cli(f, match_results)
+ sys.stdout.flush()
- match_results = OnlineMatchResults()
+ self.post_process_matches(match_results)
- for f in opts.file_list:
- process_file_cli(f, opts, settings, talker_api, match_results)
- sys.stdout.flush()
+ def create_local_metadata(self, ca: ComicArchive) -> GenericMetadata:
+ md = GenericMetadata()
+ md.set_default_page_list(ca.get_number_of_pages())
- post_process_matches(match_results, opts, settings, talker_api)
+ # now, overlay the parsed filename info
+ if self.options.runtime_parse_filename:
+ f_md = ca.metadata_from_filename(
+ self.options.filename_complicated_parser,
+ self.options.filename_remove_c2c,
+ self.options.filename_remove_fcbd,
+ self.options.filename_remove_publisher,
+ self.options.runtime_split_words,
+ )
+ md.overlay(f_md)
-def create_local_metadata(opts: argparse.Namespace, ca: ComicArchive, settings: ComicTaggerSettings) -> GenericMetadata:
- md = GenericMetadata()
- md.set_default_page_list(ca.get_number_of_pages())
+ for metadata_style in self.options.runtime_type:
+ if ca.has_metadata(metadata_style):
+ try:
+ t_md = ca.read_metadata(metadata_style)
+ md.overlay(t_md)
+ break
+ except Exception as e:
+ logger.error("Failed to load metadata for %s: %s", ca.path, e)
- # now, overlay the parsed filename info
- if opts.parse_filename:
- f_md = ca.metadata_from_filename(
- settings.complicated_parser,
- settings.remove_c2c,
- settings.remove_fcbd,
- settings.remove_publisher,
- opts.split_words,
- )
+ # finally, use explicit stuff
+ md.overlay(self.options.runtime_metadata)
- md.overlay(f_md)
+ return md
- for metadata_style in opts.type:
- if ca.has_metadata(metadata_style):
- try:
- t_md = ca.read_metadata(metadata_style)
- md.overlay(t_md)
- break
- except Exception as e:
- logger.error("Failed to load metadata for %s: %s", ca.path, e)
-
- # finally, use explicit stuff
- md.overlay(opts.metadata)
-
- return md
-
-
-def process_file_cli(
- filename: str,
- opts: argparse.Namespace,
- settings: ComicTaggerSettings,
- talker_api: ComicTalker,
- match_results: OnlineMatchResults,
-) -> None:
- batch_mode = len(opts.file_list) > 1
-
- ca = ComicArchive(filename, settings.rar_exe_path, str(graphics_path / "nocover.png"))
-
- if not os.path.lexists(filename):
- logger.error("Cannot find %s", filename)
- return
-
- if not ca.seems_to_be_a_comic_archive():
- 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):
- logger.error("This archive is not writable")
- return
-
- has = [False, False, False]
- if ca.has_cix():
- has[MetaDataStyle.CIX] = True
- if ca.has_cbi():
- has[MetaDataStyle.CBI] = True
- if ca.has_comet():
- has[MetaDataStyle.COMET] = True
-
- if opts.print:
-
- if not opts.type:
+ def print(self, ca: ComicArchive) -> None:
+ if not self.options.runtime_type:
page_count = ca.get_number_of_pages()
brief = ""
- if batch_mode:
+ if self.batch_mode:
brief = f"{ca.path}: "
if ca.is_sevenzip():
@@ -271,146 +229,149 @@ def process_file_cli(
brief += f"({page_count: >3} pages)"
brief += " tags:[ "
- if not (has[MetaDataStyle.CBI] or has[MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
+ if not (
+ ca.has_metadata(MetaDataStyle.CBI)
+ or ca.has_metadata(MetaDataStyle.CIX)
+ or ca.has_metadata(MetaDataStyle.COMET)
+ ):
brief += "none "
else:
- if has[MetaDataStyle.CBI]:
+ if ca.has_metadata(MetaDataStyle.CBI):
brief += "CBL "
- if has[MetaDataStyle.CIX]:
+ if ca.has_metadata(MetaDataStyle.CIX):
brief += "CR "
- if has[MetaDataStyle.COMET]:
+ if ca.has_metadata(MetaDataStyle.COMET):
brief += "CoMet "
brief += "]"
print(brief)
- if opts.terse:
+ if self.options.runtime_quiet:
return
print()
- if not opts.type or MetaDataStyle.CIX in opts.type:
- if has[MetaDataStyle.CIX]:
+ if not self.options.runtime_type or MetaDataStyle.CIX in self.options.runtime_type:
+ if ca.has_metadata(MetaDataStyle.CIX):
print("--------- ComicRack tags ---------")
try:
- if opts.raw:
+ if self.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 has[MetaDataStyle.CBI]:
+ if not self.options.runtime_type or MetaDataStyle.CBI in self.options.runtime_type:
+ if ca.has_metadata(MetaDataStyle.CBI):
print("------- ComicBookLover tags -------")
try:
- if opts.raw:
+ if self.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 has[MetaDataStyle.COMET]:
+ if not self.options.runtime_type or MetaDataStyle.COMET in self.options.runtime_type:
+ if ca.has_metadata(MetaDataStyle.COMET):
print("----------- CoMet tags -----------")
try:
- if opts.raw:
+ if self.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:
+ def delete(self, ca: ComicArchive) -> None:
+ for metadata_style in self.options.runtime_type:
style_name = MetaDataStyle.name[metadata_style]
- if has[metadata_style]:
- if not opts.dryrun:
+ if ca.has_metadata(metadata_style):
+ if not self.options.runtime_dryrun:
if not ca.remove_metadata(metadata_style):
- print(f"{filename}: Tag removal seemed to fail!")
+ print(f"{ca.path}: Tag removal seemed to fail!")
else:
- print(f"{filename}: Removed {style_name} tags.")
+ print(f"{ca.path}: Removed {style_name} tags.")
else:
- print(f"{filename}: dry-run. {style_name} tags not removed")
+ print(f"{ca.path}: dry-run. {style_name} tags not removed")
else:
- print(f"{filename}: This archive doesn't have {style_name} tags to remove.")
+ print(f"{ca.path}: This archive doesn't have {style_name} tags to remove.")
- elif opts.copy is not None:
- for metadata_style in opts.type:
+ def copy(self, ca: ComicArchive) -> None:
+ for metadata_style in self.options.runtime_type:
dst_style_name = MetaDataStyle.name[metadata_style]
- if opts.no_overwrite and has[metadata_style]:
- print(f"{filename}: Already has {dst_style_name} tags. Not overwriting.")
+ if not self.options.runtime_overwrite and ca.has_metadata(metadata_style):
+ print(f"{ca.path}: Already has {dst_style_name} tags. Not overwriting.")
return
- if opts.copy == metadata_style:
- print(f"{filename}: Destination and source are same: {dst_style_name}. Nothing to do.")
+ if self.options.commands_copy == metadata_style:
+ print(f"{ca.path}: Destination and source are same: {dst_style_name}. Nothing to do.")
return
- src_style_name = MetaDataStyle.name[opts.copy]
- if has[opts.copy]:
- if not opts.dryrun:
+ src_style_name = MetaDataStyle.name[self.options.commands_copy]
+ if ca.has_metadata(self.options.commands_copy):
+ if not self.options.runtime_dryrun:
try:
- md = ca.read_metadata(opts.copy)
+ md = ca.read_metadata(self.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 self.options.apply_transform_on_bulk_operation_ndetadata_style == MetaDataStyle.CBI:
+ md = CBLTransformer(md, self.options).apply()
if not ca.write_metadata(md, metadata_style):
- print(f"{filename}: Tag copy seemed to fail!")
+ print(f"{ca.path}: Tag copy seemed to fail!")
else:
- print(f"{filename}: Copied {src_style_name} tags to {dst_style_name}.")
+ print(f"{ca.path}: Copied {src_style_name} tags to {dst_style_name}.")
else:
- print(f"{filename}: dry-run. {src_style_name} tags not copied")
+ print(f"{ca.path}: dry-run. {src_style_name} tags not copied")
else:
- print(f"{filename}: This archive doesn't have {src_style_name} tags to copy.")
+ print(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.")
- elif opts.save:
-
- if opts.no_overwrite:
- for metadata_style in opts.type:
- if has[metadata_style]:
- print(f"{filename}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
+ def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> None:
+ if not self.options.runtime_overwrite:
+ for metadata_style in self.options.runtime_type:
+ if ca.has_metadata(metadata_style):
+ print(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
return
- if batch_mode:
+ if self.batch_mode:
print(f"Processing {ca.path}...")
- md = create_local_metadata(opts, ca, settings)
+ md = self.create_local_metadata(ca)
if md.issue is None or md.issue == "":
- if opts.assume_issue_one:
+ if self.options.runtime_assume_issue_one:
md.issue = "1"
# now, search online
- if opts.online:
- if opts.issue_id is not None:
+ if self.options.runtime_online:
+ if self.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 = self.talker_api.fetch_comic_data(self.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.", self.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 self.options.cbl_apply_transform_on_import:
+ ct_md = CBLTransformer(ct_md, self.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, self.options, self.talker_api)
def myoutput(text: str) -> None:
- if opts.verbose:
+ if self.options.runtime_verbose:
IssueIdentifier.default_write_output(text)
# use our overlaid MD struct to search
@@ -450,7 +411,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 self.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 +423,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 = self.actual_issue_data_fetch(matches[0]["issue_id"])
if ct_md.is_empty:
match_results.fetch_data_failures.append(str(ca.path.absolute()))
return
- if opts.overwrite:
+ if self.options.comicvine_clear_metadata_on_import:
md = ct_md
else:
notes = (
@@ -476,29 +437,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 self.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 self.actual_metadata_save(ca, md):
match_results.write_failures.append(str(ca.path.absolute()))
else:
match_results.good_matches.append(str(ca.path.absolute()))
- elif opts.rename:
+ def rename(self, ca: ComicArchive) -> None:
original_path = ca.path
msg_hdr = ""
- if batch_mode:
+ if self.batch_mode:
msg_hdr = f"{ca.path}: "
- md = create_local_metadata(opts, ca, settings)
+ md = self.create_local_metadata(ca)
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 self.options.filename_rename_set_extension_based_on_archive:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
@@ -506,11 +467,15 @@ 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 self.options.filename_rename_strict else "auto",
+ replacements=self.options.rename_replacements,
+ )
+ renamer.set_template(self.options.filename_rename_template)
+ renamer.set_issue_zero_padding(self.options.filename_rename_issue_number_padding)
+ renamer.set_smart_cleanup(self.options.filename_rename_use_smart_string_cleanup)
+ renamer.move = self.options.filename_rename_move_to_dir
try:
new_name = renamer.determine_name(ext=new_ext)
@@ -522,13 +487,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,
+ self.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", self.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, self.options.filename_rename_dir if self.options.filename_rename_move_to_dir else None
+ )
full_path = folder / new_name
@@ -537,7 +506,7 @@ def process_file_cli(
return
suffix = ""
- if not opts.dryrun:
+ if not self.options.runtime_dryrun:
# rename the file
try:
ca.rename(utils.unique_file(full_path))
@@ -548,19 +517,19 @@ def process_file_cli(
print(f"renamed '{original_path.name}' -> '{new_name}' {suffix}")
- elif opts.export_to_zip:
+ def export(self, ca: ComicArchive) -> None:
msg_hdr = ""
- if batch_mode:
+ if self.batch_mode:
msg_hdr = f"{ca.path}: "
if ca.is_zip():
logger.error(msg_hdr + "Archive is already a zip file.")
return
- filename_path = pathlib.Path(filename).absolute()
+ filename_path = ca.path
new_file = filename_path.with_suffix(".cbz")
- if opts.abort_on_conflict and new_file.exists():
+ if self.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 +537,10 @@ def process_file_cli(
delete_success = False
export_success = False
- if not opts.dryrun:
+ if not self.options.runtime_dryrun:
if ca.export_as_zip(new_file):
export_success = True
- if opts.delete_after_zip_export:
+ if self.options.runtime_delete_after_zip_export:
try:
filename_path.unlink(missing_ok=True)
delete_success = True
@@ -583,7 +552,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 self.options.runtime_delete_after_zip_export:
msg += " and delete original."
print(msg)
return
@@ -591,9 +560,47 @@ 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 self.options.runtime_delete_after_zip_export and delete_success:
msg += " (Original deleted) "
else:
msg += "Archive failed to export!"
print(msg)
+
+ def process_file_cli(self, filename: str, match_results: OnlineMatchResults) -> None:
+ if not os.path.lexists(filename):
+ logger.error("Cannot find %s", filename)
+ return
+
+ ca = ComicArchive(filename, self.options.general_rar_exe_path, str(graphics_path / "nocover.png"))
+
+ if not ca.seems_to_be_a_comic_archive():
+ logger.error("Sorry, but %s is not a comic archive!", filename)
+ return
+
+ if not ca.is_writable() and (
+ self.options.commands_delete
+ or self.options.commands_copy
+ or self.options.commands_save
+ or self.options.commands_rename
+ ):
+ logger.error("This archive is not writable")
+ return
+
+ if self.options.commands_print:
+ self.print(ca)
+
+ elif self.options.commands_delete:
+ self.delete(ca)
+
+ elif self.options.commands_copy is not None:
+ self.copy(ca)
+
+ elif self.options.commands_save:
+ self.save(ca, match_results)
+
+ elif self.options.commands_rename:
+ self.rename(ca)
+
+ elif self.options.commands_export_to_zip:
+ self.export(ca)
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/ctoptions/__init__.py b/comictaggerlib/ctoptions/__init__.py
new file mode 100644
index 0000000..fd1f386
--- /dev/null
+++ b/comictaggerlib/ctoptions/__init__.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from comictaggerlib.ctoptions.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options
+from comictaggerlib.ctoptions.file import register_settings, validate_settings
+from comictaggerlib.ctoptions.types import ComicTaggerPaths
+
+__all__ = [
+ "initial_cmd_line_parser",
+ "register_commandline",
+ "register_settings",
+ "validate_commandline_options",
+ "validate_settings",
+ "ComicTaggerPaths",
+]
diff --git a/comictaggerlib/ctoptions/cmdline.py b/comictaggerlib/ctoptions/cmdline.py
new file mode 100644
index 0000000..f0c1c36
--- /dev/null
+++ b/comictaggerlib/ctoptions/cmdline.py
@@ -0,0 +1,344 @@
+"""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 settngs
+
+from comicapi import utils
+from comicapi.genericmetadata import GenericMetadata
+from comictaggerlib import ctversion
+from comictaggerlib.ctoptions.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: settngs.Manager) -> None:
+ parser.add_setting(
+ "--config",
+ help="Config directory defaults to ~/.Config/ComicTagger\non Linux, ~/Library/Application Support/ComicTagger on Mac and %%APPDATA%%\\ComicTagger 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(
+ "--abort",
+ dest="abort_on_low_confidence",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="""Abort save operation when online match\nis of low confidence.\n\n""",
+ file=False,
+ )
+ parser.add_setting(
+ "--summary",
+ default=True,
+ action=argparse.BooleanOptionalAction,
+ help="Show the 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(
+ "--quiet",
+ "-q",
+ 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(
+ "--overwrite",
+ dest="overwrite",
+ action=argparse.BooleanOptionalAction,
+ default=True,
+ help="""Apply metadata to already tagged archives (relevant for -s or -c).""",
+ file=False,
+ )
+ parser.add_setting("files", nargs="*", file=False)
+
+
+def register_commands(parser: settngs.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: settngs.Manager) -> None:
+ parser.add_group("commands", register_commands, True)
+ parser.add_group("runtime", register_options)
+
+
+def validate_commandline_options(options: settngs.Config[settngs.Values], parser: settngs.Manager) -> settngs.Values:
+
+ if options[0].commands_version:
+ parser.exit(
+ status=1,
+ message=f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team\n"
+ "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n",
+ )
+
+ options[0].runtime_no_gui = any(
+ [
+ options[0].commands_print,
+ options[0].commands_delete,
+ options[0].commands_save,
+ options[0].commands_copy,
+ options[0].commands_rename,
+ options[0].commands_export_to_zip,
+ options[0].commands_only_set_cv_key,
+ ]
+ )
+
+ if platform.system() == "Windows" and options[0].runtime_glob:
+ # no globbing on windows shell, so do it for them
+ import glob
+
+ globs = options[0].runtime_files
+ options[0].runtime_files = []
+ for item in globs:
+ options[0].runtime_files.extend(glob.glob(item))
+
+ if (
+ options[0].commands_only_set_cv_key
+ and options[0].comicvine_cv_api_key is None
+ and options[0].comicvine_cv_url is None
+ ):
+ parser.exit(message="Key not given!\n", status=1)
+
+ if not options[0].commands_only_set_cv_key and options[0].runtime_no_gui and not options[0].runtime_files:
+ parser.exit(message="Command requires at least one filename!\n", status=1)
+
+ if options[0].commands_delete and not options[0].runtime_type:
+ parser.exit(message="Please specify the type to delete with -t\n", status=1)
+
+ if options[0].commands_save and not options[0].runtime_type:
+ parser.exit(message="Please specify the type to save with -t\n", status=1)
+
+ if options[0].commands_copy:
+ if not options[0].runtime_type:
+ parser.exit(message="Please specify the type to copy to with -t\n", status=1)
+ if len(options[0].commands_copy) > 1:
+ parser.exit(message="Please specify only one type to copy to with -c\n", status=1)
+ options[0].commands_copy = options[0].commands_copy[0]
+
+ if options[0].runtime_recursive:
+ options[0].runtime_file_list = utils.get_recursive_filelist(options[0].runtime_files)
+ else:
+ options[0].runtime_file_list = options[0].runtime_files
+
+ # take a crack at finding rar exe, if not set already
+ if options[0].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[0].general_rar_exe_path = r"C:\Program Files\WinRAR\Rar.exe"
+ elif os.path.exists(r"C:\Program Files (x86)\WinRAR\Rar.exe"):
+ options[0].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[0].general_rar_exe_path = "rar"
+
+ return options
diff --git a/comictaggerlib/ctoptions/file.py b/comictaggerlib/ctoptions/file.py
new file mode 100644
index 0000000..7791feb
--- /dev/null
+++ b/comictaggerlib/ctoptions/file.py
@@ -0,0 +1,262 @@
+from __future__ import annotations
+
+import argparse
+import uuid
+from typing import Any
+
+import settngs
+
+from comictaggerlib.ctoptions.types import AppendAction
+from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
+
+
+def general(parser: settngs.Manager) -> None:
+ # General Settings
+ parser.add_setting("--rar-exe-path", default="rar", help="The path to the rar program")
+ parser.add_setting(
+ "--allow-cbi-in-rar",
+ default=True,
+ action=argparse.BooleanOptionalAction,
+ help="Allows ComicBookLover tags in RAR/CBR files",
+ )
+ parser.add_setting("check_for_new_version", default=False, cmdline=False)
+ parser.add_setting("send_usage_stats", default=False, cmdline=False)
+
+
+def internal(parser: settngs.Manager) -> None:
+ # automatic settings
+ parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False)
+ parser.add_setting("save_data_style", default=0, cmdline=False)
+ parser.add_setting("load_data_style", default=0, cmdline=False)
+ parser.add_setting("last_opened_folder", default="", cmdline=False)
+ parser.add_setting("window_width", default=0, cmdline=False)
+ parser.add_setting("window_height", default=0, cmdline=False)
+ parser.add_setting("window_x", default=0, cmdline=False)
+ parser.add_setting("window_y", default=0, cmdline=False)
+ parser.add_setting("form_width", default=-1, cmdline=False)
+ parser.add_setting("list_width", default=-1, cmdline=False)
+ parser.add_setting("sort_column", default=-1, cmdline=False)
+ parser.add_setting("sort_direction", default=0, cmdline=False)
+
+
+def identifier(parser: settngs.Manager) -> None:
+ # identifier settings
+ parser.add_setting("--series-match-identify-thresh", default=91, type=int, help="")
+ parser.add_setting(
+ "--publisher-filter",
+ default=["Panini Comics", "Abril", "Planeta DeAgostini", "Editorial Televisa", "Dino Comics"],
+ action=AppendAction,
+ help="When enabled filters the listed publishers from all search results",
+ )
+
+
+def dialog(parser: settngs.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: settngs.Manager) -> None:
+ # filename parsing settings
+ parser.add_setting(
+ "--complicated-parser",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Enables the new parser which tries to extract more information from filenames",
+ )
+ parser.add_setting(
+ "--remove-c2c",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Removes c2c from filenames. Requires --complicated-parser",
+ )
+ parser.add_setting(
+ "--remove-fcbd",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Removes FCBD/free comic book day from filenames. Requires --complicated-parser",
+ )
+ parser.add_setting(
+ "--remove-publisher",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Attempts to remove publisher names from filenames, currently limited to Marvel and DC. Requires --complicated-parser",
+ )
+
+
+def comicvine(parser: settngs.Manager) -> None:
+ # Comic Vine settings
+ parser.add_setting(
+ "--series-match-search-thresh",
+ default=90,
+ type=int,
+ )
+ parser.add_setting("--use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting(
+ "--clear-metadata",
+ default=True,
+ help="Clears all existing metadata during import, default is to merges 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,
+ help="Removes html tables instead of converting them to text",
+ )
+ parser.add_setting(
+ "--cv-api-key",
+ help="Use the given Comic Vine API Key (persisted in settings).",
+ )
+ parser.add_setting(
+ "--cv-url",
+ 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, help="Sorts series by year"
+ )
+ parser.add_setting(
+ "--exact-series-matches-first",
+ default=True,
+ action=argparse.BooleanOptionalAction,
+ help="Puts series that are an exact match at the top of the list",
+ )
+ parser.add_setting(
+ "--always-use-publisher-filter",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Enables the publisher filter",
+ )
+ parser.add_setting(
+ "--clear-form-before-populating-from-cv",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Clears all existing metadata when applying metadata from ComicVine",
+ )
+
+
+def cbl(parser: settngs.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-transform-on-import", default=False, action=argparse.BooleanOptionalAction)
+ parser.add_setting("--apply-transform-on-bulk-operation", default=False, action=argparse.BooleanOptionalAction)
+
+
+def rename(parser: settngs.Manager) -> None:
+ # Rename settings
+ parser.add_setting("--template", default="{series} #{issue} ({year})", help="The teplate to use when renaming")
+ parser.add_setting(
+ "--issue-number-padding",
+ default=3,
+ type=int,
+ help="The minimum number of digits to use for the issue number when renaming",
+ )
+ parser.add_setting(
+ "--use-smart-string-cleanup",
+ default=True,
+ action=argparse.BooleanOptionalAction,
+ help="Attempts to intelligently cleanup whitespace when renaming",
+ )
+ parser.add_setting(
+ "--auto-extension",
+ dest="set_extension_based_on_archive",
+ default=True,
+ action=argparse.BooleanOptionalAction,
+ help="Automatically sets the extension based on the archive type e.g. cbr for rar, cbz for zip",
+ )
+ parser.add_setting("--dir", default="", help="The directory to move renamed files to")
+ parser.add_setting(
+ "--move",
+ dest="move_to_dir",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Enables moving renamed files to a separate directory",
+ )
+ parser.add_setting(
+ "--strict",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Ensures that filenames are valid for all OSs",
+ )
+ parser.add_setting(
+ "replacements",
+ default=DEFAULT_REPLACEMENTS,
+ cmdline=False,
+ )
+
+
+def autotag(parser: settngs.Manager) -> None:
+ # Auto-tag stickies
+ parser.add_setting(
+ "--save-on-low-confidence",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Automatically save metadata on low-confidence matches",
+ )
+ parser.add_setting(
+ "--dont-use-year-when-identifying",
+ default=False,
+ action=argparse.BooleanOptionalAction,
+ help="Ignore the year metadata attribute when identifying a comic",
+ )
+ 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,
+ help="When searching ignore leading numbers in the filename",
+ )
+ parser.add_setting("remove_archive_after_successful_match", default=False, cmdline=False)
+ parser.add_setting(
+ "-w",
+ "--wait-on-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: settngs.Config[settngs.Values], parser: settngs.Manager) -> dict[str, dict[str, Any]]:
+ options[0].identifier_publisher_filter = [x.strip() for x in options[0].identifier_publisher_filter if x.strip()]
+ options[0].rename_replacements = Replacements(
+ [Replacement(x[0], x[1], x[2]) for x in options[0].rename_replacements[0]],
+ [Replacement(x[0], x[1], x[2]) for x in options[0].rename_replacements[1]],
+ )
+ return options
+
+
+def register_settings(parser: settngs.Manager) -> None:
+ parser.add_group("general", general, False)
+ parser.add_group("internal", internal, False)
+ parser.add_group("identifier", identifier, False)
+ parser.add_group("dialog", dialog, False)
+ parser.add_group("filename", filename, False)
+ parser.add_group("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/ctoptions/types.py b/comictaggerlib/ctoptions/types.py
new file mode 100644
index 0000000..aee408d
--- /dev/null
+++ b/comictaggerlib/ctoptions/types.py
@@ -0,0 +1,186 @@
+from __future__ import annotations
+
+import argparse
+import pathlib
+from collections.abc import Sequence
+from typing import Any, Callable
+
+from appdirs import AppDirs
+
+from comicapi.comicarchive import MetaDataStyle
+from comicapi.genericmetadata import GenericMetadata
+
+
+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 _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 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/defaults.py b/comictaggerlib/defaults.py
new file mode 100644
index 0000000..8717d12
--- /dev/null
+++ b/comictaggerlib/defaults.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+from typing import NamedTuple
+
+
+class Replacement(NamedTuple):
+ find: str
+ replce: str
+ strict_only: bool
+
+
+class Replacements(NamedTuple):
+ literal_text: list[Replacement]
+ format_value: list[Replacement]
+
+
+DEFAULT_REPLACEMENTS = Replacements(
+ literal_text=[
+ Replacement(": ", " - ", True),
+ Replacement(":", "-", True),
+ ],
+ format_value=[
+ Replacement(": ", " - ", True),
+ Replacement(":", "-", True),
+ Replacement("/", "-", False),
+ Replacement("//", "--", False),
+ Replacement("\\", "-", True),
+ ],
+)
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/filerenamer.py b/comictaggerlib/filerenamer.py
index d52214f..828f8f2 100644
--- a/comictaggerlib/filerenamer.py
+++ b/comictaggerlib/filerenamer.py
@@ -20,42 +20,18 @@ import logging
import os
import pathlib
import string
-from typing import Any, NamedTuple, cast
+from typing import Any, cast
from pathvalidate import Platform, normalize_platform, sanitize_filename
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
+from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
logger = logging.getLogger(__name__)
-class Replacement(NamedTuple):
- find: str
- replce: str
- strict_only: bool
-
-
-class Replacements(NamedTuple):
- literal_text: list[Replacement]
- format_value: list[Replacement]
-
-
-REPLACEMENTS = Replacements(
- literal_text=[
- Replacement(": ", " - ", True),
- Replacement(":", "-", True),
- ],
- format_value=[
- Replacement(": ", " - ", True),
- Replacement(":", "-", True),
- Replacement("/", "-", False),
- Replacement("\\", "-", True),
- ],
-)
-
-
def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> pathlib.Path:
folder = ca.path.parent.absolute()
if rename_dir is not None:
@@ -67,7 +43,7 @@ def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> p
class MetadataFormatter(string.Formatter):
def __init__(
- self, smart_cleanup: bool = False, platform: str = "auto", replacements: Replacements = REPLACEMENTS
+ self, smart_cleanup: bool = False, platform: str = "auto", replacements: Replacements = DEFAULT_REPLACEMENTS
) -> None:
super().__init__()
self.smart_cleanup = smart_cleanup
@@ -200,13 +176,19 @@ class MetadataFormatter(string.Formatter):
class FileRenamer:
- def __init__(self, metadata: GenericMetadata | None, platform: str = "auto") -> None:
+ def __init__(
+ self,
+ metadata: GenericMetadata | None,
+ platform: str = "auto",
+ replacements: Replacements = DEFAULT_REPLACEMENTS,
+ ) -> None:
self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})"
self.smart_cleanup = True
self.issue_zero_padding = 3
self.metadata = metadata or GenericMetadata()
self.move = False
self.platform = platform
+ self.replacements = replacements
def set_metadata(self, metadata: GenericMetadata) -> None:
self.metadata = metadata
@@ -234,7 +216,7 @@ class FileRenamer:
new_name = ""
- fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform)
+ fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform, replacements=self.replacements)
md_dict = vars(md)
for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]:
md_dict[role] = md.get_primary_credit(role)
diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py
index f883d38..e5066a5 100644
--- a/comictaggerlib/fileselectionlist.py
+++ b/comictaggerlib/fileselectionlist.py
@@ -20,13 +20,13 @@ import os
import platform
from typing import Callable, cast
+import settngs
from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive
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
@@ -57,16 +57,13 @@ class FileSelectionList(QtWidgets.QWidget):
dataColNum = fileColNum
def __init__(
- self,
- parent: QtWidgets.QWidget,
- settings: ComicTaggerSettings,
- dirty_flag_verification: Callable[[str, str], bool],
+ self, parent: QtWidgets.QWidget, options: settngs.Namespace, 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 +224,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 +278,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..f3ad995
--- /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
+
+import settngs
+
+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: settngs.Config[settngs.Namespace], error: tuple[str, bool] | None
+) -> None:
+ os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
+ args = []
+ if options[0].runtime_darkmode:
+ args.extend(["-platform", "windows:darkmode=2"])
+ args.extend(sys.argv)
+ app = Application(args)
+ if error is not None:
+ show_exception_box(error[0])
+ if error[1]:
+ raise SystemExit(1)
+
+ # needed to catch initial open file events (macOS)
+ app.openFileRequest.connect(lambda x: options[0].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[0].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 60a88dc..a39cba8 100644
--- a/comictaggerlib/issueidentifier.py
+++ b/comictaggerlib/issueidentifier.py
@@ -20,6 +20,7 @@ import logging
import sys
from typing import Any, Callable
+import settngs
from typing_extensions import NotRequired, TypedDict
from comicapi import utils
@@ -29,8 +30,6 @@ from comicapi.issuestring import IssueString
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: settngs.Namespace, 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
@@ -200,10 +199,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()
@@ -254,7 +253,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
@@ -274,7 +275,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
@@ -464,7 +467,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]
@@ -485,6 +488,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..dc51dc3 100644
--- a/comictaggerlib/issueselectionwindow.py
+++ b/comictaggerlib/issueselectionwindow.py
@@ -17,11 +17,11 @@ from __future__ import annotations
import logging
+import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.issuestring import IssueString
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: settngs.Namespace,
talker_api: ComicTalker,
series_id: int,
issue_number: str,
@@ -53,7 +53,9 @@ 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 +73,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..db8b929
--- /dev/null
+++ b/comictaggerlib/log.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import logging.handlers
+import pathlib
+
+logger = logging.getLogger("comictagger")
+
+
+def get_filename(filename: str) -> str:
+ filename, _, number = filename.rpartition(".")
+ return filename.removesuffix("log") + number + ".log"
+
+
+def get_file_handler(filename: pathlib.Path) -> logging.FileHandler:
+ file_handler = logging.handlers.RotatingFileHandler(filename, encoding="utf-8", backupCount=10)
+ file_handler.namer = get_filename
+
+ 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)
+ logging.getLogger("comictalker").setLevel(logging.DEBUG)
+
+ log_file = log_dir / "ComicTagger.log"
+ log_dir.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..639ebb3
--- 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 signal
import sys
-import traceback
-import types
+
+import settngs
import comictalker.comictalkerapi as ct_api
from comicapi import utils
-from comictaggerlib import cli
+from comictaggerlib import cli, ctoptions
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,145 @@ 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: settngs.Namespace) -> 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 = settngs.Config({}, {})
+ self.initial_arg_parser = ctoptions.initial_cmd_line_parser()
+ self.config_load_success = False
- 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()
+
+ self.main()
+
+ 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)
+ return opts
+
+ def register_options(self) -> None:
+ self.manager = settngs.Manager(
+ """A utility for reading and writing metadata to comic archives.\n\n\nIf no options are given, %(prog)s will run in windowed mode.""",
+ "For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki",
+ )
+ ctoptions.register_commandline(self.manager)
+ ctoptions.register_settings(self.manager)
+
+ def parse_options(self, config_paths: ctoptions.ComicTaggerPaths) -> None:
+ self.options, self.config_load_success = self.manager.parse_config(
+ config_paths.user_config_dir / "settings.json"
+ )
+ self.options = self.manager.get_namespace(self.options)
+
+ self.options = ctoptions.validate_commandline_options(self.options, self.manager)
+ self.options = ctoptions.validate_settings(self.options, self.manager)
+ self.options = self.options
+
+ def initialize_dirs(self) -> None:
+ self.options[0].runtime_config.user_data_dir.mkdir(parents=True, exist_ok=True)
+ self.options[0].runtime_config.user_config_dir.mkdir(parents=True, exist_ok=True)
+ self.options[0].runtime_config.user_cache_dir.mkdir(parents=True, exist_ok=True)
+ self.options[0].runtime_config.user_state_dir.mkdir(parents=True, exist_ok=True)
+ self.options[0].runtime_config.user_log_dir.mkdir(parents=True, exist_ok=True)
+ logger.debug("user_data_dir: %s", self.options[0].runtime_config.user_data_dir)
+ logger.debug("user_config_dir: %s", self.options[0].runtime_config.user_config_dir)
+ logger.debug("user_cache_dir: %s", self.options[0].runtime_config.user_cache_dir)
+ logger.debug("user_state_dir: %s", self.options[0].runtime_config.user_state_dir)
+ logger.debug("user_log_dir: %s", self.options[0].runtime_config.user_log_dir)
+
+ def main(self) -> None:
+ assert self.options is not None
+ # options already loaded
+ error = None
+
+ 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",
)
- # 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
+ 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"])
- signal.signal(signal.SIGINT, signal.SIG_DFL)
+ utils.load_publishers()
+ update_publishers(self.options[0])
- logger.info(
- "ComicTagger Version: %s running on: %s PyInstaller: %s",
- version,
- platform.system(),
- "Yes" if getattr(sys, "frozen", None) else "No",
- )
+ if not qt_available and not self.options[0].runtime_no_gui:
+ self.options[0].runtime_no_gui = True
+ logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
- 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"])
+ # manage the CV API key
+ # None comparison is used so that the empty string can unset the value
+ if self.options[0].comicvine_cv_api_key is not None or self.options[0].comicvine_cv_url is not None:
+ settings_path = self.options[0].runtime_config.user_config_dir / "settings.json"
+ if self.config_load_success:
+ self.manager.save_file(self.options[0], settings_path)
- 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.")
-
- 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)
-
- 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()
+ if self.options[0].commands_only_set_cv_key:
+ if self.config_load_success:
+ print("Key set") # noqa: T201
+ return
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()
+ talker_api = ct_api.get_comic_talker("comicvine")( # type: ignore[call-arg]
+ version=version,
+ cache_folder=self.options[0].runtime_config.user_cache_dir,
+ series_match_thresh=self.options[0].comicvine_series_match_search_thresh,
+ remove_html_tables=self.options[0].comicvine_remove_html_tables,
+ use_series_start_as_volume=self.options[0].comicvine_use_series_start_as_volume,
+ wait_on_ratelimit=self.options[0].autotag_wait_and_retry_on_rate_limit,
+ api_url=self.options[0].comicvine_cv_url,
+ api_key=self.options[0].comicvine_cv_api_key,
)
+ except TalkerError as e:
+ logger.exception("Unable to load talker")
+ error = (str(e), True)
+
+ if not self.config_load_success:
+ error = (
+ f"Failed to load settings, check the log located in '{self.options[0].runtime_config.user_log_dir}' for more details",
+ True,
+ )
+ if self.options[0].runtime_no_gui:
+ if error and error[1]:
+ print(f"A fatal error occurred please check the log for more information: {error[0]}") # noqa: T201
+ raise SystemExit(1)
+ try:
+ cli.CLI(self.options[0], talker_api).run()
+ except Exception:
+ logger.exception("CLI mode failed")
+ else:
+ gui.open_tagger_window(talker_api, self.options, error)
diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py
index 330318c..7736ab6 100644
--- a/comictaggerlib/matchselectionwindow.py
+++ b/comictaggerlib/matchselectionwindow.py
@@ -18,6 +18,7 @@ from __future__ import annotations
import logging
import os
+import settngs
from PyQt5 import QtCore, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
@@ -38,18 +39,21 @@ class MatchSelectionWindow(QtWidgets.QDialog):
parent: QtWidgets.QWidget,
matches: list[IssueResult],
comic_archive: ComicArchive,
+ options: settngs.Values,
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..6addfac 100644
--- a/comictaggerlib/renamewindow.py
+++ b/comictaggerlib/renamewindow.py
@@ -17,13 +17,13 @@ from __future__ import annotations
import logging
+import settngs
from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
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: settngs.Config,
talker_api: ComicTalker,
) -> None:
super().__init__(parent)
@@ -54,25 +54,26 @@ 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"
- self.renamer = FileRenamer(None, platform=platform)
+ platform = "universal" if self.options[0].filename_rename_strict else "auto"
+ self.renamer = FileRenamer(None, platform=platform, replacements=self.options[0].rename_replacements)
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[0].filename_rename_template)
+ self.renamer.set_issue_zero_padding(self.options[0].filename_rename_issue_number_padding)
+ self.renamer.set_smart_cleanup(self.options[0].filename_rename_use_smart_string_cleanup)
+ self.renamer.replacements = self.options[0].rename_replacements
new_ext = ca.path.suffix # default
- if self.settings.rename_extension_based_on_archive:
+ if self.options[0].filename_rename_set_extension_based_on_archive:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
@@ -84,13 +85,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[0].filename_complicated_parser,
+ self.options[0].filename_remove_c2c,
+ self.options[0].filename_remove_fcbd,
+ self.options[0].filename_remove_publisher,
)
self.renamer.set_metadata(md)
- self.renamer.move = self.settings.rename_move_dir
+ self.renamer.move = self.options[0].filename_rename_move_to_dir
return new_ext
def do_preview(self) -> None:
@@ -103,7 +104,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[0].filename_rename_template)
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
@@ -117,7 +118,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[0].filename_rename_template,
+ self.renamer.metadata,
)
QtWidgets.QMessageBox.critical(
self,
@@ -164,7 +167,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 +195,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[0].filename_rename_dir if self.options[0].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/settingswindow.py b/comictaggerlib/settingswindow.py
index 6152e26..48ac846 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -20,15 +20,16 @@ import logging
import os
import pathlib
import platform
+from typing import Any
+import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import md_test
from comictaggerlib.ctversion import version
-from comictaggerlib.filerenamer import FileRenamer
+from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements
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 +130,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: settngs.Config, talker_api: ComicTalker) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "settingswindow.ui", self)
@@ -138,7 +139,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"
@@ -190,23 +191,64 @@ class SettingsWindow(QtWidgets.QDialog):
self.btnResetSettings.clicked.connect(self.reset_settings)
self.btnTestKey.clicked.connect(self.test_api_key)
self.btnTemplateHelp.clicked.connect(self.show_template_help)
- self.leRenameTemplate.textEdited.connect(self._rename_test)
- self.cbxMoveFiles.clicked.connect(self.rename_test)
self.cbxMoveFiles.clicked.connect(self.dir_test)
- self.cbxRenameStrict.clicked.connect(self.rename_test)
self.leDirectory.textEdited.connect(self.dir_test)
self.cbxComplicatedParser.clicked.connect(self.switch_parser)
- def rename_test(self) -> None:
+ self.btnAddLiteralReplacement.clicked.connect(self.addLiteralReplacement)
+ self.btnAddValueReplacement.clicked.connect(self.addValueReplacement)
+ self.btnRemoveLiteralReplacement.clicked.connect(self.removeLiteralReplacement)
+ self.btnRemoveValueReplacement.clicked.connect(self.removeValueReplacement)
+
+ self.leRenameTemplate.textEdited.connect(self.rename_test)
+ self.cbxMoveFiles.clicked.connect(self.rename_test)
+ self.cbxRenameStrict.clicked.connect(self.rename_test)
+ self.cbxSmartCleanup.clicked.connect(self.rename_test)
+ self.cbxChangeExtension.clicked.connect(self.rename_test)
+ self.leIssueNumPadding.textEdited.connect(self.rename_test)
+ self.twLiteralReplacements.cellChanged.connect(self.rename_test)
+ self.twValueReplacements.cellChanged.connect(self.rename_test)
+
+ def addLiteralReplacement(self) -> None:
+ self.insertRow(self.twLiteralReplacements, self.twLiteralReplacements.rowCount(), Replacement("", "", False))
+
+ def addValueReplacement(self) -> None:
+ self.insertRow(self.twValueReplacements, self.twValueReplacements.rowCount(), Replacement("", "", False))
+
+ def removeLiteralReplacement(self) -> None:
+ if self.twLiteralReplacements.currentRow() >= 0:
+ self.twLiteralReplacements.removeRow(self.twLiteralReplacements.currentRow())
+
+ def removeValueReplacement(self) -> None:
+ if self.twValueReplacements.currentRow() >= 0:
+ self.twValueReplacements.removeRow(self.twValueReplacements.currentRow())
+
+ def insertRow(self, table: QtWidgets.QTableWidget, row: int, replacement: Replacement) -> None:
+ find, replace, strict_only = replacement
+ table.insertRow(row)
+ table.setItem(row, 0, QtWidgets.QTableWidgetItem(find))
+ table.setItem(row, 1, QtWidgets.QTableWidgetItem(replace))
+ tmp = QtWidgets.QTableWidgetItem()
+ if strict_only:
+ tmp.setCheckState(QtCore.Qt.Checked)
+ else:
+ tmp.setCheckState(QtCore.Qt.Unchecked)
+ table.setItem(row, 2, tmp)
+
+ def rename_test(self, *args: Any, **kwargs: Any) -> None:
self._rename_test(self.leRenameTemplate.text())
def dir_test(self) -> None:
self.lblDir.setText(
- str(pathlib.Path(self.leDirectory.text().strip()).absolute()) if self.cbxMoveFiles.isChecked() else ""
+ str(pathlib.Path(self.leDirectory.text().strip()).resolve()) if self.cbxMoveFiles.isChecked() else ""
)
def _rename_test(self, template: str) -> None:
- fr = FileRenamer(md_test, platform="universal" if self.cbxRenameStrict.isChecked() else "auto")
+ fr = FileRenamer(
+ md_test,
+ platform="universal" if self.cbxRenameStrict.isChecked() else "auto",
+ replacements=self.get_replacemnts(),
+ )
fr.move = self.cbxMoveFiles.isChecked()
fr.set_template(template)
fr.set_issue_zero_padding(int(self.leIssueNumPadding.text()))
@@ -227,53 +269,85 @@ 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[0].general_rar_exe_path)
+ self.sbNameMatchIdentifyThresh.setValue(self.options[0].identifier_series_match_identify_thresh)
+ self.sbNameMatchSearchThresh.setValue(self.options[0].comicvine_series_match_search_thresh)
+ self.tePublisherFilter.setPlainText("\n".join(self.options[0].identifier_publisher_filter))
- self.cbxCheckForNewVersion.setChecked(self.settings.check_for_new_version)
+ self.cbxCheckForNewVersion.setChecked(self.options[0].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[0].filename_complicated_parser)
+ self.cbxRemoveC2C.setChecked(self.options[0].filename_remove_c2c)
+ self.cbxRemoveFCBD.setChecked(self.options[0].filename_remove_fcbd)
+ self.cbxRemovePublisher.setChecked(self.options[0].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[0].comicvine_use_series_start_as_volume)
+ self.cbxClearFormBeforePopulating.setChecked(self.options[0].comicvine_clear_form_before_populating_from_cv)
+ self.cbxRemoveHtmlTables.setChecked(self.options[0].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[0].comicvine_always_use_publisher_filter)
+ self.cbxSortByYear.setChecked(self.options[0].comicvine_sort_series_by_year)
+ self.cbxExactMatches.setChecked(self.options[0].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[0].comicvine_cv_api_key)
+ self.leURL.setText(self.options[0].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[0].cbl_assume_lone_credit_is_primary)
+ self.cbxCopyCharactersToTags.setChecked(self.options[0].cbl_copy_characters_to_tags)
+ self.cbxCopyTeamsToTags.setChecked(self.options[0].cbl_copy_teams_to_tags)
+ self.cbxCopyLocationsToTags.setChecked(self.options[0].cbl_copy_locations_to_tags)
+ self.cbxCopyStoryArcsToTags.setChecked(self.options[0].cbl_copy_storyarcs_to_tags)
+ self.cbxCopyNotesToComments.setChecked(self.options[0].cbl_copy_notes_to_comments)
+ self.cbxCopyWebLinkToComments.setChecked(self.options[0].cbl_copy_weblink_to_comments)
+ self.cbxApplyCBLTransformOnCVIMport.setChecked(self.options[0].cbl_apply_transform_on_import)
+ self.cbxApplyCBLTransformOnBatchOperation.setChecked(self.options[0].cbl_apply_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[0].rename_template)
+ self.leIssueNumPadding.setText(str(self.options[0].rename_issue_number_padding))
+ self.cbxSmartCleanup.setChecked(self.options[0].rename_use_smart_string_cleanup)
+ self.cbxChangeExtension.setChecked(self.options[0].rename_set_extension_based_on_archive)
+ self.cbxMoveFiles.setChecked(self.options[0].rename_move_to_dir)
+ self.leDirectory.setText(self.options[0].rename_dir)
+ self.cbxRenameStrict.setChecked(self.options[0].rename_strict)
+
+ for table, replacments in zip(
+ (self.twLiteralReplacements, self.twValueReplacements), self.options[0].rename_replacements
+ ):
+ table.clearContents()
+ for i in reversed(range(table.rowCount())):
+ table.removeRow(i)
+ for row, replacement in enumerate(replacments):
+ self.insertRow(table, row, replacement)
+
+ def get_replacemnts(self) -> Replacements:
+ literal_replacements = []
+ value_replacements = []
+ for row in range(self.twLiteralReplacements.rowCount()):
+ if self.twLiteralReplacements.item(row, 0).text():
+ literal_replacements.append(
+ Replacement(
+ self.twLiteralReplacements.item(row, 0).text(),
+ self.twLiteralReplacements.item(row, 1).text(),
+ self.twLiteralReplacements.item(row, 2).checkState() == QtCore.Qt.Checked,
+ )
+ )
+ for row in range(self.twValueReplacements.rowCount()):
+ if self.twValueReplacements.item(row, 0).text():
+ value_replacements.append(
+ Replacement(
+ self.twValueReplacements.item(row, 0).text(),
+ self.twValueReplacements.item(row, 1).text(),
+ self.twValueReplacements.item(row, 2).checkState() == QtCore.Qt.Checked,
+ )
+ )
+ return Replacements(literal_replacements, value_replacements)
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[0].rename_template)
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
@@ -287,7 +361,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[0].rename_template, self.renamer.metadata
)
QtWidgets.QMessageBox.critical(
self,
@@ -300,69 +374,74 @@ class SettingsWindow(QtWidgets.QDialog):
)
# Copy values from form to settings and save
- self.settings.rar_exe_path = str(self.leRarExePath.text())
+ self.options[0].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[0].general_rar_exe_path:
+ utils.add_to_path(os.path.dirname(self.options[0].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[0].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[0].identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
+ self.options[0].comicvine_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
+ self.options[0].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[0].filename_complicated_parser = self.cbxComplicatedParser.isChecked()
+ self.options[0].filename_remove_c2c = self.cbxRemoveC2C.isChecked()
+ self.options[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked()
+ self.options[0].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[0].comicvine_use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
+ self.options[0].comicvine_clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
+ self.options[0].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[0].comicvine_always_use_publisher_filter = self.cbxUseFilter.isChecked()
+ self.options[0].comicvine_sort_series_by_year = self.cbxSortByYear.isChecked()
+ self.options[0].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[0].comicvine_cv_api_key = self.leKey.text().strip()
+ self.talker_api.api_key = self.options[0].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[0].comicvine_cv_url = self.leURL.text().strip()
+ self.talker_api.api_url = self.options[0].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[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
+ self.options[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
+ self.options[0].cbl_copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
+ self.options[0].cbl_copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
+ self.options[0].cbl_copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
+ self.options[0].cbl_copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
+ self.options[0].cbl_copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
+ self.options[0].cbl_apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
+ self.options[0].cbl_apply_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
- self.settings.rename_strict = self.cbxRenameStrict.isChecked()
+ self.options[0].rename_template = str(self.leRenameTemplate.text())
+ self.options[0].rename_issue_number_padding = int(self.leIssueNumPadding.text())
+ self.options[0].rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
+ self.options[0].rename_set_extension_based_on_archive = self.cbxChangeExtension.isChecked()
+ self.options[0].rename_move_to_dir = self.cbxMoveFiles.isChecked()
+ self.options[0].rename_dir = self.leDirectory.text()
- self.settings.save()
+ self.options[0].rename_strict = self.cbxRenameStrict.isChecked()
+ self.options[0].rename_replacements = self.get_replacemnts()
+
+ settngs.save_file(self.options, self.options[0].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[0].runtime_config.cache_folder).clear_cache()
+ ComicCacher(self.options[0].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 +451,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 = settngs.Config(settngs.defaults(self.options[1]), self.options[1])
self.settings_to_form()
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
@@ -392,9 +471,9 @@ class SettingsWindow(QtWidgets.QDialog):
dialog.setDirectory(os.path.dirname(str(control.text())))
if name == "RAR":
- dialog.setWindowTitle("Find " + name + " program")
+ dialog.setWindowTitle(f"Find {name} program")
else:
- dialog.setWindowTitle("Find " + name + " library")
+ dialog.setWindowTitle(f"Find {name} library")
if dialog.exec():
file_list = dialog.selectedFiles()
diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py
index 38d2e91..b9aa040 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
@@ -32,6 +31,7 @@ from typing import Any, Callable
from urllib.parse import urlparse
import natsort
+import settngs
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets, uic
from comicapi import utils
@@ -58,7 +58,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 +79,24 @@ class TaggerWindow(QtWidgets.QMainWindow):
def __init__(
self,
file_list: list[str],
- settings: ComicTaggerSettings,
+ options: settngs.Config,
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
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[0].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[0].internal_install_id)
# send file list to other instance
if file_list:
socket.write(pickle.dumps(file_list))
@@ -110,20 +108,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[0].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[0].internal_install_id)
+ ok = self.socketServer.listen(options[0].internal_install_id)
if not ok:
logger.error(
"Cannot start local socket with key [%s]. Reason: %s",
- settings.install_id,
+ options[0].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 +130,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[0], 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[0].internal_sort_column,
+ QtCore.Qt.SortOrder(self.options[0].internal_sort_direction),
)
# we can't specify relative font sizes in the UI designer, so
@@ -156,14 +155,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[0].runtime_type and isinstance(options[0].runtime_type[0], int):
+ # respect the command line option tag type
+ options[0].internal_save_data_style = options[0].runtime_type[0]
+ options[0].internal_load_data_style = options[0].runtime_type[0]
+
+ self.save_data_style = options[0].internal_save_data_style
+ self.load_data_style = options[0].internal_load_data_style
self.setAcceptDrops(True)
self.config_menus()
@@ -228,8 +227,8 @@ 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[0].internal_form_width != -1:
+ self.splitter.setSizes([self.options[0].internal_form_width, self.options[0].internal_list_width])
self.raise_()
QtCore.QCoreApplication.processEvents()
self.resizeEvent(None)
@@ -246,7 +245,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[0].dialog_show_disclaimer:
checked = OptionalMessageDialog.msg(
self,
"Welcome!",
@@ -261,26 +260,9 @@ use ComicTagger on local copies of your comics.
Have fun!
""",
)
- self.settings.show_disclaimer = not checked
+ self.options[0].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[0].general_check_for_new_version:
self.check_latest_version_online()
def open_file_event(self, url: QtCore.QUrl) -> None:
@@ -293,7 +275,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[0].runtime_config.user_log_dir / "ComicTagger.log").read_text("utf-8")
except Exception:
current_logs = ""
root_logger = logging.getLogger()
@@ -494,7 +476,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 +608,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[0].filename_complicated_parser,
+ self.options[0].filename_remove_c2c,
+ self.options[0].filename_remove_fcbd,
+ self.options[0].filename_remove_publisher,
)
if len(self.metadata.pages) == 0 and self.comic_archive is not None:
self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages())
@@ -998,10 +979,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[0].filename_complicated_parser,
+ self.options[0].filename_remove_c2c,
+ self.options[0].filename_remove_fcbd,
+ self.options[0].filename_remove_publisher,
split_words,
)
if new_metadata is not None:
@@ -1022,8 +1003,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[0].internal_last_opened_folder is not None:
+ dialog.setDirectory(self.options[0].internal_last_opened_folder)
if not folder_mode:
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar *.cb7 *.7z)"
@@ -1073,7 +1054,7 @@ You have {4-self.settings.settings_warning} warnings left.
issue_count,
cover_index_list,
self.comic_archive,
- self.settings,
+ self.options[0],
self.talker_api,
autoselect,
literal,
@@ -1092,16 +1073,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 +1086,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[0].cbl_apply_transform_on_import:
+ new_metadata = CBLTransformer(new_metadata, self.options[0]).apply()
- if self.settings.clear_form_before_populating_from_cv:
+ if self.options[0].comicvine_clear_form_before_populating_from_cv:
self.clear_form()
notes = (
@@ -1170,7 +1144,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[0].internal_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 +1155,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[0].internal_save_data_style = self.save_data_style
self.update_style_tweaks()
self.update_menus()
@@ -1400,15 +1374,15 @@ 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[0].internal_window_width != 0:
+ self.move(self.options[0].internal_window_x, self.options[0].internal_window_y)
+ self.resize(self.options[0].internal_window_width, self.options[0].internal_window_height)
else:
screen = QtGui.QGuiApplication.primaryScreen().geometry()
size = self.frameGeometry()
@@ -1675,8 +1649,8 @@ 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[0].cbl_apply_transform_on_bulk_operation:
+ md = CBLTransformer(md, self.options[0]).apply()
if not ca.write_metadata(md, dest_style):
failed_list.append(ca.path)
@@ -1710,12 +1684,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[0].cbl_apply_transform_on_import:
+ ct_md = CBLTransformer(ct_md, self.options[0]).apply()
QtWidgets.QApplication.restoreOverrideCursor()
@@ -1734,7 +1708,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[0], self.talker_api)
# read in metadata, and parse file name if not there
try:
@@ -1744,10 +1718,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[0].filename_complicated_parser,
+ self.options[0].filename_remove_c2c,
+ self.options[0].filename_remove_fcbd,
+ self.options[0].filename_remove_publisher,
dlg.split_words,
)
if dlg.ignore_leading_digits_in_filename and md.series is not None:
@@ -1833,7 +1807,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[0].comicvine_auto_imprint:
md.fix_publisher()
if not ca.write_metadata(md, self.save_data_style):
@@ -1862,7 +1836,7 @@ You have {4-self.settings.settings_warning} warnings left.
atstartdlg = AutoTagStartWindow(
self,
- self.settings,
+ self.options[0],
(
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 +1939,7 @@ You have {4-self.settings.settings_warning} warnings left.
match_results.multiple_matches,
style,
self.actual_issue_data_fetch,
- self.settings,
+ self.options[0],
self.talker_api,
)
matchdlg.setModal(True)
@@ -2011,17 +1985,17 @@ 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[0].internal_window_width = appsize.width()
+ self.options[0].internal_window_height = appsize.height()
+ self.options[0].internal_window_x = self.x()
+ self.options[0].internal_window_y = self.y()
+ self.options[0].internal_form_width = self.splitter.sizes()[0]
+ self.options[0].internal_list_width = self.splitter.sizes()[1]
(
- self.settings.last_filelist_sorted_column,
- self.settings.last_filelist_sorted_order,
+ self.options[0].internal_sort_column,
+ self.options[0].internal_sort_direction,
) = self.fileSelectionList.get_sorting()
- self.settings.save()
+ settngs.save_file(self.options, self.options[0].runtime_config.user_config_dir / "settings.json")
event.accept()
else:
@@ -2070,7 +2044,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[0]).apply()
self.metadata_to_form()
def recalc_page_dimensions(self) -> None:
@@ -2096,7 +2070,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 +2089,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[0].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 +2121,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[0].internal_install_id, self.options[0].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[0].dialog_dont_notify_about_this_version):
website = "https://github.com/comictagger/comictagger"
checked = OptionalMessageDialog.msg(
self,
@@ -2162,7 +2138,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[0].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/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui
index 418bf4c..813c7a8 100644
--- a/comictaggerlib/ui/settingswindow.ui
+++ b/comictaggerlib/ui/settingswindow.ui
@@ -7,7 +7,7 @@
0
0
702
- 488
+ 513
@@ -616,7 +616,21 @@
Rename
- -
+
-
+
+
+ Add Replacement
+
+
+
+ -
+
+
+ Remove Replacement
+
+
+
+ -
QFormLayout::AllNonFixedFieldsGrow
@@ -717,8 +731,7 @@
-
- If checked will ensure reserved characters and filenames are removed for all Operating Systems.
-By default only removes restricted characters and filenames for the current Operating System.
+ If checked will ensure reserved characters and filenames are removed for all Operating Systems.<br/>By default only removes restricted characters and filenames for the current Operating System.
Strict renaming
@@ -730,6 +743,118 @@ By default only removes restricted characters and filenames for the current Oper
+ -
+
+
+ Add Replacement
+
+
+
+ -
+
+
+ Remove Replacement
+
+
+
+ -
+
+
+
+ Monaco
+
+
+
+ QAbstractItemView::SingleSelection
+
+
+ true
+
+
+
+ Find
+
+
+ AlignCenter
+
+
+
+
+ Replacement
+
+
+ AlignCenter
+
+
+
+
+ Strict Only
+
+
+ AlignCenter
+
+
+
+
+ -
+
+
+
+ Monaco
+
+
+
+ Qt::ActionsContextMenu
+
+
+ QAbstractItemView::SingleSelection
+
+
+ 3
+
+
+ true
+
+
+
+ Find
+
+
+ AlignCenter
+
+
+
+
+ Replacement
+
+
+ AlignCenter
+
+
+
+
+ Strict Only
+
+
+ AlignCenter
+
+
+
+
+ -
+
+
+ Value Text Replacements
+
+
+
+ -
+
+
+ Literal Text Replacements
+
+
+
diff --git a/comictaggerlib/volumeselectionwindow.py b/comictaggerlib/volumeselectionwindow.py
index 92637bc..4a3be9f 100644
--- a/comictaggerlib/volumeselectionwindow.py
+++ b/comictaggerlib/volumeselectionwindow.py
@@ -19,6 +19,7 @@ import itertools
import logging
from collections import deque
+import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import pyqtSignal
@@ -30,7 +31,6 @@ 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: settngs.Namespace,
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
@@ -207,7 +209,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
@@ -280,7 +282,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():
@@ -296,7 +300,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:
@@ -324,7 +328,7 @@ 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)
@@ -362,7 +366,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(
@@ -376,7 +380,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(
@@ -392,7 +396,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,
@@ -410,7 +414,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 c30aab8..3e4ea89 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
@@ -7,6 +8,7 @@ pycountry
pyicu; sys_platform == 'linux' or sys_platform == 'darwin'
rapidfuzz>=2.12.0
requests==2.*
+settngs==0.3.0
text2digits
typing_extensions
wordninja
diff --git a/setup.py b/setup.py
index b95c311..e8d1c3d 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ import os
from setuptools import find_packages, setup
-def read(fname):
+def read(fname: str) -> str:
"""
Read the contents of a file.
Parameters
@@ -60,7 +60,7 @@ setup(
),
package_data={"comictaggerlib": ["ui/*", "graphics/*"], "comicapi": ["data/*"]},
entry_points=dict(
- console_scripts=["comictagger=comictaggerlib.main:ctmain"],
+ console_scripts=["comictagger=comictaggerlib.main:main"],
pyinstaller40=[
"hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs",
],
diff --git a/tests/comiccacher_test.py b/tests/comiccacher_test.py
index 7eeb314..2bc9791 100644
--- a/tests/comiccacher_test.py
+++ b/tests/comiccacher_test.py
@@ -6,9 +6,10 @@ import comictalker.comiccacher
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()
+def test_create_cache(options, mock_version):
+ settings, definitions = options
+ 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..7b20081 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,11 +9,12 @@ from typing import Any
import pytest
import requests
+import settngs
from PIL import Image
import comicapi.comicarchive
import comicapi.genericmetadata
-import comictaggerlib.settings
+import comictaggerlib.ctoptions
import comictalker.comiccacher
import comictalker.talkers.comicvine
from comicapi import utils
@@ -55,7 +56,7 @@ def no_requests(monkeypatch) -> None:
@pytest.fixture
def comicvine_api(
- monkeypatch, cbz, comic_cache, mock_version, settings
+ monkeypatch, cbz, comic_cache, mock_version, options
) -> comictalker.talkers.comicvine.ComicVineTalker:
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method or None for invalid urls.
@@ -116,7 +117,7 @@ def comicvine_api(
cv = comictalker.talkers.comicvine.ComicVineTalker(
version=mock_version[0],
- cache_folder=settings.get_settings_folder(),
+ cache_folder=options[0].runtime_config.user_cache_dir,
api_url="",
api_key="",
series_match_thresh=90,
@@ -162,10 +163,26 @@ def seed_all_publishers(monkeypatch):
@pytest.fixture
-def settings(tmp_path):
- yield comictaggerlib.settings.ComicTaggerSettings(tmp_path / "settings")
+def options(settings_manager, tmp_path):
+
+ comictaggerlib.ctoptions.register_commandline(settings_manager)
+ comictaggerlib.ctoptions.register_settings(settings_manager)
+ defaults = settings_manager.get_namespace(settings_manager.defaults())
+ defaults[0].runtime_config = comictaggerlib.ctoptions.ComicTaggerPaths(tmp_path / "config")
+ defaults[0].runtime_config.user_data_dir.mkdir(parents=True, exist_ok=True)
+ defaults[0].runtime_config.user_config_dir.mkdir(parents=True, exist_ok=True)
+ defaults[0].runtime_config.user_cache_dir.mkdir(parents=True, exist_ok=True)
+ defaults[0].runtime_config.user_state_dir.mkdir(parents=True, exist_ok=True)
+ defaults[0].runtime_config.user_log_dir.mkdir(parents=True, exist_ok=True)
+ 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])
+def settings_manager():
+ manager = settngs.Manager()
+ yield manager
+
+
+@pytest.fixture
+def comic_cache(options, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]:
+ yield comictalker.comiccacher.ComicCacher(options[0].runtime_config.user_cache_dir, mock_version[0])
diff --git a/tests/issueidentifier_test.py b/tests/issueidentifier_test.py
index a4e3498..3ff6561 100644
--- a/tests/issueidentifier_test.py
+++ b/tests/issueidentifier_test.py
@@ -9,7 +9,8 @@ import testing.comicdata
import testing.comicvine
-def test_crop(cbz_double_cover, settings, tmp_path, comicvine_api):
+def test_crop(cbz_double_cover, options, tmp_path, comicvine_api):
+ settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz_double_cover, settings, comicvine_api)
cropped = ii.crop_cover(cbz_double_cover.archiver.read_file("double_cover.jpg"))
original_cover = cbz_double_cover.get_page(0)
@@ -21,14 +22,16 @@ def test_crop(cbz_double_cover, settings, tmp_path, comicvine_api):
@pytest.mark.parametrize("additional_md, expected", testing.comicdata.metadata_keys)
-def test_get_search_keys(cbz, settings, additional_md, expected, comicvine_api):
+def test_get_search_keys(cbz, options, additional_md, expected, comicvine_api):
+ settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api)
ii.set_additional_metadata(additional_md)
assert expected == ii.get_search_keys()
-def test_get_issue_cover_match_score(cbz, settings, comicvine_api):
+def test_get_issue_cover_match_score(cbz, options, comicvine_api):
+ settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api)
score = ii.get_issue_cover_match_score(
int(
@@ -49,7 +52,8 @@ def test_get_issue_cover_match_score(cbz, settings, comicvine_api):
assert expected == score
-def test_search(cbz, settings, comicvine_api):
+def test_search(cbz, options, comicvine_api):
+ settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api)
results = ii.search()
cv_expected = {