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",
],
},