gui
第一版完整版
@@ -6,7 +6,7 @@ a = Analysis(
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=['PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', 'qfluentwidgets', 'pandas', 'openpyxl', 'loguru', 'beautifulsoup4', 'curl_cffi', 'DrissionPage', 'requests', 'main'],
|
||||
hiddenimports=['PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', 'qfluentwidgets', 'pandas', 'openpyxl', 'loguru', 'beautifulsoup4', 'curl_cffi', 'DrissionPage', 'requests', 'main', 'gui_constants', 'gui_worker', 'gui_models'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
|
||||
@@ -827,6 +827,11 @@
|
||||
'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\gettext.py',
|
||||
'PYMODULE'),
|
||||
('glob', 'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\glob.py', 'PYMODULE'),
|
||||
('gui_constants',
|
||||
'C:\\Users\\27942\\Desktop\\haha\\gui_constants.py',
|
||||
'PYMODULE'),
|
||||
('gui_models', 'C:\\Users\\27942\\Desktop\\haha\\gui_models.py', 'PYMODULE'),
|
||||
('gui_worker', 'C:\\Users\\27942\\Desktop\\haha\\gui_worker.py', 'PYMODULE'),
|
||||
('gzip', 'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\gzip.py', 'PYMODULE'),
|
||||
('hashlib',
|
||||
'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\hashlib.py',
|
||||
|
||||
@@ -156,6 +156,9 @@ imports:
|
||||
• <a href="#enum">enum</a>
|
||||
• <a href="#functools">functools</a>
|
||||
• <a href="#genericpath">genericpath</a>
|
||||
• <a href="#gui_constants">gui_constants</a>
|
||||
• <a href="#gui_models">gui_models</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
• <a href="#heapq">heapq</a>
|
||||
• <a href="#io">io</a>
|
||||
• <a href="#json">json</a>
|
||||
@@ -2835,6 +2838,8 @@ imported by:
|
||||
• <a href="#PyQt5.QtWidgets">PyQt5.QtWidgets</a>
|
||||
• <a href="#PyQt5.QtXml">PyQt5.QtXml</a>
|
||||
• <a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_models">gui_models</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
• <a href="#qfluentwidgets._rc.resource">qfluentwidgets._rc.resource</a>
|
||||
• <a href="#qfluentwidgets.common.animation">qfluentwidgets.common.animation</a>
|
||||
• <a href="#qfluentwidgets.common.config">qfluentwidgets.common.config</a>
|
||||
@@ -3075,6 +3080,7 @@ imported by:
|
||||
• <a href="#PyQt5.QtGui">PyQt5.QtGui</a>
|
||||
• <a href="#PyQt5.QtSvg">PyQt5.QtSvg</a>
|
||||
• <a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_models">gui_models</a>
|
||||
• <a href="#qfluentwidgets.common.animation">qfluentwidgets.common.animation</a>
|
||||
• <a href="#qfluentwidgets.common.font">qfluentwidgets.common.font</a>
|
||||
• <a href="#qfluentwidgets.common.icon">qfluentwidgets.common.icon</a>
|
||||
@@ -14331,6 +14337,65 @@ imported by:
|
||||
|
||||
</div>
|
||||
|
||||
<div class="node">
|
||||
<a name="gui_constants"></a>
|
||||
<a target="code" href="///C:/Users/27942/Desktop/haha/gui_constants.py" type="text/plain"><tt>gui_constants</tt></a>
|
||||
<span class="moduletype">SourceModule</span> <div class="import">
|
||||
imports:
|
||||
<a href="#os">os</a>
|
||||
|
||||
</div>
|
||||
<div class="import">
|
||||
imported by:
|
||||
<a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_models">gui_models</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="node">
|
||||
<a name="gui_models"></a>
|
||||
<a target="code" href="///C:/Users/27942/Desktop/haha/gui_models.py" type="text/plain"><tt>gui_models</tt></a>
|
||||
<span class="moduletype">SourceModule</span> <div class="import">
|
||||
imports:
|
||||
<a href="#PyQt5.QtCore">PyQt5.QtCore</a>
|
||||
• <a href="#PyQt5.QtWidgets">PyQt5.QtWidgets</a>
|
||||
• <a href="#gui_constants">gui_constants</a>
|
||||
|
||||
</div>
|
||||
<div class="import">
|
||||
imported by:
|
||||
<a href="#gui_app.py">gui_app.py</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="node">
|
||||
<a name="gui_worker"></a>
|
||||
<a target="code" href="///C:/Users/27942/Desktop/haha/gui_worker.py" type="text/plain"><tt>gui_worker</tt></a>
|
||||
<span class="moduletype">SourceModule</span> <div class="import">
|
||||
imports:
|
||||
<a href="#PyQt5.QtCore">PyQt5.QtCore</a>
|
||||
• <a href="#gui_constants">gui_constants</a>
|
||||
• <a href="#loguru">loguru</a>
|
||||
• <a href="#main">main</a>
|
||||
• <a href="#os">os</a>
|
||||
• <a href="#pathlib">pathlib</a>
|
||||
• <a href="#time">time</a>
|
||||
• <a href="#traceback">traceback</a>
|
||||
|
||||
</div>
|
||||
<div class="import">
|
||||
imported by:
|
||||
<a href="#gui_app.py">gui_app.py</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="node">
|
||||
<a name="gzip"></a>
|
||||
<a target="code" href="///C:/Users/27942/.conda/envs/haha/Lib/gzip.py" type="text/plain"><tt>gzip</tt></a>
|
||||
@@ -16113,6 +16178,7 @@ imports:
|
||||
<div class="import">
|
||||
imported by:
|
||||
<a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
• <a href="#loguru">loguru</a>
|
||||
• <a href="#loguru._asyncio_loop">loguru._asyncio_loop</a>
|
||||
• <a href="#loguru._better_exceptions">loguru._better_exceptions</a>
|
||||
@@ -17300,6 +17366,7 @@ imports:
|
||||
<div class="import">
|
||||
imported by:
|
||||
<a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -30286,6 +30353,8 @@ imported by:
|
||||
• <a href="#gettext">gettext</a>
|
||||
• <a href="#glob">glob</a>
|
||||
• <a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_constants">gui_constants</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
• <a href="#gzip">gzip</a>
|
||||
• <a href="#http.cookiejar">http.cookiejar</a>
|
||||
• <a href="#http.server">http.server</a>
|
||||
@@ -43422,6 +43491,7 @@ imported by:
|
||||
• <a href="#filelock._util">filelock._util</a>
|
||||
• <a href="#filelock._windows">filelock._windows</a>
|
||||
• <a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
• <a href="#importlib.metadata">importlib.metadata</a>
|
||||
• <a href="#importlib.resources._common">importlib.resources._common</a>
|
||||
• <a href="#importlib.resources.abc">importlib.resources.abc</a>
|
||||
@@ -54168,6 +54238,7 @@ imported by:
|
||||
• <a href="#filelock.asyncio">filelock.asyncio</a>
|
||||
• <a href="#gc">gc</a>
|
||||
• <a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
• <a href="#gzip">gzip</a>
|
||||
• <a href="#http.cookiejar">http.cookiejar</a>
|
||||
• <a href="#http.cookies">http.cookies</a>
|
||||
@@ -54547,6 +54618,7 @@ imported by:
|
||||
• <a href="#concurrent.futures.process">concurrent.futures.process</a>
|
||||
• <a href="#doctest">doctest</a>
|
||||
• <a href="#gui_app.py">gui_app.py</a>
|
||||
• <a href="#gui_worker">gui_worker</a>
|
||||
• <a href="#http.cookiejar">http.cookiejar</a>
|
||||
• <a href="#logging">logging</a>
|
||||
• <a href="#loguru._better_exceptions">loguru._better_exceptions</a>
|
||||
|
||||
BIN
dist/多多自动发文助手.exe → dist/多多发文助手.exe
vendored
820
gui_app.py
@@ -17,7 +17,7 @@ from PyQt5.QtWidgets import (
|
||||
QStyleOptionProgressBar, QStyleOptionButton, QHeaderView,
|
||||
QTabWidget, QSplitter, QSizePolicy, QCheckBox
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QDateTime, QSize, QAbstractTableModel, QModelIndex, \
|
||||
from PyQt5.QtCore import Qt, QDateTime, QSize, QAbstractTableModel, QModelIndex, \
|
||||
QSortFilterProxyModel, QRegularExpression, QSettings, QTimer, QEvent
|
||||
from PyQt5.QtGui import QFont, QTextDocument, QTextCursor, QKeySequence, QColor, QPainter
|
||||
|
||||
@@ -30,689 +30,14 @@ from qfluentwidgets import (
|
||||
from main import Pdd
|
||||
from loguru import logger
|
||||
|
||||
from gui_constants import get_default_folder_path, TABLE_HEADERS, MODEL_VIEW_HEADERS
|
||||
from gui_worker import WorkerThread
|
||||
from gui_models import ConfigTableModel, TableActionDelegate
|
||||
|
||||
def get_default_folder_path():
|
||||
"""获取默认文件夹路径(桌面/多多自动化发文)"""
|
||||
desktop = os.path.join(os.path.expanduser("~"), "Desktop")
|
||||
default_path = os.path.join(desktop, "多多自动化发文")
|
||||
return default_path
|
||||
|
||||
|
||||
class WorkerThread(QThread):
|
||||
"""工作线程,用于执行自动化任务"""
|
||||
finished = pyqtSignal(bool, str)
|
||||
log_message = pyqtSignal(str)
|
||||
progress = pyqtSignal(int)
|
||||
item_result = pyqtSignal(object) # 单条发布结果(dict)
|
||||
|
||||
def __init__(self, config_data, is_batch_mode, prepared_files=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.config_data = config_data
|
||||
self.is_batch_mode = is_batch_mode
|
||||
self.prepared_files = prepared_files # 预查找的文件列表
|
||||
self.is_running = True
|
||||
|
||||
def run(self):
|
||||
sink_id = None
|
||||
start_ts = time.time()
|
||||
try:
|
||||
# 将日志转发到 GUI(不要 remove 全局 sink,避免影响主线程/其他模块日志)
|
||||
sink_id = logger.add(lambda msg: self.log_message.emit(str(msg).strip()))
|
||||
|
||||
if self.is_batch_mode:
|
||||
self.run_batch_mode()
|
||||
else:
|
||||
self.run_single_mode()
|
||||
except Exception as e:
|
||||
error_msg = f"执行失败: {str(e)}"
|
||||
self.finished.emit(False, error_msg)
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(f"执行失败: {e}")
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
finally:
|
||||
try:
|
||||
cost = time.time() - start_ts
|
||||
self.log_message.emit(f"[线程] 任务结束,耗时 {cost:.2f}s")
|
||||
except Exception:
|
||||
pass
|
||||
if sink_id is not None:
|
||||
try:
|
||||
logger.remove(sink_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run_single_mode(self):
|
||||
"""执行单个上传模式(action方法)"""
|
||||
try:
|
||||
config = self.config_data
|
||||
self.log_message.emit(f"开始处理单个任务(逐个上传模式)...")
|
||||
|
||||
pdd = Pdd(
|
||||
url=config.get('达人链接', ''),
|
||||
user_id=config.get('多多id', ''),
|
||||
time_start=config.get('定时发布', '') if config.get('定时发布') else None,
|
||||
ht=config.get('话题', ''),
|
||||
index=config.get('序号', ''),
|
||||
title=config.get('标题', None)
|
||||
)
|
||||
|
||||
# 如果已经预查找了文件,直接使用
|
||||
if self.prepared_files:
|
||||
self.log_message.emit(f"使用预查找的文件列表,共 {len(self.prepared_files)} 个文件")
|
||||
self.progress.emit(30)
|
||||
|
||||
# 单个上传模式,使用action方法
|
||||
# 从预查找的文件中提取文件夹路径(取第一个文件的父目录的父目录,回到最外层文件夹)
|
||||
first_file = self.prepared_files[0]
|
||||
if first_file['path'].is_file():
|
||||
# 文件路径:最外层文件夹/多多ID文件夹/文件名
|
||||
# 需要回到最外层文件夹
|
||||
folder_path = str(first_file['path'].parent.parent)
|
||||
else:
|
||||
# 文件夹路径:最外层文件夹/多多ID文件夹/文件夹名
|
||||
folder_path = str(first_file['path'].parent.parent)
|
||||
|
||||
self.log_message.emit(f"使用文件夹路径: {folder_path}")
|
||||
try:
|
||||
result = pdd.action(folder_path=folder_path, collect_all_videos=False)
|
||||
ok = bool(result.get("ok")) if isinstance(result, dict) else True
|
||||
if not ok:
|
||||
self.log_message.emit(f"发布校验失败: {result.get('reason') if isinstance(result, dict) else ''}")
|
||||
# 单条回传结果
|
||||
if isinstance(result, dict):
|
||||
self.item_result.emit({
|
||||
"user_id": config.get("多多id", ""),
|
||||
"index": config.get("序号", ""),
|
||||
"name": config.get("标题", "") or "",
|
||||
"ok": ok,
|
||||
"reason": result.get("reason", ""),
|
||||
})
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(ok, f"单个任务执行完成(ok={ok})")
|
||||
except Exception as e:
|
||||
error_msg = f"执行action方法失败: {str(e)}"
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(error_msg)
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
self.finished.emit(False, error_msg)
|
||||
return
|
||||
else:
|
||||
# 未预查找文件,使用原来的逻辑
|
||||
folder_path = config.get('文件夹路径', '').strip()
|
||||
if not folder_path:
|
||||
folder_path = get_default_folder_path()
|
||||
|
||||
if not os.path.exists(folder_path):
|
||||
error_msg = f"文件夹路径不存在: {folder_path}"
|
||||
self.finished.emit(False, error_msg)
|
||||
self.log_message.emit(error_msg)
|
||||
return
|
||||
|
||||
self.log_message.emit(f"使用文件夹路径: {folder_path}")
|
||||
self.progress.emit(30)
|
||||
|
||||
# 检查是否勾选了批量上传
|
||||
is_batch_mode = self.is_batch_mode
|
||||
|
||||
# 调用action方法
|
||||
try:
|
||||
result = pdd.action(folder_path=folder_path, collect_all_videos=is_batch_mode)
|
||||
ok = bool(result.get("ok")) if isinstance(result, dict) else True
|
||||
if not ok:
|
||||
self.log_message.emit(f"发布校验失败: {result.get('reason') if isinstance(result, dict) else ''}")
|
||||
if isinstance(result, dict):
|
||||
self.item_result.emit({
|
||||
"user_id": config.get("多多id", ""),
|
||||
"index": config.get("序号", ""),
|
||||
"name": config.get("标题", "") or "",
|
||||
"ok": ok,
|
||||
"reason": result.get("reason", ""),
|
||||
})
|
||||
except Exception as e:
|
||||
error_msg = f"执行action方法失败: {str(e)}"
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(error_msg)
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
self.finished.emit(False, error_msg)
|
||||
return
|
||||
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(ok, f"单个任务执行完成(ok={ok})")
|
||||
except Exception as e:
|
||||
error_msg = f"执行失败: {str(e)}"
|
||||
self.finished.emit(False, error_msg)
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(f"执行失败: {e}")
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
|
||||
def run_batch_mode(self):
|
||||
"""执行批量上传模式:先批量上传所有视频,然后逐个上传图片"""
|
||||
try:
|
||||
config = self.config_data
|
||||
self.log_message.emit(f"开始处理批量上传任务...")
|
||||
|
||||
index = config.get('序号', '')
|
||||
|
||||
# 创建Pdd实例
|
||||
pdd = Pdd(
|
||||
url=config.get('达人链接', ''),
|
||||
user_id=config.get('多多id', ''),
|
||||
time_start=config.get('定时发布', '') if config.get('定时发布') else None,
|
||||
ht=config.get('话题', ''),
|
||||
index=index,
|
||||
title=config.get('标题', None)
|
||||
)
|
||||
|
||||
# 如果已经预查找了文件,直接使用
|
||||
if self.prepared_files:
|
||||
self.log_message.emit(f"使用预查找的文件列表,共 {len(self.prepared_files)} 个文件")
|
||||
self.progress.emit(30)
|
||||
|
||||
# 分离视频文件和图片文件夹
|
||||
video_extensions = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm']
|
||||
video_file_paths = [f for f in self.prepared_files if f['path'].is_file() and any(
|
||||
f['path'].suffix.lower() == ext for ext in video_extensions)]
|
||||
image_folders = [f for f in self.prepared_files if f['path'].is_dir()]
|
||||
|
||||
# 如果有视频,批量上传所有视频
|
||||
if video_file_paths:
|
||||
self.log_message.emit(f"找到 {len(video_file_paths)} 个视频文件,开始批量上传...")
|
||||
self.progress.emit(30)
|
||||
|
||||
# 定义实时回调函数:每处理完一个视频就立即通知 GUI 更新状态
|
||||
def on_video_done(result_item):
|
||||
try:
|
||||
payload = dict(result_item)
|
||||
payload["user_id"] = config.get("多多id", "")
|
||||
# 确保index字段正确传递
|
||||
if "index" not in payload or not payload.get("index"):
|
||||
for vid_file in video_file_paths:
|
||||
if str(vid_file.get('path')) == str(payload.get('path')):
|
||||
payload["index"] = vid_file.get("index", config.get("序号", ""))
|
||||
break
|
||||
if "index" not in payload or not payload.get("index"):
|
||||
payload["index"] = config.get("序号", "")
|
||||
# 立即通知 GUI 更新状态
|
||||
self.item_result.emit(payload)
|
||||
self.log_message.emit(f" {'✓' if payload.get('ok') else '✗'} {payload.get('name', '')} 已更新状态")
|
||||
except Exception as e:
|
||||
self.log_message.emit(f" 回调处理异常: {e}")
|
||||
|
||||
result = pdd.action1(folder_path=video_file_paths, on_item_done=on_video_done)
|
||||
ok = bool(result.get("ok")) if isinstance(result, dict) else True
|
||||
if not ok:
|
||||
fails = [r for r in (result.get("results") or []) if not r.get("ok")]
|
||||
self.log_message.emit(f"发布校验失败条数: {len(fails)}")
|
||||
for r in fails[:20]:
|
||||
self.log_message.emit(f" ✗ {r.get('name')} | {r.get('reason')}")
|
||||
self.log_message.emit(f"批量上传视频完成,共处理 {len(video_file_paths)} 个视频")
|
||||
else:
|
||||
self.log_message.emit("未找到视频文件,跳过视频上传")
|
||||
ok = True
|
||||
|
||||
# 如果有图片文件夹,逐个上传图片
|
||||
if image_folders:
|
||||
self.log_message.emit(f"找到 {len(image_folders)} 个图片文件夹,开始逐个上传...")
|
||||
self.progress.emit(70)
|
||||
|
||||
for idx, image_folder_info in enumerate(image_folders):
|
||||
if not self.is_running:
|
||||
break
|
||||
|
||||
image_folder = str(image_folder_info['path'])
|
||||
self.log_message.emit(f"处理第 {idx + 1}/{len(image_folders)} 个图片文件夹: {image_folder}")
|
||||
# 使用action方法处理图片文件夹
|
||||
pdd.action(folder_path=image_folder)
|
||||
self.log_message.emit(f"图片文件夹 {idx + 1} 上传完成")
|
||||
|
||||
self.log_message.emit(f"所有图片文件夹上传完成,共处理 {len(image_folders)} 个文件夹")
|
||||
else:
|
||||
self.log_message.emit("未找到图片文件夹,跳过图片上传")
|
||||
|
||||
self.progress.emit(100)
|
||||
total_count = len(video_file_paths) + len(image_folders)
|
||||
self.finished.emit(ok,
|
||||
f"批量任务执行完成,共处理 {len(video_file_paths)} 个视频和 {len(image_folders)} 个图片文件夹(ok={ok})")
|
||||
else:
|
||||
# 未预查找文件,使用原来的逻辑
|
||||
folder_path = config.get('文件夹路径', '').strip()
|
||||
if not folder_path:
|
||||
folder_path = get_default_folder_path()
|
||||
|
||||
if not os.path.exists(folder_path):
|
||||
self.finished.emit(False, f"文件夹路径不存在: {folder_path}")
|
||||
return
|
||||
|
||||
# 第一步:收集所有视频文件
|
||||
self.log_message.emit("第一步:收集所有视频文件...")
|
||||
video_file_paths = self.prepare_batch_files(folder_path, config)
|
||||
|
||||
# 第二步:收集所有图片文件夹
|
||||
self.log_message.emit("第二步:收集所有图片文件夹...")
|
||||
image_folders = self.prepare_image_folders(folder_path, index)
|
||||
|
||||
# 第三步:如果有视频,批量上传所有视频
|
||||
if video_file_paths:
|
||||
self.log_message.emit(f"找到 {len(video_file_paths)} 个视频文件,开始批量上传...")
|
||||
self.progress.emit(30)
|
||||
pdd.action1(folder_path=video_file_paths)
|
||||
self.log_message.emit(f"批量上传视频完成,共处理 {len(video_file_paths)} 个视频")
|
||||
else:
|
||||
self.log_message.emit("未找到视频文件,跳过视频上传")
|
||||
|
||||
# 第四步:如果有图片文件夹,逐个上传图片
|
||||
if image_folders:
|
||||
self.log_message.emit(f"找到 {len(image_folders)} 个图片文件夹,开始逐个上传...")
|
||||
self.progress.emit(70)
|
||||
|
||||
for idx, image_folder in enumerate(image_folders):
|
||||
if not self.is_running:
|
||||
break
|
||||
|
||||
self.log_message.emit(f"处理第 {idx + 1}/{len(image_folders)} 个图片文件夹: {image_folder}")
|
||||
# 使用action方法处理图片文件夹
|
||||
pdd.action(folder_path=image_folder)
|
||||
self.log_message.emit(f"图片文件夹 {idx + 1} 上传完成")
|
||||
|
||||
self.log_message.emit(f"所有图片文件夹上传完成,共处理 {len(image_folders)} 个文件夹")
|
||||
else:
|
||||
self.log_message.emit("未找到图片文件夹,跳过图片上传")
|
||||
|
||||
self.progress.emit(100)
|
||||
total_count = len(video_file_paths) + len(image_folders)
|
||||
self.finished.emit(True,
|
||||
f"批量任务执行完成,共处理 {len(video_file_paths)} 个视频和 {len(image_folders)} 个图片文件夹")
|
||||
|
||||
except Exception as e:
|
||||
self.finished.emit(False, f"批量执行失败: {str(e)}")
|
||||
logger.error(f"批量执行失败: {e}")
|
||||
|
||||
def prepare_image_folders(self, folder_path, index):
|
||||
"""收集所有匹配序号的图片文件夹(与main.py中action方法的逻辑一致)"""
|
||||
image_folders = []
|
||||
|
||||
try:
|
||||
# 参数验证
|
||||
if not folder_path or not os.path.exists(folder_path):
|
||||
logger.warning(f"文件夹路径无效或不存在: {folder_path}")
|
||||
return image_folders
|
||||
|
||||
if not os.access(folder_path, os.R_OK):
|
||||
logger.error(f"没有权限读取文件夹: {folder_path}")
|
||||
return image_folders
|
||||
|
||||
# 遍历最外层文件夹下的所有子文件夹(与main.py逻辑一致)
|
||||
try:
|
||||
subdirs = os.listdir(folder_path)
|
||||
except PermissionError:
|
||||
logger.error(f"没有权限访问文件夹: {folder_path}")
|
||||
return image_folders
|
||||
except Exception as e:
|
||||
logger.error(f"读取文件夹失败: {e}")
|
||||
return image_folders
|
||||
|
||||
for file in subdirs:
|
||||
try:
|
||||
file_path = os.path.join(folder_path, file) # 拼接文件夹
|
||||
# 检查是否为目录,跳过文件(如.lnk快捷方式)
|
||||
if not os.path.isdir(file_path):
|
||||
continue
|
||||
|
||||
# 获取用户id下的文件
|
||||
try:
|
||||
files = os.listdir(file_path)
|
||||
except PermissionError:
|
||||
logger.warning(f"没有权限访问子文件夹: {file_path}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"读取子文件夹失败: {file_path}, 错误: {e}")
|
||||
continue
|
||||
|
||||
for file_name in files:
|
||||
try:
|
||||
# 跳过隐藏文件
|
||||
if file_name.startswith('.'):
|
||||
continue
|
||||
|
||||
# 用"-"分割文件名,检查第一部分是否等于序号
|
||||
file_names = file_name.split("-")
|
||||
if len(file_names) > 0 and file_names[0] == str(index):
|
||||
path = Path(os.path.join(file_path, file_name))
|
||||
# 确保路径存在
|
||||
if path.exists() and path.is_dir():
|
||||
# 这是一个图片文件夹
|
||||
image_folders.append(str(path))
|
||||
except Exception as e:
|
||||
logger.warning(f"处理文件 {file_name} 时出错: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"处理子文件夹 {file} 时出错: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"收集图片文件夹失败: {e}", exc_info=True)
|
||||
|
||||
return image_folders
|
||||
|
||||
def count_videos_in_folder(self, folder_path, index):
|
||||
"""统计文件夹中匹配序号的视频文件数量(与main.py逻辑一致)"""
|
||||
count = 0
|
||||
try:
|
||||
# 遍历最外层文件夹下的所有子文件夹(与main.py逻辑一致)
|
||||
for file in os.listdir(folder_path): # 获取文件夹下所有的文件夹
|
||||
file_path = os.path.join(folder_path, file) # 拼接文件夹
|
||||
# 检查是否为目录,跳过文件(如.lnk快捷方式)
|
||||
if not os.path.isdir(file_path):
|
||||
continue
|
||||
|
||||
files = os.listdir(file_path) # 获取用户id下的文件
|
||||
for file_name in files:
|
||||
# 检查是否是视频文件(.mp4)
|
||||
if ".mp4" in file_name:
|
||||
# 用"-"分割文件名,检查第一部分是否等于序号
|
||||
# 文件名格式:4-茶叶蛋大冒险-.mp4 -> ['4', '茶叶蛋大冒险', '', 'mp4']
|
||||
file_names = file_name.split("-")
|
||||
if len(file_names) > 0 and file_names[0] == str(index):
|
||||
path = Path(os.path.join(file_path, file_name))
|
||||
# 判断是否为文件
|
||||
if path.is_file():
|
||||
count += 1
|
||||
else:
|
||||
# 如果是文件夹,统计其中的文件
|
||||
for sub_file in os.listdir(path):
|
||||
sub_path = Path(os.path.join(path, sub_file))
|
||||
if sub_path.is_file():
|
||||
count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"统计视频文件失败: {e}")
|
||||
|
||||
return count
|
||||
|
||||
def prepare_batch_files(self, folder_path, config):
|
||||
"""准备批量上传的文件列表(与main.py中的action1方法逻辑一致)"""
|
||||
file_paths = []
|
||||
|
||||
# 参数验证
|
||||
if not config:
|
||||
logger.error("配置为空,无法准备批量上传文件")
|
||||
return file_paths
|
||||
|
||||
index = str(config.get('序号', '')).strip()
|
||||
if not index:
|
||||
logger.warning("序号为空,无法准备批量上传文件")
|
||||
return file_paths
|
||||
|
||||
if not folder_path or not os.path.exists(folder_path):
|
||||
logger.error(f"文件夹路径无效或不存在: {folder_path}")
|
||||
return file_paths
|
||||
|
||||
if not os.access(folder_path, os.R_OK):
|
||||
logger.error(f"没有权限读取文件夹: {folder_path}")
|
||||
return file_paths
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info("开始准备批量上传文件列表...")
|
||||
logger.info(f"文件夹路径: {folder_path}")
|
||||
logger.info(f"查找序号: {index}")
|
||||
|
||||
try:
|
||||
# 遍历最外层文件夹下的所有子文件夹(与main.py逻辑一致)
|
||||
try:
|
||||
all_items = os.listdir(folder_path)
|
||||
subdirs = [f for f in all_items if os.path.isdir(os.path.join(folder_path, f))]
|
||||
except PermissionError:
|
||||
logger.error(f"没有权限访问文件夹: {folder_path}")
|
||||
return file_paths
|
||||
except Exception as e:
|
||||
logger.error(f"读取文件夹失败: {e}")
|
||||
return file_paths
|
||||
|
||||
logger.info(f"在最外层文件夹下找到 {len(subdirs)} 个子文件夹(多多ID文件夹)")
|
||||
|
||||
for subdir_name in subdirs:
|
||||
file_path = os.path.join(folder_path, subdir_name) # 拼接文件夹
|
||||
logger.info(f" 正在扫描子文件夹: {subdir_name}")
|
||||
|
||||
# 检查是否为目录,跳过文件(如.lnk快捷方式)
|
||||
if not os.path.isdir(file_path):
|
||||
logger.info(f" 跳过(不是文件夹)")
|
||||
continue
|
||||
|
||||
files = os.listdir(file_path) # 获取用户id下的文件
|
||||
logger.info(f" 该文件夹下有 {len(files)} 个项目")
|
||||
|
||||
for file_name in files:
|
||||
logger.info(f" 检查项目: {file_name}")
|
||||
|
||||
# 检查是否是视频文件(.mp4)
|
||||
if ".mp4" in file_name:
|
||||
logger.info(f" ✓ 是视频文件(包含.mp4)")
|
||||
# 用"-"分割文件名,检查第一部分是否等于序号
|
||||
# 文件名格式:4-茶叶蛋大冒险-.mp4 -> ['4', '茶叶蛋大冒险', '', 'mp4']
|
||||
file_names = file_name.split("-")
|
||||
logger.info(f" 文件名分割结果: {file_names}")
|
||||
|
||||
if len(file_names) > 0 and file_names[0] == str(index):
|
||||
logger.info(f" ✓ 序号匹配!序号: {file_names[0]}, 目标序号: {index}")
|
||||
path = Path(os.path.join(file_path, file_name))
|
||||
|
||||
# 判断是否为文件
|
||||
if path.is_file():
|
||||
logger.info(f" ✓ 是文件,添加到列表: {path.name}")
|
||||
file_paths.append({
|
||||
"url": config.get('达人链接', ''),
|
||||
"user_id": config.get('多多id', ''),
|
||||
"time_start": config.get('定时发布', '') if config.get('定时发布') else None,
|
||||
"ht": config.get('话题', ''),
|
||||
"index": str(index),
|
||||
"path": path
|
||||
})
|
||||
logger.info(f" 当前视频总数: {len(file_paths)}")
|
||||
# 注意:这里不break,因为可能有多个匹配的文件
|
||||
else:
|
||||
logger.info(f" 是文件夹,扫描其中的文件...")
|
||||
# 如果是文件夹,添加其中的所有文件
|
||||
try:
|
||||
sub_files = os.listdir(path)
|
||||
logger.info(f" 文件夹中有 {len(sub_files)} 个文件")
|
||||
for sub_file in sub_files:
|
||||
sub_path = Path(os.path.join(path, sub_file))
|
||||
if sub_path.is_file():
|
||||
logger.info(f" ✓ 添加文件: {sub_file}")
|
||||
file_paths.append({
|
||||
"url": config.get('达人链接', ''),
|
||||
"user_id": config.get('多多id', ''),
|
||||
"time_start": config.get('定时发布', '') if config.get(
|
||||
'定时发布') else None,
|
||||
"ht": config.get('话题', ''),
|
||||
"index": str(index),
|
||||
"path": sub_path
|
||||
})
|
||||
logger.info(f" 当前视频总数: {len(file_paths)}")
|
||||
except Exception as e:
|
||||
logger.error(f" 扫描子文件夹失败: {e}")
|
||||
else:
|
||||
if len(file_names) > 0:
|
||||
logger.info(f" ✗ 序号不匹配: {file_names[0]} != {index}")
|
||||
else:
|
||||
logger.info(f" ✗ 文件名格式不正确,无法提取序号")
|
||||
else:
|
||||
logger.info(f" ✗ 不是视频文件(不包含.mp4),跳过")
|
||||
except Exception as e:
|
||||
logger.error(f"准备批量文件失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info(f"文件收集完成!共找到 {len(file_paths)} 个视频文件")
|
||||
if file_paths:
|
||||
logger.info("视频文件列表:")
|
||||
for idx, video_info in enumerate(file_paths, 1):
|
||||
logger.info(f" {idx}. {video_info['path'].name} ({video_info['path']})")
|
||||
|
||||
return file_paths
|
||||
|
||||
def stop(self):
|
||||
"""停止执行"""
|
||||
self.is_running = False
|
||||
|
||||
def requestInterruption(self):
|
||||
"""请求中断线程(重写QThread方法)"""
|
||||
super().requestInterruption()
|
||||
self.is_running = False
|
||||
|
||||
|
||||
class ConfigTableModel(QAbstractTableModel):
|
||||
"""大数据量表格模型"""
|
||||
|
||||
def __init__(self, configs, headers, parent=None):
|
||||
super().__init__(parent)
|
||||
self._configs = configs
|
||||
self._headers = headers
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
return len(self._configs)
|
||||
|
||||
def columnCount(self, parent=QModelIndex()):
|
||||
return len(self._headers)
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
if not index.isValid():
|
||||
return None
|
||||
row = index.row()
|
||||
col = index.column()
|
||||
config = self._configs[row]
|
||||
mapping = {
|
||||
0: "多多id",
|
||||
1: "序号",
|
||||
2: "话题",
|
||||
3: "定时发布",
|
||||
4: "间隔时间",
|
||||
5: "达人链接",
|
||||
6: "执行人",
|
||||
7: "情况",
|
||||
8: "文件路径",
|
||||
9: "",
|
||||
10: "",
|
||||
}
|
||||
key = mapping.get(col, "")
|
||||
if role == Qt.DisplayRole:
|
||||
return str(config.get(key, "")) if key else ""
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
# 所有单元格居中对齐
|
||||
return Qt.AlignCenter
|
||||
elif role == Qt.ToolTipRole:
|
||||
# 为达人链接和文件路径列提供 tooltip 显示完整内容
|
||||
if col in (5, 8) and key:
|
||||
return str(config.get(key, ""))
|
||||
return None
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if role != Qt.DisplayRole:
|
||||
return None
|
||||
if orientation == Qt.Horizontal:
|
||||
return self._headers[section]
|
||||
return section + 1
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return Qt.NoItemFlags
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if role != Qt.EditRole or not index.isValid():
|
||||
return False
|
||||
row = index.row()
|
||||
col = index.column()
|
||||
mapping = {
|
||||
0: "多多id",
|
||||
1: "序号",
|
||||
2: "话题",
|
||||
3: "定时发布",
|
||||
4: "间隔时间",
|
||||
5: "达人链接",
|
||||
6: "执行人",
|
||||
7: "情况",
|
||||
8: "文件路径",
|
||||
}
|
||||
key = mapping.get(col, "")
|
||||
if not key:
|
||||
return False
|
||||
self._configs[row][key] = str(value)
|
||||
self.dataChanged.emit(index, index, [Qt.DisplayRole])
|
||||
return True
|
||||
|
||||
def removeRows(self, row, count, parent=QModelIndex()):
|
||||
if row < 0 or row + count > len(self._configs):
|
||||
return False
|
||||
self.beginRemoveRows(parent, row, row + count - 1)
|
||||
del self._configs[row:row + count]
|
||||
self.endRemoveRows()
|
||||
return True
|
||||
|
||||
|
||||
class TableActionDelegate(QStyledItemDelegate):
|
||||
"""Model/View 操作列 + 进度列 delegate"""
|
||||
|
||||
def __init__(self, parent, on_delete):
|
||||
super().__init__(parent)
|
||||
self.on_delete = on_delete
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
if index.column() == 9:
|
||||
status_text = index.sibling(index.row(), 7).data() or ""
|
||||
value = 0
|
||||
if "完成" in status_text or "成功" in status_text:
|
||||
value = 100
|
||||
elif "执行中" in status_text or "进行" in status_text:
|
||||
value = 60
|
||||
elif "待" in status_text:
|
||||
value = 10
|
||||
bar = QStyleOptionProgressBar()
|
||||
bar.rect = option.rect.adjusted(6, option.rect.height() // 3, -6, -option.rect.height() // 3)
|
||||
bar.minimum = 0
|
||||
bar.maximum = 100
|
||||
bar.progress = value
|
||||
bar.textVisible = False
|
||||
QApplication.style().drawControl(QStyle.CE_ProgressBar, bar, painter)
|
||||
return
|
||||
if index.column() == 10:
|
||||
rect = option.rect
|
||||
delete_rect = rect.adjusted(6, 4, -6, -4)
|
||||
delete_btn = QStyleOptionButton()
|
||||
delete_btn.rect = delete_rect
|
||||
delete_btn.text = "删除"
|
||||
QApplication.style().drawControl(QStyle.CE_PushButton, delete_btn, painter)
|
||||
return
|
||||
super().paint(painter, option, index)
|
||||
|
||||
def editorEvent(self, event, model, option, index):
|
||||
if index.column() != 10:
|
||||
return super().editorEvent(event, model, option, index)
|
||||
if event.type() == event.MouseButtonRelease:
|
||||
rect = option.rect
|
||||
delete_rect = rect.adjusted(6, 4, -6, -4)
|
||||
if delete_rect.contains(event.pos()):
|
||||
self.on_delete(index)
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_data(self, configs):
|
||||
self.beginResetModel()
|
||||
self._configs = configs
|
||||
self.endResetModel()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 主窗口
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""主窗口"""
|
||||
@@ -1149,10 +474,7 @@ class MainWindow(QMainWindow):
|
||||
}
|
||||
""")
|
||||
self.config_table.setColumnCount(12)
|
||||
self.config_table.setHorizontalHeaderLabels([
|
||||
'☑', '多多ID', '序号', '话题', '定时发布', '间隔时间', '达人链接',
|
||||
'执行人', '情况', '文件路径', '进度', '操作'
|
||||
])
|
||||
self.config_table.setHorizontalHeaderLabels(TABLE_HEADERS)
|
||||
self.table_column_filter.addItem("全部列")
|
||||
# 第0列为勾选框;记录下拉项对应的表格列索引及 Model 列索引
|
||||
self._filter_table_columns = []
|
||||
@@ -1502,10 +824,12 @@ class MainWindow(QMainWindow):
|
||||
# 确保标志被重置
|
||||
self.is_updating_table = False
|
||||
|
||||
def _set_checkbox_item(self, row, config_index):
|
||||
"""设置勾选框列(第0列),使用嵌套布局确保真正水平+垂直居中"""
|
||||
def _set_checkbox_item(self, row, config_index, row_height=None):
|
||||
"""设置勾选框列(第0列)。row_height 传入时使用固定高度+居中对齐,避免第二行起勾选框往下错位。"""
|
||||
checkbox = QCheckBox()
|
||||
checkbox.blockSignals(True)
|
||||
checkbox.setChecked(self.configs[config_index].get('勾选', False)) # 默认不勾选
|
||||
checkbox.blockSignals(False)
|
||||
checkbox.stateChanged.connect(lambda state, idx=config_index: self._on_checkbox_changed(idx, state))
|
||||
checkbox.setStyleSheet(
|
||||
"QCheckBox { margin: 0px; padding: 0px; }"
|
||||
@@ -1513,24 +837,15 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
|
||||
wrapper = QWidget()
|
||||
wrapper.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
h = row_height if (row_height is not None and row_height > 0) else 42
|
||||
wrapper.setFixedHeight(h)
|
||||
wrapper.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
# 外层垂直布局:通过上下 stretch 实现垂直居中
|
||||
v_layout = QVBoxLayout(wrapper)
|
||||
v_layout.setContentsMargins(0, 0, 0, 0)
|
||||
v_layout.setSpacing(0)
|
||||
v_layout.addStretch()
|
||||
|
||||
# 内层水平布局:通过左右 stretch 实现水平居中
|
||||
h_layout = QHBoxLayout()
|
||||
h_layout.setContentsMargins(0, 0, 0, 0)
|
||||
h_layout.setSpacing(0)
|
||||
h_layout.addStretch()
|
||||
h_layout.addWidget(checkbox)
|
||||
h_layout.addStretch()
|
||||
|
||||
v_layout.addLayout(h_layout)
|
||||
v_layout.addStretch()
|
||||
layout = QVBoxLayout(wrapper)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(checkbox)
|
||||
|
||||
self.config_table.setCellWidget(row, 0, wrapper)
|
||||
|
||||
@@ -3430,44 +2745,52 @@ class MainWindow(QMainWindow):
|
||||
# 计算实际显示行数:数据行数和最小行数取最大值
|
||||
display_rows = max(len(self.page_row_indices), min_display_rows)
|
||||
self.config_table.setRowCount(display_rows)
|
||||
|
||||
# 填充数据行
|
||||
for table_row, config_index in enumerate(self.page_row_indices):
|
||||
config = self.configs[config_index]
|
||||
# 第0列:勾选框
|
||||
self._set_checkbox_item(table_row, config_index)
|
||||
# 其他列的索引均+1
|
||||
self.config_table.setItem(table_row, 1, self._create_centered_item(str(config.get('多多id', ''))))
|
||||
self.config_table.setItem(table_row, 2, self._create_centered_item(str(config.get('序号', ''))))
|
||||
self.config_table.setItem(table_row, 3, self._create_centered_item(str(config.get('话题', ''))))
|
||||
self.config_table.setItem(table_row, 4, self._create_centered_item(str(config.get('定时发布', ''))))
|
||||
# 间隔时间:保持原始状态,空值显示为空
|
||||
interval_val = config.get('间隔时间', '')
|
||||
# 如果是数字类型(0或其他数字),显示数字;如果是空字符串,保持为空
|
||||
if interval_val == '' or interval_val is None:
|
||||
interval_display = ''
|
||||
else:
|
||||
interval_display = str(interval_val)
|
||||
self.config_table.setItem(table_row, 5, self._create_centered_item(interval_display))
|
||||
# 达人链接列:设置 tooltip 显示完整内容
|
||||
url_text = str(config.get('达人链接', ''))
|
||||
url_item = self._create_centered_item(url_text)
|
||||
url_item.setToolTip(url_text) # 悬停显示完整链接
|
||||
self.config_table.setItem(table_row, 6, url_item)
|
||||
self.config_table.setItem(table_row, 7, self._create_centered_item(str(config.get('执行人', ''))))
|
||||
self._set_status_item(table_row, str(config.get('情况', '待执行')))
|
||||
# 文件路径列(第9列,索引为9),如果配置中没有则显示空,设置 tooltip
|
||||
file_path = str(config.get('文件路径', ''))
|
||||
file_path_item = self._create_centered_item(file_path)
|
||||
file_path_item.setToolTip(file_path) # 悬停显示完整路径
|
||||
self.config_table.setItem(table_row, 9, file_path_item)
|
||||
self._set_progress_item(table_row, str(config.get('情况', '待执行')))
|
||||
self._set_action_buttons(table_row, config_index)
|
||||
|
||||
# 确保所有行使用一致的行高
|
||||
|
||||
# 先统一设置行高,再填数据,避免后设行高导致勾选框列布局重算、出现“除第一行外往下移”
|
||||
default_row_height = self.config_table.verticalHeader().defaultSectionSize()
|
||||
for row in range(self.config_table.rowCount()):
|
||||
for row in range(display_rows):
|
||||
self.config_table.setRowHeight(row, default_row_height)
|
||||
|
||||
# 先清除第 0 列所有单元格控件,避免连续刷新时旧勾选框残留导致错位
|
||||
for row in range(display_rows):
|
||||
self.config_table.removeCellWidget(row, 0)
|
||||
|
||||
# 填充期间禁止表格重绘,避免中间布局导致勾选框错位
|
||||
self.config_table.setUpdatesEnabled(False)
|
||||
try:
|
||||
# 填充数据行
|
||||
for table_row, config_index in enumerate(self.page_row_indices):
|
||||
config = self.configs[config_index]
|
||||
# 第0列:勾选框(传入行高,固定高度+居中对齐,避免第一页第二行等错位)
|
||||
self._set_checkbox_item(table_row, config_index, default_row_height)
|
||||
# 其他列的索引均+1
|
||||
self.config_table.setItem(table_row, 1, self._create_centered_item(str(config.get('多多id', ''))))
|
||||
self.config_table.setItem(table_row, 2, self._create_centered_item(str(config.get('序号', ''))))
|
||||
self.config_table.setItem(table_row, 3, self._create_centered_item(str(config.get('话题', ''))))
|
||||
self.config_table.setItem(table_row, 4, self._create_centered_item(str(config.get('定时发布', ''))))
|
||||
# 间隔时间:保持原始状态,空值显示为空
|
||||
interval_val = config.get('间隔时间', '')
|
||||
if interval_val == '' or interval_val is None:
|
||||
interval_display = ''
|
||||
else:
|
||||
interval_display = str(interval_val)
|
||||
self.config_table.setItem(table_row, 5, self._create_centered_item(interval_display))
|
||||
url_text = str(config.get('达人链接', ''))
|
||||
url_item = self._create_centered_item(url_text)
|
||||
url_item.setToolTip(url_text)
|
||||
self.config_table.setItem(table_row, 6, url_item)
|
||||
self.config_table.setItem(table_row, 7, self._create_centered_item(str(config.get('执行人', ''))))
|
||||
self._set_status_item(table_row, str(config.get('情况', '待执行')))
|
||||
file_path = str(config.get('文件路径', ''))
|
||||
file_path_item = self._create_centered_item(file_path)
|
||||
file_path_item.setToolTip(file_path)
|
||||
self.config_table.setItem(table_row, 9, file_path_item)
|
||||
self._set_progress_item(table_row, str(config.get('情况', '待执行')))
|
||||
self._set_action_buttons(table_row, config_index)
|
||||
finally:
|
||||
self.config_table.setUpdatesEnabled(True)
|
||||
# 强制表格立即完成布局,避免第一页第二行等勾选框错位
|
||||
self.config_table.doItemsLayout()
|
||||
|
||||
# 重新启用排序功能(但不会自动排序已填充的数据)
|
||||
self.config_table.setSortingEnabled(True)
|
||||
@@ -3486,8 +2809,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _setup_model_view(self):
|
||||
"""切换到大数据量 Model/View 模式"""
|
||||
headers = ['多多ID', '序号', '话题', '定时发布', '间隔时间', '达人链接', '执行人', '情况', '文件路径', '进度',
|
||||
'操作']
|
||||
headers = MODEL_VIEW_HEADERS
|
||||
if self.table_model is None:
|
||||
self.table_model = ConfigTableModel(self.configs, headers, self)
|
||||
self.table_proxy = QSortFilterProxyModel(self)
|
||||
@@ -3578,7 +2900,14 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def update_data(self):
|
||||
"""更新数据:找出文件并保存到各行的文件路径列"""
|
||||
# 防止连续点击导致重入,避免勾选框错位
|
||||
if getattr(self, '_update_data_running', False):
|
||||
return
|
||||
try:
|
||||
self._update_data_running = True
|
||||
if hasattr(self, 'update_data_btn'):
|
||||
self.update_data_btn.setEnabled(False)
|
||||
|
||||
if not self.configs:
|
||||
self._show_infobar("warning", "提示", "请先导入Excel配置文件")
|
||||
return
|
||||
@@ -3681,6 +3010,10 @@ class MainWindow(QMainWindow):
|
||||
logger.error(f"更新数据失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
self._update_data_running = False
|
||||
if hasattr(self, 'update_data_btn'):
|
||||
self.update_data_btn.setEnabled(True)
|
||||
|
||||
def _index_matches_name(self, index, name_or_stem):
|
||||
"""序号与文件名/文件夹名是否匹配。支持:序号、序号.mp4、序号-xxx、01 与 1 数字等价。"""
|
||||
@@ -4590,4 +3923,3 @@ def main():
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
# docker run honeygain/honeygain -tou-accept -email ddrwode1@gmail.com -pass 040828cjj -device DEVICE_NAME
|
||||
|
||||
56
gui_constants.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""GUI 常量与配置:路径、表头、列映射、视频扩展名等。"""
|
||||
|
||||
import os
|
||||
|
||||
# 默认资料文件夹名(相对桌面)
|
||||
DEFAULT_FOLDER_NAME = "多多自动化发文"
|
||||
|
||||
# 表格列与配置键的映射(Model 列索引 -> config 键)
|
||||
COLUMN_KEY_MAPPING = {
|
||||
0: "多多id",
|
||||
1: "序号",
|
||||
2: "话题",
|
||||
3: "定时发布",
|
||||
4: "间隔时间",
|
||||
5: "达人链接",
|
||||
6: "执行人",
|
||||
7: "情况",
|
||||
8: "文件路径",
|
||||
9: "",
|
||||
10: "",
|
||||
}
|
||||
|
||||
# 表格列与配置键的映射(仅可编辑列,用于 setData)
|
||||
COLUMN_KEY_MAPPING_EDIT = {
|
||||
0: "多多id",
|
||||
1: "序号",
|
||||
2: "话题",
|
||||
3: "定时发布",
|
||||
4: "间隔时间",
|
||||
5: "达人链接",
|
||||
6: "执行人",
|
||||
7: "情况",
|
||||
8: "文件路径",
|
||||
}
|
||||
|
||||
# 配置表格表头(12 列:勾选 + 9 数据 + 进度 + 操作)
|
||||
TABLE_HEADERS = [
|
||||
'☑', '多多ID', '序号', '话题', '定时发布', '间隔时间', '达人链接',
|
||||
'执行人', '情况', '文件路径', '进度', '操作'
|
||||
]
|
||||
|
||||
# Model/View 模式表头(11 列:无勾选列)
|
||||
MODEL_VIEW_HEADERS = [
|
||||
'多多ID', '序号', '话题', '定时发布', '间隔时间', '达人链接',
|
||||
'执行人', '情况', '文件路径', '进度', '操作'
|
||||
]
|
||||
|
||||
# 视频文件扩展名
|
||||
VIDEO_EXTENSIONS = ['.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm']
|
||||
|
||||
|
||||
def get_default_folder_path():
|
||||
"""获取默认文件夹路径(桌面/多多自动化发文)"""
|
||||
desktop = os.path.join(os.path.expanduser("~"), "Desktop")
|
||||
return os.path.join(desktop, DEFAULT_FOLDER_NAME)
|
||||
129
gui_models.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""表格数据模型与代理:ConfigTableModel、TableActionDelegate。"""
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
QStyle,
|
||||
QStyledItemDelegate,
|
||||
QStyleOptionProgressBar,
|
||||
QStyleOptionButton,
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex
|
||||
|
||||
from gui_constants import COLUMN_KEY_MAPPING, COLUMN_KEY_MAPPING_EDIT, MODEL_VIEW_HEADERS
|
||||
|
||||
|
||||
class ConfigTableModel(QAbstractTableModel):
|
||||
"""大数据量表格模型"""
|
||||
|
||||
def __init__(self, configs, headers, parent=None):
|
||||
super().__init__(parent)
|
||||
self._configs = configs
|
||||
self._headers = headers if headers is not None else MODEL_VIEW_HEADERS
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
return len(self._configs)
|
||||
|
||||
def columnCount(self, parent=QModelIndex()):
|
||||
return len(self._headers)
|
||||
|
||||
def data(self, index, role=Qt.DisplayRole):
|
||||
if not index.isValid():
|
||||
return None
|
||||
row = index.row()
|
||||
col = index.column()
|
||||
config = self._configs[row]
|
||||
key = COLUMN_KEY_MAPPING.get(col, "")
|
||||
if role == Qt.DisplayRole:
|
||||
return str(config.get(key, "")) if key else ""
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
return Qt.AlignCenter
|
||||
elif role == Qt.ToolTipRole:
|
||||
if col in (5, 8) and key:
|
||||
return str(config.get(key, ""))
|
||||
return None
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if role != Qt.DisplayRole:
|
||||
return None
|
||||
if orientation == Qt.Horizontal:
|
||||
return self._headers[section]
|
||||
return section + 1
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return Qt.NoItemFlags
|
||||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
|
||||
|
||||
def setData(self, index, value, role=Qt.EditRole):
|
||||
if role != Qt.EditRole or not index.isValid():
|
||||
return False
|
||||
row = index.row()
|
||||
col = index.column()
|
||||
key = COLUMN_KEY_MAPPING_EDIT.get(col, "")
|
||||
if not key:
|
||||
return False
|
||||
self._configs[row][key] = str(value)
|
||||
self.dataChanged.emit(index, index, [Qt.DisplayRole])
|
||||
return True
|
||||
|
||||
def removeRows(self, row, count, parent=QModelIndex()):
|
||||
if row < 0 or row + count > len(self._configs):
|
||||
return False
|
||||
self.beginRemoveRows(parent, row, row + count - 1)
|
||||
del self._configs[row:row + count]
|
||||
self.endRemoveRows()
|
||||
return True
|
||||
|
||||
def update_data(self, configs):
|
||||
"""更新内部数据并刷新视图"""
|
||||
self.beginResetModel()
|
||||
self._configs = configs
|
||||
self.endResetModel()
|
||||
|
||||
|
||||
class TableActionDelegate(QStyledItemDelegate):
|
||||
"""Model/View 操作列 + 进度列 delegate"""
|
||||
|
||||
def __init__(self, parent, on_delete):
|
||||
super().__init__(parent)
|
||||
self.on_delete = on_delete
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
if index.column() == 9:
|
||||
status_text = index.sibling(index.row(), 7).data() or ""
|
||||
value = 0
|
||||
if "完成" in status_text or "成功" in status_text:
|
||||
value = 100
|
||||
elif "执行中" in status_text or "进行" in status_text:
|
||||
value = 60
|
||||
elif "待" in status_text:
|
||||
value = 10
|
||||
bar = QStyleOptionProgressBar()
|
||||
bar.rect = option.rect.adjusted(6, option.rect.height() // 3, -6, -option.rect.height() // 3)
|
||||
bar.minimum = 0
|
||||
bar.maximum = 100
|
||||
bar.progress = value
|
||||
bar.textVisible = False
|
||||
QApplication.style().drawControl(QStyle.CE_ProgressBar, bar, painter)
|
||||
return
|
||||
if index.column() == 10:
|
||||
rect = option.rect
|
||||
delete_rect = rect.adjusted(6, 4, -6, -4)
|
||||
delete_btn = QStyleOptionButton()
|
||||
delete_btn.rect = delete_rect
|
||||
delete_btn.text = "删除"
|
||||
QApplication.style().drawControl(QStyle.CE_PushButton, delete_btn, painter)
|
||||
return
|
||||
super().paint(painter, option, index)
|
||||
|
||||
def editorEvent(self, event, model, option, index):
|
||||
if index.column() != 10:
|
||||
return super().editorEvent(event, model, option, index)
|
||||
if event.type() == event.MouseButtonRelease:
|
||||
rect = option.rect
|
||||
delete_rect = rect.adjusted(6, 4, -6, -4)
|
||||
if delete_rect.contains(event.pos()):
|
||||
self.on_delete(index)
|
||||
return True
|
||||
return False
|
||||
470
gui_worker.py
Normal file
@@ -0,0 +1,470 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""后台工作线程:执行单个/批量上传任务,与 main.Pdd 交互。"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
|
||||
from main import Pdd
|
||||
from loguru import logger
|
||||
|
||||
from gui_constants import get_default_folder_path, VIDEO_EXTENSIONS
|
||||
|
||||
|
||||
class WorkerThread(QThread):
|
||||
"""工作线程,用于执行自动化任务"""
|
||||
finished = pyqtSignal(bool, str)
|
||||
log_message = pyqtSignal(str)
|
||||
progress = pyqtSignal(int)
|
||||
item_result = pyqtSignal(object) # 单条发布结果(dict)
|
||||
|
||||
def __init__(self, config_data, is_batch_mode, prepared_files=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.config_data = config_data
|
||||
self.is_batch_mode = is_batch_mode
|
||||
self.prepared_files = prepared_files # 预查找的文件列表
|
||||
self.is_running = True
|
||||
|
||||
def run(self):
|
||||
sink_id = None
|
||||
start_ts = time.time()
|
||||
try:
|
||||
sink_id = logger.add(lambda msg: self.log_message.emit(str(msg).strip()))
|
||||
if self.is_batch_mode:
|
||||
self.run_batch_mode()
|
||||
else:
|
||||
self.run_single_mode()
|
||||
except Exception as e:
|
||||
error_msg = f"执行失败: {str(e)}"
|
||||
self.finished.emit(False, error_msg)
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(f"执行失败: {e}")
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
finally:
|
||||
try:
|
||||
cost = time.time() - start_ts
|
||||
self.log_message.emit(f"[线程] 任务结束,耗时 {cost:.2f}s")
|
||||
except Exception:
|
||||
pass
|
||||
if sink_id is not None:
|
||||
try:
|
||||
logger.remove(sink_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run_single_mode(self):
|
||||
"""执行单个上传模式(action方法)"""
|
||||
try:
|
||||
config = self.config_data
|
||||
self.log_message.emit(f"开始处理单个任务(逐个上传模式)...")
|
||||
|
||||
pdd = Pdd(
|
||||
url=config.get('达人链接', ''),
|
||||
user_id=config.get('多多id', ''),
|
||||
time_start=config.get('定时发布', '') if config.get('定时发布') else None,
|
||||
ht=config.get('话题', ''),
|
||||
index=config.get('序号', ''),
|
||||
title=config.get('标题', None)
|
||||
)
|
||||
|
||||
if self.prepared_files:
|
||||
self.log_message.emit(f"使用预查找的文件列表,共 {len(self.prepared_files)} 个文件")
|
||||
self.progress.emit(30)
|
||||
|
||||
first_file = self.prepared_files[0]
|
||||
if first_file['path'].is_file():
|
||||
folder_path = str(first_file['path'].parent.parent)
|
||||
else:
|
||||
folder_path = str(first_file['path'].parent.parent)
|
||||
|
||||
self.log_message.emit(f"使用文件夹路径: {folder_path}")
|
||||
try:
|
||||
result = pdd.action(folder_path=folder_path, collect_all_videos=False)
|
||||
ok = bool(result.get("ok")) if isinstance(result, dict) else True
|
||||
if not ok:
|
||||
self.log_message.emit(f"发布校验失败: {result.get('reason') if isinstance(result, dict) else ''}")
|
||||
if isinstance(result, dict):
|
||||
self.item_result.emit({
|
||||
"user_id": config.get("多多id", ""),
|
||||
"index": config.get("序号", ""),
|
||||
"name": config.get("标题", "") or "",
|
||||
"ok": ok,
|
||||
"reason": result.get("reason", ""),
|
||||
})
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(ok, f"单个任务执行完成(ok={ok})")
|
||||
except Exception as e:
|
||||
error_msg = f"执行action方法失败: {str(e)}"
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(error_msg)
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
self.finished.emit(False, error_msg)
|
||||
return
|
||||
else:
|
||||
folder_path = config.get('文件夹路径', '').strip()
|
||||
if not folder_path:
|
||||
folder_path = get_default_folder_path()
|
||||
|
||||
if not os.path.exists(folder_path):
|
||||
error_msg = f"文件夹路径不存在: {folder_path}"
|
||||
self.finished.emit(False, error_msg)
|
||||
self.log_message.emit(error_msg)
|
||||
return
|
||||
|
||||
self.log_message.emit(f"使用文件夹路径: {folder_path}")
|
||||
self.progress.emit(30)
|
||||
is_batch_mode = self.is_batch_mode
|
||||
|
||||
try:
|
||||
result = pdd.action(folder_path=folder_path, collect_all_videos=is_batch_mode)
|
||||
ok = bool(result.get("ok")) if isinstance(result, dict) else True
|
||||
if not ok:
|
||||
self.log_message.emit(f"发布校验失败: {result.get('reason') if isinstance(result, dict) else ''}")
|
||||
if isinstance(result, dict):
|
||||
self.item_result.emit({
|
||||
"user_id": config.get("多多id", ""),
|
||||
"index": config.get("序号", ""),
|
||||
"name": config.get("标题", "") or "",
|
||||
"ok": ok,
|
||||
"reason": result.get("reason", ""),
|
||||
})
|
||||
except Exception as e:
|
||||
error_msg = f"执行action方法失败: {str(e)}"
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(error_msg)
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
self.finished.emit(False, error_msg)
|
||||
return
|
||||
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(ok, f"单个任务执行完成(ok={ok})")
|
||||
except Exception as e:
|
||||
error_msg = f"执行失败: {str(e)}"
|
||||
self.finished.emit(False, error_msg)
|
||||
self.log_message.emit(error_msg)
|
||||
logger.error(f"执行失败: {e}")
|
||||
import traceback
|
||||
traceback_str = traceback.format_exc()
|
||||
logger.error(traceback_str)
|
||||
self.log_message.emit(f"错误详情: {traceback_str}")
|
||||
|
||||
def run_batch_mode(self):
|
||||
"""执行批量上传模式:先批量上传所有视频,然后逐个上传图片"""
|
||||
try:
|
||||
config = self.config_data
|
||||
self.log_message.emit(f"开始处理批量上传任务...")
|
||||
index = config.get('序号', '')
|
||||
|
||||
pdd = Pdd(
|
||||
url=config.get('达人链接', ''),
|
||||
user_id=config.get('多多id', ''),
|
||||
time_start=config.get('定时发布', '') if config.get('定时发布') else None,
|
||||
ht=config.get('话题', ''),
|
||||
index=index,
|
||||
title=config.get('标题', None)
|
||||
)
|
||||
|
||||
if self.prepared_files:
|
||||
self.log_message.emit(f"使用预查找的文件列表,共 {len(self.prepared_files)} 个文件")
|
||||
self.progress.emit(30)
|
||||
|
||||
video_file_paths = [f for f in self.prepared_files if f['path'].is_file() and any(
|
||||
f['path'].suffix.lower() == ext for ext in VIDEO_EXTENSIONS)]
|
||||
image_folders = [f for f in self.prepared_files if f['path'].is_dir()]
|
||||
|
||||
if video_file_paths:
|
||||
self.log_message.emit(f"找到 {len(video_file_paths)} 个视频文件,开始批量上传...")
|
||||
self.progress.emit(30)
|
||||
|
||||
def on_video_done(result_item):
|
||||
try:
|
||||
payload = dict(result_item)
|
||||
payload["user_id"] = config.get("多多id", "")
|
||||
if "index" not in payload or not payload.get("index"):
|
||||
for vid_file in video_file_paths:
|
||||
if str(vid_file.get('path')) == str(payload.get('path')):
|
||||
payload["index"] = vid_file.get("index", config.get("序号", ""))
|
||||
break
|
||||
if "index" not in payload or not payload.get("index"):
|
||||
payload["index"] = config.get("序号", "")
|
||||
self.item_result.emit(payload)
|
||||
self.log_message.emit(f" {'✓' if payload.get('ok') else '✗'} {payload.get('name', '')} 已更新状态")
|
||||
except Exception as e:
|
||||
self.log_message.emit(f" 回调处理异常: {e}")
|
||||
|
||||
result = pdd.action1(folder_path=video_file_paths, on_item_done=on_video_done)
|
||||
ok = bool(result.get("ok")) if isinstance(result, dict) else True
|
||||
if not ok:
|
||||
fails = [r for r in (result.get("results") or []) if not r.get("ok")]
|
||||
self.log_message.emit(f"发布校验失败条数: {len(fails)}")
|
||||
for r in fails[:20]:
|
||||
self.log_message.emit(f" ✗ {r.get('name')} | {r.get('reason')}")
|
||||
self.log_message.emit(f"批量上传视频完成,共处理 {len(video_file_paths)} 个视频")
|
||||
else:
|
||||
self.log_message.emit("未找到视频文件,跳过视频上传")
|
||||
ok = True
|
||||
|
||||
if image_folders:
|
||||
self.log_message.emit(f"找到 {len(image_folders)} 个图片文件夹,开始逐个上传...")
|
||||
self.progress.emit(70)
|
||||
|
||||
for idx, image_folder_info in enumerate(image_folders):
|
||||
if not self.is_running:
|
||||
break
|
||||
image_folder = str(image_folder_info['path'])
|
||||
self.log_message.emit(f"处理第 {idx + 1}/{len(image_folders)} 个图片文件夹: {image_folder}")
|
||||
pdd.action(folder_path=image_folder)
|
||||
self.log_message.emit(f"图片文件夹 {idx + 1} 上传完成")
|
||||
|
||||
self.log_message.emit(f"所有图片文件夹上传完成,共处理 {len(image_folders)} 个文件夹")
|
||||
else:
|
||||
self.log_message.emit("未找到图片文件夹,跳过图片上传")
|
||||
|
||||
self.progress.emit(100)
|
||||
self.finished.emit(ok,
|
||||
f"批量任务执行完成,共处理 {len(video_file_paths)} 个视频和 {len(image_folders)} 个图片文件夹(ok={ok})")
|
||||
else:
|
||||
folder_path = config.get('文件夹路径', '').strip()
|
||||
if not folder_path:
|
||||
folder_path = get_default_folder_path()
|
||||
|
||||
if not os.path.exists(folder_path):
|
||||
self.finished.emit(False, f"文件夹路径不存在: {folder_path}")
|
||||
return
|
||||
|
||||
self.log_message.emit("第一步:收集所有视频文件...")
|
||||
video_file_paths = self.prepare_batch_files(folder_path, config)
|
||||
self.log_message.emit("第二步:收集所有图片文件夹...")
|
||||
image_folders = self.prepare_image_folders(folder_path, index)
|
||||
|
||||
if video_file_paths:
|
||||
self.log_message.emit(f"找到 {len(video_file_paths)} 个视频文件,开始批量上传...")
|
||||
self.progress.emit(30)
|
||||
pdd.action1(folder_path=video_file_paths)
|
||||
self.log_message.emit(f"批量上传视频完成,共处理 {len(video_file_paths)} 个视频")
|
||||
else:
|
||||
self.log_message.emit("未找到视频文件,跳过视频上传")
|
||||
|
||||
if image_folders:
|
||||
self.log_message.emit(f"找到 {len(image_folders)} 个图片文件夹,开始逐个上传...")
|
||||
self.progress.emit(70)
|
||||
|
||||
for idx, image_folder in enumerate(image_folders):
|
||||
if not self.is_running:
|
||||
break
|
||||
self.log_message.emit(f"处理第 {idx + 1}/{len(image_folders)} 个图片文件夹: {image_folder}")
|
||||
pdd.action(folder_path=image_folder)
|
||||
self.log_message.emit(f"图片文件夹 {idx + 1} 上传完成")
|
||||
|
||||
self.log_message.emit(f"所有图片文件夹上传完成,共处理 {len(image_folders)} 个文件夹")
|
||||
else:
|
||||
self.log_message.emit("未找到图片文件夹,跳过图片上传")
|
||||
|
||||
self.progress.emit(100)
|
||||
total_count = len(video_file_paths) + len(image_folders)
|
||||
self.finished.emit(True,
|
||||
f"批量任务执行完成,共处理 {len(video_file_paths)} 个视频和 {len(image_folders)} 个图片文件夹")
|
||||
|
||||
except Exception as e:
|
||||
self.finished.emit(False, f"批量执行失败: {str(e)}")
|
||||
logger.error(f"批量执行失败: {e}")
|
||||
|
||||
def prepare_image_folders(self, folder_path, index):
|
||||
"""收集所有匹配序号的图片文件夹(与main.py中action方法的逻辑一致)"""
|
||||
image_folders = []
|
||||
try:
|
||||
if not folder_path or not os.path.exists(folder_path):
|
||||
logger.warning(f"文件夹路径无效或不存在: {folder_path}")
|
||||
return image_folders
|
||||
if not os.access(folder_path, os.R_OK):
|
||||
logger.error(f"没有权限读取文件夹: {folder_path}")
|
||||
return image_folders
|
||||
try:
|
||||
subdirs = os.listdir(folder_path)
|
||||
except PermissionError:
|
||||
logger.error(f"没有权限访问文件夹: {folder_path}")
|
||||
return image_folders
|
||||
except Exception as e:
|
||||
logger.error(f"读取文件夹失败: {e}")
|
||||
return image_folders
|
||||
|
||||
for file in subdirs:
|
||||
try:
|
||||
file_path = os.path.join(folder_path, file)
|
||||
if not os.path.isdir(file_path):
|
||||
continue
|
||||
try:
|
||||
files = os.listdir(file_path)
|
||||
except (PermissionError, Exception) as e:
|
||||
logger.warning(f"读取子文件夹失败: {file_path}, 错误: {e}")
|
||||
continue
|
||||
|
||||
for file_name in files:
|
||||
try:
|
||||
if file_name.startswith('.'):
|
||||
continue
|
||||
file_names = file_name.split("-")
|
||||
if len(file_names) > 0 and file_names[0] == str(index):
|
||||
path = Path(os.path.join(file_path, file_name))
|
||||
if path.exists() and path.is_dir():
|
||||
image_folders.append(str(path))
|
||||
except Exception as e:
|
||||
logger.warning(f"处理文件 {file_name} 时出错: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.warning(f"处理子文件夹 {file} 时出错: {e}")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"收集图片文件夹失败: {e}", exc_info=True)
|
||||
return image_folders
|
||||
|
||||
def count_videos_in_folder(self, folder_path, index):
|
||||
"""统计文件夹中匹配序号的视频文件数量(与main.py逻辑一致)"""
|
||||
count = 0
|
||||
try:
|
||||
for file in os.listdir(folder_path):
|
||||
file_path = os.path.join(folder_path, file)
|
||||
if not os.path.isdir(file_path):
|
||||
continue
|
||||
files = os.listdir(file_path)
|
||||
for file_name in files:
|
||||
if ".mp4" in file_name:
|
||||
file_names = file_name.split("-")
|
||||
if len(file_names) > 0 and file_names[0] == str(index):
|
||||
path = Path(os.path.join(file_path, file_name))
|
||||
if path.is_file():
|
||||
count += 1
|
||||
else:
|
||||
for sub_file in os.listdir(path):
|
||||
sub_path = Path(os.path.join(path, sub_file))
|
||||
if sub_path.is_file():
|
||||
count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"统计视频文件失败: {e}")
|
||||
return count
|
||||
|
||||
def prepare_batch_files(self, folder_path, config):
|
||||
"""准备批量上传的文件列表(与main.py中的action1方法逻辑一致)"""
|
||||
file_paths = []
|
||||
if not config:
|
||||
logger.error("配置为空,无法准备批量上传文件")
|
||||
return file_paths
|
||||
index = str(config.get('序号', '')).strip()
|
||||
if not index:
|
||||
logger.warning("序号为空,无法准备批量上传文件")
|
||||
return file_paths
|
||||
if not folder_path or not os.path.exists(folder_path):
|
||||
logger.error(f"文件夹路径无效或不存在: {folder_path}")
|
||||
return file_paths
|
||||
if not os.access(folder_path, os.R_OK):
|
||||
logger.error(f"没有权限读取文件夹: {folder_path}")
|
||||
return file_paths
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info("开始准备批量上传文件列表...")
|
||||
logger.info(f"文件夹路径: {folder_path}")
|
||||
logger.info(f"查找序号: {index}")
|
||||
|
||||
try:
|
||||
try:
|
||||
all_items = os.listdir(folder_path)
|
||||
subdirs = [f for f in all_items if os.path.isdir(os.path.join(folder_path, f))]
|
||||
except PermissionError:
|
||||
logger.error(f"没有权限访问文件夹: {folder_path}")
|
||||
return file_paths
|
||||
except Exception as e:
|
||||
logger.error(f"读取文件夹失败: {e}")
|
||||
return file_paths
|
||||
|
||||
logger.info(f"在最外层文件夹下找到 {len(subdirs)} 个子文件夹(多多ID文件夹)")
|
||||
|
||||
for subdir_name in subdirs:
|
||||
file_path = os.path.join(folder_path, subdir_name)
|
||||
logger.info(f" 正在扫描子文件夹: {subdir_name}")
|
||||
if not os.path.isdir(file_path):
|
||||
logger.info(f" 跳过(不是文件夹)")
|
||||
continue
|
||||
files = os.listdir(file_path)
|
||||
logger.info(f" 该文件夹下有 {len(files)} 个项目")
|
||||
|
||||
for file_name in files:
|
||||
logger.info(f" 检查项目: {file_name}")
|
||||
if ".mp4" in file_name:
|
||||
logger.info(f" ✓ 是视频文件(包含.mp4)")
|
||||
file_names = file_name.split("-")
|
||||
logger.info(f" 文件名分割结果: {file_names}")
|
||||
|
||||
if len(file_names) > 0 and file_names[0] == str(index):
|
||||
logger.info(f" ✓ 序号匹配!序号: {file_names[0]}, 目标序号: {index}")
|
||||
path = Path(os.path.join(file_path, file_name))
|
||||
|
||||
if path.is_file():
|
||||
logger.info(f" ✓ 是文件,添加到列表: {path.name}")
|
||||
file_paths.append({
|
||||
"url": config.get('达人链接', ''),
|
||||
"user_id": config.get('多多id', ''),
|
||||
"time_start": config.get('定时发布', '') if config.get('定时发布') else None,
|
||||
"ht": config.get('话题', ''),
|
||||
"index": str(index),
|
||||
"path": path
|
||||
})
|
||||
logger.info(f" 当前视频总数: {len(file_paths)}")
|
||||
else:
|
||||
logger.info(f" 是文件夹,扫描其中的文件...")
|
||||
try:
|
||||
sub_files = os.listdir(path)
|
||||
logger.info(f" 文件夹中有 {len(sub_files)} 个文件")
|
||||
for sub_file in sub_files:
|
||||
sub_path = Path(os.path.join(path, sub_file))
|
||||
if sub_path.is_file():
|
||||
logger.info(f" ✓ 添加文件: {sub_file}")
|
||||
file_paths.append({
|
||||
"url": config.get('达人链接', ''),
|
||||
"user_id": config.get('多多id', ''),
|
||||
"time_start": config.get('定时发布', '') if config.get('定时发布') else None,
|
||||
"ht": config.get('话题', ''),
|
||||
"index": str(index),
|
||||
"path": sub_path
|
||||
})
|
||||
logger.info(f" 当前视频总数: {len(file_paths)}")
|
||||
except Exception as e:
|
||||
logger.error(f" 扫描子文件夹失败: {e}")
|
||||
else:
|
||||
if len(file_names) > 0:
|
||||
logger.info(f" ✗ 序号不匹配: {file_names[0]} != {index}")
|
||||
else:
|
||||
logger.info(f" ✗ 文件名格式不正确,无法提取序号")
|
||||
else:
|
||||
logger.info(f" ✗ 不是视频文件(不包含.mp4),跳过")
|
||||
except Exception as e:
|
||||
logger.error(f"准备批量文件失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info(f"文件收集完成!共找到 {len(file_paths)} 个视频文件")
|
||||
if file_paths:
|
||||
logger.info("视频文件列表:")
|
||||
for idx, video_info in enumerate(file_paths, 1):
|
||||
logger.info(f" {idx}. {video_info['path'].name} ({video_info['path']})")
|
||||
return file_paths
|
||||
|
||||
def stop(self):
|
||||
"""停止执行"""
|
||||
self.is_running = False
|
||||
|
||||
def requestInterruption(self):
|
||||
"""请求中断线程(重写QThread方法)"""
|
||||
super().requestInterruption()
|
||||
self.is_running = False
|
||||
@@ -13,7 +13,14 @@
|
||||
"padded_top_topics_start_index": 0,
|
||||
"taxonomy_version": 0,
|
||||
"top_topics_and_observing_domains": [ ]
|
||||
}, {
|
||||
"calculation_time": "13414257993246306",
|
||||
"config_version": 0,
|
||||
"model_version": "0",
|
||||
"padded_top_topics_start_index": 0,
|
||||
"taxonomy_version": 0,
|
||||
"top_topics_and_observing_domains": [ ]
|
||||
} ],
|
||||
"hex_encoded_hmac_key": "434BF7DBD7DA573B45E0A11AD9045A61B6221D14AE2F9A341E2FEF659AF071F6",
|
||||
"next_scheduled_calculation_time": "13414055093389664"
|
||||
"next_scheduled_calculation_time": "13414862793246352"
|
||||
}
|
||||
|
||||
BIN
user/user_data/Default/Cache/Cache_Data/f_00005e
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_00005f
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000060
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000061
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000062
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000063
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000064
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000065
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000066
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000067
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000068
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_000069
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_00006a
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_00006b
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_00006c
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_00006d
Normal file
BIN
user/user_data/Default/Cache/Cache_Data/f_00006e
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_00006f
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000070
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000071
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000072
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000073
Normal file
|
After Width: | Height: | Size: 437 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000074
Normal file
|
After Width: | Height: | Size: 542 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000075
Normal file
|
After Width: | Height: | Size: 564 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000076
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
user/user_data/Default/Cache/Cache_Data/f_000077
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
user/user_data/Default/Code Cache/js/096d3e2dd445b821_0
Normal file
BIN
user/user_data/Default/Code Cache/js/2396e49f092006fd_0
Normal file
BIN
user/user_data/Default/Code Cache/js/31a6b5cd5e29aa30_0
Normal file
BIN
user/user_data/Default/Code Cache/js/6d49a66dc6b03af6_0
Normal file
BIN
user/user_data/Default/Code Cache/js/732be6385167db84_0
Normal file
BIN
user/user_data/Default/Code Cache/js/76a4226c81f6b71d_0
Normal file
BIN
user/user_data/Default/Code Cache/js/8421d98e47d18680_0
Normal file
BIN
user/user_data/Default/Code Cache/js/88d11c3095a5258e_0
Normal file
BIN
user/user_data/Default/Code Cache/js/c79b72204f45162a_0
Normal file
BIN
user/user_data/Default/Code Cache/js/c956194c129c3cdd_0
Normal file
BIN
user/user_data/Default/Code Cache/js/c9b95dfe6f4ca4d9_0
Normal file
BIN
user/user_data/Default/Code Cache/js/cfa05a72369877be_0
Normal file
BIN
user/user_data/Default/Code Cache/js/d190a49222eb5ce9_0
Normal file
BIN
user/user_data/Default/Code Cache/js/e97be77875af8f53_0
Normal file
@@ -1,3 +1,3 @@
|
||||
2026/01/27-00:28:51.452 37b0 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/MANIFEST-000001
|
||||
2026/01/27-00:28:51.452 37b0 Recovering log #6
|
||||
2026/01/27-00:28:51.452 37b0 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/000006.log
|
||||
2026/01/30-22:48:23.361 4e48 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/MANIFEST-000001
|
||||
2026/01/30-22:48:23.361 4e48 Recovering log #6
|
||||
2026/01/30-22:48:23.362 4e48 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/000006.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2026/01/27-00:28:49.803 7ddc Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/MANIFEST-000001
|
||||
2026/01/27-00:28:49.804 7ddc Recovering log #6
|
||||
2026/01/27-00:28:49.804 7ddc Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/000006.log
|
||||
2026/01/30-22:46:22.373 18a0 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/MANIFEST-000001
|
||||
2026/01/30-22:46:22.373 18a0 Recovering log #6
|
||||
2026/01/30-22:46:22.373 18a0 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\File System\Origins/000006.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2026/01/27-00:27:47.374 12ac Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/MANIFEST-000001
|
||||
2026/01/27-00:27:47.374 12ac Recovering log #3
|
||||
2026/01/27-00:27:47.398 12ac Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/000003.log
|
||||
2026/01/30-22:48:26.857 4cfc Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/MANIFEST-000001
|
||||
2026/01/30-22:48:26.857 4cfc Recovering log #3
|
||||
2026/01/30-22:48:26.858 4cfc Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/000003.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2026/01/27-00:15:07.342 9b40 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/MANIFEST-000001
|
||||
2026/01/27-00:15:07.342 9b40 Recovering log #3
|
||||
2026/01/27-00:15:07.377 9b40 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/000003.log
|
||||
2026/01/30-22:46:33.257 d78 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/MANIFEST-000001
|
||||
2026/01/30-22:46:33.258 d78 Recovering log #3
|
||||
2026/01/30-22:46:33.259 d78 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\GCM Store/000003.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2026/01/27-00:28:50.976 17f4 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/MANIFEST-000001
|
||||
2026/01/27-00:28:50.979 17f4 Recovering log #11
|
||||
2026/01/27-00:28:50.982 17f4 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/000011.log
|
||||
2026/01/30-22:48:22.438 2eb0 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/MANIFEST-000001
|
||||
2026/01/30-22:48:22.442 2eb0 Recovering log #11
|
||||
2026/01/30-22:48:22.445 2eb0 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/000011.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
2026/01/27-00:28:49.356 42fc Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/MANIFEST-000001
|
||||
2026/01/27-00:28:49.359 42fc Recovering log #11
|
||||
2026/01/27-00:28:49.362 42fc Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/000011.log
|
||||
2026/01/30-22:46:21.597 5448 Reusing MANIFEST C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/MANIFEST-000001
|
||||
2026/01/30-22:46:21.601 5448 Recovering log #11
|
||||
2026/01/30-22:46:21.604 5448 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/000011.log
|
||||
|
||||