diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index 7ee6054..e76ba0d 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -24,7 +24,7 @@ from comicapi.genericmetadata import GenericMetadata from comicapi.issuestring import IssueString from comictaggerlib.coverimagewidget import CoverImageWidget from comictaggerlib.ctsettings import ct_ns -from comictaggerlib.seriesselectionwindow import SeriesSelectionWindow +from comictaggerlib.seriesselectionwindow import SelectionWindow from comictaggerlib.ui import ui_path from comictalker.comictalker import ComicTalker, RLCallBack, TalkerError @@ -39,9 +39,44 @@ class IssueNumberTableWidgetItem(QtWidgets.QTableWidgetItem): return (IssueString(self_str).as_float() or 0) < (IssueString(other_str).as_float() or 0) -class IssueSelectionWindow(SeriesSelectionWindow): +class QueryThread(QtCore.QThread): + def __init__( + self, + talker: ComicTalker, + series_id: str, + finish: QtCore.pyqtSignal, + on_ratelimit: QtCore.pyqtSignal, + ) -> None: + super().__init__() + self.series_id = series_id + self.talker = talker + self.finish = finish + self.on_ratelimit = on_ratelimit + + def run(self) -> None: + QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) + + try: + issue_list = [ + x + for x in self.talker.fetch_issues_in_series( + self.series_id, on_rate_limit=RLCallBack(lambda x, y: self.on_ratelimit.emit(x, y), 10) + ) + if x.issue_id is not None + ] + except TalkerError as e: + QtWidgets.QApplication.restoreOverrideCursor() + QtWidgets.QMessageBox.critical(None, f"{e.source} {e.code_name} Error", f"{e}") + return + + QtWidgets.QApplication.restoreOverrideCursor() + self.finish.emit(issue_list) + + +class IssueSelectionWindow(SelectionWindow): ui_file = ui_path / "issueselectionwindow.ui" CoverImageMode = CoverImageWidget.AltCoverMode + finish = QtCore.pyqtSignal(list) def __init__( self, @@ -60,42 +95,28 @@ class IssueSelectionWindow(SeriesSelectionWindow): self.issue_number = "1" self.initial_id: str = "" - self.perform_query() - - # now that the list has been sorted, find the initial record, and - # select it - if self.initial_id: - for r in range(0, self.twList.rowCount()): - issue_id = self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole) - if issue_id == self.initial_id: - self.twList.selectRow(r) - break self.leFilter.textChanged.connect(self.filter) + self.finish.connect(self.query_finished) def showEvent(self, event: QtGui.QShowEvent) -> None: - return + self.perform_query() def perform_query(self) -> None: # type: ignore[override] - QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) - - try: - self.issue_list = { - x.issue_id: x - for x in self.talker.fetch_issues_in_series( - self.series_id, on_rate_limit=RLCallBack(self.on_ratelimit, 10) - ) - if x.issue_id is not None - } - except TalkerError as e: - QtWidgets.QApplication.restoreOverrideCursor() - QtWidgets.QMessageBox.critical(self, f"{e.source} {e.code_name} Error", f"{e}") - return + self.querythread = QueryThread( + self.talker, + self.series_id, + self.finish, + self.ratelimit, + ) + self.querythread.start() + def query_finished(self, issues: list[GenericMetadata]) -> None: self.twList.setRowCount(0) self.twList.setSortingEnabled(False) - - for row, issue in enumerate(self.issue_list.values()): + self.issue_list = {i.issue_id: i for i in issues if i.issue_id is not None} + self.twList.clear() + for row, issue in enumerate(issues): self.twList.insertRow(row) self.twList.setItem(row, 0, IssueNumberTableWidgetItem()) self.twList.setItem(row, 1, QtWidgets.QTableWidgetItem()) @@ -108,8 +129,15 @@ class IssueSelectionWindow(SeriesSelectionWindow): self.twList.setSortingEnabled(True) self.twList.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder) - - QtWidgets.QApplication.restoreOverrideCursor() + self.twList: QtWidgets.QTableWidget + if self.initial_id: + for r in range(0, self.twList.rowCount()): + item = self.twList.item(r, 0) + issue_id = item.data(QtCore.Qt.ItemDataRole.UserRole) + if issue_id == self.initial_id: + self.twList.selectRow(r) + self.twList.scrollToItem(item, QtWidgets.QAbstractItemView.ScrollHint.EnsureVisible) + break def cell_double_clicked(self, r: int, c: int) -> None: self.accept() diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 7b1593a..99489a3 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -18,6 +18,7 @@ from __future__ import annotations import itertools import logging +from abc import ABCMeta, abstractmethod from collections import deque import natsort @@ -128,9 +129,11 @@ class IdentifyThread(QtCore.QThread): self.ratelimit.emit(full_time, sleep_time) -class SeriesSelectionWindow(QtWidgets.QDialog): +class SelectionWindow(QtWidgets.QDialog): + __metaclass__ = ABCMeta ui_file = ui_path / "seriesselectionwindow.ui" CoverImageMode = CoverImageWidget.URLMode + ratelimit = pyqtSignal(float, float) def __init__( self, @@ -203,31 +206,107 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.leFilter.textChanged.connect(self.filter) self.twList.selectRow(0) - if series_name: - self.series_name = series_name - self.issue_number = issue_number - self.year = year - self.issue_count = issue_count - self.series_id: str = "" - self.comic_archive = comic_archive - self.immediate_autoselect = autoselect - self.series_list: dict[str, ComicSeries] = {} - self.literal = literal - self.iddialog: IDProgressWindow | None = None - self.id_thread: IdentifyThread | None = None - self.progdialog: QtWidgets.QProgressDialog | None = None - self.search_thread: SearchThread | None = None + @abstractmethod + def perform_query(self, refresh: bool = False) -> None: ... - self.use_publisher_filter = self.config.Auto_Tag__use_publisher_filter + @abstractmethod + def cell_double_clicked(self, r: int, c: int) -> None: ... - self.btnRequery.clicked.connect(self.requery) - self.btnIssues.clicked.connect(self.show_issues) - self.btnAutoSelect.clicked.connect(self.auto_select) + @abstractmethod + def update_row(self, row: int, series: ComicSeries) -> None: ... - self.cbxPublisherFilter.setChecked(self.use_publisher_filter) - self.cbxPublisherFilter.toggled.connect(self.publisher_filter_toggled) + def set_description(self, widget: QtWidgets.QWidget, text: str) -> None: + if isinstance(widget, QtWidgets.QTextEdit): + widget.setText(text.replace("", "").replace(" None: + rows = set(range(self.twList.rowCount())) + for r in rows: + self.twList.showRow(r) + if text.strip(): + shown_rows = {x.row() for x in self.twList.findItems(text, QtCore.Qt.MatchFlag.MatchContains)} + for r in rows - shown_rows: + self.twList.hideRow(r) + + @abstractmethod + def _fetch(self, row: int) -> ComicSeries: ... + + def on_ratelimit(self, full_time: float, sleep_time: float) -> None: + self.ratelimit.emit(full_time, sleep_time) + + 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 + + row = curr.row() + + item = self._fetch(row) + QtWidgets.QApplication.restoreOverrideCursor() + + # Update current record information + self.update_row(row, item) + + +class SeriesSelectionWindow(SelectionWindow): + ui_file = ui_path / "seriesselectionwindow.ui" + CoverImageMode = CoverImageWidget.URLMode + + def __init__( + self, + parent: QtWidgets.QWidget, + config: ct_ns, + talker: ComicTalker, + series_name: str = "", + issue_number: str = "", + comic_archive: ComicArchive | None = None, + year: int | None = None, + issue_count: int | None = None, + autoselect: bool = False, + literal: bool = False, + ) -> None: + super().__init__( + parent, + config, + talker, + series_name, + issue_number, + comic_archive, + year, + issue_count, + autoselect, + literal, + ) + self.series_name = series_name + self.issue_number = issue_number + self.year = year + self.issue_count = issue_count + self.series_id: str = "" + self.comic_archive = comic_archive + self.immediate_autoselect = autoselect + self.series_list: dict[str, ComicSeries] = {} + self.literal = literal + self.iddialog: IDProgressWindow | None = None + self.id_thread: IdentifyThread | None = None + self.progdialog: QtWidgets.QProgressDialog | None = None + self.search_thread: SearchThread | None = None + + self.use_publisher_filter = self.config.Auto_Tag__use_publisher_filter + + self.btnRequery.clicked.connect(self.requery) + self.btnIssues.clicked.connect(self.show_issues) + self.btnAutoSelect.clicked.connect(self.auto_select) + + self.cbxPublisherFilter.setChecked(self.use_publisher_filter) + self.cbxPublisherFilter.toggled.connect(self.publisher_filter_toggled) + + self.ratelimit.connect(self.ratelimit_message) + + self.update_buttons() def showEvent(self, event: QtGui.QShowEvent) -> None: self.perform_query() @@ -251,8 +330,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): ) self.search_thread.searchComplete.connect(self.search_complete) self.search_thread.progressUpdate.connect(self.search_progress_update) - self.search_thread.ratelimit.connect(self.on_ratelimit) - self.search_thread.ratelimit.connect(self.parent().on_ratelimit) + self.search_thread.ratelimit.connect(self.ratelimit) self.search_thread.start() self.progdialog = QtWidgets.QProgressDialog("Searching Online", "Cancel", 0, 100, self) @@ -269,11 +347,6 @@ class SeriesSelectionWindow(QtWidgets.QDialog): def cell_double_clicked(self, r: int, c: int) -> None: self.show_issues() - def on_ratelimit(self, full_time: float, sleep_time: float) -> None: - self.log_output( - f"Rate limit reached: {full_time:.0f}s until next request. Waiting {sleep_time:.0f}s for ratelimit" - ) - def update_row(self, row: int, series: ComicSeries) -> None: item_text = series.name item = self.twList.item(row, 0) @@ -380,21 +453,13 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.id_thread.identifyComplete.connect(self.identify_complete) self.id_thread.identifyLogMsg.connect(self.log_output) self.id_thread.identifyProgress.connect(self.identify_progress) - self.id_thread.ratelimit.connect(self.on_ratelimit) - self.id_thread.ratelimit.connect(self.parent().on_ratelimit) + self.id_thread.ratelimit.connect(self.ratelimit) self.iddialog.rejected.connect(self.id_thread.cancel) self.id_thread.start() self.iddialog.exec() - - def log_output(self, text: str) -> None: - if self.iddialog is not None: - self.iddialog.textEdit.append(text.rstrip()) - self.iddialog.textEdit.ensureCursorVisible() - QtCore.QCoreApplication.processEvents() - QtCore.QCoreApplication.processEvents() - QtCore.QCoreApplication.processEvents() + self.selector = None def identify_progress(self, cur: int, total: int) -> None: if self.iddialog is not None: @@ -449,20 +514,24 @@ class SeriesSelectionWindow(QtWidgets.QDialog): def show_issues(self) -> None: from comictaggerlib.issueselectionwindow import IssueSelectionWindow - selector = IssueSelectionWindow(self, self.config, self.talker, self.series_id, self.issue_number) + self.selector = IssueSelectionWindow(self, self.config, self.talker, self.series_id, self.issue_number) + self.selector.ratelimit.connect(self.ratelimit) title = "" for series in self.series_list.values(): if series.id == self.series_id: title = f"{series.name} ({series.start_year:04}) - " if series.start_year else f"{series.name} - " break - selector.setWindowTitle(title + "Select Issue") - selector.setModal(True) - selector.exec() - if selector.result(): + self.selector.setWindowTitle(title + "Select Issue") + self.selector.setModal(True) + self.selector.finished.connect(self.issue_selected) + self.selector.open() + + def issue_selected(self, result) -> None: + if result and self.selector: # we should now have a series ID - self.issue_number = selector.issue_number - self.issue_id = selector.issue_id + self.issue_number = self.selector.issue_number + self.issue_id = self.selector.issue_id self.accept() else: self.cover_widget.update_content() @@ -615,3 +684,16 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # Update current record information self.update_row(row, item) + + def ratelimit_message(self, full_time: float, sleep_time: float) -> None: + self.log_output( + f"Rate limit reached: {full_time:.0f}s until next request. Waiting {sleep_time:.0f}s for ratelimit" + ) + + def log_output(self, text: str) -> None: + if self.iddialog is not None: + self.iddialog.textEdit.append(text.rstrip()) + self.iddialog.textEdit.ensureCursorVisible() + QtCore.QCoreApplication.processEvents() + QtCore.QCoreApplication.processEvents() + QtCore.QCoreApplication.processEvents() diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 2ea3b1b..04cc2a3 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -77,7 +77,7 @@ class TaggerWindow(QtWidgets.QMainWindow): appName = "ComicTagger" version = ctversion.version ratelimit = QtCore.pyqtSignal(float, float) - finish = QtCore.pyqtSignal(GenericMetadata, str) + query_finished = QtCore.pyqtSignal(GenericMetadata, str) def __init__( self, @@ -290,7 +290,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.page_list_editor.set_blur(self.config[0].General__blur) self.ratelimit.connect(self.on_ratelimit) - self.finish.connect(self.finish_query) + self.query_finished.connect(self.apply_query_metadata) def _sync_blur(*args: Any) -> None: self.config[0].General__blur = self.page_list_editor.blur @@ -1073,7 +1073,7 @@ class TaggerWindow(QtWidgets.QMainWindow): issue_count = utils.xlate_int(self.leIssueCount.text()) - selector = SeriesSelectionWindow( + self.selector = SeriesSelectionWindow( self, self.config[0], self.current_talker(), @@ -1085,13 +1085,16 @@ class TaggerWindow(QtWidgets.QMainWindow): autoselect, literal, ) + self.selector.ratelimit.connect(self.on_ratelimit) - selector.setWindowTitle(f"Search: '{series_name}' - Select Series") + self.selector.setWindowTitle(f"Search: '{series_name}' - Select Series") + self.selector.finished.connect(self.finish_query) - selector.setModal(True) - selector.exec() + self.selector.setModal(True) + self.selector.open() - if selector.result(): + def finish_query(self, result) -> None: + if result and self.selector: class QueryThread(QtCore.QThread): def __init__( @@ -1126,15 +1129,15 @@ class TaggerWindow(QtWidgets.QMainWindow): self.querythread = QueryThread( self.current_talker(), - selector.issue_id, - selector.series_id, - selector.issue_number, - self.finish, + self.selector.issue_id, + self.selector.series_id, + self.selector.issue_number, + self.query_finished, self.ratelimit, ) self.querythread.start() - def finish_query(self, new_metadata: GenericMetadata, issue_number: str) -> None: + def apply_query_metadata(self, new_metadata: GenericMetadata, issue_number: str) -> None: # we should now have a series ID # QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor)) QtWidgets.QApplication.restoreOverrideCursor()