diff --git a/comicapi/utils.py b/comicapi/utils.py index 497e336..a532a00 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -165,7 +165,7 @@ def sanitize_title(text: str, basic: bool = False) -> str: def titles_match(search_title: str, record_title: str, threshold: int = 90) -> bool: sanitized_search = sanitize_title(search_title) sanitized_record = sanitize_title(record_title) - ratio: int = rapidfuzz.fuzz.ratio(sanitized_search, sanitized_record) + ratio = int(rapidfuzz.fuzz.ratio(sanitized_search, sanitized_record)) logger.debug( "search title: %s ; record title: %s ; ratio: %d ; match threshold: %d", search_title, diff --git a/comictaggerlib/applicationlogwindow.py b/comictaggerlib/applicationlogwindow.py index 7b04939..afece95 100644 --- a/comictaggerlib/applicationlogwindow.py +++ b/comictaggerlib/applicationlogwindow.py @@ -23,7 +23,7 @@ class QTextEditLogger(QtCore.QObject, logging.Handler): class ApplicationLogWindow(QtWidgets.QDialog): - def __init__(self, log_handler: QTextEditLogger, parent: QtCore.QObject = None) -> None: + def __init__(self, log_handler: QTextEditLogger, parent: QtCore.QObject | None = None) -> None: super().__init__(parent) uic.loadUi(ui_path / "logwindow.ui", self) diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index 29cd789..ca0abfb 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -28,7 +28,7 @@ from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.resulttypes import IssueResult, MultipleMatch from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import reduce_widget_font_size -from comictalker.talkerbase import ComicTalker +from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): style: int, fetch_func: Callable[[IssueResult], GenericMetadata], config: settngs.Namespace, - talker_api: ComicTalker, + talker: ComicTalker, ) -> None: super().__init__(parent) @@ -52,7 +52,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): self.current_match_set: MultipleMatch = match_set_list[0] self.altCoverWidget = CoverImageWidget( - self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker_api + self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker ) gridlayout = QtWidgets.QGridLayout(self.altCoverContainer) gridlayout.addWidget(self.altCoverWidget) diff --git a/comictaggerlib/autotagprogresswindow.py b/comictaggerlib/autotagprogresswindow.py index b8362d1..f2486bb 100644 --- a/comictaggerlib/autotagprogresswindow.py +++ b/comictaggerlib/autotagprogresswindow.py @@ -22,13 +22,13 @@ from PyQt5 import QtCore, QtWidgets, uic from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import reduce_widget_font_size -from comictalker.talkerbase import ComicTalker +from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) class AutoTagProgressWindow(QtWidgets.QDialog): - def __init__(self, parent: QtWidgets.QWidget, talker_api: ComicTalker) -> None: + def __init__(self, parent: QtWidgets.QWidget, talker: ComicTalker) -> None: super().__init__(parent) uic.loadUi(ui_path / "autotagprogresswindow.ui", self) diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 5a20530..7e7db5d 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -49,7 +49,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.autotag_ignore_leading_numbers_in_filename) self.cbxRemoveAfterSuccess.setChecked(self.config.autotag_remove_archive_after_successful_match) self.cbxWaitForRateLimit.setChecked(self.config.autotag_wait_and_retry_on_rate_limit) - self.cbxAutoImprint.setChecked(self.config.talkers_auto_imprint) + self.cbxAutoImprint.setChecked(self.config.talker_auto_imprint) nlmt_tip = """The Name Match Ratio Threshold: Auto-Identify is for eliminating automatic search matches that are too long compared to your series name search. The lower @@ -75,7 +75,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.remove_after_success = False self.wait_and_retry_on_rate_limit = False self.search_string = "" - self.name_length_match_tolerance = self.config.talkers_series_match_search_thresh + self.name_length_match_tolerance = self.config.talker_series_match_search_thresh self.split_words = self.cbxSplitWords.isChecked() def search_string_toggle(self) -> None: diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index bd49c52..86fd9a3 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -34,21 +34,27 @@ 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 comictalker.talkerbase import ComicTalker, TalkerError +from comictalker.comictalker import ComicTalker, TalkerError logger = logging.getLogger(__name__) class CLI: - def __init__(self, config: settngs.Values, talker_api: ComicTalker): + def __init__(self, config: settngs.Namespace, talkers: dict[str, ComicTalker]): self.config = config - self.talker_api = talker_api + self.talkers = talkers self.batch_mode = False + def current_talker(self) -> ComicTalker: + if self.config[0].talker_source in self.talkers: + return self.talkers[self.config[0].talker_source] + logger.error("Could not find the '%s' talker", self.config[0].talker_source) + raise SystemExit(2) + def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata: # now get the particular issue data try: - ct_md = self.talker_api.fetch_comic_data(issue_id) + ct_md = self.current_talker().fetch_comic_data(issue_id) except TalkerError as e: logger.exception(f"Error retrieving issue details. Save aborted.\n{e}") return GenericMetadata() @@ -108,7 +114,7 @@ class CLI: 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.config.talkers_clear_metadata_on_import: + if self.config.talker_clear_metadata_on_import: md = ct_md else: notes = ( @@ -117,7 +123,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config.talkers_auto_imprint: + if self.config.talker_auto_imprint: md.fix_publisher() self.actual_metadata_save(ca, md) @@ -342,7 +348,7 @@ class CLI: if self.config.runtime_issue_id is not None: # we were given the actual issue ID to search with try: - ct_md = self.talker_api.fetch_comic_data(self.config.runtime_issue_id) + ct_md = self.current_talker().fetch_comic_data(self.config.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())) @@ -361,7 +367,7 @@ class CLI: match_results.no_matches.append(str(ca.path.absolute())) return - ii = IssueIdentifier(ca, self.config, self.talker_api) + ii = IssueIdentifier(ca, self.config, self.current_talker()) def myoutput(text: str) -> None: if self.config.runtime_verbose: @@ -421,7 +427,7 @@ class CLI: match_results.fetch_data_failures.append(str(ca.path.absolute())) return - if self.config.talkers_clear_metadata_on_import: + if self.config.talker_clear_metadata_on_import: md = ct_md else: notes = ( @@ -430,7 +436,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config.talkers_auto_imprint: + if self.config.talker_auto_imprint: md.fix_publisher() # ok, done building our metadata. time to save diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py index 15fc459..62bdb9f 100644 --- a/comictaggerlib/coverimagewidget.py +++ b/comictaggerlib/coverimagewidget.py @@ -31,7 +31,7 @@ from comictaggerlib.imagepopup import ImagePopup from comictaggerlib.pageloader import PageLoader from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import get_qimage_from_data, reduce_widget_font_size -from comictalker.talkerbase import ComicTalker +from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) @@ -68,17 +68,17 @@ class CoverImageWidget(QtWidgets.QWidget): parent: QtWidgets.QWidget, mode: int, cache_folder: pathlib.Path | None, - talker_api: ComicTalker | None, + talker: ComicTalker | None, expand_on_click: bool = True, ) -> None: super().__init__(parent) if mode not in (self.AltCoverMode, self.URLMode) or cache_folder is None: self.cover_fetcher = None - self.talker_api = None + self.talker = None else: self.cover_fetcher = ImageFetcher(cache_folder) - self.talker_api = None + self.talker = None uic.loadUi(ui_path / "coverimagewidget.ui", self) reduce_widget_font_size(self.label) diff --git a/comictaggerlib/ctsettings/__init__.py b/comictaggerlib/ctsettings/__init__.py index a186759..88fc1b3 100644 --- a/comictaggerlib/ctsettings/__init__.py +++ b/comictaggerlib/ctsettings/__init__.py @@ -8,7 +8,7 @@ from comictaggerlib.ctsettings.commandline import ( from comictaggerlib.ctsettings.file import register_file_settings, validate_file_settings from comictaggerlib.ctsettings.plugin import register_plugin_settings, validate_plugin_settings from comictaggerlib.ctsettings.types import ComicTaggerPaths -from comictalker.talkerbase import ComicTalker +from comictalker import ComicTalker talkers: dict[str, ComicTalker] = {} diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index ccb3956..0bcd505 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -267,8 +267,9 @@ def register_commandline_settings(parser: settngs.Manager) -> None: parser.add_group("runtime", register_settings) -def validate_commandline_settings(config: settngs.Config[settngs.Values], parser: settngs.Manager) -> settngs.Values: - +def validate_commandline_settings( + config: settngs.Config[settngs.Namespace], parser: settngs.Manager +) -> settngs.Config[settngs.Namespace]: if config[0].commands_version: parser.exit( status=1, diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index c9cb4ff..577c079 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -2,7 +2,6 @@ from __future__ import annotations import argparse import uuid -from typing import Any import settngs @@ -84,8 +83,8 @@ def filename(parser: settngs.Manager) -> None: ) -def talkers(parser: settngs.Manager) -> None: - # General settings for all information talkers +def talker(parser: settngs.Manager) -> None: + # General settings for talkers parser.add_setting("--source", default="comicvine", help="Use a specified source by source ID") parser.add_setting( "--series-match-search-thresh", @@ -225,7 +224,7 @@ def autotag(parser: settngs.Manager) -> None: ) -def validate_file_settings(config: settngs.Config[settngs.Values]) -> dict[str, dict[str, Any]]: +def validate_file_settings(config: settngs.Config[settngs.Namespace]) -> settngs.Config[settngs.Namespace]: config[0].identifier_publisher_filter = [x.strip() for x in config[0].identifier_publisher_filter if x.strip()] config[0].rename_replacements = Replacements( [Replacement(x[0], x[1], x[2]) for x in config[0].rename_replacements[0]], @@ -240,7 +239,7 @@ def register_file_settings(parser: settngs.Manager) -> None: parser.add_group("identifier", identifier, False) parser.add_group("dialog", dialog, False) parser.add_group("filename", filename, False) - parser.add_group("talkers", talkers, False) + parser.add_group("talker", talker, False) parser.add_group("cbl", cbl, False) parser.add_group("rename", rename, False) parser.add_group("autotag", autotag, False) diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py index 1e7dbb8..6f41e88 100644 --- a/comictaggerlib/gui.py +++ b/comictaggerlib/gui.py @@ -10,7 +10,7 @@ import types import settngs from comictaggerlib.graphics import graphics_path -from comictalker.talkerbase import ComicTalker +from comictalker.comictalker import ComicTalker logger = logging.getLogger("comictagger") try: @@ -83,7 +83,7 @@ except ImportError as e: def open_tagger_window( - talker_api: ComicTalker | None, config: settngs.Config[settngs.Namespace], error: tuple[str, bool] | None + talkers: dict[str, ComicTalker], config: settngs.Config[settngs.Namespace], error: tuple[str, bool] | None ) -> None: os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" args = [] @@ -96,8 +96,6 @@ def open_tagger_window( if error[1]: raise SystemExit(1) - assert talker_api is not None - # needed to catch initial open file events (macOS) app.openFileRequest.connect(lambda x: config[0].runtime_files.append(x.toLocalFile())) @@ -127,7 +125,7 @@ def open_tagger_window( QtWidgets.QApplication.processEvents() try: - tagger_window = TaggerWindow(config[0].runtime_files, config, talker_api) + tagger_window = TaggerWindow(config[0].runtime_files, config, talkers) tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png"))) tagger_window.show() diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index 66c9155..0ac4c4f 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -30,7 +30,7 @@ from comicapi.issuestring import IssueString from comictaggerlib.imagefetcher import ImageFetcher, ImageFetcherException from comictaggerlib.imagehasher import ImageHasher from comictaggerlib.resulttypes import IssueResult -from comictalker.talkerbase import ComicTalker, TalkerError +from comictalker.comictalker import ComicTalker, TalkerError logger = logging.getLogger(__name__) @@ -72,9 +72,9 @@ class IssueIdentifier: result_one_good_match = 4 result_multiple_good_matches = 5 - def __init__(self, comic_archive: ComicArchive, config: settngs.Namespace, talker_api: ComicTalker) -> None: + def __init__(self, comic_archive: ComicArchive, config: settngs.Namespace, talker: ComicTalker) -> None: self.config = config - self.talker_api = talker_api + self.talker = talker self.comic_archive: ComicArchive = comic_archive self.image_hasher = 1 @@ -188,8 +188,8 @@ class IssueIdentifier: height = bbox[3] - bbox[1] # Convert to percent - width_percent = 100 - ((width / im.width) * 100) - height_percent = 100 - ((height / im.height) * 100) + width_percent = int(100 - ((width / im.width) * 100)) + height_percent = int(100 - ((height / im.height) * 100)) logger.debug( "Width: %s Height: %s, ratio: %s %s ratio met: %s", im.width, @@ -411,7 +411,7 @@ class IssueIdentifier: self.log_msg(f"Searching for {keys['series']} #{keys['issue_number']} ...") try: - ct_search_results = self.talker_api.search_for_series(keys["series"]) + ct_search_results = self.talker.search_for_series(keys["series"]) except TalkerError as e: self.log_msg(f"Error searching for series.\n{e}") return [] @@ -460,7 +460,7 @@ class IssueIdentifier: issue_list = None try: if len(series_by_id) > 0: - issue_list = self.talker_api.fetch_issues_by_series_issue_num_and_year( + issue_list = self.talker.fetch_issues_by_series_issue_num_and_year( list(series_by_id.keys()), keys["issue_number"], keys["year"] ) except TalkerError as e: diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index 04cb51e..c1a32e1 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -24,8 +24,8 @@ from comicapi.issuestring import IssueString from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import reduce_widget_font_size +from comictalker.comictalker import ComicTalker, TalkerError from comictalker.resulttypes import ComicIssue -from comictalker.talkerbase import ComicTalker, TalkerError logger = logging.getLogger(__name__) @@ -43,7 +43,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): self, parent: QtWidgets.QWidget, config: settngs.Namespace, - talker_api: ComicTalker, + talker: ComicTalker, series_id: str, issue_number: str, ) -> None: @@ -52,7 +52,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): uic.loadUi(ui_path / "issueselectionwindow.ui", self) self.coverWidget = CoverImageWidget( - self.coverImageContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker_api + self.coverImageContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker ) gridlayout = QtWidgets.QGridLayout(self.coverImageContainer) gridlayout.addWidget(self.coverWidget) @@ -72,24 +72,24 @@ class IssueSelectionWindow(QtWidgets.QDialog): self.series_id = series_id self.issue_id: str = "" self.config = config - self.talker_api = talker_api + self.talker = talker self.url_fetch_thread = None self.issue_list: list[ComicIssue] = [] # Display talker logo and set url - self.lblIssuesSourceName.setText(talker_api.static_config.attribution_string) + self.lblIssuesSourceName.setText(talker.attribution) self.imageIssuesSourceWidget = CoverImageWidget( self.imageIssuesSourceLogo, CoverImageWidget.URLMode, config.runtime_config.user_cache_dir, - talker_api, + talker, False, ) gridlayoutIssuesSourceLogo = QtWidgets.QGridLayout(self.imageIssuesSourceLogo) gridlayoutIssuesSourceLogo.addWidget(self.imageIssuesSourceWidget) gridlayoutIssuesSourceLogo.setContentsMargins(0, 2, 0, 0) - self.imageIssuesSourceWidget.set_url(talker_api.source_details.logo) + self.imageIssuesSourceWidget.set_url(talker.logo_url) if issue_number is None or issue_number == "": self.issue_number = "1" @@ -119,7 +119,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) try: - self.issue_list = self.talker_api.fetch_issues_by_series(self.series_id) + self.issue_list = self.talker.fetch_issues_by_series(self.series_id) except TalkerError as e: QtWidgets.QApplication.restoreOverrideCursor() QtWidgets.QMessageBox.critical(self, f"{e.source} {e.code_name} Error", f"{e}") diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 2a92774..3e35a1d 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -24,7 +24,7 @@ import sys import settngs import comicapi -import comictalker.comictalkerapi as ct_api +import comictalker from comictaggerlib import cli, ctsettings from comictaggerlib.ctversion import version from comictaggerlib.log import setup_logging @@ -34,21 +34,22 @@ if sys.version_info < (3, 10): else: import importlib.metadata as importlib_metadata +logger = logging.getLogger("comictagger") + try: from comictaggerlib import gui qt_available = gui.qt_available except Exception: + logger.exception("Qt unavailable") qt_available = False -logger = logging.getLogger("comictagger") - logger.setLevel(logging.DEBUG) -def update_publishers(config: settngs.Namespace) -> None: - json_file = config.runtime_config.user_config_dir / "publishers.json" +def update_publishers(config: settngs.Config[settngs.Namespace]) -> None: + json_file = config[0].runtime_config.user_config_dir / "publishers.json" if json_file.exists(): try: comicapi.utils.update_publishers(json.loads(json_file.read_text("utf-8"))) @@ -60,7 +61,7 @@ class App: """docstring for App""" def __init__(self) -> None: - self.config = settngs.Config({}, {}) + self.config: settngs.Config[settngs.Namespace] self.initial_arg_parser = ctsettings.initial_commandline_parser() self.config_load_success = False @@ -73,9 +74,9 @@ class App: self.main() - def load_plugins(self, opts) -> None: + def load_plugins(self, opts: argparse.Namespace) -> None: comicapi.comicarchive.load_archive_plugins() - ctsettings.talkers = ct_api.get_talkers(version, opts.config.user_cache_dir) + ctsettings.talkers = comictalker.get_talkers(version, opts.config.user_cache_dir) def initialize(self) -> argparse.Namespace: conf, _ = self.initial_arg_parser.parse_known_args() @@ -92,9 +93,9 @@ class App: ctsettings.register_file_settings(self.manager) ctsettings.register_plugin_settings(self.manager) - def parse_settings(self, config_paths: ctsettings.ComicTaggerPaths) -> settngs.Config: - config, self.config_load_success = self.manager.parse_config(config_paths.user_config_dir / "settings.json") - config = self.manager.get_namespace(config) + def parse_settings(self, config_paths: ctsettings.ComicTaggerPaths) -> settngs.Config[settngs.Namespace]: + cfg, self.config_load_success = self.manager.parse_config(config_paths.user_config_dir / "settings.json") + config = self.manager.get_namespace(cfg) config = ctsettings.validate_commandline_settings(config, self.manager) config = ctsettings.validate_file_settings(config) @@ -125,7 +126,7 @@ class App: logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"]) comicapi.utils.load_publishers() - update_publishers(self.config[0]) + update_publishers(self.config) if not qt_available and not self.config[0].runtime_no_gui: self.config[0].runtime_no_gui = True @@ -143,31 +144,26 @@ class App: print("Key set") # noqa: T201 return - try: - talker_api = ctsettings.talkers[self.config[0].talkers_source] - except Exception as e: - logger.exception(f"Unable to load talker {self.config[0].talkers_source}") - error = (f"Unable to load talker {self.config[0].talkers_source}: {e}", True) - talker_api = None - if not self.config_load_success: error = ( f"Failed to load settings, check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details", True, ) + talkers = ctsettings.talkers + del ctsettings.talkers + if self.config[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) - assert talker_api is not None try: - cli.CLI(self.config[0], talker_api).run() + cli.CLI(self.config[0], talkers).run() except Exception: logger.exception("CLI mode failed") else: - gui.open_tagger_window(talker_api, self.config, error) + gui.open_tagger_window(talkers, self.config, error) -def main(): +def main() -> None: App().run() diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py index ed8864d..c942404 100644 --- a/comictaggerlib/matchselectionwindow.py +++ b/comictaggerlib/matchselectionwindow.py @@ -26,7 +26,7 @@ from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.resulttypes import IssueResult from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import reduce_widget_font_size -from comictalker.talkerbase import ComicTalker +from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) @@ -37,15 +37,15 @@ class MatchSelectionWindow(QtWidgets.QDialog): parent: QtWidgets.QWidget, matches: list[IssueResult], comic_archive: ComicArchive, - config: settngs.Values, - talker_api: ComicTalker, + config: settngs.Namespace, + talker: ComicTalker, ) -> None: super().__init__(parent) uic.loadUi(ui_path / "matchselectionwindow.ui", self) self.altCoverWidget = CoverImageWidget( - self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker_api + self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker ) gridlayout = QtWidgets.QGridLayout(self.altCoverContainer) gridlayout.addWidget(self.altCoverWidget) diff --git a/comictaggerlib/pagebrowser.py b/comictaggerlib/pagebrowser.py index 79e7968..dc923f7 100644 --- a/comictaggerlib/pagebrowser.py +++ b/comictaggerlib/pagebrowser.py @@ -25,13 +25,12 @@ from comicapi.genericmetadata import GenericMetadata from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.graphics import graphics_path from comictaggerlib.ui import ui_path -from comictalker.talkerbase import ComicTalker logger = logging.getLogger(__name__) class PageBrowserWindow(QtWidgets.QDialog): - def __init__(self, parent: QtWidgets.QWidget, talker_api: ComicTalker, metadata: GenericMetadata) -> None: + def __init__(self, parent: QtWidgets.QWidget, metadata: GenericMetadata) -> None: super().__init__(parent) uic.loadUi(ui_path / "pagebrowser.ui", self) diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 136cb39..56c7171 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -23,7 +23,6 @@ from comicapi.comicarchive import ComicArchive, MetaDataStyle from comicapi.genericmetadata import ImageMetadata, PageType from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ui import ui_path -from comictalker.talkerbase import ComicTalker logger = logging.getLogger(__name__) @@ -68,7 +67,7 @@ class PageListEditor(QtWidgets.QWidget): PageType.Deleted: "Deleted", } - def __init__(self, parent: QtWidgets.QWidget, talker_api: ComicTalker) -> None: + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) uic.loadUi(ui_path / "pagelisteditor.ui", self) diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 5bd643d..81b2f68 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -27,7 +27,7 @@ from comictaggerlib.filerenamer import FileRenamer, get_rename_dir from comictaggerlib.settingswindow import SettingsWindow from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import center_window_on_parent -from comictalker.talkerbase import ComicTalker +from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) @@ -38,8 +38,8 @@ class RenameWindow(QtWidgets.QDialog): parent: QtWidgets.QWidget, comic_archive_list: list[ComicArchive], data_style: int, - config: settngs.Config, - talker_api: ComicTalker, + config: settngs.Config[settngs.Namespace], + talker: ComicTalker, ) -> None: super().__init__(parent) @@ -55,7 +55,7 @@ class RenameWindow(QtWidgets.QDialog): ) self.config = config - self.talker_api = talker_api + self.talker = talker self.comic_archive_list = comic_archive_list self.data_style = data_style self.rename_list: list[str] = [] @@ -162,7 +162,7 @@ class RenameWindow(QtWidgets.QDialog): self.twList.setSortingEnabled(True) def modify_settings(self) -> None: - settingswin = SettingsWindow(self, self.config, self.talker_api) + settingswin = SettingsWindow(self, self.config, self.talker) settingswin.setModal(True) settingswin.show_rename_tab() settingswin.exec() diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 96a127a..164364a 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -33,8 +33,8 @@ from comictaggerlib.matchselectionwindow import MatchSelectionWindow from comictaggerlib.progresswindow import IDProgressWindow from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import reduce_widget_font_size +from comictalker.comictalker import ComicTalker, TalkerError from comictalker.resulttypes import ComicSeries -from comictalker.talkerbase import ComicTalker, TalkerError logger = logging.getLogger(__name__) @@ -45,14 +45,14 @@ class SearchThread(QtCore.QThread): def __init__( self, - talker_api: ComicTalker, + talker: ComicTalker, series_name: str, refresh: bool, literal: bool = False, series_match_thresh: int = 90, ) -> None: QtCore.QThread.__init__(self) - self.talker_api = talker_api + self.talker = talker self.series_name = series_name self.refresh: bool = refresh self.error_e: TalkerError @@ -64,7 +64,7 @@ class SearchThread(QtCore.QThread): def run(self) -> None: try: self.ct_error = False - self.ct_search_results = self.talker_api.search_for_series( + self.ct_search_results = self.talker.search_for_series( self.series_name, self.prog_callback, self.refresh, self.literal, self.series_match_thresh ) except TalkerError as e: @@ -112,7 +112,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): cover_index_list: list[int], comic_archive: ComicArchive | None, config: settngs.Namespace, - talker_api: ComicTalker, + talker: ComicTalker, autoselect: bool = False, literal: bool = False, ) -> None: @@ -121,7 +121,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): uic.loadUi(ui_path / "seriesselectionwindow.ui", self) self.imageWidget = CoverImageWidget( - self.imageContainer, CoverImageWidget.URLMode, config.runtime_config.user_cache_dir, talker_api + self.imageContainer, CoverImageWidget.URLMode, config.runtime_config.user_cache_dir, talker ) gridlayout = QtWidgets.QGridLayout(self.imageContainer) gridlayout.addWidget(self.imageWidget) @@ -156,25 +156,25 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.progdialog: QtWidgets.QProgressDialog | None = None self.search_thread: SearchThread | None = None - self.use_filter = self.config.talkers_always_use_publisher_filter + self.use_filter = self.config.talker_always_use_publisher_filter # Load to retrieve settings - self.talker_api = talker_api + self.talker = talker # Display talker logo and set url - self.lblSourceName.setText(talker_api.static_config.attribution_string) + self.lblSourceName.setText(talker.attribution) self.imageSourceWidget = CoverImageWidget( self.imageSourceLogo, CoverImageWidget.URLMode, config.runtime_config.user_cache_dir, - talker_api, + talker, False, ) gridlayoutSourceLogo = QtWidgets.QGridLayout(self.imageSourceLogo) gridlayoutSourceLogo.addWidget(self.imageSourceWidget) gridlayoutSourceLogo.setContentsMargins(0, 2, 0, 0) - self.imageSourceWidget.set_url(talker_api.source_details.logo) + self.imageSourceWidget.set_url(talker.logo_url) # Set the minimum row height to the default. # this way rows will be more consistent when resizeRowsToContents is called @@ -224,7 +224,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.iddialog.rejected.connect(self.identify_cancel) self.iddialog.show() - self.ii = IssueIdentifier(self.comic_archive, self.config, self.talker_api) + self.ii = IssueIdentifier(self.comic_archive, self.config, self.talker) md = GenericMetadata() md.series = self.series_name @@ -298,7 +298,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): if choices: selector = MatchSelectionWindow( - self, matches, self.comic_archive, talker_api=self.talker_api, config=self.config + self, matches, self.comic_archive, talker=self.talker, config=self.config ) selector.setModal(True) selector.exec() @@ -315,7 +315,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.show_issues() def show_issues(self) -> None: - selector = IssueSelectionWindow(self, self.config, self.talker_api, self.series_id, self.issue_number) + selector = IssueSelectionWindow(self, self.config, self.talker, self.series_id, self.issue_number) title = "" for record in self.ct_search_results: if record.id == self.series_id: @@ -343,7 +343,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): def perform_query(self, refresh: bool = False) -> None: self.search_thread = SearchThread( - self.talker_api, self.series_name, refresh, self.literal, self.config.talkers_series_match_search_thresh + self.talker, self.series_name, refresh, self.literal, self.config.talker_series_match_search_thresh ) self.search_thread.searchComplete.connect(self.search_complete) self.search_thread.progressUpdate.connect(self.search_progress_update) @@ -410,7 +410,7 @@ class SeriesSelectionWindow(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.config.talkers_sort_series_by_year: + if self.config.talker_sort_series_by_year: try: self.ct_search_results = sorted( self.ct_search_results, @@ -428,7 +428,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): logger.exception("bad data error sorting results by count_of_issues") # move sanitized matches to the front - if self.config.talkers_exact_series_matches_first: + if self.config.talker_exact_series_matches_first: try: sanitized = utils.sanitize_title(self.series_name, False).casefold() sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold() diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 437c216..13fc626 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -32,7 +32,7 @@ from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements from comictaggerlib.imagefetcher import ImageFetcher from comictaggerlib.ui import ui_path from comictalker.comiccacher import ComicCacher -from comictalker.talkerbase import ComicTalker +from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) @@ -130,7 +130,9 @@ Spider-Geddon #1 - New Players; Check In class SettingsWindow(QtWidgets.QDialog): - def __init__(self, parent: QtWidgets.QWidget, config: settngs.Config, talker_api: ComicTalker) -> None: + def __init__( + self, parent: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talker: ComicTalker + ) -> None: super().__init__(parent) uic.loadUi(ui_path / "settingswindow.ui", self) @@ -140,7 +142,7 @@ class SettingsWindow(QtWidgets.QDialog): ) self.config = config - self.talker_api = talker_api + self.talker = talker self.name = "Settings" if platform.system() == "Windows": @@ -294,12 +296,12 @@ class SettingsWindow(QtWidgets.QDialog): def settings_to_form(self) -> None: self.disconnect_signals() # Copy values from settings to form - if "archiver" in self.config[1] and "rar" in self.config[1]["archiver"]: - self.leRarExePath.setText(getattr(self.config[0], self.config[1]["archiver"]["rar"].internal_name)) + if "archiver" in self.config[1] and "rar" in self.config[1]["archiver"].v: + self.leRarExePath.setText(getattr(self.config[0], self.config[1]["archiver"].v["rar"].internal_name)) else: self.leRarExePath.setEnabled(False) self.sbNameMatchIdentifyThresh.setValue(self.config[0].identifier_series_match_identify_thresh) - self.sbNameMatchSearchThresh.setValue(self.config[0].talkers_series_match_search_thresh) + self.sbNameMatchSearchThresh.setValue(self.config[0].talker_series_match_search_thresh) self.tePublisherFilter.setPlainText("\n".join(self.config[0].identifier_publisher_filter)) self.cbxCheckForNewVersion.setChecked(self.config[0].general_check_for_new_version) @@ -311,12 +313,12 @@ class SettingsWindow(QtWidgets.QDialog): self.switch_parser() self.cbxUseSeriesStartAsVolume.setChecked(self.config[0].talker_comicvine_cv_use_series_start_as_volume) - self.cbxClearFormBeforePopulating.setChecked(self.config[0].talkers_clear_form_before_populating) + self.cbxClearFormBeforePopulating.setChecked(self.config[0].talker_clear_form_before_populating) self.cbxRemoveHtmlTables.setChecked(self.config[0].talker_comicvine_cv_remove_html_tables) - self.cbxUseFilter.setChecked(self.config[0].talkers_always_use_publisher_filter) - self.cbxSortByYear.setChecked(self.config[0].talkers_sort_series_by_year) - self.cbxExactMatches.setChecked(self.config[0].talkers_exact_series_matches_first) + self.cbxUseFilter.setChecked(self.config[0].talker_always_use_publisher_filter) + self.cbxSortByYear.setChecked(self.config[0].talker_sort_series_by_year) + self.cbxExactMatches.setChecked(self.config[0].talker_exact_series_matches_first) self.leKey.setText(self.config[0].talker_comicvine_cv_api_key) self.leURL.setText(self.config[0].talker_comicvine_cv_url) @@ -403,11 +405,11 @@ class SettingsWindow(QtWidgets.QDialog): ) # Copy values from form to settings and save - if "archiver" in self.config[1] and "rar" in self.config[1]["archiver"]: - setattr(self.config[0], self.config[1]["archiver"]["rar"].internal_name, str(self.leRarExePath.text())) + if "archiver" in self.config[1] and "rar" in self.config[1]["archiver"].v: + setattr(self.config[0], self.config[1]["archiver"].v["rar"].internal_name, str(self.leRarExePath.text())) # make sure rar program is now in the path for the rar class - if self.config[0].archivers_rar: + if self.config[0].archiver_rar: utils.add_to_path(os.path.dirname(str(self.leRarExePath.text()))) if not str(self.leIssueNumPadding.text()).isdigit(): @@ -416,7 +418,7 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].general_check_for_new_version = self.cbxCheckForNewVersion.isChecked() self.config[0].identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value() - self.config[0].talkers_series_match_search_thresh = self.sbNameMatchSearchThresh.value() + self.config[0].talker_series_match_search_thresh = self.sbNameMatchSearchThresh.value() self.config[0].identifier_publisher_filter = [ x.strip() for x in str(self.tePublisherFilter.toPlainText()).splitlines() if x.strip() ] @@ -427,20 +429,20 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked() self.config[0].talker_comicvine_cv_use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked() - self.config[0].talkers_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() + self.config[0].talker_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() self.config[0].talker_comicvine_cv_remove_html_tables = self.cbxRemoveHtmlTables.isChecked() - self.config[0].talkers_always_use_publisher_filter = self.cbxUseFilter.isChecked() - self.config[0].talkers_sort_series_by_year = self.cbxSortByYear.isChecked() - self.config[0].talkers_exact_series_matches_first = self.cbxExactMatches.isChecked() + self.config[0].talker_always_use_publisher_filter = self.cbxUseFilter.isChecked() + self.config[0].talker_sort_series_by_year = self.cbxSortByYear.isChecked() + self.config[0].talker_exact_series_matches_first = self.cbxExactMatches.isChecked() if self.leKey.text().strip(): self.config[0].talker_comicvine_cv_api_key = self.leKey.text().strip() - self.talker_api.api_key = self.config[0].talker_comicvine_cv_api_key + self.talker.api_key = self.config[0].talker_comicvine_cv_api_key if self.leURL.text().strip(): self.config[0].talker_comicvine_cv_url = self.leURL.text().strip() - self.talker_api.api_url = self.config[0].talker_comicvine_cv_url + self.talker.api_url = self.config[0].talker_comicvine_cv_url self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() @@ -462,10 +464,17 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].rename_strict = self.cbxRenameStrict.isChecked() self.config[0].rename_replacements = self.get_replacements() + self.update_talkers_config() + settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json") self.parent().config = self.config QtWidgets.QDialog.accept(self) + def update_talkers_config(self) -> None: + cfg = settngs.normalize_config(self.config, True, True) + if f"talker_{self.talker.id}" in cfg[0]: + self.talker.parse_settings(cfg[0][f"talker_{self.talker.id}"]) + def select_rar(self) -> None: self.select_file(self.leRarExePath, "RAR") @@ -475,7 +484,7 @@ class SettingsWindow(QtWidgets.QDialog): QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.") def test_api_key(self) -> None: - if self.talker_api.check_api_key(self.leKey.text().strip(), self.leURL.text().strip()): + if self.talker.check_api_key(self.leKey.text().strip(), self.leURL.text().strip()): QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!") else: QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.") diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index e708347..35dfc0b 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -63,7 +63,7 @@ from comictaggerlib.settingswindow import SettingsWindow from comictaggerlib.ui import ui_path from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size from comictaggerlib.versionchecker import VersionChecker -from comictalker.talkerbase import ComicTalker, TalkerError +from comictalker.comictalker import ComicTalker, TalkerError logger = logging.getLogger(__name__) @@ -79,15 +79,15 @@ class TaggerWindow(QtWidgets.QMainWindow): def __init__( self, file_list: list[str], - config: settngs.Config, - talker_api: ComicTalker, + config: settngs.Config[settngs.Namespace], + talkers: dict[str, ComicTalker], parent: QtWidgets.QWidget | None = None, ) -> None: super().__init__(parent) uic.loadUi(ui_path / "taggerwindow.ui", self) self.config = config - self.talker_api = talker_api + self.talkers = talkers self.log_window = self.setup_logger() # prevent multiple instances @@ -126,7 +126,7 @@ class TaggerWindow(QtWidgets.QMainWindow): grid_layout.addWidget(self.archiveCoverWidget) grid_layout.setContentsMargins(0, 0, 0, 0) - self.page_list_editor = PageListEditor(self.tabPages, self.talker_api) + self.page_list_editor = PageListEditor(self.tabPages) grid_layout = QtWidgets.QGridLayout(self.tabPages) grid_layout.addWidget(self.page_list_editor) @@ -249,21 +249,27 @@ class TaggerWindow(QtWidgets.QMainWindow): self, "Welcome!", """ -Thanks for trying ComicTagger!

