第一版完整版
This commit is contained in:
27942
2026-01-31 10:42:28 +08:00
parent 72ed5e60d6
commit c601e6054a
337 changed files with 7473 additions and 7394 deletions

View File

@@ -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=[],

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -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',

Binary file not shown.

View File

@@ -156,6 +156,9 @@ imports:
&#8226; <a href="#enum">enum</a>
&#8226; <a href="#functools">functools</a>
&#8226; <a href="#genericpath">genericpath</a>
&#8226; <a href="#gui_constants">gui_constants</a>
&#8226; <a href="#gui_models">gui_models</a>
&#8226; <a href="#gui_worker">gui_worker</a>
&#8226; <a href="#heapq">heapq</a>
&#8226; <a href="#io">io</a>
&#8226; <a href="#json">json</a>
@@ -2835,6 +2838,8 @@ imported by:
&#8226; <a href="#PyQt5.QtWidgets">PyQt5.QtWidgets</a>
&#8226; <a href="#PyQt5.QtXml">PyQt5.QtXml</a>
&#8226; <a href="#gui_app.py">gui_app.py</a>
&#8226; <a href="#gui_models">gui_models</a>
&#8226; <a href="#gui_worker">gui_worker</a>
&#8226; <a href="#qfluentwidgets._rc.resource">qfluentwidgets._rc.resource</a>
&#8226; <a href="#qfluentwidgets.common.animation">qfluentwidgets.common.animation</a>
&#8226; <a href="#qfluentwidgets.common.config">qfluentwidgets.common.config</a>
@@ -3075,6 +3080,7 @@ imported by:
&#8226; <a href="#PyQt5.QtGui">PyQt5.QtGui</a>
&#8226; <a href="#PyQt5.QtSvg">PyQt5.QtSvg</a>
&#8226; <a href="#gui_app.py">gui_app.py</a>
&#8226; <a href="#gui_models">gui_models</a>
&#8226; <a href="#qfluentwidgets.common.animation">qfluentwidgets.common.animation</a>
&#8226; <a href="#qfluentwidgets.common.font">qfluentwidgets.common.font</a>
&#8226; <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>
&#8226; <a href="#gui_models">gui_models</a>
&#8226; <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>
&#8226; <a href="#PyQt5.QtWidgets">PyQt5.QtWidgets</a>
&#8226; <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>
&#8226; <a href="#gui_constants">gui_constants</a>
&#8226; <a href="#loguru">loguru</a>
&#8226; <a href="#main">main</a>
&#8226; <a href="#os">os</a>
&#8226; <a href="#pathlib">pathlib</a>
&#8226; <a href="#time">time</a>
&#8226; <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>
&#8226; <a href="#gui_worker">gui_worker</a>
&#8226; <a href="#loguru">loguru</a>
&#8226; <a href="#loguru._asyncio_loop">loguru._asyncio_loop</a>
&#8226; <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>
&#8226; <a href="#gui_worker">gui_worker</a>
</div>
@@ -30286,6 +30353,8 @@ imported by:
&#8226; <a href="#gettext">gettext</a>
&#8226; <a href="#glob">glob</a>
&#8226; <a href="#gui_app.py">gui_app.py</a>
&#8226; <a href="#gui_constants">gui_constants</a>
&#8226; <a href="#gui_worker">gui_worker</a>
&#8226; <a href="#gzip">gzip</a>
&#8226; <a href="#http.cookiejar">http.cookiejar</a>
&#8226; <a href="#http.server">http.server</a>
@@ -43422,6 +43491,7 @@ imported by:
&#8226; <a href="#filelock._util">filelock._util</a>
&#8226; <a href="#filelock._windows">filelock._windows</a>
&#8226; <a href="#gui_app.py">gui_app.py</a>
&#8226; <a href="#gui_worker">gui_worker</a>
&#8226; <a href="#importlib.metadata">importlib.metadata</a>
&#8226; <a href="#importlib.resources._common">importlib.resources._common</a>
&#8226; <a href="#importlib.resources.abc">importlib.resources.abc</a>
@@ -54168,6 +54238,7 @@ imported by:
&#8226; <a href="#filelock.asyncio">filelock.asyncio</a>
&#8226; <a href="#gc">gc</a>
&#8226; <a href="#gui_app.py">gui_app.py</a>
&#8226; <a href="#gui_worker">gui_worker</a>
&#8226; <a href="#gzip">gzip</a>
&#8226; <a href="#http.cookiejar">http.cookiejar</a>
&#8226; <a href="#http.cookies">http.cookies</a>
@@ -54547,6 +54618,7 @@ imported by:
&#8226; <a href="#concurrent.futures.process">concurrent.futures.process</a>
&#8226; <a href="#doctest">doctest</a>
&#8226; <a href="#gui_app.py">gui_app.py</a>
&#8226; <a href="#gui_worker">gui_worker</a>
&#8226; <a href="#http.cookiejar">http.cookiejar</a>
&#8226; <a href="#logging">logging</a>
&#8226; <a href="#loguru._better_exceptions">loguru._better_exceptions</a>

View File

@@ -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
View 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
View 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
View 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

Binary file not shown.

View File

@@ -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"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Binary file not shown.

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More