178 lines
7.3 KiB
Python
178 lines
7.3 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import time
|
||
|
|
import re
|
||
|
|
from typing import Optional, Callable
|
||
|
|
from playwright.sync_api import sync_playwright
|
||
|
|
|
||
|
|
class BaiduUploader:
|
||
|
|
"""
|
||
|
|
百度网盘上传工具 - 纯净上传版 (移除分享逻辑)
|
||
|
|
"""
|
||
|
|
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.baidu.com/"
|
||
|
|
|
||
|
|
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)
|
||
|
|
|
||
|
|
# 提取项目编号
|
||
|
|
project_id = ""
|
||
|
|
match = re.search(r'(【[A-Za-z0-9]+】)', target_folder_name)
|
||
|
|
if match:
|
||
|
|
project_id = match.group(1)
|
||
|
|
|
||
|
|
try:
|
||
|
|
with sync_playwright() as p:
|
||
|
|
self._log(f"正在启动浏览器...")
|
||
|
|
|
||
|
|
launch_args = {
|
||
|
|
"user_data_dir": self.cookies_dir,
|
||
|
|
"headless": False,
|
||
|
|
"executable_path": self.chrome_path if self.chrome_path and os.path.exists(self.chrome_path) else None,
|
||
|
|
"viewport": {"width": 1280, "height": 800}
|
||
|
|
}
|
||
|
|
if not launch_args["executable_path"]:
|
||
|
|
launch_args.pop("executable_path")
|
||
|
|
|
||
|
|
context = p.chromium.launch_persistent_context(**launch_args)
|
||
|
|
context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
|
||
|
|
|
||
|
|
page = context.new_page()
|
||
|
|
page.set_default_timeout(60000)
|
||
|
|
|
||
|
|
# 1. 登录
|
||
|
|
self._log("检测登录状态...")
|
||
|
|
page.goto(self.url, wait_until="domcontentloaded")
|
||
|
|
|
||
|
|
if not self._wait_for_login(page):
|
||
|
|
self._log("请手动扫码登录...")
|
||
|
|
page.wait_for_selector("text=全部文件", timeout=180000)
|
||
|
|
|
||
|
|
# 2. 导航
|
||
|
|
self._log("登录成功,进入目标路径...")
|
||
|
|
page.get_by_text("全部文件").first.click(force=True)
|
||
|
|
time.sleep(4)
|
||
|
|
|
||
|
|
if not self._scan_and_enter(page, root_path):
|
||
|
|
self._create_folder(page, root_path)
|
||
|
|
time.sleep(2); self._scan_and_enter(page, root_path)
|
||
|
|
|
||
|
|
search_key = project_id if project_id else target_folder_name
|
||
|
|
if not self._scan_and_enter(page, search_key, is_fuzzy=True):
|
||
|
|
self._create_folder(page, target_folder_name)
|
||
|
|
time.sleep(2); self._scan_and_enter(page, search_key, is_fuzzy=True)
|
||
|
|
|
||
|
|
# 3. 查重
|
||
|
|
if self._check_file_exists_robust(page, filename):
|
||
|
|
self._log(f"云端已存在文件 '{filename}',跳过上传。")
|
||
|
|
time.sleep(2)
|
||
|
|
context.close()
|
||
|
|
return True
|
||
|
|
|
||
|
|
# 4. 执行上传
|
||
|
|
self._log(f"开始传输: {filename}")
|
||
|
|
page.set_input_files("input[type=file]", file_path)
|
||
|
|
|
||
|
|
# 5. 监测列表确认成功
|
||
|
|
self._log("上传已启动,监测云端状态中...")
|
||
|
|
success = self._wait_for_file_in_list(page, filename)
|
||
|
|
|
||
|
|
if success:
|
||
|
|
self._log("\n" + "="*30)
|
||
|
|
self._log("百度网盘上传圆满成功!")
|
||
|
|
self._log("="*30 + "\n")
|
||
|
|
time.sleep(3)
|
||
|
|
else:
|
||
|
|
self._log("警告:监测超时,未能在列表中看到文件,请手动核实。")
|
||
|
|
|
||
|
|
context.close()
|
||
|
|
return success
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
self._log(f"程序异常: {str(e)}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def _wait_for_login(self, page) -> bool:
|
||
|
|
for _ in range(10):
|
||
|
|
if page.get_by_text("全部文件").count() > 0: return True
|
||
|
|
time.sleep(2)
|
||
|
|
return False
|
||
|
|
|
||
|
|
def _scan_and_enter(self, page, target_name: str, is_fuzzy: bool = False) -> bool:
|
||
|
|
try:
|
||
|
|
page.evaluate("window.scrollTo(0, 300)")
|
||
|
|
found = page.evaluate(f"""
|
||
|
|
(args) => {{
|
||
|
|
const {{ name, fuzzy }} = args;
|
||
|
|
const els = [...document.querySelectorAll('span[title], a[title], .wp-s-file-list-drag-copy__item-title-text')];
|
||
|
|
const target = els.find(e => {{
|
||
|
|
const title = e.getAttribute('title') || e.innerText.trim();
|
||
|
|
return fuzzy ? title.includes(name) : title === name;
|
||
|
|
}});
|
||
|
|
if (target) {{
|
||
|
|
target.click();
|
||
|
|
const ev = new MouseEvent('dblclick', {{ 'view': window, 'bubbles': true, 'cancelable': true }});
|
||
|
|
target.dispatchEvent(ev);
|
||
|
|
return true;
|
||
|
|
}}
|
||
|
|
return false;
|
||
|
|
}}
|
||
|
|
""", {"name": target_name, "fuzzy": is_fuzzy})
|
||
|
|
if found: time.sleep(3); return True
|
||
|
|
return False
|
||
|
|
except: return False
|
||
|
|
|
||
|
|
def _create_folder(self, page, folder_name: str):
|
||
|
|
try:
|
||
|
|
page.keyboard.press("Escape")
|
||
|
|
btn = page.get_by_text("新建文件夹").first
|
||
|
|
if btn.count() > 0:
|
||
|
|
btn.click(force=True)
|
||
|
|
time.sleep(1.5)
|
||
|
|
page.keyboard.type(folder_name, delay=50)
|
||
|
|
page.keyboard.press("Enter")
|
||
|
|
time.sleep(3)
|
||
|
|
except: pass
|
||
|
|
|
||
|
|
def _check_file_exists_robust(self, page, filename: str) -> bool:
|
||
|
|
return page.evaluate(f"""
|
||
|
|
(name) => {{
|
||
|
|
const els = [...document.querySelectorAll('span[title], a[title], .wp-s-file-list-drag-copy__item-title-text')];
|
||
|
|
return els.some(e => (e.getAttribute('title') || e.innerText.trim()) === name);
|
||
|
|
}}
|
||
|
|
""", filename)
|
||
|
|
|
||
|
|
def _wait_for_file_in_list(self, page, filename: str) -> bool:
|
||
|
|
start_time = time.time(); last_refresh = time.time()
|
||
|
|
while time.time() - start_time < 1800:
|
||
|
|
if self._check_file_exists_robust(page, filename):
|
||
|
|
content = page.content()
|
||
|
|
# 如果文件在列表里,且页面内容没在报错或重试
|
||
|
|
if not any(x in content for x in ["正在上传", "传输队列", "0B/s"]):
|
||
|
|
return True
|
||
|
|
if time.time() - last_refresh > 25:
|
||
|
|
try: page.locator(".wp-s-file-list-drag-copy__header-breadcrumb-item").last.click(); time.sleep(3)
|
||
|
|
except: pass
|
||
|
|
last_refresh = time.time()
|
||
|
|
time.sleep(5)
|
||
|
|
return False
|