feat: 完成Tab①采集旧数据
新增 CollectWorker,通过后台线程逐条采集旧标题和旧封面,成功后立即 db.set_collected 写库。 采集前按别名匹配账号并检测登录态;未匹配或未登录写 skipped,单条采集失败 mark_failed 后继续。 Tab① 增加采集/停止按钮和进度状态回传;补充 GUI 测试覆盖成功采集、未登录略过和未匹配略过;同步任务文档与 progress。
This commit is contained in:
+212
-1
@@ -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__()
|
||||
|
||||
Reference in New Issue
Block a user