167 lines
7.5 KiB
Python
167 lines
7.5 KiB
Python
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import time
|
|||
|
|
from typing import Optional, Callable
|
|||
|
|
from playwright.sync_api import sync_playwright
|
|||
|
|
|
|||
|
|
class QuarkUploader:
|
|||
|
|
"""
|
|||
|
|
夸克网盘上传工具 - 终极稳健版 (双重状态锁)
|
|||
|
|
"""
|
|||
|
|
def __init__(self, chrome_path: str, cookies_dir: str, log_callback: Optional[Callable[[str], None]] = None):
|
|||
|
|
self.chrome_path = chrome_path
|
|||
|
|
self.cookies_dir = cookies_dir
|
|||
|
|
self.log_callback = log_callback
|
|||
|
|
self.url = "https://pan.quark.cn/"
|
|||
|
|
|
|||
|
|
if not os.path.exists(self.cookies_dir):
|
|||
|
|
os.makedirs(self.cookies_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
def _log(self, message: str):
|
|||
|
|
print(message)
|
|||
|
|
if self.log_callback:
|
|||
|
|
self.log_callback(message)
|
|||
|
|
|
|||
|
|
def upload_file(self, file_path: str, target_folder_name: str, root_path: str = "精品项目整理") -> bool:
|
|||
|
|
"""
|
|||
|
|
上传文件到夸克网盘
|
|||
|
|
"""
|
|||
|
|
if not os.path.exists(file_path):
|
|||
|
|
self._log(f"错误: 本地文件不存在 {file_path}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
filename = os.path.basename(file_path)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with sync_playwright() as p:
|
|||
|
|
self._log(f"正在启动浏览器...")
|
|||
|
|
launch_args = {
|
|||
|
|
"user_data_dir": self.cookies_dir,
|
|||
|
|
"headless": False,
|
|||
|
|
"viewport": {"width": 1280, "height": 800}
|
|||
|
|
}
|
|||
|
|
if self.chrome_path and os.path.exists(self.chrome_path):
|
|||
|
|
launch_args["executable_path"] = self.chrome_path
|
|||
|
|
|
|||
|
|
context = p.chromium.launch_persistent_context(**launch_args)
|
|||
|
|
page = context.new_page()
|
|||
|
|
|
|||
|
|
self._log("正在打开夸克网盘...")
|
|||
|
|
page.goto(self.url, wait_until="networkidle")
|
|||
|
|
|
|||
|
|
# 1. 登录检测
|
|||
|
|
try:
|
|||
|
|
page.wait_for_selector("text=全部文件", timeout=10000)
|
|||
|
|
except:
|
|||
|
|
self._log("请在浏览器中完成登录...")
|
|||
|
|
page.wait_for_selector("text=全部文件", timeout=180000)
|
|||
|
|
|
|||
|
|
# 2. 导航到目录
|
|||
|
|
self._log(f"定位目录: {root_path} > {target_folder_name}")
|
|||
|
|
page.get_by_text("全部文件").first.click()
|
|||
|
|
page.wait_for_timeout(2000)
|
|||
|
|
|
|||
|
|
# 进入根目录
|
|||
|
|
if not page.get_by_text(root_path, exact=True).first.is_visible():
|
|||
|
|
self._create_folder(page, root_path)
|
|||
|
|
page.get_by_text(root_path, exact=True).first.dblclick()
|
|||
|
|
page.wait_for_timeout(1000)
|
|||
|
|
|
|||
|
|
# 进入项目目录
|
|||
|
|
if not page.get_by_text(target_folder_name, exact=True).first.is_visible():
|
|||
|
|
self._create_folder(page, target_folder_name)
|
|||
|
|
page.get_by_text(target_folder_name, exact=True).first.dblclick()
|
|||
|
|
page.wait_for_timeout(2000)
|
|||
|
|
|
|||
|
|
# 3. 查重
|
|||
|
|
if page.get_by_text(filename, exact=True).count() > 0:
|
|||
|
|
self._log(f"云端已存在 '{filename}',跳过上传。")
|
|||
|
|
context.close()
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
# 4. 执行上传
|
|||
|
|
self._log(f"开始上传: {filename}")
|
|||
|
|
# 清除可能存在的旧“上传成功”提示
|
|||
|
|
page.evaluate("() => { const els = document.querySelectorAll('.ant-notification-notice'); els.forEach(e => e.remove()); }")
|
|||
|
|
|
|||
|
|
page.set_input_files("input[type=file]", file_path)
|
|||
|
|
|
|||
|
|
# 5. 确认上传已启动 (关键一步)
|
|||
|
|
self._log("等待上传任务初始化...")
|
|||
|
|
try:
|
|||
|
|
# 等待出现任何进度指示元素
|
|||
|
|
page.wait_for_selector("text=% , .ant-progress-inner, text=正在上传", timeout=20000)
|
|||
|
|
self._log("检测到上传流已建立。")
|
|||
|
|
except:
|
|||
|
|
self._log("警告:未检测到明显的进度条,可能由于文件较小或 UI 延迟,继续监测列表状态。")
|
|||
|
|
|
|||
|
|
# 6. 深度监测循环
|
|||
|
|
self._log("进入深度监测模式,请保持浏览器前台运行...")
|
|||
|
|
start_time = time.time()
|
|||
|
|
timeout = 1800 # 30分钟
|
|||
|
|
|
|||
|
|
# 连续稳定次数计数
|
|||
|
|
stable_count = 0
|
|||
|
|
|
|||
|
|
while time.time() - start_time < timeout:
|
|||
|
|
# A. 检查全局进度标志
|
|||
|
|
is_uploading = page.get_by_text("%").is_visible() or \
|
|||
|
|
page.get_by_text("正在上传").is_visible() or \
|
|||
|
|
page.locator(".ant-progress-inner").is_visible()
|
|||
|
|
|
|||
|
|
# B. 检查列表中该行的具体状态 (精准匹配)
|
|||
|
|
# 找到包含文件名的那一行 tr,看里面是否有“正在上传”字样
|
|||
|
|
row_status_text = ""
|
|||
|
|
try:
|
|||
|
|
row = page.locator(f"tr:has-text('{filename}')").first
|
|||
|
|
if row.is_visible():
|
|||
|
|
row_status_text = row.inner_text()
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
is_item_active = "正在上传" in row_status_text or "%" in row_status_text or "等待上传" in row_status_text
|
|||
|
|
|
|||
|
|
# C. 确认列表里确实有这个文件
|
|||
|
|
in_list = page.get_by_text(filename, exact=True).count() > 0
|
|||
|
|
|
|||
|
|
if not is_uploading and not is_item_active and in_list:
|
|||
|
|
stable_count += 1
|
|||
|
|
if stable_count >= 5: # 连续 5 次检测(约 15 秒)都处于稳定态
|
|||
|
|
self._log("检测到上传任务已从任务列表中消失,且列表文件状态正常。")
|
|||
|
|
self._log("为了绝对安全,最后等待 15 秒进行数据落盘同步...")
|
|||
|
|
page.wait_for_timeout(15000)
|
|||
|
|
self._log("上传确认圆满成功!")
|
|||
|
|
context.close()
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
self._log(f"上传疑似完成,正在进行稳定性校验 ({stable_count}/5)...")
|
|||
|
|
else:
|
|||
|
|
if is_uploading or is_item_active:
|
|||
|
|
self._log("监测到活跃传输流...")
|
|||
|
|
stable_count = 0 # 只要发现还在传,重置计数
|
|||
|
|
|
|||
|
|
time.sleep(3)
|
|||
|
|
|
|||
|
|
self._log("传输任务监测超时,请确认网速或手动核实。")
|
|||
|
|
context.close()
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
self._log(f"代码执行异常: {str(e)}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def _create_folder(self, page, folder_name: str):
|
|||
|
|
"""创建目录"""
|
|||
|
|
try:
|
|||
|
|
page.get_by_text("新建").first.click()
|
|||
|
|
page.wait_for_timeout(500)
|
|||
|
|
page.get_by_text("新建文件夹").first.click()
|
|||
|
|
page.wait_for_timeout(500)
|
|||
|
|
page.keyboard.type(folder_name)
|
|||
|
|
page.keyboard.press("Enter")
|
|||
|
|
self._log(f"已创建目录: {folder_name}")
|
|||
|
|
page.wait_for_timeout(1500)
|
|||
|
|
except:
|
|||
|
|
pass
|