更新3.0

This commit is contained in:
admin
2025-03-28 17:00:42 +08:00
parent 86038423e1
commit 6cc49273b6
62 changed files with 6470 additions and 5240 deletions
+327
View File
@@ -0,0 +1,327 @@
"""
主要功能实现逻辑,包括批量转存和分享。
: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}')