gui
第一版完整版
@@ -6,7 +6,7 @@ a = Analysis(
|
|||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
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=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
|
|||||||
@@ -827,6 +827,11 @@
|
|||||||
'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\gettext.py',
|
'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\gettext.py',
|
||||||
'PYMODULE'),
|
'PYMODULE'),
|
||||||
('glob', 'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\glob.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'),
|
('gzip', 'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\gzip.py', 'PYMODULE'),
|
||||||
('hashlib',
|
('hashlib',
|
||||||
'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\hashlib.py',
|
'C:\\Users\\27942\\.conda\\envs\\haha\\Lib\\hashlib.py',
|
||||||
|
|||||||
@@ -156,6 +156,9 @@ imports:
|
|||||||
• <a href="#enum">enum</a>
|
• <a href="#enum">enum</a>
|
||||||
• <a href="#functools">functools</a>
|
• <a href="#functools">functools</a>
|
||||||
• <a href="#genericpath">genericpath</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="#heapq">heapq</a>
|
||||||
• <a href="#io">io</a>
|
• <a href="#io">io</a>
|
||||||
• <a href="#json">json</a>
|
• <a href="#json">json</a>
|
||||||
@@ -2835,6 +2838,8 @@ imported by:
|
|||||||
• <a href="#PyQt5.QtWidgets">PyQt5.QtWidgets</a>
|
• <a href="#PyQt5.QtWidgets">PyQt5.QtWidgets</a>
|
||||||
• <a href="#PyQt5.QtXml">PyQt5.QtXml</a>
|
• <a href="#PyQt5.QtXml">PyQt5.QtXml</a>
|
||||||
• <a href="#gui_app.py">gui_app.py</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._rc.resource">qfluentwidgets._rc.resource</a>
|
||||||
• <a href="#qfluentwidgets.common.animation">qfluentwidgets.common.animation</a>
|
• <a href="#qfluentwidgets.common.animation">qfluentwidgets.common.animation</a>
|
||||||
• <a href="#qfluentwidgets.common.config">qfluentwidgets.common.config</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.QtGui">PyQt5.QtGui</a>
|
||||||
• <a href="#PyQt5.QtSvg">PyQt5.QtSvg</a>
|
• <a href="#PyQt5.QtSvg">PyQt5.QtSvg</a>
|
||||||
• <a href="#gui_app.py">gui_app.py</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.animation">qfluentwidgets.common.animation</a>
|
||||||
• <a href="#qfluentwidgets.common.font">qfluentwidgets.common.font</a>
|
• <a href="#qfluentwidgets.common.font">qfluentwidgets.common.font</a>
|
||||||
• <a href="#qfluentwidgets.common.icon">qfluentwidgets.common.icon</a>
|
• <a href="#qfluentwidgets.common.icon">qfluentwidgets.common.icon</a>
|
||||||
@@ -14331,6 +14337,65 @@ imported by:
|
|||||||
|
|
||||||
</div>
|
</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">
|
<div class="node">
|
||||||
<a name="gzip"></a>
|
<a name="gzip"></a>
|
||||||
<a target="code" href="///C:/Users/27942/.conda/envs/haha/Lib/gzip.py" type="text/plain"><tt>gzip</tt></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">
|
<div class="import">
|
||||||
imported by:
|
imported by:
|
||||||
<a href="#gui_app.py">gui_app.py</a>
|
<a href="#gui_app.py">gui_app.py</a>
|
||||||
|
• <a href="#gui_worker">gui_worker</a>
|
||||||
• <a href="#loguru">loguru</a>
|
• <a href="#loguru">loguru</a>
|
||||||
• <a href="#loguru._asyncio_loop">loguru._asyncio_loop</a>
|
• <a href="#loguru._asyncio_loop">loguru._asyncio_loop</a>
|
||||||
• <a href="#loguru._better_exceptions">loguru._better_exceptions</a>
|
• <a href="#loguru._better_exceptions">loguru._better_exceptions</a>
|
||||||
@@ -17300,6 +17366,7 @@ imports:
|
|||||||
<div class="import">
|
<div class="import">
|
||||||
imported by:
|
imported by:
|
||||||
<a href="#gui_app.py">gui_app.py</a>
|
<a href="#gui_app.py">gui_app.py</a>
|
||||||
|
• <a href="#gui_worker">gui_worker</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30286,6 +30353,8 @@ imported by:
|
|||||||
• <a href="#gettext">gettext</a>
|
• <a href="#gettext">gettext</a>
|
||||||
• <a href="#glob">glob</a>
|
• <a href="#glob">glob</a>
|
||||||
• <a href="#gui_app.py">gui_app.py</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="#gzip">gzip</a>
|
||||||
• <a href="#http.cookiejar">http.cookiejar</a>
|
• <a href="#http.cookiejar">http.cookiejar</a>
|
||||||
• <a href="#http.server">http.server</a>
|
• <a href="#http.server">http.server</a>
|
||||||
@@ -43422,6 +43491,7 @@ imported by:
|
|||||||
• <a href="#filelock._util">filelock._util</a>
|
• <a href="#filelock._util">filelock._util</a>
|
||||||
• <a href="#filelock._windows">filelock._windows</a>
|
• <a href="#filelock._windows">filelock._windows</a>
|
||||||
• <a href="#gui_app.py">gui_app.py</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.metadata">importlib.metadata</a>
|
||||||
• <a href="#importlib.resources._common">importlib.resources._common</a>
|
• <a href="#importlib.resources._common">importlib.resources._common</a>
|
||||||
• <a href="#importlib.resources.abc">importlib.resources.abc</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="#filelock.asyncio">filelock.asyncio</a>
|
||||||
• <a href="#gc">gc</a>
|
• <a href="#gc">gc</a>
|
||||||
• <a href="#gui_app.py">gui_app.py</a>
|
• <a href="#gui_app.py">gui_app.py</a>
|
||||||
|
• <a href="#gui_worker">gui_worker</a>
|
||||||
• <a href="#gzip">gzip</a>
|
• <a href="#gzip">gzip</a>
|
||||||
• <a href="#http.cookiejar">http.cookiejar</a>
|
• <a href="#http.cookiejar">http.cookiejar</a>
|
||||||
• <a href="#http.cookies">http.cookies</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="#concurrent.futures.process">concurrent.futures.process</a>
|
||||||
• <a href="#doctest">doctest</a>
|
• <a href="#doctest">doctest</a>
|
||||||
• <a href="#gui_app.py">gui_app.py</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="#http.cookiejar">http.cookiejar</a>
|
||||||
• <a href="#logging">logging</a>
|
• <a href="#logging">logging</a>
|
||||||
• <a href="#loguru._better_exceptions">loguru._better_exceptions</a>
|
• <a href="#loguru._better_exceptions">loguru._better_exceptions</a>
|
||||||
|
|||||||
BIN
dist/多多自动发文助手.exe → dist/多多发文助手.exe
vendored
818
gui_app.py
@@ -17,7 +17,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QStyleOptionProgressBar, QStyleOptionButton, QHeaderView,
|
QStyleOptionProgressBar, QStyleOptionButton, QHeaderView,
|
||||||
QTabWidget, QSplitter, QSizePolicy, QCheckBox
|
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
|
QSortFilterProxyModel, QRegularExpression, QSettings, QTimer, QEvent
|
||||||
from PyQt5.QtGui import QFont, QTextDocument, QTextCursor, QKeySequence, QColor, QPainter
|
from PyQt5.QtGui import QFont, QTextDocument, QTextCursor, QKeySequence, QColor, QPainter
|
||||||
|
|
||||||
@@ -30,689 +30,14 @@ from qfluentwidgets import (
|
|||||||
from main import Pdd
|
from main import Pdd
|
||||||
from loguru import logger
|
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):
|
class MainWindow(QMainWindow):
|
||||||
"""主窗口"""
|
"""主窗口"""
|
||||||
@@ -1149,10 +474,7 @@ class MainWindow(QMainWindow):
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
self.config_table.setColumnCount(12)
|
self.config_table.setColumnCount(12)
|
||||||
self.config_table.setHorizontalHeaderLabels([
|
self.config_table.setHorizontalHeaderLabels(TABLE_HEADERS)
|
||||||
'☑', '多多ID', '序号', '话题', '定时发布', '间隔时间', '达人链接',
|
|
||||||
'执行人', '情况', '文件路径', '进度', '操作'
|
|
||||||
])
|
|
||||||
self.table_column_filter.addItem("全部列")
|
self.table_column_filter.addItem("全部列")
|
||||||
# 第0列为勾选框;记录下拉项对应的表格列索引及 Model 列索引
|
# 第0列为勾选框;记录下拉项对应的表格列索引及 Model 列索引
|
||||||
self._filter_table_columns = []
|
self._filter_table_columns = []
|
||||||
@@ -1502,10 +824,12 @@ class MainWindow(QMainWindow):
|
|||||||
# 确保标志被重置
|
# 确保标志被重置
|
||||||
self.is_updating_table = False
|
self.is_updating_table = False
|
||||||
|
|
||||||
def _set_checkbox_item(self, row, config_index):
|
def _set_checkbox_item(self, row, config_index, row_height=None):
|
||||||
"""设置勾选框列(第0列),使用嵌套布局确保真正水平+垂直居中"""
|
"""设置勾选框列(第0列)。row_height 传入时使用固定高度+居中对齐,避免第二行起勾选框往下错位。"""
|
||||||
checkbox = QCheckBox()
|
checkbox = QCheckBox()
|
||||||
|
checkbox.blockSignals(True)
|
||||||
checkbox.setChecked(self.configs[config_index].get('勾选', False)) # 默认不勾选
|
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.stateChanged.connect(lambda state, idx=config_index: self._on_checkbox_changed(idx, state))
|
||||||
checkbox.setStyleSheet(
|
checkbox.setStyleSheet(
|
||||||
"QCheckBox { margin: 0px; padding: 0px; }"
|
"QCheckBox { margin: 0px; padding: 0px; }"
|
||||||
@@ -1513,24 +837,15 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
wrapper = QWidget()
|
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 实现垂直居中
|
layout = QVBoxLayout(wrapper)
|
||||||
v_layout = QVBoxLayout(wrapper)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
v_layout.setContentsMargins(0, 0, 0, 0)
|
layout.setSpacing(0)
|
||||||
v_layout.setSpacing(0)
|
layout.setAlignment(Qt.AlignCenter)
|
||||||
v_layout.addStretch()
|
layout.addWidget(checkbox)
|
||||||
|
|
||||||
# 内层水平布局:通过左右 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()
|
|
||||||
|
|
||||||
self.config_table.setCellWidget(row, 0, wrapper)
|
self.config_table.setCellWidget(row, 0, wrapper)
|
||||||
|
|
||||||
@@ -3431,44 +2746,52 @@ class MainWindow(QMainWindow):
|
|||||||
display_rows = max(len(self.page_row_indices), min_display_rows)
|
display_rows = max(len(self.page_row_indices), min_display_rows)
|
||||||
self.config_table.setRowCount(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()
|
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)
|
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)
|
self.config_table.setSortingEnabled(True)
|
||||||
|
|
||||||
@@ -3486,8 +2809,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _setup_model_view(self):
|
def _setup_model_view(self):
|
||||||
"""切换到大数据量 Model/View 模式"""
|
"""切换到大数据量 Model/View 模式"""
|
||||||
headers = ['多多ID', '序号', '话题', '定时发布', '间隔时间', '达人链接', '执行人', '情况', '文件路径', '进度',
|
headers = MODEL_VIEW_HEADERS
|
||||||
'操作']
|
|
||||||
if self.table_model is None:
|
if self.table_model is None:
|
||||||
self.table_model = ConfigTableModel(self.configs, headers, self)
|
self.table_model = ConfigTableModel(self.configs, headers, self)
|
||||||
self.table_proxy = QSortFilterProxyModel(self)
|
self.table_proxy = QSortFilterProxyModel(self)
|
||||||
@@ -3578,7 +2900,14 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def update_data(self):
|
def update_data(self):
|
||||||
"""更新数据:找出文件并保存到各行的文件路径列"""
|
"""更新数据:找出文件并保存到各行的文件路径列"""
|
||||||
|
# 防止连续点击导致重入,避免勾选框错位
|
||||||
|
if getattr(self, '_update_data_running', False):
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
|
self._update_data_running = True
|
||||||
|
if hasattr(self, 'update_data_btn'):
|
||||||
|
self.update_data_btn.setEnabled(False)
|
||||||
|
|
||||||
if not self.configs:
|
if not self.configs:
|
||||||
self._show_infobar("warning", "提示", "请先导入Excel配置文件")
|
self._show_infobar("warning", "提示", "请先导入Excel配置文件")
|
||||||
return
|
return
|
||||||
@@ -3681,6 +3010,10 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"更新数据失败: {e}")
|
logger.error(f"更新数据失败: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
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):
|
def _index_matches_name(self, index, name_or_stem):
|
||||||
"""序号与文件名/文件夹名是否匹配。支持:序号、序号.mp4、序号-xxx、01 与 1 数字等价。"""
|
"""序号与文件名/文件夹名是否匹配。支持:序号、序号.mp4、序号-xxx、01 与 1 数字等价。"""
|
||||||
@@ -4590,4 +3923,3 @@ def main():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
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,
|
"padded_top_topics_start_index": 0,
|
||||||
"taxonomy_version": 0,
|
"taxonomy_version": 0,
|
||||||
"top_topics_and_observing_domains": [ ]
|
"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",
|
"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/30-22:48:23.361 4e48 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/30-22:48:23.361 4e48 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.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/30-22:46:22.373 18a0 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/30-22:46:22.373 18a0 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 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/30-22:48:26.857 4cfc 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/30-22:48:26.857 4cfc 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.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/30-22:46:33.257 d78 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/30-22:46:33.258 d78 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.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/30-22:48:22.438 2eb0 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/30-22:48:22.442 2eb0 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.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/30-22:46:21.597 5448 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/30-22:46:21.601 5448 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.604 5448 Reusing old log C:\Users\27942\Desktop\haha\user\user_data\Default\Local Storage\leveldb/000011.log
|
||||||
|
|||||||