diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 53c599f..f44ddbf 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -15,7 +15,6 @@ # limitations under the License. from __future__ import annotations -import argparse import html import logging import os @@ -26,6 +25,7 @@ from typing import Any import settngs from PyQt5 import QtCore, QtGui, QtWidgets, uic +import comictaggerlib.ui.talkeruigenerator from comicapi import utils from comicapi.genericmetadata import md_test from comictaggerlib.ctversion import version @@ -190,6 +190,15 @@ class SettingsWindow(QtWidgets.QDialog): self.settings_to_form() self.rename_test() self.dir_test() + self.sources: dict = {} + self.sources = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs( + parent, self.tTalkerTabs, self.config, self.talkers + ) + # Select active source in dropdown + self.cobxInfoSource.setCurrentIndex(self.cobxInfoSource.findData(self.config[0].talker_source)) + + # Set General as start tab + self.tabWidget.setCurrentIndex(0) def connect_signals(self) -> None: self.btnBrowseRar.clicked.connect(self.select_rar) @@ -234,100 +243,6 @@ class SettingsWindow(QtWidgets.QDialog): self.twLiteralReplacements.cellChanged.disconnect() self.twValueReplacements.cellChanged.disconnect() - self.sources: dict = {} - self.generate_source_option_tabs() - - def generate_source_option_tabs(self) -> None: - def format_internal_name(int_name: str = "") -> str: - # Presume talker__ - int_name_split = int_name.split("_") - del int_name_split[0:3] - int_name_split[0] = int_name_split[0].capitalize() - new_name = " ".join(int_name_split) - return new_name - - # Add source sub tabs to Comic Sources tab - for talker_id, talker_obj in self.talkers.items(): - # Add source to general tab dropdown list - self.cobxInfoSource.addItem(talker_obj.name, talker_id) - - # Use a dict to make a var name from var - source_info = {} - tab_name = talker_id - source_info[tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} - layout_grid = QtWidgets.QGridLayout() - row = 0 - - full_talker_name = "talker_" + talker_id - for option in self.config[1][full_talker_name][1].values(): - current_widget = None - if option.action is not None and isinstance(option.action, type(argparse.BooleanOptionalAction)): - # bool equals a checkbox (QCheckBox) - current_widget = QtWidgets.QCheckBox(format_internal_name(option.internal_name)) - # Set widget status - current_widget.setChecked(getattr(self.config[0], option.internal_name)) - # Add widget and span all columns - layout_grid.addWidget(current_widget, row, 0, 1, -1) - elif isinstance(option.type, type(int)): - # int equals a spinbox (QSpinBox) - lbl = QtWidgets.QLabel(option.internal_name) - # Create a label - layout_grid.addWidget(lbl, row, 0) - current_widget = QtWidgets.QSpinBox() - current_widget.setRange(0, 9999) - current_widget.setValue(getattr(self.config[0], option.internal_name)) - layout_grid.addWidget(current_widget, row, 1, alignment=QtCore.Qt.AlignLeft) - elif isinstance(option.type, type(float)): - # float equals a spinbox (QDoubleSpinBox) - lbl = QtWidgets.QLabel(format_internal_name(option.internal_name)) - # Create a label - layout_grid.addWidget(lbl, row, 0) - current_widget = QtWidgets.QDoubleSpinBox() - current_widget.setRange(0, 9999.99) - current_widget.setValue(getattr(self.config[0], option.internal_name)) - layout_grid.addWidget(current_widget, row, 1, alignment=QtCore.Qt.AlignLeft) - # type of None should be string - elif option.type is None or isinstance(option.type, type(str)): - # str equals a text field (QLineEdit) - lbl = QtWidgets.QLabel(format_internal_name(option.internal_name)) - # Create a label - layout_grid.addWidget(lbl, row, 0) - current_widget = QtWidgets.QLineEdit() - # Set widget status - current_widget.setText(getattr(self.config[0], option.internal_name)) - layout_grid.addWidget(current_widget, row, 1) - # Special case for api_key, make a test button - if option.internal_name.endswith("api_key"): - btn = QtWidgets.QPushButton("Test Key") - layout_grid.addWidget(btn, row, 2) - btn.clicked.connect(lambda state, sn=talker_id: self.test_api_key(sn)) - row += 1 - - if current_widget: - # Add tooltip text - current_widget.setToolTip(option.help) - - source_info[tab_name]["widgets"][option.internal_name] = current_widget - else: - # An empty current_widget implies an unsupported type - logger.info(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") - - # Add vertical spacer - vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - layout_grid.addItem(vspacer, row, 0) - # Display the new widgets - source_info[tab_name]["tab"].setLayout(layout_grid) - - # Add new sub tab to Comic Source tab - self.tTalkerTabs.addTab(source_info[tab_name]["tab"], talker_obj.name) - self.sources.update(source_info) - - # Select active source in dropdown - self.cobxInfoSource.setCurrentIndex(self.cobxInfoSource.findData(self.config[0].talker_source)) - - # Set General as start tab - self.tabWidget.setCurrentIndex(0) - def addLiteralReplacement(self) -> None: self.insertRow(self.twLiteralReplacements, self.twLiteralReplacements.rowCount(), Replacement("", "", False)) @@ -542,7 +457,7 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].rename_strict = self.cbxRenameStrict.isChecked() self.config[0].rename_replacements = self.get_replacements() - # Read settings from sources tabs and generate self.settings.config data + # Read settings from sources tabs and generate self.config data for tab in self.sources.items(): for name, widget in tab[1]["widgets"].items(): widget_value = None @@ -574,21 +489,6 @@ class SettingsWindow(QtWidgets.QDialog): ComicCacher(self.config[0].runtime_config.user_cache_dir, version).clear_cache() QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.") - def test_api_key(self, source_id) -> None: - # Find URL and API key - for tab in self.sources.items(): - for name, widget in tab[1]["widgets"].items(): - if tab[0] == source_id: - if name.endswith("api_key"): - key = widget.text().strip() - if name.endswith("url"): - url = widget.text().strip() - - if self.talkers[source_id].check_api_key(key, url): - QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!") - else: - QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid!") - def reset_settings(self) -> None: self.config = settngs.get_namespace(settngs.defaults(self.config[1])) self.settings_to_form() diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py new file mode 100644 index 0000000..306df1e --- /dev/null +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import argparse +import logging + +import settngs +from PyQt5 import QtCore, QtWidgets + +from comictalker.comictalker import ComicTalker + +logger = logging.getLogger(__name__) + + +def call_check_api_key( + talker_id: str, + sources_info: dict[str, QtWidgets.QWidget], + talkers: dict[str, ComicTalker], + parent: QtWidgets.QWidget, +): + key = "" + # Find the correct widget to get the API key + for name, widget in sources_info[talker_id]["widgets"].items(): + if name.startswith("talker_" + talker_id) and name.endswith("api_key"): + key = widget.text().strip() + + if talkers[talker_id].check_api_key(key): + QtWidgets.QMessageBox.information(parent, "API Key Test", "Key is valid!") + else: + QtWidgets.QMessageBox.warning(parent, "API Key Test", "Key is NOT valid!") + + +def call_check_api_url( + talker_id: str, + sources_info: dict[str, QtWidgets.QWidget], + talkers: dict[str, ComicTalker], + parent: QtWidgets.QWidget, +): + url = "" + # Find the correct widget to get the URL key + for name, widget in sources_info[talker_id]["widgets"].items(): + if name.startswith("talker_" + talker_id) and name.endswith("url"): + url = widget.text().strip() + + if talkers[talker_id].check_api_url(url): + QtWidgets.QMessageBox.information(parent, "API Key Test", "URL is valid!") + else: + QtWidgets.QMessageBox.warning(parent, "API Key Test", "URL is NOT valid!") + + +def test_api_key( + btn: QtWidgets.QPushButton, + talker_id: str, + sources_info: dict[str, QtWidgets.QWidget], + talkers: dict[str, ComicTalker], + parent: QtWidgets.QWidget, +) -> None: + btn.clicked.connect(lambda: call_check_api_key(talker_id, sources_info, talkers, parent)) + + +def test_api_url( + btn: QtWidgets.QPushButton, + talker_id: str, + sources_info: dict[str, QtWidgets.QWidget], + talkers: dict[str, ComicTalker], + parent: QtWidgets.QWidget, +) -> None: + btn.clicked.connect(lambda: call_check_api_url(talker_id, sources_info, talkers, parent)) + + +def format_internal_name(int_name: str = "") -> str: + # Presume talker__: talker_comicvine_cv_widget_name + int_name_split = int_name.split("_") + del int_name_split[0:3] + int_name_split[0] = int_name_split[0].capitalize() + new_name = " ".join(int_name_split) + return new_name + + +def generate_checkbox( + option: settngs.Setting, value: bool, layout: QtWidgets.QGridLayout +) -> tuple[QtWidgets.QGridLayout, QtWidgets.QCheckBox]: + # bool equals a checkbox (QCheckBox) + widget = QtWidgets.QCheckBox(format_internal_name(option.internal_name)) + # Set widget status + widget.setChecked(value) + # Add tooltip text + widget.setToolTip(option.help) + # Add widget and span all columns + layout.addWidget(widget, layout.rowCount() + 1, 0, 1, -1) + + return layout, widget + + +def generate_spinbox( + option: settngs.Setting, value: int | float, layout: QtWidgets.QGridLayout +) -> tuple[QtWidgets.QGridLayout, QtWidgets.QSpinBox | QtWidgets.QDoubleSpinBox]: + if isinstance(value, int): + # int equals a spinbox (QSpinBox) + lbl = QtWidgets.QLabel(option.internal_name) + # Create a label + layout.addWidget(lbl, layout.rowCount() + 1, 0) + widget = QtWidgets.QSpinBox() + widget.setRange(0, 9999) + widget.setValue(value) + widget.setToolTip(option.help) + layout.addWidget(widget, layout.rowCount() - 1, 1, alignment=QtCore.Qt.AlignLeft) + + if isinstance(value, float): + # float equals a spinbox (QDoubleSpinBox) + lbl = QtWidgets.QLabel(format_internal_name(option.internal_name)) + # Create a label + layout.addWidget(lbl, layout.rowCount() + 1, 0) + widget = QtWidgets.QDoubleSpinBox() + widget.setRange(0, 9999.99) + widget.setValue(value) + widget.setToolTip(option.help) + layout.addWidget(widget, layout.rowCount() - 1, 1, alignment=QtCore.Qt.AlignLeft) + + return layout, widget + + +def generate_textbox( + option: settngs.Setting, value: str, layout: QtWidgets.QGridLayout +) -> tuple[QtWidgets.QGridLayout, QtWidgets.QLineEdit, QtWidgets.QPushButton]: + btn = None + # str equals a text field (QLineEdit) + lbl = QtWidgets.QLabel(format_internal_name(option.internal_name)) + # Create a label + layout.addWidget(lbl, layout.rowCount() + 1, 0) + widget = QtWidgets.QLineEdit() + widget.setObjectName(option.internal_name) + # Set widget status + widget.setText(value) + widget.setToolTip(option.help) + layout.addWidget(widget, layout.rowCount() - 1, 1) + # Special case for api_key, make a test button + if option.internal_name.endswith("api_key"): + btn = QtWidgets.QPushButton("Test Key") + layout.addWidget(btn, layout.rowCount() - 1, 2) + + if option.internal_name.endswith("url"): + btn = QtWidgets.QPushButton("Test URL") + layout.addWidget(btn, layout.rowCount() - 1, 2) + + return layout, widget, btn + + +def generate_source_option_tabs( + parent: QtWidgets.QWidget, + tabs: QtWidgets.QTabWidget, + config: settngs.Config[settngs.Namespace], + talkers: dict[str, ComicTalker], +) -> dict[str, QtWidgets.QWidget]: + """ + Generate GUI tabs and settings for talkers + """ + + sources: dict = {} + + # Add source sub tabs to Comic Sources tab + for talker_id, talker_obj in talkers.items(): + # Add source to general tab dropdown list + tabs.findChildren(QtWidgets.QComboBox, "cobxInfoSource")[0].addItem(talker_obj.name, talker_id) + + # Use a dict to make a var name from var + source_info = {} + tab_name = talker_id + source_info[tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} + layout_grid = QtWidgets.QGridLayout() + + for option in config[1][f"talker_{talker_id}"][1].values(): + current_widget = None + if option.action is not None and ( + isinstance(option.action, type(argparse.BooleanOptionalAction)) + or option.action == "store_true" + or option.action == "store_false" + ): + layout_grid, current_widget = generate_checkbox( + option, getattr(config[0], option.internal_name), layout_grid + ) + source_info[tab_name]["widgets"][option.internal_name] = current_widget + elif isinstance(option.type, type(int)) or isinstance(option.type, type(float)): + layout_grid, current_widget = generate_spinbox( + option, getattr(config[0], option.internal_name), layout_grid + ) + source_info[tab_name]["widgets"][option.internal_name] = current_widget + # option.type of None should be string + elif option.type is None or isinstance(option.type, type(str)): + layout_grid, current_widget, btn = generate_textbox( + option, getattr(config[0], option.internal_name), layout_grid + ) + source_info[tab_name]["widgets"][option.internal_name] = current_widget + + if option.internal_name.endswith("key"): + # Attach test api function to button. A better way? + test_api_key(btn, talker_id, source_info, talkers, parent) + if option.internal_name.endswith("url"): + # Attach test api function to button. A better way? + test_api_url(btn, talker_id, source_info, talkers, parent) + else: + logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") + + # Add vertical spacer + vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0) + # Display the new widgets + source_info[tab_name]["tab"].setLayout(layout_grid) + + # Add new sub tab to Comic Source tab + tabs.addTab(source_info[tab_name]["tab"], talker_obj.name) + sources.update(source_info) + + return sources diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index 88c078d..9f32c1f 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -118,7 +118,10 @@ class ComicTalker: self.api_url: str = "" def register_settings(self, parser: settngs.Manager) -> None: - """Allows registering settings using the settngs package with an argparse like interface""" + """ + Allows registering settings using the settngs package with an argparse like interface + NOTE: The order used will be reflected within the settings menu + """ return None def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]: @@ -128,10 +131,15 @@ class ComicTalker: """ return settings - def check_api_key(self, key: str, url: str) -> bool: + def check_api_key(self, key: str) -> bool: """ - This function should return true if the given api key and url are valid. - If the Talker does not use an api key it should validate that the url works. + This function should return true if the given api key is valid. + """ + raise NotImplementedError + + def check_api_url(self, url: str) -> bool: + """ + This function should return true if the given url is valid. """ raise NotImplementedError diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index f3d4d29..fa9dc66 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -181,8 +181,6 @@ class ComicVineTalker(ComicTalker): self.wait_on_ratelimit_time: int = 20 def register_settings(self, parser: settngs.Manager) -> None: - parser.add_setting("--cv-api-key", help="Use the given Comic Vine API Key.") - parser.add_setting("--cv-url", help="Use the given Comic Vine URL.") parser.add_setting("--cv-use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction) parser.add_setting("--cv-wait-on-ratelimit", default=False, action=argparse.BooleanOptionalAction) parser.add_setting( @@ -191,6 +189,8 @@ class ComicVineTalker(ComicTalker): 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.") + parser.add_setting("--cv-url", help="Use the given Comic Vine URL.") def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]: if settings["cv_api_key"]: @@ -208,7 +208,23 @@ class ComicVineTalker(ComicTalker): self.remove_html_tables = settings["cv_remove_html_tables"] return settngs - def check_api_key(self, key: str, url: str) -> bool: + def check_api_key(self, key: str) -> bool: + url = self.api_url + try: + test_url = urljoin(url, "issue/1/") + + cv_response: CVResult = requests.get( + test_url, + headers={"user-agent": "comictagger/" + self.version}, + params={"api_key": key, "format": "json", "field_list": "name"}, + ).json() + + # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" + return cv_response["status_code"] != 100 + except Exception: + return False + + def check_api_url(self, url: str) -> bool: if not url: url = self.api_url try: @@ -221,11 +237,11 @@ class ComicVineTalker(ComicTalker): cv_response: CVResult = requests.get( test_url, headers={"user-agent": "comictagger/" + self.version}, - params={"api_key": key, "format": "json", "field_list": "name"}, + params={"api_key": self.api_key, "format": "json", "field_list": "name"}, ).json() - # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" - return cv_response["status_code"] != 100 + # Bogus request, but if the url is correct, you get error 102: "Error in URL Format" + return cv_response["status_code"] == 102 except Exception: return False