diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 6fa6a31..b0be168 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -49,7 +49,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.options.autotag_ignore_leading_numbers_in_filename) self.cbxRemoveAfterSuccess.setChecked(self.options.autotag_remove_archive_after_successful_match) self.cbxWaitForRateLimit.setChecked(self.options.autotag_wait_and_retry_on_rate_limit) - self.cbxAutoImprint.setChecked(self.options.talkers_general_auto_imprint) + self.cbxAutoImprint.setChecked(self.options.talkers_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 diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 7938f43..db72231 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -108,7 +108,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.options.talkers_general_clear_metadata_on_import: + if self.options.talkers_clear_metadata_on_import: md = ct_md else: notes = ( @@ -117,7 +117,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.options.talkers_general_auto_imprint: + if self.options.talkers_auto_imprint: md.fix_publisher() self.actual_metadata_save(ca, md) @@ -428,7 +428,7 @@ class CLI: match_results.fetch_data_failures.append(str(ca.path.absolute())) return - if self.options.talkers_general_clear_metadata_on_import: + if self.options.talkers_clear_metadata_on_import: md = ct_md else: notes = ( @@ -437,7 +437,7 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.options.talkers_general_auto_imprint: + if self.options.talkers_auto_imprint: md.fix_publisher() # ok, done building our metadata. time to save diff --git a/comictaggerlib/ctoptions/__init__.py b/comictaggerlib/ctoptions/__init__.py index fd1f386..688d84f 100644 --- a/comictaggerlib/ctoptions/__init__.py +++ b/comictaggerlib/ctoptions/__init__.py @@ -2,12 +2,14 @@ from __future__ import annotations from comictaggerlib.ctoptions.cmdline import initial_cmd_line_parser, register_commandline, validate_commandline_options from comictaggerlib.ctoptions.file import register_settings, validate_settings +from comictaggerlib.ctoptions.talker_plugins import register_talker_settings from comictaggerlib.ctoptions.types import ComicTaggerPaths __all__ = [ "initial_cmd_line_parser", "register_commandline", "register_settings", + "register_talker_settings", "validate_commandline_options", "validate_settings", "ComicTaggerPaths", diff --git a/comictaggerlib/ctoptions/file.py b/comictaggerlib/ctoptions/file.py index 8bdef4c..a6cb332 100644 --- a/comictaggerlib/ctoptions/file.py +++ b/comictaggerlib/ctoptions/file.py @@ -93,7 +93,7 @@ def filename(parser: settngs.Manager) -> None: ) -def talkers_general(parser: settngs.Manager) -> None: +def talkers(parser: settngs.Manager) -> None: # General settings for all information talkers parser.add_setting("--source", default="comicvine", help="Use a specified source by source ID") parser.add_setting("--use-series-start-as-volume", default=False, action=argparse.BooleanOptionalAction) @@ -239,17 +239,13 @@ def validate_settings(options: settngs.Config[settngs.Values], parser: settngs.M return options -def register_settings(parser: settngs.Manager, talkers: dict[str, Any]) -> None: +def register_settings(parser: settngs.Manager) -> None: parser.add_group("general", general, False) parser.add_group("internal", internal, False) parser.add_group("identifier", identifier, False) parser.add_group("dialog", dialog, False) parser.add_group("filename", filename, False) - parser.add_group("talkers_general", talkers_general, False) + parser.add_group("talkers", talkers, False) parser.add_group("cbl", cbl, False) parser.add_group("rename", rename, False) parser.add_group("autotag", autotag, False) - - # Register talker plugin settings - for talker, cls in talkers.items(): - parser.add_group(talker, cls.comic_settings, False) diff --git a/comictaggerlib/ctoptions/talker_plugins.py b/comictaggerlib/ctoptions/talker_plugins.py new file mode 100644 index 0000000..31ea94b --- /dev/null +++ b/comictaggerlib/ctoptions/talker_plugins.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import logging + +import settngs + +from comictalker.comictalkerapi import TalkerPlugin + +logger = logging.getLogger(__name__) + + +def register_talker_settings(parser: settngs.Manager, plugins: dict[str, TalkerPlugin]) -> None: + for talker_name, talker in plugins.items(): + try: + parser.add_group(talker_name, talker["cls"].comic_settings, False) + except Exception: + logger.exception("Failed to register settings for %s", talker_name) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 0fdb7ba..18c5a13 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -66,6 +66,7 @@ class App: self.options = settngs.Config({}, {}) self.initial_arg_parser = ctoptions.initial_cmd_line_parser() self.config_load_success = False + # First get a list of classes self.talker_plugins = ct_api.get_talkers() def run(self) -> None: @@ -73,6 +74,7 @@ class App: self.register_options() self.parse_options(opts.config) self.initialize_dirs() + self.initialize_talkers() self.main() @@ -88,7 +90,8 @@ class App: "For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki", ) ctoptions.register_commandline(self.manager) - ctoptions.register_settings(self.manager, self.talker_plugins) + ctoptions.register_settings(self.manager) + ctoptions.register_talker_settings(self.manager, self.talker_plugins) def parse_options(self, config_paths: ctoptions.ComicTaggerPaths) -> None: self.options, self.config_load_success = self.manager.parse_config( @@ -100,18 +103,6 @@ class App: self.options = ctoptions.validate_settings(self.options, self.manager) self.options = self.options - # parse talker settings - for loaded in self.talker_plugins.values(): - parse_options = getattr(loaded, "parse_settings", None) - if parse_options is None: - logger.warning(f"Failed to find parse_settings in talker {loaded}") - continue - - try: - parse_options(self.options) - except Exception as e: - logger.warning(f"Failed to parse talker options for {loaded}: {e}") - def initialize_dirs(self) -> None: self.options[0].runtime_config.user_data_dir.mkdir(parents=True, exist_ok=True) self.options[0].runtime_config.user_config_dir.mkdir(parents=True, exist_ok=True) @@ -124,6 +115,17 @@ class App: logger.debug("user_state_dir: %s", self.options[0].runtime_config.user_state_dir) logger.debug("user_log_dir: %s", self.options[0].runtime_config.user_log_dir) + def initialize_talkers(self) -> None: + try: + self.talker_plugins = ct_api.get_talker_objects( + version=version, + cache_folder=self.options[0].runtime_config.user_cache_dir, + settings=self.options[0], + plugins=self.talker_plugins, + ) + except Exception: + logger.exception("Failed to initialize talkers. See log for full details.") + def main(self) -> None: assert self.options is not None # options already loaded @@ -152,12 +154,9 @@ class App: # TODO Have option to save passed in config options and quit? try: - talker_api = ct_api.get_comic_talker(self.options[0].talkers_general_source)( # type: ignore[call-arg] - version=version, - cache_folder=self.options[0].runtime_config.user_cache_dir, - ) + talker_api = self.talker_plugins[self.options[0].talkers_source]["obj"] except TalkerError as e: - logger.exception("Unable to load talker") + logger.exception(f"Unable to load talker {self.options[0].talkers_source}. Error: {str(e)}") error = (str(e), True) if not self.config_load_success: diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index f214a15..ec6b053 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -156,7 +156,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.progdialog: QtWidgets.QProgressDialog | None = None self.search_thread: SearchThread | None = None - self.use_filter = self.options.talkers_general_always_use_publisher_filter + self.use_filter = self.options.talkers_always_use_publisher_filter # Load to retrieve settings self.talker_api = talker_api @@ -395,7 +395,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.options.talkers_general_sort_series_by_year: + if self.options.talkers_sort_series_by_year: try: self.ct_search_results = sorted( self.ct_search_results, @@ -413,7 +413,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): logger.exception("bad data error sorting results by count_of_issues") # move sanitized matches to the front - if self.options.talkers_general_exact_series_matches_first: + if self.options.talkers_exact_series_matches_first: try: sanitized = utils.sanitize_title(self.series_name, False).casefold() sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold() diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 21d5f12..bdbd8a7 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -283,12 +283,12 @@ class SettingsWindow(QtWidgets.QDialog): self.switch_parser() self.cbxUseSeriesStartAsVolume.setChecked(self.options[0].comicvine_cv_use_series_start_as_volume) - self.cbxClearFormBeforePopulating.setChecked(self.options[0].talkers_general_clear_form_before_populating) + self.cbxClearFormBeforePopulating.setChecked(self.options[0].talkers_clear_form_before_populating) self.cbxRemoveHtmlTables.setChecked(self.options[0].comicvine_cv_remove_html_tables) - self.cbxUseFilter.setChecked(self.options[0].talkers_general_always_use_publisher_filter) - self.cbxSortByYear.setChecked(self.options[0].talkers_general_sort_series_by_year) - self.cbxExactMatches.setChecked(self.options[0].talkers_general_exact_series_matches_first) + self.cbxUseFilter.setChecked(self.options[0].talkers_always_use_publisher_filter) + self.cbxSortByYear.setChecked(self.options[0].talkers_sort_series_by_year) + self.cbxExactMatches.setChecked(self.options[0].talkers_exact_series_matches_first) self.leKey.setText(self.options[0].comicvine_cv_api_key) self.leURL.setText(self.options[0].comicvine_cv_url) @@ -397,12 +397,12 @@ class SettingsWindow(QtWidgets.QDialog): self.options[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked() self.options[0].comicvine_cv_use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked() - self.options[0].talkers_general_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() + self.options[0].talkers_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() self.options[0].comicvine_cv_remove_html_tables = self.cbxRemoveHtmlTables.isChecked() - self.options[0].talkers_general_always_use_publisher_filter = self.cbxUseFilter.isChecked() - self.options[0].talkers_general_sort_series_by_year = self.cbxSortByYear.isChecked() - self.options[0].talkers_general_exact_series_matches_first = self.cbxExactMatches.isChecked() + self.options[0].talkers_always_use_publisher_filter = self.cbxUseFilter.isChecked() + self.options[0].talkers_sort_series_by_year = self.cbxSortByYear.isChecked() + self.options[0].talkers_exact_series_matches_first = self.cbxExactMatches.isChecked() if self.leKey.text().strip(): self.options[0].comicvine_cv_api_key = self.leKey.text().strip() diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index d115a11..0d19458 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -1084,7 +1084,7 @@ Have fun! if self.options[0].cbl_apply_transform_on_import: new_metadata = CBLTransformer(new_metadata, self.options[0]).apply() - if self.options[0].talkers_general_clear_form_before_populating: + if self.options[0].talkers_clear_form_before_populating: self.clear_form() notes = ( @@ -1802,7 +1802,7 @@ Have fun! ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.options[0].talkers_general_auto_imprint: + if self.options[0].talkers_auto_imprint: md.fix_publisher() if not ca.write_metadata(md, self.save_data_style): diff --git a/comictalker/comictalkerapi.py b/comictalker/comictalkerapi.py index b569cef..c8fdd55 100644 --- a/comictalker/comictalkerapi.py +++ b/comictalker/comictalkerapi.py @@ -16,6 +16,8 @@ from __future__ import annotations import logging +import pathlib +from typing import Any, TypedDict import comictalker.talkers.comicvine from comictalker.talkerbase import ComicTalker, TalkerError @@ -23,16 +25,37 @@ from comictalker.talkerbase import ComicTalker, TalkerError logger = logging.getLogger(__name__) -def get_comic_talker(source_name: str) -> type[ComicTalker]: - """Retrieve the available sources modules""" - sources = get_talkers() - if source_name not in sources: - raise TalkerError(source=source_name, code=4, desc="The talker does not exist") - - talker = sources[source_name] - return talker +class TalkerPlugin(TypedDict, total=False): + cls: type[ComicTalker] + obj: ComicTalker -def get_talkers() -> dict[str, type[ComicTalker]]: +def set_talker_settings(talker, settings) -> None: + try: + talker.set_settings(settings) + except Exception as e: + logger.exception( + f"Failed to set talker settings for {talker.talker_id}, will use defaults. Error: {str(e)}", + ) + raise TalkerError(source=talker.talker_id, code=4, desc="Could not apply talker settings, will use defaults") + + +def get_talker_objects( + version: str, cache_folder: pathlib.Path, settings: dict[str, Any], plugins: dict[str, TalkerPlugin] +) -> dict[str, TalkerPlugin]: + for talker_name, talker in plugins.items(): + try: + obj = talker["cls"](version, cache_folder) + plugins[talker_name]["obj"] = obj + except Exception: + logger.exception("Failed to create talker object") + raise TalkerError(source=talker_name, code=4, desc="Failed to initialise talker object") + + # Run outside of try block so as to keep except separate + set_talker_settings(plugins[talker_name]["obj"], settings) + return plugins + + +def get_talkers() -> dict[str, TalkerPlugin]: """Returns all comic talker modules NOT objects""" - return {"comicvine": comictalker.talkers.comicvine.ComicVineTalker} + return {"comicvine": TalkerPlugin(cls=comictalker.talkers.comicvine.ComicVineTalker)} diff --git a/comictalker/talkerbase.py b/comictalker/talkerbase.py index 40413d1..4f73b70 100644 --- a/comictalker/talkerbase.py +++ b/comictalker/talkerbase.py @@ -139,27 +139,23 @@ class TalkerDataError(TalkerError): class ComicTalker: """The base class for all comic source talkers""" - default_api_url: str = "" - default_api_key: str = "" + talker_id = "" - def __init__(self, version: str, cache_folder: pathlib.Path, api_url: str = "", api_key: str = "") -> None: + def __init__(self, version: str, cache_folder: pathlib.Path) -> None: # Identity name for the information source etc. self.source_details = SourceDetails() self.static_options = SourceStaticOptions() - self.api_key = api_key self.cache_folder = cache_folder self.version = version - - self.api_key = "" - self.api_url = "" + self.api_key: str = "" + self.api_url: str = "" @classmethod def comic_settings(cls, parser: settngs.Manager) -> None: """Talker settings.""" - @classmethod - def parse_settings(cls, settings: argparse.Namespace) -> None: - """Parse settings.""" + def set_settings(self, settings: argparse.Namespace) -> None: + """Apply talker settings from config to object.""" def check_api_key(self, key: str, url: str) -> bool: """ diff --git a/comictalker/talkers/comicvine.py b/comictalker/talkers/comicvine.py index 4042c53..b23abfd 100644 --- a/comictalker/talkers/comicvine.py +++ b/comictalker/talkers/comicvine.py @@ -154,16 +154,7 @@ CV_RATE_LIMIT_STATUS = 107 class ComicVineTalker(ComicTalker): - default_api_key = "27431e6787042105bd3e47e169a624521f89f3a4" - default_api_url = "https://comicvine.gamespot.com/api" - - # Settings - api_url: str = "" - api_key: str = "" - series_match_thresh: int = 90 - remove_html_tables: bool = False - use_series_start_as_volume: bool = False - wait_on_ratelimit: bool = False + talker_id = "comicvine" def __init__( self, @@ -171,7 +162,7 @@ class ComicVineTalker(ComicTalker): cache_folder: pathlib.Path, ): super().__init__(version, cache_folder) - self.source_details = SourceDetails(name="Comic Vine", ident="comicvine") + self.source_details = SourceDetails(name="Comic Vine", ident=ComicVineTalker.talker_id) self.static_options = SourceStaticOptions( website="https://comicvine.gamespot.com/", has_issues=True, @@ -180,15 +171,18 @@ class ComicVineTalker(ComicTalker): has_nsfw=False, has_censored_covers=False, ) + # Default settings + self.api_url: str = "https://comicvine.gamespot.com/api" + self.api_key: str = "27431e6787042105bd3e47e169a624521f89f3a4" + self.series_match_thresh: int = 90 + self.remove_html_tables: bool = False + self.use_series_start_as_volume: bool = False + self.wait_for_rate_limit: bool = False # Identity name for the information source self.source_name: str = self.source_details.id self.source_name_friendly: str = self.source_details.name - # If cls api_url or api_key is empty, use default - self.api_url = ComicVineTalker.api_url or ComicVineTalker.default_api_url - self.api_key = ComicVineTalker.api_key or ComicVineTalker.default_api_key - tmp_url = urlsplit(self.api_url) # joinurl only works properly if there is a trailing slash @@ -197,15 +191,9 @@ class ComicVineTalker(ComicTalker): self.api_url = tmp_url.geturl() - self.wait_for_rate_limit: bool = ComicVineTalker.wait_on_ratelimit - # NOTE: This was hardcoded before which is why it isn't passed in + # NOTE: This was hardcoded before which is why it isn't in settings self.wait_for_rate_limit_time: int = 20 - self.remove_html_tables: bool = ComicVineTalker.remove_html_tables - self.use_series_start_as_volume: bool = ComicVineTalker.use_series_start_as_volume - - self.series_match_thresh: int = ComicVineTalker.series_match_thresh - @classmethod def comic_settings(cls, parser: settngs.Manager) -> None: # Might be general settings? @@ -233,17 +221,24 @@ class ComicVineTalker(ComicTalker): help="Use the given Comic Vine URL.", ) - @classmethod - def parse_settings(cls, settings: settngs.Config) -> None: - """Parse settings.""" - if settings[0].comicvine_cv_remove_html_tables: - cls.remove_html_tables = bool(settings[0].comicvine_cv_remove_html_tables) - if settings[0].comicvine_cv_use_series_start_as_volume: - cls.use_series_start_as_volume = settings[0].comicvine_cv_use_series_start_as_volume - if settings[0].comicvine_cv_api_key: - cls.api_key = settings[0].comicvine_cv_api_key - if settings[0].comicvine_cv_url: - cls.api_url = settings[0].comicvine_cv_url + def set_settings(self, settings: argparse.Namespace) -> None: + """Set settings.""" + if settings.comicvine_cv_remove_html_tables: + self.remove_html_tables = bool(settings.comicvine_cv_remove_html_tables) + if settings.comicvine_cv_use_series_start_as_volume: + self.use_series_start_as_volume = settings.comicvine_cv_use_series_start_as_volume + if settings.comicvine_cv_api_key: + self.api_key = settings.comicvine_cv_api_key + if settings.comicvine_cv_url: + try: + tmp_url = urlsplit(settings.comicvine_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() + except Exception: + logger.exception("Failed to parse new talker URL for %s, will use default", self.talker_id) def check_api_key(self, key: str, url: str) -> bool: if not url: diff --git a/tests/conftest.py b/tests/conftest.py index eb14644..87e6bbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -165,7 +165,8 @@ def get_plugins() -> dict: def options(settings_manager, tmp_path): comictaggerlib.ctoptions.register_commandline(settings_manager) - comictaggerlib.ctoptions.register_settings(settings_manager, get_plugins()) + comictaggerlib.ctoptions.register_settings(settings_manager) + comictaggerlib.ctoptions.register_talker_settings(settings_manager, get_plugins()) defaults = settings_manager.get_namespace(settings_manager.defaults()) defaults[0].runtime_config = comictaggerlib.ctoptions.ComicTaggerPaths(tmp_path / "config") defaults[0].runtime_config.user_data_dir.mkdir(parents=True, exist_ok=True)