Update Settings
This commit is contained in:
parent
10f36e9868
commit
19112ac79b
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -23,9 +23,9 @@ from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comicapi.comicarchive import MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.resulttypes import IssueResult, MultipleMatch
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
from comictalker.talkerbase import ComicTalker
|
||||
@ -42,23 +42,28 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
match_set_list: list[MultipleMatch],
|
||||
style: int,
|
||||
fetch_func: Callable[[IssueResult], GenericMetadata],
|
||||
settings: ComicTaggerSettings,
|
||||
options: settings.OptionValues,
|
||||
talker_api: ComicTalker,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ui_path / "matchselectionwindow.ui", self)
|
||||
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
|
||||
self.current_match_set: MultipleMatch = match_set_list[0]
|
||||
|
||||
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, talker_api, CoverImageWidget.AltCoverMode)
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer,
|
||||
CoverImageWidget.AltCoverMode,
|
||||
options["runtime"]["config"].user_cache_dir,
|
||||
talker_api,
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, talker_api, CoverImageWidget.ArchiveMode)
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode, None, None)
|
||||
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
@ -240,10 +245,10 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
md = ca.read_metadata(self._style)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
self.options["filename"]["complicated_parser"],
|
||||
self.options["filename"]["remove_c2c"],
|
||||
self.options["filename"]["remove_fcbd"],
|
||||
self.options["filename"]["remove_publisher"],
|
||||
)
|
||||
|
||||
# now get the particular issue data
|
||||
|
@ -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)
|
||||
|
@ -19,14 +19,14 @@ import logging
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.ui import ui_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent: QtWidgets.QWidget, settings: ComicTaggerSettings, msg: str) -> None:
|
||||
def __init__(self, parent: QtWidgets.QWidget, options: settings.OptionValues, msg: str) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ui_path / "autotagstartwindow.ui", self)
|
||||
@ -36,19 +36,20 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
|
||||
)
|
||||
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
|
||||
self.cbxSpecifySearchString.setChecked(False)
|
||||
self.cbxSplitWords.setChecked(False)
|
||||
self.sbNameMatchSearchThresh.setValue(self.settings.id_series_match_identify_thresh)
|
||||
self.sbNameMatchSearchThresh.setValue(self.options["identifier"]["series_match_identify_thresh"])
|
||||
self.leSearchString.setEnabled(False)
|
||||
|
||||
self.cbxSaveOnLowConfidence.setChecked(self.settings.save_on_low_confidence)
|
||||
self.cbxDontUseYear.setChecked(self.settings.dont_use_year_when_identifying)
|
||||
self.cbxAssumeIssueOne.setChecked(self.settings.assume_1_if_no_issue_num)
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.settings.ignore_leading_numbers_in_filename)
|
||||
self.cbxRemoveAfterSuccess.setChecked(self.settings.remove_archive_after_successful_match)
|
||||
self.cbxAutoImprint.setChecked(self.settings.auto_imprint)
|
||||
self.cbxSaveOnLowConfidence.setChecked(self.options["autotag"]["save_on_low_confidence"])
|
||||
self.cbxDontUseYear.setChecked(self.options["autotag"]["dont_use_year_when_identifying"])
|
||||
self.cbxAssumeIssueOne.setChecked(self.options["autotag"]["assume_1_if_no_issue_num"])
|
||||
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.options["autotag"]["ignore_leading_numbers_in_filename"])
|
||||
self.cbxRemoveAfterSuccess.setChecked(self.options["autotag"]["remove_archive_after_successful_match"])
|
||||
self.cbxWaitForRateLimit.setChecked(self.options["autotag"]["wait_and_retry_on_rate_limit"])
|
||||
self.cbxAutoImprint.setChecked(self.options["comicvine"]["auto_imprint"])
|
||||
|
||||
nlmt_tip = """<html>The <b>Name Match Ratio Threshold: Auto-Identify</b> is for eliminating automatic
|
||||
search matches that are too long compared to your series name search. The lower
|
||||
@ -72,8 +73,9 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.assume_issue_one = False
|
||||
self.ignore_leading_digits_in_filename = False
|
||||
self.remove_after_success = False
|
||||
self.wait_and_retry_on_rate_limit = False
|
||||
self.search_string = ""
|
||||
self.name_length_match_tolerance = self.settings.id_series_match_search_thresh
|
||||
self.name_length_match_tolerance = self.options["comicvine"]["series_match_search_thresh"]
|
||||
self.split_words = self.cbxSplitWords.isChecked()
|
||||
|
||||
def search_string_toggle(self) -> None:
|
||||
@ -89,14 +91,16 @@ class AutoTagStartWindow(QtWidgets.QDialog):
|
||||
self.ignore_leading_digits_in_filename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
|
||||
self.remove_after_success = self.cbxRemoveAfterSuccess.isChecked()
|
||||
self.name_length_match_tolerance = self.sbNameMatchSearchThresh.value()
|
||||
self.wait_and_retry_on_rate_limit = self.cbxWaitForRateLimit.isChecked()
|
||||
self.split_words = self.cbxSplitWords.isChecked()
|
||||
|
||||
# persist some settings
|
||||
self.settings.save_on_low_confidence = self.auto_save_on_low
|
||||
self.settings.dont_use_year_when_identifying = self.dont_use_year
|
||||
self.settings.assume_1_if_no_issue_num = self.assume_issue_one
|
||||
self.settings.ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
|
||||
self.settings.remove_archive_after_successful_match = self.remove_after_success
|
||||
self.options["autotag"]["save_on_low_confidence"] = self.auto_save_on_low
|
||||
self.options["autotag"]["dont_use_year_when_identifying"] = self.dont_use_year
|
||||
self.options["autotag"]["assume_1_if_no_issue_num"] = self.assume_issue_one
|
||||
self.options["autotag"]["ignore_leading_numbers_in_filename"] = self.ignore_leading_digits_in_filename
|
||||
self.options["autotag"]["remove_archive_after_successful_match"] = self.remove_after_success
|
||||
self.options["autotag"]["wait_and_retry_on_rate_limit"] = self.wait_and_retry_on_rate_limit
|
||||
|
||||
if self.cbxSpecifySearchString.isChecked():
|
||||
self.search_string = self.leSearchString.text()
|
||||
|
@ -18,15 +18,15 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from comicapi.genericmetadata import CreditMetadata, GenericMetadata
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CBLTransformer:
|
||||
def __init__(self, metadata: GenericMetadata, settings: ComicTaggerSettings) -> None:
|
||||
def __init__(self, metadata: GenericMetadata, options: settings.OptionValues) -> None:
|
||||
self.metadata = metadata
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
|
||||
def apply(self) -> GenericMetadata:
|
||||
# helper funcs
|
||||
@ -40,7 +40,7 @@ class CBLTransformer:
|
||||
for item in items:
|
||||
append_to_tags_if_unique(item)
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
if self.options["cbl"]["assume_lone_credit_is_primary"]:
|
||||
|
||||
# helper
|
||||
def set_lone_primary(role_list: list[str]) -> tuple[CreditMetadata | None, int]:
|
||||
@ -67,19 +67,19 @@ class CBLTransformer:
|
||||
c["primary"] = False
|
||||
self.metadata.add_credit(c["person"], "Artist", True)
|
||||
|
||||
if self.settings.copy_characters_to_tags:
|
||||
if self.options["cbl"]["copy_characters_to_tags"]:
|
||||
add_string_list_to_tags(self.metadata.characters)
|
||||
|
||||
if self.settings.copy_teams_to_tags:
|
||||
if self.options["cbl"]["copy_teams_to_tags"]:
|
||||
add_string_list_to_tags(self.metadata.teams)
|
||||
|
||||
if self.settings.copy_locations_to_tags:
|
||||
if self.options["cbl"]["copy_locations_to_tags"]:
|
||||
add_string_list_to_tags(self.metadata.locations)
|
||||
|
||||
if self.settings.copy_storyarcs_to_tags:
|
||||
if self.options["cbl"]["copy_storyarcs_to_tags"]:
|
||||
add_string_list_to_tags(self.metadata.story_arc)
|
||||
|
||||
if self.settings.copy_notes_to_comments:
|
||||
if self.options["cbl"]["copy_notes_to_comments"]:
|
||||
if self.metadata.notes is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
@ -88,7 +88,7 @@ class CBLTransformer:
|
||||
if self.metadata.notes not in self.metadata.comments:
|
||||
self.metadata.comments += self.metadata.notes
|
||||
|
||||
if self.settings.copy_weblink_to_comments:
|
||||
if self.options["cbl"]["copy_weblink_to_comments"]:
|
||||
if self.metadata.web_link is not None:
|
||||
if self.metadata.comments is None:
|
||||
self.metadata.comments = ""
|
||||
|
@ -16,7 +16,6 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@ -28,21 +27,18 @@ from pprint import pprint
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib import ctversion
|
||||
from comictaggerlib import ctversion, settings
|
||||
from comictaggerlib.cbltransformer import CBLTransformer
|
||||
from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
from comictaggerlib.issueidentifier import IssueIdentifier
|
||||
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictalker.talkerbase import ComicTalker, TalkerError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def actual_issue_data_fetch(
|
||||
issue_id: int, settings: ComicTaggerSettings, opts: argparse.Namespace, talker_api: ComicTalker
|
||||
) -> GenericMetadata:
|
||||
def actual_issue_data_fetch(issue_id: int, options: settings.OptionValues, talker_api: ComicTalker) -> GenericMetadata:
|
||||
# now get the particular issue data
|
||||
try:
|
||||
ct_md = talker_api.fetch_comic_data(issue_id)
|
||||
@ -50,15 +46,15 @@ def actual_issue_data_fetch(
|
||||
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
|
||||
return GenericMetadata()
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
ct_md = CBLTransformer(ct_md, settings).apply()
|
||||
if options["cbl"]["apply_cbl_transform_on_cv_import"]:
|
||||
ct_md = CBLTransformer(ct_md, options).apply()
|
||||
|
||||
return ct_md
|
||||
|
||||
|
||||
def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: GenericMetadata) -> bool:
|
||||
if not opts.dryrun:
|
||||
for metadata_style in opts.type:
|
||||
def actual_metadata_save(ca: ComicArchive, options: settings.OptionValues, md: GenericMetadata) -> bool:
|
||||
if not options["runtime"]["dryrun"]:
|
||||
for metadata_style in options["runtime"]["type"]:
|
||||
# write out the new data
|
||||
if not ca.write_metadata(md, metadata_style):
|
||||
logger.error("The tag save seemed to fail for style: %s!", MetaDataStyle.name[metadata_style])
|
||||
@ -67,7 +63,7 @@ def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: Generic
|
||||
print("Save complete.")
|
||||
logger.info("Save complete.")
|
||||
else:
|
||||
if opts.terse:
|
||||
if options["runtime"]["terse"]:
|
||||
logger.info("dry-run option was set, so nothing was written")
|
||||
print("dry-run option was set, so nothing was written")
|
||||
else:
|
||||
@ -80,8 +76,7 @@ def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: Generic
|
||||
def display_match_set_for_choice(
|
||||
label: str,
|
||||
match_set: MultipleMatch,
|
||||
opts: argparse.Namespace,
|
||||
settings: ComicTaggerSettings,
|
||||
options: settings.OptionValues,
|
||||
talker_api: ComicTalker,
|
||||
) -> None:
|
||||
print(f"{match_set.ca.path} -- {label}:")
|
||||
@ -102,7 +97,7 @@ def display_match_set_for_choice(
|
||||
m["issue_title"],
|
||||
)
|
||||
)
|
||||
if opts.interactive:
|
||||
if options["runtime"]["interactive"]:
|
||||
while True:
|
||||
i = input("Choose a match #, or 's' to skip: ")
|
||||
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
|
||||
@ -111,9 +106,9 @@ def display_match_set_for_choice(
|
||||
# save the data!
|
||||
# we know at this point, that the file is all good to go
|
||||
ca = match_set.ca
|
||||
md = create_local_metadata(opts, ca, settings)
|
||||
ct_md = actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"], settings, opts, talker_api)
|
||||
if opts.overwrite:
|
||||
md = create_local_metadata(ca, options)
|
||||
ct_md = actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"], options, talker_api)
|
||||
if options["comicvine"]["clear_metadata_on_import"]:
|
||||
md = ct_md
|
||||
else:
|
||||
notes = (
|
||||
@ -122,17 +117,17 @@ def display_match_set_for_choice(
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if opts.auto_imprint:
|
||||
if options["comicvine"]["auto_imprint"]:
|
||||
md.fix_publisher()
|
||||
|
||||
actual_metadata_save(ca, opts, md)
|
||||
actual_metadata_save(ca, options, md)
|
||||
|
||||
|
||||
def post_process_matches(
|
||||
match_results: OnlineMatchResults, opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker
|
||||
match_results: OnlineMatchResults, options: settings.OptionValues, talker_api: ComicTalker
|
||||
) -> None:
|
||||
# now go through the match results
|
||||
if opts.show_save_summary:
|
||||
if options["runtime"]["show_save_summary"]:
|
||||
if len(match_results.good_matches) > 0:
|
||||
print("\nSuccessful matches:\n------------------")
|
||||
for f in match_results.good_matches:
|
||||
@ -153,14 +148,14 @@ def post_process_matches(
|
||||
for f in match_results.fetch_data_failures:
|
||||
print(f)
|
||||
|
||||
if not opts.show_save_summary and not opts.interactive:
|
||||
if not options["runtime"]["show_save_summary"] and not options["runtime"]["interactive"]:
|
||||
# just quit if we're not interactive or showing the summary
|
||||
return
|
||||
|
||||
if len(match_results.multiple_matches) > 0:
|
||||
print("\nArchives with multiple high-confidence matches:\n------------------")
|
||||
for match_set in match_results.multiple_matches:
|
||||
display_match_set_for_choice("Multiple high-confidence matches", match_set, opts, settings, talker_api)
|
||||
display_match_set_for_choice("Multiple high-confidence matches", match_set, options, talker_api)
|
||||
|
||||
if len(match_results.low_confidence_matches) > 0:
|
||||
print("\nArchives with low-confidence matches:\n------------------")
|
||||
@ -170,40 +165,40 @@ def post_process_matches(
|
||||
else:
|
||||
label = "Multiple low-confidence matches"
|
||||
|
||||
display_match_set_for_choice(label, match_set, opts, settings, talker_api)
|
||||
display_match_set_for_choice(label, match_set, options, talker_api)
|
||||
|
||||
|
||||
def cli_mode(opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
|
||||
if len(opts.file_list) < 1:
|
||||
def cli_mode(options: settings.OptionValues, talker_api: ComicTalker) -> None:
|
||||
if len(options["runtime"]["file_list"]) < 1:
|
||||
logger.error("You must specify at least one filename. Use the -h option for more info")
|
||||
return
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
|
||||
for f in opts.file_list:
|
||||
process_file_cli(f, opts, settings, talker_api, match_results)
|
||||
for f in options["runtime"]["file_list"]:
|
||||
process_file_cli(f, options, talker_api, match_results)
|
||||
sys.stdout.flush()
|
||||
|
||||
post_process_matches(match_results, opts, settings, talker_api)
|
||||
post_process_matches(match_results, options, talker_api)
|
||||
|
||||
|
||||
def create_local_metadata(opts: argparse.Namespace, ca: ComicArchive, settings: ComicTaggerSettings) -> GenericMetadata:
|
||||
def create_local_metadata(ca: ComicArchive, options: settings.OptionValues) -> GenericMetadata:
|
||||
md = GenericMetadata()
|
||||
md.set_default_page_list(ca.get_number_of_pages())
|
||||
|
||||
# now, overlay the parsed filename info
|
||||
if opts.parse_filename:
|
||||
if options["runtime"]["parse_filename"]:
|
||||
f_md = ca.metadata_from_filename(
|
||||
settings.complicated_parser,
|
||||
settings.remove_c2c,
|
||||
settings.remove_fcbd,
|
||||
settings.remove_publisher,
|
||||
opts.split_words,
|
||||
options["filename"]["complicated_parser"],
|
||||
options["filename"]["remove_c2c"],
|
||||
options["filename"]["remove_fcbd"],
|
||||
options["filename"]["remove_publisher"],
|
||||
options["runtime"]["split_words"],
|
||||
)
|
||||
|
||||
md.overlay(f_md)
|
||||
|
||||
for metadata_style in opts.type:
|
||||
for metadata_style in options["runtime"]["type"]:
|
||||
if ca.has_metadata(metadata_style):
|
||||
try:
|
||||
t_md = ca.read_metadata(metadata_style)
|
||||
@ -213,21 +208,17 @@ def create_local_metadata(opts: argparse.Namespace, ca: ComicArchive, settings:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
# finally, use explicit stuff
|
||||
md.overlay(opts.metadata)
|
||||
md.overlay(options["runtime"]["metadata"])
|
||||
|
||||
return md
|
||||
|
||||
|
||||
def process_file_cli(
|
||||
filename: str,
|
||||
opts: argparse.Namespace,
|
||||
settings: ComicTaggerSettings,
|
||||
talker_api: ComicTalker,
|
||||
match_results: OnlineMatchResults,
|
||||
filename: str, options: settings.OptionValues, talker_api: ComicTalker, match_results: OnlineMatchResults
|
||||
) -> None:
|
||||
batch_mode = len(opts.file_list) > 1
|
||||
batch_mode = len(options["runtime"]["file_list"]) > 1
|
||||
|
||||
ca = ComicArchive(filename, settings.rar_exe_path, str(graphics_path / "nocover.png"))
|
||||
ca = ComicArchive(filename, options["general"]["rar_exe_path"], str(graphics_path / "nocover.png"))
|
||||
|
||||
if not os.path.lexists(filename):
|
||||
logger.error("Cannot find %s", filename)
|
||||
@ -237,7 +228,12 @@ def process_file_cli(
|
||||
logger.error("Sorry, but %s is not a comic archive!", filename)
|
||||
return
|
||||
|
||||
if not ca.is_writable() and (opts.delete or opts.copy or opts.save or opts.rename):
|
||||
if not ca.is_writable() and (
|
||||
options["commands"]["delete"]
|
||||
or options["commands"]["copy"]
|
||||
or options["commands"]["save"]
|
||||
or options["commands"]["rename"]
|
||||
):
|
||||
logger.error("This archive is not writable")
|
||||
return
|
||||
|
||||
@ -249,9 +245,9 @@ def process_file_cli(
|
||||
if ca.has_comet():
|
||||
has[MetaDataStyle.COMET] = True
|
||||
|
||||
if opts.print:
|
||||
if options["commands"]["print"]:
|
||||
|
||||
if not opts.type:
|
||||
if not options["runtime"]["type"]:
|
||||
page_count = ca.get_number_of_pages()
|
||||
|
||||
brief = ""
|
||||
@ -284,49 +280,49 @@ def process_file_cli(
|
||||
|
||||
print(brief)
|
||||
|
||||
if opts.terse:
|
||||
if options["runtime"]["terse"]:
|
||||
return
|
||||
|
||||
print()
|
||||
|
||||
if not opts.type or MetaDataStyle.CIX in opts.type:
|
||||
if not options["runtime"]["type"] or MetaDataStyle.CIX in options["runtime"]["type"]:
|
||||
if has[MetaDataStyle.CIX]:
|
||||
print("--------- ComicRack tags ---------")
|
||||
try:
|
||||
if opts.raw:
|
||||
if options["runtime"]["raw"]:
|
||||
print(ca.read_raw_cix())
|
||||
else:
|
||||
print(ca.read_cix())
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if not opts.type or MetaDataStyle.CBI in opts.type:
|
||||
if not options["runtime"]["type"] or MetaDataStyle.CBI in options["runtime"]["type"]:
|
||||
if has[MetaDataStyle.CBI]:
|
||||
print("------- ComicBookLover tags -------")
|
||||
try:
|
||||
if opts.raw:
|
||||
if options["runtime"]["raw"]:
|
||||
pprint(json.loads(ca.read_raw_cbi()))
|
||||
else:
|
||||
print(ca.read_cbi())
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if not opts.type or MetaDataStyle.COMET in opts.type:
|
||||
if not options["runtime"]["type"] or MetaDataStyle.COMET in options["runtime"]["type"]:
|
||||
if has[MetaDataStyle.COMET]:
|
||||
print("----------- CoMet tags -----------")
|
||||
try:
|
||||
if opts.raw:
|
||||
if options["runtime"]["raw"]:
|
||||
print(ca.read_raw_comet())
|
||||
else:
|
||||
print(ca.read_comet())
|
||||
except Exception as e:
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
elif opts.delete:
|
||||
for metadata_style in opts.type:
|
||||
elif options["commands"]["delete"]:
|
||||
for metadata_style in options["runtime"]["type"]:
|
||||
style_name = MetaDataStyle.name[metadata_style]
|
||||
if has[metadata_style]:
|
||||
if not opts.dryrun:
|
||||
if not options["runtime"]["dryrun"]:
|
||||
if not ca.remove_metadata(metadata_style):
|
||||
print(f"{filename}: Tag removal seemed to fail!")
|
||||
else:
|
||||
@ -336,27 +332,27 @@ def process_file_cli(
|
||||
else:
|
||||
print(f"{filename}: This archive doesn't have {style_name} tags to remove.")
|
||||
|
||||
elif opts.copy is not None:
|
||||
for metadata_style in opts.type:
|
||||
elif options["commands"]["copy"] is not None:
|
||||
for metadata_style in options["runtime"]["type"]:
|
||||
dst_style_name = MetaDataStyle.name[metadata_style]
|
||||
if opts.no_overwrite and has[metadata_style]:
|
||||
if options["runtime"]["no_overwrite"] and has[metadata_style]:
|
||||
print(f"{filename}: Already has {dst_style_name} tags. Not overwriting.")
|
||||
return
|
||||
if opts.copy == metadata_style:
|
||||
if options["commands"]["copy"] == metadata_style:
|
||||
print(f"{filename}: Destination and source are same: {dst_style_name}. Nothing to do.")
|
||||
return
|
||||
|
||||
src_style_name = MetaDataStyle.name[opts.copy]
|
||||
if has[opts.copy]:
|
||||
if not opts.dryrun:
|
||||
src_style_name = MetaDataStyle.name[options["commands"]["copy"]]
|
||||
if has[options["commands"]["copy"]]:
|
||||
if not options["runtime"]["dryrun"]:
|
||||
try:
|
||||
md = ca.read_metadata(opts.copy)
|
||||
md = ca.read_metadata(options["commands"]["copy"])
|
||||
except Exception as e:
|
||||
md = GenericMetadata()
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation and metadata_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer(md, settings).apply()
|
||||
if options["apply_cbl_transform_on_bulk_operation"] and metadata_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer(md, options).apply()
|
||||
|
||||
if not ca.write_metadata(md, metadata_style):
|
||||
print(f"{filename}: Tag copy seemed to fail!")
|
||||
@ -367,10 +363,10 @@ def process_file_cli(
|
||||
else:
|
||||
print(f"{filename}: This archive doesn't have {src_style_name} tags to copy.")
|
||||
|
||||
elif opts.save:
|
||||
elif options["commands"]["save"]:
|
||||
|
||||
if opts.no_overwrite:
|
||||
for metadata_style in opts.type:
|
||||
if options["runtime"]["no_overwrite"]:
|
||||
for metadata_style in options["runtime"]["type"]:
|
||||
if has[metadata_style]:
|
||||
print(f"{filename}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
|
||||
return
|
||||
@ -378,39 +374,39 @@ def process_file_cli(
|
||||
if batch_mode:
|
||||
print(f"Processing {ca.path}...")
|
||||
|
||||
md = create_local_metadata(opts, ca, settings)
|
||||
md = create_local_metadata(ca, options)
|
||||
if md.issue is None or md.issue == "":
|
||||
if opts.assume_issue_one:
|
||||
if options["runtime"]["assume_issue_one"]:
|
||||
md.issue = "1"
|
||||
|
||||
# now, search online
|
||||
if opts.online:
|
||||
if opts.issue_id is not None:
|
||||
if options["runtime"]["online"]:
|
||||
if options["runtime"]["issue_id"] is not None:
|
||||
# we were given the actual issue ID to search with
|
||||
try:
|
||||
ct_md = talker_api.fetch_comic_data(opts.issue_id)
|
||||
ct_md = talker_api.fetch_comic_data(options["runtime"]["issue_id"])
|
||||
except TalkerError as e:
|
||||
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
|
||||
match_results.fetch_data_failures.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
if ct_md is None:
|
||||
logger.error("No match for ID %s was found.", opts.issue_id)
|
||||
logger.error("No match for ID %s was found.", options["runtime"]["issue_id"])
|
||||
match_results.no_matches.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
if settings.apply_cbl_transform_on_cv_import:
|
||||
ct_md = CBLTransformer(ct_md, settings).apply()
|
||||
if options["cbl"]["apply_cbl_transform_on_cv_import"]:
|
||||
ct_md = CBLTransformer(ct_md, options).apply()
|
||||
else:
|
||||
if md is None or md.is_empty:
|
||||
logger.error("No metadata given to search online with!")
|
||||
match_results.no_matches.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
ii = IssueIdentifier(ca, settings, talker_api)
|
||||
ii = IssueIdentifier(ca, options, talker_api)
|
||||
|
||||
def myoutput(text: str) -> None:
|
||||
if opts.verbose:
|
||||
if options["runtime"]["verbose"]:
|
||||
IssueIdentifier.default_write_output(text)
|
||||
|
||||
# use our overlaid MD struct to search
|
||||
@ -450,7 +446,7 @@ def process_file_cli(
|
||||
logger.error("Online search: Multiple good matches. Save aborted")
|
||||
match_results.multiple_matches.append(MultipleMatch(ca, matches))
|
||||
return
|
||||
if low_confidence and opts.abort_on_low_confidence:
|
||||
if low_confidence and options["runtime"]["abort_on_low_confidence"]:
|
||||
logger.error("Online search: Low confidence match. Save aborted")
|
||||
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
|
||||
return
|
||||
@ -462,12 +458,12 @@ def process_file_cli(
|
||||
# we got here, so we have a single match
|
||||
|
||||
# now get the particular issue data
|
||||
ct_md = actual_issue_data_fetch(matches[0]["issue_id"], settings, opts, talker_api)
|
||||
ct_md = actual_issue_data_fetch(matches[0]["issue_id"], options, talker_api)
|
||||
if ct_md.is_empty:
|
||||
match_results.fetch_data_failures.append(str(ca.path.absolute()))
|
||||
return
|
||||
|
||||
if opts.overwrite:
|
||||
if options["comicvine"]["clear_metadata_on_import"]:
|
||||
md = ct_md
|
||||
else:
|
||||
notes = (
|
||||
@ -476,29 +472,29 @@ def process_file_cli(
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if opts.auto_imprint:
|
||||
if options["comicvine"]["auto_imprint"]:
|
||||
md.fix_publisher()
|
||||
|
||||
# ok, done building our metadata. time to save
|
||||
if not actual_metadata_save(ca, opts, md):
|
||||
if not actual_metadata_save(ca, options, md):
|
||||
match_results.write_failures.append(str(ca.path.absolute()))
|
||||
else:
|
||||
match_results.good_matches.append(str(ca.path.absolute()))
|
||||
|
||||
elif opts.rename:
|
||||
elif options["commands"]["rename"]:
|
||||
original_path = ca.path
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = f"{ca.path}: "
|
||||
|
||||
md = create_local_metadata(opts, ca, settings)
|
||||
md = create_local_metadata(ca, options)
|
||||
|
||||
if md.series is None:
|
||||
logger.error(msg_hdr + "Can't rename without series name")
|
||||
return
|
||||
|
||||
new_ext = "" # default
|
||||
if settings.rename_extension_based_on_archive:
|
||||
if options["filename"]["rename_set_extension_based_on_archive"]:
|
||||
if ca.is_sevenzip():
|
||||
new_ext = ".cb7"
|
||||
elif ca.is_zip():
|
||||
@ -506,11 +502,11 @@ def process_file_cli(
|
||||
elif ca.is_rar():
|
||||
new_ext = ".cbr"
|
||||
|
||||
renamer = FileRenamer(md, platform="universal" if settings.rename_strict else "auto")
|
||||
renamer.set_template(settings.rename_template)
|
||||
renamer.set_issue_zero_padding(settings.rename_issue_number_padding)
|
||||
renamer.set_smart_cleanup(settings.rename_use_smart_string_cleanup)
|
||||
renamer.move = settings.rename_move_dir
|
||||
renamer = FileRenamer(md, platform="universal" if options[filename]["rename_strict"] else "auto")
|
||||
renamer.set_template(options[filename]["rename_template"])
|
||||
renamer.set_issue_zero_padding(options[filename]["rename_issue_number_padding"])
|
||||
renamer.set_smart_cleanup(options[filename]["rename_use_smart_string_cleanup"])
|
||||
renamer.move = options[filename]["rename_move_to_dir"]
|
||||
|
||||
try:
|
||||
new_name = renamer.determine_name(ext=new_ext)
|
||||
@ -522,13 +518,17 @@ def process_file_cli(
|
||||
"Please consult the template help in the settings "
|
||||
"and the documentation on the format at "
|
||||
"https://docs.python.org/3/library/string.html#format-string-syntax",
|
||||
settings.rename_template,
|
||||
options[filename]["rename_template"],
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
logger.exception("Formatter failure: %s metadata: %s", settings.rename_template, renamer.metadata)
|
||||
logger.exception(
|
||||
"Formatter failure: %s metadata: %s", options[filename]["rename_template"], renamer.metadata
|
||||
)
|
||||
|
||||
folder = get_rename_dir(ca, settings.rename_dir if settings.rename_move_dir else None)
|
||||
folder = get_rename_dir(
|
||||
ca, options[filename]["rename_dir"] if options[filename]["rename_move_to_dir"] else None
|
||||
)
|
||||
|
||||
full_path = folder / new_name
|
||||
|
||||
@ -537,7 +537,7 @@ def process_file_cli(
|
||||
return
|
||||
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
if not options["runtime"]["dryrun"]:
|
||||
# rename the file
|
||||
try:
|
||||
ca.rename(utils.unique_file(full_path))
|
||||
@ -548,7 +548,7 @@ def process_file_cli(
|
||||
|
||||
print(f"renamed '{original_path.name}' -> '{new_name}' {suffix}")
|
||||
|
||||
elif opts.export_to_zip:
|
||||
elif options["commands"]["export_to_zip"]:
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = f"{ca.path}: "
|
||||
@ -560,7 +560,7 @@ def process_file_cli(
|
||||
filename_path = pathlib.Path(filename).absolute()
|
||||
new_file = filename_path.with_suffix(".cbz")
|
||||
|
||||
if opts.abort_on_conflict and new_file.exists():
|
||||
if options["runtime"]["abort_on_conflict"] and new_file.exists():
|
||||
print(msg_hdr + f"{new_file.name} already exists in the that folder.")
|
||||
return
|
||||
|
||||
@ -568,10 +568,10 @@ def process_file_cli(
|
||||
|
||||
delete_success = False
|
||||
export_success = False
|
||||
if not opts.dryrun:
|
||||
if not options["runtime"]["dryrun"]:
|
||||
if ca.export_as_zip(new_file):
|
||||
export_success = True
|
||||
if opts.delete_after_zip_export:
|
||||
if options["runtime"]["delete_after_zip_export"]:
|
||||
try:
|
||||
filename_path.unlink(missing_ok=True)
|
||||
delete_success = True
|
||||
@ -583,7 +583,7 @@ def process_file_cli(
|
||||
new_file.unlink(missing_ok=True)
|
||||
else:
|
||||
msg = msg_hdr + f"Dry-run: Would try to create {os.path.split(new_file)[1]}"
|
||||
if opts.delete_after_zip_export:
|
||||
if options["runtime"]["delete_after_zip_export"]:
|
||||
msg += " and delete original."
|
||||
print(msg)
|
||||
return
|
||||
@ -591,7 +591,7 @@ def process_file_cli(
|
||||
msg = msg_hdr
|
||||
if export_success:
|
||||
msg += f"Archive exported successfully to: {os.path.split(new_file)[1]}"
|
||||
if opts.delete_after_zip_export and delete_success:
|
||||
if options["runtime"]["delete_after_zip_export"] and delete_success:
|
||||
msg += " (Original deleted) "
|
||||
else:
|
||||
msg += "Archive failed to export!"
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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)
|
||||
|
@ -24,9 +24,9 @@ from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
from comictaggerlib.optionalmsgdialog import OptionalMessageDialog
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.settingswindow import linuxRarHelp, macRarHelp, windowsRarHelp
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
|
||||
@ -59,14 +59,14 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
def __init__(
|
||||
self,
|
||||
parent: QtWidgets.QWidget,
|
||||
settings: ComicTaggerSettings,
|
||||
options: settings.OptionValues,
|
||||
dirty_flag_verification: Callable[[str, str], bool],
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ui_path / "fileselectionlist.ui", self)
|
||||
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
|
||||
reduce_widget_font_size(self.twList)
|
||||
|
||||
@ -227,7 +227,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "File/Folder Open", "No readable comic archives were found.")
|
||||
|
||||
if rar_added and not utils.which(self.settings.rar_exe_path or "rar"):
|
||||
if rar_added and not utils.which(self.options["general"]["rar_exe_path"] or "rar"):
|
||||
self.rar_ro_message()
|
||||
|
||||
self.twList.setSortingEnabled(True)
|
||||
@ -281,7 +281,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
if self.is_list_dupe(path):
|
||||
return self.get_current_list_row(path)
|
||||
|
||||
ca = ComicArchive(path, self.settings.rar_exe_path, str(graphics_path / "nocover.png"))
|
||||
ca = ComicArchive(path, self.options["general"]["rar_exe_path"], str(graphics_path / "nocover.png"))
|
||||
|
||||
if ca.seems_to_be_a_comic_archive():
|
||||
row: int = self.twList.rowCount()
|
||||
|
143
comictaggerlib/gui.py
Normal file
143
comictaggerlib/gui.py
Normal file
@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging.handlers
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
from comictalker.talkerbase import ComicTalker
|
||||
|
||||
logger = logging.getLogger("comictagger")
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
def show_exception_box(log_msg: str) -> None:
|
||||
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
|
||||
If unavailable (non-console application), log an additional notice.
|
||||
"""
|
||||
if QtWidgets.QApplication.instance() is not None:
|
||||
errorbox = QtWidgets.QMessageBox()
|
||||
errorbox.setText(log_msg)
|
||||
errorbox.exec()
|
||||
QtWidgets.QApplication.exit(1)
|
||||
else:
|
||||
logger.debug("No QApplication instance available.")
|
||||
|
||||
class UncaughtHook(QtCore.QObject):
|
||||
_exception_caught = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
# this registers the exception_hook() function as hook with the Python interpreter
|
||||
sys.excepthook = self.exception_hook
|
||||
|
||||
# connect signal to execute the message box function always on main thread
|
||||
self._exception_caught.connect(show_exception_box)
|
||||
|
||||
def exception_hook(
|
||||
self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: types.TracebackType | None
|
||||
) -> None:
|
||||
"""Function handling uncaught exceptions.
|
||||
It is triggered each time an uncaught exception occurs.
|
||||
"""
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
# ignore keyboard interrupt to support console applications
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
else:
|
||||
exc_info = (exc_type, exc_value, exc_traceback)
|
||||
trace_back = "".join(traceback.format_tb(exc_traceback))
|
||||
log_msg = f"{exc_type.__name__}: {exc_value}\n\n{trace_back}"
|
||||
logger.critical("Uncaught exception: %s: %s", exc_type.__name__, exc_value, exc_info=exc_info)
|
||||
|
||||
# trigger message box show
|
||||
self._exception_caught.emit(f"Oops. An unexpected error occurred:\n{log_msg}")
|
||||
|
||||
qt_exception_hook = UncaughtHook()
|
||||
from comictaggerlib.taggerwindow import TaggerWindow
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
openFileRequest = QtCore.pyqtSignal(QtCore.QUrl, name="openfileRequest")
|
||||
|
||||
# Handles "Open With" from Finder on macOS
|
||||
def event(self, event: QtCore.QEvent) -> bool:
|
||||
if event.type() == QtCore.QEvent.FileOpen:
|
||||
logger.info(event.url().toLocalFile())
|
||||
self.openFileRequest.emit(event.url())
|
||||
return True
|
||||
return super().event(event)
|
||||
|
||||
except ImportError as e:
|
||||
|
||||
def show_exception_box(log_msg: str) -> None:
|
||||
...
|
||||
|
||||
logger.error(str(e))
|
||||
qt_available = False
|
||||
|
||||
|
||||
def open_tagger_window(
|
||||
talker_api: ComicTalker, options: settings.OptionValues, gui_exception: Exception | None
|
||||
) -> None:
|
||||
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
|
||||
args = []
|
||||
if options["runtime"]["darkmode"]:
|
||||
args.extend(["-platform", "windows:darkmode=2"])
|
||||
args.extend(sys.argv)
|
||||
app = Application(args)
|
||||
if gui_exception is not None:
|
||||
trace_back = "".join(traceback.format_tb(gui_exception.__traceback__))
|
||||
log_msg = f"{type(gui_exception).__name__}: {gui_exception}\n\n{trace_back}"
|
||||
show_exception_box(f"{log_msg}")
|
||||
raise SystemExit(1)
|
||||
|
||||
# needed to catch initial open file events (macOS)
|
||||
app.openFileRequest.connect(lambda x: options["runtime"]["files"].append(x.toLocalFile()))
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
# Set the MacOS dock icon
|
||||
app.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# For pure python, tell windows that we're not python,
|
||||
# so we can have our own taskbar icon
|
||||
import ctypes
|
||||
|
||||
myappid = "comictagger" # arbitrary string
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore[attr-defined]
|
||||
# force close of console window
|
||||
swp_hidewindow = 0x0080
|
||||
console_wnd = ctypes.windll.kernel32.GetConsoleWindow() # type: ignore[attr-defined]
|
||||
if console_wnd != 0:
|
||||
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow) # type: ignore[attr-defined]
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(str(graphics_path / "tags.png"))
|
||||
|
||||
splash = QtWidgets.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow(options["runtime"]["files"], options, talker_api)
|
||||
tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
tagger_window.show()
|
||||
|
||||
# Catch open file events (macOS)
|
||||
app.openFileRequest.connect(tagger_window.open_file_event)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish(tagger_window)
|
||||
|
||||
sys.exit(app.exec())
|
||||
except Exception:
|
||||
logger.exception("GUI mode failed")
|
||||
QtWidgets.QMessageBox.critical(
|
||||
QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc()
|
||||
)
|
@ -18,6 +18,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import sqlite3 as lite
|
||||
import tempfile
|
||||
@ -25,7 +26,6 @@ import tempfile
|
||||
import requests
|
||||
|
||||
from comictaggerlib import ctversion
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
try:
|
||||
from PyQt5 import QtCore, QtNetwork
|
||||
@ -49,11 +49,10 @@ class ImageFetcher:
|
||||
|
||||
image_fetch_complete = fetch_complete
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, cache_folder: pathlib.Path) -> None:
|
||||
|
||||
self.settings_folder = ComicTaggerSettings.get_settings_folder()
|
||||
self.db_file = os.path.join(self.settings_folder, "image_url_cache.db")
|
||||
self.cache_folder = os.path.join(self.settings_folder, "image_cache")
|
||||
self.db_file = cache_folder / "image_url_cache.db"
|
||||
self.cache_folder = cache_folder / "image_cache"
|
||||
|
||||
self.user_data = None
|
||||
self.fetched_url = ""
|
||||
|
0
comictaggerlib/imagehasher.py
Executable file → Normal file
0
comictaggerlib/imagehasher.py
Executable file → Normal file
@ -26,11 +26,10 @@ from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.imagefetcher import ImageFetcher, ImageFetcherException
|
||||
from comictaggerlib.imagehasher import ImageHasher
|
||||
from comictaggerlib.resulttypes import IssueResult
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictalker.talker_utils import parse_date_str
|
||||
from comictalker.talkerbase import ComicTalker, TalkerError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -73,8 +72,8 @@ class IssueIdentifier:
|
||||
result_one_good_match = 4
|
||||
result_multiple_good_matches = 5
|
||||
|
||||
def __init__(self, comic_archive: ComicArchive, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
|
||||
self.settings = settings
|
||||
def __init__(self, comic_archive: ComicArchive, options: settings.OptionValues, talker_api: ComicTalker) -> None:
|
||||
self.options = options
|
||||
self.talker_api = talker_api
|
||||
self.comic_archive: ComicArchive = comic_archive
|
||||
self.image_hasher = 1
|
||||
@ -97,10 +96,10 @@ class IssueIdentifier:
|
||||
|
||||
# used to eliminate series names that are too long based on our search
|
||||
# string
|
||||
self.series_match_thresh = settings.id_series_match_identify_thresh
|
||||
self.series_match_thresh = options["identifier"]["series_match_identify_thresh"]
|
||||
|
||||
# used to eliminate unlikely publishers
|
||||
self.publisher_filter = [s.strip().casefold() for s in settings.id_publisher_filter.split(",")]
|
||||
self.publisher_filter = [s.strip().casefold() for s in options["identifier"]["publisher_filter"]]
|
||||
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output
|
||||
@ -202,10 +201,10 @@ class IssueIdentifier:
|
||||
|
||||
# try to get some metadata from filename
|
||||
md_from_filename = ca.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
self.options["filename"]["complicated_parser"],
|
||||
self.options["filename"]["remove_c2c"],
|
||||
self.options["filename"]["remove_fcbd"],
|
||||
self.options["filename"]["remove_publisher"],
|
||||
)
|
||||
|
||||
working_md = md_from_filename.copy()
|
||||
@ -256,7 +255,9 @@ class IssueIdentifier:
|
||||
return Score(score=0, url="", hash=0)
|
||||
|
||||
try:
|
||||
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
|
||||
url_image_data = ImageFetcher(self.options["runtime"]["config"].user_cache_dir).fetch(
|
||||
primary_thumb_url, blocking=True
|
||||
)
|
||||
except ImageFetcherException as e:
|
||||
self.log_msg("Network issue while fetching cover image from Comic Vine. Aborting...")
|
||||
raise IssueIdentifierNetworkError from e
|
||||
@ -276,7 +277,9 @@ class IssueIdentifier:
|
||||
if use_remote_alternates:
|
||||
for alt_url in alt_urls:
|
||||
try:
|
||||
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
|
||||
alt_url_image_data = ImageFetcher(self.options["runtime"]["config"].user_cache_dir).fetch(
|
||||
alt_url, blocking=True
|
||||
)
|
||||
except ImageFetcherException as e:
|
||||
self.log_msg("Network issue while fetching alt. cover image from Comic Vine. Aborting...")
|
||||
raise IssueIdentifierNetworkError from e
|
||||
@ -466,7 +469,7 @@ class IssueIdentifier:
|
||||
)
|
||||
|
||||
# parse out the cover date
|
||||
_, month, year = parse_date_str(issue["cover_date"])
|
||||
_, month, year = utils.parse_date_str(issue["cover_date"])
|
||||
|
||||
# Now check the cover match against the primary image
|
||||
hash_list = [cover_hash]
|
||||
@ -487,6 +490,7 @@ class IssueIdentifier:
|
||||
use_remote_alternates=False,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Scoring series failed")
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
|
@ -20,8 +20,8 @@ import logging
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
from comictalker.resulttypes import ComicIssue
|
||||
@ -44,7 +44,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent: QtWidgets.QWidget,
|
||||
settings: ComicTaggerSettings,
|
||||
options: settings.OptionValues,
|
||||
talker_api: ComicTalker,
|
||||
series_id: int,
|
||||
issue_number: str,
|
||||
@ -53,7 +53,12 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
uic.loadUi(ui_path / "issueselectionwindow.ui", self)
|
||||
|
||||
self.coverWidget = CoverImageWidget(self.coverImageContainer, talker_api, CoverImageWidget.AltCoverMode)
|
||||
self.coverWidget = CoverImageWidget(
|
||||
self.coverImageContainer,
|
||||
CoverImageWidget.AltCoverMode,
|
||||
options["runtime"]["config"].user_cache_dir,
|
||||
talker_api,
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
|
||||
gridlayout.addWidget(self.coverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
@ -71,7 +76,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
self.series_id = series_id
|
||||
self.issue_id: int | None = None
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
self.talker_api = talker_api
|
||||
self.url_fetch_thread = None
|
||||
self.issue_list: list[ComicIssue] = []
|
||||
|
42
comictaggerlib/log.py
Normal file
42
comictaggerlib/log.py
Normal file
@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging.handlers
|
||||
import pathlib
|
||||
|
||||
logger = logging.getLogger("comictagger")
|
||||
|
||||
|
||||
def get_file_handler(filename: pathlib.Path) -> logging.FileHandler:
|
||||
file_handler = logging.handlers.RotatingFileHandler(filename, encoding="utf-8", backupCount=10)
|
||||
|
||||
if filename.is_file() and filename.stat().st_size > 0:
|
||||
file_handler.doRollover()
|
||||
return file_handler
|
||||
|
||||
|
||||
def setup_logging(verbose: int, log_dir: pathlib.Path) -> None:
|
||||
logging.getLogger("comicapi").setLevel(logging.DEBUG)
|
||||
logging.getLogger("comictaggerlib").setLevel(logging.DEBUG)
|
||||
|
||||
log_file = log_dir / "ComicTagger.log"
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
file_handler = get_file_handler(log_file)
|
||||
|
||||
if verbose > 1:
|
||||
stream_handler.setLevel(logging.DEBUG)
|
||||
elif verbose > 0:
|
||||
stream_handler.setLevel(logging.INFO)
|
||||
else:
|
||||
stream_handler.setLevel(logging.WARNING)
|
||||
|
||||
logging.basicConfig(
|
||||
handlers=[
|
||||
stream_handler,
|
||||
file_handler,
|
||||
],
|
||||
level=logging.WARNING,
|
||||
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
)
|
336
comictaggerlib/main.py
Executable file → Normal file
336
comictaggerlib/main.py
Executable file → Normal file
@ -15,23 +15,20 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging.handlers
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import pprint
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
import comictalker.comictalkerapi as ct_api
|
||||
from comicapi import utils
|
||||
from comictaggerlib import cli
|
||||
from comictaggerlib import cli, settings
|
||||
from comictaggerlib.ctversion import version
|
||||
from comictaggerlib.graphics import graphics_path
|
||||
from comictaggerlib.options import parse_cmd_line
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.log import setup_logging
|
||||
from comictalker.talkerbase import TalkerError
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
@ -39,235 +36,138 @@ if sys.version_info < (3, 10):
|
||||
else:
|
||||
import importlib.metadata as importlib_metadata
|
||||
|
||||
logger = logging.getLogger("comictagger")
|
||||
logging.getLogger("comicapi").setLevel(logging.DEBUG)
|
||||
logging.getLogger("comictaggerlib").setLevel(logging.DEBUG)
|
||||
logging.getLogger("comictalker").setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
try:
|
||||
qt_available = True
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from comictaggerlib import gui
|
||||
|
||||
def show_exception_box(log_msg: str) -> None:
|
||||
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
|
||||
If unavailable (non-console application), log an additional notice.
|
||||
"""
|
||||
if QtWidgets.QApplication.instance() is not None:
|
||||
errorbox = QtWidgets.QMessageBox()
|
||||
errorbox.setText(log_msg)
|
||||
errorbox.exec()
|
||||
QtWidgets.QApplication.exit(1)
|
||||
else:
|
||||
logger.debug("No QApplication instance available.")
|
||||
|
||||
class UncaughtHook(QtCore.QObject):
|
||||
_exception_caught = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
# this registers the exception_hook() function as hook with the Python interpreter
|
||||
sys.excepthook = self.exception_hook
|
||||
|
||||
# connect signal to execute the message box function always on main thread
|
||||
self._exception_caught.connect(show_exception_box)
|
||||
|
||||
def exception_hook(
|
||||
self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: types.TracebackType | None
|
||||
) -> None:
|
||||
"""Function handling uncaught exceptions.
|
||||
It is triggered each time an uncaught exception occurs.
|
||||
"""
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
# ignore keyboard interrupt to support console applications
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
else:
|
||||
exc_info = (exc_type, exc_value, exc_traceback)
|
||||
trace_back = "".join(traceback.format_tb(exc_traceback))
|
||||
log_msg = f"{exc_type.__name__}: {exc_value}\n\n{trace_back}"
|
||||
logger.critical("Uncaught exception: %s: %s", exc_type.__name__, exc_value, exc_info=exc_info)
|
||||
|
||||
# trigger message box show
|
||||
self._exception_caught.emit(f"Oops. An unexpected error occurred:\n{log_msg}")
|
||||
|
||||
qt_exception_hook = UncaughtHook()
|
||||
from comictaggerlib.taggerwindow import TaggerWindow
|
||||
|
||||
class Application(QtWidgets.QApplication):
|
||||
openFileRequest = QtCore.pyqtSignal(QtCore.QUrl, name="openfileRequest")
|
||||
|
||||
def event(self, event: QtCore.QEvent) -> bool:
|
||||
if event.type() == QtCore.QEvent.FileOpen:
|
||||
logger.info(event.url().toLocalFile())
|
||||
self.openFileRequest.emit(event.url())
|
||||
return True
|
||||
return super().event(event)
|
||||
|
||||
except ImportError as e:
|
||||
|
||||
def show_exception_box(log_msg: str) -> None:
|
||||
...
|
||||
|
||||
logger.error(str(e))
|
||||
qt_available = gui.qt_available
|
||||
except Exception:
|
||||
qt_available = False
|
||||
|
||||
|
||||
def rotate(handler: logging.handlers.RotatingFileHandler, filename: pathlib.Path) -> None:
|
||||
if filename.is_file() and filename.stat().st_size > 0:
|
||||
handler.doRollover()
|
||||
logger = logging.getLogger("comictagger")
|
||||
|
||||
|
||||
def update_publishers() -> None:
|
||||
json_file = ComicTaggerSettings.get_settings_folder() / "publishers.json"
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def update_publishers(options: dict[str, dict[str, Any]]) -> None:
|
||||
json_file = options["runtime"]["config"].user_config_dir / "publishers.json"
|
||||
if json_file.exists():
|
||||
try:
|
||||
utils.update_publishers(json.loads(json_file.read_text("utf-8")))
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Failed to load publishers from %s", json_file)
|
||||
show_exception_box(str(e))
|
||||
# show_exception_box(str(e))
|
||||
|
||||
|
||||
def ctmain() -> None:
|
||||
opts = parse_cmd_line()
|
||||
settings = ComicTaggerSettings(opts.config_path)
|
||||
class App:
|
||||
"""docstring for App"""
|
||||
|
||||
os.makedirs(ComicTaggerSettings.get_settings_folder() / "logs", exist_ok=True)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(logging.WARNING)
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log", encoding="utf-8", backupCount=10
|
||||
)
|
||||
rotate(file_handler, ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log")
|
||||
logging.basicConfig(
|
||||
handlers=[
|
||||
stream_handler,
|
||||
file_handler,
|
||||
],
|
||||
level=logging.WARNING,
|
||||
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
)
|
||||
def __init__(self) -> None:
|
||||
self.options: dict[str, dict[str, Any]] = {}
|
||||
self.initial_arg_parser = settings.initial_cmd_line_parser()
|
||||
|
||||
if settings.settings_warning < 4:
|
||||
print( # noqa: T201
|
||||
"""
|
||||
!!!Warning!!!
|
||||
The next release will save settings in a different format
|
||||
NO SETTINGS WILL BE TRANSFERED to the new version.
|
||||
See https://github.com/comictagger/comictagger/releases/1.5.5 for more information.
|
||||
""",
|
||||
file=sys.stderr,
|
||||
)
|
||||
def run(self) -> None:
|
||||
opts = self.initialize()
|
||||
self.register_options()
|
||||
self.parse_options(opts.config)
|
||||
self.initialize_dirs()
|
||||
|
||||
# manage the CV API key
|
||||
# None comparison is used so that the empty string can unset the value
|
||||
if opts.cv_api_key is not None or opts.cv_url is not None:
|
||||
settings.cv_api_key = opts.cv_api_key if opts.cv_api_key is not None else settings.cv_api_key
|
||||
settings.cv_url = opts.cv_url if opts.cv_url is not None else settings.cv_url
|
||||
settings.save()
|
||||
if opts.only_set_cv_key:
|
||||
print("Key set") # noqa: T201
|
||||
return
|
||||
self.ctmain()
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
def initialize(self) -> argparse.Namespace:
|
||||
opts, _ = self.initial_arg_parser.parse_known_args()
|
||||
assert opts is not None
|
||||
setup_logging(opts.verbose, opts.config.user_log_dir / "log")
|
||||
return opts
|
||||
|
||||
logger.info(
|
||||
"ComicTagger Version: %s running on: %s PyInstaller: %s",
|
||||
version,
|
||||
platform.system(),
|
||||
"Yes" if getattr(sys, "frozen", None) else "No",
|
||||
)
|
||||
def register_options(self) -> None:
|
||||
self.manager = settings.Manager()
|
||||
settings.register_commandline(self.manager)
|
||||
settings.register_settings(self.manager)
|
||||
|
||||
logger.debug("Installed Packages")
|
||||
for pkg in sorted(importlib_metadata.distributions(), key=lambda x: x.name):
|
||||
logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"])
|
||||
def parse_options(self, config_paths: settings.ComicTaggerPaths) -> None:
|
||||
options = self.manager.parse_options(config_paths)
|
||||
self.options = settings.validate_commandline_options(options, self.manager)
|
||||
self.options = settings.validate_settings(options, self.manager)
|
||||
|
||||
if not qt_available and not opts.no_gui:
|
||||
opts.no_gui = True
|
||||
logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
|
||||
def initialize_dirs(self) -> None:
|
||||
self.options["runtime"]["config"].user_data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.options["runtime"]["config"].user_config_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.options["runtime"]["config"].user_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.options["runtime"]["config"].user_state_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.options["runtime"]["config"].user_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.debug("user_data_dir: %s", self.options["runtime"]["config"].user_data_dir)
|
||||
logger.debug("user_config_dir: %s", self.options["runtime"]["config"].user_config_dir)
|
||||
logger.debug("user_cache_dir: %s", self.options["runtime"]["config"].user_cache_dir)
|
||||
logger.debug("user_state_dir: %s", self.options["runtime"]["config"].user_state_dir)
|
||||
logger.debug("user_log_dir: %s", self.options["runtime"]["config"].user_log_dir)
|
||||
|
||||
talker_exception = None
|
||||
try:
|
||||
talker_api = ct_api.get_comic_talker("comicvine")( # type: ignore[call-arg]
|
||||
version=version,
|
||||
cache_folder=ComicTaggerSettings.get_settings_folder(),
|
||||
series_match_thresh=settings.id_series_match_search_thresh,
|
||||
remove_html_tables=settings.remove_html_tables,
|
||||
use_series_start_as_volume=settings.use_series_start_as_volume,
|
||||
wait_on_ratelimit=settings.wait_and_retry_on_rate_limit,
|
||||
api_url=settings.cv_url,
|
||||
api_key=settings.cv_api_key,
|
||||
)
|
||||
except TalkerError as e:
|
||||
logger.exception("Unable to load talker")
|
||||
talker_exception = e
|
||||
if opts.no_gui:
|
||||
raise SystemExit(1)
|
||||
def ctmain(self) -> None:
|
||||
assert self.options is not None
|
||||
# options already loaded
|
||||
|
||||
utils.load_publishers()
|
||||
update_publishers()
|
||||
|
||||
if opts.no_gui:
|
||||
try:
|
||||
cli.cli_mode(opts, settings, talker_api)
|
||||
except Exception:
|
||||
logger.exception("CLI mode failed")
|
||||
else:
|
||||
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
|
||||
args = []
|
||||
if opts.darkmode:
|
||||
args.extend(["-platform", "windows:darkmode=2"])
|
||||
args.extend(sys.argv)
|
||||
app = Application(args)
|
||||
if talker_exception is not None:
|
||||
trace_back = "".join(traceback.format_tb(talker_exception.__traceback__))
|
||||
log_msg = f"{type(talker_exception).__name__}: {talker_exception}\n\n{trace_back}"
|
||||
show_exception_box(f"Unable to load talker: {log_msg}")
|
||||
raise SystemExit(1)
|
||||
|
||||
# needed to catch initial open file events (macOS)
|
||||
app.openFileRequest.connect(lambda x: opts.files.append(x.toLocalFile()))
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
# Set the MacOS dock icon
|
||||
app.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# For pure python, tell windows that we're not python,
|
||||
# so we can have our own taskbar icon
|
||||
import ctypes
|
||||
|
||||
myappid = "comictagger" # arbitrary string
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore[attr-defined]
|
||||
# force close of console window
|
||||
swp_hidewindow = 0x0080
|
||||
console_wnd = ctypes.windll.kernel32.GetConsoleWindow() # type: ignore[attr-defined]
|
||||
if console_wnd != 0:
|
||||
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow) # type: ignore[attr-defined]
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(str(graphics_path / "tags.png"))
|
||||
|
||||
splash = QtWidgets.QSplashScreen(img)
|
||||
splash.show()
|
||||
splash.raise_()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
try:
|
||||
tagger_window = TaggerWindow(opts.files, settings, talker_api, opts=opts)
|
||||
tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
tagger_window.show()
|
||||
|
||||
# Catch open file events (macOS)
|
||||
app.openFileRequest.connect(tagger_window.open_file_event)
|
||||
|
||||
if platform.system() != "Linux":
|
||||
splash.finish(tagger_window)
|
||||
|
||||
sys.exit(app.exec())
|
||||
except Exception:
|
||||
logger.exception("GUI mode failed")
|
||||
QtWidgets.QMessageBox.critical(
|
||||
QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc()
|
||||
# manage the CV API key
|
||||
# None comparison is used so that the empty string can unset the value
|
||||
if self.options["comicvine"]["cv_api_key"] is not None or self.options["comicvine"]["cv_url"] is not None:
|
||||
self.options["comicvine"]["cv_api_key"] = (
|
||||
self.options["comicvine"]["cv_api_key"]
|
||||
if self.options["comicvine"]["cv_api_key"] is not None
|
||||
else self.options["comicvine"]["cv_api_key"]
|
||||
)
|
||||
self.options["comicvine"]["cv_url"] = (
|
||||
self.options["comicvine"]["cv_url"]
|
||||
if self.options["comicvine"]["cv_url"] is not None
|
||||
else self.options["comicvine"]["cv_url"]
|
||||
)
|
||||
self.manager.save_file(self.options, self.options["runtime"]["config"].user_config_dir / "settings.json")
|
||||
logger.debug(pprint.pformat(self.options))
|
||||
if self.options["commands"]["only_set_cv_key"]:
|
||||
print("Key set") # noqa: T201
|
||||
return
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
logger.info(
|
||||
"ComicTagger Version: %s running on: %s PyInstaller: %s",
|
||||
version,
|
||||
platform.system(),
|
||||
"Yes" if getattr(sys, "frozen", None) else "No",
|
||||
)
|
||||
|
||||
logger.debug("Installed Packages")
|
||||
for pkg in sorted(importlib_metadata.distributions(), key=lambda x: x.name):
|
||||
logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"])
|
||||
|
||||
utils.load_publishers()
|
||||
update_publishers(self.options)
|
||||
|
||||
if not qt_available and not self.options["runtime"]["no_gui"]:
|
||||
self.options["runtime"]["no_gui"] = True
|
||||
logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.")
|
||||
|
||||
gui_exception = None
|
||||
try:
|
||||
talker_api = ct_api.get_comic_talker("comicvine")( # type: ignore[call-arg]
|
||||
version=version,
|
||||
cache_folder=self.options["runtime"]["config"].user_cache_dir,
|
||||
series_match_thresh=self.options["comicvine"]["series_match_search_thresh"],
|
||||
remove_html_tables=self.options["comicvine"]["remove_html_tables"],
|
||||
use_series_start_as_volume=self.options["comicvine"]["use_series_start_as_volume"],
|
||||
wait_on_ratelimit=self.options["comicvine"]["wait_and_retry_on_rate_limit"],
|
||||
api_url=self.options["comicvine"]["cv_url"],
|
||||
api_key=self.options["comicvine"]["cv_api_key"],
|
||||
)
|
||||
except TalkerError as e:
|
||||
logger.exception("Unable to load talker")
|
||||
gui_exception = e
|
||||
if self.options["runtime"]["no_gui"]:
|
||||
raise SystemExit(1)
|
||||
|
||||
if self.options["runtime"]["no_gui"]:
|
||||
try:
|
||||
cli.cli_mode(self.options, talker_api)
|
||||
except Exception:
|
||||
logger.exception("CLI mode failed")
|
||||
else:
|
||||
|
||||
gui.open_tagger_window(talker_api, self.options, gui_exception)
|
||||
|
@ -21,6 +21,7 @@ import os
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.resulttypes import IssueResult
|
||||
from comictaggerlib.ui import ui_path
|
||||
@ -38,18 +39,24 @@ class MatchSelectionWindow(QtWidgets.QDialog):
|
||||
parent: QtWidgets.QWidget,
|
||||
matches: list[IssueResult],
|
||||
comic_archive: ComicArchive,
|
||||
options: settings.OptionValues,
|
||||
talker_api: ComicTalker,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ui_path / "matchselectionwindow.ui", self)
|
||||
|
||||
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, talker_api, CoverImageWidget.AltCoverMode)
|
||||
self.altCoverWidget = CoverImageWidget(
|
||||
self.altCoverContainer,
|
||||
CoverImageWidget.AltCoverMode,
|
||||
options["runtime"]["config"].user_cache_dir,
|
||||
talker_api,
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
|
||||
gridlayout.addWidget(self.altCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, talker_api, CoverImageWidget.ArchiveMode)
|
||||
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode, None, None)
|
||||
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
|
||||
gridlayout.addWidget(self.archiveCoverWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -22,8 +22,8 @@ from PyQt5 import QtCore, QtWidgets, uic
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive, MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.settingswindow import SettingsWindow
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent
|
||||
@ -38,7 +38,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
parent: QtWidgets.QWidget,
|
||||
comic_archive_list: list[ComicArchive],
|
||||
data_style: int,
|
||||
settings: ComicTaggerSettings,
|
||||
options: settings.OptionValues,
|
||||
talker_api: ComicTalker,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
@ -54,25 +54,25 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
)
|
||||
)
|
||||
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
self.talker_api = talker_api
|
||||
self.comic_archive_list = comic_archive_list
|
||||
self.data_style = data_style
|
||||
self.rename_list: list[str] = []
|
||||
|
||||
self.btnSettings.clicked.connect(self.modify_settings)
|
||||
platform = "universal" if self.settings.rename_strict else "auto"
|
||||
platform = "universal" if self.options["filename"]["rename_strict"] else "auto"
|
||||
self.renamer = FileRenamer(None, platform=platform)
|
||||
|
||||
self.do_preview()
|
||||
|
||||
def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str:
|
||||
self.renamer.set_template(self.settings.rename_template)
|
||||
self.renamer.set_issue_zero_padding(self.settings.rename_issue_number_padding)
|
||||
self.renamer.set_smart_cleanup(self.settings.rename_use_smart_string_cleanup)
|
||||
self.renamer.set_template(self.options["filename"]["rename_template"])
|
||||
self.renamer.set_issue_zero_padding(self.options["filename"]["rename_issue_number_padding"])
|
||||
self.renamer.set_smart_cleanup(self.options["filename"]["rename_use_smart_string_cleanup"])
|
||||
|
||||
new_ext = ca.path.suffix # default
|
||||
if self.settings.rename_extension_based_on_archive:
|
||||
if self.options["filename"]["rename_set_extension_based_on_archive"]:
|
||||
if ca.is_sevenzip():
|
||||
new_ext = ".cb7"
|
||||
elif ca.is_zip():
|
||||
@ -84,13 +84,13 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
md = ca.read_metadata(self.data_style)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
self.options["filename"]["complicated_parser"],
|
||||
self.options["filename"]["remove_c2c"],
|
||||
self.options["filename"]["remove_fcbd"],
|
||||
self.options["filename"]["remove_publisher"],
|
||||
)
|
||||
self.renamer.set_metadata(md)
|
||||
self.renamer.move = self.settings.rename_move_dir
|
||||
self.renamer.move = self.options["filename"]["rename_move_to_dir"]
|
||||
return new_ext
|
||||
|
||||
def do_preview(self) -> None:
|
||||
@ -103,7 +103,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
try:
|
||||
new_name = self.renamer.determine_name(new_ext)
|
||||
except ValueError as e:
|
||||
logger.exception("Invalid format string: %s", self.settings.rename_template)
|
||||
logger.exception("Invalid format string: %s", self.options["filename"]["rename_template"])
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"Invalid format string!",
|
||||
@ -117,7 +117,9 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"Formatter failure: %s metadata: %s", self.settings.rename_template, self.renamer.metadata
|
||||
"Formatter failure: %s metadata: %s",
|
||||
self.options["filename"]["rename_template"],
|
||||
self.renamer.metadata,
|
||||
)
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
@ -164,7 +166,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def modify_settings(self) -> None:
|
||||
settingswin = SettingsWindow(self, self.settings, self.talker_api)
|
||||
settingswin = SettingsWindow(self, self.options, self.talker_api)
|
||||
settingswin.setModal(True)
|
||||
settingswin.show_rename_tab()
|
||||
settingswin.exec()
|
||||
@ -192,7 +194,10 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
folder = get_rename_dir(comic[0], self.settings.rename_dir if self.settings.rename_move_dir else None)
|
||||
folder = get_rename_dir(
|
||||
comic[0],
|
||||
self.options["filename"]["rename_dir"] if self.options["filename"]["rename_move_to_dir"] else None,
|
||||
)
|
||||
|
||||
full_path = folder / comic[1]
|
||||
|
||||
|
@ -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)
|
17
comictaggerlib/settings/__init__.py
Normal file
17
comictaggerlib/settings/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from comictaggerlib.settings.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options
|
||||
from comictaggerlib.settings.file import register_settings, validate_settings
|
||||
from comictaggerlib.settings.manager import Manager
|
||||
from comictaggerlib.settings.types import ComicTaggerPaths, OptionValues
|
||||
|
||||
__all__ = [
|
||||
"initial_cmd_line_parser",
|
||||
"register_commandline",
|
||||
"register_settings",
|
||||
"validate_commandline_options",
|
||||
"validate_settings",
|
||||
"Manager",
|
||||
"ComicTaggerPaths",
|
||||
"OptionValues",
|
||||
]
|
343
comictaggerlib/settings/cmdline.py
Normal file
343
comictaggerlib/settings/cmdline.py
Normal file
@ -0,0 +1,343 @@
|
||||
"""CLI options class for ComicTagger app"""
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
from typing import Any
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib import ctversion
|
||||
from comictaggerlib.settings.manager import Manager
|
||||
from comictaggerlib.settings.types import ComicTaggerPaths, metadata_type, parse_metadata_from_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def initial_cmd_line_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
# Ensure this stays up to date with register_options
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
help="Config directory defaults to ~/.ComicTagger\non Linux/Mac and %%APPDATA%% on Windows\n",
|
||||
type=ComicTaggerPaths,
|
||||
default=ComicTaggerPaths(),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="count",
|
||||
default=0,
|
||||
help="Be noisy when doing what it does.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def register_options(parser: Manager) -> None:
|
||||
parser.add_setting(
|
||||
"--config",
|
||||
help="Config directory defaults to ~/.ComicTagger\non Linux/Mac and %%APPDATA%% on Windows\n",
|
||||
type=ComicTaggerPaths,
|
||||
default=ComicTaggerPaths(),
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="count",
|
||||
default=0,
|
||||
help="Be noisy when doing what it does.",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--abort-on-conflict",
|
||||
action="store_true",
|
||||
help="""Don't export to zip if intended new filename\nexists (otherwise, creates a new unique filename).\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--delete-original",
|
||||
action="store_true",
|
||||
dest="delete_after_zip_export",
|
||||
help="""Delete original archive after successful\nexport to Zip. (only relevant for -e)""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-f",
|
||||
"--parse-filename",
|
||||
"--parsefilename",
|
||||
action="store_true",
|
||||
help="""Parse the filename to get some info,\nspecifically series name, issue number,\nvolume, and publication year.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--id",
|
||||
dest="issue_id",
|
||||
type=int,
|
||||
help="""Use the issue ID when searching online.\nOverrides all other metadata.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-o",
|
||||
"--online",
|
||||
action="store_true",
|
||||
help="""Search online and attempt to identify file\nusing existing metadata and images in archive.\nMay be used in conjunction with -f and -m.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-m",
|
||||
"--metadata",
|
||||
default=GenericMetadata(),
|
||||
type=parse_metadata_from_string,
|
||||
help="""Explicitly define, as a list, some tags to be used. e.g.:\n"series=Plastic Man, publisher=Quality Comics"\n"series=Kickers^, Inc., issue=1, year=1986"\nName-Value pairs are comma separated. Use a\n"^" to escape an "=" or a ",", as shown in\nthe example above. Some names that can be\nused: series, issue, issue_count, year,\npublisher, title\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-i",
|
||||
"--interactive",
|
||||
action="store_true",
|
||||
help="""Interactively query the user when there are\nmultiple matches for an online search.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--noabort",
|
||||
dest="abort_on_low_confidence",
|
||||
action="store_false",
|
||||
help="""Don't abort save operation when online match\nis of low confidence.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--nosummary",
|
||||
dest="show_save_summary",
|
||||
action="store_false",
|
||||
help="Suppress the default summary after a save operation.\n\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--raw",
|
||||
action="store_true",
|
||||
help="""With -p, will print out the raw tag block(s)\nfrom the file.\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
help="Recursively include files in sub-folders.",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-S",
|
||||
"--script",
|
||||
help="""Run an "add-on" python script that uses the\nComicTagger library for custom processing.\nScript arguments can follow the script name.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--split-words",
|
||||
action="store_true",
|
||||
help="""Splits words before parsing the filename.\ne.g. 'judgedredd' to 'judge dredd'\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-n",
|
||||
"--dryrun",
|
||||
action="store_true",
|
||||
help="Don't actually modify file (only relevant for -d, -s, or -r).\n\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--darkmode",
|
||||
action="store_true",
|
||||
help="Windows only. Force a dark pallet",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-g",
|
||||
"--glob",
|
||||
action="store_true",
|
||||
help="Windows only. Enable globbing",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--terse",
|
||||
action="store_true",
|
||||
help="Don't say much (for print mode).",
|
||||
file=False,
|
||||
)
|
||||
|
||||
parser.add_setting(
|
||||
"-t",
|
||||
"--type",
|
||||
metavar="{CR,CBL,COMET}",
|
||||
default=[],
|
||||
type=metadata_type,
|
||||
help="""Specify TYPE as either CR, CBL or COMET\n(as either ComicRack, ComicBookLover,\nor CoMet style tags, respectively).\nUse commas for multiple types.\nFor searching the metadata will use the first listed:\neg '-t cbl,cr' with no CBL tags, CR will be used if they exist\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
# "--no-overwrite",
|
||||
# "--nooverwrite",
|
||||
"--temporary",
|
||||
dest="no_overwrite",
|
||||
action="store_true",
|
||||
help="""Don't modify tag block if it already exists (relevant for -s or -c).""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting("files", nargs="*", file=False)
|
||||
|
||||
|
||||
def register_commands(parser: Manager) -> None:
|
||||
parser.add_setting(
|
||||
"--version",
|
||||
action="store_true",
|
||||
help="Display version.",
|
||||
file=False,
|
||||
)
|
||||
|
||||
parser.add_setting(
|
||||
"-p",
|
||||
"--print",
|
||||
action="store_true",
|
||||
help="""Print out tag info from file. Specify type\n(via -t) to get only info of that tag type.\n\n""",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-d",
|
||||
"--delete",
|
||||
action="store_true",
|
||||
help="Deletes the tag block of specified type (via -t).\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-c",
|
||||
"--copy",
|
||||
type=metadata_type,
|
||||
metavar="{CR,CBL,COMET}",
|
||||
help="Copy the specified source tag block to\ndestination style specified via -t\n(potentially lossy operation).\n\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-s",
|
||||
"--save",
|
||||
action="store_true",
|
||||
help="Save out tags as specified type (via -t).\nMust specify also at least -o, -f, or -m.\n\n",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-r",
|
||||
"--rename",
|
||||
action="store_true",
|
||||
help="Rename the file based on specified tag style.",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"-e",
|
||||
"--export-to-zip",
|
||||
action="store_true",
|
||||
help="Export RAR archive to Zip format.",
|
||||
file=False,
|
||||
)
|
||||
parser.add_setting(
|
||||
"--only-set-cv-key",
|
||||
action="store_true",
|
||||
help="Only set the Comic Vine API key and quit.\n\n",
|
||||
file=False,
|
||||
)
|
||||
|
||||
|
||||
def register_commandline(parser: Manager) -> None:
|
||||
parser.add_group("commands", register_commands, True)
|
||||
parser.add_group("runtime", register_options)
|
||||
|
||||
|
||||
def validate_commandline_options(options: dict[str, dict[str, Any]], parser: Manager) -> dict[str, dict[str, Any]]:
|
||||
|
||||
if options["commands"]["version"]:
|
||||
parser.exit(
|
||||
status=1,
|
||||
message=f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team\n"
|
||||
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n",
|
||||
)
|
||||
|
||||
options["runtime"]["no_gui"] = any(
|
||||
[
|
||||
options["commands"]["print"],
|
||||
options["commands"]["delete"],
|
||||
options["commands"]["save"],
|
||||
options["commands"]["copy"],
|
||||
options["commands"]["rename"],
|
||||
options["commands"]["export_to_zip"],
|
||||
options["commands"]["only_set_cv_key"],
|
||||
]
|
||||
)
|
||||
|
||||
if platform.system() == "Windows" and options["runtime"]["glob"]:
|
||||
# no globbing on windows shell, so do it for them
|
||||
import glob
|
||||
|
||||
globs = options["runtime"]["files"]
|
||||
options["runtime"]["files"] = []
|
||||
for item in globs:
|
||||
options["runtime"]["files"].extend(glob.glob(item))
|
||||
|
||||
if (
|
||||
options["commands"]["only_set_cv_key"]
|
||||
and options["comicvine"]["cv_api_key"] is None
|
||||
and options["comicvine"]["cv_url"] is None
|
||||
):
|
||||
parser.exit(message="Key not given!\n", status=1)
|
||||
|
||||
if not options["commands"]["only_set_cv_key"] and options["runtime"]["no_gui"] and not options["runtime"]["files"]:
|
||||
parser.exit(message="Command requires at least one filename!\n", status=1)
|
||||
|
||||
if options["commands"]["delete"] and not options["runtime"]["type"]:
|
||||
parser.exit(message="Please specify the type to delete with -t\n", status=1)
|
||||
|
||||
if options["commands"]["save"] and not options["runtime"]["type"]:
|
||||
parser.exit(message="Please specify the type to save with -t\n", status=1)
|
||||
|
||||
if options["commands"]["copy"]:
|
||||
if not options["runtime"]["type"]:
|
||||
parser.exit(message="Please specify the type to copy to with -t\n", status=1)
|
||||
if len(options["commands"]["copy"]) > 1:
|
||||
parser.exit(message="Please specify only one type to copy to with -c\n", status=1)
|
||||
options["commands"]["copy"] = options["commands"]["copy"][0]
|
||||
|
||||
if options["runtime"]["recursive"]:
|
||||
options["runtime"]["file_list"] = utils.get_recursive_filelist(options["runtime"]["files"])
|
||||
else:
|
||||
options["runtime"]["file_list"] = options["runtime"]["files"]
|
||||
|
||||
# take a crack at finding rar exe, if not set already
|
||||
if options["general"]["rar_exe_path"].strip() in ("", "rar"):
|
||||
if platform.system() == "Windows":
|
||||
# look in some likely places for Windows machines
|
||||
if os.path.exists(r"C:\Program Files\WinRAR\Rar.exe"):
|
||||
options["general"]["rar_exe_path"] = r"C:\Program Files\WinRAR\Rar.exe"
|
||||
elif os.path.exists(r"C:\Program Files (x86)\WinRAR\Rar.exe"):
|
||||
options["general"]["rar_exe_path"] = r"C:\Program Files (x86)\WinRAR\Rar.exe"
|
||||
else:
|
||||
if os.path.exists("/opt/homebrew/bin"):
|
||||
utils.add_to_path("/opt/homebrew/bin")
|
||||
# see if it's in the path of unix user
|
||||
rarpath = utils.which("rar")
|
||||
if rarpath is not None:
|
||||
options["general"]["rar_exe_path"] = "rar"
|
||||
|
||||
return options
|
229
comictaggerlib/settings/file.py
Normal file
229
comictaggerlib/settings/file.py
Normal file
@ -0,0 +1,229 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Callable
|
||||
|
||||
from comictaggerlib.settings.manager import Manager
|
||||
|
||||
|
||||
def general(parser: Manager) -> None:
|
||||
# General Settings
|
||||
parser.add_setting("--rar-exe-path", default="rar")
|
||||
parser.add_setting("--allow-cbi-in-rar", default=True, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("check_for_new_version", default=False, cmdline=False)
|
||||
parser.add_setting("send_usage_stats", default=False, cmdline=False)
|
||||
|
||||
|
||||
def internal(parser: Manager) -> None:
|
||||
# automatic settings
|
||||
parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False)
|
||||
parser.add_setting("last_selected_save_data_style", default=0, cmdline=False)
|
||||
parser.add_setting("last_selected_load_data_style", default=0, cmdline=False)
|
||||
parser.add_setting("last_opened_folder", default="", cmdline=False)
|
||||
parser.add_setting("last_main_window_width", default=0, cmdline=False)
|
||||
parser.add_setting("last_main_window_height", default=0, cmdline=False)
|
||||
parser.add_setting("last_main_window_x", default=0, cmdline=False)
|
||||
parser.add_setting("last_main_window_y", default=0, cmdline=False)
|
||||
parser.add_setting("last_form_side_width", default=-1, cmdline=False)
|
||||
parser.add_setting("last_list_side_width", default=-1, cmdline=False)
|
||||
parser.add_setting("last_filelist_sorted_column", default=-1, cmdline=False)
|
||||
parser.add_setting("last_filelist_sorted_order", default=0, cmdline=False)
|
||||
|
||||
|
||||
def identifier(parser: Manager) -> None:
|
||||
# identifier settings
|
||||
parser.add_setting("--series-match-identify-thresh", default=91, type=int)
|
||||
parser.add_setting(
|
||||
"--publisher-filter",
|
||||
default=["Panini Comics", "Abril", "Planeta DeAgostini", "Editorial Televisa", "Dino Comics"],
|
||||
action=_AppendAction,
|
||||
)
|
||||
|
||||
|
||||
def _copy_items(items: Sequence[Any] | None) -> Sequence[Any]:
|
||||
if items is None:
|
||||
return []
|
||||
# The copy module is used only in the 'append' and 'append_const'
|
||||
# actions, and it is needed only when the default value isn't a list.
|
||||
# Delay its import for speeding up the common case.
|
||||
if type(items) is list:
|
||||
return items[:]
|
||||
import copy
|
||||
|
||||
return copy.copy(items)
|
||||
|
||||
|
||||
class _AppendAction(argparse.Action):
|
||||
def __init__(
|
||||
self,
|
||||
option_strings: list[str],
|
||||
dest: str,
|
||||
nargs: str | None = None,
|
||||
const: Any = None,
|
||||
default: Any = None,
|
||||
type: Callable[[str], Any] | None = None, # noqa: A002
|
||||
choices: list[Any] | None = None,
|
||||
required: bool = False,
|
||||
help: str | None = None, # noqa: A002
|
||||
metavar: str | None = None,
|
||||
):
|
||||
self.called = False
|
||||
if nargs == 0:
|
||||
raise ValueError(
|
||||
"nargs for append actions must be != 0; if arg "
|
||||
"strings are not supplying the value to append, "
|
||||
"the append const action may be more appropriate"
|
||||
)
|
||||
if const is not None and nargs != argparse.OPTIONAL:
|
||||
raise ValueError("nargs must be %r to supply const" % argparse.OPTIONAL)
|
||||
super().__init__(
|
||||
option_strings=option_strings,
|
||||
dest=dest,
|
||||
nargs=nargs,
|
||||
const=const,
|
||||
default=default,
|
||||
type=type,
|
||||
choices=choices,
|
||||
required=required,
|
||||
help=help,
|
||||
metavar=metavar,
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
parser: argparse.ArgumentParser,
|
||||
namespace: argparse.Namespace,
|
||||
values: str | Sequence[Any] | None,
|
||||
option_string: str | None = None,
|
||||
) -> None:
|
||||
if values:
|
||||
if not self.called:
|
||||
setattr(namespace, self.dest, [])
|
||||
items = getattr(namespace, self.dest, None)
|
||||
items = _copy_items(items)
|
||||
items.append(values) # type: ignore
|
||||
setattr(namespace, self.dest, items)
|
||||
|
||||
|
||||
def dialog(parser: Manager) -> None:
|
||||
# Show/ask dialog flags
|
||||
parser.add_setting("ask_about_cbi_in_rar", default=True, cmdline=False)
|
||||
parser.add_setting("show_disclaimer", default=True, cmdline=False)
|
||||
parser.add_setting("dont_notify_about_this_version", default="", cmdline=False)
|
||||
parser.add_setting("ask_about_usage_stats", default=True, cmdline=False)
|
||||
|
||||
|
||||
def filename(parser: Manager) -> None:
|
||||
# filename parsing settings
|
||||
parser.add_setting("--complicated-parser", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--remove-c2c", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--remove-fcbd", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--remove-publisher", default=False, action=argparse.BooleanOptionalAction)
|
||||
|
||||
|
||||
def comicvine(parser: Manager) -> None:
|
||||
# Comic Vine settings
|
||||
parser.add_setting(
|
||||
"--series-match-search-thresh",
|
||||
default=90,
|
||||
)
|
||||
parser.add_setting("--use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting(
|
||||
"--overwrite",
|
||||
default=True,
|
||||
help="Overwrite all existing metadata.\nMay be used in conjunction with -o, -f and -m.\n\n",
|
||||
dest="clear_metadata_on_import",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
)
|
||||
parser.add_setting("--remove-html-tables", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting(
|
||||
"--cv-api-key",
|
||||
default="",
|
||||
help="Use the given Comic Vine API Key (persisted in settings).",
|
||||
)
|
||||
parser.add_setting(
|
||||
"--cv-url",
|
||||
default="",
|
||||
help="Use the given Comic Vine URL (persisted in settings).",
|
||||
)
|
||||
parser.add_setting(
|
||||
"-a",
|
||||
"--auto-imprint",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="""Enables the auto imprint functionality.\ne.g. if the publisher is set to 'vertigo' it\nwill be updated to 'DC Comics' and the imprint\nproperty will be set to 'Vertigo'.\n\n""",
|
||||
)
|
||||
|
||||
parser.add_setting("--sort-series-by-year", default=True, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--exact-series-matches-first", default=True, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--always-use-publisher-filter", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--clear-form-before-populating-from-cv", default=False, action=argparse.BooleanOptionalAction)
|
||||
|
||||
|
||||
def cbl(parser: Manager) -> None:
|
||||
# CBL Transform settings
|
||||
parser.add_setting("--assume-lone-credit-is-primary", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--copy-characters-to-tags", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--copy-teams-to-tags", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--copy-locations-to-tags", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--copy-storyarcs-to-tags", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--copy-notes-to-comments", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--copy-weblink-to-comments", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--apply-cbl-transform-on-cv-import", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--apply-cbl-transform-on-bulk-operation", default=False, action=argparse.BooleanOptionalAction)
|
||||
|
||||
|
||||
def rename(parser: Manager) -> None:
|
||||
# Rename settings
|
||||
parser.add_setting("--template", default="{series} #{issue} ({year})")
|
||||
parser.add_setting("--issue-number-padding", default=3, type=int)
|
||||
parser.add_setting("--use-smart-string-cleanup", default=True, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--set-extension-based-on-archive", default=True, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--dir", default="")
|
||||
parser.add_setting("--move-to-dir", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--strict", default=False, action=argparse.BooleanOptionalAction)
|
||||
|
||||
|
||||
def autotag(parser: Manager) -> None:
|
||||
# Auto-tag stickies
|
||||
parser.add_setting("--save-on-low-confidence", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--dont-use-year-when-identifying", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting(
|
||||
"-1",
|
||||
"--assume-issue-one",
|
||||
dest="assume_1_if_no_issue_num",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="""Assume issue number is 1 if not found (relevant for -s).\n\n""",
|
||||
default=False,
|
||||
)
|
||||
parser.add_setting("--ignore-leading-numbers-in-filename", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting("--remove-archive-after-successful-match", default=False, action=argparse.BooleanOptionalAction)
|
||||
parser.add_setting(
|
||||
"-w",
|
||||
"--wait-on-cv-rate-limit",
|
||||
dest="wait_and_retry_on_rate_limit",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=True,
|
||||
help="""When encountering a Comic Vine rate limit\nerror, wait and retry query.\n\n""",
|
||||
)
|
||||
|
||||
|
||||
def validate_settings(options: dict[str, dict[str, Any]], parser: Manager) -> dict[str, dict[str, Any]]:
|
||||
options["identifier"]["publisher_filter"] = [
|
||||
x.strip() for x in options["identifier"]["publisher_filter"] if x.strip()
|
||||
]
|
||||
return options
|
||||
|
||||
|
||||
def register_settings(parser: Manager) -> None:
|
||||
parser.add_group("general", general, False)
|
||||
parser.add_group("internal", internal, False)
|
||||
parser.add_group("identifier", identifier, False)
|
||||
parser.add_group("dialog", dialog, False)
|
||||
parser.add_group("filename", filename, False)
|
||||
parser.add_group("comicvine", comicvine, False)
|
||||
parser.add_group("cbl", cbl, False)
|
||||
parser.add_group("rename", rename, False)
|
||||
parser.add_group("autotag", autotag, False)
|
260
comictaggerlib/settings/manager.py
Normal file
260
comictaggerlib/settings/manager.py
Normal file
@ -0,0 +1,260 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
from collections import defaultdict
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Callable, NoReturn, Union
|
||||
|
||||
from comictaggerlib.settings.types import ComicTaggerPaths, OptionValues
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Setting:
|
||||
def __init__(
|
||||
self,
|
||||
# From argparse
|
||||
*names: str,
|
||||
action: type[argparse.Action] | None = None,
|
||||
nargs: str | int | None = None,
|
||||
const: str | None = None,
|
||||
default: str | None = None,
|
||||
type: Callable[..., Any] | None = None, # noqa: A002
|
||||
choices: Sequence[Any] | None = None,
|
||||
required: bool | None = None,
|
||||
help: str | None = None, # noqa: A002
|
||||
metavar: str | None = None,
|
||||
dest: str | None = None,
|
||||
# ComicTagger
|
||||
cmdline: bool = True,
|
||||
file: bool = True,
|
||||
group: str = "",
|
||||
exclusive: bool = False,
|
||||
):
|
||||
if not names:
|
||||
raise ValueError("names must be specified")
|
||||
self.internal_name, dest, flag = self.get_dest(group, names, dest)
|
||||
args: Sequence[str] = names
|
||||
if not metavar and action not in ("store_true", "store_false", "count"):
|
||||
metavar = dest.upper()
|
||||
|
||||
if not flag:
|
||||
args = [f"{group}_{names[0]}".lstrip("_"), *names[1:]]
|
||||
|
||||
self.action = action
|
||||
self.nargs = nargs
|
||||
self.const = const
|
||||
self.default = default
|
||||
self.type = type
|
||||
self.choices = choices
|
||||
self.required = required
|
||||
self.help = help
|
||||
self.metavar = metavar
|
||||
self.dest = dest
|
||||
self.cmdline = cmdline
|
||||
self.file = file
|
||||
self.argparse_args = args
|
||||
self.group = group
|
||||
self.exclusive = exclusive
|
||||
|
||||
self.argparse_kwargs = {
|
||||
"action": action,
|
||||
"nargs": nargs,
|
||||
"const": const,
|
||||
"default": default,
|
||||
"type": type,
|
||||
"choices": choices,
|
||||
"required": required,
|
||||
"help": help,
|
||||
"metavar": metavar,
|
||||
"dest": self.internal_name if flag else None,
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Setting({self.argparse_args}, type={self.type}, file={self.file}, cmdline={self.cmdline}, kwargs={self.argparse_kwargs})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def get_dest(self, prefix: str, names: Sequence[str], dest: str | None) -> tuple[str, str, bool]:
|
||||
dest_name = None
|
||||
flag = False
|
||||
|
||||
for n in names:
|
||||
if n.startswith("--"):
|
||||
flag = True
|
||||
dest_name = n.lstrip("-").replace("-", "_")
|
||||
break
|
||||
if n.startswith("-"):
|
||||
flag = True
|
||||
|
||||
if dest_name is None:
|
||||
dest_name = names[0]
|
||||
if dest:
|
||||
dest_name = dest
|
||||
if dest_name is None:
|
||||
raise Exception("Something failed, try again")
|
||||
|
||||
internal_name = f"{prefix}_{dest_name}".lstrip("_")
|
||||
return internal_name, dest_name, flag
|
||||
|
||||
def filter_argparse_kwargs(self) -> dict[str, Any]:
|
||||
return {k: v for k, v in self.argparse_kwargs.items() if v is not None}
|
||||
|
||||
def to_argparse(self) -> tuple[Sequence[str], dict[str, Any]]:
|
||||
return self.argparse_args, self.filter_argparse_kwargs()
|
||||
|
||||
|
||||
ArgParser = Union[argparse._MutuallyExclusiveGroup, argparse._ArgumentGroup, argparse.ArgumentParser]
|
||||
|
||||
|
||||
class Manager:
|
||||
"""docstring for SettingManager"""
|
||||
|
||||
def __init__(self, options: dict[str, dict[str, Setting]] | None = None):
|
||||
self.argparser = argparse.ArgumentParser()
|
||||
|
||||
self.options: dict[str, dict[str, Setting]] = defaultdict(lambda: dict())
|
||||
if options:
|
||||
self.options = options
|
||||
|
||||
self.current_group: ArgParser | None = None
|
||||
self.current_group_name = ""
|
||||
|
||||
def defaults(self) -> OptionValues:
|
||||
return self.normalize_options({}, file=True, cmdline=True)
|
||||
|
||||
def get_namespace_for_args(self, options: OptionValues) -> argparse.Namespace:
|
||||
namespace = argparse.Namespace()
|
||||
for group_name, group in self.options.items():
|
||||
for setting_name, setting in group.items():
|
||||
if not setting.cmdline:
|
||||
if hasattr(namespace, setting.internal_name):
|
||||
raise Exception(f"Duplicate internal name: {setting.internal_name}")
|
||||
setattr(
|
||||
namespace, setting.internal_name, options.get(group_name, {}).get(setting.dest, setting.default)
|
||||
)
|
||||
return namespace
|
||||
|
||||
def add_setting(self, *args: Any, **kwargs: Any) -> None:
|
||||
exclusive = isinstance(self.current_group, argparse._MutuallyExclusiveGroup)
|
||||
setting = Setting(*args, group=self.current_group_name, exclusive=exclusive, **kwargs)
|
||||
self.options[self.current_group_name][setting.dest] = setting
|
||||
|
||||
def create_argparser(self) -> None:
|
||||
groups: dict[str, ArgParser] = {}
|
||||
self.argparser = argparse.ArgumentParser(
|
||||
description="""A utility for reading and writing metadata to comic archives.
|
||||
|
||||
|
||||
If no options are given, %(prog)s will run in windowed mode.""",
|
||||
epilog="For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki",
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
for group_name, group in self.options.items():
|
||||
for setting_name, setting in group.items():
|
||||
if setting.cmdline:
|
||||
argparse_args, argparse_kwargs = setting.to_argparse()
|
||||
current_group: ArgParser = self.argparser
|
||||
if setting.group:
|
||||
if setting.group not in groups:
|
||||
if setting.exclusive:
|
||||
groups[setting.group] = self.argparser.add_argument_group(
|
||||
setting.group
|
||||
).add_mutually_exclusive_group()
|
||||
else:
|
||||
groups[setting.group] = self.argparser.add_argument_group(setting.group)
|
||||
|
||||
# hardcoded exception for files
|
||||
if not (setting.group == "runtime" and setting.nargs == "*"):
|
||||
current_group = groups[setting.group]
|
||||
current_group.add_argument(*argparse_args, **argparse_kwargs)
|
||||
|
||||
def add_group(self, name: str, add_settings: Callable[[Manager], None], exclusive_group: bool = False) -> None:
|
||||
self.current_group_name = name
|
||||
if exclusive_group:
|
||||
self.current_group = self.argparser.add_mutually_exclusive_group()
|
||||
add_settings(self)
|
||||
self.current_group_name = ""
|
||||
self.current_group = None
|
||||
|
||||
def exit(self, *args: Any, **kwargs: Any) -> NoReturn:
|
||||
self.argparser.exit(*args, **kwargs)
|
||||
raise SystemExit(99)
|
||||
|
||||
def save_file(self, options: OptionValues, filename: pathlib.Path) -> bool:
|
||||
self.options = options["option_definitions"]
|
||||
file_options = self.normalize_options(options, file=True)
|
||||
del file_options["option_definitions"]
|
||||
for group in list(file_options.keys()):
|
||||
if not file_options[group]:
|
||||
del file_options[group]
|
||||
if not filename.exists():
|
||||
filename.touch()
|
||||
|
||||
try:
|
||||
json_str = json.dumps(file_options, indent=2)
|
||||
with filename.open(mode="w") as file:
|
||||
file.write(json_str)
|
||||
except Exception:
|
||||
logger.exception("Failed to save config file: %s", filename)
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse_file(self, filename: pathlib.Path) -> OptionValues:
|
||||
options: OptionValues = {}
|
||||
if filename.exists():
|
||||
try:
|
||||
with filename.open() as file:
|
||||
opts = json.load(file)
|
||||
if isinstance(opts, dict):
|
||||
options = opts
|
||||
except Exception:
|
||||
logger.exception("Failed to load config file: %s", filename)
|
||||
else:
|
||||
logger.info("No config file found")
|
||||
|
||||
return self.normalize_options(options, file=True)
|
||||
|
||||
def normalize_options(
|
||||
self, raw_options: OptionValues | argparse.Namespace, file: bool = False, cmdline: bool = False
|
||||
) -> OptionValues:
|
||||
options: OptionValues = {}
|
||||
for group_name, group in self.options.items():
|
||||
group_options = {}
|
||||
for setting_name, setting in group.items():
|
||||
if (setting.cmdline and cmdline) or (setting.file and file):
|
||||
group_options[setting_name] = self.get_option(raw_options, setting, group_name)
|
||||
options[group_name] = group_options
|
||||
options["option_definitions"] = self.options
|
||||
return options
|
||||
|
||||
def get_option(self, options: OptionValues | argparse.Namespace, setting: Setting, group_name: str) -> Any:
|
||||
if isinstance(options, dict):
|
||||
return options.get(group_name, {}).get(setting.dest, setting.default)
|
||||
return getattr(options, setting.internal_name)
|
||||
|
||||
def parse_args(self, args: list[str] | None = None, namespace: argparse.Namespace | None = None) -> OptionValues:
|
||||
"""Must handle a namespace with both argparse values and file values"""
|
||||
self.create_argparser()
|
||||
ns = self.argparser.parse_args(args, namespace=namespace)
|
||||
|
||||
return self.normalize_options(ns, cmdline=True, file=True)
|
||||
|
||||
def parse_options(self, config_paths: ComicTaggerPaths, args: list[str] | None = None) -> OptionValues:
|
||||
file_options = self.parse_file(config_paths.user_config_dir / "settings.json")
|
||||
cli_options = self.parse_args(args, namespace=self.get_namespace_for_args(file_options))
|
||||
|
||||
# for group, group_options in cli_options.items():
|
||||
# if group in cli_options:
|
||||
# file_options[group].update(group_options)
|
||||
# else:
|
||||
# file_options[group] = group_options
|
||||
# Just in case something weird happens with the commandline options
|
||||
file_options["runtime"]["config"] = config_paths
|
||||
|
||||
# Normalize a final time for fun
|
||||
return self.normalize_options(cli_options, file=True, cmdline=True)
|
122
comictaggerlib/settings/types.py
Normal file
122
comictaggerlib/settings/types.py
Normal file
@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
from typing import Any
|
||||
|
||||
from appdirs import AppDirs
|
||||
|
||||
from comicapi.comicarchive import MetaDataStyle
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
|
||||
OptionValues = dict[str, dict[str, Any]]
|
||||
|
||||
|
||||
class ComicTaggerPaths(AppDirs):
|
||||
def __init__(self, config_path: pathlib.Path | str | None = None) -> None:
|
||||
super().__init__("ComicTagger", None, None, False, False)
|
||||
self.path: pathlib.Path | None = None
|
||||
if config_path:
|
||||
self.path = pathlib.Path(config_path).absolute()
|
||||
|
||||
@property
|
||||
def user_data_dir(self) -> pathlib.Path:
|
||||
if self.path:
|
||||
return self.path
|
||||
return pathlib.Path(super().user_data_dir)
|
||||
|
||||
@property
|
||||
def user_config_dir(self) -> pathlib.Path:
|
||||
if self.path:
|
||||
return self.path
|
||||
return pathlib.Path(super().user_config_dir)
|
||||
|
||||
@property
|
||||
def user_cache_dir(self) -> pathlib.Path:
|
||||
if self.path:
|
||||
path = self.path / "cache"
|
||||
return path
|
||||
return pathlib.Path(super().user_cache_dir)
|
||||
|
||||
@property
|
||||
def user_state_dir(self) -> pathlib.Path:
|
||||
if self.path:
|
||||
return self.path
|
||||
return pathlib.Path(super().user_state_dir)
|
||||
|
||||
@property
|
||||
def user_log_dir(self) -> pathlib.Path:
|
||||
if self.path:
|
||||
path = self.path / "log"
|
||||
return path
|
||||
return pathlib.Path(super().user_log_dir)
|
||||
|
||||
@property
|
||||
def site_data_dir(self) -> pathlib.Path:
|
||||
return pathlib.Path(super().site_data_dir)
|
||||
|
||||
@property
|
||||
def site_config_dir(self) -> pathlib.Path:
|
||||
return pathlib.Path(super().site_config_dir)
|
||||
|
||||
|
||||
def metadata_type(types: str) -> list[int]:
|
||||
result = []
|
||||
types = types.casefold()
|
||||
for typ in types.split(","):
|
||||
typ = typ.strip()
|
||||
if typ not in MetaDataStyle.short_name:
|
||||
choices = ", ".join(MetaDataStyle.short_name)
|
||||
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
|
||||
result.append(MetaDataStyle.short_name.index(typ))
|
||||
return result
|
||||
|
||||
|
||||
def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
|
||||
"""The metadata string is a comma separated list of name-value pairs
|
||||
The names match the attributes of the internal metadata struct (for now)
|
||||
The caret is the special "escape character", since it's not common in
|
||||
natural language text
|
||||
|
||||
example = "series=Kickers^, Inc. ,issue=1, year=1986"
|
||||
"""
|
||||
|
||||
escaped_comma = "^,"
|
||||
escaped_equals = "^="
|
||||
replacement_token = "<_~_>"
|
||||
|
||||
md = GenericMetadata()
|
||||
|
||||
# First, replace escaped commas with with a unique token (to be changed back later)
|
||||
mdstr = mdstr.replace(escaped_comma, replacement_token)
|
||||
tmp_list = mdstr.split(",")
|
||||
md_list = []
|
||||
for item in tmp_list:
|
||||
item = item.replace(replacement_token, ",")
|
||||
md_list.append(item)
|
||||
|
||||
# Now build a nice dict from the list
|
||||
md_dict = {}
|
||||
for item in md_list:
|
||||
# Make sure to fix any escaped equal signs
|
||||
i = item.replace(escaped_equals, replacement_token)
|
||||
key, value = i.split("=")
|
||||
value = value.replace(replacement_token, "=").strip()
|
||||
key = key.strip()
|
||||
if key.casefold() == "credit":
|
||||
cred_attribs = value.split(":")
|
||||
role = cred_attribs[0]
|
||||
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
|
||||
primary = len(cred_attribs) > 2
|
||||
md.add_credit(person.strip(), role.strip(), primary)
|
||||
else:
|
||||
md_dict[key] = value
|
||||
|
||||
# Map the dict to the metadata object
|
||||
for key, value in md_dict.items():
|
||||
if not hasattr(md, key):
|
||||
raise argparse.ArgumentTypeError(f"'{key}' is not a valid tag name")
|
||||
else:
|
||||
md.is_empty = False
|
||||
setattr(md, key, value)
|
||||
return md
|
@ -25,10 +25,10 @@ from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import md_test
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.ctversion import version
|
||||
from comictaggerlib.filerenamer import FileRenamer
|
||||
from comictaggerlib.imagefetcher import ImageFetcher
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictalker.comiccacher import ComicCacher
|
||||
from comictalker.talkerbase import ComicTalker
|
||||
@ -129,7 +129,7 @@ Spider-Geddon #1 - New Players; Check In
|
||||
|
||||
|
||||
class SettingsWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent: QtWidgets.QWidget, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
|
||||
def __init__(self, parent: QtWidgets.QWidget, options: settings.OptionValues, talker_api: ComicTalker) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ui_path / "settingswindow.ui", self)
|
||||
@ -138,7 +138,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
|
||||
)
|
||||
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
self.talker_api = talker_api
|
||||
self.name = "Settings"
|
||||
|
||||
@ -227,53 +227,55 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
def settings_to_form(self) -> None:
|
||||
# Copy values from settings to form
|
||||
self.leRarExePath.setText(self.settings.rar_exe_path)
|
||||
self.sbNameMatchIdentifyThresh.setValue(self.settings.id_series_match_identify_thresh)
|
||||
self.sbNameMatchSearchThresh.setValue(self.settings.id_series_match_search_thresh)
|
||||
self.tePublisherFilter.setPlainText(self.settings.id_publisher_filter)
|
||||
self.leRarExePath.setText(self.options["general"]["rar_exe_path"])
|
||||
self.sbNameMatchIdentifyThresh.setValue(self.options["identifier"]["series_match_identify_thresh"])
|
||||
self.sbNameMatchSearchThresh.setValue(self.options["comicvine"]["series_match_search_thresh"])
|
||||
self.tePublisherFilter.setPlainText("\n".join(self.options["identifier"]["publisher_filter"]))
|
||||
|
||||
self.cbxCheckForNewVersion.setChecked(self.settings.check_for_new_version)
|
||||
self.cbxCheckForNewVersion.setChecked(self.options["general"]["check_for_new_version"])
|
||||
|
||||
self.cbxComplicatedParser.setChecked(self.settings.complicated_parser)
|
||||
self.cbxRemoveC2C.setChecked(self.settings.remove_c2c)
|
||||
self.cbxRemoveFCBD.setChecked(self.settings.remove_fcbd)
|
||||
self.cbxRemovePublisher.setChecked(self.settings.remove_publisher)
|
||||
self.cbxComplicatedParser.setChecked(self.options["filename"]["complicated_parser"])
|
||||
self.cbxRemoveC2C.setChecked(self.options["filename"]["remove_c2c"])
|
||||
self.cbxRemoveFCBD.setChecked(self.options["filename"]["remove_fcbd"])
|
||||
self.cbxRemovePublisher.setChecked(self.options["filename"]["remove_publisher"])
|
||||
self.switch_parser()
|
||||
|
||||
self.cbxUseSeriesStartAsVolume.setChecked(self.settings.use_series_start_as_volume)
|
||||
self.cbxClearFormBeforePopulating.setChecked(self.settings.clear_form_before_populating_from_cv)
|
||||
self.cbxRemoveHtmlTables.setChecked(self.settings.remove_html_tables)
|
||||
self.cbxUseSeriesStartAsVolume.setChecked(self.options["comicvine"]["use_series_start_as_volume"])
|
||||
self.cbxClearFormBeforePopulating.setChecked(self.options["comicvine"]["clear_form_before_populating_from_cv"])
|
||||
self.cbxRemoveHtmlTables.setChecked(self.options["comicvine"]["remove_html_tables"])
|
||||
|
||||
self.cbxUseFilter.setChecked(self.settings.always_use_publisher_filter)
|
||||
self.cbxSortByYear.setChecked(self.settings.sort_series_by_year)
|
||||
self.cbxExactMatches.setChecked(self.settings.exact_series_matches_first)
|
||||
self.cbxUseFilter.setChecked(self.options["comicvine"]["always_use_publisher_filter"])
|
||||
self.cbxSortByYear.setChecked(self.options["comicvine"]["sort_series_by_year"])
|
||||
self.cbxExactMatches.setChecked(self.options["comicvine"]["exact_series_matches_first"])
|
||||
|
||||
self.leKey.setText(self.settings.cv_api_key)
|
||||
self.leURL.setText(self.settings.cv_url)
|
||||
self.leKey.setText(self.options["comicvine"]["cv_api_key"])
|
||||
self.leURL.setText(self.options["comicvine"]["cv_url"])
|
||||
|
||||
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.settings.assume_lone_credit_is_primary)
|
||||
self.cbxCopyCharactersToTags.setChecked(self.settings.copy_characters_to_tags)
|
||||
self.cbxCopyTeamsToTags.setChecked(self.settings.copy_teams_to_tags)
|
||||
self.cbxCopyLocationsToTags.setChecked(self.settings.copy_locations_to_tags)
|
||||
self.cbxCopyStoryArcsToTags.setChecked(self.settings.copy_storyarcs_to_tags)
|
||||
self.cbxCopyNotesToComments.setChecked(self.settings.copy_notes_to_comments)
|
||||
self.cbxCopyWebLinkToComments.setChecked(self.settings.copy_weblink_to_comments)
|
||||
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.settings.apply_cbl_transform_on_cv_import)
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setChecked(self.settings.apply_cbl_transform_on_bulk_operation)
|
||||
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.options["cbl"]["assume_lone_credit_is_primary"])
|
||||
self.cbxCopyCharactersToTags.setChecked(self.options["cbl"]["copy_characters_to_tags"])
|
||||
self.cbxCopyTeamsToTags.setChecked(self.options["cbl"]["copy_teams_to_tags"])
|
||||
self.cbxCopyLocationsToTags.setChecked(self.options["cbl"]["copy_locations_to_tags"])
|
||||
self.cbxCopyStoryArcsToTags.setChecked(self.options["cbl"]["copy_storyarcs_to_tags"])
|
||||
self.cbxCopyNotesToComments.setChecked(self.options["cbl"]["copy_notes_to_comments"])
|
||||
self.cbxCopyWebLinkToComments.setChecked(self.options["cbl"]["copy_weblink_to_comments"])
|
||||
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.options["cbl"]["apply_cbl_transform_on_cv_import"])
|
||||
self.cbxApplyCBLTransformOnBatchOperation.setChecked(
|
||||
self.options["cbl"]["apply_cbl_transform_on_bulk_operation"]
|
||||
)
|
||||
|
||||
self.leRenameTemplate.setText(self.settings.rename_template)
|
||||
self.leIssueNumPadding.setText(str(self.settings.rename_issue_number_padding))
|
||||
self.cbxSmartCleanup.setChecked(self.settings.rename_use_smart_string_cleanup)
|
||||
self.cbxChangeExtension.setChecked(self.settings.rename_extension_based_on_archive)
|
||||
self.cbxMoveFiles.setChecked(self.settings.rename_move_dir)
|
||||
self.leDirectory.setText(self.settings.rename_dir)
|
||||
self.cbxRenameStrict.setChecked(self.settings.rename_strict)
|
||||
self.leRenameTemplate.setText(self.options["rename"]["template"])
|
||||
self.leIssueNumPadding.setText(str(self.options["rename"]["issue_number_padding"]))
|
||||
self.cbxSmartCleanup.setChecked(self.options["rename"]["use_smart_string_cleanup"])
|
||||
self.cbxChangeExtension.setChecked(self.options["rename"]["set_extension_based_on_archive"])
|
||||
self.cbxMoveFiles.setChecked(self.options["rename"]["move_to_dir"])
|
||||
self.leDirectory.setText(self.options["rename"]["dir"])
|
||||
self.cbxRenameStrict.setChecked(self.options["rename"]["strict"])
|
||||
|
||||
def accept(self) -> None:
|
||||
self.rename_test()
|
||||
if self.rename_error is not None:
|
||||
if isinstance(self.rename_error, ValueError):
|
||||
logger.exception("Invalid format string: %s", self.settings.rename_template)
|
||||
logger.exception("Invalid format string: %s", self.options["rename"]["template"])
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"Invalid format string!",
|
||||
@ -287,7 +289,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
return
|
||||
else:
|
||||
logger.exception(
|
||||
"Formatter failure: %s metadata: %s", self.settings.rename_template, self.renamer.metadata
|
||||
"Formatter failure: %s metadata: %s", self.options["rename"]["template"], self.renamer.metadata
|
||||
)
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
@ -300,69 +302,77 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
)
|
||||
|
||||
# Copy values from form to settings and save
|
||||
self.settings.rar_exe_path = str(self.leRarExePath.text())
|
||||
self.options["general"]["rar_exe_path"] = str(self.leRarExePath.text())
|
||||
|
||||
# make sure rar program is now in the path for the rar class
|
||||
if self.settings.rar_exe_path:
|
||||
utils.add_to_path(os.path.dirname(self.settings.rar_exe_path))
|
||||
if self.options["general"]["rar_exe_path"]:
|
||||
utils.add_to_path(os.path.dirname(self.options["general"]["rar_exe_path"]))
|
||||
|
||||
if not str(self.leIssueNumPadding.text()).isdigit():
|
||||
self.leIssueNumPadding.setText("0")
|
||||
|
||||
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
|
||||
self.options["general"]["check_for_new_version"] = self.cbxCheckForNewVersion.isChecked()
|
||||
|
||||
self.settings.id_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
|
||||
self.settings.id_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
|
||||
self.settings.id_publisher_filter = str(self.tePublisherFilter.toPlainText())
|
||||
self.options["identifier"]["series_match_identify_thresh"] = self.sbNameMatchIdentifyThresh.value()
|
||||
self.options["comicvine"]["series_match_search_thresh"] = self.sbNameMatchSearchThresh.value()
|
||||
self.options["identifier"]["publisher_filter"] = [
|
||||
x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip()
|
||||
]
|
||||
|
||||
self.settings.complicated_parser = self.cbxComplicatedParser.isChecked()
|
||||
self.settings.remove_c2c = self.cbxRemoveC2C.isChecked()
|
||||
self.settings.remove_fcbd = self.cbxRemoveFCBD.isChecked()
|
||||
self.settings.remove_publisher = self.cbxRemovePublisher.isChecked()
|
||||
self.options["filename"]["complicated_parser"] = self.cbxComplicatedParser.isChecked()
|
||||
self.options["filename"]["remove_c2c"] = self.cbxRemoveC2C.isChecked()
|
||||
self.options["filename"]["remove_fcbd"] = self.cbxRemoveFCBD.isChecked()
|
||||
self.options["filename"]["remove_publisher"] = self.cbxRemovePublisher.isChecked()
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
|
||||
self.options["comicvine"]["use_series_start_as_volume"] = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
self.options["comicvine"][
|
||||
"clear_form_before_populating_from_cv"
|
||||
] = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.options["comicvine"]["remove_html_tables"] = self.cbxRemoveHtmlTables.isChecked()
|
||||
|
||||
self.settings.always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.settings.sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.settings.exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
self.options["comicvine"]["always_use_publisher_filter"] = self.cbxUseFilter.isChecked()
|
||||
self.options["comicvine"]["sort_series_by_year"] = self.cbxSortByYear.isChecked()
|
||||
self.options["comicvine"]["exact_series_matches_first"] = self.cbxExactMatches.isChecked()
|
||||
|
||||
# Ignore empty field
|
||||
if self.leKey.text().strip():
|
||||
self.settings.cv_api_key = self.leKey.text().strip()
|
||||
self.talker_api.api_key = self.settings.cv_api_key
|
||||
self.options["comicvine"]["cv_api_key"] = self.leKey.text().strip()
|
||||
self.talker_api.api_key = self.options["comicvine"]["cv_api_key"]
|
||||
|
||||
if self.leURL.text().strip():
|
||||
self.settings.cv_url = self.leURL.text().strip()
|
||||
self.talker_api.api_url = self.settings.cv_url
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
|
||||
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.settings.copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
|
||||
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
|
||||
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
self.options["comicvine"]["cv_url"] = self.leURL.text().strip()
|
||||
self.talker_api.api_url = self.options["comicvine"]["cv_url"]
|
||||
|
||||
self.settings.rename_template = str(self.leRenameTemplate.text())
|
||||
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
|
||||
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
|
||||
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
|
||||
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
|
||||
self.settings.rename_dir = self.leDirectory.text()
|
||||
self.options["cbl"]["assume_lone_credit_is_primary"] = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
self.options["cbl"]["copy_characters_to_tags"] = self.cbxCopyCharactersToTags.isChecked()
|
||||
self.options["cbl"]["copy_teams_to_tags"] = self.cbxCopyTeamsToTags.isChecked()
|
||||
self.options["cbl"]["copy_locations_to_tags"] = self.cbxCopyLocationsToTags.isChecked()
|
||||
self.options["cbl"]["copy_storyarcs_to_tags"] = self.cbxCopyStoryArcsToTags.isChecked()
|
||||
self.options["cbl"]["copy_notes_to_comments"] = self.cbxCopyNotesToComments.isChecked()
|
||||
self.options["cbl"]["copy_weblink_to_comments"] = self.cbxCopyWebLinkToComments.isChecked()
|
||||
self.options["cbl"]["apply_cbl_transform_on_cv_import"] = self.cbxApplyCBLTransformOnCVIMport.isChecked()
|
||||
self.options["cbl"][
|
||||
"apply_cbl_transform_on_bulk_operation"
|
||||
] = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
|
||||
|
||||
self.settings.rename_strict = self.cbxRenameStrict.isChecked()
|
||||
self.options["rename"]["template"] = str(self.leRenameTemplate.text())
|
||||
self.options["rename"]["issue_number_padding"] = int(self.leIssueNumPadding.text())
|
||||
self.options["rename"]["use_smart_string_cleanup"] = self.cbxSmartCleanup.isChecked()
|
||||
self.options["rename"]["set_extension_based_on_archive"] = self.cbxChangeExtension.isChecked()
|
||||
self.options["rename"]["move_to_dir"] = self.cbxMoveFiles.isChecked()
|
||||
self.options["rename"]["dir"] = self.leDirectory.text()
|
||||
|
||||
self.settings.save()
|
||||
self.options["rename"]["strict"] = self.cbxRenameStrict.isChecked()
|
||||
|
||||
settings.Manager().save_file(self.options, self.options["runtime"]["config"].user_config_dir / "settings.json")
|
||||
self.parent().options = self.options
|
||||
QtWidgets.QDialog.accept(self)
|
||||
|
||||
def select_rar(self) -> None:
|
||||
self.select_file(self.leRarExePath, "RAR")
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
ImageFetcher().clear_cache()
|
||||
ComicCacher(ComicTaggerSettings.get_settings_folder(), version).clear_cache()
|
||||
ImageFetcher(self.options["runtime"]["config"].cache_folder).clear_cache()
|
||||
ComicCacher(self.options["runtime"]["config"].cache_folder, version).clear_cache()
|
||||
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
|
||||
|
||||
def test_api_key(self) -> None:
|
||||
@ -372,7 +382,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
|
||||
|
||||
def reset_settings(self) -> None:
|
||||
self.settings.reset()
|
||||
self.options = settings.Manager(self.options["option_definitions"]).defaults()
|
||||
self.settings_to_form()
|
||||
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
@ -40,7 +39,7 @@ from comicapi.comicinfoxml import ComicInfoXml
|
||||
from comicapi.filenameparser import FileNameParser
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comicapi.issuestring import IssueString
|
||||
from comictaggerlib import ctversion
|
||||
from comictaggerlib import ctversion, settings
|
||||
from comictaggerlib.applicationlogwindow import ApplicationLogWindow, QTextEditLogger
|
||||
from comictaggerlib.autotagmatchwindow import AutoTagMatchWindow
|
||||
from comictaggerlib.autotagprogresswindow import AutoTagProgressWindow
|
||||
@ -58,7 +57,6 @@ from comictaggerlib.pagebrowser import PageBrowserWindow
|
||||
from comictaggerlib.pagelisteditor import PageListEditor
|
||||
from comictaggerlib.renamewindow import RenameWindow
|
||||
from comictaggerlib.resulttypes import IssueResult, MultipleMatch, OnlineMatchResults
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.settingswindow import SettingsWindow
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
|
||||
@ -80,25 +78,26 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def __init__(
|
||||
self,
|
||||
file_list: list[str],
|
||||
settings: ComicTaggerSettings,
|
||||
options: settings.OptionValues,
|
||||
talker_api: ComicTalker,
|
||||
parent: QtWidgets.QWidget | None = None,
|
||||
opts: argparse.Namespace | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ui_path / "taggerwindow.ui", self)
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
if not options:
|
||||
self.options = {}
|
||||
self.talker_api = talker_api
|
||||
self.log_window = self.setup_logger()
|
||||
|
||||
# prevent multiple instances
|
||||
socket = QtNetwork.QLocalSocket(self)
|
||||
socket.connectToServer(settings.install_id)
|
||||
socket.connectToServer(options["internal"]["install_id"])
|
||||
alive = socket.waitForConnected(3000)
|
||||
if alive:
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.info("Another application with key [%s] is already running", settings.install_id)
|
||||
logger.info("Another application with key [%s] is already running", options["internal"]["install_id"])
|
||||
# send file list to other instance
|
||||
if file_list:
|
||||
socket.write(pickle.dumps(file_list))
|
||||
@ -110,20 +109,20 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
# listen on a socket to prevent multiple instances
|
||||
self.socketServer = QtNetwork.QLocalServer(self)
|
||||
self.socketServer.newConnection.connect(self.on_incoming_socket_connection)
|
||||
ok = self.socketServer.listen(settings.install_id)
|
||||
ok = self.socketServer.listen(options["internal"]["install_id"])
|
||||
if not ok:
|
||||
if self.socketServer.serverError() == QtNetwork.QAbstractSocket.SocketError.AddressInUseError:
|
||||
self.socketServer.removeServer(settings.install_id)
|
||||
ok = self.socketServer.listen(settings.install_id)
|
||||
self.socketServer.removeServer(options["internal"]["install_id"])
|
||||
ok = self.socketServer.listen(options["internal"]["install_id"])
|
||||
if not ok:
|
||||
logger.error(
|
||||
"Cannot start local socket with key [%s]. Reason: %s",
|
||||
settings.install_id,
|
||||
options["internal"]["install_id"],
|
||||
self.socketServer.errorString(),
|
||||
)
|
||||
sys.exit()
|
||||
|
||||
self.archiveCoverWidget = CoverImageWidget(self.coverImageContainer, talker_api, CoverImageWidget.ArchiveMode)
|
||||
self.archiveCoverWidget = CoverImageWidget(self.coverImageContainer, CoverImageWidget.ArchiveMode, None, None)
|
||||
grid_layout = QtWidgets.QGridLayout(self.coverImageContainer)
|
||||
grid_layout.addWidget(self.archiveCoverWidget)
|
||||
grid_layout.setContentsMargins(0, 0, 0, 0)
|
||||
@ -132,14 +131,15 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
grid_layout = QtWidgets.QGridLayout(self.tabPages)
|
||||
grid_layout.addWidget(self.page_list_editor)
|
||||
|
||||
self.fileSelectionList = FileSelectionList(self.widgetListHolder, self.settings, self.dirty_flag_verification)
|
||||
self.fileSelectionList = FileSelectionList(self.widgetListHolder, self.options, self.dirty_flag_verification)
|
||||
grid_layout = QtWidgets.QGridLayout(self.widgetListHolder)
|
||||
grid_layout.addWidget(self.fileSelectionList)
|
||||
|
||||
self.fileSelectionList.selectionChanged.connect(self.file_list_selection_changed)
|
||||
self.fileSelectionList.listCleared.connect(self.file_list_cleared)
|
||||
self.fileSelectionList.set_sorting(
|
||||
self.settings.last_filelist_sorted_column, QtCore.Qt.SortOrder(self.settings.last_filelist_sorted_order)
|
||||
self.options["internal"]["last_filelist_sorted_column"],
|
||||
QtCore.Qt.SortOrder(self.options["internal"]["last_filelist_sorted_order"]),
|
||||
)
|
||||
|
||||
# we can't specify relative font sizes in the UI designer, so
|
||||
@ -156,14 +156,14 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.scrollAreaWidgetContents.adjustSize()
|
||||
|
||||
self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png")))
|
||||
# TODO: this needs to be looked at
|
||||
if opts is not None and opts.type:
|
||||
# respect the command line option tag type
|
||||
settings.last_selected_save_data_style = opts.type[0]
|
||||
settings.last_selected_load_data_style = opts.type[0]
|
||||
|
||||
self.save_data_style = settings.last_selected_save_data_style
|
||||
self.load_data_style = settings.last_selected_load_data_style
|
||||
if options["runtime"]["type"] and isinstance(options["runtime"]["type"][0], int):
|
||||
# respect the command line option tag type
|
||||
options["internal"]["last_selected_save_data_style"] = options["runtime"]["type"][0]
|
||||
options["internal"]["last_selected_load_data_style"] = options["runtime"]["type"][0]
|
||||
|
||||
self.save_data_style = options["internal"]["last_selected_save_data_style"]
|
||||
self.load_data_style = options["internal"]["last_selected_load_data_style"]
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
self.config_menus()
|
||||
@ -228,8 +228,10 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.show()
|
||||
self.set_app_position()
|
||||
if self.settings.last_form_side_width != -1:
|
||||
self.splitter.setSizes([self.settings.last_form_side_width, self.settings.last_list_side_width])
|
||||
if self.options["internal"]["last_form_side_width"] != -1:
|
||||
self.splitter.setSizes(
|
||||
[self.options["internal"]["last_form_side_width"], self.options["internal"]["last_list_side_width"]]
|
||||
)
|
||||
self.raise_()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
self.resizeEvent(None)
|
||||
@ -246,7 +248,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
if len(file_list) != 0:
|
||||
self.fileSelectionList.add_path_list(file_list)
|
||||
|
||||
if self.settings.show_disclaimer:
|
||||
if self.options["dialog"]["show_disclaimer"]:
|
||||
checked = OptionalMessageDialog.msg(
|
||||
self,
|
||||
"Welcome!",
|
||||
@ -261,26 +263,9 @@ use ComicTagger on local copies of your comics.<br><br>
|
||||
Have fun!
|
||||
""",
|
||||
)
|
||||
self.settings.show_disclaimer = not checked
|
||||
self.options["dialog"]["show_disclaimer"] = not checked
|
||||
|
||||
if self.settings.settings_warning < 4:
|
||||
checked = OptionalMessageDialog.msg(
|
||||
self,
|
||||
"Warning!",
|
||||
f"""<span style="font-size:15px">
|
||||
{" "*100}
|
||||
The next release will save settings in a different format
|
||||
<span style="font-weight: bold;font-size:19px">no settings will be transfered</span> to the new version.<br/>
|
||||
See <a href="https://github.com/comictagger/comictagger/releases/1.5.5">https://github.com/comictagger/comictagger/releases/1.5.5</a>
|
||||
for more information
|
||||
<br/><br/>
|
||||
You have {4-self.settings.settings_warning} warnings left.
|
||||
</span>""",
|
||||
)
|
||||
if checked:
|
||||
self.settings.settings_warning += 1
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
if self.options["general"]["check_for_new_version"]:
|
||||
self.check_latest_version_online()
|
||||
|
||||
def open_file_event(self, url: QtCore.QUrl) -> None:
|
||||
@ -293,7 +278,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
|
||||
def setup_logger(self) -> ApplicationLogWindow:
|
||||
try:
|
||||
current_logs = (ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log").read_text("utf-8")
|
||||
current_logs = (self.options["runtime"]["config"].user_log_dir / "ComicTagger.log").read_text("utf-8")
|
||||
except Exception:
|
||||
current_logs = ""
|
||||
root_logger = logging.getLogger()
|
||||
@ -494,7 +479,6 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
if non_zip_count != 0:
|
||||
EW = ExportWindow(
|
||||
self,
|
||||
self.settings,
|
||||
(
|
||||
f"You have selected {non_zip_count} archive(s) to export to Zip format. "
|
||||
""" New archives will be created in the same folder as the original.
|
||||
@ -627,10 +611,10 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
def actual_load_current_archive(self) -> None:
|
||||
if self.metadata.is_empty and self.comic_archive is not None:
|
||||
self.metadata = self.comic_archive.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
self.options["filename"]["complicated_parser"],
|
||||
self.options["filename"]["remove_c2c"],
|
||||
self.options["filename"]["remove_fcbd"],
|
||||
self.options["filename"]["remove_publisher"],
|
||||
)
|
||||
if len(self.metadata.pages) == 0 and self.comic_archive is not None:
|
||||
self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages())
|
||||
@ -998,10 +982,10 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
# copy the form onto metadata object
|
||||
self.form_to_metadata()
|
||||
new_metadata = self.comic_archive.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
self.options["filename"]["complicated_parser"],
|
||||
self.options["filename"]["remove_c2c"],
|
||||
self.options["filename"]["remove_fcbd"],
|
||||
self.options["filename"]["remove_publisher"],
|
||||
split_words,
|
||||
)
|
||||
if new_metadata is not None:
|
||||
@ -1022,8 +1006,8 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
else:
|
||||
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFiles)
|
||||
|
||||
if self.settings.last_opened_folder is not None:
|
||||
dialog.setDirectory(self.settings.last_opened_folder)
|
||||
if self.options["internal"]["last_opened_folder"] is not None:
|
||||
dialog.setDirectory(self.options["internal"]["last_opened_folder"])
|
||||
|
||||
if not folder_mode:
|
||||
archive_filter = "Comic archive files (*.cbz *.zip *.cbr *.rar *.cb7 *.7z)"
|
||||
@ -1073,7 +1057,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
issue_count,
|
||||
cover_index_list,
|
||||
self.comic_archive,
|
||||
self.settings,
|
||||
self.options,
|
||||
self.talker_api,
|
||||
autoselect,
|
||||
literal,
|
||||
@ -1092,16 +1076,9 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
self.form_to_metadata()
|
||||
|
||||
try:
|
||||
if selector.issue_id:
|
||||
new_metadata = self.talker_api.fetch_comic_data(selector.issue_id)
|
||||
elif selector.volume_id and selector.issue_number:
|
||||
# Would this ever be needed?
|
||||
new_metadata = self.talker_api.fetch_comic_data(
|
||||
series_id=selector.volume_id, issue_number=selector.issue_number
|
||||
)
|
||||
else:
|
||||
# Only left with series? Isn't series only handled elsewhere?
|
||||
new_metadata = self.talker_api.fetch_comic_data(series_id=selector.volume_id)
|
||||
new_metadata = self.talker_api.fetch_comic_data(
|
||||
issue_id=selector.issue_id or 0, series_id=selector.volume_id, issue_number=selector.issue_number
|
||||
)
|
||||
except TalkerError as e:
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
QtWidgets.QMessageBox.critical(
|
||||
@ -1112,10 +1089,10 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
else:
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
if new_metadata is not None:
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
new_metadata = CBLTransformer(new_metadata, self.settings).apply()
|
||||
if self.options["cbl"]["apply_cbl_transform_on_cv_import"]:
|
||||
new_metadata = CBLTransformer(new_metadata, self.options).apply()
|
||||
|
||||
if self.settings.clear_form_before_populating_from_cv:
|
||||
if self.options["comicvine"]["clear_form_before_populating_from_cv"]:
|
||||
self.clear_form()
|
||||
|
||||
notes = (
|
||||
@ -1170,7 +1147,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
"Change Tag Read Style", "If you change read tag style now, data in the form will be lost. Are you sure?"
|
||||
):
|
||||
self.load_data_style = self.cbLoadDataStyle.itemData(s)
|
||||
self.settings.last_selected_load_data_style = self.load_data_style
|
||||
self.options["internal"]["last_selected_load_data_style"] = self.load_data_style
|
||||
self.update_menus()
|
||||
if self.comic_archive is not None:
|
||||
self.load_archive(self.comic_archive)
|
||||
@ -1181,7 +1158,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
|
||||
def set_save_data_style(self, s: int) -> None:
|
||||
self.save_data_style = self.cbSaveDataStyle.itemData(s)
|
||||
self.settings.last_selected_save_data_style = self.save_data_style
|
||||
self.options["internal"]["last_selected_save_data_style"] = self.save_data_style
|
||||
self.update_style_tweaks()
|
||||
self.update_menus()
|
||||
|
||||
@ -1400,15 +1377,17 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
|
||||
def show_settings(self) -> None:
|
||||
|
||||
settingswin = SettingsWindow(self, self.settings, self.talker_api)
|
||||
settingswin = SettingsWindow(self, self.options, self.talker_api)
|
||||
settingswin.setModal(True)
|
||||
settingswin.exec()
|
||||
settingswin.result()
|
||||
|
||||
def set_app_position(self) -> None:
|
||||
if self.settings.last_main_window_width != 0:
|
||||
self.move(self.settings.last_main_window_x, self.settings.last_main_window_y)
|
||||
self.resize(self.settings.last_main_window_width, self.settings.last_main_window_height)
|
||||
if self.options["internal"]["last_main_window_width"] != 0:
|
||||
self.move(self.options["internal"]["last_main_window_x"], self.options["internal"]["last_main_window_y"])
|
||||
self.resize(
|
||||
self.options["internal"]["last_main_window_width"], self.options["internal"]["last_main_window_height"]
|
||||
)
|
||||
else:
|
||||
screen = QtGui.QGuiApplication.primaryScreen().geometry()
|
||||
size = self.frameGeometry()
|
||||
@ -1675,8 +1654,11 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
if ca.has_metadata(src_style) and ca.is_writable():
|
||||
md = ca.read_metadata(src_style)
|
||||
|
||||
if dest_style == MetaDataStyle.CBI and self.settings.apply_cbl_transform_on_bulk_operation:
|
||||
md = CBLTransformer(md, self.settings).apply()
|
||||
if (
|
||||
dest_style == MetaDataStyle.CBI
|
||||
and self.options["cbl"]["apply_cbl_transform_on_bulk_operation"]
|
||||
):
|
||||
md = CBLTransformer(md, self.options).apply()
|
||||
|
||||
if not ca.write_metadata(md, dest_style):
|
||||
failed_list.append(ca.path)
|
||||
@ -1710,12 +1692,12 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
|
||||
try:
|
||||
ct_md = self.talker_api.fetch_comic_data(match["issue_id"])
|
||||
except TalkerError as e:
|
||||
logger.exception(f"Save aborted.\n{e}")
|
||||
except TalkerError:
|
||||
logger.exception("Save aborted.")
|
||||
|
||||
if not ct_md.is_empty:
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
ct_md = CBLTransformer(ct_md, self.settings).apply()
|
||||
if self.options["cbl"]["apply_cbl_transform_on_cv_import"]:
|
||||
ct_md = CBLTransformer(ct_md, self.options).apply()
|
||||
|
||||
QtWidgets.QApplication.restoreOverrideCursor()
|
||||
|
||||
@ -1734,7 +1716,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
self, ca: ComicArchive, match_results: OnlineMatchResults, dlg: AutoTagStartWindow
|
||||
) -> tuple[bool, OnlineMatchResults]:
|
||||
success = False
|
||||
ii = IssueIdentifier(ca, self.settings, self.talker_api)
|
||||
ii = IssueIdentifier(ca, self.options, self.talker_api)
|
||||
|
||||
# read in metadata, and parse file name if not there
|
||||
try:
|
||||
@ -1744,10 +1726,10 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
logger.error("Failed to load metadata for %s: %s", ca.path, e)
|
||||
if md.is_empty:
|
||||
md = ca.metadata_from_filename(
|
||||
self.settings.complicated_parser,
|
||||
self.settings.remove_c2c,
|
||||
self.settings.remove_fcbd,
|
||||
self.settings.remove_publisher,
|
||||
self.options["filename"]["complicated_parser"],
|
||||
self.options["filename"]["remove_c2c"],
|
||||
self.options["filename"]["remove_fcbd"],
|
||||
self.options["filename"]["remove_publisher"],
|
||||
dlg.split_words,
|
||||
)
|
||||
if dlg.ignore_leading_digits_in_filename and md.series is not None:
|
||||
@ -1833,7 +1815,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
)
|
||||
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
|
||||
|
||||
if self.settings.auto_imprint:
|
||||
if self.options["comicvine"]["auto_imprint"]:
|
||||
md.fix_publisher()
|
||||
|
||||
if not ca.write_metadata(md, self.save_data_style):
|
||||
@ -1862,7 +1844,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
|
||||
atstartdlg = AutoTagStartWindow(
|
||||
self,
|
||||
self.settings,
|
||||
self.options,
|
||||
(
|
||||
f"You have selected {len(ca_list)} archive(s) to automatically identify and write {MetaDataStyle.name[style]} tags to."
|
||||
"\n\nPlease choose options below, and select OK to Auto-Tag."
|
||||
@ -1965,7 +1947,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
match_results.multiple_matches,
|
||||
style,
|
||||
self.actual_issue_data_fetch,
|
||||
self.settings,
|
||||
self.options,
|
||||
self.talker_api,
|
||||
)
|
||||
matchdlg.setModal(True)
|
||||
@ -2011,17 +1993,19 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
f"Exit {self.appName}", "If you quit now, data in the form will be lost. Are you sure?"
|
||||
):
|
||||
appsize = self.size()
|
||||
self.settings.last_main_window_width = appsize.width()
|
||||
self.settings.last_main_window_height = appsize.height()
|
||||
self.settings.last_main_window_x = self.x()
|
||||
self.settings.last_main_window_y = self.y()
|
||||
self.settings.last_form_side_width = self.splitter.sizes()[0]
|
||||
self.settings.last_list_side_width = self.splitter.sizes()[1]
|
||||
self.options["internal"]["last_main_window_width"] = appsize.width()
|
||||
self.options["internal"]["last_main_window_height"] = appsize.height()
|
||||
self.options["internal"]["last_main_window_x"] = self.x()
|
||||
self.options["internal"]["last_main_window_y"] = self.y()
|
||||
self.options["internal"]["last_form_side_width"] = self.splitter.sizes()[0]
|
||||
self.options["internal"]["last_list_side_width"] = self.splitter.sizes()[1]
|
||||
(
|
||||
self.settings.last_filelist_sorted_column,
|
||||
self.settings.last_filelist_sorted_order,
|
||||
self.options["internal"]["last_filelist_sorted_column"],
|
||||
self.options["internal"]["last_filelist_sorted_order"],
|
||||
) = self.fileSelectionList.get_sorting()
|
||||
self.settings.save()
|
||||
settings.Manager().save_file(
|
||||
self.options, self.options["runtime"]["config"].user_config_dir / "settings.json"
|
||||
)
|
||||
|
||||
event.accept()
|
||||
else:
|
||||
@ -2070,7 +2054,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
|
||||
def apply_cbl_transform(self) -> None:
|
||||
self.form_to_metadata()
|
||||
self.metadata = CBLTransformer(self.metadata, self.settings).apply()
|
||||
self.metadata = CBLTransformer(self.metadata, self.options).apply()
|
||||
self.metadata_to_form()
|
||||
|
||||
def recalc_page_dimensions(self) -> None:
|
||||
@ -2096,7 +2080,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
"File Rename", "If you rename files now, unsaved data in the form will be lost. Are you sure?"
|
||||
):
|
||||
|
||||
dlg = RenameWindow(self, ca_list, self.load_data_style, self.settings, self.talker_api)
|
||||
dlg = RenameWindow(self, ca_list, self.load_data_style, self.options, self.talker_api)
|
||||
dlg.setModal(True)
|
||||
if dlg.exec() and self.comic_archive is not None:
|
||||
self.fileSelectionList.update_selected_rows()
|
||||
@ -2115,7 +2099,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
QtCore.QTimer.singleShot(1, self.fileSelectionList.revert_selection)
|
||||
return
|
||||
|
||||
self.settings.last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
|
||||
self.options["internal"]["last_opened_folder"] = os.path.abspath(os.path.split(comic_archive.path)[0])
|
||||
self.comic_archive = comic_archive
|
||||
try:
|
||||
self.metadata = self.comic_archive.read_metadata(self.load_data_style)
|
||||
@ -2147,11 +2131,13 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
def check_latest_version_online(self) -> None:
|
||||
version_checker = VersionChecker()
|
||||
self.version_check_complete(
|
||||
version_checker.get_latest_version(self.settings.install_id, self.settings.send_usage_stats)
|
||||
version_checker.get_latest_version(
|
||||
self.options["internal"]["install_id"], self.options["general"]["send_usage_stats"]
|
||||
)
|
||||
)
|
||||
|
||||
def version_check_complete(self, new_version: tuple[str, str]) -> None:
|
||||
if new_version[0] not in (self.version, self.settings.dont_notify_about_this_version):
|
||||
if new_version[0] not in (self.version, self.options["dialog"]["dont_notify_about_this_version"]):
|
||||
website = "https://github.com/comictagger/comictagger"
|
||||
checked = OptionalMessageDialog.msg(
|
||||
self,
|
||||
@ -2162,7 +2148,7 @@ You have {4-self.settings.settings_warning} warnings left.
|
||||
"Don't tell me about this version again",
|
||||
)
|
||||
if checked:
|
||||
self.settings.dont_notify_about_this_version = new_version[0]
|
||||
self.options["dialog"]["dont_notify_about_this_version"] = new_version[0]
|
||||
|
||||
def on_incoming_socket_connection(self) -> None:
|
||||
# Accept connection from other instance.
|
||||
|
@ -25,12 +25,12 @@ from PyQt5.QtCore import pyqtSignal
|
||||
from comicapi import utils
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
from comictaggerlib import settings
|
||||
from comictaggerlib.coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.issueidentifier import IssueIdentifier
|
||||
from comictaggerlib.issueselectionwindow import IssueSelectionWindow
|
||||
from comictaggerlib.matchselectionwindow import MatchSelectionWindow
|
||||
from comictaggerlib.progresswindow import IDProgressWindow
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
from comictaggerlib.ui import ui_path
|
||||
from comictaggerlib.ui.qtutils import reduce_widget_font_size
|
||||
from comictalker.resulttypes import ComicVolume
|
||||
@ -111,7 +111,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
issue_count: int,
|
||||
cover_index_list: list[int],
|
||||
comic_archive: ComicArchive | None,
|
||||
settings: ComicTaggerSettings,
|
||||
options: settings.OptionValues,
|
||||
talker_api: ComicTalker,
|
||||
autoselect: bool = False,
|
||||
literal: bool = False,
|
||||
@ -120,7 +120,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
uic.loadUi(ui_path / "volumeselectionwindow.ui", self)
|
||||
|
||||
self.imageWidget = CoverImageWidget(self.imageContainer, talker_api, CoverImageWidget.URLMode)
|
||||
self.imageWidget = CoverImageWidget(
|
||||
self.imageContainer, CoverImageWidget.URLMode, options["runtime"]["config"].user_cache_dir, talker_api
|
||||
)
|
||||
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
|
||||
gridlayout.addWidget(self.imageWidget)
|
||||
gridlayout.setContentsMargins(0, 0, 0, 0)
|
||||
@ -136,7 +138,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
)
|
||||
)
|
||||
|
||||
self.settings = settings
|
||||
self.options = options
|
||||
self.series_name = series_name
|
||||
self.issue_number = issue_number
|
||||
self.issue_id: int | None = None
|
||||
@ -154,7 +156,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.progdialog: QtWidgets.QProgressDialog | None = None
|
||||
self.search_thread: SearchThread | None = None
|
||||
|
||||
self.use_filter = self.settings.always_use_publisher_filter
|
||||
self.use_filter = self.options["comicvine"]["always_use_publisher_filter"]
|
||||
|
||||
# Load to retrieve settings
|
||||
self.talker_api = talker_api
|
||||
@ -205,7 +207,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.iddialog.rejected.connect(self.identify_cancel)
|
||||
self.iddialog.show()
|
||||
|
||||
self.ii = IssueIdentifier(self.comic_archive, self.settings, self.talker_api)
|
||||
self.ii = IssueIdentifier(self.comic_archive, self.options, self.talker_api)
|
||||
|
||||
md = GenericMetadata()
|
||||
md.series = self.series_name
|
||||
@ -278,7 +280,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
choices = True
|
||||
|
||||
if choices:
|
||||
selector = MatchSelectionWindow(self, matches, self.comic_archive, self.talker_api)
|
||||
selector = MatchSelectionWindow(
|
||||
self, matches, self.comic_archive, talker_api=self.talker_api, options=self.options
|
||||
)
|
||||
selector.setModal(True)
|
||||
selector.exec()
|
||||
if selector.result():
|
||||
@ -294,7 +298,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.show_issues()
|
||||
|
||||
def show_issues(self) -> None:
|
||||
selector = IssueSelectionWindow(self, self.settings, self.talker_api, self.volume_id, self.issue_number)
|
||||
selector = IssueSelectionWindow(self, self.options, self.talker_api, self.volume_id, self.issue_number)
|
||||
title = ""
|
||||
for record in self.ct_search_results:
|
||||
if record["id"] == self.volume_id:
|
||||
@ -322,7 +326,11 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
def perform_query(self, refresh: bool = False) -> None:
|
||||
|
||||
self.search_thread = SearchThread(
|
||||
self.talker_api, self.series_name, refresh, self.literal, self.settings.id_series_match_search_thresh
|
||||
self.talker_api,
|
||||
self.series_name,
|
||||
refresh,
|
||||
self.literal,
|
||||
self.options["comicvine"]["series_match_search_thresh"],
|
||||
)
|
||||
self.search_thread.searchComplete.connect(self.search_complete)
|
||||
self.search_thread.progressUpdate.connect(self.search_progress_update)
|
||||
@ -360,7 +368,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
def search_complete(self) -> None:
|
||||
if self.progdialog is not None:
|
||||
self.progdialog.accept()
|
||||
self.progdialog = None
|
||||
del self.progdialog
|
||||
if self.search_thread is not None and self.search_thread.ct_error:
|
||||
# TODO Currently still opens the window
|
||||
QtWidgets.QMessageBox.critical(
|
||||
@ -374,7 +382,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
# filter the publishers if enabled set
|
||||
if self.use_filter:
|
||||
try:
|
||||
publisher_filter = {s.strip().casefold() for s in self.settings.id_publisher_filter.split(",")}
|
||||
publisher_filter = {s.strip().casefold() for s in self.options["identifier"]["publisher_filter"]}
|
||||
# use '' as publisher name if None
|
||||
self.ct_search_results = list(
|
||||
filter(
|
||||
@ -390,7 +398,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
# compare as str in case extra chars ie. '1976?'
|
||||
# - missing (none) values being converted to 'None' - consistent with prior behaviour in v1.2.3
|
||||
# sort by start_year if set
|
||||
if self.settings.sort_series_by_year:
|
||||
if self.options["comicvine"]["sort_series_by_year"]:
|
||||
try:
|
||||
self.ct_search_results = sorted(
|
||||
self.ct_search_results,
|
||||
@ -408,7 +416,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
logger.exception("bad data error sorting results by count_of_issues")
|
||||
|
||||
# move sanitized matches to the front
|
||||
if self.settings.exact_series_matches_first:
|
||||
if self.options["comicvine"]["exact_series_matches_first"]:
|
||||
try:
|
||||
sanitized = utils.sanitize_title(self.series_name, False).casefold()
|
||||
sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold()
|
||||
|
@ -1,3 +1,4 @@
|
||||
appdirs==1.4.4
|
||||
beautifulsoup4>=4.1
|
||||
importlib_metadata>=3.3.0
|
||||
natsort>=8.1.0
|
||||
|
2
setup.py
2
setup.py
@ -14,7 +14,7 @@ import os
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
def read(fname):
|
||||
def read(fname: str) -> str:
|
||||
"""
|
||||
Read the contents of a file.
|
||||
Parameters
|
||||
|
@ -7,8 +7,8 @@ from testing.comicdata import search_results
|
||||
|
||||
|
||||
def test_create_cache(settings, mock_version):
|
||||
comictalker.comiccacher.ComicCacher(settings.get_settings_folder(), mock_version[0])
|
||||
assert (settings.get_settings_folder() / "settings").exists()
|
||||
comictalker.comiccacher.ComicCacher(settings["runtime"]["config"].user_cache_dir, mock_version[0])
|
||||
assert (settings["runtime"]["config"].user_cache_dir).exists()
|
||||
|
||||
|
||||
def test_search_results(comic_cache):
|
||||
|
@ -17,6 +17,7 @@ import comictaggerlib.settings
|
||||
import comictalker.comiccacher
|
||||
import comictalker.talkers.comicvine
|
||||
from comicapi import utils
|
||||
from comictaggerlib import settings as ctsettings
|
||||
from testing import comicvine, filenames
|
||||
from testing.comicdata import all_seed_imprints, seed_imprints
|
||||
|
||||
@ -116,7 +117,7 @@ def comicvine_api(
|
||||
|
||||
cv = comictalker.talkers.comicvine.ComicVineTalker(
|
||||
version=mock_version[0],
|
||||
cache_folder=settings.get_settings_folder(),
|
||||
cache_folder=settings["runtime"]["config"].user_cache_dir,
|
||||
api_url="",
|
||||
api_key="",
|
||||
series_match_thresh=90,
|
||||
@ -163,9 +164,20 @@ def seed_all_publishers(monkeypatch):
|
||||
|
||||
@pytest.fixture
|
||||
def settings(tmp_path):
|
||||
yield comictaggerlib.settings.ComicTaggerSettings(tmp_path / "settings")
|
||||
|
||||
manager = ctsettings.Manager()
|
||||
ctsettings.register_commandline(manager)
|
||||
ctsettings.register_settings(manager)
|
||||
defaults = manager.defaults()
|
||||
defaults["runtime"]["config"] = ctsettings.ComicTaggerPaths(tmp_path / "config")
|
||||
defaults["runtime"]["config"].user_data_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults["runtime"]["config"].user_config_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults["runtime"]["config"].user_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults["runtime"]["config"].user_state_dir.mkdir(parents=True, exist_ok=True)
|
||||
defaults["runtime"]["config"].user_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
yield defaults
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def comic_cache(settings, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]:
|
||||
yield comictalker.comiccacher.ComicCacher(settings.get_settings_folder(), mock_version[0])
|
||||
yield comictalker.comiccacher.ComicCacher(settings["runtime"]["config"].user_cache_dir, mock_version[0])
|
||||
|
Loading…
x
Reference in New Issue
Block a user