-Be aware that this is beta-level software, and consider it experimental. -You should use it very carefully when modifying your data files. As the -license says, it's "AS IS!"

-Also, be aware that writing tags to comic archives will change their file hashes, -which has implications with respect to other software packages. It's best to -use ComicTagger on local copies of your comics.

-Have fun! -""", + Thanks for trying ComicTagger!

+ Be aware that this is beta-level software, and consider it experimental. + You should use it very carefully when modifying your data files. As the + license says, it's "AS IS!"

+ Also, be aware that writing tags to comic archives will change their file hashes, + which has implications with respect to other software packages. It's best to + use ComicTagger on local copies of your comics.

+ Have fun! + """, ) self.config[0].dialog_show_disclaimer = not checked if self.config[0].general_check_for_new_version: self.check_latest_version_online() + def current_talker(self) -> ComicTalker: + if self.config[0].talker_source in self.talkers: + return self.talkers[self.config[0].talker_source] + logger.error("Could not find the '%s' talker", self.config[0].talker_source) + raise SystemExit(2) + def open_file_event(self, url: QtCore.QUrl) -> None: logger.info(url.toLocalFile()) self.fileSelectionList.add_path_list([url.toLocalFile()]) @@ -1045,7 +1051,7 @@ Have fun! cover_index_list, self.comic_archive, self.config[0], - self.talker_api, + self.current_talker(), autoselect, literal, ) @@ -1063,7 +1069,7 @@ Have fun! self.form_to_metadata() try: - new_metadata = self.talker_api.fetch_comic_data( + new_metadata = self.current_talker().fetch_comic_data( issue_id=selector.issue_id, series_id=selector.series_id, issue_number=selector.issue_number ) except TalkerError as e: @@ -1075,11 +1081,11 @@ Have fun! if self.config[0].cbl_apply_transform_on_import: new_metadata = CBLTransformer(new_metadata, self.config[0]).apply() - if self.config[0].talkers_clear_form_before_populating: + if self.config[0].talker_clear_form_before_populating: self.clear_form() notes = ( - f"Tagged with ComicTagger {ctversion.version} using info from {self.talker_api.source_details.name} on" + f"Tagged with ComicTagger {ctversion.version} using info from {self.current_talker().name} on" f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {new_metadata.issue_id}]" ) self.metadata.overlay( @@ -1360,7 +1366,7 @@ Have fun! def show_settings(self) -> None: - settingswin = SettingsWindow(self, self.config, self.talker_api) + settingswin = SettingsWindow(self, self.config, self.current_talker()) settingswin.setModal(True) settingswin.exec() settingswin.result() @@ -1669,7 +1675,7 @@ Have fun! QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) try: - ct_md = self.talker_api.fetch_comic_data(match["issue_id"]) + ct_md = self.current_talker().fetch_comic_data(match["issue_id"]) except TalkerError: logger.exception("Save aborted.") @@ -1694,7 +1700,7 @@ Have fun! self, ca: ComicArchive, match_results: OnlineMatchResults, dlg: AutoTagStartWindow ) -> tuple[bool, OnlineMatchResults]: success = False - ii = IssueIdentifier(ca, self.config[0], self.talker_api) + ii = IssueIdentifier(ca, self.config[0], self.current_talker()) # read in metadata, and parse file name if not there try: @@ -1793,7 +1799,7 @@ Have fun! ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config[0].talkers_auto_imprint: + if self.config[0].talker_auto_imprint: md.fix_publisher() if not ca.write_metadata(md, self.save_data_style): @@ -1834,7 +1840,7 @@ Have fun! if not atstartdlg.exec(): return - self.atprogdialog = AutoTagProgressWindow(self, self.talker_api) + self.atprogdialog = AutoTagProgressWindow(self, self.current_talker()) self.atprogdialog.setModal(True) self.atprogdialog.show() self.atprogdialog.progressBar.setMaximum(len(ca_list)) @@ -1926,7 +1932,7 @@ Have fun! style, self.actual_issue_data_fetch, self.config[0], - self.talker_api, + self.current_talker(), ) matchdlg.setModal(True) matchdlg.exec() @@ -1989,7 +1995,7 @@ Have fun! def show_page_browser(self) -> None: if self.page_browser is None: - self.page_browser = PageBrowserWindow(self, self.talker_api, self.metadata) + self.page_browser = PageBrowserWindow(self, self.metadata) if self.comic_archive is not None: self.page_browser.set_comic_archive(self.comic_archive) self.page_browser.finished.connect(self.page_browser_closed) @@ -2056,7 +2062,7 @@ Have fun! "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.config, self.talker_api) + dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.current_talker()) dlg.setModal(True) if dlg.exec() and self.comic_archive is not None: self.fileSelectionList.update_selected_rows() diff --git a/comictalker/__init__.py b/comictalker/__init__.py index 9d48db4..e5bd095 100644 --- a/comictalker/__init__.py +++ b/comictalker/__init__.py @@ -1 +1,31 @@ from __future__ import annotations + +import logging +import pathlib + +import comictalker.talkers.comicvine +from comictalker.comictalker import ComicTalker, TalkerError +from comictalker.resulttypes import ComicIssue, ComicSeries + +logger = logging.getLogger(__name__) + +__all__ = [ + "ComicTalker", + "TalkerError", + "ComicIssue", + "ComicSeries", +] + + +def get_talkers(version: str, cache: pathlib.Path) -> dict[str, ComicTalker]: + """Returns all comic talker instances""" + talkers: dict[str, ComicTalker] = {} + + for talker in [comictalker.talkers.comicvine.ComicVineTalker]: + try: + obj = talker(version, cache) + talkers[obj.id] = obj + except Exception: + logger.exception("Failed to load talker: %s", "comicvine") + raise TalkerError(source="comicvine", code=4, desc="Failed to initialise talker") + return talkers diff --git a/comictalker/talkerbase.py b/comictalker/comictalker.py similarity index 81% rename from comictalker/talkerbase.py rename to comictalker/comictalker.py index 60bbf01..7fe7d5a 100644 --- a/comictalker/talkerbase.py +++ b/comictalker/comictalker.py @@ -25,38 +25,6 @@ from comictalker.resulttypes import ComicIssue, ComicSeries logger = logging.getLogger(__name__) -class SourceDetails: - def __init__( - self, - name: str = "", - ident: str = "", - logo: str = "", - ): - self.name = name - self.id = ident - self.logo = logo - - -class SourceStaticSettings: - def __init__( - self, - website: str = "", - attribution_string: str = "", # Full string including web link, example: Metadata provided by Example - has_issues: bool = False, - has_alt_covers: bool = False, - requires_apikey: bool = False, - has_nsfw: bool = False, - has_censored_covers: bool = False, - ) -> None: - self.website = website - self.attribution_string = attribution_string - self.has_issues = has_issues - self.has_alt_covers = has_alt_covers - self.requires_apikey = requires_apikey - self.has_nsfw = has_nsfw - self.has_censored_covers = has_censored_covers - - class TalkerError(Exception): """Base class exception for information sources. @@ -71,10 +39,8 @@ class TalkerError(Exception): codes = {1: "General", 2: "Network", 3: "Data", 4: "Other"} - def __init__(self, source: str, desc: str, code: int = 4, sub_code: int = 0) -> None: + def __init__(self, source: str, desc: str = "Unknown", code: int = 4, sub_code: int = 0) -> None: super().__init__() - if desc == "": - desc = "Unknown" self.desc = desc self.code = code self.code_name = self.codes[code] @@ -136,14 +102,16 @@ class TalkerDataError(TalkerError): super().__init__(source, desc, 3, sub_code) -# Class talkers instance class ComicTalker: """The base class for all comic source talkers""" + name: str = "Example" + id: str = "example" + logo_url: str = "https://example.com/logo.png" + website: str = "https://example.com/" + attribution: str = f"Metadata provided by {name}" + def __init__(self, version: str, cache_folder: pathlib.Path) -> None: - # Identity name for the information source etc. - self.source_details = SourceDetails() - self.static_config = SourceStaticSettings() self.cache_folder = cache_folder self.version = version self.api_key: str = "" @@ -153,10 +121,12 @@ class ComicTalker: """Allows registering settings using the settngs package with an argparse like interface""" return None - def parse_settings(self, settings: dict[str, Any]) -> None: - """settings is a dictionary of options defined in register_settings. - It is only guaranteed that the settings defined in register_settings will be present.""" - return None + def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]: + """ + settings is a dictionary of settings defined in register_settings. + It is only guaranteed that the settings defined in register_settings will be present. + """ + return settings def check_api_key(self, key: str, url: str) -> bool: """ diff --git a/comictalker/comictalkerapi.py b/comictalker/comictalkerapi.py deleted file mode 100644 index 664f7cb..0000000 --- a/comictalker/comictalkerapi.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Handles collecting data from source talkers.""" - -# 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 logging -import pathlib - -import comictalker.talkers.comicvine -from comictalker.talkerbase import ComicTalker, TalkerError - -logger = logging.getLogger(__name__) - - -def get_talkers(version: str, cache: pathlib.Path) -> dict[str, ComicTalker]: - """Returns all comic talker instances""" - # TODO separate PR will bring talkers in via entry points. TalkerError etc. source will then be a var - talkers: dict[str, ComicTalker] = {} - - for talker in [comictalker.talkers.comicvine.ComicVineTalker]: - try: - obj = talker(version, cache) - talkers[obj.source_details.id] = obj - except Exception: - logger.exception("Failed to load talker: %s", "comicvine") - raise TalkerError(source="comicvine", code=4, desc="Failed to initialise talker") - return talkers diff --git a/comictalker/talker_utils.py b/comictalker/talker_utils.py index 26efecb..bd605ca 100644 --- a/comictalker/talker_utils.py +++ b/comictalker/talker_utils.py @@ -1,5 +1,3 @@ -"""Generic sources utils to format API data and the like. -""" # Copyright 2012-2014 Anthony Beville # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +21,7 @@ from bs4 import BeautifulSoup from comicapi import utils from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString -from comictalker.talkerbase import ComicIssue +from comictalker.resulttypes import ComicIssue logger = logging.getLogger(__name__) diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 1689203..8d77cc8 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -33,8 +33,8 @@ from comicapi import utils from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString from comictalker.comiccacher import ComicCacher +from comictalker.comictalker import ComicTalker, TalkerDataError, TalkerNetworkError from comictalker.resulttypes import ComicIssue, ComicSeries, Credit -from comictalker.talkerbase import ComicTalker, SourceDetails, SourceStaticSettings, TalkerDataError, TalkerNetworkError logger = logging.getLogger(__name__) @@ -150,40 +150,28 @@ class CVResult(TypedDict, Generic[T]): version: str -CV_RATE_LIMIT_STATUS = 107 +CV_STATUS_RATELIMIT = 107 class ComicVineTalker(ComicTalker): + name: str = "Comic Vine" + id: str = "comicvine" + logo_url: str = "https://comicvine.gamespot.com/a/bundles/comicvinesite/images/logo.png" + website: str = "https://comicvine.gamespot.com/" + attribution: str = f"Metadata provided by {name}" + def __init__( self, version: str, cache_folder: pathlib.Path, ): super().__init__(version, cache_folder) - self.source_details = SourceDetails( - name="Comic Vine", - ident="comicvine", - logo="https://comicvine.gamespot.com/a/bundles/comicvinesite/images/logo.png", - ) - self.static_config = SourceStaticSettings( - website="https://comicvine.gamespot.com/", - attribution_string="Metadata provided by Comic Vine", - has_issues=True, - has_alt_covers=True, - requires_apikey=True, - has_nsfw=False, - has_censored_covers=False, - ) # Default settings self.api_url: str = "https://comicvine.gamespot.com/api" self.api_key: str = "27431e6787042105bd3e47e169a624521f89f3a4" self.remove_html_tables: bool = False self.use_series_start_as_volume: bool = False - self.wait_for_rate_limit: bool = False - - # Identity name for the information source - self.source_name: str = self.source_details.id - self.source_name_friendly: str = self.source_details.name + self.wait_on_ratelimit: bool = False tmp_url = urlsplit(self.api_url) @@ -194,7 +182,7 @@ class ComicVineTalker(ComicTalker): self.api_url = tmp_url.geturl() # NOTE: This was hardcoded before which is why it isn't in settings - self.wait_for_rate_limit_time: int = 20 + self.wait_on_ratelimit_time: int = 20 def register_settings(self, parser: settngs.Manager) -> None: parser.add_setting("--cv-use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction) @@ -214,9 +202,7 @@ class ComicVineTalker(ComicTalker): help="Use the given Comic Vine URL.", ) - def parse_settings(self, settings: dict[str, Any]) -> None: - self.remove_html_tables = settings["cv_remove_html_tables"] - self.use_series_start_as_volume = settings["cv_use_series_start_as_volume"] + def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]: if settings["cv_api_key"]: self.api_key = settings["cv_api_key"] if settings["cv_url"]: @@ -227,6 +213,11 @@ class ComicVineTalker(ComicTalker): self.api_url = tmp_url.geturl() + self.use_series_start_as_volume = settings["cv_use_series_start_as_volume"] + self.wait_on_ratelimit = settings["cv_wait_on_ratelimit"] + self.remove_html_tables = settings["cv_remove_html_tables"] + return settngs + def check_api_key(self, key: str, url: str) -> bool: if not url: url = self.api_url @@ -259,7 +250,7 @@ class ComicVineTalker(ComicTalker): wait_times = [1, 2, 3, 4] while True: cv_response: CVResult = self.get_url_content(url, params) - if self.wait_for_rate_limit and cv_response["status_code"] == CV_RATE_LIMIT_STATUS: + if self.wait_on_ratelimit and cv_response["status_code"] == CV_STATUS_RATELIMIT: logger.info(f"Rate limit encountered. Waiting for {limit_wait_time} minutes\n") time.sleep(limit_wait_time * 60) total_time_waited += limit_wait_time @@ -267,15 +258,13 @@ class ComicVineTalker(ComicTalker): if counter < 3: counter += 1 # don't wait much more than 20 minutes - if total_time_waited < self.wait_for_rate_limit_time: + if total_time_waited < self.wait_on_ratelimit_time: continue if cv_response["status_code"] != 1: logger.debug( - f"{self.source_name_friendly} query failed with error #{cv_response['status_code']}: [{cv_response['error']}]." - ) - raise TalkerNetworkError( - self.source_name_friendly, 0, f"{cv_response['status_code']}: {cv_response['error']}" + f"{self.name} query failed with error #{cv_response['status_code']}: [{cv_response['error']}]." ) + raise TalkerNetworkError(self.name, 0, f"{cv_response['status_code']}: {cv_response['error']}") # it's all good break @@ -298,16 +287,16 @@ class ComicVineTalker(ComicTalker): break except requests.exceptions.Timeout: - logger.debug(f"Connection to {self.source_name_friendly} timed out.") - raise TalkerNetworkError(self.source_name_friendly, 4) + logger.debug(f"Connection to {self.name} timed out.") + raise TalkerNetworkError(self.name, 4) except requests.exceptions.RequestException as e: logger.debug(f"Request exception: {e}") - raise TalkerNetworkError(self.source_name_friendly, 0, str(e)) from e + raise TalkerNetworkError(self.name, 0, str(e)) from e except json.JSONDecodeError as e: logger.debug(f"JSON decode error: {e}") - raise TalkerDataError(self.source_name_friendly, 2, "ComicVine did not provide json") + raise TalkerDataError(self.name, 2, "ComicVine did not provide json") - raise TalkerNetworkError(self.source_name_friendly, 5) + raise TalkerNetworkError(self.name, 5) def format_search_results(self, search_results: list[CVSeries]) -> list[ComicSeries]: formatted_results = [] @@ -415,13 +404,13 @@ class ComicVineTalker(ComicTalker): ) -> list[ComicSeries]: # Sanitize the series name for comicvine searching, comicvine search ignore symbols search_series_name = utils.sanitize_title(series_name, literal) - logger.info(f"{self.source_name_friendly} searching: {search_series_name}") + logger.info(f"{self.name} searching: {search_series_name}") # Before we search online, look in our cache, since we might have done this same search recently # For literal searches always retrieve from online cvc = ComicCacher(self.cache_folder, self.version) if not refresh_cache and not literal: - cached_search_results = cvc.get_search_results(self.source_name, series_name) + cached_search_results = cvc.get_search_results(self.id, series_name) if len(cached_search_results) > 0: return cached_search_results @@ -495,7 +484,7 @@ class ComicVineTalker(ComicTalker): # Cache these search results, even if it's literal we cache the results # The most it will cause is extra processing time - cvc.add_search_results(self.source_name, series_name, formatted_search_results) + cvc.add_search_results(self.id, series_name, formatted_search_results) return formatted_search_results @@ -514,7 +503,7 @@ class ComicVineTalker(ComicTalker): def fetch_series_data(self, series_id: int) -> ComicSeries: # before we search online, look in our cache, since we might already have this info cvc = ComicCacher(self.cache_folder, self.version) - cached_series_result = cvc.get_series_info(str(series_id), self.source_name) + cached_series_result = cvc.get_series_info(str(series_id), self.id) if cached_series_result is not None: return cached_series_result @@ -531,14 +520,14 @@ class ComicVineTalker(ComicTalker): formatted_series_results = self.format_search_results([series_results]) if series_results: - cvc.add_series_info(self.source_name, formatted_series_results[0]) + cvc.add_series_info(self.id, formatted_series_results[0]) return formatted_series_results[0] def fetch_issues_by_series(self, series_id: str) -> list[ComicIssue]: # before we search online, look in our cache, since we might already have this info cvc = ComicCacher(self.cache_folder, self.version) - cached_series_issues_result = cvc.get_series_issues_info(series_id, self.source_name) + cached_series_issues_result = cvc.get_series_issues_info(series_id, self.id) series_data = self.fetch_series_data(int(series_id)) @@ -575,7 +564,7 @@ class ComicVineTalker(ComicTalker): # Format to expected output formatted_series_issues_result = self.format_issue_results(series_issues_result) - cvc.add_series_issues_info(self.source_name, formatted_series_issues_result) + cvc.add_series_issues_info(self.id, formatted_series_issues_result) return formatted_series_issues_result @@ -640,7 +629,7 @@ class ComicVineTalker(ComicTalker): if f_record and f_record.complete: # Cache had full record return talker_utils.map_comic_issue_to_metadata( - f_record, self.source_name_friendly, self.remove_html_tables, self.use_series_start_as_volume + f_record, self.name, self.remove_html_tables, self.use_series_start_as_volume ) if f_record is not None: @@ -650,12 +639,12 @@ class ComicVineTalker(ComicTalker): def fetch_issue_data_by_issue_id(self, issue_id: str) -> GenericMetadata: # before we search online, look in our cache, since we might already have this info cvc = ComicCacher(self.cache_folder, self.version) - cached_issues_result = cvc.get_issue_info(int(issue_id), self.source_name) + cached_issues_result = cvc.get_issue_info(int(issue_id), self.id) if cached_issues_result and cached_issues_result.complete: return talker_utils.map_comic_issue_to_metadata( cached_issues_result, - self.source_name_friendly, + self.name, self.remove_html_tables, self.use_series_start_as_volume, ) @@ -672,12 +661,12 @@ class ComicVineTalker(ComicTalker): # Due to issue not returning publisher, fetch the series. cv_issues[0].series = self.fetch_series_data(int(cv_issues[0].series.id)) - cvc.add_series_issues_info(self.source_name, cv_issues) + cvc.add_series_issues_info(self.id, cv_issues) # Now, map the ComicIssue data to generic metadata return talker_utils.map_comic_issue_to_metadata( cv_issues[0], - self.source_name_friendly, + self.name, self.remove_html_tables, self.use_series_start_as_volume, ) diff --git a/requirements.txt b/requirements.txt index 2d29204..a6aefcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ natsort>=8.1.0 pathvalidate pillow>=9.1.0, <10 pycountry -pyicu; sys_platform == 'linux' or sys_platform == 'darwin' +#pyicu; sys_platform == 'linux' or sys_platform == 'darwin' rapidfuzz>=2.12.0 requests==2.* settngs==0.5.0 diff --git a/tests/comicvinetalker_test.py b/tests/comicvinetalker_test.py index 5621189..149bddb 100644 --- a/tests/comicvinetalker_test.py +++ b/tests/comicvinetalker_test.py @@ -11,7 +11,7 @@ import testing.comicvine def test_search_for_series(comicvine_api, comic_cache): results = comicvine_api.search_for_series("cory doctorows futuristic tales of the here and now") cache_issues = comic_cache.get_search_results( - comicvine_api.source_name, "cory doctorows futuristic tales of the here and now" + comicvine_api.id, "cory doctorows futuristic tales of the here and now" ) assert results == cache_issues @@ -20,7 +20,7 @@ def test_fetch_series_data(comicvine_api, comic_cache): result = comicvine_api.fetch_series_data(23437) # del result["description"] # del result["image_url"] - cache_result = comic_cache.get_series_info(23437, comicvine_api.source_name) + cache_result = comic_cache.get_series_info(23437, comicvine_api.id) # del cache_result["description"] # del cache_result["image_url"] assert result == cache_result @@ -28,7 +28,7 @@ def test_fetch_series_data(comicvine_api, comic_cache): def test_fetch_issues_by_series(comicvine_api, comic_cache): results = comicvine_api.fetch_issues_by_series(23437) - cache_issues = comic_cache.get_series_issues_info(23437, comicvine_api.source_name) + cache_issues = comic_cache.get_series_issues_info(23437, comicvine_api.id) assert dataclasses.asdict(results[0])["series"] == dataclasses.asdict(cache_issues[0])["series"] diff --git a/tests/conftest.py b/tests/conftest.py index 3127f98..40084d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,8 +15,8 @@ from PIL import Image import comicapi.comicarchive import comicapi.genericmetadata import comictaggerlib.ctsettings +import comictalker import comictalker.comiccacher -import comictalker.comictalkerapi import comictalker.talkers.comicvine from comicapi import utils from testing import comicvine, filenames