Merge branch 'settings' into develop

This commit is contained in:
Timmy Welch 2022-12-15 20:17:50 -08:00
commit 9004ee1a6b
No known key found for this signature in database
39 changed files with 2002 additions and 1680 deletions

View File

@ -31,7 +31,7 @@ repos:
rev: v1.7.7
hooks:
- id: autoflake
args: [-i]
args: [-i, --remove-all-unused-imports, --ignore-init-module-imports]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:

View File

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

View File

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

View File

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

View File

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

View File

@ -17,16 +17,16 @@ from __future__ import annotations
import logging
import settngs
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
logger = logging.getLogger(__name__)
class AutoTagStartWindow(QtWidgets.QDialog):
def __init__(self, parent: QtWidgets.QWidget, settings: ComicTaggerSettings, msg: str) -> None:
def __init__(self, parent: QtWidgets.QWidget, options: settngs.Namespace, msg: str) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "autotagstartwindow.ui", self)
@ -36,19 +36,20 @@ class AutoTagStartWindow(QtWidgets.QDialog):
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.settings = settings
self.options = options
self.cbxSpecifySearchString.setChecked(False)
self.cbxSplitWords.setChecked(False)
self.sbNameMatchSearchThresh.setValue(self.settings.id_series_match_identify_thresh)
self.sbNameMatchSearchThresh.setValue(self.options.identifier_series_match_identify_thresh)
self.leSearchString.setEnabled(False)
self.cbxSaveOnLowConfidence.setChecked(self.settings.save_on_low_confidence)
self.cbxDontUseYear.setChecked(self.settings.dont_use_year_when_identifying)
self.cbxAssumeIssueOne.setChecked(self.settings.assume_1_if_no_issue_num)
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.settings.ignore_leading_numbers_in_filename)
self.cbxRemoveAfterSuccess.setChecked(self.settings.remove_archive_after_successful_match)
self.cbxAutoImprint.setChecked(self.settings.auto_imprint)
self.cbxSaveOnLowConfidence.setChecked(self.options.autotag_save_on_low_confidence)
self.cbxDontUseYear.setChecked(self.options.autotag_dont_use_year_when_identifying)
self.cbxAssumeIssueOne.setChecked(self.options.autotag_assume_1_if_no_issue_num)
self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.options.autotag_ignore_leading_numbers_in_filename)
self.cbxRemoveAfterSuccess.setChecked(self.options.autotag_remove_archive_after_successful_match)
self.cbxWaitForRateLimit.setChecked(self.options.autotag_wait_and_retry_on_rate_limit)
self.cbxAutoImprint.setChecked(self.options.comicvine_auto_imprint)
nlmt_tip = """<html>The <b>Name Match Ratio Threshold: Auto-Identify</b> is for eliminating automatic
search matches that are too long compared to your series name search. The lower
@ -72,8 +73,9 @@ class AutoTagStartWindow(QtWidgets.QDialog):
self.assume_issue_one = False
self.ignore_leading_digits_in_filename = False
self.remove_after_success = False
self.wait_and_retry_on_rate_limit = False
self.search_string = ""
self.name_length_match_tolerance = self.settings.id_series_match_search_thresh
self.name_length_match_tolerance = self.options.comicvine_series_match_search_thresh
self.split_words = self.cbxSplitWords.isChecked()
def search_string_toggle(self) -> None:
@ -89,14 +91,16 @@ class AutoTagStartWindow(QtWidgets.QDialog):
self.ignore_leading_digits_in_filename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
self.remove_after_success = self.cbxRemoveAfterSuccess.isChecked()
self.name_length_match_tolerance = self.sbNameMatchSearchThresh.value()
self.wait_and_retry_on_rate_limit = self.cbxWaitForRateLimit.isChecked()
self.split_words = self.cbxSplitWords.isChecked()
# persist some settings
self.settings.save_on_low_confidence = self.auto_save_on_low
self.settings.dont_use_year_when_identifying = self.dont_use_year
self.settings.assume_1_if_no_issue_num = self.assume_issue_one
self.settings.ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
self.settings.remove_archive_after_successful_match = self.remove_after_success
self.options.autotag_save_on_low_confidence = self.auto_save_on_low
self.options.autotag_dont_use_year_when_identifying = self.dont_use_year
self.options.autotag_assume_1_if_no_issue_num = self.assume_issue_one
self.options.autotag_ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
self.options.autotag_remove_archive_after_successful_match = self.remove_after_success
self.options.autotag_wait_and_retry_on_rate_limit = self.wait_and_retry_on_rate_limit
if self.cbxSpecifySearchString.isChecked():
self.search_string = self.leSearchString.text()

View File

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

View File

@ -16,15 +16,15 @@
# limitations under the License.
from __future__ import annotations
import argparse
import json
import logging
import os
import pathlib
import sys
from datetime import datetime
from pprint import pprint
import settngs
from comicapi import utils
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
@ -34,229 +34,187 @@ from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
from comictaggerlib.graphics import graphics_path
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
from comictaggerlib.settings import ComicTaggerSettings
from comictalker.talkerbase import ComicTalker, TalkerError
logger = logging.getLogger(__name__)
def actual_issue_data_fetch(
issue_id: int, settings: ComicTaggerSettings, opts: argparse.Namespace, talker_api: ComicTalker
) -> GenericMetadata:
# now get the particular issue data
try:
ct_md = talker_api.fetch_comic_data(issue_id)
except TalkerError as e:
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
return GenericMetadata()
class CLI:
def __init__(self, options: settngs.Values, talker_api: ComicTalker):
self.options = options
self.talker_api = talker_api
self.batch_mode = False
if settings.apply_cbl_transform_on_cv_import:
ct_md = CBLTransformer(ct_md, settings).apply()
def actual_issue_data_fetch(self, issue_id: int) -> GenericMetadata:
# now get the particular issue data
try:
ct_md = self.talker_api.fetch_comic_data(issue_id)
except TalkerError as e:
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
return GenericMetadata()
return ct_md
if self.options.cbl_apply_transform_on_import:
ct_md = CBLTransformer(ct_md, self.options).apply()
return ct_md
def actual_metadata_save(ca: ComicArchive, opts: argparse.Namespace, md: GenericMetadata) -> bool:
if not opts.dryrun:
for metadata_style in opts.type:
# write out the new data
if not ca.write_metadata(md, metadata_style):
logger.error("The tag save seemed to fail for style: %s!", MetaDataStyle.name[metadata_style])
return False
def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool:
if not self.options.runtime_dryrun:
for metadata_style in self.options.runtime_type:
# write out the new data
if not ca.write_metadata(md, metadata_style):
logger.error("The tag save seemed to fail for style: %s!", MetaDataStyle.name[metadata_style])
return False
print("Save complete.")
logger.info("Save complete.")
else:
if opts.terse:
logger.info("dry-run option was set, so nothing was written")
print("dry-run option was set, so nothing was written")
print("Save complete.")
logger.info("Save complete.")
else:
logger.info("dry-run option was set, so nothing was written, but here is the final set of tags:")
print("dry-run option was set, so nothing was written, but here is the final set of tags:")
print(f"{md}")
return True
def display_match_set_for_choice(
label: str,
match_set: MultipleMatch,
opts: argparse.Namespace,
settings: ComicTaggerSettings,
talker_api: ComicTalker,
) -> None:
print(f"{match_set.ca.path} -- {label}:")
# sort match list by year
match_set.matches.sort(key=lambda k: k["year"] or 0)
for (counter, m) in enumerate(match_set.matches):
counter += 1
print(
" {}. {} #{} [{}] ({}/{}) - {}".format(
counter,
m["series"],
m["issue_number"],
m["publisher"],
m["month"],
m["year"],
m["issue_title"],
)
)
if opts.interactive:
while True:
i = input("Choose a match #, or 's' to skip: ")
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
break
if i != "s":
# save the data!
# we know at this point, that the file is all good to go
ca = match_set.ca
md = create_local_metadata(opts, ca, settings)
ct_md = actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"], settings, opts, talker_api)
if opts.overwrite:
md = ct_md
if self.options.runtime_quiet:
logger.info("dry-run option was set, so nothing was written")
print("dry-run option was set, so nothing was written")
else:
notes = (
f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on"
f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {ct_md.issue_id}]"
logger.info("dry-run option was set, so nothing was written, but here is the final set of tags:")
print("dry-run option was set, so nothing was written, but here is the final set of tags:")
print(f"{md}")
return True
def display_match_set_for_choice(self, label: str, match_set: MultipleMatch) -> None:
print(f"{match_set.ca.path} -- {label}:")
# sort match list by year
match_set.matches.sort(key=lambda k: k["year"] or 0)
for (counter, m) in enumerate(match_set.matches):
counter += 1
print(
" {}. {} #{} [{}] ({}/{}) - {}".format(
counter,
m["series"],
m["issue_number"],
m["publisher"],
m["month"],
m["year"],
m["issue_title"],
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
)
if self.options.runtime_interactive:
while True:
i = input("Choose a match #, or 's' to skip: ")
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
break
if i != "s":
# save the data!
# we know at this point, that the file is all good to go
ca = match_set.ca
md = self.create_local_metadata(ca)
ct_md = self.actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"])
if self.options.comicvine_clear_metadata_on_import:
md = ct_md
else:
notes = (
f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on"
f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {ct_md.issue_id}]"
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
if opts.auto_imprint:
md.fix_publisher()
if self.options.comicvine_auto_imprint:
md.fix_publisher()
actual_metadata_save(ca, opts, md)
self.actual_metadata_save(ca, md)
def post_process_matches(self, match_results: OnlineMatchResults) -> None:
# now go through the match results
if self.options.runtime_summary:
if len(match_results.good_matches) > 0:
print("\nSuccessful matches:\n------------------")
for f in match_results.good_matches:
print(f)
def post_process_matches(
match_results: OnlineMatchResults, opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker
) -> None:
# now go through the match results
if opts.show_save_summary:
if len(match_results.good_matches) > 0:
print("\nSuccessful matches:\n------------------")
for f in match_results.good_matches:
print(f)
if len(match_results.no_matches) > 0:
print("\nNo matches:\n------------------")
for f in match_results.no_matches:
print(f)
if len(match_results.no_matches) > 0:
print("\nNo matches:\n------------------")
for f in match_results.no_matches:
print(f)
if len(match_results.write_failures) > 0:
print("\nFile Write Failures:\n------------------")
for f in match_results.write_failures:
print(f)
if len(match_results.write_failures) > 0:
print("\nFile Write Failures:\n------------------")
for f in match_results.write_failures:
print(f)
if len(match_results.fetch_data_failures) > 0:
print("\nNetwork Data Fetch Failures:\n------------------")
for f in match_results.fetch_data_failures:
print(f)
if len(match_results.fetch_data_failures) > 0:
print("\nNetwork Data Fetch Failures:\n------------------")
for f in match_results.fetch_data_failures:
print(f)
if not self.options.runtime_summary and not self.options.runtime_interactive:
# just quit if we're not interactive or showing the summary
return
if not opts.show_save_summary and not opts.interactive:
# just quit if we're not interactive or showing the summary
return
if len(match_results.multiple_matches) > 0:
print("\nArchives with multiple high-confidence matches:\n------------------")
for match_set in match_results.multiple_matches:
self.display_match_set_for_choice("Multiple high-confidence matches", match_set)
if len(match_results.multiple_matches) > 0:
print("\nArchives with multiple high-confidence matches:\n------------------")
for match_set in match_results.multiple_matches:
display_match_set_for_choice("Multiple high-confidence matches", match_set, opts, settings, talker_api)
if len(match_results.low_confidence_matches) > 0:
print("\nArchives with low-confidence matches:\n------------------")
for match_set in match_results.low_confidence_matches:
if len(match_set.matches) == 1:
label = "Single low-confidence match"
else:
label = "Multiple low-confidence matches"
if len(match_results.low_confidence_matches) > 0:
print("\nArchives with low-confidence matches:\n------------------")
for match_set in match_results.low_confidence_matches:
if len(match_set.matches) == 1:
label = "Single low-confidence match"
else:
label = "Multiple low-confidence matches"
self.display_match_set_for_choice(label, match_set)
display_match_set_for_choice(label, match_set, opts, settings, talker_api)
def run(self) -> None:
if len(self.options.runtime_file_list) < 1:
logger.error("You must specify at least one filename. Use the -h option for more info")
return
match_results = OnlineMatchResults()
self.batch_mode = len(self.options.runtime_file_list) > 1
def cli_mode(opts: argparse.Namespace, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
if len(opts.file_list) < 1:
logger.error("You must specify at least one filename. Use the -h option for more info")
return
for f in self.options.runtime_file_list:
self.process_file_cli(f, match_results)
sys.stdout.flush()
match_results = OnlineMatchResults()
self.post_process_matches(match_results)
for f in opts.file_list:
process_file_cli(f, opts, settings, talker_api, match_results)
sys.stdout.flush()
def create_local_metadata(self, ca: ComicArchive) -> GenericMetadata:
md = GenericMetadata()
md.set_default_page_list(ca.get_number_of_pages())
post_process_matches(match_results, opts, settings, talker_api)
# now, overlay the parsed filename info
if self.options.runtime_parse_filename:
f_md = ca.metadata_from_filename(
self.options.filename_complicated_parser,
self.options.filename_remove_c2c,
self.options.filename_remove_fcbd,
self.options.filename_remove_publisher,
self.options.runtime_split_words,
)
md.overlay(f_md)
def create_local_metadata(opts: argparse.Namespace, ca: ComicArchive, settings: ComicTaggerSettings) -> GenericMetadata:
md = GenericMetadata()
md.set_default_page_list(ca.get_number_of_pages())
for metadata_style in self.options.runtime_type:
if ca.has_metadata(metadata_style):
try:
t_md = ca.read_metadata(metadata_style)
md.overlay(t_md)
break
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
# now, overlay the parsed filename info
if opts.parse_filename:
f_md = ca.metadata_from_filename(
settings.complicated_parser,
settings.remove_c2c,
settings.remove_fcbd,
settings.remove_publisher,
opts.split_words,
)
# finally, use explicit stuff
md.overlay(self.options.runtime_metadata)
md.overlay(f_md)
return md
for metadata_style in opts.type:
if ca.has_metadata(metadata_style):
try:
t_md = ca.read_metadata(metadata_style)
md.overlay(t_md)
break
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
# finally, use explicit stuff
md.overlay(opts.metadata)
return md
def process_file_cli(
filename: str,
opts: argparse.Namespace,
settings: ComicTaggerSettings,
talker_api: ComicTalker,
match_results: OnlineMatchResults,
) -> None:
batch_mode = len(opts.file_list) > 1
ca = ComicArchive(filename, settings.rar_exe_path, str(graphics_path / "nocover.png"))
if not os.path.lexists(filename):
logger.error("Cannot find %s", filename)
return
if not ca.seems_to_be_a_comic_archive():
logger.error("Sorry, but %s is not a comic archive!", filename)
return
if not ca.is_writable() and (opts.delete or opts.copy or opts.save or opts.rename):
logger.error("This archive is not writable")
return
has = [False, False, False]
if ca.has_cix():
has[MetaDataStyle.CIX] = True
if ca.has_cbi():
has[MetaDataStyle.CBI] = True
if ca.has_comet():
has[MetaDataStyle.COMET] = True
if opts.print:
if not opts.type:
def print(self, ca: ComicArchive) -> None:
if not self.options.runtime_type:
page_count = ca.get_number_of_pages()
brief = ""
if batch_mode:
if self.batch_mode:
brief = f"{ca.path}: "
if ca.is_sevenzip():
@ -271,146 +229,149 @@ def process_file_cli(
brief += f"({page_count: >3} pages)"
brief += " tags:[ "
if not (has[MetaDataStyle.CBI] or has[MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
if not (
ca.has_metadata(MetaDataStyle.CBI)
or ca.has_metadata(MetaDataStyle.CIX)
or ca.has_metadata(MetaDataStyle.COMET)
):
brief += "none "
else:
if has[MetaDataStyle.CBI]:
if ca.has_metadata(MetaDataStyle.CBI):
brief += "CBL "
if has[MetaDataStyle.CIX]:
if ca.has_metadata(MetaDataStyle.CIX):
brief += "CR "
if has[MetaDataStyle.COMET]:
if ca.has_metadata(MetaDataStyle.COMET):
brief += "CoMet "
brief += "]"
print(brief)
if opts.terse:
if self.options.runtime_quiet:
return
print()
if not opts.type or MetaDataStyle.CIX in opts.type:
if has[MetaDataStyle.CIX]:
if not self.options.runtime_type or MetaDataStyle.CIX in self.options.runtime_type:
if ca.has_metadata(MetaDataStyle.CIX):
print("--------- ComicRack tags ---------")
try:
if opts.raw:
if self.options.runtime_raw:
print(ca.read_raw_cix())
else:
print(ca.read_cix())
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
if not opts.type or MetaDataStyle.CBI in opts.type:
if has[MetaDataStyle.CBI]:
if not self.options.runtime_type or MetaDataStyle.CBI in self.options.runtime_type:
if ca.has_metadata(MetaDataStyle.CBI):
print("------- ComicBookLover tags -------")
try:
if opts.raw:
if self.options.runtime_raw:
pprint(json.loads(ca.read_raw_cbi()))
else:
print(ca.read_cbi())
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
if not opts.type or MetaDataStyle.COMET in opts.type:
if has[MetaDataStyle.COMET]:
if not self.options.runtime_type or MetaDataStyle.COMET in self.options.runtime_type:
if ca.has_metadata(MetaDataStyle.COMET):
print("----------- CoMet tags -----------")
try:
if opts.raw:
if self.options.runtime_raw:
print(ca.read_raw_comet())
else:
print(ca.read_comet())
except Exception as e:
logger.error("Failed to load metadata for %s: %s", ca.path, e)
elif opts.delete:
for metadata_style in opts.type:
def delete(self, ca: ComicArchive) -> None:
for metadata_style in self.options.runtime_type:
style_name = MetaDataStyle.name[metadata_style]
if has[metadata_style]:
if not opts.dryrun:
if ca.has_metadata(metadata_style):
if not self.options.runtime_dryrun:
if not ca.remove_metadata(metadata_style):
print(f"{filename}: Tag removal seemed to fail!")
print(f"{ca.path}: Tag removal seemed to fail!")
else:
print(f"{filename}: Removed {style_name} tags.")
print(f"{ca.path}: Removed {style_name} tags.")
else:
print(f"{filename}: dry-run. {style_name} tags not removed")
print(f"{ca.path}: dry-run. {style_name} tags not removed")
else:
print(f"{filename}: This archive doesn't have {style_name} tags to remove.")
print(f"{ca.path}: This archive doesn't have {style_name} tags to remove.")
elif opts.copy is not None:
for metadata_style in opts.type:
def copy(self, ca: ComicArchive) -> None:
for metadata_style in self.options.runtime_type:
dst_style_name = MetaDataStyle.name[metadata_style]
if opts.no_overwrite and has[metadata_style]:
print(f"{filename}: Already has {dst_style_name} tags. Not overwriting.")
if not self.options.runtime_overwrite and ca.has_metadata(metadata_style):
print(f"{ca.path}: Already has {dst_style_name} tags. Not overwriting.")
return
if opts.copy == metadata_style:
print(f"{filename}: Destination and source are same: {dst_style_name}. Nothing to do.")
if self.options.commands_copy == metadata_style:
print(f"{ca.path}: Destination and source are same: {dst_style_name}. Nothing to do.")
return
src_style_name = MetaDataStyle.name[opts.copy]
if has[opts.copy]:
if not opts.dryrun:
src_style_name = MetaDataStyle.name[self.options.commands_copy]
if ca.has_metadata(self.options.commands_copy):
if not self.options.runtime_dryrun:
try:
md = ca.read_metadata(opts.copy)
md = ca.read_metadata(self.options.commands_copy)
except Exception as e:
md = GenericMetadata()
logger.error("Failed to load metadata for %s: %s", ca.path, e)
if settings.apply_cbl_transform_on_bulk_operation and metadata_style == MetaDataStyle.CBI:
md = CBLTransformer(md, settings).apply()
if self.options.apply_transform_on_bulk_operation_ndetadata_style == MetaDataStyle.CBI:
md = CBLTransformer(md, self.options).apply()
if not ca.write_metadata(md, metadata_style):
print(f"{filename}: Tag copy seemed to fail!")
print(f"{ca.path}: Tag copy seemed to fail!")
else:
print(f"{filename}: Copied {src_style_name} tags to {dst_style_name}.")
print(f"{ca.path}: Copied {src_style_name} tags to {dst_style_name}.")
else:
print(f"{filename}: dry-run. {src_style_name} tags not copied")
print(f"{ca.path}: dry-run. {src_style_name} tags not copied")
else:
print(f"{filename}: This archive doesn't have {src_style_name} tags to copy.")
print(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.")
elif opts.save:
if opts.no_overwrite:
for metadata_style in opts.type:
if has[metadata_style]:
print(f"{filename}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> None:
if not self.options.runtime_overwrite:
for metadata_style in self.options.runtime_type:
if ca.has_metadata(metadata_style):
print(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.")
return
if batch_mode:
if self.batch_mode:
print(f"Processing {ca.path}...")
md = create_local_metadata(opts, ca, settings)
md = self.create_local_metadata(ca)
if md.issue is None or md.issue == "":
if opts.assume_issue_one:
if self.options.runtime_assume_issue_one:
md.issue = "1"
# now, search online
if opts.online:
if opts.issue_id is not None:
if self.options.runtime_online:
if self.options.runtime_issue_id is not None:
# we were given the actual issue ID to search with
try:
ct_md = talker_api.fetch_comic_data(opts.issue_id)
ct_md = self.talker_api.fetch_comic_data(self.options.runtime_issue_id)
except TalkerError as e:
logger.exception(f"Error retrieving issue details. Save aborted.\n{e}")
match_results.fetch_data_failures.append(str(ca.path.absolute()))
return
if ct_md is None:
logger.error("No match for ID %s was found.", opts.issue_id)
logger.error("No match for ID %s was found.", self.options.runtime_issue_id)
match_results.no_matches.append(str(ca.path.absolute()))
return
if settings.apply_cbl_transform_on_cv_import:
ct_md = CBLTransformer(ct_md, settings).apply()
if self.options.cbl_apply_transform_on_import:
ct_md = CBLTransformer(ct_md, self.options).apply()
else:
if md is None or md.is_empty:
logger.error("No metadata given to search online with!")
match_results.no_matches.append(str(ca.path.absolute()))
return
ii = IssueIdentifier(ca, settings, talker_api)
ii = IssueIdentifier(ca, self.options, self.talker_api)
def myoutput(text: str) -> None:
if opts.verbose:
if self.options.runtime_verbose:
IssueIdentifier.default_write_output(text)
# use our overlaid MD struct to search
@ -450,7 +411,7 @@ def process_file_cli(
logger.error("Online search: Multiple good matches. Save aborted")
match_results.multiple_matches.append(MultipleMatch(ca, matches))
return
if low_confidence and opts.abort_on_low_confidence:
if low_confidence and self.options.runtime_abort_on_low_confidence:
logger.error("Online search: Low confidence match. Save aborted")
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
return
@ -462,12 +423,12 @@ def process_file_cli(
# we got here, so we have a single match
# now get the particular issue data
ct_md = actual_issue_data_fetch(matches[0]["issue_id"], settings, opts, talker_api)
ct_md = self.actual_issue_data_fetch(matches[0]["issue_id"])
if ct_md.is_empty:
match_results.fetch_data_failures.append(str(ca.path.absolute()))
return
if opts.overwrite:
if self.options.comicvine_clear_metadata_on_import:
md = ct_md
else:
notes = (
@ -476,29 +437,29 @@ def process_file_cli(
)
md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger")))
if opts.auto_imprint:
if self.options.comicvine_auto_imprint:
md.fix_publisher()
# ok, done building our metadata. time to save
if not actual_metadata_save(ca, opts, md):
if not self.actual_metadata_save(ca, md):
match_results.write_failures.append(str(ca.path.absolute()))
else:
match_results.good_matches.append(str(ca.path.absolute()))
elif opts.rename:
def rename(self, ca: ComicArchive) -> None:
original_path = ca.path
msg_hdr = ""
if batch_mode:
if self.batch_mode:
msg_hdr = f"{ca.path}: "
md = create_local_metadata(opts, ca, settings)
md = self.create_local_metadata(ca)
if md.series is None:
logger.error(msg_hdr + "Can't rename without series name")
return
new_ext = "" # default
if settings.rename_extension_based_on_archive:
if self.options.filename_rename_set_extension_based_on_archive:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
@ -506,11 +467,15 @@ def process_file_cli(
elif ca.is_rar():
new_ext = ".cbr"
renamer = FileRenamer(md, platform="universal" if settings.rename_strict else "auto")
renamer.set_template(settings.rename_template)
renamer.set_issue_zero_padding(settings.rename_issue_number_padding)
renamer.set_smart_cleanup(settings.rename_use_smart_string_cleanup)
renamer.move = settings.rename_move_dir
renamer = FileRenamer(
md,
platform="universal" if self.options.filename_rename_strict else "auto",
replacements=self.options.rename_replacements,
)
renamer.set_template(self.options.filename_rename_template)
renamer.set_issue_zero_padding(self.options.filename_rename_issue_number_padding)
renamer.set_smart_cleanup(self.options.filename_rename_use_smart_string_cleanup)
renamer.move = self.options.filename_rename_move_to_dir
try:
new_name = renamer.determine_name(ext=new_ext)
@ -522,13 +487,17 @@ def process_file_cli(
"Please consult the template help in the settings "
"and the documentation on the format at "
"https://docs.python.org/3/library/string.html#format-string-syntax",
settings.rename_template,
self.options.filename_rename_template,
)
return
except Exception:
logger.exception("Formatter failure: %s metadata: %s", settings.rename_template, renamer.metadata)
logger.exception(
"Formatter failure: %s metadata: %s", self.options.filename_rename_template, renamer.metadata
)
folder = get_rename_dir(ca, settings.rename_dir if settings.rename_move_dir else None)
folder = get_rename_dir(
ca, self.options.filename_rename_dir if self.options.filename_rename_move_to_dir else None
)
full_path = folder / new_name
@ -537,7 +506,7 @@ def process_file_cli(
return
suffix = ""
if not opts.dryrun:
if not self.options.runtime_dryrun:
# rename the file
try:
ca.rename(utils.unique_file(full_path))
@ -548,19 +517,19 @@ def process_file_cli(
print(f"renamed '{original_path.name}' -> '{new_name}' {suffix}")
elif opts.export_to_zip:
def export(self, ca: ComicArchive) -> None:
msg_hdr = ""
if batch_mode:
if self.batch_mode:
msg_hdr = f"{ca.path}: "
if ca.is_zip():
logger.error(msg_hdr + "Archive is already a zip file.")
return
filename_path = pathlib.Path(filename).absolute()
filename_path = ca.path
new_file = filename_path.with_suffix(".cbz")
if opts.abort_on_conflict and new_file.exists():
if self.options.runtime_abort_on_conflict and new_file.exists():
print(msg_hdr + f"{new_file.name} already exists in the that folder.")
return
@ -568,10 +537,10 @@ def process_file_cli(
delete_success = False
export_success = False
if not opts.dryrun:
if not self.options.runtime_dryrun:
if ca.export_as_zip(new_file):
export_success = True
if opts.delete_after_zip_export:
if self.options.runtime_delete_after_zip_export:
try:
filename_path.unlink(missing_ok=True)
delete_success = True
@ -583,7 +552,7 @@ def process_file_cli(
new_file.unlink(missing_ok=True)
else:
msg = msg_hdr + f"Dry-run: Would try to create {os.path.split(new_file)[1]}"
if opts.delete_after_zip_export:
if self.options.runtime_delete_after_zip_export:
msg += " and delete original."
print(msg)
return
@ -591,9 +560,47 @@ def process_file_cli(
msg = msg_hdr
if export_success:
msg += f"Archive exported successfully to: {os.path.split(new_file)[1]}"
if opts.delete_after_zip_export and delete_success:
if self.options.runtime_delete_after_zip_export and delete_success:
msg += " (Original deleted) "
else:
msg += "Archive failed to export!"
print(msg)
def process_file_cli(self, filename: str, match_results: OnlineMatchResults) -> None:
if not os.path.lexists(filename):
logger.error("Cannot find %s", filename)
return
ca = ComicArchive(filename, self.options.general_rar_exe_path, str(graphics_path / "nocover.png"))
if not ca.seems_to_be_a_comic_archive():
logger.error("Sorry, but %s is not a comic archive!", filename)
return
if not ca.is_writable() and (
self.options.commands_delete
or self.options.commands_copy
or self.options.commands_save
or self.options.commands_rename
):
logger.error("This archive is not writable")
return
if self.options.commands_print:
self.print(ca)
elif self.options.commands_delete:
self.delete(ca)
elif self.options.commands_copy is not None:
self.copy(ca)
elif self.options.commands_save:
self.save(ca, match_results)
elif self.options.commands_rename:
self.rename(ca)
elif self.options.commands_export_to_zip:
self.export(ca)

View File

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

View File

@ -0,0 +1,14 @@
from __future__ import annotations
from comictaggerlib.ctoptions.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options
from comictaggerlib.ctoptions.file import register_settings, validate_settings
from comictaggerlib.ctoptions.types import ComicTaggerPaths
__all__ = [
"initial_cmd_line_parser",
"register_commandline",
"register_settings",
"validate_commandline_options",
"validate_settings",
"ComicTaggerPaths",
]

View File

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

View File

@ -0,0 +1,262 @@
from __future__ import annotations
import argparse
import uuid
from typing import Any
import settngs
from comictaggerlib.ctoptions.types import AppendAction
from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
def general(parser: settngs.Manager) -> None:
# General Settings
parser.add_setting("--rar-exe-path", default="rar", help="The path to the rar program")
parser.add_setting(
"--allow-cbi-in-rar",
default=True,
action=argparse.BooleanOptionalAction,
help="Allows ComicBookLover tags in RAR/CBR files",
)
parser.add_setting("check_for_new_version", default=False, cmdline=False)
parser.add_setting("send_usage_stats", default=False, cmdline=False)
def internal(parser: settngs.Manager) -> None:
# automatic settings
parser.add_setting("install_id", default=uuid.uuid4().hex, cmdline=False)
parser.add_setting("save_data_style", default=0, cmdline=False)
parser.add_setting("load_data_style", default=0, cmdline=False)
parser.add_setting("last_opened_folder", default="", cmdline=False)
parser.add_setting("window_width", default=0, cmdline=False)
parser.add_setting("window_height", default=0, cmdline=False)
parser.add_setting("window_x", default=0, cmdline=False)
parser.add_setting("window_y", default=0, cmdline=False)
parser.add_setting("form_width", default=-1, cmdline=False)
parser.add_setting("list_width", default=-1, cmdline=False)
parser.add_setting("sort_column", default=-1, cmdline=False)
parser.add_setting("sort_direction", default=0, cmdline=False)
def identifier(parser: settngs.Manager) -> None:
# identifier settings
parser.add_setting("--series-match-identify-thresh", default=91, type=int, help="")
parser.add_setting(
"--publisher-filter",
default=["Panini Comics", "Abril", "Planeta DeAgostini", "Editorial Televisa", "Dino Comics"],
action=AppendAction,
help="When enabled filters the listed publishers from all search results",
)
def dialog(parser: settngs.Manager) -> None:
# Show/ask dialog flags
parser.add_setting("ask_about_cbi_in_rar", default=True, cmdline=False)
parser.add_setting("show_disclaimer", default=True, cmdline=False)
parser.add_setting("dont_notify_about_this_version", default="", cmdline=False)
parser.add_setting("ask_about_usage_stats", default=True, cmdline=False)
def filename(parser: settngs.Manager) -> None:
# filename parsing settings
parser.add_setting(
"--complicated-parser",
default=False,
action=argparse.BooleanOptionalAction,
help="Enables the new parser which tries to extract more information from filenames",
)
parser.add_setting(
"--remove-c2c",
default=False,
action=argparse.BooleanOptionalAction,
help="Removes c2c from filenames. Requires --complicated-parser",
)
parser.add_setting(
"--remove-fcbd",
default=False,
action=argparse.BooleanOptionalAction,
help="Removes FCBD/free comic book day from filenames. Requires --complicated-parser",
)
parser.add_setting(
"--remove-publisher",
default=False,
action=argparse.BooleanOptionalAction,
help="Attempts to remove publisher names from filenames, currently limited to Marvel and DC. Requires --complicated-parser",
)
def comicvine(parser: settngs.Manager) -> None:
# Comic Vine settings
parser.add_setting(
"--series-match-search-thresh",
default=90,
type=int,
)
parser.add_setting("--use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting(
"--clear-metadata",
default=True,
help="Clears all existing metadata during import, default is to merges metadata.\nMay be used in conjunction with -o, -f and -m.\n\n",
dest="clear_metadata_on_import",
action=argparse.BooleanOptionalAction,
)
parser.add_setting(
"--remove-html-tables",
default=False,
action=argparse.BooleanOptionalAction,
help="Removes html tables instead of converting them to text",
)
parser.add_setting(
"--cv-api-key",
help="Use the given Comic Vine API Key (persisted in settings).",
)
parser.add_setting(
"--cv-url",
help="Use the given Comic Vine URL (persisted in settings).",
)
parser.add_setting(
"-a",
"--auto-imprint",
action=argparse.BooleanOptionalAction,
default=False,
help="Enables the auto imprint functionality.\ne.g. if the publisher is set to 'vertigo' it\nwill be updated to 'DC Comics' and the imprint\nproperty will be set to 'Vertigo'.\n\n",
)
parser.add_setting(
"--sort-series-by-year", default=True, action=argparse.BooleanOptionalAction, help="Sorts series by year"
)
parser.add_setting(
"--exact-series-matches-first",
default=True,
action=argparse.BooleanOptionalAction,
help="Puts series that are an exact match at the top of the list",
)
parser.add_setting(
"--always-use-publisher-filter",
default=False,
action=argparse.BooleanOptionalAction,
help="Enables the publisher filter",
)
parser.add_setting(
"--clear-form-before-populating-from-cv",
default=False,
action=argparse.BooleanOptionalAction,
help="Clears all existing metadata when applying metadata from ComicVine",
)
def cbl(parser: settngs.Manager) -> None:
# CBL Transform settings
parser.add_setting("--assume-lone-credit-is-primary", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-characters-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-teams-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-locations-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-storyarcs-to-tags", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-notes-to-comments", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--copy-weblink-to-comments", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--apply-transform-on-import", default=False, action=argparse.BooleanOptionalAction)
parser.add_setting("--apply-transform-on-bulk-operation", default=False, action=argparse.BooleanOptionalAction)
def rename(parser: settngs.Manager) -> None:
# Rename settings
parser.add_setting("--template", default="{series} #{issue} ({year})", help="The teplate to use when renaming")
parser.add_setting(
"--issue-number-padding",
default=3,
type=int,
help="The minimum number of digits to use for the issue number when renaming",
)
parser.add_setting(
"--use-smart-string-cleanup",
default=True,
action=argparse.BooleanOptionalAction,
help="Attempts to intelligently cleanup whitespace when renaming",
)
parser.add_setting(
"--auto-extension",
dest="set_extension_based_on_archive",
default=True,
action=argparse.BooleanOptionalAction,
help="Automatically sets the extension based on the archive type e.g. cbr for rar, cbz for zip",
)
parser.add_setting("--dir", default="", help="The directory to move renamed files to")
parser.add_setting(
"--move",
dest="move_to_dir",
default=False,
action=argparse.BooleanOptionalAction,
help="Enables moving renamed files to a separate directory",
)
parser.add_setting(
"--strict",
default=False,
action=argparse.BooleanOptionalAction,
help="Ensures that filenames are valid for all OSs",
)
parser.add_setting(
"replacements",
default=DEFAULT_REPLACEMENTS,
cmdline=False,
)
def autotag(parser: settngs.Manager) -> None:
# Auto-tag stickies
parser.add_setting(
"--save-on-low-confidence",
default=False,
action=argparse.BooleanOptionalAction,
help="Automatically save metadata on low-confidence matches",
)
parser.add_setting(
"--dont-use-year-when-identifying",
default=False,
action=argparse.BooleanOptionalAction,
help="Ignore the year metadata attribute when identifying a comic",
)
parser.add_setting(
"-1",
"--assume-issue-one",
dest="assume_1_if_no_issue_num",
action=argparse.BooleanOptionalAction,
help="Assume issue number is 1 if not found (relevant for -s).\n\n",
default=False,
)
parser.add_setting(
"--ignore-leading-numbers-in-filename",
default=False,
action=argparse.BooleanOptionalAction,
help="When searching ignore leading numbers in the filename",
)
parser.add_setting("remove_archive_after_successful_match", default=False, cmdline=False)
parser.add_setting(
"-w",
"--wait-on-rate-limit",
dest="wait_and_retry_on_rate_limit",
action=argparse.BooleanOptionalAction,
default=True,
help="When encountering a Comic Vine rate limit\nerror, wait and retry query.\n\n",
)
def validate_settings(options: settngs.Config[settngs.Values], parser: settngs.Manager) -> dict[str, dict[str, Any]]:
options[0].identifier_publisher_filter = [x.strip() for x in options[0].identifier_publisher_filter if x.strip()]
options[0].rename_replacements = Replacements(
[Replacement(x[0], x[1], x[2]) for x in options[0].rename_replacements[0]],
[Replacement(x[0], x[1], x[2]) for x in options[0].rename_replacements[1]],
)
return options
def register_settings(parser: settngs.Manager) -> None:
parser.add_group("general", general, False)
parser.add_group("internal", internal, False)
parser.add_group("identifier", identifier, False)
parser.add_group("dialog", dialog, False)
parser.add_group("filename", filename, False)
parser.add_group("comicvine", comicvine, False)
parser.add_group("cbl", cbl, False)
parser.add_group("rename", rename, False)
parser.add_group("autotag", autotag, False)

View File

@ -0,0 +1,186 @@
from __future__ import annotations
import argparse
import pathlib
from collections.abc import Sequence
from typing import Any, Callable
from appdirs import AppDirs
from comicapi.comicarchive import MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
class ComicTaggerPaths(AppDirs):
def __init__(self, config_path: pathlib.Path | str | None = None) -> None:
super().__init__("ComicTagger", None, None, False, False)
self.path: pathlib.Path | None = None
if config_path:
self.path = pathlib.Path(config_path).absolute()
@property
def user_data_dir(self) -> pathlib.Path:
if self.path:
return self.path
return pathlib.Path(super().user_data_dir)
@property
def user_config_dir(self) -> pathlib.Path:
if self.path:
return self.path
return pathlib.Path(super().user_config_dir)
@property
def user_cache_dir(self) -> pathlib.Path:
if self.path:
path = self.path / "cache"
return path
return pathlib.Path(super().user_cache_dir)
@property
def user_state_dir(self) -> pathlib.Path:
if self.path:
return self.path
return pathlib.Path(super().user_state_dir)
@property
def user_log_dir(self) -> pathlib.Path:
if self.path:
path = self.path / "log"
return path
return pathlib.Path(super().user_log_dir)
@property
def site_data_dir(self) -> pathlib.Path:
return pathlib.Path(super().site_data_dir)
@property
def site_config_dir(self) -> pathlib.Path:
return pathlib.Path(super().site_config_dir)
def metadata_type(types: str) -> list[int]:
result = []
types = types.casefold()
for typ in types.split(","):
typ = typ.strip()
if typ not in MetaDataStyle.short_name:
choices = ", ".join(MetaDataStyle.short_name)
raise argparse.ArgumentTypeError(f"invalid choice: {typ} (choose from {choices.upper()})")
result.append(MetaDataStyle.short_name.index(typ))
return result
def _copy_items(items: Sequence[Any] | None) -> Sequence[Any]:
if items is None:
return []
# The copy module is used only in the 'append' and 'append_const'
# actions, and it is needed only when the default value isn't a list.
# Delay its import for speeding up the common case.
if type(items) is list:
return items[:]
import copy
return copy.copy(items)
class AppendAction(argparse.Action):
def __init__(
self,
option_strings: list[str],
dest: str,
nargs: str | None = None,
const: Any = None,
default: Any = None,
type: Callable[[str], Any] | None = None, # noqa: A002
choices: list[Any] | None = None,
required: bool = False,
help: str | None = None, # noqa: A002
metavar: str | None = None,
):
self.called = False
if nargs == 0:
raise ValueError(
"nargs for append actions must be != 0; if arg "
"strings are not supplying the value to append, "
"the append const action may be more appropriate"
)
if const is not None and nargs != argparse.OPTIONAL:
raise ValueError("nargs must be %r to supply const" % argparse.OPTIONAL)
super().__init__(
option_strings=option_strings,
dest=dest,
nargs=nargs,
const=const,
default=default,
type=type,
choices=choices,
required=required,
help=help,
metavar=metavar,
)
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None,
) -> None:
if values:
if not self.called:
setattr(namespace, self.dest, [])
items = getattr(namespace, self.dest, None)
items = _copy_items(items)
items.append(values) # type: ignore
setattr(namespace, self.dest, items)
def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
"""The metadata string is a comma separated list of name-value pairs
The names match the attributes of the internal metadata struct (for now)
The caret is the special "escape character", since it's not common in
natural language text
example = "series=Kickers^, Inc. ,issue=1, year=1986"
"""
escaped_comma = "^,"
escaped_equals = "^="
replacement_token = "<_~_>"
md = GenericMetadata()
# First, replace escaped commas with with a unique token (to be changed back later)
mdstr = mdstr.replace(escaped_comma, replacement_token)
tmp_list = mdstr.split(",")
md_list = []
for item in tmp_list:
item = item.replace(replacement_token, ",")
md_list.append(item)
# Now build a nice dict from the list
md_dict = {}
for item in md_list:
# Make sure to fix any escaped equal signs
i = item.replace(escaped_equals, replacement_token)
key, value = i.split("=")
value = value.replace(replacement_token, "=").strip()
key = key.strip()
if key.casefold() == "credit":
cred_attribs = value.split(":")
role = cred_attribs[0]
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
primary = len(cred_attribs) > 2
md.add_credit(person.strip(), role.strip(), primary)
else:
md_dict[key] = value
# Map the dict to the metadata object
for key, value in md_dict.items():
if not hasattr(md, key):
raise argparse.ArgumentTypeError(f"'{key}' is not a valid tag name")
else:
md.is_empty = False
setattr(md, key, value)
return md

View File

@ -0,0 +1,29 @@
from __future__ import annotations
from typing import NamedTuple
class Replacement(NamedTuple):
find: str
replce: str
strict_only: bool
class Replacements(NamedTuple):
literal_text: list[Replacement]
format_value: list[Replacement]
DEFAULT_REPLACEMENTS = Replacements(
literal_text=[
Replacement(": ", " - ", True),
Replacement(":", "-", True),
],
format_value=[
Replacement(": ", " - ", True),
Replacement(":", "-", True),
Replacement("/", "-", False),
Replacement("//", "--", False),
Replacement("\\", "-", True),
],
)

View File

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

View File

@ -20,42 +20,18 @@ import logging
import os
import pathlib
import string
from typing import Any, NamedTuple, cast
from typing import Any, cast
from pathvalidate import Platform, normalize_platform, sanitize_filename
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
from comictaggerlib.defaults import DEFAULT_REPLACEMENTS, Replacement, Replacements
logger = logging.getLogger(__name__)
class Replacement(NamedTuple):
find: str
replce: str
strict_only: bool
class Replacements(NamedTuple):
literal_text: list[Replacement]
format_value: list[Replacement]
REPLACEMENTS = Replacements(
literal_text=[
Replacement(": ", " - ", True),
Replacement(":", "-", True),
],
format_value=[
Replacement(": ", " - ", True),
Replacement(":", "-", True),
Replacement("/", "-", False),
Replacement("\\", "-", True),
],
)
def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> pathlib.Path:
folder = ca.path.parent.absolute()
if rename_dir is not None:
@ -67,7 +43,7 @@ def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> p
class MetadataFormatter(string.Formatter):
def __init__(
self, smart_cleanup: bool = False, platform: str = "auto", replacements: Replacements = REPLACEMENTS
self, smart_cleanup: bool = False, platform: str = "auto", replacements: Replacements = DEFAULT_REPLACEMENTS
) -> None:
super().__init__()
self.smart_cleanup = smart_cleanup
@ -200,13 +176,19 @@ class MetadataFormatter(string.Formatter):
class FileRenamer:
def __init__(self, metadata: GenericMetadata | None, platform: str = "auto") -> None:
def __init__(
self,
metadata: GenericMetadata | None,
platform: str = "auto",
replacements: Replacements = DEFAULT_REPLACEMENTS,
) -> None:
self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})"
self.smart_cleanup = True
self.issue_zero_padding = 3
self.metadata = metadata or GenericMetadata()
self.move = False
self.platform = platform
self.replacements = replacements
def set_metadata(self, metadata: GenericMetadata) -> None:
self.metadata = metadata
@ -234,7 +216,7 @@ class FileRenamer:
new_name = ""
fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform)
fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform, replacements=self.replacements)
md_dict = vars(md)
for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]:
md_dict[role] = md.get_primary_credit(role)

View File

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

143
comictaggerlib/gui.py Normal file
View File

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

View File

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

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

View File

@ -20,6 +20,7 @@ import logging
import sys
from typing import Any, Callable
import settngs
from typing_extensions import NotRequired, TypedDict
from comicapi import utils
@ -29,8 +30,6 @@ from comicapi.issuestring import IssueString
from comictaggerlib.imagefetcher import ImageFetcher, ImageFetcherException
from comictaggerlib.imagehasher import ImageHasher
from comictaggerlib.resulttypes import IssueResult
from comictaggerlib.settings import ComicTaggerSettings
from comictalker.talker_utils import parse_date_str
from comictalker.talkerbase import ComicTalker, TalkerError
logger = logging.getLogger(__name__)
@ -73,8 +72,8 @@ class IssueIdentifier:
result_one_good_match = 4
result_multiple_good_matches = 5
def __init__(self, comic_archive: ComicArchive, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
self.settings = settings
def __init__(self, comic_archive: ComicArchive, options: settngs.Namespace, talker_api: ComicTalker) -> None:
self.options = options
self.talker_api = talker_api
self.comic_archive: ComicArchive = comic_archive
self.image_hasher = 1
@ -97,10 +96,10 @@ class IssueIdentifier:
# used to eliminate series names that are too long based on our search
# string
self.series_match_thresh = settings.id_series_match_identify_thresh
self.series_match_thresh = options.identifier_series_match_identify_thresh
# used to eliminate unlikely publishers
self.publisher_filter = [s.strip().casefold() for s in settings.id_publisher_filter.split(",")]
self.publisher_filter = [s.strip().casefold() for s in options.identifier_publisher_filter]
self.additional_metadata = GenericMetadata()
self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output
@ -200,10 +199,10 @@ class IssueIdentifier:
# try to get some metadata from filename
md_from_filename = ca.metadata_from_filename(
self.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options.filename_complicated_parser,
self.options.filename_remove_c2c,
self.options.filename_remove_fcbd,
self.options.filename_remove_publisher,
)
working_md = md_from_filename.copy()
@ -254,7 +253,9 @@ class IssueIdentifier:
return Score(score=0, url="", hash=0)
try:
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
url_image_data = ImageFetcher(self.options.runtime_config.user_cache_dir).fetch(
primary_thumb_url, blocking=True
)
except ImageFetcherException as e:
self.log_msg("Network issue while fetching cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError from e
@ -274,7 +275,9 @@ class IssueIdentifier:
if use_remote_alternates:
for alt_url in alt_urls:
try:
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
alt_url_image_data = ImageFetcher(self.options.runtime_config.user_cache_dir).fetch(
alt_url, blocking=True
)
except ImageFetcherException as e:
self.log_msg("Network issue while fetching alt. cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError from e
@ -464,7 +467,7 @@ class IssueIdentifier:
)
# parse out the cover date
_, month, year = parse_date_str(issue["cover_date"])
_, month, year = utils.parse_date_str(issue["cover_date"])
# Now check the cover match against the primary image
hash_list = [cover_hash]
@ -485,6 +488,7 @@ class IssueIdentifier:
use_remote_alternates=False,
)
except Exception:
logger.exception("Scoring series failed")
self.match_list = []
return self.match_list

View File

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

49
comictaggerlib/log.py Normal file
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,13 +17,13 @@ from __future__ import annotations
import logging
import settngs
from PyQt5 import QtCore, QtWidgets, uic
from comicapi import utils
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.filerenamer import FileRenamer, get_rename_dir
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.settingswindow import SettingsWindow
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import center_window_on_parent
@ -38,7 +38,7 @@ class RenameWindow(QtWidgets.QDialog):
parent: QtWidgets.QWidget,
comic_archive_list: list[ComicArchive],
data_style: int,
settings: ComicTaggerSettings,
options: settngs.Config,
talker_api: ComicTalker,
) -> None:
super().__init__(parent)
@ -54,25 +54,26 @@ class RenameWindow(QtWidgets.QDialog):
)
)
self.settings = settings
self.options = options
self.talker_api = talker_api
self.comic_archive_list = comic_archive_list
self.data_style = data_style
self.rename_list: list[str] = []
self.btnSettings.clicked.connect(self.modify_settings)
platform = "universal" if self.settings.rename_strict else "auto"
self.renamer = FileRenamer(None, platform=platform)
platform = "universal" if self.options[0].filename_rename_strict else "auto"
self.renamer = FileRenamer(None, platform=platform, replacements=self.options[0].rename_replacements)
self.do_preview()
def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str:
self.renamer.set_template(self.settings.rename_template)
self.renamer.set_issue_zero_padding(self.settings.rename_issue_number_padding)
self.renamer.set_smart_cleanup(self.settings.rename_use_smart_string_cleanup)
self.renamer.set_template(self.options[0].filename_rename_template)
self.renamer.set_issue_zero_padding(self.options[0].filename_rename_issue_number_padding)
self.renamer.set_smart_cleanup(self.options[0].filename_rename_use_smart_string_cleanup)
self.renamer.replacements = self.options[0].rename_replacements
new_ext = ca.path.suffix # default
if self.settings.rename_extension_based_on_archive:
if self.options[0].filename_rename_set_extension_based_on_archive:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
@ -84,13 +85,13 @@ class RenameWindow(QtWidgets.QDialog):
md = ca.read_metadata(self.data_style)
if md.is_empty:
md = ca.metadata_from_filename(
self.settings.complicated_parser,
self.settings.remove_c2c,
self.settings.remove_fcbd,
self.settings.remove_publisher,
self.options[0].filename_complicated_parser,
self.options[0].filename_remove_c2c,
self.options[0].filename_remove_fcbd,
self.options[0].filename_remove_publisher,
)
self.renamer.set_metadata(md)
self.renamer.move = self.settings.rename_move_dir
self.renamer.move = self.options[0].filename_rename_move_to_dir
return new_ext
def do_preview(self) -> None:
@ -103,7 +104,7 @@ class RenameWindow(QtWidgets.QDialog):
try:
new_name = self.renamer.determine_name(new_ext)
except ValueError as e:
logger.exception("Invalid format string: %s", self.settings.rename_template)
logger.exception("Invalid format string: %s", self.options[0].filename_rename_template)
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
@ -117,7 +118,9 @@ class RenameWindow(QtWidgets.QDialog):
return
except Exception as e:
logger.exception(
"Formatter failure: %s metadata: %s", self.settings.rename_template, self.renamer.metadata
"Formatter failure: %s metadata: %s",
self.options[0].filename_rename_template,
self.renamer.metadata,
)
QtWidgets.QMessageBox.critical(
self,
@ -164,7 +167,7 @@ class RenameWindow(QtWidgets.QDialog):
self.twList.setSortingEnabled(True)
def modify_settings(self) -> None:
settingswin = SettingsWindow(self, self.settings, self.talker_api)
settingswin = SettingsWindow(self, self.options, self.talker_api)
settingswin.setModal(True)
settingswin.show_rename_tab()
settingswin.exec()
@ -192,7 +195,10 @@ class RenameWindow(QtWidgets.QDialog):
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
folder = get_rename_dir(comic[0], self.settings.rename_dir if self.settings.rename_move_dir else None)
folder = get_rename_dir(
comic[0],
self.options[0].filename_rename_dir if self.options[0].filename_rename_move_to_dir else None,
)
full_path = folder / comic[1]

View File

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

View File

@ -20,15 +20,16 @@ import logging
import os
import pathlib
import platform
from typing import Any
import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import md_test
from comictaggerlib.ctversion import version
from comictaggerlib.filerenamer import FileRenamer
from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
from comictalker.comiccacher import ComicCacher
from comictalker.talkerbase import ComicTalker
@ -129,7 +130,7 @@ Spider-Geddon #1 - New Players; Check In
class SettingsWindow(QtWidgets.QDialog):
def __init__(self, parent: QtWidgets.QWidget, settings: ComicTaggerSettings, talker_api: ComicTalker) -> None:
def __init__(self, parent: QtWidgets.QWidget, options: settngs.Config, talker_api: ComicTalker) -> None:
super().__init__(parent)
uic.loadUi(ui_path / "settingswindow.ui", self)
@ -138,7 +139,7 @@ class SettingsWindow(QtWidgets.QDialog):
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.settings = settings
self.options = options
self.talker_api = talker_api
self.name = "Settings"
@ -190,23 +191,64 @@ class SettingsWindow(QtWidgets.QDialog):
self.btnResetSettings.clicked.connect(self.reset_settings)
self.btnTestKey.clicked.connect(self.test_api_key)
self.btnTemplateHelp.clicked.connect(self.show_template_help)
self.leRenameTemplate.textEdited.connect(self._rename_test)
self.cbxMoveFiles.clicked.connect(self.rename_test)
self.cbxMoveFiles.clicked.connect(self.dir_test)
self.cbxRenameStrict.clicked.connect(self.rename_test)
self.leDirectory.textEdited.connect(self.dir_test)
self.cbxComplicatedParser.clicked.connect(self.switch_parser)
def rename_test(self) -> None:
self.btnAddLiteralReplacement.clicked.connect(self.addLiteralReplacement)
self.btnAddValueReplacement.clicked.connect(self.addValueReplacement)
self.btnRemoveLiteralReplacement.clicked.connect(self.removeLiteralReplacement)
self.btnRemoveValueReplacement.clicked.connect(self.removeValueReplacement)
self.leRenameTemplate.textEdited.connect(self.rename_test)
self.cbxMoveFiles.clicked.connect(self.rename_test)
self.cbxRenameStrict.clicked.connect(self.rename_test)
self.cbxSmartCleanup.clicked.connect(self.rename_test)
self.cbxChangeExtension.clicked.connect(self.rename_test)
self.leIssueNumPadding.textEdited.connect(self.rename_test)
self.twLiteralReplacements.cellChanged.connect(self.rename_test)
self.twValueReplacements.cellChanged.connect(self.rename_test)
def addLiteralReplacement(self) -> None:
self.insertRow(self.twLiteralReplacements, self.twLiteralReplacements.rowCount(), Replacement("", "", False))
def addValueReplacement(self) -> None:
self.insertRow(self.twValueReplacements, self.twValueReplacements.rowCount(), Replacement("", "", False))
def removeLiteralReplacement(self) -> None:
if self.twLiteralReplacements.currentRow() >= 0:
self.twLiteralReplacements.removeRow(self.twLiteralReplacements.currentRow())
def removeValueReplacement(self) -> None:
if self.twValueReplacements.currentRow() >= 0:
self.twValueReplacements.removeRow(self.twValueReplacements.currentRow())
def insertRow(self, table: QtWidgets.QTableWidget, row: int, replacement: Replacement) -> None:
find, replace, strict_only = replacement
table.insertRow(row)
table.setItem(row, 0, QtWidgets.QTableWidgetItem(find))
table.setItem(row, 1, QtWidgets.QTableWidgetItem(replace))
tmp = QtWidgets.QTableWidgetItem()
if strict_only:
tmp.setCheckState(QtCore.Qt.Checked)
else:
tmp.setCheckState(QtCore.Qt.Unchecked)
table.setItem(row, 2, tmp)
def rename_test(self, *args: Any, **kwargs: Any) -> None:
self._rename_test(self.leRenameTemplate.text())
def dir_test(self) -> None:
self.lblDir.setText(
str(pathlib.Path(self.leDirectory.text().strip()).absolute()) if self.cbxMoveFiles.isChecked() else ""
str(pathlib.Path(self.leDirectory.text().strip()).resolve()) if self.cbxMoveFiles.isChecked() else ""
)
def _rename_test(self, template: str) -> None:
fr = FileRenamer(md_test, platform="universal" if self.cbxRenameStrict.isChecked() else "auto")
fr = FileRenamer(
md_test,
platform="universal" if self.cbxRenameStrict.isChecked() else "auto",
replacements=self.get_replacemnts(),
)
fr.move = self.cbxMoveFiles.isChecked()
fr.set_template(template)
fr.set_issue_zero_padding(int(self.leIssueNumPadding.text()))
@ -227,53 +269,85 @@ class SettingsWindow(QtWidgets.QDialog):
def settings_to_form(self) -> None:
# Copy values from settings to form
self.leRarExePath.setText(self.settings.rar_exe_path)
self.sbNameMatchIdentifyThresh.setValue(self.settings.id_series_match_identify_thresh)
self.sbNameMatchSearchThresh.setValue(self.settings.id_series_match_search_thresh)
self.tePublisherFilter.setPlainText(self.settings.id_publisher_filter)
self.leRarExePath.setText(self.options[0].general_rar_exe_path)
self.sbNameMatchIdentifyThresh.setValue(self.options[0].identifier_series_match_identify_thresh)
self.sbNameMatchSearchThresh.setValue(self.options[0].comicvine_series_match_search_thresh)
self.tePublisherFilter.setPlainText("\n".join(self.options[0].identifier_publisher_filter))
self.cbxCheckForNewVersion.setChecked(self.settings.check_for_new_version)
self.cbxCheckForNewVersion.setChecked(self.options[0].general_check_for_new_version)
self.cbxComplicatedParser.setChecked(self.settings.complicated_parser)
self.cbxRemoveC2C.setChecked(self.settings.remove_c2c)
self.cbxRemoveFCBD.setChecked(self.settings.remove_fcbd)
self.cbxRemovePublisher.setChecked(self.settings.remove_publisher)
self.cbxComplicatedParser.setChecked(self.options[0].filename_complicated_parser)
self.cbxRemoveC2C.setChecked(self.options[0].filename_remove_c2c)
self.cbxRemoveFCBD.setChecked(self.options[0].filename_remove_fcbd)
self.cbxRemovePublisher.setChecked(self.options[0].filename_remove_publisher)
self.switch_parser()
self.cbxUseSeriesStartAsVolume.setChecked(self.settings.use_series_start_as_volume)
self.cbxClearFormBeforePopulating.setChecked(self.settings.clear_form_before_populating_from_cv)
self.cbxRemoveHtmlTables.setChecked(self.settings.remove_html_tables)
self.cbxUseSeriesStartAsVolume.setChecked(self.options[0].comicvine_use_series_start_as_volume)
self.cbxClearFormBeforePopulating.setChecked(self.options[0].comicvine_clear_form_before_populating_from_cv)
self.cbxRemoveHtmlTables.setChecked(self.options[0].comicvine_remove_html_tables)
self.cbxUseFilter.setChecked(self.settings.always_use_publisher_filter)
self.cbxSortByYear.setChecked(self.settings.sort_series_by_year)
self.cbxExactMatches.setChecked(self.settings.exact_series_matches_first)
self.cbxUseFilter.setChecked(self.options[0].comicvine_always_use_publisher_filter)
self.cbxSortByYear.setChecked(self.options[0].comicvine_sort_series_by_year)
self.cbxExactMatches.setChecked(self.options[0].comicvine_exact_series_matches_first)
self.leKey.setText(self.settings.cv_api_key)
self.leURL.setText(self.settings.cv_url)
self.leKey.setText(self.options[0].comicvine_cv_api_key)
self.leURL.setText(self.options[0].comicvine_cv_url)
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.settings.assume_lone_credit_is_primary)
self.cbxCopyCharactersToTags.setChecked(self.settings.copy_characters_to_tags)
self.cbxCopyTeamsToTags.setChecked(self.settings.copy_teams_to_tags)
self.cbxCopyLocationsToTags.setChecked(self.settings.copy_locations_to_tags)
self.cbxCopyStoryArcsToTags.setChecked(self.settings.copy_storyarcs_to_tags)
self.cbxCopyNotesToComments.setChecked(self.settings.copy_notes_to_comments)
self.cbxCopyWebLinkToComments.setChecked(self.settings.copy_weblink_to_comments)
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.settings.apply_cbl_transform_on_cv_import)
self.cbxApplyCBLTransformOnBatchOperation.setChecked(self.settings.apply_cbl_transform_on_bulk_operation)
self.cbxAssumeLoneCreditIsPrimary.setChecked(self.options[0].cbl_assume_lone_credit_is_primary)
self.cbxCopyCharactersToTags.setChecked(self.options[0].cbl_copy_characters_to_tags)
self.cbxCopyTeamsToTags.setChecked(self.options[0].cbl_copy_teams_to_tags)
self.cbxCopyLocationsToTags.setChecked(self.options[0].cbl_copy_locations_to_tags)
self.cbxCopyStoryArcsToTags.setChecked(self.options[0].cbl_copy_storyarcs_to_tags)
self.cbxCopyNotesToComments.setChecked(self.options[0].cbl_copy_notes_to_comments)
self.cbxCopyWebLinkToComments.setChecked(self.options[0].cbl_copy_weblink_to_comments)
self.cbxApplyCBLTransformOnCVIMport.setChecked(self.options[0].cbl_apply_transform_on_import)
self.cbxApplyCBLTransformOnBatchOperation.setChecked(self.options[0].cbl_apply_transform_on_bulk_operation)
self.leRenameTemplate.setText(self.settings.rename_template)
self.leIssueNumPadding.setText(str(self.settings.rename_issue_number_padding))
self.cbxSmartCleanup.setChecked(self.settings.rename_use_smart_string_cleanup)
self.cbxChangeExtension.setChecked(self.settings.rename_extension_based_on_archive)
self.cbxMoveFiles.setChecked(self.settings.rename_move_dir)
self.leDirectory.setText(self.settings.rename_dir)
self.cbxRenameStrict.setChecked(self.settings.rename_strict)
self.leRenameTemplate.setText(self.options[0].rename_template)
self.leIssueNumPadding.setText(str(self.options[0].rename_issue_number_padding))
self.cbxSmartCleanup.setChecked(self.options[0].rename_use_smart_string_cleanup)
self.cbxChangeExtension.setChecked(self.options[0].rename_set_extension_based_on_archive)
self.cbxMoveFiles.setChecked(self.options[0].rename_move_to_dir)
self.leDirectory.setText(self.options[0].rename_dir)
self.cbxRenameStrict.setChecked(self.options[0].rename_strict)
for table, replacments in zip(
(self.twLiteralReplacements, self.twValueReplacements), self.options[0].rename_replacements
):
table.clearContents()
for i in reversed(range(table.rowCount())):
table.removeRow(i)
for row, replacement in enumerate(replacments):
self.insertRow(table, row, replacement)
def get_replacemnts(self) -> Replacements:
literal_replacements = []
value_replacements = []
for row in range(self.twLiteralReplacements.rowCount()):
if self.twLiteralReplacements.item(row, 0).text():
literal_replacements.append(
Replacement(
self.twLiteralReplacements.item(row, 0).text(),
self.twLiteralReplacements.item(row, 1).text(),
self.twLiteralReplacements.item(row, 2).checkState() == QtCore.Qt.Checked,
)
)
for row in range(self.twValueReplacements.rowCount()):
if self.twValueReplacements.item(row, 0).text():
value_replacements.append(
Replacement(
self.twValueReplacements.item(row, 0).text(),
self.twValueReplacements.item(row, 1).text(),
self.twValueReplacements.item(row, 2).checkState() == QtCore.Qt.Checked,
)
)
return Replacements(literal_replacements, value_replacements)
def accept(self) -> None:
self.rename_test()
if self.rename_error is not None:
if isinstance(self.rename_error, ValueError):
logger.exception("Invalid format string: %s", self.settings.rename_template)
logger.exception("Invalid format string: %s", self.options[0].rename_template)
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
@ -287,7 +361,7 @@ class SettingsWindow(QtWidgets.QDialog):
return
else:
logger.exception(
"Formatter failure: %s metadata: %s", self.settings.rename_template, self.renamer.metadata
"Formatter failure: %s metadata: %s", self.options[0].rename_template, self.renamer.metadata
)
QtWidgets.QMessageBox.critical(
self,
@ -300,69 +374,74 @@ class SettingsWindow(QtWidgets.QDialog):
)
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
self.options[0].general_rar_exe_path = str(self.leRarExePath.text())
# make sure rar program is now in the path for the rar class
if self.settings.rar_exe_path:
utils.add_to_path(os.path.dirname(self.settings.rar_exe_path))
if self.options[0].general_rar_exe_path:
utils.add_to_path(os.path.dirname(self.options[0].general_rar_exe_path))
if not str(self.leIssueNumPadding.text()).isdigit():
self.leIssueNumPadding.setText("0")
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
self.options[0].general_check_for_new_version = self.cbxCheckForNewVersion.isChecked()
self.settings.id_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
self.settings.id_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
self.settings.id_publisher_filter = str(self.tePublisherFilter.toPlainText())
self.options[0].identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value()
self.options[0].comicvine_series_match_search_thresh = self.sbNameMatchSearchThresh.value()
self.options[0].identifier_publisher_filter = [
x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip()
]
self.settings.complicated_parser = self.cbxComplicatedParser.isChecked()
self.settings.remove_c2c = self.cbxRemoveC2C.isChecked()
self.settings.remove_fcbd = self.cbxRemoveFCBD.isChecked()
self.settings.remove_publisher = self.cbxRemovePublisher.isChecked()
self.options[0].filename_complicated_parser = self.cbxComplicatedParser.isChecked()
self.options[0].filename_remove_c2c = self.cbxRemoveC2C.isChecked()
self.options[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked()
self.options[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked()
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
self.options[0].comicvine_use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
self.options[0].comicvine_clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
self.options[0].comicvine_remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
self.settings.always_use_publisher_filter = self.cbxUseFilter.isChecked()
self.settings.sort_series_by_year = self.cbxSortByYear.isChecked()
self.settings.exact_series_matches_first = self.cbxExactMatches.isChecked()
self.options[0].comicvine_always_use_publisher_filter = self.cbxUseFilter.isChecked()
self.options[0].comicvine_sort_series_by_year = self.cbxSortByYear.isChecked()
self.options[0].comicvine_exact_series_matches_first = self.cbxExactMatches.isChecked()
# Ignore empty field
if self.leKey.text().strip():
self.settings.cv_api_key = self.leKey.text().strip()
self.talker_api.api_key = self.settings.cv_api_key
self.options[0].comicvine_cv_api_key = self.leKey.text().strip()
self.talker_api.api_key = self.options[0].comicvine_cv_api_key
if self.leURL.text().strip():
self.settings.cv_url = self.leURL.text().strip()
self.talker_api.api_url = self.settings.cv_url
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.settings.copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
self.settings.copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
self.settings.copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
self.settings.copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
self.settings.copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
self.settings.copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
self.settings.apply_cbl_transform_on_cv_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.options[0].comicvine_cv_url = self.leURL.text().strip()
self.talker_api.api_url = self.options[0].comicvine_cv_url
self.settings.rename_template = str(self.leRenameTemplate.text())
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
self.settings.rename_dir = self.leDirectory.text()
self.options[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
self.options[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked()
self.options[0].cbl_copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked()
self.options[0].cbl_copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked()
self.options[0].cbl_copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked()
self.options[0].cbl_copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked()
self.options[0].cbl_copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked()
self.options[0].cbl_apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked()
self.options[0].cbl_apply_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.settings.rename_strict = self.cbxRenameStrict.isChecked()
self.options[0].rename_template = str(self.leRenameTemplate.text())
self.options[0].rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.options[0].rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.options[0].rename_set_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.options[0].rename_move_to_dir = self.cbxMoveFiles.isChecked()
self.options[0].rename_dir = self.leDirectory.text()
self.settings.save()
self.options[0].rename_strict = self.cbxRenameStrict.isChecked()
self.options[0].rename_replacements = self.get_replacemnts()
settngs.save_file(self.options, self.options[0].runtime_config.user_config_dir / "settings.json")
self.parent().options = self.options
QtWidgets.QDialog.accept(self)
def select_rar(self) -> None:
self.select_file(self.leRarExePath, "RAR")
def clear_cache(self) -> None:
ImageFetcher().clear_cache()
ComicCacher(ComicTaggerSettings.get_settings_folder(), version).clear_cache()
ImageFetcher(self.options[0].runtime_config.cache_folder).clear_cache()
ComicCacher(self.options[0].runtime_config.cache_folder, version).clear_cache()
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
def test_api_key(self) -> None:
@ -372,7 +451,7 @@ class SettingsWindow(QtWidgets.QDialog):
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
def reset_settings(self) -> None:
self.settings.reset()
self.options = settngs.Config(settngs.defaults(self.options[1]), self.options[1])
self.settings_to_form()
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
@ -392,9 +471,9 @@ class SettingsWindow(QtWidgets.QDialog):
dialog.setDirectory(os.path.dirname(str(control.text())))
if name == "RAR":
dialog.setWindowTitle("Find " + name + " program")
dialog.setWindowTitle(f"Find {name} program")
else:
dialog.setWindowTitle("Find " + name + " library")
dialog.setWindowTitle(f"Find {name} library")
if dialog.exec():
file_list = dialog.selectedFiles()

View File

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

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>702</width>
<height>488</height>
<height>513</height>
</rect>
</property>
<property name="windowTitle">
@ -616,7 +616,21 @@
<string>Rename</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<item row="3" column="2">
<widget class="QPushButton" name="btnAddValueReplacement">
<property name="text">
<string>Add Replacement</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="btnRemoveLiteralReplacement">
<property name="text">
<string>Remove Replacement</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
@ -717,8 +731,7 @@
<item row="8" column="0">
<widget class="QCheckBox" name="cbxRenameStrict">
<property name="toolTip">
<string>If checked will ensure reserved characters and filenames are removed for all Operating Systems.
By default only removes restricted characters and filenames for the current Operating System.</string>
<string>If checked will ensure reserved characters and filenames are removed for all Operating Systems.&lt;br/&gt;By default only removes restricted characters and filenames for the current Operating System.</string>
</property>
<property name="text">
<string>Strict renaming</string>
@ -730,6 +743,118 @@ By default only removes restricted characters and filenames for the current Oper
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="btnAddLiteralReplacement">
<property name="text">
<string>Add Replacement</string>
</property>
</widget>
</item>
<item row="3" column="3">
<widget class="QPushButton" name="btnRemoveValueReplacement">
<property name="text">
<string>Remove Replacement</string>
</property>
</widget>
</item>
<item row="2" column="2" colspan="2">
<widget class="QTableWidget" name="twValueReplacements">
<property name="font">
<font>
<family>Monaco</family>
</font>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Find</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Replacement</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Strict Only</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QTableWidget" name="twLiteralReplacements">
<property name="font">
<font>
<family>Monaco</family>
</font>
</property>
<property name="contextMenuPolicy">
<enum>Qt::ActionsContextMenu</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="columnCount">
<number>3</number>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Find</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Replacement</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
<column>
<property name="text">
<string>Strict Only</string>
</property>
<property name="textAlignment">
<set>AlignCenter</set>
</property>
</column>
</widget>
</item>
<item row="1" column="2" colspan="2">
<widget class="QLabel" name="lblValueReplacements">
<property name="text">
<string>Value Text Replacements</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="lblLiteralReplacements">
<property name="text">
<string>Literal Text Replacements</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tRARTools">

View File

@ -19,6 +19,7 @@ import itertools
import logging
from collections import deque
import settngs
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import pyqtSignal
@ -30,7 +31,6 @@ from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.issueselectionwindow import IssueSelectionWindow
from comictaggerlib.matchselectionwindow import MatchSelectionWindow
from comictaggerlib.progresswindow import IDProgressWindow
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import ui_path
from comictaggerlib.ui.qtutils import reduce_widget_font_size
from comictalker.resulttypes import ComicVolume
@ -111,7 +111,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
issue_count: int,
cover_index_list: list[int],
comic_archive: ComicArchive | None,
settings: ComicTaggerSettings,
options: settngs.Namespace,
talker_api: ComicTalker,
autoselect: bool = False,
literal: bool = False,
@ -120,7 +120,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
uic.loadUi(ui_path / "volumeselectionwindow.ui", self)
self.imageWidget = CoverImageWidget(self.imageContainer, talker_api, CoverImageWidget.URLMode)
self.imageWidget = CoverImageWidget(
self.imageContainer, CoverImageWidget.URLMode, options.runtime_config.user_cache_dir, talker_api
)
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
gridlayout.addWidget(self.imageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
@ -136,7 +138,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
)
)
self.settings = settings
self.options = options
self.series_name = series_name
self.issue_number = issue_number
self.issue_id: int | None = None
@ -154,7 +156,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.progdialog: QtWidgets.QProgressDialog | None = None
self.search_thread: SearchThread | None = None
self.use_filter = self.settings.always_use_publisher_filter
self.use_filter = self.options.comicvine_always_use_publisher_filter
# Load to retrieve settings
self.talker_api = talker_api
@ -207,7 +209,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.iddialog.rejected.connect(self.identify_cancel)
self.iddialog.show()
self.ii = IssueIdentifier(self.comic_archive, self.settings, self.talker_api)
self.ii = IssueIdentifier(self.comic_archive, self.options, self.talker_api)
md = GenericMetadata()
md.series = self.series_name
@ -280,7 +282,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
choices = True
if choices:
selector = MatchSelectionWindow(self, matches, self.comic_archive, self.talker_api)
selector = MatchSelectionWindow(
self, matches, self.comic_archive, talker_api=self.talker_api, options=self.options
)
selector.setModal(True)
selector.exec()
if selector.result():
@ -296,7 +300,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.show_issues()
def show_issues(self) -> None:
selector = IssueSelectionWindow(self, self.settings, self.talker_api, self.volume_id, self.issue_number)
selector = IssueSelectionWindow(self, self.options, self.talker_api, self.volume_id, self.issue_number)
title = ""
for record in self.ct_search_results:
if record["id"] == self.volume_id:
@ -324,7 +328,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
def perform_query(self, refresh: bool = False) -> None:
self.search_thread = SearchThread(
self.talker_api, self.series_name, refresh, self.literal, self.settings.id_series_match_search_thresh
self.talker_api, self.series_name, refresh, self.literal, self.options.comicvine_series_match_search_thresh
)
self.search_thread.searchComplete.connect(self.search_complete)
self.search_thread.progressUpdate.connect(self.search_progress_update)
@ -362,7 +366,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
def search_complete(self) -> None:
if self.progdialog is not None:
self.progdialog.accept()
self.progdialog = None
del self.progdialog
if self.search_thread is not None and self.search_thread.ct_error:
# TODO Currently still opens the window
QtWidgets.QMessageBox.critical(
@ -376,7 +380,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
# filter the publishers if enabled set
if self.use_filter:
try:
publisher_filter = {s.strip().casefold() for s in self.settings.id_publisher_filter.split(",")}
publisher_filter = {s.strip().casefold() for s in self.options.identifier_publisher_filter}
# use '' as publisher name if None
self.ct_search_results = list(
filter(
@ -392,7 +396,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
# compare as str in case extra chars ie. '1976?'
# - missing (none) values being converted to 'None' - consistent with prior behaviour in v1.2.3
# sort by start_year if set
if self.settings.sort_series_by_year:
if self.options.comicvine_sort_series_by_year:
try:
self.ct_search_results = sorted(
self.ct_search_results,
@ -410,7 +414,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
logger.exception("bad data error sorting results by count_of_issues")
# move sanitized matches to the front
if self.settings.exact_series_matches_first:
if self.options.comicvine_exact_series_matches_first:
try:
sanitized = utils.sanitize_title(self.series_name, False).casefold()
sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold()

View File

@ -1,3 +1,4 @@
appdirs==1.4.4
beautifulsoup4>=4.1
importlib_metadata>=3.3.0
natsort>=8.1.0
@ -7,6 +8,7 @@ pycountry
pyicu; sys_platform == 'linux' or sys_platform == 'darwin'
rapidfuzz>=2.12.0
requests==2.*
settngs==0.3.0
text2digits
typing_extensions
wordninja

View File

@ -14,7 +14,7 @@ import os
from setuptools import find_packages, setup
def read(fname):
def read(fname: str) -> str:
"""
Read the contents of a file.
Parameters
@ -60,7 +60,7 @@ setup(
),
package_data={"comictaggerlib": ["ui/*", "graphics/*"], "comicapi": ["data/*"]},
entry_points=dict(
console_scripts=["comictagger=comictaggerlib.main:ctmain"],
console_scripts=["comictagger=comictaggerlib.main:main"],
pyinstaller40=[
"hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs",
],

View File

@ -6,9 +6,10 @@ import comictalker.comiccacher
from testing.comicdata import search_results
def test_create_cache(settings, mock_version):
comictalker.comiccacher.ComicCacher(settings.get_settings_folder(), mock_version[0])
assert (settings.get_settings_folder() / "settings").exists()
def test_create_cache(options, mock_version):
settings, definitions = options
comictalker.comiccacher.ComicCacher(settings.runtime_config.user_cache_dir, mock_version[0])
assert (settings.runtime_config.user_cache_dir).exists()
def test_search_results(comic_cache):

View File

@ -9,11 +9,12 @@ from typing import Any
import pytest
import requests
import settngs
from PIL import Image
import comicapi.comicarchive
import comicapi.genericmetadata
import comictaggerlib.settings
import comictaggerlib.ctoptions
import comictalker.comiccacher
import comictalker.talkers.comicvine
from comicapi import utils
@ -55,7 +56,7 @@ def no_requests(monkeypatch) -> None:
@pytest.fixture
def comicvine_api(
monkeypatch, cbz, comic_cache, mock_version, settings
monkeypatch, cbz, comic_cache, mock_version, options
) -> comictalker.talkers.comicvine.ComicVineTalker:
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method or None for invalid urls.
@ -116,7 +117,7 @@ def comicvine_api(
cv = comictalker.talkers.comicvine.ComicVineTalker(
version=mock_version[0],
cache_folder=settings.get_settings_folder(),
cache_folder=options[0].runtime_config.user_cache_dir,
api_url="",
api_key="",
series_match_thresh=90,
@ -162,10 +163,26 @@ def seed_all_publishers(monkeypatch):
@pytest.fixture
def settings(tmp_path):
yield comictaggerlib.settings.ComicTaggerSettings(tmp_path / "settings")
def options(settings_manager, tmp_path):
comictaggerlib.ctoptions.register_commandline(settings_manager)
comictaggerlib.ctoptions.register_settings(settings_manager)
defaults = settings_manager.get_namespace(settings_manager.defaults())
defaults[0].runtime_config = comictaggerlib.ctoptions.ComicTaggerPaths(tmp_path / "config")
defaults[0].runtime_config.user_data_dir.mkdir(parents=True, exist_ok=True)
defaults[0].runtime_config.user_config_dir.mkdir(parents=True, exist_ok=True)
defaults[0].runtime_config.user_cache_dir.mkdir(parents=True, exist_ok=True)
defaults[0].runtime_config.user_state_dir.mkdir(parents=True, exist_ok=True)
defaults[0].runtime_config.user_log_dir.mkdir(parents=True, exist_ok=True)
yield defaults
@pytest.fixture
def comic_cache(settings, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]:
yield comictalker.comiccacher.ComicCacher(settings.get_settings_folder(), mock_version[0])
def settings_manager():
manager = settngs.Manager()
yield manager
@pytest.fixture
def comic_cache(options, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]:
yield comictalker.comiccacher.ComicCacher(options[0].runtime_config.user_cache_dir, mock_version[0])

View File

@ -9,7 +9,8 @@ import testing.comicdata
import testing.comicvine
def test_crop(cbz_double_cover, settings, tmp_path, comicvine_api):
def test_crop(cbz_double_cover, options, tmp_path, comicvine_api):
settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz_double_cover, settings, comicvine_api)
cropped = ii.crop_cover(cbz_double_cover.archiver.read_file("double_cover.jpg"))
original_cover = cbz_double_cover.get_page(0)
@ -21,14 +22,16 @@ def test_crop(cbz_double_cover, settings, tmp_path, comicvine_api):
@pytest.mark.parametrize("additional_md, expected", testing.comicdata.metadata_keys)
def test_get_search_keys(cbz, settings, additional_md, expected, comicvine_api):
def test_get_search_keys(cbz, options, additional_md, expected, comicvine_api):
settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api)
ii.set_additional_metadata(additional_md)
assert expected == ii.get_search_keys()
def test_get_issue_cover_match_score(cbz, settings, comicvine_api):
def test_get_issue_cover_match_score(cbz, options, comicvine_api):
settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api)
score = ii.get_issue_cover_match_score(
int(
@ -49,7 +52,8 @@ def test_get_issue_cover_match_score(cbz, settings, comicvine_api):
assert expected == score
def test_search(cbz, settings, comicvine_api):
def test_search(cbz, options, comicvine_api):
settings, definitions = options
ii = comictaggerlib.issueidentifier.IssueIdentifier(cbz, settings, comicvine_api)
results = ii.search()
cv_expected = {