Compare commits

..

16 Commits

Author SHA1 Message Date
e7cc05679f Bump metron-talker version 2023-09-17 08:09:43 -07:00
99461c54f1 Fix a crash when setting the page type with no comic selected 2023-09-15 21:03:41 -07:00
ddd98ee86d Add metron-talker as an optional dependency 2023-09-15 15:13:14 -07:00
1d25179171 Allow unsetting metadata fields on the commandline fixes #528 2023-09-14 11:30:05 -07:00
7efef0bb44 Merge branch 'mizaki-on_change_windows' into develop 2023-09-14 11:20:01 -07:00
366e9cf6e8 Move update into own function. Add title missing to trigger issue update. 2023-09-13 21:35:52 +01:00
57abe22515 Merge branch 'mizaki-fix_auto_id' into develop 2023-09-12 15:16:16 -07:00
c7a49b3643 Fix crash with series and issue window if the year is None. Closes #523 2023-09-10 13:42:17 +01:00
1125788bb7 Update series and issue rows after calling for more information. Closes #512 2023-09-10 13:31:20 +01:00
034a25a813 Fix auto-identify crash 2023-09-07 14:44:30 +01:00
f72c0c8224 Fix call to check_api 2023-09-06 04:56:30 -04:00
0a2340b6dc Remove the --script commandline option 2023-09-06 03:00:27 -04:00
bf2b4ab268 Rename check_api_key to check_status
Parameter is changed to a settings dict so that a Talker can retrieve any info it needs
Change issue_id type annotation to str
2023-09-06 02:59:59 -04:00
40bd3d5bb8 Fix generation and saving of talker settings fixes #515 #514 2023-09-05 14:43:17 -04:00
61d2a8b833 Fix issue padding validation fixes #513 2023-09-05 14:42:03 -04:00
b04dad8015 Stop deleting self.progialog in the series selection window 2023-09-05 14:41:07 -04:00
11 changed files with 140 additions and 109 deletions

View File

@ -143,12 +143,6 @@ def register_runtime(parser: settngs.Manager) -> None:
help="Recursively include files in sub-folders.",
file=False,
)
parser.add_setting(
"-S",
"--script",
help="""Run an "add-on" python script that uses the\nComicTagger library for custom processing.\nScript arguments can follow the script name.\n\n""",
file=False,
)
parser.add_setting(
"--split-words",
action="store_true",

View File

@ -105,7 +105,7 @@ def parse_metadata_from_string(mdstr: str) -> GenericMetadata:
for item in md_list:
# Make sure to fix any escaped equal signs
i = item.replace(escaped_equals, replacement_token)
key, value = utils.split(i, "=")
key, _, value = i.partition("=")
value = value.replace(replacement_token, "=").strip()
key = key.strip()
if key.casefold() == "credit":

View File

@ -160,31 +160,10 @@ class IssueSelectionWindow(QtWidgets.QDialog):
for row, issue in enumerate(self.issue_list.values()):
self.twList.insertRow(row)
for i in range(3):
self.twList.setItem(row, i, QtWidgets.QTableWidgetItem())
item_text = issue.issue or ""
item = IssueNumberTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, issue.issue_id)
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = ""
if issue.year is not None:
item_text += f"-{issue.year:04}"
if issue.month is not None:
item_text += f"-{issue.month:02}"
qtw_item = QtWidgets.QTableWidgetItem(item_text.strip("-"))
qtw_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
qtw_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 1, qtw_item)
item_text = issue.title or ""
qtw_item = QtWidgets.QTableWidgetItem(item_text)
qtw_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
qtw_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 2, qtw_item)
self.update_row(row, issue)
if IssueString(issue.issue).as_string().casefold() == IssueString(self.issue_number).as_string().casefold():
self.initial_id = issue.issue_id or ""
@ -204,21 +183,56 @@ class IssueSelectionWindow(QtWidgets.QDialog):
html = text
widget.setHtml(html, QtCore.QUrl(self.talker.website))
def update_row(self, row: int, issue: GenericMetadata) -> None:
item_text = issue.issue or ""
item = self.twList.item(row, 0)
item.setText(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, issue.issue_id)
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, item_text)
item_text = ""
if issue.year is not None:
item_text += f"-{issue.year:04}"
if issue.month is not None:
item_text += f"-{issue.month:02}"
qtw_item = self.twList.item(row, 1)
qtw_item.setText(item_text.strip("-"))
qtw_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
qtw_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item_text = issue.title or ""
qtw_item = self.twList.item(row, 2)
qtw_item.setText(item_text)
qtw_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
qtw_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None:
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
row = curr.row()
self.issue_id = self.twList.item(row, 0).data(QtCore.Qt.ItemDataRole.UserRole)
# list selection was changed, update the the issue cover
# list selection was changed, update the issue cover
issue = self.issue_list[self.issue_id]
if not (issue.issue and issue.year and issue.month and issue.cover_image):
issue = self.talker.fetch_comic_data(issue_id=self.issue_id)
if not (issue.issue and issue.year and issue.month and issue.cover_image and issue.title):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
try:
issue = self.talker.fetch_comic_data(issue_id=self.issue_id)
except TalkerError:
pass
QtWidgets.QApplication.restoreOverrideCursor()
self.issue_number = issue.issue or ""
self.coverWidget.set_issue_details(self.issue_id, [issue.cover_image or "", *issue.alternate_images])
if issue.description is None:
self.set_description(self.teDescription, "")
else:
self.set_description(self.teDescription, issue.description)
# Update current record information
self.update_row(row, issue)

