feat: 完成Tab①采集旧数据

新增 CollectWorker,通过后台线程逐条采集旧标题和旧封面,成功后立即 db.set_collected 写库。

采集前按别名匹配账号并检测登录态;未匹配或未登录写 skipped,单条采集失败 mark_failed 后继续。

Tab① 增加采集/停止按钮和进度状态回传;补充 GUI 测试覆盖成功采集、未登录略过和未匹配略过;同步任务文档与 progress。
This commit is contained in:
chengma
2026-06-27 11:51:42 +08:00
parent 3bf42dc11e
commit 6e00b1b093
6 changed files with 311 additions and 9 deletions
+212 -1
View File
@@ -75,7 +75,7 @@ QTabBar::tab:hover:!selected {
if QT_IMPORT_ERROR is None:
from . import accounts, appconfig, db, excel
from . import accounts, appconfig, db, editor, excel
from . import config as account_config
@@ -204,13 +204,20 @@ if QT_IMPORT_ERROR is None:
self.current_batch_id = None
self.has_import_result = False
self.last_import_stats = None
self.collect_worker = None
self.collect_thread = None
self.import_button = QPushButton("导入 Excel...")
self.refresh_button = QPushButton("刷新")
self.collect_button = QPushButton("采集旧标题/旧封面")
self.stop_collect_button = QPushButton("停止")
self.stop_collect_button.setEnabled(False)
toolbar = QHBoxLayout()
toolbar.addWidget(self.import_button)
toolbar.addWidget(self.refresh_button)
toolbar.addWidget(self.collect_button)
toolbar.addWidget(self.stop_collect_button)
toolbar.addStretch(1)
self.summary_label = QLabel("未导入任务")
@@ -245,6 +252,8 @@ if QT_IMPORT_ERROR is None:
self.import_button.clicked.connect(self.import_excel)
self.refresh_button.clicked.connect(self.refresh_tasks)
self.collect_button.clicked.connect(self.collect_old_data)
self.stop_collect_button.clicked.connect(self.stop_collect)
self.show_all_button.clicked.connect(self.show_all_tasks)
self.show_unmatched_button.clicked.connect(self.show_unmatched_tasks)
@@ -306,6 +315,79 @@ if QT_IMPORT_ERROR is None:
self._update_summary(task_rows, account_rows)
self._update_empty_label(len(task_rows))
def collect_old_data(self, checked=False):
tasks = list(self.model.all_tasks)
if not tasks:
self._set_status("没有可采集任务")
return
worker = CollectWorker(tasks, db_path=self.db_path, config=self.config)
worker.progress.connect(self._on_collect_progress)
worker.row_updated.connect(self._on_collect_row_updated)
worker.log.connect(self._set_status)
worker.failed.connect(self._on_collect_failed)
worker.finished.connect(self._on_collect_finished)
worker.cancelled.connect(self._on_collect_cancelled)
thread = run_worker(worker, thread_name="CollectWorker", start=False)
thread.finished.connect(lambda: self._forget_collect_thread(thread))
self.collect_worker = worker
self.collect_thread = thread
self._set_collect_running(True)
thread.start()
def stop_collect(self, checked=False):
if self.collect_worker is not None:
self.collect_worker.cancel()
self._set_status("正在停止采集...")
def _set_collect_running(self, running):
self.import_button.setEnabled(not running)
self.refresh_button.setEnabled(not running)
self.collect_button.setEnabled(not running)
self.stop_collect_button.setEnabled(running)
def _forget_collect_thread(self, thread):
if self.collect_thread is thread:
self.collect_thread = None
self.collect_worker = None
def _on_collect_progress(self, payload):
self._set_status(
"采集进度:{done}/{total},成功{collected},略过{skipped},失败{failed}".format(
done=payload.get("done", 0),
total=payload.get("total", 0),
collected=payload.get("collected", 0),
skipped=payload.get("skipped", 0),
failed=payload.get("failed", 0),
)
)
def _on_collect_row_updated(self, task_id, fields):
self.refresh_tasks()
def _on_collect_failed(self, task_id, error):
self._set_status(f"任务 {task_id} 采集失败:{error}")
def _on_collect_finished(self, payload):
self._set_collect_running(False)
self.refresh_tasks()
self._set_status(
"采集完成:成功{collected},略过{skipped},失败{failed}".format(
collected=payload.get("collected", 0),
skipped=payload.get("skipped", 0),
failed=payload.get("failed", 0),
)
)
def _on_collect_cancelled(self, payload):
self._set_collect_running(False)
self.refresh_tasks()
self._set_status(
"采集已停止:完成{done}/{total}".format(
done=payload.get("done", 0),
total=payload.get("total", 0),
)
)
def show_all_tasks(self, checked=False):
self.model.set_filter_mode("all")
self._update_empty_label(len(self.model.all_tasks))
@@ -458,6 +540,135 @@ if QT_IMPORT_ERROR is None:
from .workers import BaseWorker, run_worker
class CollectWorker(BaseWorker):
"""Collect old title and cover for imported tasks."""
def __init__(self, tasks, db_path=None, config=None):
super().__init__()
self.tasks = list(tasks)
self.db_path = db_path
self.config = config
def execute(self):
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
account_by_alias = {
str(account.alias).strip(): account
for account in account_rows
if str(account.alias).strip()
}
eligible = [
task for task in self.tasks
if getattr(task, "stage", None) == "imported"
]
total = len(eligible)
collected = 0
skipped = 0
failed = 0
done = 0
for task in eligible:
if self.should_cancel():
break
account = account_by_alias.get(str(task.alias).strip())
if account is None:
skipped += 1
done += 1
reason = "别名未匹配账号"
db.mark_skipped(task.id, reason, path=self.db_path)
self.row_updated.emit(task.id, {"status": "skipped", "last_error": reason})
self._emit_progress(done, total, collected, skipped, failed)
continue
status = self._login_status(account)
if not status.get("logged_in"):
skipped += 1
done += 1
reason = self._login_skip_reason(status)
db.mark_skipped(task.id, reason, path=self.db_path)
self.row_updated.emit(task.id, {"status": "skipped", "last_error": reason})
self._emit_progress(done, total, collected, skipped, failed)
continue
try:
db.mark_running(task.id, "collect", path=self.db_path)
self.row_updated.emit(task.id, {"status": "running"})
result = editor.collect(
account,
{
"item_id": task.item_id,
"old_cover_path": self._old_cover_path(account, task),
},
)
db.set_collected(
task.id,
result.get("old_title", ""),
result.get("old_cover_path", ""),
path=self.db_path,
)
collected += 1
self.row_updated.emit(
task.id,
{
"stage": "collected",
"status": "success",
"old_title": result.get("old_title", ""),
"old_cover_path": result.get("old_cover_path", ""),
},
)
except Exception as exc:
failed += 1
error = str(exc) or exc.__class__.__name__
db.mark_failed(task.id, "collect", error, path=self.db_path)
self.failed.emit(task.id, error)
self.row_updated.emit(task.id, {"status": "failed", "last_error": error})
finally:
done += 1
self._emit_progress(done, total, collected, skipped, failed)
return {
"ok": failed == 0,
"total": total,
"done": done,
"collected": collected,
"skipped": skipped,
"failed": failed,
}
def _emit_progress(self, done, total, collected, skipped, failed):
self.progress.emit(
{
"done": done,
"total": total,
"collected": collected,
"skipped": skipped,
"failed": failed,
}
)
def _login_status(self, account):
try:
return accounts.detect_login(account, path=self.db_path, config=self.config)
except Exception as exc:
return {
"logged_in": False,
"reason": f"LOGIN_CHECK_FAILED: {exc}",
}
def _login_skip_reason(self, status):
reason = status.get("reason")
return f"账号未登录: {reason}" if reason else "账号未登录"
def _old_cover_path(self, account, task):
image_root = appconfig.image_dir(self.config)
return os.path.abspath(
os.path.join(
image_root,
account.slug,
f"{task.item_id}_old.jpg",
)
)
class AccountLoginCheckWorker(BaseWorker):
def __init__(self, account, db_path=None, config=None, timeout=8):
super().__init__()