Files
Xinyue-Search/extend/util/operations.py
T
2025-03-28 17:00:42 +08:00

328 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
主要功能实现逻辑,包括批量转存和分享。
:author: assassing
:contact: https://github.com/hxz393
:copyright: Copyright 2024, hxz393. 保留所有权利。
"""
import sys
import time
import traceback
from typing import Union, List, Dict, Any
import ttkbootstrap as ttk
from src.constants import EXP_MAP, DELAY_SECONDS, INVALID_CHARS, ERROR_CODES, SAVE_LIMIT, COLOR_MAP
from src.network import Network
from src.ui import CustomDialog
from src.utils import thread_it, write_config, parse_response, normalize_link, parse_url_and_code, update_cookie, generate_code
class Operations:
"""
包括主功能实现流程,涉及部分对 UI 的操作。
日志文字不额外提取到映射字典,方便定位错误。
:param root: 主窗口
"""
def __init__(self, root):
self.root = root
self.network = Network()
def save(self) -> None:
"""
转存主函数,流程为:
1.获取用户输入,更新按钮和标签状态;
2.检查用户输入,有问题则终止并发送日志;
3.获取 bdstoken,如果获取失败则终止;
4.获取根目录文件列表,建立转存目录;
5.批量转存链接。先验证链接有效性,再转存文件,插入结果到日志框。
"""
try:
self.prepare_run()
self.setup_save()
self.handle_input()
self.handle_bdstoken()
self.handle_create_dir(folder_name=self.folder_name)
self.handle_process_save()
except Exception as e:
self.insert_logs(f'程序出现未预料错误,信息如下:\n{e}\n{traceback.format_exc()}')
finally:
self.network.s.close()
self.change_status('stopped')
def share(self) -> None:
"""
分享主函数,流程为:
0.弹出设置窗口,设置分享参数;
1.获取用户输入,更新按钮和标签状态;
2.检查用户输入,有问题则终止并发送日志;
3.获取 bdstoken,如果获取失败则终止;
4.获取目标目录文件列表,如果为空则终止;
5.批量分享文件。先向链接框插入文件名,再创建分享,插入结果到日志框。
"""
try:
# 创建对话框实例,如果对话框没有返回输入值,则忽略
self.dialog_result = CustomDialog(self.root)
if self.dialog_result.result:
self.prepare_run()
self.handle_input(task_count_check=False)
self.handle_bdstoken()
self.handle_list_dir()
self.setup_share()
self.handle_process_share()
except Exception as e:
self.insert_logs(f'程序出现未预料错误,信息如下:\n{e}\n{traceback.format_exc()}')
finally:
self.network.s.close()
self.change_status('stopped')
def prepare_run(self) -> None:
"""获取变量,准备运行。转存和分享共用逻辑"""
# 从用户输入获取变量
self.cookie = "".join(self.root.entry_cookie.get().split())
self.folder_name = "".join(self.root.entry_folder_name.get().split())
self.network.s.trust_env = self.root.var_trust_env.get()
self.custom_mode = self.root.var_custom_mode.get()
self.check_mode = self.root.var_check_mode.get()
# 更新 cookie、初始化任务总数、更改状态、写入配置文件
self.completed_task_count = 0
self.network.headers['Cookie'] = self.cookie
self.change_status('init')
write_config(f'{self.cookie}\n{self.folder_name}')
def setup_save(self) -> None:
"""准备链接,更新状态"""
# 从文本链接控件获取全部链接,清洗并标准化链接。注意链接后拼接一个空格,是为了后面能统一处理带与不带提取码的链接
raw_links = self.root.text_links.get(1.0, ttk.END).splitlines()
self.link_list = [normalize_link(f'{link} ') for link in raw_links if link]
self.link_list_org = list(dict.fromkeys(link for link in raw_links if link))
# 更新任务总数和状态
self.total_task_count = len(self.link_list)
self.change_status('running')
def setup_share(self) -> None:
"""准备参数,更新状态"""
# 从设置对话框获取参数变量
self.expiry, self.password, self.random_password = self.dialog_result.result
# 更新任务总数和状态
self.total_task_count = len(self.dir_list_all)
self.change_status('sharing')
def handle_input(self, task_count_check: bool = True) -> None:
"""输入检查,如链接数限制和 cookie 格式"""
# 转存时才检查输入链接数量
if task_count_check:
self.check_condition(self.total_task_count == 0,
message='无有效链接。')
self.check_condition(self.total_task_count > SAVE_LIMIT,
message=f'批量转存一次不能超过 {SAVE_LIMIT},当前链接数:{self.total_task_count}')
# cookie 带非 ascii 字符,或不包含 BAIDUID 时,铁定不对
self.check_condition(not self.cookie.isascii() or self.cookie.find('BAIDUID') == -1,
message='百度网盘 cookie 输入不正确,请查看使用帮助。')
# 非法字符由官方规定的。符号 / 可以使用,作为子目录的分隔符
self.check_condition(any(char in self.folder_name for char in INVALID_CHARS),
message=r'转存目录名有非法字符,不能包含:< > | * ? \ :')
def handle_bdstoken(self) -> None:
"""获取 bdstoken 相关逻辑"""
self.network.bdstoken = self.network.get_bdstoken()
self.check_condition(isinstance(self.network.bdstoken, int),
message=f'没获取到 bdstoken 参数,错误代码:{self.network.bdstoken}')
def handle_list_dir(self) -> None:
"""获取目标目录下的文件和目录列表"""
self.dir_list_all = self.network.get_dir_list(f'/{self.folder_name}')
self.check_condition(isinstance(self.dir_list_all, int) or not self.dir_list_all,
message=f'目录 {self.folder_name} 中没获取到任何内容,请求返回:{self.dir_list_all}')
def handle_create_dir(self, folder_name: str) -> None:
"""新建目录。如果目录已存在则不新建,否则会建立一个带时间戳的空目录"""
result = self.network.get_dir_list(f'/{folder_name}')
# 如果 result 为错误代码数字,代表目标目录不存在
if self.folder_name and isinstance(result, int):
return_code = self.network.create_dir(folder_name)
self.check_condition(return_code != 0,
message=f'创建目录失败,错误代码:{return_code}')
def handle_process_save(self) -> None:
"""执行批量转存"""
for url_code in self.link_list:
self.process_save(url_code)
def handle_process_share(self) -> None:
"""执行批量分享"""
for info in self.dir_list_all:
self.process_share(info)
def process_save(self, url_code: str) -> None:
"""执行转存操作并记录结果"""
# 跳过非网盘链接
if 'https://pan.baidu.com/' not in url_code:
self.insert_logs(f'不支持的链接:{url_code}')
else:
# 执行转存过程,通过简单的循环判断是否要暂停
self.pause_detection(url_code)
# 处理完毕一个链接,更新状态栏任务计数
self.change_status('update')
def process_share(self, info: Dict[str, Any]) -> None:
"""执行分享操作并记录结果"""
# 插入要分享的文件或文件夹到链接输入框,对文件夹加入 "/" 标记来区别
is_dir = "/" if info["isdir"] == 1 else ""
filename = f"{info['server_filename']}{is_dir}"
msg = f'目录:{filename}' if is_dir else f'文件:{filename}'
self.insert_logs(msg, alt=True)
# 处理提取码
password = self.password if not self.random_password else generate_code()
# 发送创建分享请求
r = self.network.create_share(info['fs_id'], EXP_MAP[self.expiry], password)
if isinstance(r, str):
result = f'分享成功:{r}?pwd={password} {msg}'
else:
result = f'分享失败:错误代码({r} {msg}'
# 记录日志,更改状态
self.insert_logs(result)
self.change_status('update')
# noinspection PyArgumentList
def change_status(self, status: str) -> None:
"""运行状态变化更新函数"""
if status == 'init':
self.root.label_status.config(text='准备就绪!', font=('', 10), bootstyle='primary', cursor="arrow")
self.root.label_status.unbind("<Button-1>")
self.root.text_logs.config(fg=COLOR_MAP['text'])
self.root.text_logs.delete(1.0, ttk.END)
elif status == 'running':
self.running = True
self.root.bottom_share.config(state="disabled")
self.root.bottom_save.config(text='点击暂停', bootstyle='danger', command=lambda: self.change_status('paused'))
elif status == 'paused':
self.running = False
self.root.bottom_save.config(text='点击继续', bootstyle='success', command=lambda: self.change_status('running'))
elif status == 'update':
self.completed_task_count += 1
self.root.label_status.config(text=f'总进度:{self.completed_task_count}/{self.total_task_count}', bootstyle='success')
elif status == 'sharing':
self.root.text_links.delete(1.0, ttk.END)
self.root.bottom_share.config(state="disabled")
self.root.bottom_save.config(state="disabled")
elif status == 'error':
self.root.label_status.config(text='发生错误:', bootstyle='danger')
else:
self.running = False
self.root.bottom_save.config(text='批量转存', state="normal", bootstyle='primary', command=lambda: thread_it(self.save, ))
self.root.bottom_share.config(state="normal")
def check_condition(self, condition: bool, message: str) -> None:
"""
用户输入或函数返回检查。
如果条件 condition 为 True,表示错误不可恢复,中断当前任务流程。
单个链接处理出错,应该直接调用 insert_logs 函数记录错误,不中断任务。
:param condition: 条件表达式
:param message: 插入到日志框的内容
"""
if condition:
self.change_status('error')
self.insert_logs(message)
sys.exit()
def insert_logs(self, message: str, alt: bool = False) -> None:
"""
在文本框末尾插入内容
:param message: 插入到日志框的内容
:param alt: 如果为 True,插入到链接输入框,用于批量分享时记录文件名
"""
text_box = self.root.text_links if alt else self.root.text_logs
text_box.insert('end', f'{message}\n')
text_box.see(ttk.END)
def pause_detection(self, url_code: str) -> None:
"""循环检测暂停逻辑,每个转存任务开始时都会检测"""
# 只要 self.running 状态没变,会一直等待。而状态变化由用户点击按钮控制
while not self.running:
time.sleep(DELAY_SECONDS)
# 执行验证链接和转存文件
self.verify_and_save(url_code)
# 转存完毕等待一小段时间。如果要转存超过 1000 个链接,可以增加 DELAY_SECONDS 到 3 以上。
time.sleep(DELAY_SECONDS)
def verify_and_save(self, url_code: str) -> None:
"""验证链接和转存文件"""
# 验证链接是否有效,返回的数字为错误代码,反之返回参数列表
result = self.verify_link(*parse_url_and_code(url_code))
# 如果开启检查模式,插入检查结果,然后结束
if self.check_mode:
self.check_only(result, url_code)
else:
self.save_file(result, url_code, self.folder_name)
def verify_link(self, url: str, password: str) -> Union[List[str], int]:
"""验证链接有效性,验证通过返回转存所需参数列表"""
# 对于有提取码的链接先验证提取码,试图获取更新 bdclnd
if password:
bdclnd = self.network.verify_pass_code(url, password)
# 如果 bdclnd 是错误代码,直接返回
if isinstance(bdclnd, int):
return bdclnd
# 更新 bdclnd 到 cookie
self.network.headers['Cookie'] = update_cookie(bdclnd, self.network.headers['Cookie'])
# 直接访问没有提取码的链接,或更新 bdclnd 后再次访问,获取转存必须的 3 个参数
response = self.network.get_transfer_params(url)
# 这里不考虑网络异常了,假设请求一定会返回页面内容,对其进行解析
return parse_response(response)
def check_only(self, result: Union[List[str], int], url_code: str) -> None:
"""开启检查模式时,只管判断返回值类型,并输出结果到日志"""
if isinstance(result, list):
self.insert_logs(f'链接有效:{url_code} {"目录" if result[4] == ["1"] else "文件"}{result[3]}')
else:
self.insert_logs(f'链接无效:{url_code} 原因:{ERROR_CODES.get(result, f"错误代码({result}")}')
def creat_user_dir(self, folder_name: str) -> str:
"""建立用户指定目录,返回完整路径。目录名从原始输入取,函数为 custom_mode 专用"""
self.check_condition(not folder_name, message='必须输入转存目录')
# 对原始输入进行分割
link_org_sep = self.link_list_org[self.completed_task_count].split()
# 建立自定义目录,如果没有指定则用行数代替
custom_folder = link_org_sep[0]
folder_name = f'{folder_name}/{custom_folder}' if 'pan.baidu.com' not in custom_folder else f'{folder_name}/{self.completed_task_count + 1}'
# 此处用替换处理目标目录名非法字符,不报错了
folder_name = folder_name.translate(str.maketrans({char: '_' for char in INVALID_CHARS}))
self.handle_create_dir(folder_name)
return folder_name
def save_file(self, result: Union[List[str], int], url_code: str, folder_name: str) -> None:
"""转存文件。返回结果为列表时,执行转存文件,否则跳过转存"""
file_info = ""
if isinstance(result, list):
file_info = f'{"目录" if result[4] == ["1"] else "文件"}{result[3]}'
# 如果开启安全转存模式,对每个转存链接建立目录
if self.custom_mode:
folder_name = self.creat_user_dir(folder_name)
result = self.network.transfer_file(result, folder_name)
file_info = f'{file_info} 转存到:{folder_name}'
# 改在这里检查链接重复,在日志中查找。链接有出现过不转存,直接赋值结果为错误代码 4
elif url_code in self.root.text_logs.get('1.0', 'end'):
result = 4
# 正常转存
else:
result = self.network.transfer_file(result, folder_name)
# 最后插入转存结果到日志框
self.insert_logs(f'{ERROR_CODES.get(result, f"转存失败,错误代码({result}")}{url_code} {file_info}')