View File

@ -123,7 +123,7 @@ class PageListEditor(QtWidgets.QWidget):
self.addAction(action_item)
def select_page_type_item(self, idx: int) -> None:
if self.cbPageType.isEnabled():
if self.cbPageType.isEnabled() and self.listWidget.rowCount() > 0:
self.cbPageType.setCurrentIndex(idx)
self.change_page_type(idx)
@ -207,7 +207,7 @@ class PageListEditor(QtWidgets.QWidget):
def change_page_type(self, i: int) -> None:
new_type = self.cbPageType.itemData(i)
if self.get_current_page_type() != new_type:
if self.listWidget.count() > 0 and self.get_current_page_type() != new_type:
self.set_current_page_type(new_type)
self.emit_front_cover_change()
self.modified.emit()

View File

@ -334,7 +334,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
title = ""
for series in self.series_list.values():
if series.id == self.series_id:
title = f"{series.name} ({series.start_year:04}) - "
title = f"{series.name} ({series.start_year:04}) - " if series.start_year else f"{series.name} - "
break
selector.setWindowTitle(title + "Select Issue")
@ -349,7 +349,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
self.imageWidget.update_content()
def select_by_id(self) -> None:
for r in range(self.twList.rows()):
for r in range(self.twList.rowCount()):
if self.series_id == self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole):
self.twList.selectRow(r)
break
@ -398,7 +398,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
def search_complete(self) -> None:
if self.progdialog is not None:
self.progdialog.accept()
del self.progdialog
self.progdialog = None
if self.search_thread is not None and self.search_thread.ct_error:
# TODO Currently still opens the window
QtWidgets.QMessageBox.critical(
@ -481,36 +481,10 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
for row, series in enumerate(self.series_list.values()):
self.twList.insertRow(row)
for i in range(4):
self.twList.setItem(row, i, QtWidgets.QTableWidgetItem())
item_text = series.name
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, series.id)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 0, item)
if series.start_year is not None:
item_text = f"{series.start_year:04}"
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, series.start_year)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 1, item)
if series.count_of_issues is not None:
item_text = f"{series.count_of_issues:04}"
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, series.count_of_issues)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 2, item)
if series.publisher is not None:
item_text = series.publisher
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, 3, item)
self.update_row(row, series)
self.twList.setSortingEnabled(True)
self.twList.selectRow(0)
@ -552,13 +526,42 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
html = text
widget.setHtml(html, QUrl(self.talker.website))
def update_row(self, row: int, series: ComicSeries) -> None:
item_text = series.name
item = self.twList.item(row, 0)
item.setText(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, series.id)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item_text = str(series.start_year)
item = self.twList.item(row, 1)
item.setText(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item_text = str(series.count_of_issues)
item = self.twList.item(row, 2)
item.setText(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, series.count_of_issues)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
if series.publisher is not None:
item_text = series.publisher
item = self.twList.item(row, 3)
item.setText(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None:
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.series_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
row = curr.row()
self.series_id = self.twList.item(row, 0).data(QtCore.Qt.ItemDataRole.UserRole)
# list selection was changed, update the info on the series
series = self.series_list[self.series_id]
@ -570,9 +573,19 @@ class SeriesSelectionWindow(QtWidgets.QDialog):
and series.description
and series.image_url
):
series = self.talker.fetch_series(self.series_id)
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
# Changing of usernames and passwords with using cache can cause talker errors to crash out
try:
series = self.talker.fetch_series(self.series_id)
except TalkerError:
pass
QtWidgets.QApplication.restoreOverrideCursor()
if series.description is None:
self.set_description(self.teDetails, "")
else:
self.set_description(self.teDetails, series.description)
self.imageWidget.set_url(series.image_url)
# Update current record information
self.update_row(row, series)

View File

@ -277,6 +277,8 @@ class SettingsWindow(QtWidgets.QDialog):
)
def _rename_test(self, template: str) -> None:
if not str(self.leIssueNumPadding.text()).isdigit():
self.leIssueNumPadding.setText("0")
fr = FileRenamer(
md_test,
platform="universal" if self.cbxRenameStrict.isChecked() else "auto",
@ -461,7 +463,7 @@ class SettingsWindow(QtWidgets.QDialog):
self.config[0].File_Rename_replacements = self.get_replacements()
# Read settings from talker tabs
comictaggerlib.ui.talkeruigenerator.form_settings_to_config(self.sources, self.config)
self.config = comictaggerlib.ui.talkeruigenerator.form_settings_to_config(self.sources, self.config)
self.update_talkers_config()

View File

@ -69,17 +69,8 @@ def generate_api_widgets(
layout: QtWidgets.QGridLayout,
) -> None:
# *args enforces keyword arguments and allows position arguments to be ignored
def call_check_api(
*args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker: ComicTalker
) -> None:
url = ""
key = ""
if le_key is not None:
key = le_key.text().strip()
if le_url is not None:
url = le_url.text().strip()
check_text, check_bool = talker.check_api_key(url, key)
def call_check_api(*args: Any, tab: TalkerTab, talker: ComicTalker) -> None:
check_text, check_bool = talker.check_status(get_config_dict(tab))
if check_bool:
QtWidgets.QMessageBox.information(None, "API Test Success", check_text)
else:
@ -115,7 +106,7 @@ def generate_api_widgets(
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=talker))
btn.clicked.connect(partial(call_check_api, tab=widgets, talker=talker))
def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox:
@ -199,6 +190,22 @@ def settings_to_talker_form(sources: Sources, config: settngs.Config[ct_ns]) ->
logger.debug("Failed to set value of %s for %s(%s)", dest, talker.name, talker.id)
def get_config_dict(tab: TalkerTab) -> dict[str, Any]:
talker_options = {}
# dest is guaranteed to be unique within a talker and refer to the correct item in config.values['group name']
for dest, widget in tab.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()
talker_options[dest] = widget_value
return talker_options
def form_settings_to_config(sources: Sources, config: settngs.Config) -> settngs.Config[ct_ns]:
# Update the currently selected talker
config.values.Sources_source = sources.cbx_sources.currentData()
@ -207,17 +214,7 @@ def form_settings_to_config(sources: Sources, config: settngs.Config) -> settngs
# Iterate over the tabs, the talker is included in the tab so no extra lookup is needed
for talker, tab in sources.tabs:
talker_options = cfg.values[group_for_plugin(talker)]
# dest is guaranteed to be unique within a talker and refer to the correct item in config.values['group name']
for dest, widget in tab.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()
talker_options[dest] = widget_value
talker_options.update(get_config_dict(tab))
return cast(settngs.Config[ct_ns], settngs.get_namespace(cfg, True, True))
@ -251,7 +248,7 @@ def generate_source_option_tabs(
comic_talker_tab_layout.addLayout(talker_layout)
# Add source sub tabs to Comic Sources tab
for t_id, talker in talkers.items():
for t_id, talker in list(talkers.items()):
# Add source to general tab dropdown list
sources.cbx_sources.addItem(talker.name, t_id)
tab = TalkerTab(tab=QtWidgets.QWidget(), widgets={})
@ -260,12 +257,12 @@ def generate_source_option_tabs(
url_option: settngs.Setting | None = None
key_option: settngs.Setting | None = None
for option in config.definitions[group_for_plugin(talker)].v.values():
if not option.file:
continue
elif option.dest == f"{t_id}_key":
if option.dest == f"{t_id}_key":
key_option = option
elif option.dest == f"{t_id}_url":
url_option = option
elif not option.file:
continue
elif option._guess_type() is bool:
current_widget = generate_checkbox(option, layout_grid)
tab.widgets[option.dest] = current_widget

View File

@ -248,7 +248,7 @@ class ComicCacher:
return results
def get_issue_info(self, issue_id: int, source: str, expire_stale: bool = True) -> tuple[Issue, bool] | None:
def get_issue_info(self, issue_id: str, source: str, expire_stale: bool = True) -> tuple[Issue, bool] | None:
with sqlite3.connect(self.db_file) as con:
con.row_factory = sqlite3.Row
cur = con.cursor()

View File

@ -142,16 +142,20 @@ class ComicTalker:
settings[f"{self.id}_url"] = None
return settings
def check_api_key(self, url: str, key: str) -> tuple[str, bool]:
def check_status(self, settings: dict[str, Any]) -> tuple[str, bool]:
"""
This function should return (msg, True) if the given API key and URL are valid,
This function should return (msg, True) if the given settings are valid,
where msg is a message to display to the user.
This function should return (msg, False) if the given API key or URL are not valid,
This function should return (msg, False) if the given settings 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.
This function MUST NOT keep any values from the given settings, this is a test function only.
If the Talker uses the network it should ensure that it can authenticate with the given settings.
If settings provides an invalid URL, API key or other piece of information used to authenticate it MUST fail.
The only time that defaults should be used is when the value is an empty string or None
If the Talker does not use the network it should validate that all local data has been configured correctly.
Caching MUST NOT be implemented on this function.
"""

View File

@ -206,8 +206,8 @@ class ComicVineTalker(ComicTalker):
return settings
def check_api_key(self, url: str, key: str) -> tuple[str, bool]:
url = talker_utils.fix_url(url)
def check_status(self, settings: dict[str, Any]) -> tuple[str, bool]:
url = talker_utils.fix_url(settings[f"{self.id}_url"])
if not url:
url = self.default_api_url
try:
@ -216,7 +216,11 @@ class ComicVineTalker(ComicTalker):
cv_response: CVResult = requests.get(
test_url,
headers={"user-agent": "comictagger/" + self.version},
params={"api_key": key or self.default_api_key, "format": "json", "field_list": "name"},
params={
"api_key": settings[f"{self.id}_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"
@ -583,7 +587,7 @@ class ComicVineTalker(ComicTalker):
def _fetch_issue_data_by_issue_id(self, issue_id: str) -> GenericMetadata:
# before we search online, look in our cache, since we might already have this info
cvc = ComicCacher(self.cache_folder, self.version)
cached_issue = cvc.get_issue_info(int(issue_id), self.id)
cached_issue = cvc.get_issue_info(issue_id, self.id)
if cached_issue and cached_issue[1]:
return self._map_comic_issue_to_metadata(

View File

@ -80,9 +80,12 @@ QTW =
all =
PyQt5
PyQtWebEngine
metron-talker>=0.1.1
py7zr
rarfile>=4.0
pyicu;sys_platform == 'linux' or sys_platform == 'darwin'
metron =
metron-talker>=0.1.1
[options.package_data]
comicapi =