From 76c6fe40592f99dc2ae627a0f1c7a9045de34ddd Mon Sep 17 00:00:00 2001 From: 27942 Date: Mon, 26 Jan 2026 11:07:40 +0800 Subject: [PATCH] hahaa --- gui_app.py | 891 +++++++++++++++++++++-------------------------------- 1 file changed, 352 insertions(+), 539 deletions(-) diff --git a/gui_app.py b/gui_app.py index fab56ae..58fcc98 100644 --- a/gui_app.py +++ b/gui_app.py @@ -793,6 +793,14 @@ class MainWindow(QMainWindow): self._table_view_resize_timer = QTimer() self._table_view_resize_timer.setSingleShot(True) self._table_view_resize_timer.timeout.connect(self._delayed_update_table_view_ratios) + if not hasattr(self, '_auto_resize_timer'): + self._auto_resize_timer = QTimer() + self._auto_resize_timer.setSingleShot(True) + self._auto_resize_timer.timeout.connect(self._auto_resize_table_columns) + if not hasattr(self, '_auto_resize_table_view_timer'): + self._auto_resize_table_view_timer = QTimer() + self._auto_resize_table_view_timer.setSingleShot(True) + self._auto_resize_table_view_timer.timeout.connect(self._auto_resize_table_view_columns) self.setWindowTitle("拼多多自动化发布工具") self.setGeometry(100, 100, 1000, 800) @@ -1254,9 +1262,6 @@ class MainWindow(QMainWindow): table_layout.addWidget(self.table_empty_label) pagination_row = QHBoxLayout() - self.table_select_all = CheckBox("全选当前页") - self.table_select_all.stateChanged.connect(self.toggle_select_all_rows) - pagination_row.addWidget(self.table_select_all) pagination_row.addStretch() self.page_size_combo = QComboBox() self.page_size_combo.addItems(["10", "20", "50", "100"]) @@ -1825,19 +1830,6 @@ class MainWindow(QMainWindow): "文件路径": cell(8), }) - def update_table_selection_count(self): - """更新已选行数量""" - if not hasattr(self, "table_select_count"): - return - if self.use_model_view and self.table_view.selectionModel(): - rows = set(idx.row() for idx in self.table_view.selectionModel().selectedRows()) - self.table_select_count.setText(f"已选: {len(rows)}") - return - rows = set() - for r in self.config_table.selectedRanges(): - rows.update(range(r.topRow(), r.bottomRow() + 1)) - self.table_select_count.setText(f"已选: {len(rows)}") - def _save_splitter_sizes(self): """保存分割器尺寸""" if not hasattr(self, "config_splitter"): @@ -1857,13 +1849,6 @@ class MainWindow(QMainWindow): # 首次默认:配置区偏大 self.config_splitter.setSizes([450, 550]) - def toggle_select_all_rows(self): - """全选/取消全选当前页""" - if self.table_select_all.isChecked(): - self.config_table.selectAll() - else: - self.config_table.clearSelection() - def change_page_size(self, value): """修改分页大小""" try: @@ -2016,7 +2001,7 @@ class MainWindow(QMainWindow): src_index = self.table_proxy.mapToSource(self.table_proxy.index(row, 0)) if src_index.isValid(): self.table_model.removeRows(src_index.row(), 1) - self.update_table_selection_count() + self._update_checked_count() return ranges = self.config_table.selectedRanges() if not ranges: @@ -2275,19 +2260,14 @@ class MainWindow(QMainWindow): if item: self.config_table.scrollToItem(item) - def _apply_schedule_intervals(self, configs_with_rows): + def _apply_schedule_intervals(self, configs_with_indices): """按多多ID应用定时发布+间隔时间规则 - 规则说明: - 1. 如果某条有定时时间,就按照定时时间发布,并记录为基准时间 - 2. 如果某条没有定时时间,也没有间隔时间,就是立即发布(不设置定时) - 3. 如果某条没有定时时间,但有间隔时间,就是在上一条发布时间的基础上加上间隔时间发布 - - 注意:按多多ID分组,同一个多多ID的所有数据按行顺序处理 + configs_with_indices: [{"config_index": config_index, "config": config}, ...] """ from collections import defaultdict grouped = defaultdict(list) - for item in configs_with_rows: + for item in configs_with_indices: config = item["config"] user_id = config.get("多多id", "") if not user_id: @@ -2296,12 +2276,13 @@ class MainWindow(QMainWindow): updated_count = 0 for user_id, items in grouped.items(): - items.sort(key=lambda x: x["row_idx"]) + # 按原始配置列表中的顺序处理 + items.sort(key=lambda x: x["config_index"]) base_time = None # 上一条的发布时间(基准时间) for entry in items: config = entry["config"] - row_idx = entry["row_idx"] + config_index = entry["config_index"] schedule_text = (config.get("定时发布") or "").strip() interval_value = config.get("间隔时间", 0) @@ -2314,7 +2295,6 @@ class MainWindow(QMainWindow): # 情况1:当前条目有定时时间 -> 使用该定时时间,并记录为基准时间 if parsed_time: base_time = parsed_time - # 不修改当前条目的定时时间,保持用户设置的原值 # 情况2:当前条目没有定时时间,但有间隔时间 elif not parsed_time and current_interval > 0: @@ -2322,21 +2302,10 @@ class MainWindow(QMainWindow): # 有基准时间 -> 新时间 = 基准时间 + 间隔时间 base_time = base_time + timedelta(seconds=current_interval) new_text = self._format_schedule_time(base_time) - config["定时发布"] = new_text - # 同步到configs(使用分页映射) - if self.page_row_indices and row_idx < len(self.page_row_indices): - config_index = self.page_row_indices[row_idx] - else: - config_index = row_idx - if config_index < len(self.configs): - self.configs[config_index]["定时发布"] = new_text - # 第0列为勾选框,定时发布列为第4列 - self._update_table_cell(row_idx, 4, new_text, highlight=True) + + # 更新配置和表格单元格(如果可见) + self._update_table_cell(config_index, 4, new_text, highlight=True, is_config_index=True) updated_count += 1 - # 如果没有基准时间,则保持空(立即发布) - - # 情况3:当前条目没有定时时间,也没有间隔时间 -> 立即发布(保持空) - # 不做任何处理,保持原样 return updated_count @@ -2400,92 +2369,96 @@ class MainWindow(QMainWindow): return "" return dt.strftime("%Y-%m-%d %H:%M:%S") - def _update_table_cell(self, row, col, value, highlight=False): - """更新表格单元格并同步configs""" + def _update_table_cell(self, index, col, value, highlight=False, is_config_index=False): + """更新配置数据及其对应的表格单元格(如果当前可见) + + index: 行号 (is_config_index=False) 或 配置列表索引 (is_config_index=True) + """ try: - # 临时断开 itemChanged 信号,防止递归 - self.config_table.itemChanged.disconnect(self.on_table_item_changed) - try: - item = QTableWidgetItem(str(value)) - item.setTextAlignment(Qt.AlignCenter) # 居中对齐 - item.setFlags(item.flags() & ~Qt.ItemIsEditable) - if highlight: - item.setBackground(QColor("#E6F4FF")) - self.config_table.setItem(row, col, item) - # 同步到配置(使用分页映射,且列索引已包含勾选框) - if self.page_row_indices and row < len(self.page_row_indices): - config_index = self.page_row_indices[row] - else: - config_index = row - if config_index < len(self.configs): - col_to_field = { - 4: "定时发布", # 第0列为勾选框 - 8: "情况", - 9: "文件路径", - } - field = col_to_field.get(col) - if field: - self.configs[config_index][field] = str(value) - finally: - # 重新连接信号 - self.config_table.itemChanged.connect(self.on_table_item_changed) + # 1. 确定配置索引和表格行号 + if is_config_index: + config_index = index + row_idx = self.page_row_indices.index(config_index) if (hasattr(self, 'page_row_indices') and config_index in self.page_row_indices) else -1 + else: + row_idx = index + config_index = self.page_row_indices[row_idx] if (hasattr(self, 'page_row_indices') and row_idx < len(self.page_row_indices)) else row_idx + + # 2. 更新数据源 self.configs + if 0 <= config_index < len(self.configs): + col_to_field = { + 4: "定时发布", + 8: "情况", + 9: "文件路径", + } + field = col_to_field.get(col) + if field: + self.configs[config_index][field] = str(value) + + # 3. 如果行可见,更新 UI + if row_idx >= 0 and row_idx < self.config_table.rowCount(): + # 临时断开 itemChanged 信号,防止递归 + self.config_table.itemChanged.disconnect(self.on_table_item_changed) + try: + item = QTableWidgetItem(str(value)) + item.setTextAlignment(Qt.AlignCenter) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + if highlight: + item.setBackground(QColor("#E6F4FF")) + self.config_table.setItem(row_idx, col, item) + finally: + self.config_table.itemChanged.connect(self.on_table_item_changed) except Exception as e: - logger.warning(f"更新表格单元格失败: {e}") - # 确保信号重新连接 + logger.warning(f"更新单元格失败: {e}") try: self.config_table.itemChanged.connect(self.on_table_item_changed) - except: - pass + except: pass - def _update_table_status(self, row_idx, status): - """更新表格中指定行的状态(情况列)""" + def _update_table_status(self, index, status, is_config_index=False): + """更新状态列及其对应的表格单元格 + + index: 行号 (is_config_index=False) 或 配置列表索引 (is_config_index=True) + """ try: - # 检查表格是否存在 - if not hasattr(self, 'config_table'): - return - - if row_idx < 0 or row_idx >= self.config_table.rowCount(): - return - - # 临时断开 itemChanged 信号,防止递归 - self.config_table.itemChanged.disconnect(self.on_table_item_changed) - try: - # 第8列是"情况"列(第0列为勾选框) - status_item = QTableWidgetItem(status) - status_item.setTextAlignment(Qt.AlignCenter) # 居中对齐 - status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable) - - # 根据状态设置不同的颜色 - if status == "已完成": - status_item.setBackground(QColor("#D4EDDA")) # 浅绿色 - status_item.setForeground(QColor("#155724")) # 深绿色文字 - elif status == "失败": - status_item.setBackground(QColor("#F8D7DA")) # 浅红色 - status_item.setForeground(QColor("#721C24")) # 深红色文字 - elif status == "执行中": - status_item.setBackground(QColor("#D1ECF1")) # 浅蓝色 - status_item.setForeground(QColor("#0C5460")) # 深蓝色文字 - - self.config_table.setItem(row_idx, 8, status_item) - - # 同步更新configs(使用分页映射) - if hasattr(self, 'configs'): - if self.page_row_indices and row_idx < len(self.page_row_indices): - config_index = self.page_row_indices[row_idx] - else: - config_index = row_idx - if config_index < len(self.configs): - self.configs[config_index]["情况"] = status - finally: - # 重新连接信号 - self.config_table.itemChanged.connect(self.on_table_item_changed) + # 1. 确定配置索引和表格行号 + if is_config_index: + config_index = index + row_idx = self.page_row_indices.index(config_index) if (hasattr(self, 'page_row_indices') and config_index in self.page_row_indices) else -1 + else: + row_idx = index + config_index = self.page_row_indices[row_idx] if (hasattr(self, 'page_row_indices') and row_idx < len(self.page_row_indices)) else row_idx + + # 2. 更新数据源 + if 0 <= config_index < len(self.configs): + self.configs[config_index]["情况"] = status + + # 3. 如果可见,更新 UI + if row_idx >= 0 and row_idx < self.config_table.rowCount(): + self.config_table.itemChanged.disconnect(self.on_table_item_changed) + try: + # 第8列是"情况"列 + status_item = QTableWidgetItem(status) + status_item.setTextAlignment(Qt.AlignCenter) + status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable) + + # 颜色逻辑 + if status == "已完成": + status_item.setBackground(QColor("#D4EDDA")) + status_item.setForeground(QColor("#155724")) + elif status == "失败": + status_item.setBackground(QColor("#F8D7DA")) + status_item.setForeground(QColor("#721C24")) + elif status == "执行中": + status_item.setBackground(QColor("#D1ECF1")) + status_item.setForeground(QColor("#0C5460")) + + self.config_table.setItem(row_idx, 8, status_item) + finally: + self.config_table.itemChanged.connect(self.on_table_item_changed) except Exception as e: - logger.error(f"更新表格状态失败(行{row_idx}): {e}") - # 确保信号重新连接 + logger.error(f"更新状态失败: {e}") try: self.config_table.itemChanged.connect(self.on_table_item_changed) - except: - pass + except: pass def _highlight_color(self): @@ -2821,13 +2794,11 @@ class MainWindow(QMainWindow): self.log_match_status.setText(f"匹配: {count}") def _apply_table_column_widths(self): - """应用配置表列宽(根据内容自适应,所有列按比例填充剩余空间)""" + """应用配置表列宽(仅按窗口宽度自适应,不根据内容变化)""" header = self.config_table.horizontalHeader() header.setStretchLastSection(True) - # 先使用ResizeToContents模式根据内容自适应列宽 - header.setSectionResizeMode(QHeaderView.ResizeToContents) - + # 禁用根据内容自适应,防止导入或更新数据时列宽剧烈跳动 # 设置最小列宽,确保内容可见 min_widths = { 0: 50, # 勾选框 @@ -2844,20 +2815,22 @@ class MainWindow(QMainWindow): 11: 100 # 操作 } - # 应用最小宽度限制 - for col, min_width in min_widths.items(): - if col < self.config_table.columnCount(): - current_width = self.config_table.columnWidth(col) - if current_width < min_width: - self.config_table.setColumnWidth(col, min_width) - # 切换到Interactive模式,允许手动拖拽调整 header.setSectionResizeMode(0, QHeaderView.Fixed) # 勾选框固定 for col in range(1, self.config_table.columnCount()): header.setSectionResizeMode(col, QHeaderView.Interactive) + if col in min_widths: + # 仅在第一次或比例未建立时设置初始宽度 + if not self._column_width_ratios: + self.config_table.setColumnWidth(col, min_widths[col]) - # 计算并保存列宽比例(用于自动调整) - self._update_column_width_ratios() + # 如果已经有保存的比例,直接按比例缩放 + if self._column_width_ratios: + self._auto_resize_table_columns() + else: + # 第一次加载,根据当前设置的初始宽度计算比例 + self._update_column_width_ratios() + self._auto_resize_table_columns() def _update_column_width_ratios(self): """更新列宽比例""" @@ -2931,21 +2904,19 @@ class MainWindow(QMainWindow): def on_table_geometry_changed(self): """表格几何形状改变时的回调(窗口大小改变)""" if not self._is_manual_resize: - # 延迟执行自动调整,避免频繁触发 - QTimer.singleShot(50, self._auto_resize_table_columns) + # 使用独立的定时器进行防抖,避免频繁触发导致卡顿 + self._auto_resize_timer.stop() + self._auto_resize_timer.start(50) def _apply_table_view_column_widths(self): - """应用 Model/View 列宽(根据内容自适应,所有列按比例填充剩余空间)""" + """应用 Model/View 列宽(仅按窗口宽度自适应,不根据内容变化)""" if not self.table_view.model(): return header = self.table_view.horizontalHeader() header.setStretchLastSection(True) - # 先使用ResizeToContents模式根据内容自适应列宽 - header.setSectionResizeMode(QHeaderView.ResizeToContents) - - # 设置最小列宽,确保内容可见 + # 禁用根据内容自适应 min_widths = { 0: 80, # 多多ID 1: 60, # 序号 @@ -2960,19 +2931,18 @@ class MainWindow(QMainWindow): 10: 100 # 操作 } - # 应用最小宽度限制 - for col, min_width in min_widths.items(): - if col < self.table_view.model().columnCount(): - current_width = self.table_view.columnWidth(col) - if current_width < min_width: - self.table_view.setColumnWidth(col, min_width) - - # 切换到Interactive模式,允许手动拖拽调整 + # 切换到Interactive模式 for col in range(self.table_view.model().columnCount()): header.setSectionResizeMode(col, QHeaderView.Interactive) + if col in min_widths and not self._table_view_column_width_ratios: + self.table_view.setColumnWidth(col, min_widths[col]) - # 计算并保存列宽比例(用于自动调整) - self._update_table_view_column_width_ratios() + # 按比例分配或初始化比例 + if self._table_view_column_width_ratios: + self._auto_resize_table_view_columns() + else: + self._update_table_view_column_width_ratios() + self._auto_resize_table_view_columns() def _update_table_view_column_width_ratios(self): """更新Model/View模式的列宽比例""" @@ -3043,8 +3013,9 @@ class MainWindow(QMainWindow): def on_table_view_geometry_changed(self): """Model/View模式表格几何形状改变时的回调(窗口大小改变)""" if not self._is_table_view_manual_resize: - # 延迟执行自动调整,避免频繁触发 - QTimer.singleShot(50, self._auto_resize_table_view_columns) + # 使用独立的定时器进行防抖,避免频繁触发导致卡顿 + self._auto_resize_table_view_timer.stop() + self._auto_resize_table_view_timer.start(50) def _show_infobar(self, level, title, content): """显示提示条""" @@ -3304,8 +3275,11 @@ class MainWindow(QMainWindow): # 设置焦点到第一个输入框 self.add_user_id_input.setFocus() - def update_table(self): - """更新配置表格""" + def update_table(self, skip_sync=False): + """更新配置表格。skip_sync=True 时跳过 表格→configs 同步(如刚由更新数据写入 configs 后刷新表格)。""" + if not self.use_model_view and not skip_sync: + self._sync_configs_from_table() + self.is_updating_table = True total_rows = len(self.configs) # 设置最小显示行数,即使没有数据也显示空行 @@ -3324,8 +3298,6 @@ class MainWindow(QMainWindow): for btn in [self.page_first_btn, self.page_prev_btn, self.page_next_btn, self.page_last_btn, self.page_size_combo]: btn.setEnabled(True) - if hasattr(self, "table_select_all"): - self.table_select_all.setEnabled(True) total_pages = max(1, (total_rows + self.page_size - 1) // self.page_size) if self.current_page > total_pages: self.current_page = total_pages @@ -3386,7 +3358,7 @@ class MainWindow(QMainWindow): if hasattr(self, "page_info_label"): self.page_info_label.setText(f"第 {self.current_page}/{total_pages} 页") self.is_updating_table = False - self.update_table_selection_count() + self._update_checked_count() def _setup_model_view(self): """切换到大数据量 Model/View 模式""" @@ -3398,8 +3370,6 @@ class MainWindow(QMainWindow): self.table_proxy.setSourceModel(self.table_model) self.table_proxy.setFilterKeyColumn(-1) self.table_view.setModel(self.table_proxy) - if self.table_view.selectionModel(): - self.table_view.selectionModel().selectionChanged.connect(self.update_table_selection_count) self.table_delegate = TableActionDelegate(self.table_view, self._edit_row_from_view, self._delete_row_from_view) self.table_view.setItemDelegate(self.table_delegate) @@ -3419,13 +3389,12 @@ class MainWindow(QMainWindow): for btn in [self.page_first_btn, self.page_prev_btn, self.page_next_btn, self.page_last_btn, self.page_size_combo]: btn.setEnabled(False) - if hasattr(self, "table_select_all"): - self.table_select_all.setEnabled(False) if hasattr(self, "table_empty_label"): self.table_empty_label.setVisible(len(self.configs) == 0) if hasattr(self, "page_info_label"): total_rows = len(self.configs) self.page_info_label.setText(f"大数据模式:{total_rows} 行") + self._update_checked_count() self._show_infobar("warning", "提示", "数据量较大,已切换到Model/View模式(部分功能受限)") def _edit_row_from_view(self, index): @@ -3502,58 +3471,60 @@ class MainWindow(QMainWindow): self.log_text.append("=" * 50) self.log_text.append("开始批量更新文件路径...") self.log_text.append(f"共有 {len(self.configs)} 行配置需要处理") + self.log_text.append(f"搜索根目录: {folder_path} (结构: 根目录/多多ID/序号(-xxx).mp4 或 序号(-xxx)/)") total_found_files = 0 rows_with_files = 0 - + skip_no_id = 0 + no_match_log = [] + # 标记正在更新,防止触发信号 self.is_updating_table = True # 遍历所有配置项进行更新 for config_idx, config in enumerate(self.configs): - # 验证必填字段 user_id = str(config.get('多多id', '')).strip() index = str(config.get('序号', '')).strip() if not user_id or not index: + skip_no_id += 1 continue - # 查找该配置对应的文件 found_files = self._find_files_for_config(config, folder_path) if found_files: - # 将找到的文件路径拼接成字符串(多个文件用分号分隔) file_paths_str = "; ".join([str(f['path']) for f in found_files]) config['文件路径'] = file_paths_str - rows_with_files += 1 total_found_files += len(found_files) else: config['文件路径'] = "" + no_match_log.append(f"多多ID={user_id} 序号={index}") - # 更新 UI 表格显示 + # 更新 UI 表格显示(跳过 sync:configs 已由更新数据写入,勿用表格回写覆盖) if self.use_model_view: if self.table_model: self.table_model.layoutChanged.emit() else: - self.update_table() + self.update_table(skip_sync=True) # 结束更新 self.is_updating_table = False - # 计算勾选行的统计数据(用于状态栏和卡片) - checked_with_files = sum(1 for config in self.configs - if config.get('勾选', False) and config.get('文件路径', '').strip()) - checked_file_count = sum(len(config.get('文件路径', '').split(';')) - for config in self.configs - if config.get('勾选', False) and config.get('文件路径', '').strip()) - + if skip_no_id: + self.log_text.append(f"跳过 {skip_no_id} 行(多多ID或序号为空)") + if no_match_log: + self.log_text.append(f"未匹配到文件 {len(no_match_log)} 行:") + for s in no_match_log[:20]: + self.log_text.append(f" · {s}") + if len(no_match_log) > 20: + self.log_text.append(f" ... 还有 {len(no_match_log) - 20} 行") self.log_text.append("=" * 50) self.log_text.append(f"更新完成!总计: {rows_with_files}/{len(self.configs)} 行匹配到文件,共 {total_found_files} 个文件") - self.update_status_label.setText(f"已更新: {checked_with_files}行,{checked_file_count}个文件") + self.update_status_label.setText(f"已更新: {rows_with_files}行,{total_found_files}个文件") self.update_status_label.setStyleSheet("color: #4CAF50; font-size: 10px;") - self.set_status_cards(update_text=f"已更新: {checked_with_files}行") + self.set_status_cards(update_text=f"已更新: {rows_with_files}行") self._update_status_statistics() # 更新映射 @@ -3564,13 +3535,13 @@ class MainWindow(QMainWindow): self.log_text.append("开始计算间隔时间并更新定时发布时间...") configs_with_rows = [] for i, cfg in enumerate(self.configs): - configs_with_rows.append({"row_idx": i, "config": cfg}) + configs_with_rows.append({"config_index": i, "config": cfg}) updated_count = self._apply_schedule_intervals(configs_with_rows) if updated_count > 0: self.log_text.append(f"✓ 已自动计算并更新 {updated_count} 行的定时发布时间") if not self.use_model_view: - self.update_table() + self.update_table(skip_sync=True) elif self.table_model: self.table_model.layoutChanged.emit() @@ -3587,127 +3558,124 @@ class MainWindow(QMainWindow): import traceback traceback.print_exc() + def _index_matches_name(self, index, name_or_stem): + """序号与文件名/文件夹名是否匹配。支持:序号、序号.mp4、序号-xxx、01 与 1 数字等价。""" + if not name_or_stem or not index: + return False + s = str(name_or_stem).strip() + if s == index: + return True + if s.startswith(index + "-"): + return True + parts = s.split("-") + part0 = parts[0].strip() if parts else "" + if part0 == index: + return True + try: + if int(s) == int(index): + return True + if part0 and int(part0) == int(index): + return True + except (ValueError, TypeError): + pass + return False + def _find_files_for_config(self, config, folder_path): - """根据配置查找文件(辅助方法)""" + """根据配置查找文件(辅助方法)。目录结构:文件夹路径/多多ID/[(子目录)/] 序号(-xxx).mp4 或 序号(-xxx)/""" found_files = [] - # 参数验证 if not config: logger.warning("配置为空,无法查找文件") return found_files - if not folder_path or not os.path.exists(folder_path): logger.warning(f"文件夹路径无效或不存在: {folder_path}") return found_files - index = str(config.get('序号', '')).strip() if not index: logger.warning("序号为空,无法查找文件") return found_files - + user_id = str(config.get('多多id', '')).strip() + if not user_id: + logger.warning("多多ID为空,无法查找文件") + return found_files + video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm'] try: - # 检查文件夹是否可读 if not os.access(folder_path, os.R_OK): logger.error(f"没有权限读取文件夹: {folder_path}") return found_files - - # 遍历最外层文件夹下的所有子文件夹 try: subdirs = [f for f in os.listdir(folder_path) if os.path.isdir(os.path.join(folder_path, f))] - except PermissionError: - logger.error(f"没有权限访问文件夹: {folder_path}") - return found_files - except Exception as e: + except (PermissionError, OSError) as e: logger.error(f"读取文件夹失败: {e}") return found_files - # 找到匹配当前多多ID的文件夹 target_subdir = None - user_id = str(config.get('多多id', '')).strip() - if not user_id: - logger.warning("多多ID为空,无法查找文件") - return found_files - for subdir_name in subdirs: if subdir_name == user_id: target_subdir = os.path.join(folder_path, subdir_name) break - if not target_subdir or not os.path.exists(target_subdir): return found_files - # 扫描该文件夹下的文件 - try: - items = os.listdir(target_subdir) - except PermissionError: - logger.error(f"没有权限访问子文件夹: {target_subdir}") - return found_files - except Exception as e: - logger.error(f"读取子文件夹失败: {e}") - return found_files + def append_file(fpath): + path_obj = Path(fpath) + if not path_obj.exists(): + return + if path_obj.is_file(): + if any(path_obj.suffix.lower() == ext for ext in video_extensions): + found_files.append({ + "url": config.get('达人链接', ''), + "user_id": user_id, + "time_start": config.get('定时发布', '') if config.get('定时发布') else None, + "ht": config.get('话题', ''), + "index": index, + "path": path_obj + }) + elif path_obj.is_dir(): + found_files.append({ + "url": config.get('达人链接', ''), + "user_id": user_id, + "time_start": config.get('定时发布', '') if config.get('定时发布') else None, + "ht": config.get('话题', ''), + "index": index, + "path": path_obj + }) - for item_name in items: + search_dirs = [target_subdir] + try: + for sub in os.listdir(target_subdir): + p = os.path.join(target_subdir, sub) + if os.path.isdir(p): + search_dirs.append(p) + except (PermissionError, OSError): + pass + + for search_dir in search_dirs: try: - item_path = os.path.join(target_subdir, item_name) - - # 跳过隐藏文件/文件夹 + items = os.listdir(search_dir) + except (PermissionError, OSError): + continue + for item_name in items: if item_name.startswith('.'): continue - - name_parts = item_name.split("-") - - # 检查序号是否匹配 - match = False - if len(name_parts) > 0: - part0 = name_parts[0].strip() - if part0 == index: - match = True - else: - # 尝试数字匹配(处理 01 与 1 的匹配) - try: - if int(part0) == int(index): - match = True - except: - pass - - if match: + item_path = os.path.join(search_dir, item_name) + try: path_obj = Path(item_path) - - # 确保路径存在 - if not path_obj.exists(): - continue - if path_obj.is_file(): - # 检查是否为视频文件 - if any(path_obj.suffix.lower() == ext for ext in video_extensions): - found_files.append({ - "url": config.get('达人链接', ''), - "user_id": user_id, - "time_start": config.get('定时发布', '') if config.get('定时发布') else None, - "ht": config.get('话题', ''), - "index": index, - "path": path_obj - }) - elif path_obj.is_dir(): - # 如果是文件夹,可能是图片文件夹 - found_files.append({ - "url": config.get('达人链接', ''), - "user_id": user_id, - "time_start": config.get('定时发布', '') if config.get('定时发布') else None, - "ht": config.get('话题', ''), - "index": index, - "path": path_obj - }) - except Exception as e: - logger.warning(f"处理文件 {item_name} 时出错: {e}") - continue # 继续处理其他文件 - + base = path_obj.stem + else: + base = item_name + if not self._index_matches_name(index, base): + continue + append_file(item_path) + except Exception as e: + logger.warning(f"处理 {item_name} 时出错: {e}") + continue except Exception as e: logger.error(f"查找文件失败: {e}", exc_info=True) - return found_files def execute_task(self): @@ -3748,10 +3716,14 @@ class MainWindow(QMainWindow): def execute_batch_from_excel(self): """从 Excel配置批量执行(自动判断相同多多ID的mp4文件,批量上传)""" + # 执行前强制同步当前页数据,确保手动修改的内容被包含 + if not self.use_model_view: + self._sync_configs_from_table() + # 开始新任务时重置当前批次状态(不累计历史数据) self.set_status_cards(pending=0, running=0, success=0, failed=0) - # 获取文件夹路径,如果为空则使用默认路径 + # 获取文件夹路径 folder_path = self.folder_path_input.text().strip() if not folder_path: folder_path = get_default_folder_path() @@ -3760,307 +3732,161 @@ class MainWindow(QMainWindow): self._show_infobar("warning", "警告", f"文件夹路径不存在: {folder_path}") return - # 从表格中获取所有配置(使用用户修改后的值) - if self.config_table.rowCount() == 0: - self._show_infobar("warning", "警告", "配置列表为空,请先导入Excel配置") - return - - # 筛选勾选的配置 - checked_configs = [config for config in self.configs if config.get('勾选', False)] - if not checked_configs: + # 筛选勾选的待处理配置(直接从 self.configs 获取最新数据) + checked_indices = [i for i, cfg in enumerate(self.configs) if cfg.get('勾选', False)] + if not checked_indices: self._show_infobar("warning", "提示", "请先勾选需要上传的数据行") return - # 统计已完成的数量 - completed_count = sum(1 for config in checked_configs - if '已完成' in config.get('情况', '') - or '完成' in config.get('情况', '') - or '成功' in config.get('情况', '')) - pending_count = len(checked_configs) - completed_count + # 分析勾选项 + all_configs_with_files = [] + configs_to_process = [] # 用于计算间隔时间 - if pending_count == 0: - self._show_infobar("warning", "提示", f"已选择的 {len(checked_configs)} 行均为已完成状态,无需上传") - return + for idx in checked_indices: + config = self.configs[idx] + # 跳过状态为「已完成」的任务(仅上传勾选且未完成的项) + status = (config.get('情况') or '').strip() + if '已完成' in status: + continue + + # 验证必填 + if not config.get('多多id') or not config.get('序号'): + continue + + configs_to_process.append({"config_index": idx, "config": config}) + + if not configs_to_process: + self._show_infobar("warning", "提示", "所选项已完成或数据不完整") + return + + # 应用定时发布 + 间隔时间逻辑(跨分页全局应用) + self._apply_schedule_intervals(configs_to_process) + self.log_text.append("=" * 50) self.log_text.append(f"开始分析配置,准备批量上传...") - self.log_text.append(f"已选择: {len(checked_configs)} 行 | 待上传: {pending_count} 行 | 已完成: {completed_count} 行(将跳过)") - # 收集所有配置及其对应的文件 - all_configs_with_files = [] - configs_with_rows = [] - video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm'] - - for row_idx in range(self.config_table.rowCount()): - # 只处理被勾选的行(使用分页映射) - if self.page_row_indices and row_idx < len(self.page_row_indices): - config_index = self.page_row_indices[row_idx] - else: - config_index = row_idx - if config_index >= len(self.configs) or not self.configs[config_index].get('勾选', False): - continue - - config = self.get_config_from_table(row_idx) - if not config: - continue - - # 跳过状态为"已完成"或"成功"的行 - status = config.get('情况', '') - if '已完成' in status or '完成' in status or '成功' in status: - self.log_text.append(f"第 {row_idx + 1} 行:状态为【{status}】,已跳过") - continue - - # 验证必填字段 - if not config.get('多多id') or not config.get('序号'): - self.log_text.append(f"第 {row_idx + 1} 行:多多ID或序号为空,跳过") - continue - - # 添加文件夹路径 - config['文件夹路径'] = folder_path - configs_with_rows.append({"row_idx": row_idx, "config": config}) - - # 应用定时发布 + 间隔时间逻辑(按多多ID分组) - self._apply_schedule_intervals(configs_with_rows) - - for item in configs_with_rows: - row_idx = item["row_idx"] + for item in configs_to_process: + idx = item["config_index"] config = item["config"] - - # 从文件路径列读取文件路径 + + # 尝试获取文件 file_path_str = config.get('文件路径', '').strip() + found_files = [] if file_path_str: - # 文件路径可能用分号分隔(多个文件) file_paths = [p.strip() for p in file_path_str.split(';') if p.strip()] - files = [] for fp in file_paths: if os.path.exists(fp): - path_obj = Path(fp) - files.append({ + found_files.append({ "url": config.get('达人链接', ''), "user_id": config.get('多多id', ''), "time_start": config.get('定时发布', '') if config.get('定时发布') else None, "ht": config.get('话题', ''), "index": config.get('序号', ''), - "path": path_obj + "path": Path(fp) }) - - if files: - all_configs_with_files.append({ - 'config': config, - 'files': files, - 'row_idx': row_idx - }) - self.log_text.append(f"第 {row_idx + 1} 行:从文件路径列读取到 {len(files)} 个文件") - else: - # 如果文件路径列为空,尝试查找文件 - self.log_text.append(f"第 {row_idx + 1} 行:文件路径列为空,尝试查找文件...") + + if not found_files: + # 再次尝试实时查找 found_files = self._find_files_for_config(config, folder_path) - if found_files: - all_configs_with_files.append({ - 'config': config, - 'files': found_files, - 'row_idx': row_idx - }) - self.log_text.append(f"第 {row_idx + 1} 行:找到 {len(found_files)} 个文件") + + if found_files: + all_configs_with_files.append({ + 'config': config, + 'files': found_files, + 'config_index': idx + }) + self.log_text.append(f"索引 {idx+1} ({config.get('多多id')}): 准备上传 {len(found_files)} 个项目") if not all_configs_with_files: - skipped_count = len(checked_configs) - len(all_configs_with_files) - if skipped_count > 0: - self._show_infobar("warning", "警告", f"未找到任何文件,请先点击‘更新数据’按钮({skipped_count} 行已跳过)") - else: - self._show_infobar("warning", "警告", "未找到任何文件,请先点击‘更新数据’按钮") + self._show_infobar("warning", "警告", "未找到任何匹配文件,请先点击‘更新数据’按钮") return total_tasks = sum(len(item['files']) for item in all_configs_with_files) - pending_tasks = total_tasks - self.set_status_cards(pending=pending_tasks) + self.set_status_cards(pending=total_tasks) self.set_running_progress(0, total_tasks) - # 按多多ID分组 + # 按多多ID分组任务 from collections import defaultdict grouped_by_user_id = defaultdict(list) for item in all_configs_with_files: user_id = item['config'].get('多多id', '') grouped_by_user_id[user_id].append(item) - self.log_text.append("=" * 50) - self.log_text.append(f"按多多ID分组:共 {len(grouped_by_user_id)} 个不同的多多ID") - - # 检查是否有正在运行的线程 - try: - if self.worker_thread and self.worker_thread.isRunning(): - self._show_infobar("warning", "警告", "已有任务正在执行,请等待完成") - return - except Exception as e: - logger.warning(f"检查线程状态时出错: {e}") - # 如果检查失败,重置 worker_thread - self.worker_thread = None + # 检查线程 + if self.worker_thread and self.worker_thread.isRunning(): + self._show_infobar("warning", "警告", "已有任务正在执行,请等待完成") + return - # 禁用按钮 self.execute_btn.setEnabled(False) self.progress_bar.setVisible(True) self.progress_bar.setValue(0) - self.set_status_cards(running=1) - # 构建任务队列(不阻塞GUI线程) + # 构建任务队列 self.batch_task_queue = [] self.current_batch_task_index = 0 self.batch_total_tasks = total_tasks self.batch_processed = 0 self.batch_pending_tasks = total_tasks - # 处理每个多多ID组,构建任务队列 + video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm'] + for user_id, items in grouped_by_user_id.items(): - self.log_text.append(f"\n处理多多ID: {user_id},共 {len(items)} 个配置") - - # 收集该多多ID下的所有文件 all_files = [] - try: - for item in items: - files = item.get('files', []) - if files: - all_files.extend(files) - except Exception as e: - logger.warning(f"收集文件时出错: {e}") - continue + related_config_indices = [] + for item in items: + all_files.extend(item['files']) + if item['config_index'] not in related_config_indices: + related_config_indices.append(item['config_index']) - # 分离视频文件和图片文件夹 - video_files = [f for f in all_files if - f['path'].is_file() and any(f['path'].suffix.lower() == ext for ext in video_extensions)] + video_files = [f for f in all_files if f['path'].is_file() and any(f['path'].suffix.lower() == ext for ext in video_extensions)] image_folders = [f for f in all_files if f['path'].is_dir()] - self.log_text.append(f" 视频文件: {len(video_files)} 个") - self.log_text.append(f" 图片文件夹: {len(image_folders)} 个") - - # 使用第一个配置创建Pdd实例(因为同一个多多ID,配置应该相同) - if not items or len(items) == 0: - continue + # 准备工作线程需要的配置(同一个多多ID共用基准配置) first_config = items[0].get('config', {}) - if not first_config: - continue - pdd = Pdd( - url=first_config.get('达人链接', ''), - user_id=user_id, - time_start=first_config.get('定时发布', '') if first_config.get('定时发布') else None, - ht=first_config.get('话题', ''), - index=first_config.get('序号', ''), - title=first_config.get('标题', None) - ) - # 第一步:如果有多个视频文件(>1),批量上传所有视频 + # 1. 批量视频任务 if len(video_files) > 1: - # 检查是否都是mp4文件 - all_mp4 = all(f['path'].suffix.lower() == '.mp4' for f in video_files) - if all_mp4: - self.log_text.append(f" ✓ 检测到 {len(video_files)} 个mp4文件,批量上传所有视频(action1方法)") - else: - self.log_text.append(f" ✓ 检测到 {len(video_files)} 个视频文件,批量上传所有视频(action1方法)") - - # 添加到任务队列 - # 收集所有相关的行索引(根据文件中的index和user_id匹配) - related_row_indices = [] - try: - for video_file in video_files: - file_index = video_file.get('index', '') - for item in items: - if item.get('config', {}).get('序号', '') == file_index: - row_idx = item.get('row_idx') - if row_idx is not None and row_idx not in related_row_indices: - related_row_indices.append(row_idx) - break - except Exception as e: - logger.warning(f"收集行索引时出错: {e}") - related_row_indices = [] - self.batch_task_queue.append({ 'type': 'batch_video', 'config': first_config, 'files': video_files, 'user_id': user_id, 'count': len(video_files), - 'row_indices': related_row_indices # 添加行索引列表 + 'config_indices': related_config_indices }) elif len(video_files) == 1: - # 只有1个视频,单个上传 - self.log_text.append(f" → 只有1个视频文件,单个上传") - # 添加到任务队列 - # 收集相关的行索引 - related_row_indices = [] - try: - if video_files: - file_index = video_files[0].get('index', '') - for item in items: - if item.get('config', {}).get('序号', '') == file_index: - row_idx = item.get('row_idx') - if row_idx is not None: - related_row_indices.append(row_idx) - break - except Exception as e: - logger.warning(f"收集行索引时出错: {e}") - related_row_indices = [] - + # 只有1个视频,也要找对应的 config_index + vid_index = video_files[0].get('index', '') + matching_idx = next((it['config_index'] for it in items if it['config'].get('序号') == vid_index), related_config_indices[0]) self.batch_task_queue.append({ 'type': 'single_video', 'config': first_config, 'files': video_files, 'user_id': user_id, 'count': 1, - 'row_indices': related_row_indices # 添加行索引列表 + 'config_indices': [matching_idx] }) - # 第二步:如果有图片文件夹,逐个上传图片 - if image_folders: - self.log_text.append(f" → 准备上传 {len(image_folders)} 个图片文件夹") - for idx, img_folder in enumerate(image_folders, 1): - # 找到该图片文件夹对应的配置(通过序号匹配) - folder_index = img_folder.get('index', '') - matching_config = None - try: - for item in items: - config = item.get('config', {}) - if config.get('序号', '') == folder_index: - matching_config = config - break - except Exception as e: - logger.warning(f"查找匹配配置时出错: {e}") + # 2. 图片文件夹任务 + for img_folder in image_folders: + f_idx = img_folder.get('index', '') + matching_idx = next((it['config_index'] for it in items if it['config'].get('序号') == f_idx), related_config_indices[0]) + matching_config = next((it['config'] for it in items if it['config'].get('序号') == f_idx), first_config) + + self.batch_task_queue.append({ + 'type': 'image_folder', + 'config': matching_config, + 'files': [img_folder], + 'user_id': user_id, + 'count': 1, + 'config_indices': [matching_idx] + }) - # 如果找不到匹配的配置,使用第一个配置 - if not matching_config: - matching_config = first_config - - # 添加到任务队列 - # 找到对应的行索引 - related_row_indices = [] - try: - for item in items: - if item.get('config', {}).get('序号', '') == folder_index: - row_idx = item.get('row_idx') - if row_idx is not None: - related_row_indices.append(row_idx) - break - except Exception as e: - logger.warning(f"收集行索引时出错: {e}") - related_row_indices = [] - - self.batch_task_queue.append({ - 'type': 'image_folder', - 'config': matching_config, - 'files': [img_folder], - 'user_id': user_id, - 'folder_index': folder_index, - 'count': 1, - 'index': idx, - 'total': len(image_folders), - 'row_indices': related_row_indices # 添加行索引列表 - }) - - # 开始执行任务队列(异步,不阻塞GUI) if self.batch_task_queue: - self.log_text.append("=" * 50) - self.log_text.append(f"任务队列构建完成,共 {len(self.batch_task_queue)} 个任务") self._process_next_batch_task() else: - self._show_infobar("warning", "警告", "未找到任何任务") + self._show_infobar("warning", "警告", "任务分析失败,未构建有效队列") self.execute_btn.setEnabled(True) self.progress_bar.setVisible(False) @@ -4148,13 +3974,13 @@ class MainWindow(QMainWindow): # 更新表格状态为"执行中" try: - row_indices = task.get('row_indices', []) - if row_indices: - for row_idx in row_indices: + config_indices = task.get('config_indices', []) + if config_indices: + for cfg_idx in config_indices: try: - self._update_table_status(row_idx, "执行中") + self._update_table_status(cfg_idx, "执行中", is_config_index=True) except Exception as e: - logger.warning(f"更新状态失败(行{row_idx}): {e}") + logger.warning(f"更新状态失败(索引{cfg_idx}): {e}") except Exception as e: logger.warning(f"更新执行中状态失败: {e}") @@ -4189,7 +4015,7 @@ class MainWindow(QMainWindow): task = self.batch_task_queue[self.current_batch_task_index] task_type = task['type'] count = task.get('count', 1) - row_indices = task.get('row_indices', []) + config_indices = task.get('config_indices', []) # 更新进度 if success: @@ -4198,23 +4024,16 @@ class MainWindow(QMainWindow): elif task_type == 'single_video': self.log_text.append(f" ✓ 单个视频上传完成") elif task_type == 'image_folder': - idx = task.get('index', 0) - self.log_text.append(f" ✓ 图片文件夹 {idx} 上传完成") + self.log_text.append(f" ✓ 图片文件夹上传完成") - # 更新表格状态为"已完成" - for row_idx in row_indices: - try: - self._update_table_status(row_idx, "已完成") - except Exception as e: - logger.warning(f"更新状态失败(行{row_idx}): {e}") + # 更新状态为"已完成" + for cfg_idx in config_indices: + self._update_table_status(cfg_idx, "已完成", is_config_index=True) else: self.log_text.append(f" ✗ 任务失败: {message}") - # 更新表格状态为"失败" - for row_idx in row_indices: - try: - self._update_table_status(row_idx, "失败") - except Exception as e: - logger.warning(f"更新状态失败(行{row_idx}): {e}") + # 更新状态为"失败" + for cfg_idx in config_indices: + self._update_table_status(cfg_idx, "失败", is_config_index=True) # 更新统计 self.batch_processed += count @@ -4267,15 +4086,16 @@ class MainWindow(QMainWindow): return 0 def _rebuild_row_map(self): - """重建 多多ID+序号 -> 表格行号 映射""" + """重建 多多ID+序号 -> 表格行号 映射(仅针对当前页可见行)""" m = {} try: - if not hasattr(self, "config_table"): + if not hasattr(self, "config_table") or self.use_model_view: self._row_map_by_user_index = {} return for r in range(self.config_table.rowCount()): - user_id = self.get_cell_text(r, 0) - idx = self.get_cell_text(r, 1) + # 多多ID在第1列,序号在第2列(第0列是勾选框) + user_id = self.get_cell_text(r, 1) + idx = self.get_cell_text(r, 2) if user_id and idx: m[(user_id, idx)] = r except Exception: @@ -4283,10 +4103,7 @@ class MainWindow(QMainWindow): self._row_map_by_user_index = m def _on_worker_item_result(self, payload): - """ - 接收自动化侧逐条结果: - payload: {user_id, index, ok, reason, name, ...} - """ + """接收自动化侧逐条结果""" try: if not isinstance(payload, dict): return @@ -4299,23 +4116,19 @@ class MainWindow(QMainWindow): if not user_id or not idx: return - if not self._row_map_by_user_index: - self._rebuild_row_map() - - row = self._row_map_by_user_index.get((user_id, idx)) - if row is None: - # 尝试刷新一次映射 - self._rebuild_row_map() - row = self._row_map_by_user_index.get((user_id, idx)) - if row is None: - # 找不到对应行,仍然写日志 - self.log_text.append(f"[结果] user={user_id} 序号={idx} ok={ok} {reason}") - return - - self._update_table_status(row, "已完成" if ok else "失败") - # 记录每条的详细日志 - label = name if name else payload.get("path", "") or "" - self.log_text.append(f"[结果] R{row+1} user={user_id} 序号={idx} ok={ok} {label} {reason}") + # 全局搜索匹配的多多ID和序号 + found_config_idx = -1 + for i, cfg in enumerate(self.configs): + if str(cfg.get("多多id")) == user_id and str(cfg.get("序号")) == idx: + found_config_idx = i + break + + if found_config_idx != -1: + self._update_table_status(found_config_idx, "已完成" if ok else "失败", is_config_index=True) + label = name if name else payload.get("path", "") or "" + self.log_text.append(f"[结果] {user_id}-{idx}: {'✓' if ok else '✗'} {label} {reason}") + else: + self.log_text.append(f"[结果] {user_id}-{idx}: ok={ok} {reason} (未在列表中找到匹配项)") except Exception as e: logger.warning(f"处理单条结果失败: {e}")