From 4b3b9d86917c9ddbc8f2c844269b4e8ad9595e29 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Fri, 10 Feb 2023 21:16:35 +0000 Subject: [PATCH 01/53] Entry points for talkers --- comictalker/__init__.py | 17 ++++++++++++----- setup.py | 3 +++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/comictalker/__init__.py b/comictalker/__init__.py index e5bd095..e216f27 100644 --- a/comictalker/__init__.py +++ b/comictalker/__init__.py @@ -2,8 +2,13 @@ from __future__ import annotations import logging import pathlib +import sys + +if sys.version_info < (3, 10): + from importlib_metadata import entry_points +else: + from importlib.metadata import entry_points -import comictalker.talkers.comicvine from comictalker.comictalker import ComicTalker, TalkerError from comictalker.resulttypes import ComicIssue, ComicSeries @@ -21,11 +26,13 @@ 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]: + for talker in entry_points(group="comictagger.talkers"): try: - obj = talker(version, cache) + talker_cls = talker.load() + obj = talker_cls(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") + logger.exception("Failed to load talker: %s", talker.name) + raise TalkerError(source=talker.name, code=4, desc="Failed to initialise talker") + return talkers diff --git a/setup.py b/setup.py index 291d1bc..d234566 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,9 @@ setup( "rar = comicapi.archivers.rar:RarArchiver", "folder = comicapi.archivers.folder:FolderArchiver", ], + "comictagger.talkers": [ + "comicvine = comictalker.talkers.comicvine:ComicVineTalker", + ], }, classifiers=[ "Development Status :: 4 - Beta", From 83a8d5d5e1dda1c6b30fab355879ca3f5889cd07 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Sat, 11 Feb 2023 01:18:56 +0000 Subject: [PATCH 02/53] Generate settings tabs for each talker --- comictaggerlib/renamewindow.py | 6 +- comictaggerlib/settingswindow.py | 150 +++++++++++--- comictaggerlib/taggerwindow.py | 4 +- comictaggerlib/ui/settingswindow.ui | 299 ++++++++++++---------------- 4 files changed, 252 insertions(+), 207 deletions(-) diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 9bed8df..d266f8f 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -39,7 +39,7 @@ class RenameWindow(QtWidgets.QDialog): comic_archive_list: list[ComicArchive], data_style: int, config: settngs.Config[settngs.Namespace], - talker: ComicTalker, + talkers: dict[str, ComicTalker], ) -> None: super().__init__(parent) @@ -55,7 +55,7 @@ class RenameWindow(QtWidgets.QDialog): ) self.config = config - self.talker = talker + self.talkers = talkers self.comic_archive_list = comic_archive_list self.data_style = data_style self.rename_list: list[str] = [] @@ -160,7 +160,7 @@ class RenameWindow(QtWidgets.QDialog): self.twList.setSortingEnabled(True) def modify_settings(self) -> None: - settingswin = SettingsWindow(self, self.config, self.talker) + settingswin = SettingsWindow(self, self.config, self.talkers) settingswin.setModal(True) settingswin.show_rename_tab() settingswin.exec() diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 68a046e..53c599f 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -15,6 +15,7 @@ # limitations under the License. from __future__ import annotations +import argparse import html import logging import os @@ -131,7 +132,7 @@ Spider-Geddon #1 - New Players; Check In class SettingsWindow(QtWidgets.QDialog): def __init__( - self, parent: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talker: ComicTalker + self, parent: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talkers: dict[str, ComicTalker] ) -> None: super().__init__(parent) @@ -142,7 +143,7 @@ class SettingsWindow(QtWidgets.QDialog): ) self.config = config - self.talker = talker + self.talkers = talkers self.name = "Settings" if platform.system() == "Windows": @@ -194,7 +195,6 @@ class SettingsWindow(QtWidgets.QDialog): self.btnBrowseRar.clicked.connect(self.select_rar) self.btnClearCache.clicked.connect(self.clear_cache) self.btnResetSettings.clicked.connect(self.reset_settings) - self.btnTestKey.clicked.connect(self.test_api_key) self.btnTemplateHelp.clicked.connect(self.show_template_help) self.cbxMoveFiles.clicked.connect(self.dir_test) self.leDirectory.textEdited.connect(self.dir_test) @@ -223,7 +223,6 @@ class SettingsWindow(QtWidgets.QDialog): self.btnRemoveValueReplacement.clicked.disconnect() self.btnResetSettings.clicked.disconnect() self.btnTemplateHelp.clicked.disconnect() - self.btnTestKey.clicked.disconnect() self.cbxChangeExtension.clicked.disconnect() self.cbxComplicatedParser.clicked.disconnect() self.cbxMoveFiles.clicked.disconnect() @@ -235,6 +234,100 @@ 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)) @@ -312,17 +405,11 @@ class SettingsWindow(QtWidgets.QDialog): self.cbxRemovePublisher.setChecked(self.config[0].filename_remove_publisher) self.switch_parser() - self.cbxUseSeriesStartAsVolume.setChecked(self.config[0].talker_comicvine_cv_use_series_start_as_volume) 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].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) - self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].cbl_assume_lone_credit_is_primary) self.cbxCopyCharactersToTags.setChecked(self.config[0].cbl_copy_characters_to_tags) self.cbxCopyTeamsToTags.setChecked(self.config[0].cbl_copy_teams_to_tags) @@ -428,21 +515,12 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked() 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].talker_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() - self.config[0].talker_comicvine_cv_remove_html_tables = self.cbxRemoveHtmlTables.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_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_url = self.config[0].talker_comicvine_cv_url + self.config[0].talker_source = str(self.cobxInfoSource.itemData(self.cobxInfoSource.currentIndex())) self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() @@ -464,6 +542,19 @@ 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 + for tab in self.sources.items(): + for name, widget in tab[1]["widgets"].items(): + widget_value = None + if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)): + widget_value = widget.value() + elif isinstance(widget, QtWidgets.QLineEdit): + widget_value = widget.text().strip() + elif isinstance(widget, QtWidgets.QCheckBox): + widget_value = widget.isChecked() + + setattr(self.config[0], name, widget_value) + self.update_talkers_config() settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json") @@ -472,8 +563,8 @@ class SettingsWindow(QtWidgets.QDialog): 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}"]) + for talker, talker_obj in self.talkers.items(): + talker_obj.parse_settings(cfg[0][f"talker_{talker}"]) def select_rar(self) -> None: self.select_file(self.leRarExePath, "RAR") @@ -483,11 +574,20 @@ 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) -> None: - if self.talker.check_api_key(self.leKey.text().strip(), self.leURL.text().strip()): + 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.") + 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])) diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index d51da19..db4deb6 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1354,7 +1354,7 @@ class TaggerWindow(QtWidgets.QMainWindow): QtWidgets.QMessageBox.warning(self, self.tr("Web Link"), self.tr("Web Link is invalid.")) def show_settings(self) -> None: - settingswin = SettingsWindow(self, self.config, self.current_talker()) + settingswin = SettingsWindow(self, self.config, self.talkers) settingswin.setModal(True) settingswin.exec() settingswin.result() @@ -2047,7 +2047,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if self.dirty_flag_verification( "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.current_talker()) + dlg = RenameWindow(self, ca_list, self.load_data_style, self.config, self.talkers) dlg.setModal(True) if dlg.exec() and self.comic_archive is not None: self.fileSelectionList.update_selected_rows() diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index 813c7a8..7607a7e 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -7,7 +7,7 @@ 0 0 702 - 513 + 559 @@ -28,7 +28,7 @@ - 0 + 3 @@ -308,75 +308,11 @@ - + - Comic Vine + Comic Sources - - - - - 0 - 0 - - - - - - - - - Use Series Start Date as Volume - - - - - - - Clear Form Before Importing Comic Vine data - - - - - - - Remove HTML tables from CV summary field - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - - - Initially sort Series search results by Starting Year instead of No. Issues - - - - - - - Initially show Series Name exact matches first - - - - - - - - @@ -391,100 +327,127 @@ - - - - 0 - 0 - + + + 0 - - - - - - 0 - 0 - - - - <html><head/><body><p>A personal API key from <a href="http://www.comicvine.com/api/"><span style=" text-decoration: underline; color:#0000ff;">Comic Vine</span></a> is recommended in order to search for tag data. Login (or create a new account) there to get your key, and enter it below.</p></body></html> - - - Qt::RichText - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse - - - - - - - Test Key - - - - - - - - 0 - 0 - - - - false - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 200 - 16777215 - - - - Comic Vine API Key - - - - - - - Comic Vine URL - - - - - - - + + + General + + + + + 10 + 70 + 650 + 22 + + + + Use Series Start Date as Volume + + + + + + 10 + 90 + 666 + 22 + + + + Clear Form Before Importing Comic Vine data + + + + + + 10 + 130 + 666 + 22 + + + + Initially sort Series search results by Starting Year instead of No. Issues + + + + + + 10 + 150 + 666 + 22 + + + + Initially show Series Name exact matches first + + + + + + 5 + 120 + 648 + 3 + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + 190 + 13 + 301 + 32 + + + + + + + 15 + 13 + 171 + 32 + + + + Select Information Source: + + + + + + 5 + 60 + 648 + 3 + + + + Qt::Horizontal + + + + + + @@ -774,25 +737,16 @@ Find - - AlignCenter - Replacement - - AlignCenter - Strict Only - - AlignCenter - @@ -819,25 +773,16 @@ Find - - AlignCenter - Replacement - - AlignCenter - Strict Only - - AlignCenter - From 6a6a3320cba41a9f418021acba27b46311a3b6c3 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 14 Feb 2023 01:32:56 +0000 Subject: [PATCH 03/53] Move talker settings menu generator to a separate file --- comictaggerlib/settingswindow.py | 122 ++------------ comictaggerlib/ui/talkeruigenerator.py | 213 +++++++++++++++++++++++++ comictalker/comictalker.py | 16 +- comictalker/talkers/comicvine.py | 28 +++- 4 files changed, 258 insertions(+), 121 deletions(-) create mode 100644 comictaggerlib/ui/talkeruigenerator.py 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 From 2fde11a704ca4c2c91770771f42a77188ecc1543 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 14 Feb 2023 01:47:32 +0000 Subject: [PATCH 04/53] Test for menu generator format_internal_name --- tests/talker_test.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/talker_test.py diff --git a/tests/talker_test.py b/tests/talker_test.py new file mode 100644 index 0000000..c7ced2c --- /dev/null +++ b/tests/talker_test.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +import comictaggerlib.ui.talkeruigenerator + + +def test_format_internal_name(): + assert comictaggerlib.ui.talkeruigenerator.format_internal_name("talker_comicvine_cv_test_name") == "Test name" From b727b1288d9dd62bc373ace4dae8e1bd6a641ef8 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Wed, 15 Feb 2023 17:05:14 +0000 Subject: [PATCH 05/53] Apply credit datatype to person data from cache --- comictalker/comiccacher.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/comictalker/comiccacher.py b/comictalker/comiccacher.py index bd3ea8c..47855de 100644 --- a/comictalker/comiccacher.py +++ b/comictalker/comiccacher.py @@ -378,7 +378,12 @@ class ComicCacher: ) # now process the results - + credits = [] + try: + for credit in json.loads(row[13]): + credits.append(Credit(**credit)) + except Exception: + logger.exception("credits failed") record = ComicIssue( id=row[1], name=row[2], @@ -392,7 +397,7 @@ class ComicCacher: alt_image_urls=row[10].strip().splitlines(), characters=row[11].strip().splitlines(), locations=row[12].strip().splitlines(), - credits=json.loads(row[13]), + credits=credits, teams=row[14].strip().splitlines(), story_arcs=row[15].strip().splitlines(), complete=bool(row[16]), From a41c5a8af5de90e84cbd2e434b019fb0db27a114 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 16 Feb 2023 16:33:35 -0800 Subject: [PATCH 06/53] Automate contributions --- .github/workflows/contributions.yaml | 41 ++++++++++++++++++++++++++++ .mailmap | 9 ++++++ AUTHORS | 0 README.md | 6 ++++ 4 files changed, 56 insertions(+) create mode 100644 .github/workflows/contributions.yaml create mode 100644 .mailmap create mode 100644 AUTHORS diff --git a/.github/workflows/contributions.yaml b/.github/workflows/contributions.yaml new file mode 100644 index 0000000..ca4b5e4 --- /dev/null +++ b/.github/workflows/contributions.yaml @@ -0,0 +1,41 @@ +name: Contributions +on: + push: + branches: + - '**' + tags-ignore: + - '**' + +jobs: + contrib-readme-job: + permissions: + contents: write + runs-on: ubuntu-latest + env: + CI_COMMIT_AUTHOR: github-actions[bot] + CI_COMMIT_EMAIL: <41898282+github-actions[bot]@users.noreply.github.com> + CI_COMMIT_MESSAGE: Update AUTHORS + name: A job to automate contrib in readme + steps: + - name: Contribute List + uses: akhilmhdh/contributors-readme-action@v2.3.6 + with: + use_username: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Update AUTHORS + run: | + git config --global log.mailmap true + git log --reverse '--format=%aN <%aE>' | cat -n | sort -uk2 | sort -n | cut -f2- >AUTHORS + + - name: Commit and push AUTHORS + run: | + git pull + git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ env.CI_COMMIT_EMAIL }}" + git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}" + git push diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..d5aa596 --- /dev/null +++ b/.mailmap @@ -0,0 +1,9 @@ +Andrew W. Buchanan +Davide Romanini +Davide Romanini +Michael Fitzurka +Timmy Welch +beville <(no author)@6c5673fe-1810-88d6-992b-cd32ca31540c> +beville +beville +beville diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index a2b4454..42cba24 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,9 @@ choco install comictagger 2. Clone this repository `git clone https://github.com/comictagger/comictagger.git` 3. `pip3 install -r requirements_dev.txt` 7. `pip3 install .` or `pip3 install .[GUI]` + + +## Contributors + + + From 0c1093d58e87b1c7cc29312936aea0467f1887ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 01:14:27 +0000 Subject: [PATCH 07/53] docs(contributor): contrib-readme-action has updated readme --- README.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/README.md b/README.md index 42cba24..c276be0 100644 --- a/README.md +++ b/README.md @@ -66,4 +66,107 @@ choco install comictagger ## Contributors + + + + + + + + + + + + + + + + + + +
+ + beville +
+ beville +
+
+ + davide-romanini +
+ davide-romanini +
+
+ + fcanc +
+ fcanc +
+
+ + lordwelch +
+ lordwelch +
+
+ + mizaki +
+ mizaki +
+
+ + MichaelFitzurka +
+ MichaelFitzurka +
+
+ + abuchanan920 +
+ abuchanan920 +
+
+ + AlbanSeurat +
+ AlbanSeurat +
+
+ + rhaussmann +
+ rhaussmann +
+
+ + jpcranford +
+ jpcranford +
+
+ + PawlakMarek +
+ PawlakMarek +
+
+ + Xav83 +
+ Xav83 +
+
+ + thFrgttn +
+ thFrgttn +
+
+ + tlc +
+ tlc +
+
From 9c7bf2e2354c818dc9eb9442b8fa7fdd0b4bb93d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 01:14:29 +0000 Subject: [PATCH 08/53] Update AUTHORS --- AUTHORS | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AUTHORS b/AUTHORS index e69de29..5f986fc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -0,0 +1,14 @@ +beville +Davide Romanini +fcanc +Alban Seurat +tlc +Marek Pawlak +Timmy Welch +J.P. Cranford +thFrgttn <39759781+thFrgttn@users.noreply.github.com> +Andrew W. Buchanan +Michael Fitzurka +Richard Haussmann +Mizaki +Xavier Jouvenot From 2491999a33b24cf038a360ac0ea327555e4f2bb0 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 16 Feb 2023 17:23:13 -0800 Subject: [PATCH 09/53] Update copyright statements to ComicTagger Authors --- AUTHORS | 4 ++-- comicapi/comet.py | 2 +- comicapi/comicarchive.py | 2 +- comicapi/comicbookinfo.py | 2 +- comicapi/comicinfoxml.py | 2 +- comicapi/filenameparser.py | 2 +- comicapi/genericmetadata.py | 2 +- comicapi/issuestring.py | 2 +- comicapi/utils.py | 2 +- comictaggerlib/autotagmatchwindow.py | 2 +- comictaggerlib/autotagprogresswindow.py | 2 +- comictaggerlib/autotagstartwindow.py | 2 +- comictaggerlib/cbltransformer.py | 2 +- comictaggerlib/cli.py | 2 +- comictaggerlib/coverimagewidget.py | 2 +- comictaggerlib/crediteditorwindow.py | 2 +- comictaggerlib/ctsettings/commandline.py | 2 +- comictaggerlib/exportwindow.py | 2 +- comictaggerlib/filerenamer.py | 2 +- comictaggerlib/fileselectionlist.py | 2 +- comictaggerlib/imagefetcher.py | 2 +- comictaggerlib/imagehasher.py | 2 +- comictaggerlib/imagepopup.py | 2 +- comictaggerlib/issueidentifier.py | 2 +- comictaggerlib/issueselectionwindow.py | 2 +- comictaggerlib/logwindow.py | 2 +- comictaggerlib/main.py | 2 +- comictaggerlib/matchselectionwindow.py | 2 +- comictaggerlib/optionalmsgdialog.py | 2 +- comictaggerlib/pagebrowser.py | 2 +- comictaggerlib/pagelisteditor.py | 2 +- comictaggerlib/pageloader.py | 2 +- comictaggerlib/progresswindow.py | 2 +- comictaggerlib/renamewindow.py | 2 +- comictaggerlib/seriesselectionwindow.py | 2 +- comictaggerlib/settingswindow.py | 2 +- comictaggerlib/taggerwindow.py | 2 +- comictaggerlib/versionchecker.py | 2 +- comictalker/comiccacher.py | 2 +- comictalker/comictalker.py | 2 +- comictalker/talker_utils.py | 2 +- comictalker/talkers/comicvine.py | 2 +- scripts/inventory.py | 2 +- scripts/make_links.py | 2 +- scripts/move2folder.py | 4 +--- scripts/name_fixer.py | 2 +- scripts/remove_ads.py | 2 +- scripts/shrink.py | 2 +- scripts/validate_cover.py | 2 +- 49 files changed, 50 insertions(+), 52 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5f986fc..7117733 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,6 @@ -beville +Anthony Beville Davide Romanini -fcanc +Fabio Cancedda Alban Seurat tlc Marek Pawlak diff --git a/comicapi/comet.py b/comicapi/comet.py index a3de3b2..ee7deb0 100644 --- a/comicapi/comet.py +++ b/comicapi/comet.py @@ -1,6 +1,6 @@ """A class to encapsulate CoMet data""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index e84abd1..57bc0a7 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -1,5 +1,5 @@ """A class to represent a single comic, be it file or folder of images""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/comicbookinfo.py b/comicapi/comicbookinfo.py index 1ceff44..ef282ac 100644 --- a/comicapi/comicbookinfo.py +++ b/comicapi/comicbookinfo.py @@ -1,5 +1,5 @@ """A class to encapsulate the ComicBookInfo data""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/comicinfoxml.py b/comicapi/comicinfoxml.py index 8d97ded..aa50ee0 100644 --- a/comicapi/comicinfoxml.py +++ b/comicapi/comicinfoxml.py @@ -1,5 +1,5 @@ """A class to encapsulate ComicRack's ComicInfo.xml data""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/filenameparser.py b/comicapi/filenameparser.py index 0b9f599..60d3285 100644 --- a/comicapi/filenameparser.py +++ b/comicapi/filenameparser.py @@ -2,7 +2,7 @@ This should probably be re-written, but, well, it mostly works! """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 377f334..243eee9 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -5,7 +5,7 @@ tagging schemes and databases, such as ComicVine or GCD. This makes conversion possible, however lossy it might be """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/issuestring.py b/comicapi/issuestring.py index 9150246..149e5b1 100644 --- a/comicapi/issuestring.py +++ b/comicapi/issuestring.py @@ -4,7 +4,7 @@ Class for handling the odd permutations of an 'issue number' that the comics industry throws at us. e.g.: "12", "12.1", "0", "-1", "5AU", "100-2" """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comicapi/utils.py b/comicapi/utils.py index 7c8a5c9..ad54ed7 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -1,5 +1,5 @@ """Some generic utilities""" -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index 62301bc..2abd2c5 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select from automated issue matches""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/autotagprogresswindow.py b/comictaggerlib/autotagprogresswindow.py index f2486bb..5650f55 100644 --- a/comictaggerlib/autotagprogresswindow.py +++ b/comictaggerlib/autotagprogresswindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to show ID log and progress""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 7e7db5d..9385ad7 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to confirm and set config for auto-tag""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py index 4be2171..a1aa714 100644 --- a/comictaggerlib/cbltransformer.py +++ b/comictaggerlib/cbltransformer.py @@ -1,6 +1,6 @@ """A class to manage modifying metadata specifically for CBL/CBI""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index e91d0fe..7614dfc 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -1,7 +1,7 @@ #!/usr/bin/python """ComicTagger CLI functions""" # -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py index 07ab9da..aef5459 100644 --- a/comictaggerlib/coverimagewidget.py +++ b/comictaggerlib/coverimagewidget.py @@ -4,7 +4,7 @@ Display cover images from either a local archive, or from Comic Vine. TODO: This should be re-factored using subclasses! """ # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/crediteditorwindow.py b/comictaggerlib/crediteditorwindow.py index 678e73e..f94c294 100644 --- a/comictaggerlib/crediteditorwindow.py +++ b/comictaggerlib/crediteditorwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to edit credits""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index f6109d8..10f3440 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -1,6 +1,6 @@ """CLI settings for ComicTagger""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/exportwindow.py b/comictaggerlib/exportwindow.py index a3f8f73..595e443 100644 --- a/comictaggerlib/exportwindow.py +++ b/comictaggerlib/exportwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to confirm and set options for export to zip""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py index 103fb6d..3abad0e 100644 --- a/comictaggerlib/filerenamer.py +++ b/comictaggerlib/filerenamer.py @@ -1,6 +1,6 @@ """Functions for renaming files based on metadata""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/fileselectionlist.py b/comictaggerlib/fileselectionlist.py index c84b049..a8a6970 100644 --- a/comictaggerlib/fileselectionlist.py +++ b/comictaggerlib/fileselectionlist.py @@ -1,6 +1,6 @@ """A PyQt5 widget for managing list of comic archive files""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/imagefetcher.py b/comictaggerlib/imagefetcher.py index 01163a1..779340f 100644 --- a/comictaggerlib/imagefetcher.py +++ b/comictaggerlib/imagefetcher.py @@ -1,6 +1,6 @@ """A class to manage fetching and caching of images by URL""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/imagehasher.py b/comictaggerlib/imagehasher.py index f8662ff..f30270c 100644 --- a/comictaggerlib/imagehasher.py +++ b/comictaggerlib/imagehasher.py @@ -1,6 +1,6 @@ """A class to manage creating image content hashes, and calculate hamming distances""" # -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/imagepopup.py b/comictaggerlib/imagepopup.py index 1702535..445e4a2 100644 --- a/comictaggerlib/imagepopup.py +++ b/comictaggerlib/imagepopup.py @@ -1,6 +1,6 @@ """A PyQT4 widget to display a popup image""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index 43451e4..7d0e3b0 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -1,6 +1,6 @@ """A class to automatically identify a comic archive""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index c5b47ab..b6532a7 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select specific issue from list""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/logwindow.py b/comictaggerlib/logwindow.py index ee66466..8b3bda8 100644 --- a/comictaggerlib/logwindow.py +++ b/comictaggerlib/logwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to a text file or log""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 3e35a1d..047a802 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -1,6 +1,6 @@ """A python app to (automatically) tag comic archives""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py index 65432b8..6383eeb 100644 --- a/comictaggerlib/matchselectionwindow.py +++ b/comictaggerlib/matchselectionwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select from automated issue matches""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/optionalmsgdialog.py b/comictaggerlib/optionalmsgdialog.py index c71d704..df2e267 100644 --- a/comictaggerlib/optionalmsgdialog.py +++ b/comictaggerlib/optionalmsgdialog.py @@ -11,7 +11,7 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question", ) """ # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/pagebrowser.py b/comictaggerlib/pagebrowser.py index 114bee7..567c72e 100644 --- a/comictaggerlib/pagebrowser.py +++ b/comictaggerlib/pagebrowser.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to show pages of a comic archive""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/pagelisteditor.py b/comictaggerlib/pagelisteditor.py index 6ba1461..2f322cf 100644 --- a/comictaggerlib/pagelisteditor.py +++ b/comictaggerlib/pagelisteditor.py @@ -1,6 +1,6 @@ """A PyQt5 widget for editing the page list info""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/pageloader.py b/comictaggerlib/pageloader.py index 8049265..cd38213 100644 --- a/comictaggerlib/pageloader.py +++ b/comictaggerlib/pageloader.py @@ -1,6 +1,6 @@ """A PyQT4 class to load a page image from a ComicArchive in a background thread""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/progresswindow.py b/comictaggerlib/progresswindow.py index 1a2342f..3544157 100644 --- a/comictaggerlib/progresswindow.py +++ b/comictaggerlib/progresswindow.py @@ -1,6 +1,6 @@ """A PyQt5 dialog to show ID log and progress""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 9bed8df..13f15f1 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to confirm rename""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 1707f0f..ad7e15c 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to select specific series/volume from list""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 68a046e..a7270ad 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -1,6 +1,6 @@ """A PyQT4 dialog to enter app settings""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index d51da19..94407fc 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1,6 +1,6 @@ """The main window of the ComicTagger app""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictaggerlib/versionchecker.py b/comictaggerlib/versionchecker.py index f0da9e2..d696dbb 100644 --- a/comictaggerlib/versionchecker.py +++ b/comictaggerlib/versionchecker.py @@ -1,6 +1,6 @@ """Version checker""" # -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictalker/comiccacher.py b/comictalker/comiccacher.py index bd3ea8c..50af836 100644 --- a/comictalker/comiccacher.py +++ b/comictalker/comiccacher.py @@ -1,6 +1,6 @@ """A python class to manage caching of data from Comic Vine""" # -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License; # you may not use this file except in compliance with the License. diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index 88c078d..6b5dd7f 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -1,4 +1,4 @@ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictalker/talker_utils.py b/comictalker/talker_utils.py index bd605ca..bd15389 100644 --- a/comictalker/talker_utils.py +++ b/comictalker/talker_utils.py @@ -1,4 +1,4 @@ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index f3d4d29..1df6e80 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -1,7 +1,7 @@ """ ComicVine information source """ -# Copyright 2012-2014 Anthony Beville +# Copyright 2012-2014 ComicTagger Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/inventory.py b/scripts/inventory.py index efa33ec..f68fd65 100755 --- a/scripts/inventory.py +++ b/scripts/inventory.py @@ -1,7 +1,7 @@ #!/usr/bin/python """Print out a line-by-line list of basic tag info from all comics""" -# Copyright 2012 Anthony Beville +# Copyright 2012 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/make_links.py b/scripts/make_links.py index dd93120..24fd8dd 100755 --- a/scripts/make_links.py +++ b/scripts/make_links.py @@ -4,7 +4,7 @@ Make some tree structures and symbolic links to comic files based on metadata organizing by date and series, in different trees """ -# Copyright 2012 Anthony Beville +# Copyright 2012 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/move2folder.py b/scripts/move2folder.py index 154afcb..de81cd5 100755 --- a/scripts/move2folder.py +++ b/scripts/move2folder.py @@ -1,9 +1,7 @@ #!/usr/bin/python """Moves comic files based on metadata organizing in a tree by Publisher/Series (Volume)""" -# This script is based on make_links.py by Anthony Beville - -# Copyright 2015 Fabio Cancedda, Anthony Beville +# Copyright 2015 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/name_fixer.py b/scripts/name_fixer.py index fdbfa1d..5117069 100755 --- a/scripts/name_fixer.py +++ b/scripts/name_fixer.py @@ -1,7 +1,7 @@ #!/usr/bin/python """Fix the comic file names using a list of transforms""" -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/remove_ads.py b/scripts/remove_ads.py index 31f4579..934828e 100755 --- a/scripts/remove_ads.py +++ b/scripts/remove_ads.py @@ -5,7 +5,7 @@ and deleted. Walks recursively through the given folders. Originals are kept in a sub-folder at the level of the original """ -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/shrink.py b/scripts/shrink.py index eb74ef3..e618993 100644 --- a/scripts/shrink.py +++ b/scripts/shrink.py @@ -1,7 +1,7 @@ #!/usr/bin/python """Reduce the image size of pages in the comic archive""" -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/validate_cover.py b/scripts/validate_cover.py index 4bb9077..72e185f 100755 --- a/scripts/validate_cover.py +++ b/scripts/validate_cover.py @@ -2,7 +2,7 @@ """Test archive cover against Comic Vine for a given issue ID """ -# Copyright 2013 Anthony Beville +# Copyright 2013 ComicTagger Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 5ecaf89d154e1b0d550554f6f469e60b26487fc0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 01:23:54 +0000 Subject: [PATCH 10/53] Update AUTHORS --- AUTHORS | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7117733..318650f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,6 @@ -Anthony Beville +beville Davide Romanini -Fabio Cancedda +fcanc Alban Seurat tlc Marek Pawlak @@ -12,3 +12,4 @@ Michael Fitzurka Richard Haussmann Mizaki Xavier Jouvenot +github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> From 0714b94ca18094cbed2dcd11241e625611d795dd Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Fri, 17 Feb 2023 10:16:21 -0800 Subject: [PATCH 11/53] Restrict contributions updates to only run on pushes to develop --- .github/workflows/contributions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/contributions.yaml b/.github/workflows/contributions.yaml index ca4b5e4..526c500 100644 --- a/.github/workflows/contributions.yaml +++ b/.github/workflows/contributions.yaml @@ -2,7 +2,7 @@ name: Contributions on: push: branches: - - '**' + - 'develop' tags-ignore: - '**' From 628dd5e456f21f192c00d855dad247f5749947ed Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Fri, 17 Feb 2023 13:43:41 -0800 Subject: [PATCH 12/53] Fix actions failure when there are no new contributors --- .github/workflows/contributions.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/contributions.yaml b/.github/workflows/contributions.yaml index 526c500..df7870b 100644 --- a/.github/workflows/contributions.yaml +++ b/.github/workflows/contributions.yaml @@ -34,8 +34,10 @@ jobs: - name: Commit and push AUTHORS run: | - git pull - git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" - git config --global user.email "${{ env.CI_COMMIT_EMAIL }}" - git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}" - git push + if ! git diff --exit-code; then + git pull + git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ env.CI_COMMIT_EMAIL }}" + git commit -a -m "${{ env.CI_COMMIT_MESSAGE }}" + git push + fi From 02fd8beda8a70fe99970da7a73eba599840d180d Mon Sep 17 00:00:00 2001 From: Mizaki Date: Sat, 18 Feb 2023 01:15:46 +0000 Subject: [PATCH 13/53] Use None as parent for api and url message boxes Rename test_api_key and test_api_url to api_key_btn_connect and api_url_btn_connect Make separate function to set form values, called in settings_to_form Change isinstance to is Call findChildren only once --- comictaggerlib/settingswindow.py | 16 +++--- comictaggerlib/ui/talkeruigenerator.py | 69 ++++++++++++++------------ 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index f44ddbf..40b288a 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -186,16 +186,13 @@ class SettingsWindow(QtWidgets.QDialog): self.leRenameTemplate.setToolTip(f"
{html.escape(template_tooltip)}
") self.rename_error: Exception | None = None + self.sources: dict = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs( + self.tTalkerTabs, self.config, self.talkers + ) self.connect_signals() 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) @@ -351,6 +348,13 @@ class SettingsWindow(QtWidgets.QDialog): table.removeRow(i) for row, replacement in enumerate(replacments): self.insertRow(table, row, replacement) + + # Set talker values + comictaggerlib.ui.talkeruigenerator.settings_to_talker_form(self.sources, self.config) + + # Select active source in dropdown + self.cobxInfoSource.setCurrentIndex(self.cobxInfoSource.findData(self.config[0].talker_source)) + self.connect_signals() def get_replacements(self) -> Replacements: diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 306df1e..edc9e4b 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -15,7 +15,6 @@ 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 @@ -24,16 +23,15 @@ def call_check_api_key( key = widget.text().strip() if talkers[talker_id].check_api_key(key): - QtWidgets.QMessageBox.information(parent, "API Key Test", "Key is valid!") + QtWidgets.QMessageBox.information(None, "API Key Test", "Key is valid!") else: - QtWidgets.QMessageBox.warning(parent, "API Key Test", "Key is NOT valid!") + QtWidgets.QMessageBox.warning(None, "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 @@ -42,29 +40,27 @@ def call_check_api_url( url = widget.text().strip() if talkers[talker_id].check_api_url(url): - QtWidgets.QMessageBox.information(parent, "API Key Test", "URL is valid!") + QtWidgets.QMessageBox.information(None, "API Key Test", "URL is valid!") else: - QtWidgets.QMessageBox.warning(parent, "API Key Test", "URL is NOT valid!") + QtWidgets.QMessageBox.warning(None, "API Key Test", "URL is NOT valid!") -def test_api_key( +def api_key_btn_connect( 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)) + btn.clicked.connect(lambda: call_check_api_key(talker_id, sources_info, talkers)) -def test_api_url( +def api_url_btn_connect( 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)) + btn.clicked.connect(lambda: call_check_api_url(talker_id, sources_info, talkers)) def format_internal_name(int_name: str = "") -> str: @@ -77,12 +73,10 @@ def format_internal_name(int_name: str = "") -> str: def generate_checkbox( - option: settngs.Setting, value: bool, layout: QtWidgets.QGridLayout + option: settngs.Setting, 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 @@ -101,7 +95,6 @@ def generate_spinbox( 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) @@ -112,7 +105,6 @@ def generate_spinbox( 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) @@ -120,7 +112,7 @@ def generate_spinbox( def generate_textbox( - option: settngs.Setting, value: str, layout: QtWidgets.QGridLayout + option: settngs.Setting, layout: QtWidgets.QGridLayout ) -> tuple[QtWidgets.QGridLayout, QtWidgets.QLineEdit, QtWidgets.QPushButton]: btn = None # str equals a text field (QLineEdit) @@ -129,8 +121,6 @@ def generate_textbox( 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 @@ -145,8 +135,23 @@ def generate_textbox( return layout, widget, btn +def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]): + for talker in sources.items(): + for name, widget in talker[1]["widgets"].items(): + value = getattr(config[0], name) + value_type = type(value) + try: + if value_type is str: + widget.setText(value) + if value_type is int or value_type is float: + widget.setValue(value) + if value_type is bool: + widget.setChecked(value) + except Exception: + logger.debug("Failed to set value of %s", name) + + def generate_source_option_tabs( - parent: QtWidgets.QWidget, tabs: QtWidgets.QTabWidget, config: settngs.Config[settngs.Namespace], talkers: dict[str, ComicTalker], @@ -156,11 +161,12 @@ def generate_source_option_tabs( """ sources: dict = {} + cobxInfoSource = tabs.findChildren(QtWidgets.QComboBox, "cobxInfoSource")[0] # 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) + cobxInfoSource.addItem(talker_obj.name, talker_id) # Use a dict to make a var name from var source_info = {} @@ -171,32 +177,29 @@ def generate_source_option_tabs( 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)) + option.action is argparse.BooleanOptionalAction + or option.action is bool 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 - ) + layout_grid, current_widget = generate_checkbox(option, layout_grid) source_info[tab_name]["widgets"][option.internal_name] = current_widget - elif isinstance(option.type, type(int)) or isinstance(option.type, type(float)): + elif option.type is int or option.type is 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 - ) + elif option.type is None or option.type is str: + layout_grid, current_widget, btn = generate_textbox(option, 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) + api_key_btn_connect(btn, talker_id, source_info, talkers) 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) + api_url_btn_connect(btn, talker_id, source_info, talkers) else: logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") From a24bd1c7196d674644780a6bf623849c7bc4e71e Mon Sep 17 00:00:00 2001 From: Mizaki Date: Sat, 18 Feb 2023 17:16:56 +0000 Subject: [PATCH 14/53] Generate talker general tab programatically. Move search options to search tab. --- comictaggerlib/ctsettings/file.py | 74 +++++----- comictaggerlib/settingswindow.py | 28 ++-- comictaggerlib/ui/settingswindow.ui | 197 ++++++------------------- comictaggerlib/ui/talkeruigenerator.py | 27 +++- 4 files changed, 114 insertions(+), 212 deletions(-) diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 3d780f5..7fd8140 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -46,6 +46,43 @@ def identifier(parser: settngs.Manager) -> None: action=AppendAction, help="When enabled filters the listed publishers from all search results", ) + parser.add_setting("--series-match-search-thresh", default=90, type=int) + 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( + "-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", + default=False, + action=argparse.BooleanOptionalAction, + help="Clears all existing metadata when applying metadata from comic source", + ) def dialog(parser: settngs.Manager) -> None: @@ -86,43 +123,6 @@ def filename(parser: settngs.Manager) -> None: 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", default=90, type=int) - 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( - "-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", - default=False, - action=argparse.BooleanOptionalAction, - help="Clears all existing metadata when applying metadata from comic source", - ) def cbl(parser: settngs.Manager) -> None: diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 40b288a..a5558cf 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -306,7 +306,7 @@ class SettingsWindow(QtWidgets.QDialog): else: self.leRarExePath.setEnabled(False) self.sbNameMatchIdentifyThresh.setValue(self.config[0].identifier_series_match_identify_thresh) - self.sbNameMatchSearchThresh.setValue(self.config[0].talker_series_match_search_thresh) + self.sbNameMatchSearchThresh.setValue(self.config[0].identifier_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) @@ -317,10 +317,10 @@ class SettingsWindow(QtWidgets.QDialog): self.cbxRemovePublisher.setChecked(self.config[0].filename_remove_publisher) self.switch_parser() - self.cbxClearFormBeforePopulating.setChecked(self.config[0].talker_clear_form_before_populating) - 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.cbxClearFormBeforePopulating.setChecked(self.config[0].identifier_clear_form_before_populating) + self.cbxUseFilter.setChecked(self.config[0].identifier_always_use_publisher_filter) + self.cbxSortByYear.setChecked(self.config[0].identifier_sort_series_by_year) + self.cbxExactMatches.setChecked(self.config[0].identifier_exact_series_matches_first) self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].cbl_assume_lone_credit_is_primary) self.cbxCopyCharactersToTags.setChecked(self.config[0].cbl_copy_characters_to_tags) @@ -352,9 +352,6 @@ class SettingsWindow(QtWidgets.QDialog): # Set talker values comictaggerlib.ui.talkeruigenerator.settings_to_talker_form(self.sources, self.config) - # Select active source in dropdown - self.cobxInfoSource.setCurrentIndex(self.cobxInfoSource.findData(self.config[0].talker_source)) - self.connect_signals() def get_replacements(self) -> Replacements: @@ -424,7 +421,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].talker_series_match_search_thresh = self.sbNameMatchSearchThresh.value() + self.config[0].identifier_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() ] @@ -434,12 +431,10 @@ class SettingsWindow(QtWidgets.QDialog): self.config[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked() self.config[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked() - self.config[0].talker_clear_form_before_populating = self.cbxClearFormBeforePopulating.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() - - self.config[0].talker_source = str(self.cobxInfoSource.itemData(self.cobxInfoSource.currentIndex())) + self.config[0].identifier_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() + self.config[0].identifier_always_use_publisher_filter = self.cbxUseFilter.isChecked() + self.config[0].identifier_sort_series_by_year = self.cbxSortByYear.isChecked() + self.config[0].identifier_exact_series_matches_first = self.cbxExactMatches.isChecked() self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() @@ -471,6 +466,9 @@ class SettingsWindow(QtWidgets.QDialog): widget_value = widget.text().strip() elif isinstance(widget, QtWidgets.QCheckBox): widget_value = widget.isChecked() + # The talker source dropdown + elif isinstance(widget, QtWidgets.QComboBox): + widget_value = widget.itemData(widget.currentIndex()) setattr(self.config[0], name, widget_value) diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index 7607a7e..c1485c9 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -136,24 +136,14 @@ Searching - - - - <html><head/><body><p>These settings are for the automatic issue identifier which searches online for matches. </p><p>Hover the mouse over an entry field for more info.</p></body></html> - - - true - - - - - + + Qt::Horizontal - + QFormLayout::AllNonFixedFieldsGrow @@ -252,6 +242,44 @@ + + + + <html><head/><body><p>These settings are for the automatic issue identifier which searches online for matches. </p><p>Hover the mouse over an entry field for more info.</p></body></html> + + + true + + + + + + + Qt::Horizontal + + + + + + + Initially show Series Name exact matches first + + + + + + + Initially sort Series search results by Starting Year instead of No. Issues + + + + + + + Clear Form Before Importing Comic Vine data + + + @@ -313,154 +341,13 @@ Comic Sources - - - - - 0 - 0 - - - - Qt::Horizontal - - - - 0 + -1 - - - General - - - - - 10 - 70 - 650 - 22 - - - - Use Series Start Date as Volume - - - - - - 10 - 90 - 666 - 22 - - - - Clear Form Before Importing Comic Vine data - - - - - - 10 - 130 - 666 - 22 - - - - Initially sort Series search results by Starting Year instead of No. Issues - - - - - - 10 - 150 - 666 - 22 - - - - Initially show Series Name exact matches first - - - - - - 5 - 120 - 648 - 3 - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - - 190 - 13 - 301 - 32 - - - - - - - 15 - 13 - 171 - 32 - - - - Select Information Source: - - - - - - 5 - 60 - 648 - 3 - - - - Qt::Horizontal - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index edc9e4b..3a5c236 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -142,7 +142,11 @@ def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settn value_type = type(value) try: if value_type is str: - widget.setText(value) + # Special case for general dropdown box + if name == "talker_source": + widget.setCurrentIndex(widget.findData(config[0].talker_source)) + else: + widget.setText(value) if value_type is int or value_type is float: widget.setValue(value) if value_type is bool: @@ -161,15 +165,28 @@ def generate_source_option_tabs( """ sources: dict = {} - cobxInfoSource = tabs.findChildren(QtWidgets.QComboBox, "cobxInfoSource")[0] + # Use a dict to make a var name from var + source_info = {} + + # Add General tab + source_info["general"] = {"tab": QtWidgets.QWidget(), "widgets": {}} + general_layout = QtWidgets.QGridLayout() + lbl_info = QtWidgets.QLabel("Information Source:") + cbx_info = QtWidgets.QComboBox() + general_layout.addWidget(lbl_info, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + general_layout.addWidget(cbx_info, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + general_layout.addItem(vspacer, 1, 0) + source_info["general"]["widgets"]["talker_source"] = cbx_info + + source_info["general"]["tab"].setLayout(general_layout) + tabs.addTab(source_info["general"]["tab"], "General") # Add source sub tabs to Comic Sources tab for talker_id, talker_obj in talkers.items(): # Add source to general tab dropdown list - cobxInfoSource.addItem(talker_obj.name, talker_id) + source_info["general"]["widgets"]["talker_source"].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() From fefb3ce6cda98727a19427ed5edb297a9c6fe8e5 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Sun, 19 Feb 2023 23:33:22 +0000 Subject: [PATCH 15/53] Remove general tab from talker tab and use base tab from settings window. Additional clean up. --- comictaggerlib/settingswindow.py | 19 +-- comictaggerlib/ui/settingswindow.ui | 10 +- comictaggerlib/ui/talkeruigenerator.py | 160 +++++++++++++------------ tests/talker_test.py | 13 +- 4 files changed, 101 insertions(+), 101 deletions(-) diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index a5558cf..888e440 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -187,7 +187,7 @@ class SettingsWindow(QtWidgets.QDialog): self.rename_error: Exception | None = None self.sources: dict = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs( - self.tTalkerTabs, self.config, self.talkers + self.tComicTalkers, self.config, self.talkers ) self.connect_signals() self.settings_to_form() @@ -456,21 +456,8 @@ 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.config data - for tab in self.sources.items(): - for name, widget in tab[1]["widgets"].items(): - widget_value = None - if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)): - widget_value = widget.value() - elif isinstance(widget, QtWidgets.QLineEdit): - widget_value = widget.text().strip() - elif isinstance(widget, QtWidgets.QCheckBox): - widget_value = widget.isChecked() - # The talker source dropdown - elif isinstance(widget, QtWidgets.QComboBox): - widget_value = widget.itemData(widget.currentIndex()) - - setattr(self.config[0], name, widget_value) + # Read settings from talker tabs + comictaggerlib.ui.talkeruigenerator.form_settings_to_config(self.sources, self.config) self.update_talkers_config() diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index c1485c9..82b1129 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -340,15 +340,7 @@ Comic Sources - - - - - -1 - - - - + diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 3a5c236..15aed6a 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -63,79 +63,72 @@ def api_url_btn_connect( btn.clicked.connect(lambda: call_check_api_url(talker_id, sources_info, talkers)) -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] +def format_internal_name(dest_name: str) -> str: + int_name_split = dest_name.split("_") + del int_name_split[0:1] int_name_split[0] = int_name_split[0].capitalize() new_name = " ".join(int_name_split) + return new_name -def generate_checkbox( - option: settngs.Setting, layout: QtWidgets.QGridLayout -) -> tuple[QtWidgets.QGridLayout, QtWidgets.QCheckBox]: - # bool equals a checkbox (QCheckBox) - widget = QtWidgets.QCheckBox(format_internal_name(option.internal_name)) - # Add tooltip text +def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox: + widget = QtWidgets.QCheckBox(format_internal_name(option.dest)) widget.setToolTip(option.help) - # Add widget and span all columns - layout.addWidget(widget, layout.rowCount() + 1, 0, 1, -1) + layout.addWidget(widget, layout.rowCount(), 0, 1, -1) - return layout, widget + return 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.setToolTip(option.help) - layout.addWidget(widget, layout.rowCount() - 1, 1, alignment=QtCore.Qt.AlignLeft) +def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QSpinBox: + row = layout.rowCount() + lbl = QtWidgets.QLabel(format_internal_name(option.dest)) + layout.addWidget(lbl, row, 0) + widget = QtWidgets.QSpinBox() + widget.setRange(0, 9999) + widget.setToolTip(option.help) + layout.addWidget(widget, row, 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.setToolTip(option.help) - layout.addWidget(widget, layout.rowCount() - 1, 1, alignment=QtCore.Qt.AlignLeft) + return widget - return layout, widget + +def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QDoubleSpinBox: + row = layout.rowCount() + lbl = QtWidgets.QLabel(format_internal_name(option.dest)) + layout.addWidget(lbl, row, 0) + widget = QtWidgets.QDoubleSpinBox() + widget.setRange(0, 9999.99) + widget.setToolTip(option.help) + layout.addWidget(widget, row, 1, alignment=QtCore.Qt.AlignLeft) + + return widget def generate_textbox( option: settngs.Setting, layout: QtWidgets.QGridLayout -) -> tuple[QtWidgets.QGridLayout, QtWidgets.QLineEdit, QtWidgets.QPushButton]: +) -> tuple[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) + row = layout.rowCount() + lbl = QtWidgets.QLabel(format_internal_name(option.dest)) + layout.addWidget(lbl, row, 0) widget = QtWidgets.QLineEdit() widget.setObjectName(option.internal_name) widget.setToolTip(option.help) - layout.addWidget(widget, layout.rowCount() - 1, 1) + layout.addWidget(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.addWidget(btn, layout.rowCount() - 1, 2) + layout.addWidget(btn, row, 2) if option.internal_name.endswith("url"): btn = QtWidgets.QPushButton("Test URL") - layout.addWidget(btn, layout.rowCount() - 1, 2) + layout.addWidget(btn, row, 2) - return layout, widget, btn + return widget, btn -def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]): +def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: for talker in sources.items(): for name, widget in talker[1]["widgets"].items(): value = getattr(config[0], name) @@ -155,8 +148,25 @@ def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settn logger.debug("Failed to set value of %s", name) +def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: + for tab in sources.items(): + for name, widget in tab[1]["widgets"].items(): + widget_value = None + if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)): + widget_value = widget.value() + elif isinstance(widget, QtWidgets.QLineEdit): + widget_value = widget.text().strip() + elif isinstance(widget, QtWidgets.QCheckBox): + widget_value = widget.isChecked() + # The talker source dropdown + elif isinstance(widget, QtWidgets.QComboBox): + widget_value = widget.itemData(widget.currentIndex()) + + setattr(config[0], name, widget_value) + + def generate_source_option_tabs( - tabs: QtWidgets.QTabWidget, + comic_talker_tab: QtWidgets.QWidget, config: settngs.Config[settngs.Namespace], talkers: dict[str, ComicTalker], ) -> dict[str, QtWidgets.QWidget]: @@ -165,30 +175,32 @@ def generate_source_option_tabs( """ sources: dict = {} - # Use a dict to make a var name from var - source_info = {} - # Add General tab - source_info["general"] = {"tab": QtWidgets.QWidget(), "widgets": {}} - general_layout = QtWidgets.QGridLayout() + # Tab comes with a QVBoxLayout + comic_talker_tab_layout = comic_talker_tab.layout() + + talker_layout = QtWidgets.QGridLayout() lbl_info = QtWidgets.QLabel("Information Source:") cbx_info = QtWidgets.QComboBox() - general_layout.addWidget(lbl_info, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) - general_layout.addWidget(cbx_info, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) - vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - general_layout.addItem(vspacer, 1, 0) - source_info["general"]["widgets"]["talker_source"] = cbx_info + line = QtWidgets.QFrame() + line.setFrameShape(QtWidgets.QFrame.HLine) + line.setFrameShadow(QtWidgets.QFrame.Sunken) + talker_tabs = QtWidgets.QTabWidget() - source_info["general"]["tab"].setLayout(general_layout) - tabs.addTab(source_info["general"]["tab"], "General") + talker_layout.addWidget(lbl_info, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + talker_layout.addWidget(cbx_info, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + talker_layout.addWidget(line, 1, 0, 1, -1) + talker_layout.addWidget(talker_tabs, 2, 0, 1, -1) + + comic_talker_tab_layout.addLayout(talker_layout) # Add source sub tabs to Comic Sources tab for talker_id, talker_obj in talkers.items(): # Add source to general tab dropdown list - source_info["general"]["widgets"]["talker_source"].addItem(talker_obj.name, talker_id) + cbx_info.addItem(talker_obj.name, talker_id) tab_name = talker_id - source_info[tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} + sources[tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} layout_grid = QtWidgets.QGridLayout() for option in config[1][f"talker_{talker_id}"][1].values(): @@ -199,24 +211,25 @@ def generate_source_option_tabs( or option.action == "store_true" or option.action == "store_false" ): - layout_grid, current_widget = generate_checkbox(option, layout_grid) - source_info[tab_name]["widgets"][option.internal_name] = current_widget - elif option.type is int or option.type is 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 + current_widget = generate_checkbox(option, layout_grid) + sources[tab_name]["widgets"][option.internal_name] = current_widget + elif option.type is int: + current_widget = generate_spinbox(option, layout_grid) + sources[tab_name]["widgets"][option.internal_name] = current_widget + elif option.type is float: + current_widget = generate_doublespinbox(option, layout_grid) + sources[tab_name]["widgets"][option.internal_name] = current_widget # option.type of None should be string elif option.type is None or option.type is str: - layout_grid, current_widget, btn = generate_textbox(option, layout_grid) - source_info[tab_name]["widgets"][option.internal_name] = current_widget + current_widget, btn = generate_textbox(option, layout_grid) + sources[tab_name]["widgets"][option.internal_name] = current_widget if option.internal_name.endswith("key"): # Attach test api function to button. A better way? - api_key_btn_connect(btn, talker_id, source_info, talkers) + api_key_btn_connect(btn, talker_id, sources, talkers) if option.internal_name.endswith("url"): # Attach test api function to button. A better way? - api_url_btn_connect(btn, talker_id, source_info, talkers) + api_url_btn_connect(btn, talker_id, sources, talkers) else: logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") @@ -224,10 +237,9 @@ def generate_source_option_tabs( 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) + sources[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) + talker_tabs.addTab(sources[tab_name]["tab"], talker_obj.name) return sources diff --git a/tests/talker_test.py b/tests/talker_test.py index c7ced2c..c305a52 100644 --- a/tests/talker_test.py +++ b/tests/talker_test.py @@ -1,7 +1,16 @@ from __future__ import annotations +import pytest + import comictaggerlib.ui.talkeruigenerator +test_names = [ + ("cv_test_name", "Test name"), + ("cv2_test_name", "Test name"), +] -def test_format_internal_name(): - assert comictaggerlib.ui.talkeruigenerator.format_internal_name("talker_comicvine_cv_test_name") == "Test name" + +@pytest.mark.parametrize("int_name, expected", test_names) +def test_format_internal_name(int_name, expected): + results = comictaggerlib.ui.talkeruigenerator.format_internal_name(int_name) + assert results == expected From bd5e23f93f32e2c256a8f6c5326c85897d3eddfb Mon Sep 17 00:00:00 2001 From: Mizaki Date: Mon, 20 Feb 2023 00:44:51 +0000 Subject: [PATCH 16/53] Add another test case for format_internal_name --- tests/talker_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/talker_test.py b/tests/talker_test.py index c305a52..5b81534 100644 --- a/tests/talker_test.py +++ b/tests/talker_test.py @@ -7,6 +7,7 @@ import comictaggerlib.ui.talkeruigenerator test_names = [ ("cv_test_name", "Test name"), ("cv2_test_name", "Test name"), + ("mu_use_this_test", "Use this test"), ] From f439797b03faf49b2f8dfacd188436f11dd75f56 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Mon, 20 Feb 2023 18:45:39 +0000 Subject: [PATCH 17/53] Use new display_name from settngs. Add source combobox getting and setting and add to sources dict of widgets. --- comictaggerlib/ui/talkeruigenerator.py | 56 ++++++++++++-------------- comictalker/talkers/comicvine.py | 23 ++++++++--- requirements.txt | 2 +- tests/talker_test.py | 17 -------- 4 files changed, 44 insertions(+), 54 deletions(-) delete mode 100644 tests/talker_test.py diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 15aed6a..581778d 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -63,17 +63,8 @@ def api_url_btn_connect( btn.clicked.connect(lambda: call_check_api_url(talker_id, sources_info, talkers)) -def format_internal_name(dest_name: str) -> str: - int_name_split = dest_name.split("_") - del int_name_split[0:1] - int_name_split[0] = int_name_split[0].capitalize() - new_name = " ".join(int_name_split) - - return new_name - - def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox: - widget = QtWidgets.QCheckBox(format_internal_name(option.dest)) + widget = QtWidgets.QCheckBox(option.display_name) widget.setToolTip(option.help) layout.addWidget(widget, layout.rowCount(), 0, 1, -1) @@ -82,7 +73,7 @@ def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QSpinBox: row = layout.rowCount() - lbl = QtWidgets.QLabel(format_internal_name(option.dest)) + lbl = QtWidgets.QLabel(option.display_name) layout.addWidget(lbl, row, 0) widget = QtWidgets.QSpinBox() widget.setRange(0, 9999) @@ -94,7 +85,7 @@ def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QDoubleSpinBox: row = layout.rowCount() - lbl = QtWidgets.QLabel(format_internal_name(option.dest)) + lbl = QtWidgets.QLabel(option.display_name) layout.addWidget(lbl, row, 0) widget = QtWidgets.QDoubleSpinBox() widget.setRange(0, 9999.99) @@ -109,7 +100,7 @@ def generate_textbox( ) -> tuple[QtWidgets.QLineEdit, QtWidgets.QPushButton]: btn = None row = layout.rowCount() - lbl = QtWidgets.QLabel(format_internal_name(option.dest)) + lbl = QtWidgets.QLabel(option.display_name) layout.addWidget(lbl, row, 0) widget = QtWidgets.QLineEdit() widget.setObjectName(option.internal_name) @@ -129,17 +120,16 @@ def generate_textbox( def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: - for talker in sources.items(): + # Set the active talker in sources combo box + sources["talker_source"].setCurrentIndex(sources["talker_source"].findData(config[0].talker_source)) + + for talker in sources["tabs"].items(): for name, widget in talker[1]["widgets"].items(): value = getattr(config[0], name) value_type = type(value) try: if value_type is str: - # Special case for general dropdown box - if name == "talker_source": - widget.setCurrentIndex(widget.findData(config[0].talker_source)) - else: - widget.setText(value) + widget.setText(value) if value_type is int or value_type is float: widget.setValue(value) if value_type is bool: @@ -149,7 +139,10 @@ def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settn def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: - for tab in sources.items(): + # Source combo box value + config[0].talker_source = sources["talker_source"].itemData(sources["talker_source"].currentIndex()) + + for tab in sources["tabs"].items(): for name, widget in tab[1]["widgets"].items(): widget_value = None if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)): @@ -158,9 +151,6 @@ def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settn widget_value = widget.text().strip() elif isinstance(widget, QtWidgets.QCheckBox): widget_value = widget.isChecked() - # The talker source dropdown - elif isinstance(widget, QtWidgets.QComboBox): - widget_value = widget.itemData(widget.currentIndex()) setattr(config[0], name, widget_value) @@ -174,7 +164,8 @@ def generate_source_option_tabs( Generate GUI tabs and settings for talkers """ - sources: dict = {} + # Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget + sources: dict = {"tabs": {}} # Tab comes with a QVBoxLayout comic_talker_tab_layout = comic_talker_tab.layout() @@ -194,13 +185,16 @@ def generate_source_option_tabs( comic_talker_tab_layout.addLayout(talker_layout) + # Add cbx_info combobox to sources for getting and setting talker + sources["talker_source"] = cbx_info + # Add source sub tabs to Comic Sources tab for talker_id, talker_obj in talkers.items(): # Add source to general tab dropdown list cbx_info.addItem(talker_obj.name, talker_id) tab_name = talker_id - sources[tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} + sources["tabs"][tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} layout_grid = QtWidgets.QGridLayout() for option in config[1][f"talker_{talker_id}"][1].values(): @@ -212,17 +206,17 @@ def generate_source_option_tabs( or option.action == "store_false" ): current_widget = generate_checkbox(option, layout_grid) - sources[tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget elif option.type is int: current_widget = generate_spinbox(option, layout_grid) - sources[tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget elif option.type is float: current_widget = generate_doublespinbox(option, layout_grid) - sources[tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget # option.type of None should be string elif option.type is None or option.type is str: current_widget, btn = generate_textbox(option, layout_grid) - sources[tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget if option.internal_name.endswith("key"): # Attach test api function to button. A better way? @@ -237,9 +231,9 @@ def generate_source_option_tabs( vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0) # Display the new widgets - sources[tab_name]["tab"].setLayout(layout_grid) + sources["tabs"][tab_name]["tab"].setLayout(layout_grid) # Add new sub tab to Comic Source tab - talker_tabs.addTab(sources[tab_name]["tab"], talker_obj.name) + talker_tabs.addTab(sources["tabs"][tab_name]["tab"], talker_obj.name) return sources diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index fa9dc66..282c636 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -181,16 +181,29 @@ class ComicVineTalker(ComicTalker): 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) - parser.add_setting("--cv-wait-on-ratelimit", default=False, action=argparse.BooleanOptionalAction) + parser.add_setting( + "--cv-use-series-start-as-volume", + default=False, + action=argparse.BooleanOptionalAction, + display_name="Use series start as volume", + help="Use the series start year as the volume number", + ) + parser.add_setting( + "--cv-wait-on-ratelimit", + default=False, + action=argparse.BooleanOptionalAction, + display_name="Wait on ratelimit", + help="Wait when the rate limit is hit", + ) parser.add_setting( "--cv-remove-html-tables", default=False, action=argparse.BooleanOptionalAction, - help="Removes html tables instead of converting them to text.", + display_name="Remove HTML tables", + 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.") + parser.add_setting("--cv-api-key", display_name="API Key", help="Use the given Comic Vine API Key") + parser.add_setting("--cv-url", display_name="URL", help="Use the given Comic Vine URL") def parse_settings(self, settings: dict[str, Any]) -> dict[str, Any]: if settings["cv_api_key"]: diff --git a/requirements.txt b/requirements.txt index a6aefcd..7a936ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ pycountry #pyicu; sys_platform == 'linux' or sys_platform == 'darwin' rapidfuzz>=2.12.0 requests==2.* -settngs==0.5.0 +settngs==0.6.2 text2digits typing_extensions wordninja diff --git a/tests/talker_test.py b/tests/talker_test.py deleted file mode 100644 index 5b81534..0000000 --- a/tests/talker_test.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -import pytest - -import comictaggerlib.ui.talkeruigenerator - -test_names = [ - ("cv_test_name", "Test name"), - ("cv2_test_name", "Test name"), - ("mu_use_this_test", "Use this test"), -] - - -@pytest.mark.parametrize("int_name, expected", test_names) -def test_format_internal_name(int_name, expected): - results = comictaggerlib.ui.talkeruigenerator.format_internal_name(int_name) - assert results == expected From fb83863654ed5268ec1dfeb4a237556f2bda127e Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 20 Feb 2023 16:02:15 -0800 Subject: [PATCH 18/53] Update plugin settings Make "runtime" a persistent group, allows normalizing without losing validation Simplify archiver setting generation Generate options for setting a url and key for all talkers Return validated talker settings Require that the talker id must match the entry point name Add api_url and api_key as default attributes on talkers Add default handling of api_url and api_key to register_settings Update settngs to 0.6.2 to be able to add settings to a group and use the display_name attribute Error if no talkers are loaded Update talker entry point to comictagger.talker --- .../__pyinstaller/hook-comictaggerlib.py | 4 +- comictaggerlib/ctsettings/commandline.py | 2 +- comictaggerlib/ctsettings/plugin.py | 44 +++++++++------ comictaggerlib/main.py | 17 ++++-- comictaggerlib/settingswindow.py | 13 ++--- comictalker/__init__.py | 9 ++-- comictalker/comictalker.py | 18 +++++-- comictalker/talker_utils.py | 9 ++++ comictalker/talkers/comicvine.py | 54 ++++++++----------- requirements.txt | 2 +- setup.py | 2 +- 11 files changed, 102 insertions(+), 72 deletions(-) diff --git a/comictaggerlib/__pyinstaller/hook-comictaggerlib.py b/comictaggerlib/__pyinstaller/hook-comictaggerlib.py index 4143e64..8b7b6c0 100644 --- a/comictaggerlib/__pyinstaller/hook-comictaggerlib.py +++ b/comictaggerlib/__pyinstaller/hook-comictaggerlib.py @@ -1,7 +1,7 @@ from __future__ import annotations -from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import collect_data_files, collect_entry_point -datas = [] +datas, hiddenimports = collect_entry_point("comictagger.talker") datas += collect_data_files("comictaggerlib.ui") datas += collect_data_files("comictaggerlib.graphics") diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index 10f3440..e9e8376 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -236,7 +236,7 @@ def register_commands(parser: settngs.Manager) -> None: def register_commandline_settings(parser: settngs.Manager) -> None: parser.add_group("commands", register_commands, True) - parser.add_group("runtime", register_settings) + parser.add_persistent_group("runtime", register_settings) def validate_commandline_settings( diff --git a/comictaggerlib/ctsettings/plugin.py b/comictaggerlib/ctsettings/plugin.py index 84fbb90..6debffa 100644 --- a/comictaggerlib/ctsettings/plugin.py +++ b/comictaggerlib/ctsettings/plugin.py @@ -12,23 +12,39 @@ logger = logging.getLogger("comictagger") def archiver(manager: settngs.Manager) -> None: - exe_registered: set[str] = set() for archiver in comicapi.comicarchive.archivers: - if archiver.exe and archiver.exe not in exe_registered: + if archiver.exe: + # add_setting will overwrite anything with the same name. + # So we only end up with one option even if multiple archivers use the same exe. manager.add_setting( - f"--{archiver.exe.replace(' ', '-').replace('_', '-').strip().strip('-')}", + f"--{settngs.sanitize_name(archiver.exe)}", default=archiver.exe, help="Path to the %(default)s executable\n\n", ) - exe_registered.add(archiver.exe) def register_talker_settings(manager: settngs.Manager) -> None: - for talker_name, talker in comictaggerlib.ctsettings.talkers.items(): + for talker_id, talker in comictaggerlib.ctsettings.talkers.items(): + + def api_options(manager: settngs.Manager) -> None: + manager.add_setting( + f"--{talker_id}-key", + default="", + display_name="API Key", + help=f"API Key for {talker.name} (default: {talker.default_api_key})", + ) + manager.add_setting( + f"--{talker_id}-url", + default="", + display_name="URL", + help=f"URL for {talker.name} (default: {talker.default_api_url})", + ) + try: - manager.add_persistent_group("talker_" + talker_name, talker.register_settings, False) + manager.add_persistent_group("talker_" + talker_id, api_options, False) + manager.add_persistent_group("talker_" + talker_id, talker.register_settings, False) except Exception: - logger.exception("Failed to register settings for %s", talker_name) + logger.exception("Failed to register settings for %s", talker_id) def validate_archive_settings(config: settngs.Config[settngs.Namespace]) -> settngs.Config[settngs.Namespace]: @@ -37,11 +53,7 @@ def validate_archive_settings(config: settngs.Config[settngs.Namespace]) -> sett cfg = settngs.normalize_config(config, file=True, cmdline=True, defaults=False) for archiver in comicapi.comicarchive.archivers: exe_name = settngs.sanitize_name(archiver.exe) - if ( - exe_name in cfg[0]["archiver"] - and cfg[0]["archiver"][exe_name] - and cfg[0]["archiver"][exe_name] != archiver.exe - ): + if exe_name in cfg[0]["archiver"] and cfg[0]["archiver"][exe_name]: if os.path.basename(cfg[0]["archiver"][exe_name]) == archiver.exe: comicapi.utils.add_to_path(os.path.dirname(cfg[0]["archiver"][exe_name])) else: @@ -53,15 +65,15 @@ def validate_archive_settings(config: settngs.Config[settngs.Namespace]) -> sett def validate_talker_settings(config: settngs.Config[settngs.Namespace]) -> settngs.Config[settngs.Namespace]: # Apply talker settings from config file cfg = settngs.normalize_config(config, True, True) - for talker_name, talker in list(comictaggerlib.ctsettings.talkers.items()): + for talker_id, talker in list(comictaggerlib.ctsettings.talkers.items()): try: - talker.parse_settings(cfg[0]["talker_" + talker_name]) + cfg[0]["talker_" + talker_id] = talker.parse_settings(cfg[0]["talker_" + talker_id]) except Exception as e: # Remove talker as we failed to apply the settings - del comictaggerlib.ctsettings.talkers[talker_name] + del comictaggerlib.ctsettings.talkers[talker_id] logger.exception("Failed to initialize talker settings: %s", e) - return config + return settngs.get_namespace(cfg) def validate_plugin_settings(config: settngs.Config[settngs.Namespace]) -> settngs.Config[settngs.Namespace]: diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 047a802..3b0a6f0 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -119,6 +119,15 @@ class App: # config already loaded error = None + talkers = ctsettings.talkers + del ctsettings.talkers + + if len(talkers) < 1: + error = error = ( + f"Failed to load any talkers, please re-install and check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details", + True, + ) + signal.signal(signal.SIGINT, signal.SIG_DFL) logger.debug("Installed Packages") @@ -134,7 +143,10 @@ class App: # manage the CV API key # None comparison is used so that the empty string can unset the value - if self.config[0].talker_comicvine_cv_api_key is not None or self.config[0].talker_comicvine_cv_url is not None: + if not error and ( + self.config[0].talker_comicvine_comicvine_key is not None + or self.config[0].talker_comicvine_comicvine_url is not None + ): settings_path = self.config[0].runtime_config.user_config_dir / "settings.json" if self.config_load_success: self.manager.save_file(self.config[0], settings_path) @@ -150,9 +162,6 @@ class App: 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 diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index a7270ad..aadcb71 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -320,8 +320,8 @@ class SettingsWindow(QtWidgets.QDialog): 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) + self.leKey.setText(self.config[0].talker_comicvine_comicvine_key) + self.leURL.setText(self.config[0].talker_comicvine_comicvine_url) self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].cbl_assume_lone_credit_is_primary) self.cbxCopyCharactersToTags.setChecked(self.config[0].cbl_copy_characters_to_tags) @@ -436,13 +436,8 @@ class SettingsWindow(QtWidgets.QDialog): 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_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_url = self.config[0].talker_comicvine_cv_url + self.config[0].talker_comicvine_comicvine_key = self.leKey.text().strip() + self.config[0].talker_comicvine_comicvine_url = self.leURL.text().strip() self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() diff --git a/comictalker/__init__.py b/comictalker/__init__.py index e216f27..0e5f6d6 100644 --- a/comictalker/__init__.py +++ b/comictalker/__init__.py @@ -26,13 +26,16 @@ def get_talkers(version: str, cache: pathlib.Path) -> dict[str, ComicTalker]: """Returns all comic talker instances""" talkers: dict[str, ComicTalker] = {} - for talker in entry_points(group="comictagger.talkers"): + for talker in entry_points(group="comictagger.talker"): try: talker_cls = talker.load() obj = talker_cls(version, cache) - talkers[obj.id] = obj + if obj.id != talker.name: + logger.error("Talker ID must be the same as the entry point name") + continue + talkers[talker.name] = obj + except Exception: logger.exception("Failed to load talker: %s", talker.name) - raise TalkerError(source=talker.name, code=4, desc="Failed to initialise talker") return talkers diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index 6b5dd7f..aebffd6 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -21,6 +21,7 @@ import settngs from comicapi.genericmetadata import GenericMetadata from comictalker.resulttypes import ComicIssue, ComicSeries +from comictalker.talker_utils import fix_url logger = logging.getLogger(__name__) @@ -107,15 +108,15 @@ class ComicTalker: name: str = "Example" id: str = "example" - logo_url: str = "https://example.com/logo.png" - website: str = "https://example.com/" + website: str = "https://example.com" + logo_url: str = f"{website}/logo.png" attribution: str = f"Metadata provided by {name}" def __init__(self, version: str, cache_folder: pathlib.Path) -> None: self.cache_folder = cache_folder self.version = version - self.api_key: str = "" - self.api_url: str = "" + self.api_key = self.default_api_key = "" + self.api_url = self.default_api_url = "" def register_settings(self, parser: settngs.Manager) -> None: """Allows registering settings using the settngs package with an argparse like interface""" @@ -126,6 +127,15 @@ class ComicTalker: settings is a dictionary of settings defined in register_settings. It is only guaranteed that the settings defined in register_settings will be present. """ + if settings[f"{self.id}_key"]: + self.api_key = settings[f"{self.id}_key"] + if settings[f"{self.id}_url"]: + self.api_url = fix_url(settings[f"{self.id}_url"]) + + if self.api_key == "": + self.api_key = self.default_api_key + if self.api_url == "": + self.api_url = self.default_api_url return settings def check_api_key(self, key: str, url: str) -> bool: diff --git a/comictalker/talker_utils.py b/comictalker/talker_utils.py index bd15389..8f8f329 100644 --- a/comictalker/talker_utils.py +++ b/comictalker/talker_utils.py @@ -15,6 +15,7 @@ from __future__ import annotations import logging import re +from urllib.parse import urlsplit from bs4 import BeautifulSoup @@ -26,6 +27,14 @@ from comictalker.resulttypes import ComicIssue logger = logging.getLogger(__name__) +def fix_url(url: str) -> str: + tmp_url = urlsplit(url) + # joinurl only works properly if there is a trailing slash + if tmp_url.path and tmp_url.path[-1] != "/": + tmp_url = tmp_url._replace(path=tmp_url.path + "/") + return tmp_url.geturl() + + def map_comic_issue_to_metadata( issue_results: ComicIssue, source: str, remove_html_tables: bool = False, use_year_volume: bool = False ) -> GenericMetadata: diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 1df6e80..5b426b0 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -22,7 +22,7 @@ import logging import pathlib import time from typing import Any, Callable, Generic, TypeVar -from urllib.parse import urljoin, urlsplit +from urllib.parse import urljoin import requests import settngs @@ -156,33 +156,36 @@ 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/" + website: str = "https://comicvine.gamespot.com" + logo_url: str = f"{website}/a/bundles/comicvinesite/images/logo.png" attribution: str = f"Metadata provided by {name}" def __init__(self, version: str, cache_folder: pathlib.Path): super().__init__(version, cache_folder) # Default settings - self.api_url: str = "https://comicvine.gamespot.com/api" - self.api_key: str = "27431e6787042105bd3e47e169a624521f89f3a4" + self.default_api_url = self.api_url = f"{self.website}/api/" + self.default_api_key = self.api_key = "27431e6787042105bd3e47e169a624521f89f3a4" self.remove_html_tables: bool = False self.use_series_start_as_volume: bool = False self.wait_on_ratelimit: bool = False - tmp_url = urlsplit(self.api_url) - - # joinurl only works properly if there is a trailing slash - if tmp_url.path and tmp_url.path[-1] != "/": - tmp_url = tmp_url._replace(path=tmp_url.path + "/") - - self.api_url = tmp_url.geturl() - # NOTE: This was hardcoded before which is why it isn't in settings 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.") + # The empty string being the default allows this setting to be unset, allowing the default to change + parser.add_setting( + f"--{self.id}-key", + default="", + display_name="API Key", + help=f"Use the given Comic Vine API Key. (default: {self.default_api_key})", + ) + parser.add_setting( + f"--{self.id}-url", + default="", + display_name="API URL", + help=f"Use the given Comic Vine URL. (default: {self.default_api_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( @@ -193,35 +196,24 @@ class ComicVineTalker(ComicTalker): ) 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"]: - tmp_url = urlsplit(settings["cv_url"]) - # joinurl only works properly if there is a trailing slash - if tmp_url.path and tmp_url.path[-1] != "/": - tmp_url = tmp_url._replace(path=tmp_url.path + "/") - - self.api_url = tmp_url.geturl() + settings = super().parse_settings(settings) 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 + return settings def check_api_key(self, key: str, url: str) -> bool: + url = talker_utils.fix_url(url) if not url: - url = self.api_url + url = self.default_api_url try: - tmp_url = urlsplit(url) - if tmp_url.path and tmp_url.path[-1] != "/": - tmp_url = tmp_url._replace(path=tmp_url.path + "/") - url = tmp_url.geturl() 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"}, + params={"api_key": key or self.default_api_key, "format": "json", "field_list": "name"}, ).json() # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" diff --git a/requirements.txt b/requirements.txt index a6aefcd..7a936ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ pycountry #pyicu; sys_platform == 'linux' or sys_platform == 'darwin' rapidfuzz>=2.12.0 requests==2.* -settngs==0.5.0 +settngs==0.6.2 text2digits typing_extensions wordninja diff --git a/setup.py b/setup.py index d234566..b2cb90a 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( "rar = comicapi.archivers.rar:RarArchiver", "folder = comicapi.archivers.folder:FolderArchiver", ], - "comictagger.talkers": [ + "comictagger.talker": [ "comicvine = comictalker.talkers.comicvine:ComicVineTalker", ], }, From 59893b1d1cd6b3c83215f43640b496b9517098c8 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 21 Feb 2023 00:38:13 +0000 Subject: [PATCH 19/53] Fix optoin.type ifs --- comictaggerlib/ui/talkeruigenerator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 581778d..a5cd24a 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -201,7 +201,7 @@ def generate_source_option_tabs( current_widget = None if option.action is not None and ( option.action is argparse.BooleanOptionalAction - or option.action is bool + or option.type is bool or option.action == "store_true" or option.action == "store_false" ): @@ -214,7 +214,7 @@ def generate_source_option_tabs( current_widget = generate_doublespinbox(option, layout_grid) sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget # option.type of None should be string - elif option.type is None or option.type is str: + elif (option.type is None and option.action is None) or option.type is str: current_widget, btn = generate_textbox(option, layout_grid) sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget From 316bd52f21125d006e67d616c473e6f78fc2f1f5 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 21 Feb 2023 00:42:11 +0000 Subject: [PATCH 20/53] Use currentData for combo box --- comictaggerlib/ui/talkeruigenerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index a5cd24a..1f2a36a 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -140,7 +140,7 @@ def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settn def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: # Source combo box value - config[0].talker_source = sources["talker_source"].itemData(sources["talker_source"].currentIndex()) + config[0].talker_source = sources["talker_source"].currentData() for tab in sources["tabs"].items(): for name, widget in tab[1]["widgets"].items(): From aba59bdbfe6aea0a2ef1f9757dd0406bffc84a6d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 00:43:46 +0000 Subject: [PATCH 21/53] docs(contributor): contrib-readme-action has updated readme --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c276be0..f8378ca 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ choco install comictagger - - fcanc + + mizaki
- fcanc + mizaki
@@ -96,19 +96,19 @@ choco install comictagger lordwelch - - - mizaki -
- mizaki -
- MichaelFitzurka
MichaelFitzurka
+ + + + fcanc +
+ fcanc +
From 5b5a483e25a447dcdbdf92b41ae66ef674fbdbd3 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 21 Feb 2023 00:58:13 +0000 Subject: [PATCH 22/53] Fix api key test button generation --- comictaggerlib/ui/talkeruigenerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 1f2a36a..9966d3f 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -108,7 +108,7 @@ def generate_textbox( layout.addWidget(widget, row, 1) # Special case for api_key, make a test button - if option.internal_name.endswith("api_key"): + if option.internal_name.endswith("key"): btn = QtWidgets.QPushButton("Test Key") layout.addWidget(btn, row, 2) From 8b9332e15004640710080727ae12aab2d46cdcf7 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 21 Feb 2023 20:00:47 -0800 Subject: [PATCH 23/53] Fix linux build --- .github/workflows/build.yaml | 3 +-- .github/workflows/package.yaml | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 337661e..dcd6901 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -89,7 +89,7 @@ jobs: if: runner.os == 'macOS' - name: Install linux dependencies run: | - sudo apt-get install pkg-config libicu-dev + sudo apt-get install pkg-config libicu-dev libqt5gui5 export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" python -m pip install --no-binary=pyicu pyicu @@ -106,7 +106,6 @@ jobs: - name: Archive production artifacts uses: actions/upload-artifact@v2 - if: runner.os != 'Linux' # linux binary currently has a segfault when running on latest fedora with: name: "${{ format('ComicTagger-{0}', runner.os) }}" path: | diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index eb37ecd..b7c73f0 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -50,7 +50,7 @@ jobs: if: runner.os == 'macOS' - name: Install linux dependencies run: | - sudo apt-get install pkg-config libicu-dev + sudo apt-get install pkg-config libicu-dev libqt5gui5 export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" python -m pip install --no-binary=pyicu pyicu @@ -88,6 +88,7 @@ jobs: name: "${{ env.release_name }}" prerelease: "${{ contains(github.ref, '-') }}" # alpha-releases should be 1.3.0-alpha.x full releases should be 1.3.0 draft: false + # upload the single application zip file for each OS and include the wheel built on linux files: | - dist/!(*Linux).zip + dist/*.zip dist/*${{ fromJSON('["never", ""]')[runner.os == 'Linux'] }}.whl From 118429f84c599ab1a1fee2b866fb07f5d8a5cadb Mon Sep 17 00:00:00 2001 From: Mizaki Date: Thu, 23 Feb 2023 00:42:48 +0000 Subject: [PATCH 24/53] Change source term to metadata Generate API text field in their own function API tests return string message of result Add help to text field lables --- comictaggerlib/ui/settingswindow.ui | 2 +- comictaggerlib/ui/talkeruigenerator.py | 138 +++++++++++-------------- comictalker/comictalker.py | 4 +- comictalker/talkers/comicvine.py | 13 ++- 4 files changed, 75 insertions(+), 82 deletions(-) diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index 82b1129..ee7d80d 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -338,7 +338,7 @@
- Comic Sources + Metadata Sources diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 9966d3f..cc785e8 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse import logging +from functools import partial import settngs from PyQt5 import QtCore, QtWidgets @@ -11,56 +12,53 @@ from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) -def call_check_api_key( +def generate_api_widgets( talker_id: str, - sources_info: dict[str, QtWidgets.QWidget], + sources: dict[str, QtWidgets.QWidget], + config: settngs.Config[settngs.Namespace], + layout: QtWidgets.QGridLayout, talkers: dict[str, ComicTalker], ): - 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() + # *args enforces keyword arguments and allows position arguments to be ignored + def call_check_api(*args, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker_id: str): + url = "" + key = "" + if le_key is not None: + key = le_key.text().strip() + if le_url is not None: + url = le_url.text().strip() + QtWidgets.QMessageBox.information(None, "API Test", talkers[talker_id].check_api_key(url, key)) - if talkers[talker_id].check_api_key(key): - QtWidgets.QMessageBox.information(None, "API Key Test", "Key is valid!") - else: - QtWidgets.QMessageBox.warning(None, "API Key Test", "Key is NOT valid!") + # get the actual config objects in case they have overwritten the default + talker_key = config[1][f"talker_{talker_id}"][1][f"{talker_id}_key"] + talker_url = config[1][f"talker_{talker_id}"][1][f"{talker_id}_url"] + btn_test_row = None + le_key = None + le_url = None + # only file settings are saved + if talker_key.file: + # record the current row so we know where to add the button + btn_test_row = layout.rowCount() + le_key = generate_textbox(talker_key, layout) + # To enable setting and getting + sources["tabs"][talker_id]["widgets"][f"talker_{talker_id}_{talker_id}_key"] = le_key -def call_check_api_url( - talker_id: str, - sources_info: dict[str, QtWidgets.QWidget], - talkers: dict[str, ComicTalker], -): - 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() + # only file settings are saved + if talker_url.file: + # record the current row so we know where to add the button + # We overwrite so that the default will be next to the url text box + btn_test_row = layout.rowCount() + le_url = generate_textbox(talker_url, layout) + # To enable setting and getting + sources["tabs"][talker_id]["widgets"][f"talker_{talker_id}_{talker_id}_url"] = le_url - if talkers[talker_id].check_api_url(url): - QtWidgets.QMessageBox.information(None, "API Key Test", "URL is valid!") - else: - QtWidgets.QMessageBox.warning(None, "API Key Test", "URL is NOT valid!") - - -def api_key_btn_connect( - btn: QtWidgets.QPushButton, - talker_id: str, - sources_info: dict[str, QtWidgets.QWidget], - talkers: dict[str, ComicTalker], -) -> None: - btn.clicked.connect(lambda: call_check_api_key(talker_id, sources_info, talkers)) - - -def api_url_btn_connect( - btn: QtWidgets.QPushButton, - talker_id: str, - sources_info: dict[str, QtWidgets.QWidget], - talkers: dict[str, ComicTalker], -) -> None: - btn.clicked.connect(lambda: call_check_api_url(talker_id, sources_info, talkers)) + # The button row was recorded so we add it + if btn_test_row is not None: + btn = QtWidgets.QPushButton("Test API") + layout.addWidget(btn, btn_test_row, 2) + # partial is used as connect will pass in event information + btn.clicked.connect(partial(call_check_api, le_url=le_url, le_key=le_key, talker_id=talker_id)) def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox: @@ -95,33 +93,21 @@ def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayou return widget -def generate_textbox( - option: settngs.Setting, layout: QtWidgets.QGridLayout -) -> tuple[QtWidgets.QLineEdit, QtWidgets.QPushButton]: - btn = None +def generate_textbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QLineEdit: row = layout.rowCount() lbl = QtWidgets.QLabel(option.display_name) layout.addWidget(lbl, row, 0) widget = QtWidgets.QLineEdit() - widget.setObjectName(option.internal_name) + lbl.setToolTip(option.help) widget.setToolTip(option.help) layout.addWidget(widget, row, 1) - # Special case for api_key, make a test button - if option.internal_name.endswith("key"): - btn = QtWidgets.QPushButton("Test Key") - layout.addWidget(btn, row, 2) - - if option.internal_name.endswith("url"): - btn = QtWidgets.QPushButton("Test URL") - layout.addWidget(btn, row, 2) - - return widget, btn + return widget def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: - # Set the active talker in sources combo box - sources["talker_source"].setCurrentIndex(sources["talker_source"].findData(config[0].talker_source)) + # Set the active talker via id in sources combo box + sources["cbx_select_talker"].setCurrentIndex(sources["cbx_select_talker"].findData(config[0].talker_source)) for talker in sources["tabs"].items(): for name, widget in talker[1]["widgets"].items(): @@ -140,7 +126,7 @@ def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settn def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[settngs.Namespace]) -> None: # Source combo box value - config[0].talker_source = sources["talker_source"].currentData() + config[0].talker_source = sources["cbx_select_talker"].currentData() for tab in sources["tabs"].items(): for name, widget in tab[1]["widgets"].items(): @@ -171,33 +157,37 @@ def generate_source_option_tabs( comic_talker_tab_layout = comic_talker_tab.layout() talker_layout = QtWidgets.QGridLayout() - lbl_info = QtWidgets.QLabel("Information Source:") - cbx_info = QtWidgets.QComboBox() + lbl_select_talker = QtWidgets.QLabel("Metadata Sources:") + cbx_select_talker = QtWidgets.QComboBox() line = QtWidgets.QFrame() line.setFrameShape(QtWidgets.QFrame.HLine) line.setFrameShadow(QtWidgets.QFrame.Sunken) talker_tabs = QtWidgets.QTabWidget() - talker_layout.addWidget(lbl_info, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) - talker_layout.addWidget(cbx_info, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + talker_layout.addWidget(lbl_select_talker, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + talker_layout.addWidget(cbx_select_talker, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) talker_layout.addWidget(line, 1, 0, 1, -1) talker_layout.addWidget(talker_tabs, 2, 0, 1, -1) comic_talker_tab_layout.addLayout(talker_layout) - # Add cbx_info combobox to sources for getting and setting talker - sources["talker_source"] = cbx_info + # Add combobox to sources for getting and setting talker + sources["cbx_select_talker"] = cbx_select_talker # Add source sub tabs to Comic Sources tab for talker_id, talker_obj in talkers.items(): # Add source to general tab dropdown list - cbx_info.addItem(talker_obj.name, talker_id) + cbx_select_talker.addItem(talker_obj.name, talker_id) tab_name = talker_id sources["tabs"][tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} layout_grid = QtWidgets.QGridLayout() for option in config[1][f"talker_{talker_id}"][1].values(): + if not option.file: + continue + if option.dest in (f"{talker_id}_url", f"{talker_id}_key"): + continue current_widget = None if option.action is not None and ( option.action is argparse.BooleanOptionalAction @@ -215,18 +205,14 @@ def generate_source_option_tabs( sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget # option.type of None should be string elif (option.type is None and option.action is None) or option.type is str: - current_widget, btn = generate_textbox(option, layout_grid) + current_widget = generate_textbox(option, layout_grid) sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget - - if option.internal_name.endswith("key"): - # Attach test api function to button. A better way? - api_key_btn_connect(btn, talker_id, sources, talkers) - if option.internal_name.endswith("url"): - # Attach test api function to button. A better way? - api_url_btn_connect(btn, talker_id, sources, talkers) else: logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") + # Add talker URL and API key fields + generate_api_widgets(talker_id, sources, config, layout_grid, talkers) + # Add vertical spacer vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0) diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index 26b08e8..5a5a916 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -141,9 +141,9 @@ class ComicTalker: self.api_url = self.default_api_url return settings - def check_api_key(self, key: str) -> bool: + def check_api_key(self, url: str, key: str) -> str: """ - This function should return true if the given api key is valid. + This function should return a string with the test outcome for display to user. """ raise NotImplementedError diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 09fbc92..c14edb0 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -216,7 +216,11 @@ class ComicVineTalker(ComicTalker): self.remove_html_tables = settings["cv_remove_html_tables"] return settings - def check_api_key(self, key: str, url: str) -> bool: + def check_api_key( + self, + url: str, + key: str, + ) -> str: url = talker_utils.fix_url(url) if not url: url = self.default_api_url @@ -230,9 +234,12 @@ class ComicVineTalker(ComicTalker): ).json() # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" - return cv_response["status_code"] != 100 + if cv_response["status_code"] != 100: + return "API key is valid" + else: + return "API key is INVALID!" except Exception: - return False + return "Failed to connect to the URL!" def search_for_series( self, From b4a3e8c2ee88f10cd2a8c7fef658ea6a1db2d09d Mon Sep 17 00:00:00 2001 From: Mizaki Date: Fri, 24 Feb 2023 00:06:48 +0000 Subject: [PATCH 25/53] Add missing tool tips to labels Change metadata select label Use named tuple for talker tabs Retrun a string and bool for api check --- comictaggerlib/ui/talkeruigenerator.py | 41 +++++++++++++++++--------- comictalker/comictalker.py | 4 +-- comictalker/talkers/comicvine.py | 12 +++----- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index cc785e8..d34c63e 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import logging from functools import partial +from typing import NamedTuple import settngs from PyQt5 import QtCore, QtWidgets @@ -12,6 +13,11 @@ from comictalker.comictalker import ComicTalker logger = logging.getLogger(__name__) +class TalkerTab(NamedTuple): + tab: QtWidgets.QWidget + widgets: dict[str, QtWidgets.QWidget] + + def generate_api_widgets( talker_id: str, sources: dict[str, QtWidgets.QWidget], @@ -27,7 +33,12 @@ def generate_api_widgets( key = le_key.text().strip() if le_url is not None: url = le_url.text().strip() - QtWidgets.QMessageBox.information(None, "API Test", talkers[talker_id].check_api_key(url, key)) + + check_text, check_bool = talkers[talker_id].check_api_key(url, key) + if check_bool: + QtWidgets.QMessageBox.information(None, "API Test Success", check_text) + else: + QtWidgets.QMessageBox.warning(None, "API Test Failed", check_text) # get the actual config objects in case they have overwritten the default talker_key = config[1][f"talker_{talker_id}"][1][f"{talker_id}_key"] @@ -42,7 +53,7 @@ def generate_api_widgets( btn_test_row = layout.rowCount() le_key = generate_textbox(talker_key, layout) # To enable setting and getting - sources["tabs"][talker_id]["widgets"][f"talker_{talker_id}_{talker_id}_key"] = le_key + sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_key"] = le_key # only file settings are saved if talker_url.file: @@ -51,7 +62,7 @@ def generate_api_widgets( btn_test_row = layout.rowCount() le_url = generate_textbox(talker_url, layout) # To enable setting and getting - sources["tabs"][talker_id]["widgets"][f"talker_{talker_id}_{talker_id}_url"] = le_url + sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_url"] = le_url # The button row was recorded so we add it if btn_test_row is not None: @@ -72,6 +83,7 @@ def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QSpinBox: row = layout.rowCount() lbl = QtWidgets.QLabel(option.display_name) + lbl.setToolTip(option.help) layout.addWidget(lbl, row, 0) widget = QtWidgets.QSpinBox() widget.setRange(0, 9999) @@ -84,6 +96,7 @@ def generate_spinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QDoubleSpinBox: row = layout.rowCount() lbl = QtWidgets.QLabel(option.display_name) + lbl.setToolTip(option.help) layout.addWidget(lbl, row, 0) widget = QtWidgets.QDoubleSpinBox() widget.setRange(0, 9999.99) @@ -96,9 +109,9 @@ def generate_doublespinbox(option: settngs.Setting, layout: QtWidgets.QGridLayou def generate_textbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QLineEdit: row = layout.rowCount() lbl = QtWidgets.QLabel(option.display_name) + lbl.setToolTip(option.help) layout.addWidget(lbl, row, 0) widget = QtWidgets.QLineEdit() - lbl.setToolTip(option.help) widget.setToolTip(option.help) layout.addWidget(widget, row, 1) @@ -110,7 +123,7 @@ def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settn sources["cbx_select_talker"].setCurrentIndex(sources["cbx_select_talker"].findData(config[0].talker_source)) for talker in sources["tabs"].items(): - for name, widget in talker[1]["widgets"].items(): + for name, widget in talker[1].widgets.items(): value = getattr(config[0], name) value_type = type(value) try: @@ -129,7 +142,7 @@ def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settn config[0].talker_source = sources["cbx_select_talker"].currentData() for tab in sources["tabs"].items(): - for name, widget in tab[1]["widgets"].items(): + for name, widget in tab[1].widgets.items(): widget_value = None if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)): widget_value = widget.value() @@ -157,7 +170,7 @@ def generate_source_option_tabs( comic_talker_tab_layout = comic_talker_tab.layout() talker_layout = QtWidgets.QGridLayout() - lbl_select_talker = QtWidgets.QLabel("Metadata Sources:") + lbl_select_talker = QtWidgets.QLabel("Metadata Source:") cbx_select_talker = QtWidgets.QComboBox() line = QtWidgets.QFrame() line.setFrameShape(QtWidgets.QFrame.HLine) @@ -180,7 +193,7 @@ def generate_source_option_tabs( cbx_select_talker.addItem(talker_obj.name, talker_id) tab_name = talker_id - sources["tabs"][tab_name] = {"tab": QtWidgets.QWidget(), "widgets": {}} + sources["tabs"][tab_name] = TalkerTab(tab=QtWidgets.QWidget(), widgets={}) layout_grid = QtWidgets.QGridLayout() for option in config[1][f"talker_{talker_id}"][1].values(): @@ -196,17 +209,17 @@ def generate_source_option_tabs( or option.action == "store_false" ): current_widget = generate_checkbox(option, layout_grid) - sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget elif option.type is int: current_widget = generate_spinbox(option, layout_grid) - sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget elif option.type is float: current_widget = generate_doublespinbox(option, layout_grid) - sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget # option.type of None should be string elif (option.type is None and option.action is None) or option.type is str: current_widget = generate_textbox(option, layout_grid) - sources["tabs"][tab_name]["widgets"][option.internal_name] = current_widget + sources["tabs"][tab_name]("widget")[option.internal_name] = current_widget else: logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") @@ -217,9 +230,9 @@ def generate_source_option_tabs( vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0) # Display the new widgets - sources["tabs"][tab_name]["tab"].setLayout(layout_grid) + sources["tabs"][tab_name].tab.setLayout(layout_grid) # Add new sub tab to Comic Source tab - talker_tabs.addTab(sources["tabs"][tab_name]["tab"], talker_obj.name) + talker_tabs.addTab(sources["tabs"][tab_name].tab, talker_obj.name) return sources diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index 5a5a916..e5c5d04 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -141,9 +141,9 @@ class ComicTalker: self.api_url = self.default_api_url return settings - def check_api_key(self, url: str, key: str) -> str: + def check_api_key(self, url: str, key: str) -> tuple[str, bool]: """ - This function should return a string with the test outcome for display to user. + This function should return a string with the test outcome for display to user and a bool. """ raise NotImplementedError diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index c14edb0..3e43e05 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -216,11 +216,7 @@ class ComicVineTalker(ComicTalker): self.remove_html_tables = settings["cv_remove_html_tables"] return settings - def check_api_key( - self, - url: str, - key: str, - ) -> str: + def check_api_key(self, url: str, key: str) -> tuple[str, bool]: url = talker_utils.fix_url(url) if not url: url = self.default_api_url @@ -235,11 +231,11 @@ class ComicVineTalker(ComicTalker): # Bogus request, but if the key is wrong, you get error 100: "Invalid API Key" if cv_response["status_code"] != 100: - return "API key is valid" + return "The API key is valid", True else: - return "API key is INVALID!" + return "The API key is INVALID!", False except Exception: - return "Failed to connect to the URL!" + return "Failed to connect to the URL!", False def search_for_series( self, From 2611c284b87b82998dff6861b9322d89570ea316 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Fri, 24 Feb 2023 13:23:29 +0000 Subject: [PATCH 26/53] Revert "docs(contributor): contrib-readme-action has updated readme" This reverts commit aba59bdbfe6aea0a2ef1f9757dd0406bffc84a6d. --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f8378ca..c276be0 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ choco install comictagger - - mizaki + + fcanc
- mizaki + fcanc
@@ -96,19 +96,19 @@ choco install comictagger lordwelch + + + mizaki +
+ mizaki +
+ MichaelFitzurka
MichaelFitzurka
- - - - fcanc -
- fcanc -
From 924467cc57c6f56172dcc9b9889904be5110af46 Mon Sep 17 00:00:00 2001 From: Ben Longman Date: Sun, 26 Feb 2023 22:12:50 -0800 Subject: [PATCH 27/53] Add AppImage Support --- .github/workflows/build.yaml | 14 +++++++++++++- .github/workflows/package.yaml | 7 +++++++ Makefile | 10 +++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index dcd6901..eb5d993 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -89,7 +89,7 @@ jobs: if: runner.os == 'macOS' - name: Install linux dependencies run: | - sudo apt-get install pkg-config libicu-dev libqt5gui5 + sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2 export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" python -m pip install --no-binary=pyicu pyicu @@ -104,6 +104,11 @@ jobs: run: | make dist + - name: build appimage + run: | + make appimage + if: runner.os == 'Linux' + - name: Archive production artifacts uses: actions/upload-artifact@v2 with: @@ -111,6 +116,13 @@ jobs: path: | dist/*.zip + - name: Archive production artifacts - appimage + uses: actions/upload-artifact@v2 + with: + name: "${{ format('ComicTagger-{0}', runner.os) }}" + path: | + dist/*.AppImage + - name: PyTest run: | python -m pytest diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index b7c73f0..f4d3c71 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -74,6 +74,11 @@ jobs: run: | make dist + - name: Build AppImage package + run: | + make appimage + if: runner.os == 'Linux' + - name: Get release name if: startsWith(github.ref, 'refs/tags/') shell: bash @@ -92,3 +97,5 @@ jobs: files: | dist/*.zip dist/*${{ fromJSON('["never", ""]')[runner.os == 'Linux'] }}.whl + dist/*.AppImage + diff --git a/Makefile b/Makefile index 8fe3924..1f44182 100644 --- a/Makefile +++ b/Makefile @@ -61,4 +61,12 @@ $(INSTALL_STAMP): $(PYTHON_VENV) requirements.txt requirements_dev.txt dist: pyinstaller -y comictagger.spec - cd dist && zip -m -r $(FINAL_NAME).zip $(APP_NAME) + cd dist && zip -r $(FINAL_NAME).zip $(APP_NAME) + +appimage: dist + cp -a dist/comictagger dist/appimage + curl -L https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage > dist/appimagetool + chmod +x dist/appimagetool + cd dist/appimage/ && ln -s comictaggerlib/graphics/app.png app.png && ln -s comictagger AppRun + sed -e 's|/usr/local/share/comictagger/app.png|app|g' -e 's|%%CTSCRIPT%% %F|./comictagger|g' desktop-integration/linux/ComicTagger.desktop > dist/appimage/AppRun.desktop + cd dist && ./appimagetool appimage From eaf0ef2f1b901d3937877a8428cdb8270143854f Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 27 Feb 2023 22:12:12 -0800 Subject: [PATCH 28/53] Fix Makefile dependencies Remove dist/appimage before copy to prevent issues with 2nd run Add dist/appimagetool target so that the appimage tool is downloaded once --- Makefile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 1f44182..20860fd 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ else FINAL_NAME=ComicTagger-$(VERSION_STR)-$(shell uname -s) endif -.PHONY: all clean pydist dist CI check +.PHONY: all clean pydist dist CI check appimage all: clean dist @@ -59,14 +59,18 @@ $(INSTALL_STAMP): $(PYTHON_VENV) requirements.txt requirements_dev.txt $(PYTHON_VENV) -m pip install -e . touch $(INSTALL_STAMP) -dist: +dist: dist/$(FINAL_NAME).zip + +dist/$(FINAL_NAME).zip: pyinstaller -y comictagger.spec cd dist && zip -r $(FINAL_NAME).zip $(APP_NAME) -appimage: dist - cp -a dist/comictagger dist/appimage +dist/appimagetool: curl -L https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage > dist/appimagetool chmod +x dist/appimagetool + +appimage: dist dist/appimagetool + rm -rf dist/appimage && cp -a dist/comictagger dist/appimage cd dist/appimage/ && ln -s comictaggerlib/graphics/app.png app.png && ln -s comictagger AppRun sed -e 's|/usr/local/share/comictagger/app.png|app|g' -e 's|%%CTSCRIPT%% %F|./comictagger|g' desktop-integration/linux/ComicTagger.desktop > dist/appimage/AppRun.desktop cd dist && ./appimagetool appimage From 5b2a06870a85ab875cbb758116bbcdb8e405d1bc Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 23 Feb 2023 10:36:57 -0800 Subject: [PATCH 29/53] Fix talker settings validation --- comictaggerlib/settingswindow.py | 7 ++++--- comictalker/comictalker.py | 2 ++ comictalker/talker_utils.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 05acdcc..1b7b356 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -28,6 +28,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets, uic import comictaggerlib.ui.talkeruigenerator from comicapi import utils from comicapi.genericmetadata import md_test +from comictaggerlib import ctsettings from comictaggerlib.ctversion import version from comictaggerlib.filerenamer import FileRenamer, Replacement, Replacements from comictaggerlib.imagefetcher import ImageFetcher @@ -466,9 +467,9 @@ class SettingsWindow(QtWidgets.QDialog): QtWidgets.QDialog.accept(self) def update_talkers_config(self) -> None: - cfg = settngs.normalize_config(self.config, True, True) - for talker, talker_obj in self.talkers.items(): - talker_obj.parse_settings(cfg[0][f"talker_{talker}"]) + ctsettings.talkers = self.talkers + self.config = ctsettings.plugin.validate_talker_settings(self.config) + del ctsettings.talkers def select_rar(self) -> None: self.select_file(self.leRarExePath, "RAR") diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index e5c5d04..aac83ae 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -135,6 +135,8 @@ class ComicTalker: if settings[f"{self.id}_url"]: self.api_url = fix_url(settings[f"{self.id}_url"]) + settings[f"{self.id}_url"] = self.api_url + if self.api_key == "": self.api_key = self.default_api_key if self.api_url == "": diff --git a/comictalker/talker_utils.py b/comictalker/talker_utils.py index 8f8f329..c397d79 100644 --- a/comictalker/talker_utils.py +++ b/comictalker/talker_utils.py @@ -14,6 +14,7 @@ from __future__ import annotations import logging +import posixpath import re from urllib.parse import urlsplit @@ -30,8 +31,7 @@ logger = logging.getLogger(__name__) def fix_url(url: str) -> str: tmp_url = urlsplit(url) # joinurl only works properly if there is a trailing slash - if tmp_url.path and tmp_url.path[-1] != "/": - tmp_url = tmp_url._replace(path=tmp_url.path + "/") + tmp_url = tmp_url._replace(path=posixpath.normpath(tmp_url.path) + "/") return tmp_url.geturl() From 9bf998ca9edb4b7a9f050b1f78a74f581834c16f Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 27 Feb 2023 22:29:23 -0800 Subject: [PATCH 30/53] Remove check_api_url and fix docstrings --- comictalker/comictalker.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/comictalker/comictalker.py b/comictalker/comictalker.py index aac83ae..319dd9b 100644 --- a/comictalker/comictalker.py +++ b/comictalker/comictalker.py @@ -120,8 +120,8 @@ class ComicTalker: def register_settings(self, parser: settngs.Manager) -> None: """ - Allows registering settings using the settngs package with an argparse like interface - NOTE: The order used will be reflected within the settings menu + Allows registering settings using the settngs package with an argparse like interface. + The order that settings are declared is the order they will be displayed. """ return None @@ -145,13 +145,14 @@ class ComicTalker: def check_api_key(self, url: str, key: str) -> tuple[str, bool]: """ - This function should return a string with the test outcome for display to user and a bool. - """ - raise NotImplementedError + This function should return (msg, True) if the given API key and URL are valid, + where msg is a message to display to the user. - def check_api_url(self, url: str) -> bool: - """ - This function should return true if the given url is valid. + This function should return (msg, False) if the given API key or URL are not valid, + where msg is a message to display to the user. + + If the Talker does not use an API key it should validate that the URL works. + If the Talker does not use an API key or URL it should check that the source is available. """ raise NotImplementedError @@ -166,10 +167,14 @@ class ComicTalker: """ This function should return a list of series that match the given series name according to the source the Talker uses. + Sanitizing the series name is the responsibility of the talker. + If `literal` == True then it is requested that no filtering or transformation/sanitizing of the title or results be performed by the talker. + A sensible amount of results should be returned. + For example the `ComicVineTalker` stops requesting new pages after the results become too different from the `series_name` by use of the `titles_match` function provided by the `comicapi.utils` module, and only allows a maximum of 5 pages @@ -203,7 +208,9 @@ class ComicTalker: """ This function should return a single issue for each series id in the `series_id_list` and it should match the issue_number. + Preferably it should also only return issues published in the given `year`. + If there is no year given (`year` == None) or the Talker does not have issue publication info return the results unfiltered. """ From c3e889279baaa1e8ded7d2034b7d55900cdf0f77 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 27 Feb 2023 22:30:31 -0800 Subject: [PATCH 31/53] Fix EOF --- .github/workflows/package.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index f4d3c71..6eebcc4 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -98,4 +98,3 @@ jobs: dist/*.zip dist/*${{ fromJSON('["never", ""]')[runner.os == 'Linux'] }}.whl dist/*.AppImage - From 4d90417ecfa95d164234c3edd0efd266f1aaa1f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 06:31:07 +0000 Subject: [PATCH 32/53] Update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 318650f..c526237 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,4 @@ Richard Haussmann Mizaki Xavier Jouvenot github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> +Ben Longman From dacd767162234c558c6588f93b84a425ee4defd6 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 28 Feb 2023 14:59:58 +0000 Subject: [PATCH 33/53] String widget fix --- comictaggerlib/ui/talkeruigenerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index d34c63e..68a0dcd 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -219,7 +219,7 @@ def generate_source_option_tabs( # option.type of None should be string elif (option.type is None and option.action is None) or option.type is str: current_widget = generate_textbox(option, layout_grid) - sources["tabs"][tab_name]("widget")[option.internal_name] = current_widget + sources["tabs"][tab_name].widget[option.internal_name] = current_widget else: logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") From 6133b886fb8997b6da86881e58ea8a1bd0df2c38 Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 28 Feb 2023 15:06:59 +0000 Subject: [PATCH 34/53] String widget fix-fix --- comictaggerlib/ui/talkeruigenerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 68a0dcd..eef533e 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -219,7 +219,7 @@ def generate_source_option_tabs( # option.type of None should be string elif (option.type is None and option.action is None) or option.type is str: current_widget = generate_textbox(option, layout_grid) - sources["tabs"][tab_name].widget[option.internal_name] = current_widget + sources["tabs"][tab_name].widgets[option.internal_name] = current_widget else: logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") From f0b9bc6c77e5d4dc43b16afc107815e09c4d5a6b Mon Sep 17 00:00:00 2001 From: Mizaki Date: Tue, 28 Feb 2023 15:37:52 +0000 Subject: [PATCH 35/53] Missed name changes from options move --- comictaggerlib/autotagstartwindow.py | 4 ++-- comictaggerlib/cli.py | 8 ++++---- comictaggerlib/seriesselectionwindow.py | 8 ++++---- comictaggerlib/taggerwindow.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 9385ad7..efb3c29 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.talker_auto_imprint) + self.cbxAutoImprint.setChecked(self.config.identifier_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.talker_series_match_search_thresh + self.name_length_match_tolerance = self.config.identifier_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 7614dfc..c79299c 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -114,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.talker_clear_metadata_on_import: + if self.config.identifier_clear_metadata_on_import: md = ct_md else: notes = ( @@ -123,7 +123,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config.talker_auto_imprint: + if self.config.identifier_auto_imprint: md.fix_publisher() self.actual_metadata_save(ca, md) @@ -427,7 +427,7 @@ class CLI: match_results.fetch_data_failures.append(str(ca.path.absolute())) return - if self.config.talker_clear_metadata_on_import: + if self.config.identifier_clear_metadata_on_import: md = ct_md else: notes = ( @@ -436,7 +436,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config.talker_auto_imprint: + if self.config.identifier_auto_imprint: md.fix_publisher() # ok, done building our metadata. time to save diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index ad7e15c..56c8706 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -151,7 +151,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.progdialog: QtWidgets.QProgressDialog | None = None self.search_thread: SearchThread | None = None - self.use_filter = self.config.talker_always_use_publisher_filter + self.use_filter = self.config.identifier_always_use_publisher_filter # Load to retrieve settings self.talker = talker @@ -336,7 +336,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): def perform_query(self, refresh: bool = False) -> None: self.search_thread = SearchThread( - self.talker, self.series_name, refresh, self.literal, self.config.talker_series_match_search_thresh + self.talker, self.series_name, refresh, self.literal, self.config.identifier_series_match_search_thresh ) self.search_thread.searchComplete.connect(self.search_complete) self.search_thread.progressUpdate.connect(self.search_progress_update) @@ -403,7 +403,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.talker_sort_series_by_year: + if self.config.identifier_sort_series_by_year: try: self.ct_search_results = sorted( self.ct_search_results, @@ -421,7 +421,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.talker_exact_series_matches_first: + if self.config.identifier_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/taggerwindow.py b/comictaggerlib/taggerwindow.py index c395047..2b5acdc 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1071,7 +1071,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if self.config[0].cbl_apply_transform_on_import: new_metadata = CBLTransformer(new_metadata, self.config[0]).apply() - if self.config[0].talker_clear_form_before_populating: + if self.config[0].identifier_clear_form_before_populating: self.clear_form() notes = ( @@ -1785,7 +1785,7 @@ class TaggerWindow(QtWidgets.QMainWindow): ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config[0].talker_auto_imprint: + if self.config[0].identifier_auto_imprint: md.fix_publisher() if not ca.write_metadata(md, self.save_data_style): From 33796aa4756d9ce91219dbc036dcbacc7ae25e68 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 6 Apr 2023 10:48:40 -0700 Subject: [PATCH 36/53] Fix #447 --- comictaggerlib/cli.py | 22 +++++++++------------- comictaggerlib/renamewindow.py | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index c79299c..ea9828d 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -458,18 +458,18 @@ class CLI: return new_ext = "" # default - if self.config.filename_rename_set_extension_based_on_archive: + if self.config.rename_set_extension_based_on_archive: new_ext = ca.extension() renamer = FileRenamer( md, - platform="universal" if self.config.filename_rename_strict else "auto", + platform="universal" if self.config.rename_strict else "auto", replacements=self.config.rename_replacements, ) - renamer.set_template(self.config.filename_rename_template) - renamer.set_issue_zero_padding(self.config.filename_rename_issue_number_padding) - renamer.set_smart_cleanup(self.config.filename_rename_use_smart_string_cleanup) - renamer.move = self.config.filename_rename_move_to_dir + renamer.set_template(self.config.rename_template) + renamer.set_issue_zero_padding(self.config.rename_issue_number_padding) + renamer.set_smart_cleanup(self.config.rename_use_smart_string_cleanup) + renamer.move = self.config.rename_move_to_dir try: new_name = renamer.determine_name(ext=new_ext) @@ -481,17 +481,13 @@ class 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", - self.config.filename_rename_template, + self.config.rename_template, ) return except Exception: - logger.exception( - "Formatter failure: %s metadata: %s", self.config.filename_rename_template, renamer.metadata - ) + logger.exception("Formatter failure: %s metadata: %s", self.config.rename_template, renamer.metadata) - folder = get_rename_dir( - ca, self.config.filename_rename_dir if self.config.filename_rename_move_to_dir else None - ) + folder = get_rename_dir(ca, self.config.rename_dir if self.config.rename_move_to_dir else None) full_path = folder / new_name diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 4f0eb4c..9bc31a4 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -73,7 +73,7 @@ class RenameWindow(QtWidgets.QDialog): self.renamer.replacements = self.config[0].rename_replacements new_ext = ca.path.suffix # default - if self.config[0].filename_rename_set_extension_based_on_archive: + if self.config[0].rename_set_extension_based_on_archive: new_ext = ca.extension() if md is None: From e2dfcc91ce39b0526ddd45d624963472118c3c0f Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 13 Apr 2023 20:58:30 -0600 Subject: [PATCH 37/53] Revert get_recursive_filelist Fixes #449 --- comicapi/utils.py | 9 +++++---- tests/utils_test.py | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/comicapi/utils.py b/comicapi/utils.py index ad54ed7..797b969 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -14,7 +14,6 @@ # limitations under the License. from __future__ import annotations -import glob import json import logging import os @@ -65,9 +64,11 @@ def get_recursive_filelist(pathlist: list[str]) -> list[str]: filelist: list[str] = [] for p in pathlist: if os.path.isdir(p): - filelist.extend(x for x in glob.glob(f"{p}{os.sep}/**", recursive=True) if not os.path.isdir(x)) - elif str(p) not in filelist: - filelist.append(str(p)) + for root, _, files in os.walk(p): + for f in files: + filelist.append(os.path.join(root, f)) + else: + filelist.append(p) return filelist diff --git a/tests/utils_test.py b/tests/utils_test.py index ba525ee..b5d00d7 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -27,8 +27,11 @@ def test_recursive_list_with_file(tmp_path) -> None: temp_txt2 = tmp_path / "info2.txt" temp_txt2.write_text("this is here") + glob_in_name = tmp_path / "[e-b]" + glob_in_name.mkdir() + expected_result = {str(foo_png), str(temp_cbr), str(temp_file), str(temp_txt), str(temp_txt2)} - result = set(comicapi.utils.get_recursive_filelist([str(temp_txt2), tmp_path])) + result = set(comicapi.utils.get_recursive_filelist([str(temp_txt2), tmp_path, str(glob_in_name)])) assert result == expected_result From bf5503769024b8c6286fea37d4df718b32f38fa2 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 18 Apr 2023 21:03:50 -0700 Subject: [PATCH 38/53] Make PyICU optional Fix more locale issues Update README.md --- README.md | 32 +++++++++++++--------- comicapi/comicarchive.py | 3 +- comicapi/utils.py | 50 ++++++++++++++++++++++++++++++++-- comictaggerlib/gui.py | 8 +++++- comictaggerlib/imagefetcher.py | 4 +++ comictaggerlib/main.py | 5 ++++ comictaggerlib/ui/qtutils.py | 4 +++ requirements-ICU.txt | 1 + requirements.txt | 1 - tests/comicarchive_test.py | 2 +- tests/utils_test.py | 40 +++++++++++++++++++++++++++ 11 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 requirements-ICU.txt diff --git a/README.md b/README.md index c276be0..3f3d51e 100644 --- a/README.md +++ b/README.md @@ -33,21 +33,13 @@ For details, screen-shots, and more, visit [the Wiki](https://github.com/comicta ## Installation +It is reccomended to either use the Binaries or a package manager for the best experience + ### Binaries -Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). +Windows, Linux and MacOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). -Just unzip the archive in any folder and run, no additional installation steps are required. - -### PIP installation - -A pip package is provided, you can install it with: - -``` - $ pip3 install comictagger[GUI] -``` - -There are two optional dependencies GUI and CBR. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI]` +Unzip the archive in any folder and run, no additional installation steps are required. ### Chocolatey installation (Windows only) @@ -55,12 +47,26 @@ A [Chocolatey package](https://community.chocolatey.org/packages/comictagger), m ```powershell choco install comictagger ``` + +### PyPi installation + +! Please note that pages may not be sorted correctly if ICU is not installed on Linux and MacOS. +If you have issues installing ICU please see the PyICU documentation [here](https://pyicu.org) + +A PyPi package is provided, you can install it with: + +``` + $ pip3 install comictagger[GUI,ICU] +``` + +There are several optional dependencies GUI, CBR, 7Z and ICU. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI,ICU]` + ### From source 1. Ensure you have python 3.9 installed 2. Clone this repository `git clone https://github.com/comictagger/comictagger.git` 3. `pip3 install -r requirements_dev.txt` - 7. `pip3 install .` or `pip3 install .[GUI]` + 7. `pip3 install .` or `pip3 install .[GUI,ICU]` ## Contributors diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 57bc0a7..b225bba 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -22,7 +22,6 @@ import shutil import sys from typing import cast -import natsort import wordninja from comicapi import filenamelexer, filenameparser, utils @@ -280,7 +279,7 @@ class ComicArchive: # seems like some archive creators are on Windows, and don't know about case-sensitivity! if sort_list: - files = cast(list[str], natsort.os_sorted(files)) + files = cast(list[str], utils.os_sorted(files)) # make a sub-list of image files self.page_list = [] diff --git a/comicapi/utils.py b/comicapi/utils.py index 797b969..f44b765 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -15,25 +15,69 @@ from __future__ import annotations import json +import locale import logging import os import pathlib import unicodedata from collections import defaultdict -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from shutil import which # noqa: F401 from typing import Any +import natsort import pycountry import rapidfuzz.fuzz import comicapi.data +try: + import icu + + del icu + icu_available = True +except ImportError: + icu_available = False + logger = logging.getLogger(__name__) -class UtilsVars: - already_fixed_encoding = False +def _custom_key(tup): + lst = [] + for x in natsort.os_sort_keygen()(tup): + ret = x + if len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "": + ret = (str(x[1]), *x[1:]) + + lst.append(ret) + return tuple(lst) + + +def os_sorted(lst: Iterable) -> Iterable: + if icu_available: + raise Exception("fuck off") + return natsort.os_sorted(lst) + return sorted(lst, key=_custom_key) + + +def save_locale() -> dict[int, tuple[str | None, str | None]]: + locales: dict[int, tuple[str | None, str | None]] = { + locale.LC_ALL: (None, None), + locale.LC_COLLATE: (None, None), + locale.LC_CTYPE: (None, None), + locale.LC_MESSAGES: (None, None), + locale.LC_MONETARY: (None, None), + locale.LC_NUMERIC: (None, None), + locale.LC_TIME: (None, None), + } + for x in locales: + locales[x] = locale.getlocale(x) + return locales + + +def set_locale(locales: dict[int, tuple[str | None, str | None]]) -> None: + for x, value in locales.items(): + locale.setlocale(x, value) def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) -> str: diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py index 6f41e88..3d45285 100644 --- a/comictaggerlib/gui.py +++ b/comictaggerlib/gui.py @@ -9,14 +9,20 @@ import types import settngs +from comicapi.utils import save_locale, set_locale from comictaggerlib.graphics import graphics_path from comictalker.comictalker import ComicTalker logger = logging.getLogger("comictagger") + try: - qt_available = True + loc = save_locale() from PyQt5 import QtCore, QtGui, QtWidgets + qt_available = True + + set_locale(loc) + 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. diff --git a/comictaggerlib/imagefetcher.py b/comictaggerlib/imagefetcher.py index 779340f..6f23bdd 100644 --- a/comictaggerlib/imagefetcher.py +++ b/comictaggerlib/imagefetcher.py @@ -25,11 +25,15 @@ import tempfile import requests +from comicapi.utils import save_locale, set_locale from comictaggerlib import ctversion try: + loc = save_locale() from PyQt5 import QtCore, QtNetwork + set_locale(loc) + qt_available = True except ImportError: qt_available = False diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 3b0a6f0..28a4da8 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -25,6 +25,7 @@ import settngs import comicapi import comictalker +from comicapi.utils import save_locale, set_locale from comictaggerlib import cli, ctsettings from comictaggerlib.ctversion import version from comictaggerlib.log import setup_logging @@ -36,9 +37,13 @@ else: logger = logging.getLogger("comictagger") + try: + loc = save_locale() from comictaggerlib import gui + set_locale(loc) + qt_available = gui.qt_available except Exception: logger.exception("Qt unavailable") diff --git a/comictaggerlib/ui/qtutils.py b/comictaggerlib/ui/qtutils.py index c3ce3dc..dbef9f9 100644 --- a/comictaggerlib/ui/qtutils.py +++ b/comictaggerlib/ui/qtutils.py @@ -6,13 +6,17 @@ import io import logging import traceback +from comicapi.utils import save_locale, set_locale from comictaggerlib.graphics import graphics_path logger = logging.getLogger(__name__) try: + loc = save_locale() from PyQt5 import QtGui, QtWidgets + set_locale(loc) + qt_available = True except ImportError: qt_available = False diff --git a/requirements-ICU.txt b/requirements-ICU.txt new file mode 100644 index 0000000..80c4759 --- /dev/null +++ b/requirements-ICU.txt @@ -0,0 +1 @@ +pyicu; sys_platform == 'linux' or sys_platform == 'darwin' diff --git a/requirements.txt b/requirements.txt index 7a936ec..dde80d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ natsort>=8.1.0 pathvalidate pillow>=9.1.0, <10 pycountry -#pyicu; sys_platform == 'linux' or sys_platform == 'darwin' rapidfuzz>=2.12.0 requests==2.* settngs==0.6.2 diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index cec116a..bad70f0 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -12,7 +12,7 @@ from testing.filenames import datadir @pytest.mark.xfail(not comicapi.archivers.rar.rar_support, reason="rar support") -def test_getPageNameList(): +def test_get_page_name_list(): c = comicapi.comicarchive.ComicArchive(datadir / "fake_cbr.cbr") assert c.seems_to_be_a_comic_archive() pageNameList = c.get_page_name_list() diff --git a/tests/utils_test.py b/tests/utils_test.py index b5d00d7..a9bcda4 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -7,6 +7,46 @@ import pytest import comicapi.utils +def test_os_sorted(): + page_name_list = [ + "cover.jpg", + "Page1.jpeg", + "!cover.jpg", + "page4.webp", + "test/!cover.tar.gz", + "!cover.tar.gz", + "00.jpg", + "ignored.txt", + "page0.jpg", + "test/00.tar.gz", + ".ignored.jpg", + "Page3.gif", + "!cover.tar.gz", + "Page2.png", + "page10.jpg", + "!cover", + ] + + assert comicapi.utils.os_sorted(page_name_list) == [ + "!cover", + "!cover.jpg", + "!cover.tar.gz", + "!cover.tar.gz", # Depending on locale punctuation or numbers might come first (Linux, MacOS) + ".ignored.jpg", + "00.jpg", + "cover.jpg", + "ignored.txt", + "page0.jpg", + "Page1.jpeg", + "Page2.png", + "Page3.gif", + "page4.webp", + "page10.jpg", + "test/!cover.tar.gz", + "test/00.tar.gz", + ] + + def test_recursive_list_with_file(tmp_path) -> None: foo_png = tmp_path / "foo.png" foo_png.write_text("not a png") From c1aba269a9a05fdf7a1044d180ac5be34a319c1e Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 22 Apr 2023 21:28:14 -0700 Subject: [PATCH 39/53] Revert "Make PyICU optional" This reverts commit bf5503769024b8c6286fea37d4df718b32f38fa2. --- README.md | 32 +++++++++------------- comicapi/comicarchive.py | 3 +- comicapi/utils.py | 50 ++-------------------------------- comictaggerlib/gui.py | 8 +----- comictaggerlib/imagefetcher.py | 4 --- comictaggerlib/main.py | 5 ---- comictaggerlib/ui/qtutils.py | 4 --- requirements-ICU.txt | 1 - requirements.txt | 1 + tests/comicarchive_test.py | 2 +- tests/utils_test.py | 40 --------------------------- 11 files changed, 21 insertions(+), 129 deletions(-) delete mode 100644 requirements-ICU.txt diff --git a/README.md b/README.md index 3f3d51e..c276be0 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,21 @@ For details, screen-shots, and more, visit [the Wiki](https://github.com/comicta ## Installation -It is reccomended to either use the Binaries or a package manager for the best experience - ### Binaries -Windows, Linux and MacOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). +Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). -Unzip the archive in any folder and run, no additional installation steps are required. +Just unzip the archive in any folder and run, no additional installation steps are required. + +### PIP installation + +A pip package is provided, you can install it with: + +``` + $ pip3 install comictagger[GUI] +``` + +There are two optional dependencies GUI and CBR. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI]` ### Chocolatey installation (Windows only) @@ -47,26 +55,12 @@ A [Chocolatey package](https://community.chocolatey.org/packages/comictagger), m ```powershell choco install comictagger ``` - -### PyPi installation - -! Please note that pages may not be sorted correctly if ICU is not installed on Linux and MacOS. -If you have issues installing ICU please see the PyICU documentation [here](https://pyicu.org) - -A PyPi package is provided, you can install it with: - -``` - $ pip3 install comictagger[GUI,ICU] -``` - -There are several optional dependencies GUI, CBR, 7Z and ICU. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI,ICU]` - ### From source 1. Ensure you have python 3.9 installed 2. Clone this repository `git clone https://github.com/comictagger/comictagger.git` 3. `pip3 install -r requirements_dev.txt` - 7. `pip3 install .` or `pip3 install .[GUI,ICU]` + 7. `pip3 install .` or `pip3 install .[GUI]` ## Contributors diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index b225bba..57bc0a7 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -22,6 +22,7 @@ import shutil import sys from typing import cast +import natsort import wordninja from comicapi import filenamelexer, filenameparser, utils @@ -279,7 +280,7 @@ class ComicArchive: # seems like some archive creators are on Windows, and don't know about case-sensitivity! if sort_list: - files = cast(list[str], utils.os_sorted(files)) + files = cast(list[str], natsort.os_sorted(files)) # make a sub-list of image files self.page_list = [] diff --git a/comicapi/utils.py b/comicapi/utils.py index f44b765..797b969 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -15,69 +15,25 @@ from __future__ import annotations import json -import locale import logging import os import pathlib import unicodedata from collections import defaultdict -from collections.abc import Iterable, Mapping +from collections.abc import Mapping from shutil import which # noqa: F401 from typing import Any -import natsort import pycountry import rapidfuzz.fuzz import comicapi.data -try: - import icu - - del icu - icu_available = True -except ImportError: - icu_available = False - logger = logging.getLogger(__name__) -def _custom_key(tup): - lst = [] - for x in natsort.os_sort_keygen()(tup): - ret = x - if len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "": - ret = (str(x[1]), *x[1:]) - - lst.append(ret) - return tuple(lst) - - -def os_sorted(lst: Iterable) -> Iterable: - if icu_available: - raise Exception("fuck off") - return natsort.os_sorted(lst) - return sorted(lst, key=_custom_key) - - -def save_locale() -> dict[int, tuple[str | None, str | None]]: - locales: dict[int, tuple[str | None, str | None]] = { - locale.LC_ALL: (None, None), - locale.LC_COLLATE: (None, None), - locale.LC_CTYPE: (None, None), - locale.LC_MESSAGES: (None, None), - locale.LC_MONETARY: (None, None), - locale.LC_NUMERIC: (None, None), - locale.LC_TIME: (None, None), - } - for x in locales: - locales[x] = locale.getlocale(x) - return locales - - -def set_locale(locales: dict[int, tuple[str | None, str | None]]) -> None: - for x, value in locales.items(): - locale.setlocale(x, value) +class UtilsVars: + already_fixed_encoding = False def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) -> str: diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py index 3d45285..6f41e88 100644 --- a/comictaggerlib/gui.py +++ b/comictaggerlib/gui.py @@ -9,19 +9,13 @@ import types import settngs -from comicapi.utils import save_locale, set_locale from comictaggerlib.graphics import graphics_path from comictalker.comictalker import ComicTalker logger = logging.getLogger("comictagger") - try: - loc = save_locale() - from PyQt5 import QtCore, QtGui, QtWidgets - qt_available = True - - set_locale(loc) + 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. diff --git a/comictaggerlib/imagefetcher.py b/comictaggerlib/imagefetcher.py index 6f23bdd..779340f 100644 --- a/comictaggerlib/imagefetcher.py +++ b/comictaggerlib/imagefetcher.py @@ -25,15 +25,11 @@ import tempfile import requests -from comicapi.utils import save_locale, set_locale from comictaggerlib import ctversion try: - loc = save_locale() from PyQt5 import QtCore, QtNetwork - set_locale(loc) - qt_available = True except ImportError: qt_available = False diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 28a4da8..3b0a6f0 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -25,7 +25,6 @@ import settngs import comicapi import comictalker -from comicapi.utils import save_locale, set_locale from comictaggerlib import cli, ctsettings from comictaggerlib.ctversion import version from comictaggerlib.log import setup_logging @@ -37,13 +36,9 @@ else: logger = logging.getLogger("comictagger") - try: - loc = save_locale() from comictaggerlib import gui - set_locale(loc) - qt_available = gui.qt_available except Exception: logger.exception("Qt unavailable") diff --git a/comictaggerlib/ui/qtutils.py b/comictaggerlib/ui/qtutils.py index dbef9f9..c3ce3dc 100644 --- a/comictaggerlib/ui/qtutils.py +++ b/comictaggerlib/ui/qtutils.py @@ -6,17 +6,13 @@ import io import logging import traceback -from comicapi.utils import save_locale, set_locale from comictaggerlib.graphics import graphics_path logger = logging.getLogger(__name__) try: - loc = save_locale() from PyQt5 import QtGui, QtWidgets - set_locale(loc) - qt_available = True except ImportError: qt_available = False diff --git a/requirements-ICU.txt b/requirements-ICU.txt deleted file mode 100644 index 80c4759..0000000 --- a/requirements-ICU.txt +++ /dev/null @@ -1 +0,0 @@ -pyicu; sys_platform == 'linux' or sys_platform == 'darwin' diff --git a/requirements.txt b/requirements.txt index dde80d6..7a936ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ natsort>=8.1.0 pathvalidate pillow>=9.1.0, <10 pycountry +#pyicu; sys_platform == 'linux' or sys_platform == 'darwin' rapidfuzz>=2.12.0 requests==2.* settngs==0.6.2 diff --git a/tests/comicarchive_test.py b/tests/comicarchive_test.py index bad70f0..cec116a 100644 --- a/tests/comicarchive_test.py +++ b/tests/comicarchive_test.py @@ -12,7 +12,7 @@ from testing.filenames import datadir @pytest.mark.xfail(not comicapi.archivers.rar.rar_support, reason="rar support") -def test_get_page_name_list(): +def test_getPageNameList(): c = comicapi.comicarchive.ComicArchive(datadir / "fake_cbr.cbr") assert c.seems_to_be_a_comic_archive() pageNameList = c.get_page_name_list() diff --git a/tests/utils_test.py b/tests/utils_test.py index a9bcda4..b5d00d7 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -7,46 +7,6 @@ import pytest import comicapi.utils -def test_os_sorted(): - page_name_list = [ - "cover.jpg", - "Page1.jpeg", - "!cover.jpg", - "page4.webp", - "test/!cover.tar.gz", - "!cover.tar.gz", - "00.jpg", - "ignored.txt", - "page0.jpg", - "test/00.tar.gz", - ".ignored.jpg", - "Page3.gif", - "!cover.tar.gz", - "Page2.png", - "page10.jpg", - "!cover", - ] - - assert comicapi.utils.os_sorted(page_name_list) == [ - "!cover", - "!cover.jpg", - "!cover.tar.gz", - "!cover.tar.gz", # Depending on locale punctuation or numbers might come first (Linux, MacOS) - ".ignored.jpg", - "00.jpg", - "cover.jpg", - "ignored.txt", - "page0.jpg", - "Page1.jpeg", - "Page2.png", - "Page3.gif", - "page4.webp", - "page10.jpg", - "test/!cover.tar.gz", - "test/00.tar.gz", - ] - - def test_recursive_list_with_file(tmp_path) -> None: foo_png = tmp_path / "foo.png" foo_png.write_text("not a png") From e70c47d12af8f0bb5b5469ac50b993ea4b20ed24 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 22 Apr 2023 22:00:26 -0700 Subject: [PATCH 40/53] Make PyICU optional Update README.md --- comicapi/comicarchive.py | 3 +-- comicapi/utils.py | 30 ++++++++++++++++++++--- comictagger.py | 10 -------- comictagger.spec | 2 +- comictaggerlib/__main__.py | 5 ++++ comictaggerlib/gui.py | 4 +-- comictaggerlib/main.py | 48 ++++++++++++++++++++++++++++++++++++ localefix.py | 50 -------------------------------------- setup.py | 2 +- tests/utils_test.py | 40 ++++++++++++++++++++++++++++++ 10 files changed, 125 insertions(+), 69 deletions(-) delete mode 100755 comictagger.py create mode 100644 comictaggerlib/__main__.py delete mode 100644 localefix.py diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 57bc0a7..b225bba 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -22,7 +22,6 @@ import shutil import sys from typing import cast -import natsort import wordninja from comicapi import filenamelexer, filenameparser, utils @@ -280,7 +279,7 @@ class ComicArchive: # seems like some archive creators are on Windows, and don't know about case-sensitivity! if sort_list: - files = cast(list[str], natsort.os_sorted(files)) + files = cast(list[str], utils.os_sorted(files)) # make a sub-list of image files self.page_list = [] diff --git a/comicapi/utils.py b/comicapi/utils.py index 797b969..699e6d4 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -18,22 +18,46 @@ import json import logging import os import pathlib +import platform import unicodedata from collections import defaultdict -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from shutil import which # noqa: F401 from typing import Any +import natsort import pycountry import rapidfuzz.fuzz import comicapi.data +try: + import icu + + del icu + icu_available = True +except ImportError: + icu_available = False + logger = logging.getLogger(__name__) -class UtilsVars: - already_fixed_encoding = False +def _custom_key(tup): + lst = [] + for x in natsort.os_sort_keygen()(tup): + ret = x + if len(x) > 1 and isinstance(x[1], int) and isinstance(x[0], str) and x[0] == "": + ret = ("a", *x[1:]) + + lst.append(ret) + return tuple(lst) + + +def os_sorted(lst: Iterable) -> Iterable: + key = _custom_key + if icu_available or platform.system() == "Windows": + key = natsort.os_sort_keygen() + return sorted(lst, key=key) def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) -> str: diff --git a/comictagger.py b/comictagger.py deleted file mode 100755 index 17c515d..0000000 --- a/comictagger.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import localefix -from comictaggerlib.main import App - -if __name__ == "__main__": - localefix.configure_locale() - - App().run() diff --git a/comictagger.spec b/comictagger.spec index 413c6e4..fa199b9 100644 --- a/comictagger.spec +++ b/comictagger.spec @@ -9,7 +9,7 @@ block_cipher = None a = Analysis( - ["comictagger.py"], + ["comictaggerlib/__main__.py"], pathex=[], binaries=[], datas=[], diff --git a/comictaggerlib/__main__.py b/comictaggerlib/__main__.py new file mode 100644 index 0000000..9b7d9ab --- /dev/null +++ b/comictaggerlib/__main__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from comictaggerlib.main import main + +main() diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py index 6f41e88..541c647 100644 --- a/comictaggerlib/gui.py +++ b/comictaggerlib/gui.py @@ -73,12 +73,12 @@ try: return True return super().event(event) -except ImportError as e: +except ImportError: def show_exception_box(log_msg: str) -> None: ... - logger.error(str(e)) + logger.exception("Qt unavailable") qt_available = False diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 3b0a6f0..ea8b070 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -17,8 +17,12 @@ from __future__ import annotations import argparse import json +import locale +import logging import logging.handlers +import os import signal +import subprocess import sys import settngs @@ -48,6 +52,49 @@ except Exception: logger.setLevel(logging.DEBUG) +def _lang_code_mac() -> str: + """ + stolen from https://github.com/mu-editor/mu + Returns the user's language preference as defined in the Language & Region + preference pane in macOS's System Preferences. + """ + + # Uses the shell command `defaults read -g AppleLocale` that prints out a + # language code to standard output. Assumptions about the command: + # - It exists and is in the shell's PATH. + # - It accepts those arguments. + # - It returns a usable language code. + # + # Reference documentation: + # - The man page for the `defaults` command on macOS. + # - The macOS underlying API: + # https://developer.apple.com/documentation/foundation/nsuserdefaults. + + lang_detect_command = "defaults read -g AppleLocale" + + status, output = subprocess.getstatusoutput(lang_detect_command) + if status == 0: + # Command was successful. + lang_code = output + else: + logging.warning("Language detection command failed: %r", output) + lang_code = "" + + return lang_code + + +def configure_locale() -> None: + if sys.platform == "darwin" and "LANG" not in os.environ: + code = _lang_code_mac() + if code != "": + os.environ["LANG"] = f"{code}.utf-8" + + locale.setlocale(locale.LC_ALL, "") + sys.stdout.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] + sys.stderr.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] + sys.stdin.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] + + def update_publishers(config: settngs.Config[settngs.Namespace]) -> None: json_file = config[0].runtime_config.user_config_dir / "publishers.json" if json_file.exists(): @@ -66,6 +113,7 @@ class App: self.config_load_success = False def run(self) -> None: + configure_locale() conf = self.initialize() self.initialize_dirs(conf.config) self.load_plugins(conf) diff --git a/localefix.py b/localefix.py deleted file mode 100644 index b7bf897..0000000 --- a/localefix.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import locale -import logging -import os -import subprocess -import sys - - -def _lang_code_mac() -> str: - """ - stolen from https://github.com/mu-editor/mu - Returns the user's language preference as defined in the Language & Region - preference pane in macOS's System Preferences. - """ - - # Uses the shell command `defaults read -g AppleLocale` that prints out a - # language code to standard output. Assumptions about the command: - # - It exists and is in the shell's PATH. - # - It accepts those arguments. - # - It returns a usable language code. - # - # Reference documentation: - # - The man page for the `defaults` command on macOS. - # - The macOS underlying API: - # https://developer.apple.com/documentation/foundation/nsuserdefaults. - - lang_detect_command = "defaults read -g AppleLocale" - - status, output = subprocess.getstatusoutput(lang_detect_command) - if status == 0: - # Command was successful. - lang_code = output - else: - logging.warning("Language detection command failed: %r", output) - lang_code = "" - - return lang_code - - -def configure_locale() -> None: - if sys.platform == "darwin" and "LANG" not in os.environ: - code = _lang_code_mac() - if code != "": - os.environ["LANG"] = f"{code}.utf-8" - - locale.setlocale(locale.LC_ALL, "") - sys.stdout.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] - sys.stderr.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] - sys.stdin.reconfigure(encoding=sys.getdefaultencoding()) # type: ignore[attr-defined] diff --git a/setup.py b/setup.py index b2cb90a..5ff775b 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ install_requires = read("requirements.txt").splitlines() extras_require = {} extra_req_files = glob.glob("requirements-*.txt") for extra_req_file in extra_req_files: - name = os.path.splitext(extra_req_file)[0].replace("requirements-", "", 1) + name = os.path.splitext(extra_req_file)[0].removeprefix("requirements-") extras_require[name] = read(extra_req_file).splitlines() # If there are any extras, add a catch-all case that includes everything. diff --git a/tests/utils_test.py b/tests/utils_test.py index b5d00d7..a9bcda4 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -7,6 +7,46 @@ import pytest import comicapi.utils +def test_os_sorted(): + page_name_list = [ + "cover.jpg", + "Page1.jpeg", + "!cover.jpg", + "page4.webp", + "test/!cover.tar.gz", + "!cover.tar.gz", + "00.jpg", + "ignored.txt", + "page0.jpg", + "test/00.tar.gz", + ".ignored.jpg", + "Page3.gif", + "!cover.tar.gz", + "Page2.png", + "page10.jpg", + "!cover", + ] + + assert comicapi.utils.os_sorted(page_name_list) == [ + "!cover", + "!cover.jpg", + "!cover.tar.gz", + "!cover.tar.gz", # Depending on locale punctuation or numbers might come first (Linux, MacOS) + ".ignored.jpg", + "00.jpg", + "cover.jpg", + "ignored.txt", + "page0.jpg", + "Page1.jpeg", + "Page2.png", + "Page3.gif", + "page4.webp", + "page10.jpg", + "test/!cover.tar.gz", + "test/00.tar.gz", + ] + + def test_recursive_list_with_file(tmp_path) -> None: foo_png = tmp_path / "foo.png" foo_png.write_text("not a png") From 5b3e9c9026fe26c621dc603a9033efee4040c1d4 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 22 Apr 2023 10:49:47 -0700 Subject: [PATCH 41/53] Switch to rarfile for rar/cbr support --- comicapi/archivers/rar.py | 6 +++--- requirements-CBR.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/comicapi/archivers/rar.py b/comicapi/archivers/rar.py index 726404a..1dc2df8 100644 --- a/comicapi/archivers/rar.py +++ b/comicapi/archivers/rar.py @@ -12,7 +12,7 @@ import time from comicapi.archivers import Archiver try: - from unrar.cffi import rarfile + import rarfile rar_support = True except ImportError: @@ -22,7 +22,7 @@ except ImportError: logger = logging.getLogger(__name__) if not rar_support: - logger.error("unrar-cffi unavailable") + logger.error("rar unavailable") class RarArchiver(Archiver): @@ -43,7 +43,7 @@ class RarArchiver(Archiver): def get_comment(self) -> str: rarc = self.get_rar_obj() - return rarc.comment.decode("utf-8") if rarc else "" + return (rarc.comment if rarc else "") or "" def set_comment(self, comment: str) -> bool: if rar_support and self.exe: diff --git a/requirements-CBR.txt b/requirements-CBR.txt index 0e3a2cd..f5c5f0a 100644 --- a/requirements-CBR.txt +++ b/requirements-CBR.txt @@ -1 +1 @@ -unrar-cffi>=0.2.2 +rarfile>=4.0 From c4b7411261223b2fcdb8b7f909f57644cdba6054 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sat, 22 Apr 2023 17:54:58 -0700 Subject: [PATCH 42/53] Use tox for building --- .flake8 | 6 - .github/workflows/build.yaml | 67 ++--- .github/workflows/package.yaml | 57 +--- .pre-commit-config.yaml | 28 +- .travis.yml | 59 ---- CONTRIBUTING.md | 75 ++--- MANIFEST.in | 7 - Makefile | 76 ----- README.md | 14 +- .../linux => build-tools}/ComicTagger.desktop | 2 +- .../comictagger.spec | 4 +- build-tools/get_appimage.py | 26 ++ {mac => build-tools/mac}/Makefile | 0 {mac => build-tools/mac}/app.icns | Bin {mac => build-tools/mac}/make_thin.sh | 0 {mac => build-tools/mac}/volume.icns | Bin {windows => build-tools/windows}/app.ico | Bin build-tools/zip_artifacts.py | 47 +++ comictaggerlib/filerenamer.py | 5 +- desktop-integration/mac/ComicTagger | 4 - desktop-integration/mac/Info.plist | 32 -- desktop-integration/mac/main.sh | 17 -- .../windows/ComicTagger-pip.lnk | 4 - requirements-7Z.txt | 1 - requirements-CBR.txt | 1 - requirements-GUI.txt | 1 - requirements.txt | 14 - requirements_dev.txt | 12 - setup.cfg | 281 ++++++++++++++++++ setup.py | 91 +----- 30 files changed, 437 insertions(+), 494 deletions(-) delete mode 100644 .flake8 delete mode 100644 .travis.yml delete mode 100644 MANIFEST.in delete mode 100644 Makefile rename {desktop-integration/linux => build-tools}/ComicTagger.desktop (92%) rename comictagger.spec => build-tools/comictagger.spec (98%) create mode 100644 build-tools/get_appimage.py rename {mac => build-tools/mac}/Makefile (100%) rename {mac => build-tools/mac}/app.icns (100%) rename {mac => build-tools/mac}/make_thin.sh (100%) rename {mac => build-tools/mac}/volume.icns (100%) rename {windows => build-tools/windows}/app.ico (100%) create mode 100644 build-tools/zip_artifacts.py delete mode 100644 desktop-integration/mac/ComicTagger delete mode 100644 desktop-integration/mac/Info.plist delete mode 100755 desktop-integration/mac/main.sh delete mode 100644 desktop-integration/windows/ComicTagger-pip.lnk delete mode 100644 requirements-7Z.txt delete mode 100644 requirements-CBR.txt delete mode 100644 requirements-GUI.txt delete mode 100644 requirements.txt delete mode 100644 requirements_dev.txt create mode 100644 setup.cfg diff --git a/.flake8 b/.flake8 deleted file mode 100644 index f2b9c0f..0000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 120 -extend-ignore = E203, E501, A003 -extend-exclude = venv, scripts, build, dist, comictaggerlib/ctversion.py -per-file-ignores = - comictaggerlib/cli.py: T20 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eb5d993..165951a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,8 +1,8 @@ name: CI env: - PIP: pip - PYTHON: python + PKG_CONFIG_PATH: /usr/local/opt/icu4c/lib/pkgconfig + LC_COLLATE: en_US.UTF-8 on: pull_request: push: @@ -23,24 +23,18 @@ jobs: os: [ubuntu-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: syphar/restore-virtualenv@v1.2 - id: cache-virtualenv - - - uses: syphar/restore-pip-download-cache@v1 - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - - name: Install build dependencies run: | - python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt + python -m pip install flake8 - uses: reviewdog/action-setup@v1 with: @@ -57,72 +51,45 @@ jobs: os: [ubuntu-latest, macos-10.15, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: syphar/restore-virtualenv@v1.2 - id: cache-virtualenv - - - uses: syphar/restore-pip-download-cache@v1 - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - - - name: Install build dependencies + - name: Install tox run: | - python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt + python -m pip install --upgrade --upgrade-strategy eager tox - - name: Install Windows build dependencies - run: | - choco install -y zip - if: runner.os == 'Windows' - name: Install macos dependencies run: | brew install icu4c pkg-config - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'macOS' + - name: Install linux dependencies run: | sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2 - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'Linux' - name: Build and install PyPi packages run: | - make clean pydist - python -m pip install "dist/$(python setup.py --fullname)-py3-none-any.whl[all]" - - - name: build - run: | - make dist - - - name: build appimage - run: | - make appimage - if: runner.os == 'Linux' + python -m tox r -m build - name: Archive production artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: "${{ format('ComicTagger-{0}', runner.os) }}" path: | dist/*.zip - - - name: Archive production artifacts - appimage - uses: actions/upload-artifact@v2 - with: - name: "${{ format('ComicTagger-{0}', runner.os) }}" - path: | dist/*.AppImage - name: PyTest run: | - python -m pytest + python -m tox r diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 6eebcc4..96e094e 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -1,8 +1,8 @@ name: Package env: - PIP: pip - PYTHON: python + PKG_CONFIG_PATH: /usr/local/opt/icu4c/lib/pkgconfig + LC_COLLATE: en_US.UTF-8 on: push: tags: @@ -18,66 +18,41 @@ jobs: os: [ubuntu-latest, macos-10.15, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - uses: syphar/restore-virtualenv@v1.2 - id: cache-virtualenv - - - uses: syphar/restore-pip-download-cache@v1 - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - - - name: Install build dependencies + - name: Install tox run: | - python -m pip install --upgrade --upgrade-strategy eager -r requirements_dev.txt + python -m pip install --upgrade --upgrade-strategy eager tox - - name: Install Windows build dependencies - run: | - choco install -y zip if: runner.os == 'Windows' - name: Install macos dependencies run: | brew install icu4c pkg-config - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'macOS' + - name: Install linux dependencies run: | sudo apt-get install pkg-config libicu-dev libqt5gui5 - export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; - export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" - python -m pip install --no-binary=pyicu pyicu + # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; + # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'Linux' - name: Build, Install and Test PyPi packages run: | - make clean pydist - python -m pip install "dist/$(python setup.py --fullname)-py3-none-any.whl[all]" - python -m flake8 - python -m pytest - - - name: "Publish distribution 📦 to PyPI" - if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Linux' - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - packages_dir: dist - - - name: Build PyInstaller package - run: | - make dist - - - name: Build AppImage package - run: | - make appimage - if: runner.os == 'Linux' + python -m tox r + python -m tox r -m release + env: + TWINE_USERNAME=__token__ + TWINE_PASSWORD=${{ secrets.PYPI_API_TOKEN }} - name: Get release name if: startsWith(github.ref, 'refs/tags/') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29226fc..870f477 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,25 +13,25 @@ repos: rev: v2.2.0 hooks: - id: setup-cfg-fmt -- repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort - args: [--af,--add-import, 'from __future__ import annotations'] -- repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 - hooks: - - id: pyupgrade - args: [--py39-plus] -- repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - repo: https://github.com/PyCQA/autoflake rev: v2.0.1 hooks: - id: autoflake args: [-i, --remove-all-unused-imports, --ignore-init-module-imports] +- repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: [--py39-plus] +- repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + args: [--af,--add-import, 'from __future__ import annotations'] +- repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 99a9ce0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,59 +0,0 @@ -language: python -# Only build tags -if: type = pull_request OR tag IS present -branches: - only: - - develop - - /^\d+\.\d+\.\d+.*$/ -env: - global: - - PYTHON=python3 - - PIP=pip3 - - SETUPTOOLS_SCM_PRETEND_VERSION=$TRAVIS_TAG - - MAKE=make -matrix: - include: - - os: linux - python: 3.8 - - name: "Python: 3.7" - os: osx - language: shell - python: 3.7 - env: PYTHON=python3 PIP="python3 -m pip" - cache: - - directories: - - $HOME/Library/Caches/pip - - os: windows - language: bash - env: PATH=/C/Python37:/C/Python37/Scripts:$PATH MAKE=mingw32-make PIP=pip PYTHON=python -before_install: - - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install -y python --version 3.7.9; choco install -y mingw zip; fi -install: - - $PIP install -r requirements_dev.txt - - $PIP install -r requirements-GUI.txt - - $PIP install -r requirements-CBR.txt -script: - - if [ "$TRAVIS_OS_NAME" != "linux" ]; then $MAKE dist ; fi - -deploy: - - name: "$TRAVIS_TAG" - body: Released ComicTagger $TRAVIS_TAG - provider: releases - skip_cleanup: true - api_key: - secure: RgohcOJOfLhXXT12bMWaLwOqhe+ClSCYXjYuUJuWK4/E1fdd1xu1ebdQU+MI/R8cZ0Efz3sr2n3NkO/Aa8gN68xEfuF7RVRMm64P9oPrfZgGdsD6H43rU/6kN8bgaDRmCYpLTfXaJ+/gq0x1QDkhWJuceF2BYEGGvL0BvS/TUsLyjVxs8ujTplLyguXHNEv4/7Yz7SBNZZmUHjBuq/y+l8ds3ra9rSgAVAN1tMXoFKJPv+SNNkpTo5WUNMPzBnN041F1rzqHwYDLog2V7Krp9JkXzheRFdAr51/tJBYzEd8AtYVdYvaIvoO6A4PiTZ7MpsmcZZPAWqLQU00UTm/PhT/LVR+7+f8lOBG07RgNNHB+edjDRz3TAuqyuZl9wURWTZKTPuO49TkZMz7Wm0DRNZHvBm1IXLeSG7Tll2YL1+WpZNZg+Dhro2J1QD3vxDXafhMdTCB4z0q5aKpG93IT0p6oXOO0oEGOPZYbA2c5R3SXWSyqd1E1gdhbVjIZr59h++TEf1zz07tvWHqPuAF/Ly/j+dIcY2wj0EzRWaSASWgUpTnMljAkHtWhqDw4GXGDRkRUWRJl1d0/JyVqCeIdRzDQNl8/q7BcO3F1zqr1PgnYdz0lfwWxL1/ekw2vHOJE/GOdkyvX0aJrnaOV338mjJbfGHYv4ESc9ow1kdtIbiU= - file_glob: true - file: dist/*.zip - draft: true - on: - tags: true - condition: $TRAVIS_OS_NAME != "linux" - - provider: pypi - user: __token__ - password: - secure: h+y5WkE8igf864dnsbGPFvOBkyPkuBYtnDRt+EgxHd71EZnV2YP7ns2Cx12su/SVVDdZCBlmHVtkhl6Jmqy+0rTkSYx+3mlBOqyl8Cj5+BlP/dP7Bdmhs2uLZk2YYL1avbC0A6eoNJFtCkjurnB/jCGE433rvMECWJ5x2HsQTKchCmDAEdAZbRBJrzLFsrIC+6NXW1IJZjd+OojbhLSyVar2Jr32foh6huTcBu/x278V1+zIC/Rwy3W67+3c4aZxYrI47FoYFza0jjFfr3EoSkKYUSByMTIvhWaqB2gIsF0T160jgDd8Lcgej+86ACEuG0v01VE7xoougqlOaJ94eAmapeM7oQXzekSwSAxcK3JQSfgWk/AvPhp07T4pQ8vCZmky6yqvVp1EzfKarTeub1rOnv+qo1znKLrBtOoq6t8pOAeczDdIDs51XT/hxaijpMRCM8vHxN4Kqnc4DY+3KcF7UFyH1ifQJHQe71tLBsM/GnAcJM5/3ykFVGvRJ716p4aa6IoGsdNk6bqlysNh7nURDl+bfm+CDXRkO2jkFwUFNqPHW7JwY6ZFx+b5SM3TzC3obJhfMS7OC37fo2geISOTR0xVie6NvpN6TjNAxFTfDxWJI7yH3Al2w43B3uYDd97WeiN+B+HVWtdaER87IVSRbRqFrRub+V+xrozT0y0= - skip_existing: true - skip_cleanup: true - on: - tags: true - condition: $TRAVIS_OS_NAME = "linux" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f01cd8..1e9e4ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ Please open a [GitHub Pull Request](https://github.com/comictagger/comictagger/p Currently only python 3.9 is supported however 3.10 will probably work if you try it -Those on linux should install `Pillow` from the system package manager if possible and if the GUI and/or the CBR/RAR comicbooks are going to be used `pyqt5` and `unrar-cffi` should be installed from the system package manager +Those on linux should install `Pillow` from the system package manager if possible and if the GUI `pyqt5` should be installed from the system package manager Those on macOS will need to ensure that you are using python3 in x86 mode either by installing an x86 only version of python or using the universal installer and using `python3-intel64` instead of `python3` @@ -50,10 +50,10 @@ Those on macOS will need to ensure that you are using python3 in x86 mode either git clone https://github.com/comictagger/comictagger.git ``` -2. It is preferred to use a virtual env for running from source, adding the `--system-site-packages` allows packages already installed via the system package manager to be used: +2. It is preferred to use a virtual env for running from source: ``` -python3 -m venv --system-site-packages venv +python3 -m venv venv ``` 3. Activate the virtual env: @@ -65,73 +65,34 @@ or if on windows PowerShell . venv/bin/activate.ps1 ``` -4. install dependencies: +4. Install tox: ```bash -pip install -r requirements_dev.txt -r requirements.txt -# if installing optional dependencies -pip install -r requirements-GUI.txt -r requirements-CBR.txt +pip install tox ``` -5. install ComicTagger +5. If you are on an M1 Mac you will need to export two environment variables for tests to pass. ``` -pip install . +export tox_python=python3.9-intel64 +export tox_env=m1env ``` -6. (optionally) run pytest to ensure that their are no failures (xfailed means expected failure) +6. install ComicTagger ``` -$ pytest -============================= test session starts ============================== -platform darwin -- Python 3.9.12, pytest-7.1.1, pluggy-1.0.0 -rootdir: /Users/timmy/build/source/comictagger -collected 61 items - -tests/test_FilenameParser.py ..x......x.xxx.xx....xxxxxx.xx.x..xxxxxxx [ 67%] -tests/test_comicarchive.py x... [ 73%] -tests/test_rename.py ..xxx.xx..XXX.XX [100%] - -================== 27 passed, 29 xfailed, 5 xpassed in 2.68s =================== +tox run -e venv ``` 7. Make your changes -8. run code tools and correct any issues +8. Build to ensure that your changes work: this will produce a binary build in the dist folder ```bash -black . -isort . -flake8 . -pytest +tox run -m build ``` -black: formats all of the code consistently so there are no surprises
+The build runs these formatters and linters automatically + +setup-cfg-fmt: Formats the setup.cfg file +autoflake: Removes unused imports isort: sorts imports so that you can always find where an import is located
+black: formats all of the code consistently so there are no surprises
flake8: checks for code quality and style (warns for unused imports and similar issues)
+mypy: checks the types of variables and functions to catch errors pytest: runs tests for ComicTagger functionality - - -if on mac or linux most of this can be accomplished by running -``` -make install -# or make PYTHON=python3-intel64 install -. venv/bin/activate -make CI -``` -There is also `make check` which will run all of the code tools in a read-only capacity -``` -$ make check -venv/bin/black --check . -All done! ✨ 🍰 ✨ -52 files would be left unchanged. -venv/bin/isort --check . -Skipped 6 files -venv/bin/flake8 . -venv/bin/pytest -============================= test session starts ============================== -platform darwin -- Python 3.9.12, pytest-7.1.1, pluggy-1.0.0 -rootdir: /Users/timmy/build/source/comictagger -collected 61 items - -tests/test_FilenameParser.py ..x......x.xxx.xx....xxxxxx.xx.x..xxxxxxx [ 67%] -tests/test_comicarchive.py x... [ 73%] -tests/test_rename.py ..xxx.xx..XXX.XX [100%] - -================== 27 passed, 29 xfailed, 5 xpassed in 2.68s =================== -``` diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e341d47..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include README.md -include release_notes.txt -include requirements.txt -recursive-include scripts *.py *.txt -recursive-include desktop-integration * -include windows/app.ico -include mac/app.icns diff --git a/Makefile b/Makefile deleted file mode 100644 index 20860fd..0000000 --- a/Makefile +++ /dev/null @@ -1,76 +0,0 @@ -PIP ?= pip3 -PYTHON ?= python3 -VERSION_STR := $(shell $(PYTHON) setup.py --version) - -SITE_PACKAGES := $(shell $(PYTHON) -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') -PACKAGE_PATH = $(SITE_PACKAGES)/comictagger.egg-link - -VENV := $(shell echo $${VIRTUAL_ENV-venv}) -PY3 := $(shell command -v $(PYTHON) 2> /dev/null) -PYTHON_VENV := $(VENV)/bin/python -INSTALL_STAMP := $(VENV)/.install.stamp - - -ifeq ($(OS),Windows_NT) - PYTHON_VENV := $(VENV)/Scripts/python.exe - OS_VERSION=win-$(PROCESSOR_ARCHITECTURE) - APP_NAME=comictagger.exe - FINAL_NAME=ComicTagger-$(VERSION_STR)-$(OS_VERSION).exe -else ifeq ($(shell uname -s),Darwin) - OS_VERSION=osx-$(shell defaults read loginwindow SystemVersionStampAsString)-$(shell uname -m) - APP_NAME=ComicTagger.app - FINAL_NAME=ComicTagger-$(VERSION_STR)-$(OS_VERSION).app -else - APP_NAME=comictagger - FINAL_NAME=ComicTagger-$(VERSION_STR)-$(shell uname -s) -endif - -.PHONY: all clean pydist dist CI check appimage - -all: clean dist - -$(PYTHON_VENV): - @if [ -z $(PY3) ]; then echo "Python 3 could not be found."; exit 2; fi - $(PY3) -m venv $(VENV) - -clean: - find . -maxdepth 4 -type d -name "__pycache__" -print -depth -exec rm -rf {} \; - rm -rf $(PACKAGE_PATH) $(INSTALL_STAMP) build dist MANIFEST comictaggerlib/ctversion.py - $(MAKE) -C mac clean - -CI: install - $(PYTHON_VENV) -m black . - $(PYTHON_VENV) -m isort . - $(PYTHON_VENV) -m flake8 . - $(PYTHON_VENV) -m pytest - -check: install - $(PYTHON_VENV) -m black --check . - $(PYTHON_VENV) -m isort --check . - $(PYTHON_VENV) -m flake8 . - $(PYTHON_VENV) -m pytest - -pydist: - $(PYTHON_VENV) -m build - -install: $(INSTALL_STAMP) -$(INSTALL_STAMP): $(PYTHON_VENV) requirements.txt requirements_dev.txt - $(PYTHON_VENV) -m pip install -r requirements_dev.txt - $(PYTHON_VENV) -m pip install -e . - touch $(INSTALL_STAMP) - -dist: dist/$(FINAL_NAME).zip - -dist/$(FINAL_NAME).zip: - pyinstaller -y comictagger.spec - cd dist && zip -r $(FINAL_NAME).zip $(APP_NAME) - -dist/appimagetool: - curl -L https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage > dist/appimagetool - chmod +x dist/appimagetool - -appimage: dist dist/appimagetool - rm -rf dist/appimage && cp -a dist/comictagger dist/appimage - cd dist/appimage/ && ln -s comictaggerlib/graphics/app.png app.png && ln -s comictagger AppRun - sed -e 's|/usr/local/share/comictagger/app.png|app|g' -e 's|%%CTSCRIPT%% %F|./comictagger|g' desktop-integration/linux/ComicTagger.desktop > dist/appimage/AppRun.desktop - cd dist && ./appimagetool appimage diff --git a/README.md b/README.md index c276be0..477dc6b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ For details, screen-shots, and more, visit [the Wiki](https://github.com/comicta ### Binaries -Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). +Windows, Linux and MacOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases). Just unzip the archive in any folder and run, no additional installation steps are required. @@ -47,7 +47,14 @@ A pip package is provided, you can install it with: $ pip3 install comictagger[GUI] ``` -There are two optional dependencies GUI and CBR. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI]` +There are optional dependencies. You can install the optional dependencies by specifying one or more of them in braces e.g. `comictagger[CBR,GUI]` + +Optional dependencies: +1. `ICU`: Ensures that comic pages are supported correctly. This should always be installed. *Currently only exists in the latest alpha release * +1. `CBR`: Provides support for CBR/RAR files. +1. `GUI`: Installs the GUI. +1. `7Z`: Provides support for CB7/7Z files. +1. `all`: Installs all of the above optional dependencies. ### Chocolatey installation (Windows only) @@ -59,8 +66,7 @@ choco install comictagger 1. Ensure you have python 3.9 installed 2. Clone this repository `git clone https://github.com/comictagger/comictagger.git` - 3. `pip3 install -r requirements_dev.txt` - 7. `pip3 install .` or `pip3 install .[GUI]` + 7. `pip3 install .[ICU]` or `pip3 install .[GUI,ICU]` ## Contributors diff --git a/desktop-integration/linux/ComicTagger.desktop b/build-tools/ComicTagger.desktop similarity index 92% rename from desktop-integration/linux/ComicTagger.desktop rename to build-tools/ComicTagger.desktop index 1300d91..b34ba7a 100644 --- a/desktop-integration/linux/ComicTagger.desktop +++ b/build-tools/ComicTagger.desktop @@ -3,7 +3,7 @@ Encoding=UTF-8 Name=ComicTagger GenericName=Comic Metadata Editor Comment=A cross-platform GUI/CLI app for writing metadata to comic archives -Exec=%%CTSCRIPT%% %F +Exec=comictagger %F Icon=/usr/local/share/comictagger/app.png Terminal=false Type=Application diff --git a/comictagger.spec b/build-tools/comictagger.spec similarity index 98% rename from comictagger.spec rename to build-tools/comictagger.spec index fa199b9..8f1f040 100644 --- a/comictagger.spec +++ b/build-tools/comictagger.spec @@ -9,7 +9,7 @@ block_cipher = None a = Analysis( - ["comictaggerlib/__main__.py"], + ["../comictaggerlib/__main__.py"], pathex=[], binaries=[], datas=[], @@ -237,5 +237,5 @@ if platform.system() not in ["Windows"]: }, ], }, - bundle_identifier=None, + bundle_identifier="com.comictagger", ) diff --git a/build-tools/get_appimage.py b/build-tools/get_appimage.py new file mode 100644 index 0000000..5a5ed1c --- /dev/null +++ b/build-tools/get_appimage.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import os +import pathlib +import stat + +import requests + + +def urlretrieve(url: str, dest: str) -> None: + resp = requests.get(url) + if resp.status_code == 200: + pathlib.Path(dest).write_bytes(resp.content) + + +APPIMAGETOOL = "build/appimagetool-x86_64.AppImage" +if os.path.exists(APPIMAGETOOL): + raise SystemExit(0) + +urlretrieve( + "https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage", APPIMAGETOOL +) +os.chmod(APPIMAGETOOL, stat.S_IRWXU) + +if not os.path.exists(APPIMAGETOOL): + raise SystemExit(1) diff --git a/mac/Makefile b/build-tools/mac/Makefile similarity index 100% rename from mac/Makefile rename to build-tools/mac/Makefile diff --git a/mac/app.icns b/build-tools/mac/app.icns similarity index 100% rename from mac/app.icns rename to build-tools/mac/app.icns diff --git a/mac/make_thin.sh b/build-tools/mac/make_thin.sh similarity index 100% rename from mac/make_thin.sh rename to build-tools/mac/make_thin.sh diff --git a/mac/volume.icns b/build-tools/mac/volume.icns similarity index 100% rename from mac/volume.icns rename to build-tools/mac/volume.icns diff --git a/windows/app.ico b/build-tools/windows/app.ico similarity index 100% rename from windows/app.ico rename to build-tools/windows/app.ico diff --git a/build-tools/zip_artifacts.py b/build-tools/zip_artifacts.py new file mode 100644 index 0000000..300b069 --- /dev/null +++ b/build-tools/zip_artifacts.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import os +import pathlib +import platform +import zipfile + +from comictaggerlib.ctversion import __version__ + +app = "ComicTagger" +exe = app.casefold() +if platform.system() == "Windows": + os_version = f"win-{platform.machine()}" + app_name = f"{exe}.exe" + final_name = f"{app}-{__version__}-{os_version}.exe" +elif platform.system() == "Darwin": + ver = platform.mac_ver() + os_version = f"osx-{ver[0]}-{ver[2]}" + app_name = f"{app}.app" + final_name = f"{app}-{__version__}-{os_version}.app" +else: + app_name = exe + final_name = f"ComicTagger-{__version__}-{platform.system()}" + +path = f"dist/{app_name}" +zip_file = pathlib.Path(f"dist/{final_name}.zip") + + +def addToZip(zf, path, zippath): + if os.path.isfile(path): + zf.write(path, zippath) + elif os.path.isdir(path): + if zippath: + zf.write(path, zippath) + for nm in sorted(os.listdir(path)): + addToZip(zf, os.path.join(path, nm), os.path.join(zippath, nm)) + # else: ignore + + +zip_file.unlink(missing_ok=True) +with zipfile.ZipFile(zip_file, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=8) as zf: + zippath = os.path.basename(path) + if not zippath: + zippath = os.path.basename(os.path.dirname(path)) + if zippath in ("", os.curdir, os.pardir): + zippath = "" + addToZip(zf, path, zippath) diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py index 3abad0e..8aca69e 100644 --- a/comictaggerlib/filerenamer.py +++ b/comictaggerlib/filerenamer.py @@ -20,6 +20,7 @@ import logging import os import pathlib import string +from collections.abc import Mapping, Sequence from typing import Any, cast from pathvalidate import Platform, normalize_platform, sanitize_filename @@ -94,8 +95,8 @@ class MetadataFormatter(string.Formatter): def _vformat( self, format_string: str, - args: list[Any], - kwargs: dict[str, Any], + args: Sequence[Any], + kwargs: Mapping[str, Any], used_args: set[Any], recursion_depth: int, auto_arg_index: int = 0, diff --git a/desktop-integration/mac/ComicTagger b/desktop-integration/mac/ComicTagger deleted file mode 100644 index 7c79e43..0000000 --- a/desktop-integration/mac/ComicTagger +++ /dev/null @@ -1,4 +0,0 @@ -This file is a placeholder that will automatically be replaced with a symlink to -the local machine's Python framework python binary. - -When pip does an uninstall, it will remove the link. diff --git a/desktop-integration/mac/Info.plist b/desktop-integration/mac/Info.plist deleted file mode 100644 index cdbf493..0000000 --- a/desktop-integration/mac/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - English - CFBundleExecutable - main.sh - CFBundleIconFile - app.icns - CFBundleIdentifier - org.comictagger.comictagger - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ComicTagger - CFBundlePackageType - APPL - CFBundleShortVersionString - %%CTVERSION%% - CFBundleSignature - ???? - CFBundleVersion - %%CTVERSION%% - NSAppleScriptEnabled - YES - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/desktop-integration/mac/main.sh b/desktop-integration/mac/main.sh deleted file mode 100755 index 1d2fe4f..0000000 --- a/desktop-integration/mac/main.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -# This is a lot of hoop-jumping to get the absolute path -# of this script, so that we can use the Symlinked python -# binary to call the CT script. This is all so that the -# Mac menu doesn't say "Python". - -realpath() -{ - [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" -} - -CTSCRIPT=%%CTSCRIPT%% - -THIS=$(realpath $0) -THIS_FOLDER=$(dirname $THIS) -"$THIS_FOLDER/ComicTagger" "$CTSCRIPT" diff --git a/desktop-integration/windows/ComicTagger-pip.lnk b/desktop-integration/windows/ComicTagger-pip.lnk deleted file mode 100644 index c0cadfa..0000000 --- a/desktop-integration/windows/ComicTagger-pip.lnk +++ /dev/null @@ -1,4 +0,0 @@ -This file is a placeholder that will automatically be replaced with a Windows -shortcut on the user's desktop. - -When pip does an uninstall, it will remove the shortcut file. diff --git a/requirements-7Z.txt b/requirements-7Z.txt deleted file mode 100644 index 014d24e..0000000 --- a/requirements-7Z.txt +++ /dev/null @@ -1 +0,0 @@ -py7zr diff --git a/requirements-CBR.txt b/requirements-CBR.txt deleted file mode 100644 index f5c5f0a..0000000 --- a/requirements-CBR.txt +++ /dev/null @@ -1 +0,0 @@ -rarfile>=4.0 diff --git a/requirements-GUI.txt b/requirements-GUI.txt deleted file mode 100644 index 2d07ca0..0000000 --- a/requirements-GUI.txt +++ /dev/null @@ -1 +0,0 @@ -PyQt5 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7a936ec..0000000 --- a/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -appdirs==1.4.4 -beautifulsoup4>=4.1 -importlib_metadata>=3.3.0 -natsort>=8.1.0 -pathvalidate -pillow>=9.1.0, <10 -pycountry -#pyicu; sys_platform == 'linux' or sys_platform == 'darwin' -rapidfuzz>=2.12.0 -requests==2.* -settngs==0.6.2 -text2digits -typing_extensions -wordninja diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index ace72dd..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,12 +0,0 @@ -black>=22 -build -flake8==4.* -flake8-black -flake8-encodings -flake8-isort -isort>=5.10 -pyinstaller>=5.6.2 -pytest==7.* -setuptools>=42 -setuptools_scm[toml]>=3.4 -wheel diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..953e11d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,281 @@ +[metadata] +name = comictagger +description = A cross-platform GUI/CLI app for writing metadata to comic archives +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/comictagger/comictagger +author = ComicTagger team +author_email = comictagger@gmail.com +license = Apache License 2.0 +classifiers = + Development Status :: 4 - Beta + Environment :: Console + Environment :: MacOS X + Environment :: Win32 (MS Windows) + Environment :: X11 Applications :: Qt + Intended Audience :: End Users/Desktop + License :: OSI Approved :: Apache Software License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Topic :: Multimedia :: Graphics + Topic :: Other/Nonlisted Topic + Topic :: Utilities +keywords = + comictagger + comics + comic + metadata + tagging + tagger + +[options] +packages = find: +install_requires = + appdirs==1.4.4 + beautifulsoup4>=4.1 + importlib-metadata>=3.3.0 + natsort>=8.1.0 + pathvalidate + pillow>=9.1.0,<10 + pycountry + rapidfuzz>=2.12.0 + requests==2.* + settngs==0.6.2 + text2digits + typing-extensions + wordninja +python_requires = >=3.9 + +[options.packages.find] +exclude = tests; testing + +[options.entry_points] +console_scripts = comictagger=comictaggerlib.main:main +comicapi.archiver = + zip = comicapi.archivers.zip:ZipArchiver + sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver + rar = comicapi.archivers.rar:RarArchiver + folder = comicapi.archivers.folder:FolderArchiver +comictagger.talker = + comicvine = comictalker.talkers.comicvine:ComicVineTalker +pyinstaller40 = + hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs + +[options.extras_require] +7Z = + py7zr +CBR = + rarfile>=4.0 +GUI = + PyQt5 +ICU = + pyicu;sys_platform == 'linux' or sys_platform == 'darwin' +all = + PyQt5 + py7zr + rarfile>=4.0 + pyicu;sys_platform == 'linux' or sys_platform == 'darwin' + +[options.package_data] +comicapi = + data/* +comictaggerlib = + ui/* + graphics/* + +[tox:tox] +env_list = + format + py3.9-{none,gui,7z,cbr,icu,all} +minversion = 4.4.12 +basepython = {env:tox_python:python3.9} + +[testenv] +description = run the tests with pytest +package = wheel +wheel_build_env = .pkg +deps = + pytest>=7 +extras = + 7z: 7Z + cbr: CBR + gui: GUI + icu: ICU + all: all +commands = + python -m pytest {tty:--color=yes} {posargs} + icu,all: python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu + +[m1env] +description = run the tests with pytest +package = wheel +wheel_build_env = .pkg +deps = + pytest>=7 + icu,all: pyicu-binary +extras = + 7z: 7Z + cbr: CBR + gui: GUI + all: 7Z,CBR,GUI + +[testenv:py3.9-{icu,all}] +base = {env:tox_env:testenv} + +[testenv:format] +labels = + release + build +skip_install = true +deps = + black>=22 + isort>=5.10 + setup-cfg-fmt + autoflake + pyupgrade +commands = + -setup-cfg-fmt setup.cfg + -python -m autoflake -i --remove-all-unused-imports --ignore-init-module-imports . + -python -m isort --af --add-import 'from __future__ import annotations' . + -python -m black . + +[testenv:lint] +labels = + release +skip_install = true +depends = format +deps = + flake8==4.* + flake8-black + flake8-encodings + flake8-isort + mypy + types-setuptools + types-requests +commands = + python -m flake8 . + python -m mypy --ignore-missing-imports comicapi comictaggerlib comictalker + +[testenv:clean] +description = Clean development outputs +labels = + release + build +depends = + format + lint +skip_install = true +commands = + -python -c 'import shutil,pathlib; \ + shutil.rmtree("./build/", ignore_errors=True); \ + shutil.rmtree("./dist/", ignore_errors=True); \ + pathlib.Path("./comictaggerlib/ctversion.py").unlink(missing_ok=True); \ + pathlib.Path("comictagger.spec").unlink(missing_ok=True)' + +[testenv:wheel] +description = Generate wheel and tar.gz +labels = + release + build +depends = clean +skip_install = true +deps = + build +commands = + python -m build + +[testenv:pypi-upload] +description = Upload wheel to PyPi +platform = Linux +labels = + release +skip_install = true +depends = wheel +deps = + twine +passenv = + TWINE_* +setenv = + TWINE_NON_INTERACTIVE=true +commands = + python -m twine upload dist/*.whl dist/*.tar.gz + +[testenv:pyinstaller] +description = Generate pyinstaller executable +labels = + release + build +base = {env:tox_env:testenv} +depends = + clean + pypi-upload +deps = + pyinstaller>=5.6.2 +extras = + all +commands = + python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu + pyinstaller -y build-tools/comictagger.spec + +[testenv:appimage] +description = Generate appimage executable +skip_install = true +platform = Linux +base = {env:tox_env:testenv} +labels = + release + build +depends = + clean + pypi-upload + pyinstaller +deps = + requests +extras = + all +commands = + python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu + -python -c 'import shutil; shutil.rmtree("./build/", ignore_errors=True)' + python -c 'import shutil,pathlib; shutil.copytree("./dist/comictagger/", "./build/appimage", dirs_exist_ok=True); \ + shutil.copy("./comictaggerlib/graphics/app.png", "./build/appimage/app.png"); \ + pathlib.Path("./build/appimage/AppRun").symlink_to("comictagger"); \ + pathlib.Path("./build/appimage/AppRun.desktop").write_text( \ + pathlib.Path("build-tools/ComicTagger.desktop").read_text() \ + .replace("/usr/local/share/comictagger/app.png", "app") \ + .replace("Exec=comictagger", "Exec=./comictagger"))' + python ./build-tools/get_appimage.py + ./build/appimagetool ./build/appimage + +[testenv:zip_artifacts] +description = Zip release artifacts +labels = + release + build +depends = + wheel + pyinstaller + appimage +commands = + python ./build-tools/zip_artifacts.py + +[testenv:venv] +envdir = venv +deps = + flake8==4.* + flake8-black + flake8-encodings + flake8-isort + mypy + types-setuptools + types-requests + build + pyinstaller>=5.6.2 + +[flake8] +max-line-length = 120 +extend-ignore = E203, E501, A003 +extend-exclude = venv, scripts, build, dist, comictaggerlib/ctversion.py +per-file-ignores = + comictaggerlib/cli.py: T20 diff --git a/setup.py b/setup.py index 5ff775b..a03590f 100644 --- a/setup.py +++ b/setup.py @@ -1,92 +1,5 @@ -# Setup file for comictagger python source (no wheels yet) -# -# An entry point script called "comictagger" will be created -# -# Currently commented out, an experiment at desktop integration. -# It seems that post installation tweaks are broken by wheel files. -# Kept here for further research - from __future__ import annotations -import glob -import os +from setuptools import setup -from setuptools import find_packages, setup - - -def read(fname: str) -> str: - """ - Read the contents of a file. - Parameters - ---------- - fname : str - Path to file. - Returns - ------- - str - File contents. - """ - with open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8") as f: - return f.read() - - -install_requires = read("requirements.txt").splitlines() - -# Dynamically determine extra dependencies -extras_require = {} -extra_req_files = glob.glob("requirements-*.txt") -for extra_req_file in extra_req_files: - name = os.path.splitext(extra_req_file)[0].removeprefix("requirements-") - extras_require[name] = read(extra_req_file).splitlines() - -# If there are any extras, add a catch-all case that includes everything. -# This assumes that entries in extras_require are lists (not single strings), -# and that there are no duplicated packages across the extras. -if extras_require: - extras_require["all"] = sorted({x for v in extras_require.values() for x in v}) - - -setup( - name="comictagger", - install_requires=install_requires, - extras_require=extras_require, - python_requires=">=3.9", - description="A cross-platform GUI/CLI app for writing metadata to comic archives", - author="ComicTagger team", - author_email="comictagger@gmail.com", - url="https://github.com/comictagger/comictagger", - packages=find_packages(exclude=["tests", "testing"]), - package_data={"comictaggerlib": ["ui/*", "graphics/*"], "comicapi": ["data/*"]}, - entry_points={ - "console_scripts": ["comictagger=comictaggerlib.main:main"], - "pyinstaller40": ["hook-dirs = comictaggerlib.__pyinstaller:get_hook_dirs"], - "comicapi.archiver": [ - "zip = comicapi.archivers.zip:ZipArchiver", - "sevenzip = comicapi.archivers.sevenzip:SevenZipArchiver", - "rar = comicapi.archivers.rar:RarArchiver", - "folder = comicapi.archivers.folder:FolderArchiver", - ], - "comictagger.talker": [ - "comicvine = comictalker.talkers.comicvine:ComicVineTalker", - ], - }, - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Environment :: Win32 (MS Windows)", - "Environment :: MacOS X", - "Environment :: X11 Applications :: Qt", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: Apache Software License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.9", - "Topic :: Utilities", - "Topic :: Other/Nonlisted Topic", - "Topic :: Multimedia :: Graphics", - ], - keywords=["comictagger", "comics", "comic", "metadata", "tagging", "tagger"], - license="Apache License 2.0", - long_description=read("README.md"), - long_description_content_type="text/markdown", -) +setup() From afdb08fa15643501229f43972e358a368555cb16 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 23 Apr 2023 01:49:42 -0700 Subject: [PATCH 43/53] Fix package.yaml --- .github/workflows/package.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 96e094e..2117392 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -51,8 +51,8 @@ jobs: python -m tox r python -m tox r -m release env: - TWINE_USERNAME=__token__ - TWINE_PASSWORD=${{ secrets.PYPI_API_TOKEN }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - name: Get release name if: startsWith(github.ref, 'refs/tags/') From 941bbf545f431316d5647cc4545006fb28f43335 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 23 Apr 2023 01:52:56 -0700 Subject: [PATCH 44/53] Remove extraneous if --- .github/workflows/package.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 2117392..50d5b3f 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -31,7 +31,6 @@ jobs: run: | python -m pip install --upgrade --upgrade-strategy eager tox - if: runner.os == 'Windows' - name: Install macos dependencies run: | brew install icu4c pkg-config From ec65132cf266e4489e75b83d86d95c01bfef2828 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Sun, 23 Apr 2023 02:01:41 -0700 Subject: [PATCH 45/53] Mark mypy as optional --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 953e11d..d8c9adb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -156,7 +156,7 @@ deps = types-requests commands = python -m flake8 . - python -m mypy --ignore-missing-imports comicapi comictaggerlib comictalker + -python -m mypy --ignore-missing-imports comicapi comictaggerlib comictalker [testenv:clean] description = Clean development outputs From 14fa70e60814a6524422702ef81872e808ce95f9 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 25 Apr 2023 00:55:23 -0700 Subject: [PATCH 46/53] Separate xlate into separate functions based on return type fixes #454 --- comicapi/comet.py | 6 +-- comicapi/comicarchive.py | 14 +++---- comicapi/comicbookinfo.py | 24 +++++------ comicapi/comicinfoxml.py | 16 +++---- comicapi/genericmetadata.py | 2 +- comicapi/utils.py | 47 ++++++++++++--------- comictaggerlib/seriesselectionwindow.py | 4 +- comictaggerlib/taggerwindow.py | 20 ++++----- comictalker/talker_utils.py | 8 ++-- comictalker/talkers/comicvine.py | 4 +- tests/utils_test.py | 56 +++++++++++++++---------- 11 files changed, 112 insertions(+), 89 deletions(-) diff --git a/comicapi/comet.py b/comicapi/comet.py index ee7deb0..02e52d0 100644 --- a/comicapi/comet.py +++ b/comicapi/comet.py @@ -141,14 +141,14 @@ class CoMet: md.series = utils.xlate(get("series")) md.title = utils.xlate(get("title")) md.issue = utils.xlate(get("issue")) - md.volume = utils.xlate(get("volume"), True) + md.volume = utils.xlate_int(get("volume")) md.comments = utils.xlate(get("description")) md.publisher = utils.xlate(get("publisher")) md.language = utils.xlate(get("language")) md.format = utils.xlate(get("format")) - md.page_count = utils.xlate(get("pages"), True) + md.page_count = utils.xlate_int(get("pages")) md.maturity_rating = utils.xlate(get("rating")) - md.price = utils.xlate(get("price"), is_float=True) + md.price = utils.xlate_float(get("price")) md.is_version_of = utils.xlate(get("isVersionOf")) md.rights = utils.xlate(get("rights")) md.identifier = utils.xlate(get("identifier")) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index b225bba..ca089a2 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -561,13 +561,13 @@ class ComicArchive: ) metadata.alternate_number = utils.xlate(p.filename_info["alternate"]) metadata.issue = utils.xlate(p.filename_info["issue"]) - metadata.issue_count = utils.xlate(p.filename_info["issue_count"]) + metadata.issue_count = utils.xlate_int(p.filename_info["issue_count"]) metadata.publisher = utils.xlate(p.filename_info["publisher"]) metadata.series = utils.xlate(p.filename_info["series"]) metadata.title = utils.xlate(p.filename_info["title"]) - metadata.volume = utils.xlate(p.filename_info["volume"]) - metadata.volume_count = utils.xlate(p.filename_info["volume_count"]) - metadata.year = utils.xlate(p.filename_info["year"]) + metadata.volume = utils.xlate_int(p.filename_info["volume"]) + metadata.volume_count = utils.xlate_int(p.filename_info["volume_count"]) + metadata.year = utils.xlate_int(p.filename_info["year"]) metadata.scan_info = utils.xlate(p.filename_info["remainder"]) metadata.format = "FCBD" if p.filename_info["fcbd"] else None @@ -582,11 +582,11 @@ class ComicArchive: if fnp.series: metadata.series = fnp.series if fnp.volume: - metadata.volume = utils.xlate(fnp.volume, True) + metadata.volume = utils.xlate_int(fnp.volume) if fnp.year: - metadata.year = utils.xlate(fnp.year, True) + metadata.year = utils.xlate_int(fnp.year) if fnp.issue_count: - metadata.issue_count = utils.xlate(fnp.issue_count, True) + metadata.issue_count = utils.xlate_int(fnp.issue_count) if fnp.remainder: metadata.scan_info = fnp.remainder diff --git a/comicapi/comicbookinfo.py b/comicapi/comicbookinfo.py index ef282ac..cad1bb8 100644 --- a/comicapi/comicbookinfo.py +++ b/comicapi/comicbookinfo.py @@ -85,16 +85,16 @@ class ComicBookInfo: metadata.title = utils.xlate(cbi["title"]) metadata.issue = utils.xlate(cbi["issue"]) metadata.publisher = utils.xlate(cbi["publisher"]) - metadata.month = utils.xlate(cbi["publicationMonth"], True) - metadata.year = utils.xlate(cbi["publicationYear"], True) - metadata.issue_count = utils.xlate(cbi["numberOfIssues"], True) + metadata.month = utils.xlate_int(cbi["publicationMonth"]) + metadata.year = utils.xlate_int(cbi["publicationYear"]) + metadata.issue_count = utils.xlate_int(cbi["numberOfIssues"]) metadata.comments = utils.xlate(cbi["comments"]) metadata.genre = utils.xlate(cbi["genre"]) - metadata.volume = utils.xlate(cbi["volume"], True) - metadata.volume_count = utils.xlate(cbi["numberOfVolumes"], True) + metadata.volume = utils.xlate_int(cbi["volume"]) + metadata.volume_count = utils.xlate_int(cbi["numberOfVolumes"]) metadata.language = utils.xlate(cbi["language"]) metadata.country = utils.xlate(cbi["country"]) - metadata.critical_rating = utils.xlate(cbi["rating"], True) + metadata.critical_rating = utils.xlate_int(cbi["rating"]) metadata.credits = [ Credits( @@ -152,16 +152,16 @@ class ComicBookInfo: assign("title", utils.xlate(metadata.title)) assign("issue", utils.xlate(metadata.issue)) assign("publisher", utils.xlate(metadata.publisher)) - assign("publicationMonth", utils.xlate(metadata.month, True)) - assign("publicationYear", utils.xlate(metadata.year, True)) - assign("numberOfIssues", utils.xlate(metadata.issue_count, True)) + assign("publicationMonth", utils.xlate_int(metadata.month)) + assign("publicationYear", utils.xlate_int(metadata.year)) + assign("numberOfIssues", utils.xlate_int(metadata.issue_count)) assign("comments", utils.xlate(metadata.comments)) assign("genre", utils.xlate(metadata.genre)) - assign("volume", utils.xlate(metadata.volume, True)) - assign("numberOfVolumes", utils.xlate(metadata.volume_count, True)) + assign("volume", utils.xlate_int(metadata.volume)) + assign("numberOfVolumes", utils.xlate_int(metadata.volume_count)) assign("language", utils.xlate(utils.get_language_from_iso(metadata.language))) assign("country", utils.xlate(metadata.country)) - assign("rating", utils.xlate(metadata.critical_rating, True)) + assign("rating", utils.xlate_int(metadata.critical_rating)) assign("credits", metadata.credits) assign("tags", list(metadata.tags)) diff --git a/comicapi/comicinfoxml.py b/comicapi/comicinfoxml.py index aa50ee0..124b6dc 100644 --- a/comicapi/comicinfoxml.py +++ b/comicapi/comicinfoxml.py @@ -190,16 +190,16 @@ class ComicInfoXml: md.series = utils.xlate(get("Series")) md.title = utils.xlate(get("Title")) md.issue = IssueString(utils.xlate(get("Number"))).as_string() - md.issue_count = utils.xlate(get("Count"), True) - md.volume = utils.xlate(get("Volume"), True) + md.issue_count = utils.xlate_int(get("Count")) + md.volume = utils.xlate_int(get("Volume")) md.alternate_series = utils.xlate(get("AlternateSeries")) md.alternate_number = IssueString(utils.xlate(get("AlternateNumber"))).as_string() - md.alternate_count = utils.xlate(get("AlternateCount"), True) + md.alternate_count = utils.xlate_int(get("AlternateCount")) md.comments = utils.xlate(get("Summary")) md.notes = utils.xlate(get("Notes")) - md.year = utils.xlate(get("Year"), True) - md.month = utils.xlate(get("Month"), True) - md.day = utils.xlate(get("Day"), True) + md.year = utils.xlate_int(get("Year")) + md.month = utils.xlate_int(get("Month")) + md.day = utils.xlate_int(get("Day")) md.publisher = utils.xlate(get("Publisher")) md.imprint = utils.xlate(get("Imprint")) md.genre = utils.xlate(get("Genre")) @@ -210,12 +210,12 @@ class ComicInfoXml: md.characters = utils.xlate(get("Characters")) md.teams = utils.xlate(get("Teams")) md.locations = utils.xlate(get("Locations")) - md.page_count = utils.xlate(get("PageCount"), True) + md.page_count = utils.xlate_int(get("PageCount")) md.scan_info = utils.xlate(get("ScanInformation")) md.story_arc = utils.xlate(get("StoryArc")) md.series_group = utils.xlate(get("SeriesGroup")) md.maturity_rating = utils.xlate(get("AgeRating")) - md.critical_rating = utils.xlate(get("CommunityRating"), is_float=True) + md.critical_rating = utils.xlate_float(get("CommunityRating")) tmp = utils.xlate(get("BlackAndWhite")) if tmp is not None and tmp.casefold() in ["yes", "true", "1"]: diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py index 243eee9..e5a4e4b 100644 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -122,7 +122,7 @@ class GenericMetadata: pages: list[ImageMetadata] = dataclasses.field(default_factory=list) # Some CoMet-only items - price: str | None = None + price: float | None = None is_version_of: str | None = None rights: str | None = None identifier: str | None = None diff --git a/comicapi/utils.py b/comicapi/utils.py index 699e6d4..9cb579c 100644 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -68,17 +68,17 @@ def combine_notes(existing_notes: str | None, new_notes: str | None, split: str) return (untouched_notes + "\n" + (new_notes or "")).strip() -def parse_date_str(date_str: str) -> tuple[int | None, int | None, int | None]: +def parse_date_str(date_str: str | None) -> tuple[int | None, int | None, int | None]: day = None month = None year = None if date_str: parts = date_str.split("-") - year = xlate(parts[0], True) + year = xlate_int(parts[0]) if len(parts) > 1: - month = xlate(parts[1], True) + month = xlate_int(parts[1]) if len(parts) > 2: - day = xlate(parts[2], True) + day = xlate_int(parts[2]) return day, month, year @@ -107,23 +107,32 @@ def add_to_path(dirname: str) -> None: os.environ["PATH"] = os.pathsep.join(paths) -def xlate(data: Any, is_int: bool = False, is_float: bool = False) -> Any: +def xlate_int(data: Any) -> int | None: + data = xlate_float(data) + if data is None: + return None + return int(data) + + +def xlate_float(data: Any) -> float | None: + if data is None or data == "": + return None + i: str | int | float + if isinstance(data, (int, float)): + i = data + else: + i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890."), "1234567890."))) + if i == "": + return None + try: + return float(i) + except ValueError: + return None + + +def xlate(data: Any) -> str | None: if data is None or data == "": return None - if is_int or is_float: - i: str | int | float - if isinstance(data, (int, float)): - i = data - else: - i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890."), "1234567890."))) - if i == "": - return None - try: - if is_float: - return float(i) - return int(float(i)) - except ValueError: - return None return str(data) diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 56c8706..2da9f59 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -103,7 +103,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): series_name: str, issue_number: str, year: int | None, - issue_count: int, + issue_count: int | None, cover_index_list: list[int], comic_archive: ComicArchive | None, config: settngs.Namespace, @@ -303,7 +303,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): if found_match is not None: self.iddialog.accept() - self.series_id = utils.xlate(found_match["series_id"]) + self.series_id = utils.xlate(found_match["series_id"]) or "" self.issue_number = found_match["issue_number"] self.select_by_id() self.show_issues() diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 2b5acdc..0363392 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -898,13 +898,13 @@ class TaggerWindow(QtWidgets.QMainWindow): md.is_empty = False md.alternate_number = IssueString(self.leAltIssueNum.text()).as_string() md.issue = IssueString(self.leIssueNum.text()).as_string() - md.issue_count = utils.xlate(self.leIssueCount.text(), True) - md.volume = utils.xlate(self.leVolumeNum.text(), True) - md.volume_count = utils.xlate(self.leVolumeCount.text(), True) - md.month = utils.xlate(self.lePubMonth.text(), True) - md.year = utils.xlate(self.lePubYear.text(), True) - md.day = utils.xlate(self.lePubDay.text(), True) - md.alternate_count = utils.xlate(self.leAltIssueCount.text(), True) + md.issue_count = utils.xlate_int(self.leIssueCount.text()) + md.volume = utils.xlate_int(self.leVolumeNum.text()) + md.volume_count = utils.xlate_int(self.leVolumeCount.text()) + md.month = utils.xlate_int(self.lePubMonth.text()) + md.year = utils.xlate_int(self.lePubYear.text()) + md.day = utils.xlate_int(self.lePubDay.text()) + md.alternate_count = utils.xlate_int(self.leAltIssueCount.text()) md.series = self.leSeries.text() md.title = self.leTitle.text() @@ -915,7 +915,7 @@ class TaggerWindow(QtWidgets.QMainWindow): md.notes = self.teNotes.toPlainText() md.maturity_rating = self.cbMaturityRating.currentText() - md.critical_rating = utils.xlate(self.dsbCriticalRating.cleanText(), is_float=True) + md.critical_rating = utils.xlate_float(self.dsbCriticalRating.cleanText()) if md.critical_rating == 0.0: md.critical_rating = None @@ -1027,9 +1027,9 @@ class TaggerWindow(QtWidgets.QMainWindow): QtWidgets.QMessageBox.information(self, "Online Search", "Need to enter a series name to search.") return - year = utils.xlate(self.lePubYear.text(), True) + year = utils.xlate_int(self.lePubYear.text()) - issue_count = utils.xlate(self.leIssueCount.text(), True) + issue_count = utils.xlate_int(self.leIssueCount.text()) cover_index_list = self.metadata.get_cover_page_index_list() selector = SeriesSelectionWindow( diff --git a/comictalker/talker_utils.py b/comictalker/talker_utils.py index c397d79..a965bf2 100644 --- a/comictalker/talker_utils.py +++ b/comictalker/talker_utils.py @@ -56,7 +56,7 @@ def map_comic_issue_to_metadata( if issue_results.cover_date: metadata.day, metadata.month, metadata.year = utils.parse_date_str(issue_results.cover_date) elif issue_results.series.start_year: - metadata.year = utils.xlate(issue_results.series.start_year, True) + metadata.year = utils.xlate_int(issue_results.series.start_year) metadata.comments = cleanup_html(issue_results.description, remove_html_tables) if use_year_volume: @@ -91,11 +91,11 @@ def parse_date_str(date_str: str) -> tuple[int | None, int | None, int | None]: year = None if date_str: parts = date_str.split("-") - year = utils.xlate(parts[0], True) + year = utils.xlate_int(parts[0]) if len(parts) > 1: - month = utils.xlate(parts[1], True) + month = utils.xlate_int(parts[1]) if len(parts) > 2: - day = utils.xlate(parts[2], True) + day = utils.xlate_int(parts[2]) return day, month, year diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 3e43e05..6338758 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -393,7 +393,7 @@ class ComicVineTalker(ComicTalker): series_filter += str(vid) + "|" flt = f"volume:{series_filter},issue_number:{issue_number}" # CV uses volume to mean series - int_year = utils.xlate(year, True) + int_year = utils.xlate_int(year) if int_year is not None: flt += f",cover_date:{int_year}-1-1|{int_year + 1}-1-1" @@ -501,7 +501,7 @@ class ComicVineTalker(ComicTalker): else: image_url = record["image"].get("super_url", "") - start_year = utils.xlate(record.get("start_year", ""), True) + start_year = utils.xlate_int(record.get("start_year", "")) aliases = record.get("aliases") or "" diff --git a/tests/utils_test.py b/tests/utils_test.py index a9bcda4..43ebc94 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -77,32 +77,46 @@ def test_recursive_list_with_file(tmp_path) -> None: xlate_values = [ - ({"data": "", "is_int": False, "is_float": False}, None), - ({"data": None, "is_int": False, "is_float": False}, None), - ({"data": None, "is_int": True, "is_float": False}, None), - ({"data": " ", "is_int": True, "is_float": False}, None), - ({"data": "", "is_int": True, "is_float": False}, None), - ({"data": "9..", "is_int": True, "is_float": False}, None), - ({"data": "9", "is_int": False, "is_float": False}, "9"), - ({"data": 9, "is_int": False, "is_float": False}, "9"), - ({"data": 9, "is_int": True, "is_float": False}, 9), - ({"data": "9", "is_int": True, "is_float": False}, 9), - ({"data": 9.3, "is_int": True, "is_float": False}, 9), - ({"data": "9.3", "is_int": True, "is_float": False}, 9), - ({"data": "9.", "is_int": True, "is_float": False}, 9), - ({"data": " 9 . 3 l", "is_int": True, "is_float": False}, 9), - ({"data": 9, "is_int": False, "is_float": True}, 9.0), - ({"data": "9", "is_int": False, "is_float": True}, 9.0), - ({"data": 9.3, "is_int": False, "is_float": True}, 9.3), - ({"data": "9.3", "is_int": False, "is_float": True}, 9.3), - ({"data": "9.", "is_int": False, "is_float": True}, 9.0), - ({"data": " 9 . 3 l", "is_int": False, "is_float": True}, 9.3), + ("", None), + (None, None), + ("9", "9"), + (9, "9"), +] +xlate_int_values = [ + (None, None), + (" ", None), + ("", None), + ("9..", None), + (9, 9), + ("9", 9), + (9.3, 9), + ("9.3", 9), + ("9.", 9), + (" 9 . 3 l", 9), +] +xlate_float_values = [ + (9, 9.0), + ("9", 9.0), + (9.3, 9.3), + ("9.3", 9.3), + ("9.", 9.0), + (" 9 . 3 l", 9.3), ] @pytest.mark.parametrize("value, result", xlate_values) def test_xlate(value, result): - assert comicapi.utils.xlate(**value) == result + assert comicapi.utils.xlate(value) == result + + +@pytest.mark.parametrize("value, result", xlate_float_values) +def test_xlate_float(value, result): + assert comicapi.utils.xlate_float(value) == result + + +@pytest.mark.parametrize("value, result", xlate_int_values) +def test_xlate_int(value, result): + assert comicapi.utils.xlate_int(value) == result language_values = [ From f94c9ef8574b3b47978c7e8e920426c529531499 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Mon, 24 Apr 2023 22:59:50 -0700 Subject: [PATCH 47/53] Update appimage step Fix platform case Remove icu check from appimage step as ComicTagger is not installed Add appimagetool to allowed commands Fix appimage paths --- build-tools/get_appimage.py | 21 ++++++++++++++------- setup.cfg | 27 ++++++++++++++------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/build-tools/get_appimage.py b/build-tools/get_appimage.py index 5a5ed1c..82b5d0f 100644 --- a/build-tools/get_appimage.py +++ b/build-tools/get_appimage.py @@ -1,26 +1,33 @@ from __future__ import annotations +import argparse import os import pathlib import stat import requests +parser = argparse.ArgumentParser() +parser.add_argument("APPIMAGETOOL", default="build/appimagetool-x86_64.AppImage", type=pathlib.Path, nargs="?") -def urlretrieve(url: str, dest: str) -> None: +opts = parser.parse_args() +opts.APPIMAGETOOL = opts.APPIMAGETOOL.absolute() + + +def urlretrieve(url: str, dest: pathlib.Path) -> None: resp = requests.get(url) if resp.status_code == 200: - pathlib.Path(dest).write_bytes(resp.content) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(resp.content) -APPIMAGETOOL = "build/appimagetool-x86_64.AppImage" -if os.path.exists(APPIMAGETOOL): +if opts.APPIMAGETOOL.exists(): raise SystemExit(0) urlretrieve( - "https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage", APPIMAGETOOL + "https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage", opts.APPIMAGETOOL ) -os.chmod(APPIMAGETOOL, stat.S_IRWXU) +os.chmod(opts.APPIMAGETOOL, stat.S_IRWXU) -if not os.path.exists(APPIMAGETOOL): +if not opts.APPIMAGETOOL.exists(): raise SystemExit(1) diff --git a/setup.cfg b/setup.cfg index d8c9adb..9c58e7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -222,7 +222,7 @@ commands = [testenv:appimage] description = Generate appimage executable skip_install = true -platform = Linux +platform = linux base = {env:tox_env:testenv} labels = release @@ -233,20 +233,21 @@ depends = pyinstaller deps = requests -extras = - all +allowlist_externals = + {tox_root}/build/appimagetool-x86_64.AppImage +change_dir = dist +commands_pre = + -python -c 'import shutil; shutil.rmtree("{tox_root}/build/", ignore_errors=True)' + python {tox_root}/build-tools/get_appimage.py {tox_root}/build/appimagetool-x86_64.AppImage commands = - python -c 'import importlib,platform; importlib.import_module("icu") if platform.system() != "Windows" else ...' # Sanity check for icu - -python -c 'import shutil; shutil.rmtree("./build/", ignore_errors=True)' - python -c 'import shutil,pathlib; shutil.copytree("./dist/comictagger/", "./build/appimage", dirs_exist_ok=True); \ - shutil.copy("./comictaggerlib/graphics/app.png", "./build/appimage/app.png"); \ - pathlib.Path("./build/appimage/AppRun").symlink_to("comictagger"); \ - pathlib.Path("./build/appimage/AppRun.desktop").write_text( \ - pathlib.Path("build-tools/ComicTagger.desktop").read_text() \ + python -c 'import shutil,pathlib; shutil.copytree("{tox_root}/dist/comictagger/", "{tox_root}/build/appimage", dirs_exist_ok=True); \ + shutil.copy("{tox_root}/comictaggerlib/graphics/app.png", "{tox_root}/build/appimage/app.png"); \ + pathlib.Path("{tox_root}/build/appimage/AppRun").symlink_to("comictagger"); \ + pathlib.Path("{tox_root}/build/appimage/AppRun.desktop").write_text( \ + pathlib.Path("{tox_root}/build-tools/ComicTagger.desktop").read_text() \ .replace("/usr/local/share/comictagger/app.png", "app") \ - .replace("Exec=comictagger", "Exec=./comictagger"))' - python ./build-tools/get_appimage.py - ./build/appimagetool ./build/appimage + .replace("Exec=comictagger", "Exec={tox_root}/comictagger"))' + {tox_root}/build/appimagetool-x86_64.AppImage {tox_root}/build/appimage [testenv:zip_artifacts] description = Zip release artifacts From 1dc93c351d98494f8a6781e6e50c1478439eb9f8 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 25 Apr 2023 00:57:02 -0700 Subject: [PATCH 48/53] Update settngs to typed version fixes #453 --- comictaggerlib/cli.py | 8 ++++---- setup.cfg | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index ea9828d..cfca3e0 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -40,15 +40,15 @@ logger = logging.getLogger(__name__) class CLI: - def __init__(self, config: settngs.Namespace, talkers: dict[str, ComicTalker]): + def __init__(self, config: settngs.Namespace, talkers: dict[str, ComicTalker]) -> None: self.config = config 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) + if self.config.talker_source in self.talkers: + return self.talkers[self.config.talker_source] + logger.error("Could not find the '%s' talker", self.config.talker_source) raise SystemExit(2) def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata: diff --git a/setup.cfg b/setup.cfg index 9c58e7f..bd7f99d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ install_requires = pycountry rapidfuzz>=2.12.0 requests==2.* - settngs==0.6.2 + settngs==0.6.3 text2digits typing-extensions wordninja From c07e1c4168bda30d7b1eda739d63e9cd52ff614c Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 25 Apr 2023 00:07:39 -0700 Subject: [PATCH 49/53] Add additional typing --- comicapi/archivers/archiver.py | 2 +- comictaggerlib/ui/talkeruigenerator.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/comicapi/archivers/archiver.py b/comicapi/archivers/archiver.py index efe58e5..51d1a68 100644 --- a/comicapi/archivers/archiver.py +++ b/comicapi/archivers/archiver.py @@ -25,7 +25,7 @@ class Archiver(Protocol): """ enabled: bool = True - def __init__(self): + def __init__(self) -> None: self.path = pathlib.Path() def get_comment(self) -> str: diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index eef533e..ff5e5b5 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -3,7 +3,7 @@ from __future__ import annotations import argparse import logging from functools import partial -from typing import NamedTuple +from typing import Any, NamedTuple import settngs from PyQt5 import QtCore, QtWidgets @@ -24,9 +24,9 @@ def generate_api_widgets( config: settngs.Config[settngs.Namespace], layout: QtWidgets.QGridLayout, talkers: dict[str, ComicTalker], -): +) -> None: # *args enforces keyword arguments and allows position arguments to be ignored - def call_check_api(*args, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker_id: str): + def call_check_api(*args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker_id: str) -> None: url = "" key = "" if le_key is not None: From d3ff40c249ec4a22c145ab50f63389580b3d1cf5 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 25 Apr 2023 01:59:24 -0700 Subject: [PATCH 50/53] Only update the image in CoverImageWidget if the url matches the current url This fixes an issue causing the first issue cover to show when using the auto-identify feature Fixes #455 --- comictaggerlib/coverimagewidget.py | 8 +++++--- comictaggerlib/imagefetcher.py | 16 ++++++++-------- comictaggerlib/issueselectionwindow.py | 2 +- comictaggerlib/seriesselectionwindow.py | 2 ++ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/comictaggerlib/coverimagewidget.py b/comictaggerlib/coverimagewidget.py index aef5459..c167ca5 100644 --- a/comictaggerlib/coverimagewidget.py +++ b/comictaggerlib/coverimagewidget.py @@ -60,7 +60,7 @@ class CoverImageWidget(QtWidgets.QWidget): URLMode = 1 DataMode = 3 - image_fetch_complete = QtCore.pyqtSignal(QtCore.QByteArray) + image_fetch_complete = QtCore.pyqtSignal(str, QtCore.QByteArray) def __init__( self, @@ -201,7 +201,7 @@ class CoverImageWidget(QtWidgets.QWidget): elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]: self.load_url() elif self.mode == CoverImageWidget.DataMode: - self.cover_remote_fetch_complete(self.imageData) + self.cover_remote_fetch_complete("", self.imageData) else: self.load_page() @@ -238,7 +238,9 @@ class CoverImageWidget(QtWidgets.QWidget): self.cover_fetcher.fetch(self.url_list[self.imageIndex]) # called when the image is done loading from internet - def cover_remote_fetch_complete(self, image_data: bytes) -> None: + def cover_remote_fetch_complete(self, url: str, image_data: bytes) -> None: + if url and url not in self.url_list: + return img = get_qimage_from_data(image_data) self.current_pixmap = QtGui.QPixmap.fromImage(img) self.set_display_pixmap() diff --git a/comictaggerlib/imagefetcher.py b/comictaggerlib/imagefetcher.py index 779340f..05b1afe 100644 --- a/comictaggerlib/imagefetcher.py +++ b/comictaggerlib/imagefetcher.py @@ -41,7 +41,7 @@ class ImageFetcherException(Exception): ... -def fetch_complete(image_data: bytes | QtCore.QByteArray) -> None: +def fetch_complete(url: str, image_data: bytes | QtCore.QByteArray) -> None: ... @@ -79,22 +79,22 @@ class ImageFetcher: # first look in the DB image_data = self.get_image_from_cache(url) # Async for retrieving covers seems to work well - if blocking: # if blocking or not qt_available: + if blocking or not qt_available: if not image_data: try: image_data = requests.get(url, headers={"user-agent": "comictagger/" + ctversion.version}).content + # save the image to the cache + self.add_image_to_cache(self.fetched_url, image_data) except Exception as e: logger.exception("Fetching url failed: %s") raise ImageFetcherException("Network Error!") from e - - # save the image to the cache - self.add_image_to_cache(self.fetched_url, image_data) + ImageFetcher.image_fetch_complete(url, image_data) return image_data if qt_available: # if we found it, just emit the signal asap if image_data: - ImageFetcher.image_fetch_complete(QtCore.QByteArray(image_data)) + ImageFetcher.image_fetch_complete(url, QtCore.QByteArray(image_data)) return b"" # didn't find it. look online @@ -110,9 +110,9 @@ class ImageFetcher: image_data = reply.readAll() # save the image to the cache - self.add_image_to_cache(self.fetched_url, image_data) + self.add_image_to_cache(reply.request().url().toString(), image_data) - ImageFetcher.image_fetch_complete(image_data) + ImageFetcher.image_fetch_complete(reply.request().url().toString(), image_data) def create_image_db(self) -> None: # this will wipe out any existing version diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index b6532a7..f1b9e9c 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -106,7 +106,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): # now that the list has been sorted, find the initial record, and # select it - if self.initial_id is None: + if not self.initial_id: self.twList.selectRow(0) else: for r in range(0, self.twList.rowCount()): diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 2da9f59..9a208bb 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -326,6 +326,8 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.issue_number = selector.issue_number self.issue_id = selector.issue_id self.accept() + else: + self.imageWidget.update_content() def select_by_id(self) -> None: for r in range(0, self.twList.rowCount()): From 83aabfd9c351d59c9f5009c9a137ad0e98728abf Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 25 Apr 2023 16:11:19 -0700 Subject: [PATCH 51/53] Upgrade pre-commit --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 870f477..fa79b71 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,12 +14,12 @@ repos: hooks: - id: setup-cfg-fmt - repo: https://github.com/PyCQA/autoflake - rev: v2.0.1 + rev: v2.1.1 hooks: - id: autoflake args: [-i, --remove-all-unused-imports, --ignore-init-module-imports] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.3.2 hooks: - id: pyupgrade args: [--py39-plus] @@ -29,7 +29,7 @@ repos: - id: isort args: [--af,--add-import, 'from __future__ import annotations'] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 @@ -38,7 +38,7 @@ repos: - id: flake8 additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-length, flake8-print] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.2.0 hooks: - id: mypy additional_dependencies: [types-setuptools, types-requests] From ff15bff94c96ecef3f21a224c996ee342dc9e6bf Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 25 Apr 2023 16:26:05 -0700 Subject: [PATCH 52/53] Fix pypi upload --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bd7f99d..492a050 100644 --- a/setup.cfg +++ b/setup.cfg @@ -188,7 +188,7 @@ commands = [testenv:pypi-upload] description = Upload wheel to PyPi -platform = Linux +platform = linux labels = release skip_install = true From 30f1db1c73681c018e8e2d250648f9f805a00a22 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Wed, 26 Apr 2023 14:46:18 -0700 Subject: [PATCH 53/53] Update requirements and Linux build dependencies --- .github/workflows/package.yaml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 50d5b3f..27f82c5 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -40,7 +40,7 @@ jobs: - name: Install linux dependencies run: | - sudo apt-get install pkg-config libicu-dev libqt5gui5 + sudo apt-get install pkg-config libicu-dev libqt5gui5 libfuse2 # export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"; # export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH" if: runner.os == 'Linux' diff --git a/setup.cfg b/setup.cfg index 492a050..035b7cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = requests==2.* settngs==0.6.3 text2digits - typing-extensions + typing-extensions>=4.3.0 wordninja python_requires = >=3.9