From 48f3f03c4b400b982e3935ab98aa7ba9af04061f Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sat, 24 Jan 2026 17:43:45 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=BD=91=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- get_links.php | 291 ------------ index.html | 1197 ------------------------------------------------- 2 files changed, 1488 deletions(-) delete mode 100644 get_links.php delete mode 100644 index.html diff --git a/get_links.php b/get_links.php deleted file mode 100644 index 91e7220..0000000 --- a/get_links.php +++ /dev/null @@ -1,291 +0,0 @@ - [ - 'method' => 'GET', - 'header' => [ - 'User-Agent: 123pan-Download-Page/1.0', - 'Accept: application/vnd.github.v3+json', - 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' - ], - 'timeout' => 15, - 'ignore_errors' => true - ], - 'ssl' => [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ] - ]; - - $context = stream_context_create($options); - - // 尝试使用file_get_contents - $response = @file_get_contents($url, false, $context); - - if ($response === false || empty($response)) { - // 尝试使用cURL - $response = getViaCurl($url); - } - - if ($response === false || empty($response)) { - return false; - } - - // 验证响应 - $data = json_decode($response, true); - - if (json_last_error() !== JSON_ERROR_NONE) { - return false; - } - - // 检查是否有错误信息 - if (isset($data['message'])) { - if (strpos($data['message'], 'API rate limit') !== false || - strpos($data['message'], 'rate limit') !== false) { - // GitHub API限制 - error_log('GitHub API rate limit exceeded'); - return false; - } - // 其他API错误 - return false; - } - - // 如果没有数据或不是数组 - if (!is_array($data) || empty($data)) { - return false; - } - - // 处理数据格式 - $processed_releases = []; - foreach ($data as $release) { - // 跳过草稿版 - if (isset($release['draft']) && $release['draft'] === true) { - continue; - } - - // 跳过预发布版(如果需要只显示稳定版) - // if (isset($release['prerelease']) && $release['prerelease'] === true) { - // continue; - // } - - $processed_release = [ - 'tag_name' => $release['tag_name'] ?? '', - 'name' => $release['name'] ?? $release['tag_name'], - 'body' => $release['body'] ?? '', - 'published_at' => $release['published_at'] ?? '', - 'assets' => [] - ]; - - if (isset($release['assets']) && is_array($release['assets'])) { - foreach ($release['assets'] as $asset) { - $processed_release['assets'][] = [ - 'name' => $asset['name'] ?? '', - 'size' => $asset['size'] ?? 0, - 'browser_download_url' => $asset['browser_download_url'] ?? '', - 'content_type' => $asset['content_type'] ?? 'application/octet-stream' - ]; - } - } - - $processed_releases[] = $processed_release; - } - - // 如果没有处理后的发布信息 - if (empty($processed_releases)) { - return false; - } - - return json_encode($processed_releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -} - -/** - * 使用cURL获取数据 - */ -function getViaCurl($url) { - if (!function_exists('curl_init')) { - return false; - } - - $ch = curl_init(); - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_USERAGENT => '123pan-Download-Page/1.0', - CURLOPT_HTTPHEADER => [ - 'Accept: application/vnd.github.v3+json', - 'Accept-Language: zh-CN,zh;q=0.9' - ], - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_TIMEOUT => 15, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_FAILONERROR => false, - CURLOPT_ENCODING => '', // 接受gzip压缩 - CURLOPT_CONNECTTIMEOUT => 10 - ]); - - $response = curl_exec($ch); - - if (curl_errno($ch)) { - curl_close($ch); - return false; - } - - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($http_code === 200 && !empty($response)) { - return $response; - } - - return false; -} - -/** - * 备用数据源 - 返回静态数据 - */ -function getFromAlternativeSource() { - // 尝试获取最新版本的信息 - $latest_url = "https://api.github.com/repos/Qxyz17/123pan/releases/latest"; - $options = [ - 'http' => [ - 'method' => 'GET', - 'header' => [ - 'User-Agent: 123pan-Download-Page/1.0', - 'Accept: application/vnd.github.v3+json' - ], - 'timeout' => 10 - ] - ]; - - $context = stream_context_create($options); - $response = @file_get_contents($latest_url, false, $context); - - if ($response !== false && !empty($response)) { - $data = json_decode($response, true); - - if (json_last_error() === JSON_ERROR_NONE && isset($data['tag_name'])) { - $processed_release = [ - 'tag_name' => $data['tag_name'] ?? '', - 'name' => $data['name'] ?? $data['tag_name'], - 'body' => $data['body'] ?? '最新版本', - 'published_at' => $data['published_at'] ?? date('Y-m-d\TH:i:s\Z'), - 'assets' => [] - ]; - - if (isset($data['assets']) && is_array($data['assets'])) { - foreach ($data['assets'] as $asset) { - $processed_release['assets'][] = [ - 'name' => $asset['name'] ?? '', - 'size' => $asset['size'] ?? 0, - 'browser_download_url' => $asset['browser_download_url'] ?? '', - 'content_type' => $asset['content_type'] ?? 'application/octet-stream' - ]; - } - } - - return json_encode([$processed_release], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - } - - // 如果都失败了,返回静态数据 - return getStaticReleases(); -} - -/** - * 返回静态发布数据 - */ -function getStaticReleases() { - $static_releases = [ - [ - 'tag_name' => 'v1.0.0', - 'name' => '123pan 最新版本', - 'body' => '如果无法自动获取发布信息,请直接访问 GitHub Releases 页面下载最新版本。', - 'published_at' => date('Y-m-d\TH:i:s\Z'), - 'assets' => [ - [ - 'name' => '前往 GitHub Releases 下载', - 'size' => 0, - 'browser_download_url' => 'https://github.com/Qxyz17/123pan/releases', - 'content_type' => 'text/html' - ], - [ - 'name' => '直接下载最新版本', - 'size' => 0, - 'browser_download_url' => 'https://github.com/Qxyz17/123pan/releases/latest', - 'content_type' => 'application/octet-stream' - ] - ] - ] - ]; - - return json_encode($static_releases, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -} -?> diff --git a/index.html b/index.html deleted file mode 100644 index bb1cb5d..0000000 --- a/index.html +++ /dev/null @@ -1,1197 +0,0 @@ - - - - - - 123pan - - - - - - -
-
- - -
- - -
- - -
- -
-
-
- 123pan图标 -
-

🚀 123pan

-

突破限制 · 高效下载 · 简单易用

- - - -

123pan是一款基于Python开发的高效下载辅助工具,通过模拟安卓客户端协议,帮助用户绕过123云盘的自用下载流量限制,实现无阻碍下载体验。

-
- -
- -

📖 项目介绍

- -
-

项目原地址:https://github.com/Qxyz17/123pan

- -

工具提供两种使用方式(安卓协议/网页协议),支持文件管理全流程操作,适用于需要下载云盘文件的用户。

- -

✨ 功能

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
功能支持协议
🔑 账号登录安卓/网页
📂 文件浏览安卓/网页
💾 高速下载安卓协议
📤 文件上传安卓协议
🔗 生成链接安卓/网页
🗑️ 文件管理安卓/网页
- -
-

重要提示

-

⚠️ 注意:网页协议已停止更新且受流量限制,强烈推荐使用安卓协议

-
- -

🚀 快速开始

- -
-
-
1
-

下载安装

-

前往下载页面获取最新版本的可执行文件或源代码

-
-
-
2
-

运行配置

-

首次运行将自动生成配置文件,按照提示输入账号信息

-
-
-
3
-

开始使用

-

登录账号后即可开始使用所有功能,享受高速下载体验

-
-
- -

⚙️ 配置说明

-

首次运行工具后,会在C:\Users\%USERNAME%\AppData\Roaming\Qxyz17\123pan生成 config.json 配置文件,格式如下:

- -
-
{
-  "userName": "账号",
-  "passWord": "密码",
-  "authorization": "令牌",
-  "deviceType": "驱动类型",
-  "osVersion": "安卓版本",
-  "settings": {
-    "defaultDownloadPath": "默认下载路径",
-    "askDownloadLocation": 开关
-  }
-}
-
- -

📝 使用教程

-

1. 登录流程

-

- 运行工具后输入账号密码

-

- 成功登录后会显示云盘根目录文件列表

- -

2. 文件下载

-

- 输入文件编号,按提示确认下载

-

- 下载文件临时以 .123pan 为后缀,完成后自动恢复原名称

- -

⚠️ 注意事项

- -
-

使用前请仔细阅读

-
    -
  • 📶 确保网络连接稳定,下载大文件时建议使用有线网络
  • -
  • 🔒 本工具仅用于学习研究,请勿用于商业用途
  • -
  • ⚖️ 使用者需遵守123云盘用户协议,滥用可能导致账号限制
  • -
  • 🔄 定期更新工具以获取最新协议支持
  • -
-
- -

🤝 贡献指南

-

欢迎提交PR改进代码,或通过Issues反馈问题。

-
-
- - -
-
-

下载 123pan

-

获取最新版本的123pan工具,支持Windows、macOS和Linux系统。

-
- -
-
-
-

正在获取发布信息...

-
- -
- - - -
-

📦 下载说明

-

123pan提供多种下载方式,您可以根据自己的需求选择合适的版本:

- -
-
-
-

Windows用户

-

下载123pan.exe可执行文件,双击即可运行

-
-
-
-

macOS用户

-

下载123pan.py文件,通过终端运行

-
-
-
-

Linux用户

-

下载123pan.py文件,通过终端运行

-
-
- -
-

注意事项

-
    -
  • 请从上方"发布"区域下载最新版本,以确保获得最佳体验
  • -
  • 如果您遇到任何问题,请检查是否已安装Python 3.8或更高版本
  • -
  • Windows用户如遇到安全警告,请选择"更多信息"->"仍要运行"
  • -
-
-
-
- - -
-
-

关于 123pan

-

123pan 是一个开源的Python工具,专门设计用于与123网盘进行交互。它提供了一系列命令行工具和API,帮助用户更高效地管理网盘中的文件。

- -

该项目旨在简化123网盘的操作流程,支持批量上传、下载、删除和文件信息查询等功能。无论是个人用户还是开发者,都可以通过这个工具提高工作效率。

- -

123pan 基于Python开发,兼容多个操作系统,并遵循Apache 2.0开源协议,允许用户自由使用、修改和分发。

- -
- -
- - - 访问 GitHub 项目页面 - - - - 报告问题 - -
-
-
-
- - - - - - - From bb087b2c8fb033aa72a823bb2ff5f0b4f6ae02e7 Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sun, 25 Jan 2026 19:35:48 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=8A=9F=E8=83=BD=E5=92=8Cvscode=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 12 ++++++++++ README.md | 1 + src/123pan.py | 56 +++++++++++++++++++++++---------------------- src/log.py | 42 ++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/log.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..68bfc39 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "调试", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/src/123pan.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 439ed50..87b80af 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ - [ ] 文件拖拽上传 - [ ] 拖拽上传功能 - [x] 界面美化 +- [ ] 去掉历史遗留的不需要的功能,优化日志 ## 🤝 贡献指南 diff --git a/src/123pan.py b/src/123pan.py index afe1f67..5f5ae4a 100644 --- a/src/123pan.py +++ b/src/123pan.py @@ -9,6 +9,9 @@ import re import uuid import platform +from log import get_logger + +logger = get_logger(__name__) # 配置文件路径 if platform.system() == 'Windows': @@ -50,7 +53,7 @@ def load_config(): config["settings"] = default_config["settings"] return config except Exception as e: - print(f"加载配置失败: {e}") + logger.error(f"加载配置失败: {e}") return default_config return default_config @@ -63,7 +66,7 @@ def save_config(config): json.dump(config, f, indent=2, ensure_ascii=False) return True except Exception as e: - print(f"保存配置失败: {e}") + logger.error(f"保存配置失败: {e}") return False @staticmethod @@ -267,7 +270,7 @@ def __init__( self.read_ini(user_name, pass_word, input_pwd, authorization) else: if user_name == "" or pass_word == "": - print("读取已禁用,用户名或密码为空") + logger.warning("读取已禁用,用户名或密码为空") if input_pwd: user_name = input("请输入用户名:") pass_word = input("请输入密码:") @@ -308,8 +311,8 @@ def login(self): res_sign = login_res.json() res_code_login = res_sign["code"] if res_code_login != 200: - print("code = 1 Error:" + str(res_code_login)) - print(res_sign.get("message", "")) + logger.error("code = 1 Error:" + str(res_code_login)) + logger.error(res_sign.get("message", "")) return res_code_login set_cookies = login_res.headers.get("Set-Cookie", "") set_cookies_list = {} @@ -341,9 +344,9 @@ def save_file(self): "osVersion": self.osversion, }) ConfigManager.save_config(config) - print("账号已保存") + logger.info("账号已保存") except Exception as e: - print("保存账号失败:", e) + logger.error("保存账号失败:", e) def get_dir(self, save=True): return self.get_dir_by_id(self.parent_file_id, save) @@ -380,13 +383,13 @@ def get_dir_by_id(self, file_id, save=True, all=False, limit=100): try: a = requests.get(base_url, headers=self.header_logined, params=params, timeout=30) except Exception: - print("连接失败") + logger.error("连接失败") return -1, [] text = a.json() res_code_getdir = text["code"] if res_code_getdir != 0: - print("code = 2 Error:" + str(res_code_getdir)) - print(text.get("message", "")) + logger.error("code = 2 Error:" + str(res_code_getdir)) + logger.error(text.get("message", "")) return res_code_getdir, [] lists_page = text["data"]["InfoList"] lists += lists_page @@ -395,13 +398,12 @@ def get_dir_by_id(self, file_id, save=True, all=False, limit=100): page += 1 times += 1 if times % 5 == 0: - print("警告:文件夹内文件过多:" + str(lenth_now) + "/" + str(total)) - print("为防止对服务器造成影响,暂停3秒") - print("请耐心等待!") + logger.warning("警告:文件夹内文件过多:" + str(lenth_now) + "/" + str(total)) + logger.info("为防止对服务器造成影响,暂停3秒") time.sleep(3) if lenth_now < total: - print("文件夹内文件过多:" + str(lenth_now) + "/" + str(total)) + logger.warning("文件夹内文件过多:" + str(lenth_now) + "/" + str(total)) self.all_file = False else: self.all_file = True @@ -413,7 +415,6 @@ def get_dir_by_id(self, file_id, save=True, all=False, limit=100): return res_code_getdir, lists def show(self): - print("--------------------") for i in self.list: file_size = i["Size"] if file_size > 1073741824: @@ -424,7 +425,7 @@ def show(self): download_size_print = str(round(file_size / 1024, 2)).ljust(6) + " KB" if i["Type"] == 0: - print( + logger.debug( "\033[33m" + "编号:", self.list.index(i) + 1, "\033[0m \t\t" + download_size_print + "\t\t\033[36m", @@ -432,7 +433,7 @@ def show(self): "\033[0m", ) elif i["Type"] == 1: - print( + logger.debug( "\033[35m" + "编号:", self.list.index(i) + 1, " \t\t\033[36m", @@ -440,9 +441,7 @@ def show(self): "\033[0m", ) if not self.all_file: - print("剩余" + str(self.total - len(self.list)) + "个文件未获取") - print("输入more继续获取") - print("--------------------") + logger.info("剩余" + str(self.total - len(self.list)) + "个文件未获取") # fileNumber 从0开始,0为第一个文件,传入时需要减一 def link_by_number(self, file_number, showlink=True): @@ -477,22 +476,22 @@ def link_by_fileDetail(self, file_detail, showlink=True): link_res_json = link_res.json() res_code_download = link_res_json["code"] if res_code_download != 0: - print("code = 3 Error:" + str(res_code_download)) - print(link_res_json.get("message", "")) + logger.error("code = 3 Error:" + str(res_code_download)) + logger.debug(link_res_json.get("message", "")) return res_code_download down_load_url = link_res.json()["data"]["DownloadUrl"] next_to_get = requests.get(down_load_url, timeout=10, allow_redirects=False).text url_pattern = re.compile(r"href='(https?://[^']+)'") redirect_url = url_pattern.findall(next_to_get)[0] if showlink: - print(redirect_url) + logger.debug(redirect_url) return redirect_url def download(self, file_number, download_path="download"): file_detail = self.list[file_number] if file_detail["Type"] == 1: - print("开始下载") + logger.info("开始下载") file_name = file_detail["FileName"] + ".zip" else: file_name = file_detail["FileName"] # 文件名 @@ -502,10 +501,11 @@ def download(self, file_number, download_path="download"): return self.download_from_url(down_load_url, file_name, download_path) + #考虑删除,但目前有依赖,不要动这里 def download_from_url(self, url, file_name, download_path="download"): if os.path.exists(download_path + "/" + file_name): if self.download_mode == 4: - print("文件 " + file_name + "已跳过") + logger.info("文件 " + file_name + "已跳过") return print("文件 " + file_name + " 已存在,是否要覆盖?") sure_download = input("输入1覆盖,2跳过,3全部覆盖,4全部跳过:") @@ -521,7 +521,7 @@ def download_from_url(self, url, file_name, download_path="download"): os.remove(download_path + "/" + file_name) if not os.path.exists(download_path): - print("文件夹不存在,创建文件夹") + logger.info("文件夹不存在,创建文件夹") os.makedirs(download_path) down = requests.get(url, stream=True, timeout=10) @@ -568,6 +568,7 @@ def download_from_url(self, url, file_name, download_path="download"): end="", ) elif data_count == content_size: + #Qxyz17说让我不动 print("\r [%s%s] %d%% %s" % (50 * "█", "", 100, ""), end="") print("\nok") @@ -590,7 +591,7 @@ def get_all_things(self, id): def download_dir(self, file_detail, download_path_root="download"): self.name_dict[file_detail["FileId"]] = file_detail["FileName"] if file_detail["Type"] != 1: - print("不是文件夹") + logger.warning("不是文件夹") return all_list = self.get_dir_by_id(file_detail["FileId"], save=False, all=True, limit=100)[1] @@ -620,6 +621,7 @@ def recycle(self): self.recycle_list = recycle_list # fileNumber 从0开始,0为第一个文件,传入时需要减一 + # 不要乱动,之后考虑移除 def delete_file(self, file, by_num=True, operation=True): # operation = 'true' 删除 , operation = 'false' 恢复 if by_num: diff --git a/src/log.py b/src/log.py new file mode 100644 index 0000000..95704a0 --- /dev/null +++ b/src/log.py @@ -0,0 +1,42 @@ +import logging +import platform +import os + +# 配置文件路径 +if platform.system() == 'Windows': + CONFIG_DIR = os.path.join(os.environ.get('APPDATA', ''), 'Qxyz17', '123pan') +else: + CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config', 'Qxyz17', '123pan') +LOG_FILE = os.path.join(CONFIG_DIR, '123pan.log') + + +def get_logger(name: str = "123pan"): + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + #logger.setLevel(logging.INFO) + + # 防止重复添加 handler + if not logger.handlers: + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8') + file_handler.setFormatter(formatter) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logger + +''' +调用方法:在其他文件中 +from log import get_logger +logger = get_logger(__name__) +然后需要时(例子↓) +logger.info("xxx") +发布版本时把debug级别的那一行注释掉改为info的那一行 +''' \ No newline at end of file From 27a6d807344127300115daec7161ee91de95cf92 Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sun, 1 Feb 2026 17:26:21 +0800 Subject: [PATCH 3/8] Refactor logging setup in log.py and remove deprecated sign_py.py and web.py files - Added a check to create the CONFIG_DIR if it does not exist in log.py. - Removed sign_py.py as it is no longer needed. - Removed web.py as it is deprecated and replaced by a new implementation. --- README.md | 21 +- src/123pan.py | 405 ++++++++---------------- src/log.py | 3 + src/sign_py.py | 121 -------- src/web.py | 824 ------------------------------------------------- 5 files changed, 138 insertions(+), 1236 deletions(-) delete mode 100644 src/sign_py.py delete mode 100644 src/web.py diff --git a/README.md b/README.md index 87b80af..aba97cf 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,16 @@ 123pan是一款基于Python开发的高效下载辅助工具,通过模拟安卓客户端协议,帮助用户绕过123云盘的自用下载流量限制,实现无阻碍下载体验。 -工具提供两种使用方式(安卓协议/网页协议),支持文件管理全流程操作,适用于需要下载云盘文件的用户。 - --- ## ✨ 功能 -| 功能 | 支持协议 | -|------|----------| -| 🔑 账号登录 | 安卓/网页 | -| 📂 文件浏览 | 安卓/网页 | -| 💾 高速下载 | 安卓协议 | -| 📤 文件上传 | 安卓协议 | -| 🔗 生成链接 | 安卓/网页 | -| 🗑️ 文件管理 | 安卓/网页 | - -> ⚠️ 注意:网页协议已停止更新且受流量限制,**强烈推荐使用安卓协议** +- 🔑 账号登录 +- 📂 文件浏览 +- 💾 高速下载 +- 📤 文件上传 +- 🔗 生成链接 +- 🗑️ 文件管理 --- @@ -91,12 +85,9 @@ --- ## 待开发功能 -- [x] 更换qt5为qt6 - [ ] 退出登录 - [ ] 文件拖拽上传 - [ ] 拖拽上传功能 -- [x] 界面美化 -- [ ] 去掉历史遗留的不需要的功能,优化日志 ## 🤝 贡献指南 diff --git a/src/123pan.py b/src/123pan.py index 5f5ae4a..63951a3 100644 --- a/src/123pan.py +++ b/src/123pan.py @@ -155,7 +155,7 @@ def __init__( user_name="", pass_word="", authorization="", - input_pwd=True, + input_pwd=False, ): self.all_device_type = [ @@ -255,7 +255,6 @@ def __init__( self.devicetype = random.choice(self.all_device_type) self.osversion = random.choice(self.all_os_versions) - self.download_mode = 1 self.cookies = None self.recycle_list = None self.list = [] @@ -270,12 +269,7 @@ def __init__( self.read_ini(user_name, pass_word, input_pwd, authorization) else: if user_name == "" or pass_word == "": - logger.warning("读取已禁用,用户名或密码为空") - if input_pwd: - user_name = input("请输入用户名:") - pass_word = input("请输入密码:") - else: - raise Exception("用户名或密码为空:读取禁用时,userName和passWord不能为空") + raise Exception("用户名或密码为空") self.user_name = user_name self.password = pass_word self.authorization = authorization @@ -301,6 +295,7 @@ def __init__( self.get_dir() def login(self): + """登录123云盘账户并获取授权令牌""" data = {"type": 1, "passport": self.user_name, "password": self.password} login_res = requests.post( "https://www.123pan.com/b/api/user/sign_in", @@ -333,7 +328,7 @@ def login(self): return res_code_login def save_file(self): - """保存配置到统一配置文件""" + """将账户信息保存到配置文件""" try: config = ConfigManager.load_config() config.update({ @@ -349,11 +344,18 @@ def save_file(self): logger.error("保存账号失败:", e) def get_dir(self, save=True): + """获取当前目录下的文件列表""" return self.get_dir_by_id(self.parent_file_id, save) - # 按页(非123页数)读取文件 - # all = True 强制获取所有文件 def get_dir_by_id(self, file_id, save=True, all=False, limit=100): + """按文件夹ID获取文件列表(支持分页) + + Args: + file_id: 文件夹ID + save: 是否保存结果到列表 + all: 是否强制获取所有文件 + limit: 每页限制数量 + """ get_pages = 3 res_code_getdir = 0 page = self.file_page * get_pages + 1 @@ -415,33 +417,11 @@ def get_dir_by_id(self, file_id, save=True, all=False, limit=100): return res_code_getdir, lists def show(self): - for i in self.list: - file_size = i["Size"] - if file_size > 1073741824: - download_size_print = str(round(file_size / 1073741824, 2)).ljust(6) + " GB" - elif file_size > 1048576: - download_size_print = str(round(file_size / 1048576, 2)).ljust(6) + " MB" - else: - download_size_print = str(round(file_size / 1024, 2)).ljust(6) + " KB" - - if i["Type"] == 0: - logger.debug( - "\033[33m" + "编号:", - self.list.index(i) + 1, - "\033[0m \t\t" + download_size_print + "\t\t\033[36m", - i["FileName"], - "\033[0m", - ) - elif i["Type"] == 1: - logger.debug( - "\033[35m" + "编号:", - self.list.index(i) + 1, - " \t\t\033[36m", - i["FileName"], - "\033[0m", - ) + """显示文件列表信息到日志""" if not self.all_file: - logger.info("剩余" + str(self.total - len(self.list)) + "个文件未获取") + logger.info(f"获取了{len(self.list)}/{self.total}个文件") + else: + logger.info(f"获取全部{len(self.list)}个文件") # fileNumber 从0开始,0为第一个文件,传入时需要减一 def link_by_number(self, file_number, showlink=True): @@ -476,15 +456,15 @@ def link_by_fileDetail(self, file_detail, showlink=True): link_res_json = link_res.json() res_code_download = link_res_json["code"] if res_code_download != 0: - logger.error("code = 3 Error:" + str(res_code_download)) - logger.debug(link_res_json.get("message", "")) + logger.error("获取下载链接失败,返回码: " + str(res_code_download)) + logger.error(link_res_json.get("message", "")) return res_code_download down_load_url = link_res.json()["data"]["DownloadUrl"] next_to_get = requests.get(down_load_url, timeout=10, allow_redirects=False).text url_pattern = re.compile(r"href='(https?://[^']+)'") redirect_url = url_pattern.findall(next_to_get)[0] if showlink: - logger.debug(redirect_url) + logger.info(f"获取下载链接成功: {redirect_url}") return redirect_url @@ -501,78 +481,29 @@ def download(self, file_number, download_path="download"): return self.download_from_url(down_load_url, file_name, download_path) - #考虑删除,但目前有依赖,不要动这里 def download_from_url(self, url, file_name, download_path="download"): - if os.path.exists(download_path + "/" + file_name): - if self.download_mode == 4: - logger.info("文件 " + file_name + "已跳过") - return - print("文件 " + file_name + " 已存在,是否要覆盖?") - sure_download = input("输入1覆盖,2跳过,3全部覆盖,4全部跳过:") - if sure_download == "2": - return - elif sure_download == "3": - self.download_mode = 3 - elif sure_download == "4": - self.download_mode = 4 - print("已跳过") - return - else: - os.remove(download_path + "/" + file_name) - + """从URL下载文件""" if not os.path.exists(download_path): - logger.info("文件夹不存在,创建文件夹") + logger.info("创建下载目录") os.makedirs(download_path) + + file_path = os.path.join(download_path, file_name) + temp_path = file_path + ".123pan" + + # 如果临时文件存在,删除它(防止之前的不完整下载) + if os.path.exists(temp_path): + os.remove(temp_path) + down = requests.get(url, stream=True, timeout=10) - - file_size = int(down.headers.get("Content-Length", 0) or 0) # 文件大小 - content_size = int(file_size) # 文件总大小 - data_count = 0 # 当前已传输的大小 - if file_size > 1048576: - size_print_download = str(round(file_size / 1048576, 2)) + "MB" - else: - size_print_download = str(round(file_size / 1024, 2)) + "KB" - print(file_name + " " + size_print_download) - time1 = time.time() - time_temp = time1 - data_count_temp = 0 + file_size = int(down.headers.get("Content-Length", 0) or 0) + # 以.123pan后缀下载,下载完成重命名,防止下载中断 - with open(download_path + "/" + file_name + ".123pan", "wb") as f: - for i in down.iter_content(1024): - f.write(i) - done_block = int((data_count / content_size) * 50) if content_size else 0 - data_count = data_count + len(i) - # 实时进度条进度 - now_jd = (data_count / content_size) * 100 if content_size else 0 - # 测速 - time1 = time.time() - pass_time = time1 - time_temp - if pass_time > 1: - time_temp = time1 - pass_data = int(data_count) - int(data_count_temp) - data_count_temp = data_count - speed = pass_data / int(pass_time) - speed_m = speed / 1048576 - if speed_m > 1: - speed_print = str(round(speed_m, 2)) + "MB/S" - else: - speed_print = str(round(speed_m * 1024, 2)) + "KB/S" - print( - "\r [%s%s] %d%% %s" - % ( - done_block * "█", - " " * (50 - 1 - done_block), - now_jd, - speed_print, - ), - end="", - ) - elif data_count == content_size: - #Qxyz17说让我不动 - print("\r [%s%s] %d%% %s" % (50 * "█", "", 100, ""), end="") - print("\nok") - - os.rename(download_path + "/" + file_name + ".123pan", download_path + "/" + file_name) + with open(temp_path, "wb") as f: + for chunk in down.iter_content(8192): + if chunk: + f.write(chunk) + + os.rename(temp_path, file_path) def get_all_things(self, id): self.dir_list.remove(id) @@ -621,25 +552,20 @@ def recycle(self): self.recycle_list = recycle_list # fileNumber 从0开始,0为第一个文件,传入时需要减一 - # 不要乱动,之后考虑移除 def delete_file(self, file, by_num=True, operation=True): - # operation = 'true' 删除 , operation = 'false' 恢复 + """删除或恢复文件""" if by_num: - print(file) if not str(file).isdigit(): - print("请输入数字") - return -1 + raise ValueError("文件索引必须是数字") if 0 <= file < len(self.list): file_detail = self.list[file] else: - print("不在合理范围内") - return + raise IndexError("文件索引超出范围") else: if file in self.list: file_detail = file else: - print("文件不存在") - return + raise ValueError("文件不存在") data_delete = { "driveId": 0, "fileTrashInfoList": file_detail, @@ -656,75 +582,40 @@ def delete_file(self, file, by_num=True, operation=True): message = dele_json.get("message", "") print(message) - def share(self): - file_id_list = "" - share_name_list = [] - add = "1" - while str(add) == "1": - share_num = input("分享文件的编号:") - num_test2 = share_num.isdigit() - if num_test2: - share_num = int(share_num) - if 0 < share_num < len(self.list) + 1: - share_id = self.list[int(share_num) - 1]["FileId"] - share_name = self.list[int(share_num) - 1]["FileName"] - share_name_list.append(share_name) - print(share_name_list) - file_id_list = file_id_list + str(share_id) + "," - add = input("输入1添加文件,0发起分享,其他取消") - else: - print("请输入数字,,") - add = "1" - if str(add) == "0": - share_pwd = input("提取码,不设留空:") - file_id_list = file_id_list.strip(",") - data = { - "driveId": 0, - "expiration": "2099-12-12T08:00:00+08:00", - "fileIdList": file_id_list, - "shareName": "123云盘分享", - "sharePwd": share_pwd, - "event": "shareCreate" - } - share_res = requests.post( - "https://www.123pan.com/a/api/share/create", - headers=self.header_logined, - data=json.dumps(data), - timeout=10 - ) - share_res_json = share_res.json() - if share_res_json.get("code", -1) != 0: - print(share_res_json.get("message", "")) - print("分享失败") - return - message = share_res_json.get("message", "") - print(message) - share_key = share_res_json["data"]["ShareKey"] - share_url = "https://www.123pan.com/s/" + share_key - print("分享链接:\n" + share_url + "提取码:" + share_pwd) - else: - print("退出分享") + def share(self, file_id_list, share_pwd=""): + """分享文件""" + if not file_id_list: + raise ValueError("文件ID列表为空") + data = { + "driveId": 0, + "expiration": "2099-12-12T08:00:00+08:00", + "fileIdList": file_id_list, + "shareName": "123云盘分享", + "sharePwd": share_pwd or "", + "event": "shareCreate" + } + share_res = requests.post( + "https://www.123pan.com/a/api/share/create", + headers=self.header_logined, + data=json.dumps(data), + timeout=10 + ) + share_res_json = share_res.json() + if share_res_json.get("code", -1) != 0: + raise RuntimeError(f"分享失败: {share_res_json.get('message', '')}") + share_key = share_res_json["data"]["ShareKey"] + share_url = "https://www.123pan.com/s/" + share_key + return share_url def up_load(self, file_path): - file_path = file_path.replace('"', "") - file_path = file_path.replace("\\", "/") - file_name = file_path.split("/")[-1] - print("文件名:", file_name) + file_path = file_path.replace('"', "").replace("\\", "/") + file_name = os.path.basename(file_path) if not os.path.exists(file_path): - print("文件不存在,请检查路径是否正确") - return + raise FileNotFoundError("文件不存在") if os.path.isdir(file_path): - print("暂不支持文件夹上传") - return + raise IsADirectoryError("不支持文件夹上传") fsize = os.path.getsize(file_path) - with open(file_path, "rb") as f: - md5 = hashlib.md5() - while True: - data = f.read(64 * 1024) - if not data: - break - md5.update(data) - readable_hash = md5.hexdigest() + readable_hash = self._compute_file_md5(file_path) list_up_request = { "driveId": 0, @@ -745,39 +636,18 @@ def up_load(self, file_path): up_res_json = up_res.json() res_code_up = up_res_json.get("code", -1) if res_code_up == 5060: - sure_upload = input("检测到1个同名文件,输入1覆盖,2保留两者,0取消:") - if sure_upload == "1": - list_up_request["duplicate"] = 1 - - elif sure_upload == "2": - list_up_request["duplicate"] = 2 - else: - print("取消上传") - return - up_res = requests.post( - "https://www.123pan.com/b/api/file/upload_request", - headers=self.header_logined, - data=json.dumps(list_up_request), - timeout=10 - ) - up_res_json = up_res.json() - res_code_up = up_res_json.get("code", -1) - if res_code_up == 0: - reuse = up_res_json["data"].get("Reuse", False) - if reuse: - print("上传成功,文件已MD5复用") - return - else: - print(up_res_json) - print("上传请求失败") - return + # 同名文件处理由调用者在GUI中处理 + raise RuntimeError("同名文件存在") + if res_code_up != 0: + raise RuntimeError(f"上传请求失败: {up_res_json}") + if up_res_json["data"].get("Reuse", False): + return up_file_id bucket = up_res_json["data"]["Bucket"] storage_node = up_res_json["data"]["StorageNode"] upload_key = up_res_json["data"]["Key"] upload_id = up_res_json["data"]["UploadId"] up_file_id = up_res_json["data"]["FileId"] # 上传文件的fileId,完成上传后需要用到 - print("上传文件的fileId:", up_file_id) # 获取已将上传的分块 start_data = { @@ -794,13 +664,8 @@ def up_load(self, file_path): ) start_res_json = start_res.json() res_code_up = start_res_json.get("code", -1) - if res_code_up == 0: - pass - else: - print(start_data) - print(start_res_json) - print("获取传输列表失败") - return + if res_code_up != 0: + raise RuntimeError(f"获取传输列表失败: {start_res_json}") # 分块,每一块取一次链接,依次上传 block_size = 5242880 @@ -809,9 +674,6 @@ def up_load(self, file_path): put_size = 0 while True: data = f.read(block_size) - - precent = round(put_size / fsize, 2) if fsize else 0 - print("\r已上传:" + str(precent * 100) + "%", end="") put_size = put_size + len(data) if not data: @@ -836,11 +698,8 @@ def up_load(self, file_path): ) get_link_res_json = get_link_res.json() res_code_up = get_link_res_json.get("code", -1) - if res_code_up == 0: - pass - else: - print("获取链接失败") - return + if res_code_up != 0: + raise RuntimeError(f"获取链接失败: {get_link_res_json}") upload_url = get_link_res_json["data"]["presignedUrls"][ str(part_number_start) ] @@ -848,7 +707,7 @@ def up_load(self, file_path): part_number_start = part_number_start + 1 - print("\n处理中") + uploaded_list_url = "https://www.123pan.com/b/api/file/s3_list_upload_parts" uploaded_comp_data = { "bucket": bucket, @@ -884,60 +743,48 @@ def up_load(self, file_path): ) close_res_json = close_up_session_res.json() res_code_up = close_res_json.get("code", -1) - if res_code_up == 0: - print("上传成功") - else: - print("上传失败") - print(close_res_json) - return + if res_code_up != 0: + raise RuntimeError(f"上传完成确认失败: {close_res_json}") + return up_file_id # dirId 就是 fileNumber,从0开始,0为第一个文件,传入时需要减一 !!!(好像文件夹都排在前面) def cd(self, dir_num): - if not dir_num.isdigit(): - if dir_num == "..": - if len(self.parent_file_list) > 1: - self.all_file = False - self.file_page = 0 - - self.parent_file_list.pop() - self.parent_file_id = self.parent_file_list[-1] - self.list = [] - self.parent_file_name_list.pop() - self.get_dir() - self.show() - else: - print("已经是根目录") - return - if dir_num == "/": + """进入文件夹""" + if dir_num == "..": + if len(self.parent_file_list) > 1: self.all_file = False self.file_page = 0 - - self.parent_file_id = 0 - self.parent_file_list = [0] + self.parent_file_list.pop() + self.parent_file_id = self.parent_file_list[-1] self.list = [] - self.parent_file_name_list = [] + self.parent_file_name_list.pop() self.get_dir() - self.show() - return - print("输入错误") + else: + raise RuntimeError("已经是根目录") + return + if dir_num == "/": + self.all_file = False + self.file_page = 0 + self.parent_file_id = 0 + self.parent_file_list = [0] + self.list = [] + self.parent_file_name_list = [] + self.get_dir() return + if not str(dir_num).isdigit(): + raise ValueError("文件夹编号必须是数字") dir_num = int(dir_num) - 1 if dir_num > (len(self.list) - 1) or dir_num < 0: - print("输入错误") - return + raise IndexError("文件夹编号超出范围") if self.list[dir_num]["Type"] != 1: - print("不是文件夹") - return - + raise TypeError("选中项不是文件夹") self.all_file = False self.file_page = 0 - self.parent_file_id = self.list[dir_num]["FileId"] self.parent_file_list.append(self.parent_file_id) self.parent_file_name_list.append(self.list[dir_num]["FileName"]) self.list = [] self.get_dir() - self.show() def cdById(self, file_id): self.all_file = False @@ -955,6 +802,7 @@ def read_ini( input_pwd, authorization="", ): + """从配置文件读取账号信息""" try: config = ConfigManager.load_config() deviceType = config.get("deviceType", "") @@ -966,27 +814,21 @@ def read_ini( user_name = config.get("userName", user_name) pass_word = config.get("passWord", pass_word) authorization = config.get("authorization", authorization) - - except Exception: - print("获取配置失败,重新输入") - + except Exception as e: + logger.error(f"获取配置失败: {e}") if user_name == "" or pass_word == "": - if input_pwd: - user_name = input("userName:") - pass_word = input("passWord:") - authorization = "" - else: - raise Exception("禁止输入模式下,没有账号或密码") + raise Exception("无法从配置获取账号信息") self.user_name = user_name self.password = pass_word self.authorization = authorization def mkdir(self, dirname, remakedir=False): + """创建文件夹""" if not remakedir: for i in self.list: if i["FileName"] == dirname: - print("文件夹已存在") + logger.info("文件夹已存在") return i["FileId"] url = "https://www.123pan.com/a/api/file/upload_request" @@ -1011,18 +853,29 @@ def mkdir(self, dirname, remakedir=False): try: res_json = res_mk.json() except json.decoder.JSONDecodeError: - print("创建失败") - print(res_mk.text) + logger.error("创建失败") + logger.error(res_mk.text) return code_mkdir = res_json.get("code", -1) if code_mkdir == 0: - print("创建成功: ", res_json["data"]["FileId"]) + logger.info(f"创建成功: {res_json['data']['FileId']}") self.get_dir() return res_json["data"]["Info"]["FileId"] - print(res_json) - print("创建失败") + logger.error(f"创建失败: {res_json}") return + + @staticmethod + def _compute_file_md5(file_path): + """计算文件MD5值""" + md5 = hashlib.md5() + with open(file_path, "rb") as f: + while True: + data = f.read(64 * 1024) + if not data: + break + md5.update(data) + return md5.hexdigest() # 线程辅助 class WorkerSignals(QtCore.QObject): @@ -1076,7 +929,7 @@ def __init__(self, parent=None): form = QtWidgets.QFormLayout() self.le_user = QtWidgets.QLineEdit() self.le_pass = QtWidgets.QLineEdit() - self.le_pass.setEchoMode(QtWidgets.QLineEdit.Password) + self.le_pass.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) form.addRow("用户名:", self.le_user) form.addRow("密码:", self.le_pass) layout.addLayout(form) diff --git a/src/log.py b/src/log.py index 95704a0..2bab00e 100644 --- a/src/log.py +++ b/src/log.py @@ -21,6 +21,9 @@ def get_logger(name: str = "123pan"): '%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) + if not os.path.exists(CONFIG_DIR): + os.makedirs(CONFIG_DIR, exist_ok=True) + file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8') file_handler.setFormatter(formatter) diff --git a/src/sign_py.py b/src/sign_py.py deleted file mode 100644 index f10ac79..0000000 --- a/src/sign_py.py +++ /dev/null @@ -1,121 +0,0 @@ -import time -import random -from datetime import datetime - - -def getSign(e): - def unsigned_right_shift(n, shift): - return (n % 0x100000000) >> shift - - def simulate_js_overflow(js_int, n): - # 转二进制 - if js_int < 0: - js_int = -js_int - js_int = str(bin(js_int))[2:] - js_int = js_int.zfill(32) - js_int = js_int.replace("0", "2") - js_int = js_int.replace("1", "0") - js_int = js_int.replace("2", "1") - js_int = int(js_int, 2) + 1 - bin_int = str(bin(js_int))[2:].zfill(32) - if n < 0: - # 转补码 - n = -n - n = str(bin(n))[2:] - n = n.zfill(32) - n = n.replace("0", "2") - n = n.replace("1", "0") - n = n.replace("2", "1") - n = int(n, 2) + 1 - bin_n = str(bin(n))[2:].zfill(32) - result = "" - for i in range(0, len(bin_int)): - temp = int(bin_n[i]) ^ int(bin_int[i]) - result = result + str(temp) - if result[0] == "1": - # 取补码 - result = result.replace("0", "2") - result = result.replace("1", "0") - result = result.replace("2", "1") - result = int(result, 2) + 1 - result = -result - else: - result = int(result, 2) - return result - - def A(t): - r = t.replace('\r\n', '\n') - a = -1 - - def generate_array(): - t = [] - for e in range(256): - n = e - for _ in range(8): - if n & 1: # 如果 n 的最低位是 1 - # print("入口:n:", n) - n = simulate_js_overflow(3988292384, unsigned_right_shift(n, 1)) - else: - n = unsigned_right_shift(n, 1) - t.append(n) - return t - - n = generate_array() - # print(n) - for i in range(len(r)): - # print("a:", unsigned_right_shift(a, 8)) - a = unsigned_right_shift(a, 8) ^ n[255 & (a ^ ord(r[i]))] - # print("zz", a) - return str((simulate_js_overflow(-1, a)) & 0xFFFFFFFF) - - def generate_timestamp(): - return round((time.time() + datetime.now().astimezone().utcoffset().total_seconds() + 28800) / 1) - - def adjust_timestamp(o, timestamp): - if timestamp: - i = timestamp - m = i - if 20 <= abs(1000 * o - 1000 * int(m)) / 1000 / 60: - return i - return o - - def formatDate(t, e=None, n=8): - t = int(t) # Use the original timestamp - t = t - 480 * 60 - r = datetime.fromtimestamp(t + 3600 * n) # Convert to seconds and add 'n' hours - data = { - 'y': str(r.year), - 'm': f"0{r.month}" if r.month < 10 else str(r.month), - 'd': f"0{r.day}" if r.day < 10 else str(r.day), - 'h': f"0{r.hour}" if r.hour < 10 else str(r.hour), - 'f': f"0{r.minute}" if r.minute < 10 else str(r.minute) - } - return data - - def generate_signature(a, o, e, n, r): - s = ["a", "d", "e", "f", "g", "h", "l", "m", "y", "i", "j", "n", "o", "p", "k", "q", "r", "s", "t", "u", "b", - "c", "v", "w", "s", "z"] - u = formatDate(o) - h = u['y'] - g = u['m'] - l = u['d'] - c = u['h'] - u = u['f'] - d = ''.join([h, g, l, c, u]) - f = [s[int(p)] for p in d] - h = A(''.join(f)) - g = A(f"{o}|{a}|{e}|{n}|{r}|{h}") - return [h, f"{o}-{a}-{g}"] - - a = str(random.randint(0, 9999999)) - o = generate_timestamp() - o = adjust_timestamp(o, timestamp=round(time.time())) - - n = "web" - r = '3' - return generate_signature(a, o, e, n, r) - - -if __name__ == '__main__': - e = '/b/api/file/list/new' - print(getSign(e)) diff --git a/src/web.py b/src/web.py deleted file mode 100644 index 5fd7cc1..0000000 --- a/src/web.py +++ /dev/null @@ -1,824 +0,0 @@ -import re -import time -from sign_py import getSign -import requests -import hashlib -import os -import json -import base64 - - -class Pan123: - def __init__( - self, - readfile=True, - user_name="", - pass_word="", - authorization="", - input_pwd=True, - ): - self.recycle_list = None - self.list = None - if readfile: - self.read_ini(user_name, pass_word, input_pwd, authorization) - else: - if user_name == "" or pass_word == "": - print("读取已禁用,用户名或密码为空") - if input_pwd: - user_name = input("请输入用户名:") - pass_word = input("请输入密码:") - else: - raise Exception("用户名或密码为空:读取禁用时,userName和passWord不能为空") - self.user_name = user_name - self.password = pass_word - self.authorization = authorization - self.header_only_usage = { - "user-agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/" - "537.36 (KHTML, like Gecko) Chrome/109.0.0.0 " - "Safari/537.36 Edg/109.0.1474.0", - "app-version": "2", - "platform": "web", - } - self.header_logined = { - "Accept": "*/*", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", - "App-Version": "3", - "Authorization": self.authorization, - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "LoginUuid": "z-uk_yT8HwR4raGX1gqGk", - "Pragma": "no-cache", - "Referer": "https://www.123pan.com/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/" - "537.36 (KHTML, like Gecko) Chrome/119.0.0.0 " - "Safari/537.36 Edg/119.0.0.0", - "platform": "web", - "sec-ch-ua": "^\\^Microsoft", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "^\\^Windows^^", - } - self.parent_file_id = 0 # 路径,文件夹的id,0为根目录 - self.parent_file_list = [0] - res_code_getdir = self.get_dir() - if res_code_getdir != 0: - self.login() - self.get_dir() - - def login(self): - data = {"remember": True, "passport": self.user_name, "password": self.password} - sign = getSign("/b/api/user/sign_in") - login_res = requests.post( - "https://www.123pan.com/b/api/user/sign_in", - headers=self.header_only_usage, - data=data, - params={sign[0]: sign[1]}, timeout=10 - ) - res_sign = login_res.json() - res_code_login = res_sign["code"] - if res_code_login != 200: - print("code = 1 Error:" + str(res_code_login)) - print(res_sign["message"]) - return res_code_login - token = res_sign["data"]["token"] - self.authorization = "Bearer " + token - header_logined = { - "Accept": "*/*", - "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", - "App-Version": "3", - "Authorization": self.authorization, - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "LoginUuid": "z-uk_yT8HwR4raGX1gqGk", - "Pragma": "no-cache", - "Referer": "https://www.123pan.com/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/" - "537.36 (KHTML, like" - " Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0", - "platform": "web", - "sec-ch-ua": "^\\^Microsoft", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "^\\^Windows^^", - } - self.header_logined = header_logined - # ret['cookie'] = cookie - self.save_file() - return res_code_login - - def save_file(self): - with open("123pan.txt", "w",encoding="utf_8") as f: - save_list = { - "userName": self.user_name, - "passWord": self.password, - "authorization": self.authorization, - } - - f.write(json.dumps(save_list)) - print("Save!") - - def get_dir(self): - res_code_getdir = 0 - page = 1 - lists = [] - lenth_now = 0 - total = -1 - while lenth_now < total or total == -1: - base_url = "https://www.123pan.com/b/api/file/list/new" - - # print(self.headerLogined) - sign = getSign("/b/api/file/list/new") - print(sign) - params = { - sign[0]: sign[1], - "driveId": 0, - "limit": 100, - "next": 0, - "orderBy": "file_id", - "orderDirection": "desc", - "parentFileId": str(self.parent_file_id), - "trashed": False, - "SearchData": "", - "Page": str(page), - "OnlyLookAbnormalFile": 0, - } - - a = requests.get(base_url, headers=self.header_logined, params=params, timeout=10) - # print(a.text) - # print(a.headers) - text = a.json() - res_code_getdir = text["code"] - if res_code_getdir != 0: - print(a.text) - print(a.headers) - print("code = 2 Error:" + str(res_code_getdir)) - return res_code_getdir - lists_page = text["data"]["InfoList"] - lists += lists_page - total = text["data"]["Total"] - lenth_now += len(lists_page) - page += 1 - file_num = 0 - for i in lists: - i["FileNum"] = file_num - file_num += 1 - - self.list = lists - return res_code_getdir - - def show(self): - print("--------------------") - for i in self.list: - file_size = i["Size"] - if file_size > 1048576: - download_size_print = str(round(file_size / 1048576, 2)) + "M" - else: - download_size_print = str(round(file_size / 1024, 2)) + "K" - - if i["Type"] == 0: - print( - "\033[33m" + "编号:", - self.list.index(i) + 1, - "\033[0m \t\t" + download_size_print + "\t\t\033[36m", - i["FileName"], - "\033[0m", - ) - elif i["Type"] == 1: - print( - "\033[35m" + "编号:", - self.list.index(i) + 1, - " \t\t\033[36m", - i["FileName"], - "\033[0m", - ) - - print("--------------------") - - # fileNumber 从0开始,0为第一个文件,传入时需要减一 !!! - def link(self, file_number, showlink=True): - file_detail = self.list[file_number] - type_detail = file_detail["Type"] - if type_detail == 1: - down_request_url = "https://www.123pan.com/a/api/file/batch_download_info" - down_request_data = {"fileIdList": [{"fileId": int(file_detail["FileId"])}]} - - else: - down_request_url = "https://www.123pan.com/a/api/file/download_info" - down_request_data = { - "driveId": 0, - "etag": file_detail["Etag"], - "fileId": file_detail["FileId"], - "s3keyFlag": file_detail["S3KeyFlag"], - "type": file_detail["Type"], - "fileName": file_detail["FileName"], - "size": file_detail["Size"], - } - # print(down_request_data) - - sign = getSign("/a/api/file/download_info") - - link_res = requests.post( - down_request_url, - headers=self.header_logined, - params={sign[0]: sign[1]}, - data=down_request_data, - timeout=10 - ) - # print(linkRes.text) - res_code_download = link_res.json()["code"] - if res_code_download != 0: - print("code = 3 Error:" + str(res_code_download)) - # print(linkRes.json()) - return res_code_download - download_link_base64 = link_res.json()["data"]["DownloadUrl"] - base64_url = re.findall("params=(.*)&", download_link_base64)[0] - # print(Base64Url) - down_load_url = base64.b64decode(base64_url) - down_load_url = down_load_url.decode("utf-8") - - next_to_get = requests.get(down_load_url,timeout=10).json() - redirect_url = next_to_get["data"]["redirect_url"] - if showlink: - print(redirect_url) - - return redirect_url - - def download(self, file_number): - file_detail = self.list[file_number] - down_load_url = self.link(file_number, showlink=False) - file_name = file_detail["FileName"] # 文件名 - if os.path.exists(file_name): - print("文件 " + file_name + " 已存在,是否要覆盖?") - sure_download = input("输入1覆盖,2取消:") - if sure_download != "1": - return - down = requests.get(down_load_url, stream=True, timeout=10) - - file_size = int(down.headers["Content-Length"]) # 文件大小 - content_size = int(file_size) # 文件总大小 - data_count = 0 # 当前已传输的大小 - if file_size > 1048576: - size_print_download = str(round(file_size / 1048576, 2)) + "M" - else: - size_print_download = str(round(file_size / 1024, 2)) + "K" - print(file_name + " " + size_print_download) - time1 = time.time() - time_temp = time1 - data_count_temp = 0 - with open(file_name, "wb") as f: - for i in down.iter_content(1024): - f.write(i) - done_block = int((data_count / content_size) * 50) - data_count = data_count + len(i) - # 实时进度条进度 - now_jd = (data_count / content_size) * 100 - # %% 表示% - # 测速 - time1 = time.time() - pass_time = time1 - time_temp - if pass_time > 1: - time_temp = time1 - pass_data = int(data_count) - int(data_count_temp) - data_count_temp = data_count - speed = pass_data / int(pass_time) - speed_m = speed / 1048576 - if speed_m > 1: - speed_print = str(round(speed_m, 2)) + "M/S" - else: - speed_print = str(round(speed_m * 1024, 2)) + "K/S" - print( - "\r [%s%s] %d%% %s" - % ( - done_block * "█", - " " * (50 - 1 - done_block), - now_jd, - speed_print, - ), - end="", - ) - elif data_count == content_size: - print("\r [%s%s] %d%% %s" % (50 * "█", "", 100, ""), end="") - print("\nok") - - def recycle(self): - recycle_id = 0 - url = ( - "https://www.123pan.com/a/api/file/list/new?driveId=0&limit=100&next=0" - "&orderBy=fileId&orderDirection=desc&parentFileId=" - + str(recycle_id) - + "&trashed=true&&Page=1" - ) - recycle_res = requests.get(url, headers=self.header_logined, timeout=10) - json_recycle = recycle_res.json() - recycle_list = json_recycle["data"]["InfoList"] - self.recycle_list = recycle_list - - # fileNumber 从0开始,0为第一个文件,传入时需要减一 !!! - def delete_file(self, file, by_num=True, operation=True): - # operation = 'true' 删除 , operation = 'false' 恢复 - if by_num: - print(file) - if not str(file).isdigit(): - print("请输入数字") - return -1 - if 0 <= file < len(self.list): - file_detail = self.list[file] - else: - print("不在合理范围内") - return - else: - if file in self.list: - file_detail = file - else: - print("文件不存在") - return - data_delete = { - "driveId": 0, - "fileTrashInfoList": file_detail, - "operation": operation, - } - delete_res = requests.post( - "https://www.123pan.com/a/api/file/trash", - data=json.dumps(data_delete), - headers=self.header_logined, - timeout=10 - ) - dele_json = delete_res.json() - print(dele_json) - message = dele_json["message"] - print(message) - - def share(self): - file_id_list = "" - share_name_list = [] - add = "1" - while str(add) == "1": - share_num = input("分享文件的编号:") - num_test2 = share_num.isdigit() - if num_test2: - share_num = int(share_num) - if 0 < share_num < len(self.list) + 1: - share_id = self.list[int(share_num) - 1]["FileId"] - share_name = self.list[int(share_num) - 1]["FileName"] - share_name_list.append(share_name) - print(share_name_list) - file_id_list = file_id_list + str(share_id) + "," - add = input("输入1添加文件,0发起分享,其他取消") - else: - print("请输入数字,,") - add = "1" - if str(add) == "0": - share_pwd = input("提取码,不设留空:") - file_id_list = file_id_list.strip(",") - data = { - "driveId": 0, - "expiration": "2024-02-09T11:42:45+08:00", - "fileIdList": file_id_list, - "shareName": "我的分享", - "sharePwd": share_pwd, - } - share_res = requests.post( - "https://www.123pan.com/a/api/share/create", - headers=self.header_logined, - data=json.dumps(data), - timeout=10 - ) - share_res_json = share_res.json() - message = share_res_json["message"] - print(message) - share_key = share_res_json["data"]["ShareKey"] - share_url = "https://www.123pan.com/s/" + share_key - print("分享链接:\n" + share_url + "提取码:" + share_pwd) - else: - print("退出分享") - - def up_load(self, file_path): - file_path = file_path.replace('"', "") - file_path = file_path.replace("\\", "/") - file_name = file_path.split("/")[-1] - print("文件名:", file_name) - if not os.path.exists(file_path): - print("文件不存在,请检查路径是否正确") - return - if os.path.isdir(file_path): - print("暂不支持文件夹上传") - return - fsize = os.path.getsize(file_path) - with open(file_path, "rb") as f: - md5 = hashlib.md5() - while True: - data = f.read(64 * 1024) - if not data: - break - md5.update(data) - readable_hash = md5.hexdigest() - - list_up_request = { - "driveId": 0, - "etag": readable_hash, - "fileName": file_name, - "parentFileId": self.parent_file_id, - "size": fsize, - "type": 0, - "duplicate": 0, - } - - sign = getSign("/b/api/file/upload_request") - up_res = requests.post( - "https://www.123pan.com/b/api/file/upload_request", - headers=self.header_logined, - params={sign[0]: sign[1]}, - data=list_up_request, - timeout=10 - ) - up_res_json = up_res.json() - res_code_up = up_res_json["code"] - if res_code_up == 5060: - sure_upload = input("检测到1个同名文件,输入1覆盖,2保留两者,0取消:") - if sure_upload == "1": - list_up_request["duplicate"] = 1 - - elif sure_upload == "2": - list_up_request["duplicate"] = 2 - else: - print("取消上传") - return - sign = getSign("/b/api/file/upload_request") - up_res = requests.post( - "https://www.123pan.com/b/api/file/upload_request", - headers=self.header_logined, - params={sign[0]: sign[1]}, - data=json.dumps(list_up_request), - timeout=10 - ) - up_res_json = up_res.json() - res_code_up = up_res_json["code"] - if res_code_up == 0: - # print(upResJson) - # print("上传请求成功") - reuse = up_res_json["data"]["Reuse"] - if reuse: - print("上传成功,文件已MD5复用") - return - else: - print(up_res_json) - print("上传请求失败") - return - - bucket = up_res_json["data"]["Bucket"] - storage_node = up_res_json["data"]["StorageNode"] - upload_key = up_res_json["data"]["Key"] - upload_id = up_res_json["data"]["UploadId"] - up_file_id = up_res_json["data"]["FileId"] # 上传文件的fileId,完成上传后需要用到 - print("上传文件的fileId:", up_file_id) - - # 获取已将上传的分块 - start_data = { - "bucket": bucket, - "key": upload_key, - "uploadId": upload_id, - "storageNode": storage_node, - } - start_res = requests.post( - "https://www.123pan.com/b/api/file/s3_list_upload_parts", - headers=self.header_logined, - data=json.dumps(start_data), - timeout=10 - ) - start_res_json = start_res.json() - res_code_up = start_res_json["code"] - if res_code_up == 0: - # print(startResJson) - pass - else: - print(start_data) - print(start_res_json) - - print("获取传输列表失败") - return - - # 分块,每一块取一次链接,依次上传 - block_size = 5242880 - with open(file_path, "rb") as f: - part_number_start = 1 - put_size = 0 - while True: - data = f.read(block_size) - - precent = round(put_size / fsize, 2) - print("\r已上传:" + str(precent * 100) + "%", end="") - put_size = put_size + len(data) - - if not data: - break - get_link_data = { - "bucket": bucket, - "key": upload_key, - "partNumberEnd": part_number_start + 1, - "partNumberStart": part_number_start, - "uploadId": upload_id, - "StorageNode": storage_node, - } - - get_link_url = ( - "https://www.123pan.com/b/api/file/s3_repare_upload_parts_batch" - ) - get_link_res = requests.post( - get_link_url, - headers=self.header_logined, - data=json.dumps(get_link_data), - timeout=10 - ) - get_link_res_json = get_link_res.json() - res_code_up = get_link_res_json["code"] - if res_code_up == 0: - # print("获取链接成功") - pass - else: - print("获取链接失败") - # print(getLinkResJson) - return - # print(getLinkResJson) - upload_url = get_link_res_json["data"]["presignedUrls"][ - str(part_number_start) - ] - # print("上传链接",uploadUrl) - requests.put(upload_url, data=data, timeout=10) - # print("put") - - part_number_start = part_number_start + 1 - - print("\n处理中") - # 完成标志 - # 1.获取已上传的块 - uploaded_list_url = "https://www.123pan.com/b/api/file/s3_list_upload_parts" - uploaded_comp_data = { - "bucket": bucket, - "key": upload_key, - "uploadId": upload_id, - "storageNode": storage_node, - } - # print(uploadedCompData) - requests.post( - uploaded_list_url, - headers=self.header_logined, - data=json.dumps(uploaded_comp_data), - timeout=10 - ) - compmultipart_up_url = ( - "https://www.123pan.com/b/api/file/s3_complete_multipart_upload" - ) - requests.post( - compmultipart_up_url, - headers=self.header_logined, - data=json.dumps(uploaded_comp_data), - timeout=10 - ) - - # 3.报告完成上传,关闭upload session - if fsize > 64 * 1024 * 1024: - time.sleep(3) - close_up_session_url = "https://www.123pan.com/b/api/file/upload_complete" - close_up_session_data = {"fileId": up_file_id} - # print(closeUpSessionData) - close_up_session_res = requests.post( - close_up_session_url, - headers=self.header_logined, - data=json.dumps(close_up_session_data), - timeout=10 - ) - close_res_json = close_up_session_res.json() - # print(closeResJson) - res_code_up = close_res_json["code"] - if res_code_up == 0: - print("上传成功") - else: - print("上传失败") - print(close_res_json) - return - - # dirId 就是 fileNumber,从0开始,0为第一个文件,传入时需要减一 !!!(好像文件夹都排在前面) - def cd(self, dir_num): - if not dir_num.isdigit(): - if dir_num == "..": - if len(self.parent_file_list) > 1: - self.parent_file_list.pop() - self.parent_file_id = self.parent_file_list[-1] - self.get_dir() - self.show() - else: - print("已经是根目录") - return - if dir_num == "/": - self.parent_file_id = 0 - self.parent_file_list = [0] - self.get_dir() - self.show() - return - print("输入错误") - return - dir_num = int(dir_num) - 1 - if dir_num >= (len(self.list) - 1) or dir_num < 0: - print("输入错误") - return - if self.list[dir_num]["Type"] != 1: - print("不是文件夹") - return - self.parent_file_id = self.list[dir_num]["FileId"] - self.parent_file_list.append(self.parent_file_id) - self.get_dir() - self.show() - - def cdById(self, file_id): - self.parent_file_id = file_id - self.parent_file_list.append(self.parent_file_id) - self.get_dir() - self.get_dir() - self.show() - - def read_ini( - self, - user_name, - pass_word, - input_pwd, - authorization="", - ): - try: - with open("123pan.txt", "r",encoding="utf-8") as f: - text = f.read() - text = json.loads(text) - user_name = text["userName"] - pass_word = text["passWord"] - authorization = text["authorization"] - - except: - print("获取配置失败,重新登录") - - if user_name == "" or pass_word == "": - if input_pwd: - user_name = input("userName:") - pass_word = input("passWord:") - authorization = "" - else: - raise Exception("禁止输入模式下,没有账号或密码") - - self.user_name = user_name - self.password = pass_word - self.authorization = authorization - - def mkdir(self, dirname, remakedir=False): - if not remakedir: - for i in self.list: - if i["FileName"] == dirname: - print("文件夹已存在") - return i["FileId"] - - url = "https://www.123pan.com/a/api/file/upload_request" - data_mk = { - "driveId": 0, - "etag": "", - "fileName": dirname, - "parentFileId": self.parent_file_id, - "size": 0, - "type": 1, - "duplicate": 1, - "NotReuse": True, - "event": "newCreateFolder", - "operateType": 1, - } - sign = getSign("/a/api/file/upload_request") - res_mk = requests.post( - url, - headers=self.header_logined, - data=json.dumps(data_mk), - params={sign[0]: sign[1]}, - timeout=10 - ) - try: - res_json = res_mk.json() - print(res_json) - except json.decoder.JSONDecodeError: - print("创建失败") - print(res_mk.text) - return - code_mkdir = res_json["code"] - - if code_mkdir == 0: - print("创建成功: ", res_json["data"]["FileId"]) - self.get_dir() - return res_json["data"]["Info"]["FileId"] - print("创建失败") - print(res_json) - return - - -if __name__ == "__main__": - print("web协议将废弃,请使用android协议") - pan = Pan123(readfile=True, input_pwd=True) - pan.show() - while True: - command = input("\033[91m >\033[0m") - if command == "ls": - pan.show() - if command == "re": - code = pan.get_dir() - if code == 0: - print("刷新目录成功") - pan.show() - if command.isdigit(): - if int(command) > len(pan.list) or int(command) < 1: - print("输入错误") - continue - if pan.list[int(command) - 1]["Type"] == 1: - pan.cdById(pan.list[int(command) - 1]["FileId"]) - else: - size = pan.list[int(command) - 1]["Size"] - if size > 1048576: - size_print_show = str(round(size / 1048576, 2)) + "M" - else: - size_print_show = str(round(size / 1024, 2)) + "K" - # print(pan.list[int(command) - 1]) - name = pan.list[int(command) - 1]["FileName"] - print(name + " " + size_print_show) - print("press 1 to download now: ", end="") - sure = input() - if sure == "1": - pan.download(int(command) - 1) - elif command[0:9] == "download ": - if command[9:].isdigit(): - if int(command[9:]) > len(pan.list) or int(command[9:]) < 1: - print("输入错误") - continue - pan.download(int(command[9:]) - 1) - else: - print("输入错误") - elif command == "exit": - break - elif command == "log": - pan.login() - pan.get_dir() - pan.show() - - elif command[0:5] == "link ": - if command[5:].isdigit(): - if int(command[5:]) > len(pan.list) or int(command[5:]) < 1: - print("输入错误") - continue - pan.link(int(command[5:]) - 1) - else: - print("输入错误") - elif command == "upload": - filepath = input("请输入文件路径:") - pan.up_load(filepath) - pan.get_dir() - pan.show() - elif command == "share": - pan.share() - elif command[0:6] == "delete": - if command == "delete": - print("请输入要删除的文件编号:", end="") - fileNumber = input() - else: - if command[6] == " ": - fileNumber = command[7:] - else: - print("输入错误") - continue - if fileNumber == "": - print("请输入要删除的文件编号:", end="") - fileNumber = input() - else: - fileNumber = fileNumber[0:] - if fileNumber.isdigit(): - if int(fileNumber) > len(pan.list) or int(fileNumber) < 1: - print("输入错误") - continue - pan.delete_file(int(fileNumber) - 1) - pan.get_dir() - pan.show() - else: - print("输入错误") - - elif command[:3] == "cd ": - path = command[3:] - pan.cd(path) - elif command[0:5] == "mkdir": - if command == "mkdir": - newPath = input("请输入目录名:") - else: - newPath = command[6:] - if newPath == "": - newPath = input("请输入目录名:") - else: - newPath = newPath[0:] - print(pan.mkdir(newPath)) - - elif command == "reload": - pan.read_ini("", "", True) - print("读取成功") - pan.get_dir() - pan.show() From 8ee09d28e520a1697b84cdc0960854d308082c22 Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sun, 1 Feb 2026 17:33:20 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BE=A7=E6=A0=8F?= =?UTF-8?q?=E6=98=BE=E7=A4=BAbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/123pan.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/123pan.py b/src/123pan.py index 63951a3..d51c9ad 100644 --- a/src/123pan.py +++ b/src/123pan.py @@ -20,6 +20,22 @@ CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config', 'Qxyz17', '123pan') CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.json') +# 自定义侧栏按钮类 +class SidebarButton(QtWidgets.QPushButton): + """侧栏按钮,支持hover事件""" + entered = QtCore.pyqtSignal() + left = QtCore.pyqtSignal() + + def enterEvent(self, event): + """鼠标进入事件""" + super().enterEvent(event) + self.entered.emit() + + def leaveEvent(self, event): + """鼠标离开事件""" + super().leaveEvent(event) + self.left.emit() + # 配置管理类 class ConfigManager: @staticmethod @@ -1042,7 +1058,7 @@ def __init__(self): self.sidebar_original_geoms = {} # 文件页按钮 - self.btn_files = QtWidgets.QPushButton("📁 文件") + self.btn_files = SidebarButton("📁 文件") self.btn_files.setMinimumHeight(50) self.btn_files.setStyleSheet( "font-size: 16px; text-align: left; padding-left: 20px;" @@ -1054,7 +1070,7 @@ def __init__(self): self.sidebar_buttons.append(self.btn_files) # 传输页按钮 - self.btn_transfer = QtWidgets.QPushButton("🔄 传输") + self.btn_transfer = SidebarButton("🔄 传输") self.btn_transfer.setMinimumHeight(50) self.btn_transfer.setStyleSheet( "font-size: 16px; text-align: left; padding-left: 20px;" @@ -1067,8 +1083,8 @@ def __init__(self): # 为侧边栏按钮添加悬停和点击事件,实现动画效果 for btn in self.sidebar_buttons: - btn.enterEvent = lambda event, b=btn: self.on_sidebar_button_hover(b) - btn.leaveEvent = lambda event, b=btn: self.on_sidebar_button_leave(b) + btn.entered.connect(lambda b=btn: self.on_sidebar_button_hover(b)) + btn.left.connect(lambda b=btn: self.on_sidebar_button_leave(b)) btn.pressed.connect(lambda b=btn: self.on_sidebar_button_pressed(b)) btn.released.connect(lambda b=btn: self.on_sidebar_button_released(b)) From 473723ec3d8691fd2e057d00934fa14b3c692eee Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sun, 1 Feb 2026 17:52:41 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- icon.ico | Bin 168173 -> 0 bytes src/123pan.py | 2583 +--------------------------------------- src/api.py | 752 ++++++++++++ src/config.py | 73 ++ src/log.py | 14 +- src/main_window.py | 1586 ++++++++++++++++++++++++ src/threading_utils.py | 45 + src/ui_widgets.py | 178 +++ 9 files changed, 2648 insertions(+), 2585 deletions(-) delete mode 100644 icon.ico create mode 100644 src/api.py create mode 100644 src/config.py create mode 100644 src/main_window.py create mode 100644 src/threading_utils.py create mode 100644 src/ui_widgets.py diff --git a/README.md b/README.md index aba97cf..ea4053b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- 123pan + 123pan # 🚀 123pan diff --git a/icon.ico b/icon.ico deleted file mode 100644 index 05b66f5aecb5c83aab8eb13aa94ad91925a05e32..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168173 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 x^yhEe>+anA 64 * 1024 * 1024: - time.sleep(3) - close_up_session_url = "https://www.123pan.com/b/api/file/upload_complete" - close_up_session_data = {"fileId": up_file_id} - close_up_session_res = requests.post( - close_up_session_url, - headers=self.header_logined, - data=json.dumps(close_up_session_data), - timeout=10 - ) - close_res_json = close_up_session_res.json() - res_code_up = close_res_json.get("code", -1) - if res_code_up != 0: - raise RuntimeError(f"上传完成确认失败: {close_res_json}") - return up_file_id - - # dirId 就是 fileNumber,从0开始,0为第一个文件,传入时需要减一 !!!(好像文件夹都排在前面) - def cd(self, dir_num): - """进入文件夹""" - if dir_num == "..": - if len(self.parent_file_list) > 1: - self.all_file = False - self.file_page = 0 - self.parent_file_list.pop() - self.parent_file_id = self.parent_file_list[-1] - self.list = [] - self.parent_file_name_list.pop() - self.get_dir() - else: - raise RuntimeError("已经是根目录") - return - if dir_num == "/": - self.all_file = False - self.file_page = 0 - self.parent_file_id = 0 - self.parent_file_list = [0] - self.list = [] - self.parent_file_name_list = [] - self.get_dir() - return - if not str(dir_num).isdigit(): - raise ValueError("文件夹编号必须是数字") - dir_num = int(dir_num) - 1 - if dir_num > (len(self.list) - 1) or dir_num < 0: - raise IndexError("文件夹编号超出范围") - if self.list[dir_num]["Type"] != 1: - raise TypeError("选中项不是文件夹") - self.all_file = False - self.file_page = 0 - self.parent_file_id = self.list[dir_num]["FileId"] - self.parent_file_list.append(self.parent_file_id) - self.parent_file_name_list.append(self.list[dir_num]["FileName"]) - self.list = [] - self.get_dir() - - def cdById(self, file_id): - self.all_file = False - self.file_page = 0 - self.list = [] - self.parent_file_id = file_id - self.parent_file_list.append(self.parent_file_id) - self.get_dir() - self.show() - - def read_ini( - self, - user_name, - pass_word, - input_pwd, - authorization="", - ): - """从配置文件读取账号信息""" - try: - config = ConfigManager.load_config() - deviceType = config.get("deviceType", "") - osVersion = config.get("osVersion", "") - if deviceType: - self.devicetype = deviceType - if osVersion: - self.osversion = osVersion - user_name = config.get("userName", user_name) - pass_word = config.get("passWord", pass_word) - authorization = config.get("authorization", authorization) - except Exception as e: - logger.error(f"获取配置失败: {e}") - if user_name == "" or pass_word == "": - raise Exception("无法从配置获取账号信息") - - self.user_name = user_name - self.password = pass_word - self.authorization = authorization +# https://github.com/123panNextGen/123pan +# src/123pan.py - def mkdir(self, dirname, remakedir=False): - """创建文件夹""" - if not remakedir: - for i in self.list: - if i["FileName"] == dirname: - logger.info("文件夹已存在") - return i["FileId"] - - url = "https://www.123pan.com/a/api/file/upload_request" - data_mk = { - "driveId": 0, - "etag": "", - "fileName": dirname, - "parentFileId": self.parent_file_id, - "size": 0, - "type": 1, - "duplicate": 1, - "NotReuse": True, - "event": "newCreateFolder", - "operateType": 1, - } - res_mk = requests.post( - url, - headers=self.header_logined, - data=json.dumps(data_mk), - timeout=10 - ) - try: - res_json = res_mk.json() - except json.decoder.JSONDecodeError: - logger.error("创建失败") - logger.error(res_mk.text) - return - code_mkdir = res_json.get("code", -1) - - if code_mkdir == 0: - logger.info(f"创建成功: {res_json['data']['FileId']}") - self.get_dir() - return res_json["data"]["Info"]["FileId"] - logger.error(f"创建失败: {res_json}") - return - - @staticmethod - def _compute_file_md5(file_path): - """计算文件MD5值""" - md5 = hashlib.md5() - with open(file_path, "rb") as f: - while True: - data = f.read(64 * 1024) - if not data: - break - md5.update(data) - return md5.hexdigest() - -# 线程辅助 -class WorkerSignals(QtCore.QObject): - finished = QtCore.pyqtSignal() - error = QtCore.pyqtSignal(str) - result = QtCore.pyqtSignal(object) - progress = QtCore.pyqtSignal(int) - log = QtCore.pyqtSignal(str) - cancel = QtCore.pyqtSignal() - -class ThreadedTask(QtCore.QRunnable): - def __init__(self, fn, *args, **kwargs): - super().__init__() - self.fn = fn - self.args = args - self.kwargs = kwargs - self.signals = WorkerSignals() - self.is_cancelled = False - - @QtCore.pyqtSlot() - def run(self): - try: - if self.is_cancelled: - return - res = self.fn(*self.args, **self.kwargs, signals=self.signals, task=self) - if not self.is_cancelled: - self.signals.result.emit(res) - except Exception as e: - if not self.is_cancelled: - self.signals.error.emit(str(e)) - finally: - if not self.is_cancelled: - self.signals.finished.emit() - - def cancel(self): - """取消任务""" - self.is_cancelled = True - self.signals.cancel.emit() - -# 登录对话框 -class LoginDialog(QtWidgets.QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("登录123云盘") - self.setModal(True) - self.resize(420, 150) - self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint) - - layout = QtWidgets.QVBoxLayout(self) - - form = QtWidgets.QFormLayout() - self.le_user = QtWidgets.QLineEdit() - self.le_pass = QtWidgets.QLineEdit() - self.le_pass.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) - form.addRow("用户名:", self.le_user) - form.addRow("密码:", self.le_pass) - layout.addLayout(form) - - h = QtWidgets.QHBoxLayout() - h.addStretch() - self.btn_ok = QtWidgets.QPushButton("登录") - self.btn_cancel = QtWidgets.QPushButton("取消") - h.addWidget(self.btn_ok) - h.addWidget(self.btn_cancel) - layout.addLayout(h) - - self.btn_ok.clicked.connect(self.on_ok) - self.btn_cancel.clicked.connect(self.reject) - - self.pan = None - self.login_error = None - - # 从配置文件中加载用户名 - config = ConfigManager.load_config() - self.le_user.setText(config.get("userName", "")) - - def on_ok(self): - user = self.le_user.text().strip() - pwd = self.le_pass.text() - if not user or not pwd: - QtWidgets.QMessageBox.information(self, "提示", "请输入用户名和密码。") - return - QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.WaitCursor) - try: - # 构造123pan并登录 - try: - self.pan = Pan123(readfile=False, user_name=user, pass_word=pwd, input_pwd=False) - except Exception: - self.pan = Pan123(readfile=False, user_name=user, pass_word=pwd, input_pwd=False) - if not getattr(self.pan, "authorization", None): - code = self.pan.login() - if code != 200 and code != 0: - self.login_error = f"登录失败,返回码: {code}" - QtWidgets.QApplication.restoreOverrideCursor() - QtWidgets.QMessageBox.critical(self, "登录失败", self.login_error) - return - except Exception as e: - self.login_error = str(e) - QtWidgets.QApplication.restoreOverrideCursor() - QtWidgets.QMessageBox.critical(self, "登录异常", "登录时发生异常:\n" + str(e)) - return - finally: - QtWidgets.QApplication.restoreOverrideCursor() - - try: - if hasattr(self.pan, "save_file"): - self.pan.save_file() - except Exception: - pass - self.accept() - - def get_pan(self): - return self.pan - -# 主窗口 -class MainWindow(QtWidgets.QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("123云盘") - self.resize(980, 620) - self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint) - - self.pan = None - self.threadpool = QtCore.QThreadPool.globalInstance() - # 设置线程池的最大线程数,允许同时下载多个文件 - self.threadpool.setMaxThreadCount(64) - - # 应用123云盘主题 - self.apply_blue_white_theme() - - # 中央布局 - central = QtWidgets.QWidget() - self.setCentralWidget(central) - main_layout = QtWidgets.QHBoxLayout(central) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - # 创建侧边栏 - self.sidebar = QtWidgets.QWidget() - self.sidebar.setMinimumWidth(200) - self.sidebar.setMaximumWidth(200) - self.sidebar.setStyleSheet( - "background-color: rgba(255, 255, 255, 0.95);" - "border-right: 1px solid rgba(0, 0, 0, 0.05);" - "border-radius: 0;" - ) - sidebar_layout = QtWidgets.QVBoxLayout(self.sidebar) - sidebar_layout.setContentsMargins(10, 20, 10, 10) - sidebar_layout.setSpacing(8) - sidebar_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) - - # 侧边栏标题 - sidebar_title = QtWidgets.QLabel("功能菜单") - sidebar_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - sidebar_title.setStyleSheet( - "font-size: 20px; font-weight: bold; color: #334155; margin-bottom: 20px;" - "padding: 10px 0;" - ) - sidebar_layout.addWidget(sidebar_title) - - # 侧边栏按钮组 - self.sidebar_buttons = [] - self.sidebar_animations = {} - self.sidebar_original_geoms = {} - - # 文件页按钮 - self.btn_files = SidebarButton("📁 文件") - self.btn_files.setMinimumHeight(50) - self.btn_files.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" - "border: none;" - ) - sidebar_layout.addWidget(self.btn_files) - self.sidebar_buttons.append(self.btn_files) - - # 传输页按钮 - self.btn_transfer = SidebarButton("🔄 传输") - self.btn_transfer.setMinimumHeight(50) - self.btn_transfer.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" - ) - sidebar_layout.addWidget(self.btn_transfer) - self.sidebar_buttons.append(self.btn_transfer) - - # 为侧边栏按钮添加悬停和点击事件,实现动画效果 - for btn in self.sidebar_buttons: - btn.entered.connect(lambda b=btn: self.on_sidebar_button_hover(b)) - btn.left.connect(lambda b=btn: self.on_sidebar_button_leave(b)) - btn.pressed.connect(lambda b=btn: self.on_sidebar_button_pressed(b)) - btn.released.connect(lambda b=btn: self.on_sidebar_button_released(b)) - - # 保存按钮的原始位置 - QtCore.QTimer.singleShot(100, lambda b=btn: self.save_original_position(b)) - - sidebar_layout.addStretch() - main_layout.addWidget(self.sidebar) - - # 创建右侧内容区域 - right_content = QtWidgets.QWidget() - right_layout = QtWidgets.QVBoxLayout(right_content) - right_layout.setContentsMargins(10, 10, 10, 10) - right_layout.setSpacing(8) - - # 顶部横向按钮栏(左上角为设置按钮) - toolbar_h = QtWidgets.QHBoxLayout() - toolbar_h.setSpacing(6) - - # 设置按钮(左上角齿轮图标) - self.btn_settings = QtWidgets.QPushButton("⚙️") - self.btn_settings.setToolTip("设置") - self.btn_settings.setMinimumHeight(36) - self.btn_settings.setMinimumWidth(45) - self.btn_settings.setMaximumHeight(36) - self.btn_settings.setMaximumWidth(45) - self.btn_settings.setStyleSheet( - "font-size: 20px;" - "background-color: transparent;" - "border: none;" - "border-radius: 8px;" - ) - self.btn_settings.setObjectName("btn_settings") - toolbar_h.addWidget(self.btn_settings) - - # 操作按钮(横向排列) - self.btn_refresh = QtWidgets.QPushButton("刷新") - self.btn_more = QtWidgets.QPushButton("更多") - self.btn_up = QtWidgets.QPushButton("上级") - self.btn_delete = QtWidgets.QPushButton("删除") - self.btn_download = QtWidgets.QPushButton("下载") - self.btn_share = QtWidgets.QPushButton("分享") - self.btn_link = QtWidgets.QPushButton("显示链接") - self.btn_upload = QtWidgets.QPushButton("上传文件") - self.btn_mkdir = QtWidgets.QPushButton("新建文件夹") - - # 设置按钮最小宽度统一外观 - btns = [self.btn_refresh, self.btn_more, self.btn_up, self.btn_download, self.btn_link, - self.btn_upload, self.btn_mkdir, self.btn_delete, self.btn_share] - - # 为每个按钮添加动画效果 - self.button_animations = {} - for b in btns: - b.setMinimumHeight(30) - b.setMinimumWidth(110) - toolbar_h.addWidget(b) - - # 为按钮添加悬停和点击事件,实现动画效果 - b.enterEvent = lambda event, btn=b: self.on_button_hover(btn) - b.leaveEvent = lambda event, btn=b: self.on_button_leave(btn) - b.pressed.connect(lambda btn=b: self.on_button_pressed(btn)) - b.released.connect(lambda btn=b: self.on_button_released(btn)) - - # 初始化按钮动画 - animation = QtCore.QPropertyAnimation(b, b"geometry") - animation.setDuration(100) - self.button_animations[b] = animation - - toolbar_h.addStretch() - right_layout.addLayout(toolbar_h) - - # 路径栏 - self.path_widget = QtWidgets.QWidget() - path_h = QtWidgets.QHBoxLayout(self.path_widget) - path_h.addWidget(QtWidgets.QLabel("路径:")) - self.lbl_path = QtWidgets.QLabel("/") - font = self.lbl_path.font() - font.setBold(True) - self.lbl_path.setFont(font) - path_h.addWidget(self.lbl_path) - path_h.addStretch() - right_layout.addWidget(self.path_widget) - - # 创建页面堆栈 - self.page_stack = QtWidgets.QStackedWidget() - - # 文件页面 - self.files_page = QtWidgets.QWidget() - files_layout = QtWidgets.QVBoxLayout(self.files_page) - files_layout.setContentsMargins(0, 0, 0, 0) - - # 文件列表区域(包含表格和加载动画) - file_list_widget = QtWidgets.QWidget() - file_list_layout = QtWidgets.QVBoxLayout(file_list_widget) - file_list_layout.setContentsMargins(0, 0, 0, 0) - - # 文件列表表格 - self.table = QtWidgets.QTableWidget(0, 5) - self.table.setHorizontalHeaderLabels(["", "编号", "名称", "类型", "大小"]) - self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) - self.table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) - self.table.doubleClicked.connect(self.on_table_double) - self.table.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) - self.table.customContextMenuRequested.connect(self.on_table_context_menu) - self.table.verticalHeader().setVisible(False) - self.table.horizontalHeader().setStretchLastSection(True) - file_list_layout.addWidget(self.table, stretch=1) - - # 加载动画布局 - self.loading_widget = QtWidgets.QWidget() - loading_layout = QtWidgets.QVBoxLayout(self.loading_widget) - loading_layout.setContentsMargins(0, 0, 0, 0) - loading_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - - # 加载标签 - self.loading_label = QtWidgets.QLabel() - self.loading_label.setText("正在加载...") - font = self.loading_label.font() - font.setPointSize(14) - self.loading_label.setFont(font) - self.loading_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - loading_layout.addWidget(self.loading_label) - - # 旋转动画 - self.loading_spinner = QtWidgets.QLabel() - self.loading_spinner.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - # 创建一个简单的旋转动画 - self.spinner_timer = QtCore.QTimer() - self.spinner_angle = 0 - self.spinner_timer.timeout.connect(self.update_spinner) - self.spinner_timer.start(50) # 每50毫秒更新一次 - - loading_layout.addWidget(self.loading_spinner) - - # 初始隐藏加载动画 - self.loading_widget.setVisible(False) - file_list_layout.addWidget(self.loading_widget) - - files_layout.addWidget(file_list_widget, stretch=1) - - # 传输任务管理 - self.transfer_tasks = [] - self.next_task_id = 0 - self.active_tasks = {} # 保存活动任务的引用,用于取消 - - # 传输页面 - self.transfer_page = QtWidgets.QWidget() - transfer_layout = QtWidgets.QVBoxLayout(self.transfer_page) - transfer_layout.setContentsMargins(0, 0, 0, 0) - - # 传输页面内容 - transfer_title = QtWidgets.QLabel("传输任务") - transfer_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - transfer_title.setStyleSheet("font-size: 24px; font-weight: bold; color: #334155; margin: 20px 0;") - transfer_layout.addWidget(transfer_title) - - self.transfer_table = QtWidgets.QTableWidget(0, 6) - self.transfer_table.setHorizontalHeaderLabels(["类型", "文件名", "大小", "进度", "状态", "操作"]) - self.transfer_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) - self.transfer_table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) - self.transfer_table.verticalHeader().setVisible(False) - self.transfer_table.horizontalHeader().setStretchLastSection(True) - # 设置列宽 - self.transfer_table.setColumnWidth(0, 80) - self.transfer_table.setColumnWidth(2, 120) - self.transfer_table.setColumnWidth(3, 100) - self.transfer_table.setColumnWidth(4, 100) - self.transfer_table.setColumnWidth(5, 80) - transfer_layout.addWidget(self.transfer_table, stretch=1) - - # 添加页面到堆栈 - self.page_stack.addWidget(self.files_page) - self.page_stack.addWidget(self.transfer_page) - - right_layout.addWidget(self.page_stack, stretch=1) - main_layout.addWidget(right_content, stretch=1) - - # 状态栏显示简短提示/进度 - self.status = self.statusBar() - self.status.showMessage("准备就绪") - - # 信号连接 - self.btn_settings.clicked.connect(self.on_settings) - self.btn_refresh.clicked.connect(lambda: self.refresh_file_list(reset_page=True)) - self.btn_more.clicked.connect(lambda: self.refresh_file_list(reset_page=False)) - self.btn_up.clicked.connect(self.on_up) - self.btn_download.clicked.connect(self.on_download) - self.btn_link.clicked.connect(self.on_showlink) - self.btn_upload.clicked.connect(self.on_upload) - self.btn_mkdir.clicked.connect(self.on_mkdir) - self.btn_delete.clicked.connect(self.on_delete) - self.btn_share.clicked.connect(self.on_share) - - # 侧边栏按钮信号 - self.btn_files.clicked.connect(lambda: self.switch_page(0)) - self.btn_transfer.clicked.connect(lambda: self.switch_page(1)) - - # 初始化默认页面 - self.switch_page(0) - - # 启动登录流程 - self.startup_login_flow() - - def apply_blue_white_theme(self): - """ - 123云盘主题样式表 - iOS 26 Liquid Glass 液态毛玻璃效果 - """ - style = """ - /* 全局样式 */ - QWidget { - background-color: rgba(255, 255, 255, 0.8); - color: #1E293B; - font-family: "SF Pro Display", "Segoe UI", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial; - font-size: 13px; - } - - /* 主窗口 */ - QMainWindow { - background-color: rgba(245, 245, 247, 0.95); - } - - /* 表格样式 - 液态毛玻璃效果(模拟) */ - QTableWidget { - background-color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(255, 255, 255, 0.8); - border-radius: 12px; - padding: 8px; - gridline-color: rgba(0, 0, 0, 0.05); - } - - /* 表格行样式 */ - QTableWidget::item { - padding: 10px 6px; - border: none; - background-color: transparent; - border-radius: 6px; - } - - /* 表格行悬停效果 */ - QTableWidget::item:hover { - background-color: rgba(59, 130, 246, 0.1); - } - - /* 表格行选中效果 */ - QTableWidget::item:selected { - background-color: rgba(59, 130, 246, 0.9); - color: #FFFFFF; - } - - /* 表头样式 */ - QHeaderView::section { - background-color: rgba(255, 255, 255, 0.95); - color: #334155; - padding: 12px 16px; - border: none; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - font-weight: 600; - text-align: left; - border-radius: 8px 8px 0 0; - } - - QHeaderView { - background-color: transparent; - border: none; - } - - /* 按钮样式 - 液态毛玻璃效果(模拟) */ - QPushButton { - background-color: rgba(255, 255, 255, 0.95); - color: #3B82F6; - border: 1px solid rgba(59, 130, 246, 0.4); - border-radius: 12px; - padding: 10px 18px; - font-weight: 500; - font-size: 14px; - } - - QPushButton:hover { - background-color: rgba(255, 255, 255, 0.98); - border-color: rgba(59, 130, 246, 0.6); - } - - QPushButton:pressed { - background-color: rgba(230, 240, 255, 0.95); - border-color: rgba(59, 130, 246, 0.8); - } - - QPushButton:disabled { - background-color: rgba(240, 240, 245, 0.8); - border-color: rgba(148, 163, 184, 0.4); - color: rgba(148, 163, 184, 0.8); - } - - /* 输入控件样式 - 液态毛玻璃效果(模拟) */ - QLineEdit, QTextEdit, QComboBox { - background-color: rgba(255, 255, 255, 0.95); - border: 1px solid rgba(0, 0, 0, 0.08); - padding: 10px 14px; - border-radius: 12px; - } - - QLineEdit:focus, QTextEdit:focus, QComboBox:focus { - border-color: rgba(59, 130, 246, 0.6); - } - - /* 状态栏样式 - 液态毛玻璃效果(模拟) */ - QStatusBar { - background-color: rgba(255, 255, 255, 0.95); - color: #334155; - padding: 8px 16px; - border-top: 1px solid rgba(0, 0, 0, 0.05); - } - - /* 菜单样式 - 液态毛玻璃效果(模拟) */ - QMenu { - background-color: rgba(255, 255, 255, 0.98); - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - padding: 8px 0; - } - - QMenu::item { - padding: 10px 24px; - background-color: transparent; - border: none; - border-radius: 8px; - margin: 2px 8px; - } - - QMenu::item:selected { - background-color: rgba(59, 130, 246, 0.15); - color: #3B82F6; - } - - /* 滚动条样式 - 液态毛玻璃效果(模拟) */ - QScrollBar { - background-color: rgba(255, 255, 255, 0.7); - border-radius: 10px; - width: 10px; - height: 10px; - } - - QScrollBar::handle { - background-color: rgba(59, 130, 246, 0.6); - border-radius: 10px; - min-width: 24px; - min-height: 24px; - } - - QScrollBar::handle:hover { - background-color: rgba(59, 130, 246, 0.8); - } - - QScrollBar::add-line, QScrollBar::sub-line { - background-color: transparent; - } - - /* 对话框样式 - 液态毛玻璃效果(模拟) */ - QDialog { - background-color: rgba(255, 255, 255, 0.98); - border: 1px solid rgba(255, 255, 255, 0.9); - border-radius: 16px; - } - - /* 分组框样式 - 液态毛玻璃效果(模拟) */ - QGroupBox { - background-color: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(0, 0, 0, 0.08); - border-radius: 12px; - margin-top: 16px; - padding: 16px; - } - - QGroupBox::title { - color: #334155; - font-weight: 600; - subcontrol-origin: margin; - subcontrol-position: top left; - padding: 0 12px; - } - - /* 复选框样式 - 液态毛玻璃效果(模拟) */ - QCheckBox { - spacing: 8px; - } - - QCheckBox::indicator { - width: 20px; - height: 20px; - border: 2px solid rgba(59, 130, 246, 0.6); - border-radius: 6px; - background-color: rgba(255, 255, 255, 0.95); - } - - QCheckBox::indicator:checked { - background-color: rgba(59, 130, 246, 0.95); - border-color: rgba(59, 130, 246, 0.95); - } - - /* 标签样式 */ - QLabel { - color: #334155; - } - - /* 路径标签 */ - QLabel#lbl_path { - font-weight: 600; - color: #3B82F6; - font-size: 14px; - } - - /* 加载动画标签 */ - QLabel#loading_label { - color: #3B82F6; - } - - /* 设置按钮特殊样式 */ - QPushButton#btn_settings { - background-color: transparent; - border: none; - border-radius: 8px; - font-size: 18px; - padding: 6px; - color: #3B82F6; - } - - QPushButton#btn_settings:hover { - background-color: rgba(59, 130, 246, 0.1); - } - """ - self.setStyleSheet(style) - - def on_settings(self): - """打开设置对话框""" - dlg = SettingsDialog(self) - if dlg.exec() == QtWidgets.QDialog.DialogCode.Accepted: - settings = dlg.get_settings() - # 保存设置到配置文件 - config = ConfigManager.load_config() - config["settings"] = settings - ConfigManager.save_config(config) - QtWidgets.QMessageBox.information(self, "设置", "设置已保存") - - def startup_login_flow(self): - cfg_loaded = False - config = ConfigManager.load_config() - if config.get("userName") and config.get("passWord"): - try: - self.pan = Pan123(readfile=True, input_pwd=False) - res_code = self.pan.get_dir(save=False)[0] - if res_code == 0: - cfg_loaded = True - else: - cfg_loaded = False - except Exception: - cfg_loaded = False - - if not cfg_loaded: - dlg = LoginDialog(self) - if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted: - QtWidgets.QMessageBox.information(self, "提示", "未登录,程序将退出。") - QtCore.QTimer.singleShot(0, self.close) - return - self.pan = dlg.get_pan() - - self.refresh_file_list(reset_page=True) - - def prompt_selected_row(self): - rows = self.table.selectionModel().selectedRows() - if not rows: - QtWidgets.QMessageBox.information(self, "提示", "请先选择一项。") - return None - return rows[0].row() - - def get_file_icon(self, file_detail): - """根据文件类型获取图标""" - file_type = file_detail.get("Type", 0) - file_name = file_detail.get("FileName", "") - - # 创建一个32x32的图标 - pixmap = QtGui.QPixmap(32, 32) - pixmap.fill(QtCore.Qt.GlobalColor.transparent) - painter = QtGui.QPainter(pixmap) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) - - if file_type == 1: # 文件夹 - # 绘制文件夹图标 - painter.setBrush(QtGui.QColor(255, 193, 7)) - painter.setPen(QtGui.QColor(255, 152, 0)) - # 文件夹主体 - painter.drawRect(6, 10, 20, 16) - # 文件夹盖子 - painter.drawRect(6, 6, 16, 8) - else: # 文件 - # 根据文件扩展名选择图标颜色 - ext = os.path.splitext(file_name)[1].lower() - colors = { - ".txt": QtGui.QColor(25, 118, 210), - ".pdf": QtGui.QColor(211, 47, 47), - ".doc": QtGui.QColor(33, 150, 243), - ".docx": QtGui.QColor(33, 150, 243), - ".xls": QtGui.QColor(76, 175, 80), - ".xlsx": QtGui.QColor(76, 175, 80), - ".ppt": QtGui.QColor(255, 193, 7), - ".pptx": QtGui.QColor(255, 193, 7), - ".jpg": QtGui.QColor(156, 39, 176), - ".jpeg": QtGui.QColor(156, 39, 176), - ".png": QtGui.QColor(156, 39, 176), - ".gif": QtGui.QColor(156, 39, 176), - ".mp3": QtGui.QColor(94, 53, 177), - ".mp4": QtGui.QColor(233, 30, 99), - ".zip": QtGui.QColor(121, 85, 72), - ".rar": QtGui.QColor(121, 85, 72), - ".7z": QtGui.QColor(121, 85, 72), - } - - color = colors.get(ext, QtGui.QColor(100, 116, 139)) - painter.setBrush(color) - painter.setPen(color.darker(120)) - - # 绘制文件图标 - painter.drawRect(6, 8, 20, 20) - # 绘制文件顶部的横线 - painter.setBrush(color.darker(120)) - painter.drawRect(6, 8, 20, 4) - - painter.end() - return QtGui.QIcon(pixmap) - - def populate_table(self): - if not self.pan: - return - self.table.setRowCount(0) - - # 逐行添加,使用定时器实现动画效果 - for i, item in enumerate(self.pan.list): - # 使用定时器延迟添加,实现逐行出现的效果 - QtCore.QTimer.singleShot(i * 30, lambda idx=i: self._add_row(idx)) - - names = getattr(self.pan, "parent_file_name_list", []) - path = "/" + "/".join(names) if names else "/" - self.lbl_path.setText(path) - - def _add_row(self, index): - """添加行,逐行显示""" - if index >= len(self.pan.list): - return - - item = self.pan.list[index] - row = self.table.rowCount() - self.table.insertRow(row) - - # 添加文件图标 - icon = self.get_file_icon(item) - icon_item = QtWidgets.QTableWidgetItem() - icon_item.setIcon(icon) - self.table.setItem(row, 0, icon_item) - - # 设置列宽,图标列不需要太宽 - self.table.setColumnWidth(0, 40) - - # 编号 - self.table.setItem(row, 1, QtWidgets.QTableWidgetItem(str(index + 1))) - - # 文件名 - name_item = QtWidgets.QTableWidgetItem(item.get("FileName", "")) - # 文件夹使用粗体 - if item.get("Type", 0) == 1: - font = name_item.font() - font.setBold(True) - name_item.setFont(font) - self.table.setItem(row, 2, name_item) - - # 文件类型 - typ = "文件夹" if item.get("Type", 0) == 1 else "文件" - self.table.setItem(row, 3, QtWidgets.QTableWidgetItem(typ)) - - # 文件大小 - size = item.get("Size", 0) - if size > 1073741824: - s = f"{round(size / 1073741824, 2)} GB" - elif size > 1048576: - s = f"{round(size / 1048576, 2)} MB" - else: - s = f"{round(size / 1024, 2)} KB" - self.table.setItem(row, 4, QtWidgets.QTableWidgetItem(s)) - - def update_spinner(self): - """更新旋转动画""" - self.spinner_angle = (self.spinner_angle + 10) % 360 - pixmap = QtGui.QPixmap(32, 32) - pixmap.fill(QtCore.Qt.GlobalColor.transparent) - painter = QtGui.QPainter(pixmap) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) - - # 绘制旋转圆环 - pen = QtGui.QPen(QtGui.QColor(59, 130, 246), 3) - pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) - painter.setPen(pen) - rect = QtCore.QRect(4, 4, 24, 24) - painter.drawArc(rect, (90 - self.spinner_angle) * 16, 180 * 16) - - painter.end() - self.loading_spinner.setPixmap(pixmap) - - def refresh_file_list(self, reset_page=True): - if not self.pan: - QtWidgets.QMessageBox.information(self, "提示", "尚未初始化,请先登录。") - return - if reset_page: - self.pan.all_file = False - self.pan.file_page = 0 - self.pan.list = [] - - # 显示加载动画 - self.table.setVisible(False) - self.loading_widget.setVisible(True) - self.status.showMessage("正在获取目录...") - - task = ThreadedTask(self._task_get_dir) - task.signals.result.connect(self._after_get_dir) - task.signals.error.connect(lambda e: self._show_error("获取目录失败: " + e)) - self.threadpool.start(task) - - def _task_get_dir(self, signals=None, task=None): - code, _ = self.pan.get_dir(save=True) - return code - - def _after_get_dir(self, code): - # 隐藏加载动画,显示表格 - self.loading_widget.setVisible(False) - self.table.setVisible(True) - - if code != 0: - self.status.showMessage(f"获取目录返回码: {code}", 5000) - else: - self.status.showMessage("目录获取完成", 3000) - self.populate_table() - - def on_table_double(self, index): - row = index.row() - typ_item = self.table.item(row, 3) - if typ_item and typ_item.text() == "文件夹": - try: - # 保存要进入的文件夹编号 - self.target_folder_num = str(row + 1) - # 添加淡出动画 - self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") - self.fade_animation.setDuration(200) - self.fade_animation.setStartValue(1.0) - self.fade_animation.setEndValue(0.0) - self.fade_animation.finished.connect(self._after_fade_out_enter_folder) - self.fade_animation.start() - except Exception as e: - self._show_error("进入文件夹失败: " + str(e)) - else: - ret = QtWidgets.QMessageBox.question(self, "下载", "是否下载所选文件?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) - if ret == QtWidgets.QMessageBox.StandardButton.Yes: - self.on_download() - - def _after_fade_out_enter_folder(self): - """淡出动画完成后执行的操作 - 进入文件夹""" - try: - self.pan.cd(self.target_folder_num) - self.populate_table() - # 添加淡入动画 - self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") - self.fade_animation.setDuration(200) - self.fade_animation.setStartValue(0.0) - self.fade_animation.setEndValue(1.0) - self.fade_animation.start() - except Exception as e: - self._show_error("进入文件夹失败: " + str(e)) - - def on_button_hover(self, button): - """按钮悬停效果 - 修复动画冲突""" - # 停止当前正在运行的动画 - if button in self.button_animations: - self.button_animations[button].stop() - - # 保存原始位置,用于恢复 - if not hasattr(self, 'button_original_geoms'): - self.button_original_geoms = {} - if button not in self.button_original_geoms: - self.button_original_geoms[button] = button.geometry() - - # 创建放大动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - original_geom = self.button_original_geoms[button] - # 基于原始位置计算新位置,避免累积误差 - new_geom = QtCore.QRect( - original_geom.x() - 2, - original_geom.y() - 2, - original_geom.width() + 4, - original_geom.height() + 4 - ) - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(new_geom) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation - - def on_button_leave(self, button): - """按钮离开效果 - 修复动画冲突""" - # 停止当前正在运行的动画 - if button in self.button_animations: - self.button_animations[button].stop() - - # 恢复到原始位置 - if hasattr(self, 'button_original_geoms') and button in self.button_original_geoms: - # 创建恢复动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - original_geom = self.button_original_geoms[button] - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(original_geom) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation - - def on_button_pressed(self, button): - """按钮按下效果 - 修复动画冲突""" - # 停止当前正在运行的动画 - if button in self.button_animations: - self.button_animations[button].stop() - - # 创建按下动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - # 基于当前位置轻微缩小 - new_geom = QtCore.QRect( - current_geom.x() + 1, - current_geom.y() + 1, - current_geom.width() - 2, - current_geom.height() - 2 - ) - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(new_geom) - scale_animation.setDuration(100) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.InQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation - - def on_button_released(self, button): - """按钮释放效果 - 修复动画冲突""" - # 停止当前正在运行的动画 - if button in self.button_animations: - self.button_animations[button].stop() - - # 恢复到原始放大状态(如果是悬停中)或原始状态 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - current_geom = button.geometry() - - if hasattr(self, 'button_original_geoms') and button in self.button_original_geoms: - # 检查鼠标是否仍然在按钮上 - if button.underMouse(): - # 恢复到悬停放大状态 - original_geom = self.button_original_geoms[button] - new_geom = QtCore.QRect( - original_geom.x() - 2, - original_geom.y() - 2, - original_geom.width() + 4, - original_geom.height() + 4 - ) - else: - # 恢复到原始状态 - new_geom = self.button_original_geoms[button] - - scale_animation.setStartValue(current_geom) - scale_animation.setEndValue(new_geom) - scale_animation.setDuration(100) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.button_animations[button] = scale_animation - - def on_table_context_menu(self, pos): - row = self.table.indexAt(pos).row() - if row < 0: - return - menu = QtWidgets.QMenu() - a_download = menu.addAction("下载") - a_link = menu.addAction("显示链接") - a_delete = menu.addAction("删除") - a_share = menu.addAction("分享") - action = menu.exec(self.table.viewport().mapToGlobal(pos)) - self.table.selectRow(row) - if action == a_download: - self.on_download() - elif action == a_link: - self.on_showlink() - elif action == a_delete: - self.on_delete() - elif action == a_share: - self.on_share() - - def on_up(self): - if not self.pan: - return - try: - # 添加淡出动画 - self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") - self.fade_animation.setDuration(200) - self.fade_animation.setStartValue(1.0) - self.fade_animation.setEndValue(0.0) - self.fade_animation.finished.connect(self._after_fade_out_up) - self.fade_animation.start() - except Exception as e: - self._show_error("返回上级失败: " + str(e)) - - def _after_fade_out_up(self): - """淡出动画完成后执行的操作 - 返回上级""" - try: - self.pan.cd("..") - self.populate_table() - # 添加淡入动画 - self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") - self.fade_animation.setDuration(200) - self.fade_animation.setStartValue(0.0) - self.fade_animation.setEndValue(1.0) - self.fade_animation.start() - except Exception as e: - self._show_error("返回上级失败: " + str(e)) - - def save_original_position(self, button): - """保存按钮的原始位置""" - self.sidebar_original_geoms[button] = button.geometry() - - def switch_page(self, page_index): - """切换页面""" - # 切换堆栈页面 - self.page_stack.setCurrentIndex(page_index) - - # 更新按钮样式 - for i, btn in enumerate(self.sidebar_buttons): - if i == page_index: - btn.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" - "border: none;" - ) - else: - btn.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" - ) - - # 根据页面显示/隐藏路径栏和相关按钮 - if page_index == 0: # 文件页面 - self.path_widget.setVisible(True) - self.btn_refresh.setVisible(True) - self.btn_more.setVisible(True) - self.btn_up.setVisible(True) - self.btn_delete.setVisible(True) - self.btn_download.setVisible(True) - self.btn_share.setVisible(True) - self.btn_link.setVisible(True) - self.btn_upload.setVisible(True) - self.btn_mkdir.setVisible(True) - else: # 传输页面 - self.path_widget.setVisible(False) - self.btn_refresh.setVisible(False) - self.btn_more.setVisible(False) - self.btn_up.setVisible(False) - self.btn_delete.setVisible(False) - self.btn_download.setVisible(False) - self.btn_share.setVisible(False) - self.btn_link.setVisible(False) - self.btn_upload.setVisible(False) - self.btn_mkdir.setVisible(False) - - def on_sidebar_button_hover(self, button): - """侧边栏按钮悬停效果""" - # 停止当前正在运行的动画 - if button in self.sidebar_animations: - self.sidebar_animations[button].stop() - - # 获取原始位置 - if button not in self.sidebar_original_geoms: - self.save_original_position(button) - original_geom = self.sidebar_original_geoms[button] - - # 创建缩放动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - scale_animation.setStartValue(button.geometry()) - scale_animation.setEndValue(QtCore.QRect( - original_geom.x() - 5, - original_geom.y() - 2, - original_geom.width() + 10, - original_geom.height() + 4 - )) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.sidebar_animations[button] = scale_animation - - def on_sidebar_button_leave(self, button): - """侧边栏按钮离开效果""" - # 停止当前正在运行的动画 - if button in self.sidebar_animations: - self.sidebar_animations[button].stop() - - # 获取原始位置 - if button not in self.sidebar_original_geoms: - self.save_original_position(button) - original_geom = self.sidebar_original_geoms[button] - - # 创建恢复动画 - scale_animation = QtCore.QPropertyAnimation(button, b"geometry") - scale_animation.setStartValue(button.geometry()) - scale_animation.setEndValue(original_geom) - scale_animation.setDuration(150) - scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) - scale_animation.start() - - # 保存动画引用 - self.sidebar_animations[button] = scale_animation - - def on_sidebar_button_pressed(self, button): - """侧边栏按钮按下效果""" - # 改变背景色 - button.setStyleSheet( - button.styleSheet().replace( - "background-color: rgba(59, 130, 246, 0.9);", - "background-color: rgba(37, 99, 235, 0.9);" - ).replace( - "background-color: transparent;", - "background-color: rgba(59, 130, 246, 0.1);" - ) - ) - - def on_sidebar_button_released(self, button): - """侧边栏按钮释放效果""" - # 恢复背景色 - if button == self.btn_files: - if self.page_stack.currentIndex() == 0: - button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" - "border: none;" - ) - else: - button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" - ) - elif button == self.btn_transfer: - if self.page_stack.currentIndex() == 1: - button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: rgba(59, 130, 246, 0.9);" - "color: white; border-radius: 12px;" - "border: none;" - ) - else: - button.setStyleSheet( - "font-size: 16px; text-align: left; padding-left: 20px;" - "background-color: transparent; color: #334155;" - "border-radius: 12px;" - "border: none;" - ) - - def add_transfer_task(self, task_type, file_name, file_size): - """添加传输任务到列表和表格""" - task_id = self.next_task_id - self.next_task_id += 1 - - # 创建任务对象 - task = { - "id": task_id, - "type": task_type, # "下载" 或 "上传" - "file_name": file_name, - "file_size": file_size, - "progress": 0, - "status": "等待中", - "file_path": "", # 用于保存下载文件路径,便于取消时删除 - "threaded_task": None # 保存线程任务引用 - } - - # 添加到任务列表 - self.transfer_tasks.append(task) - - # 添加到表格 - row = self.transfer_table.rowCount() - self.transfer_table.insertRow(row) - - # 设置表格内容 - self.transfer_table.setItem(row, 0, QtWidgets.QTableWidgetItem(task_type)) - self.transfer_table.setItem(row, 1, QtWidgets.QTableWidgetItem(file_name)) - self.transfer_table.setItem(row, 2, QtWidgets.QTableWidgetItem(self.format_file_size(file_size))) - self.transfer_table.setItem(row, 3, QtWidgets.QTableWidgetItem("0%")) - self.transfer_table.setItem(row, 4, QtWidgets.QTableWidgetItem("等待中")) - - # 添加取消按钮 - cancel_btn = QtWidgets.QPushButton("取消") - cancel_btn.setStyleSheet( - "background-color: rgba(239, 68, 68, 0.1);" - "color: #EF4444;" - "border: 1px solid rgba(239, 68, 68, 0.3);" - "border-radius: 8px;" - "padding: 4px 12px;" - "font-size: 12px;" - ) - cancel_btn.clicked.connect(lambda _, tid=task_id: self.cancel_transfer_task(tid)) - self.transfer_table.setCellWidget(row, 5, cancel_btn) - - return task_id - - def update_transfer_task(self, task_id, progress, status): - """更新传输任务的进度和状态""" - # 查找任务 - for i, task in enumerate(self.transfer_tasks): - if task["id"] == task_id: - # 更新任务对象 - task["progress"] = progress - task["status"] = status - - # 更新表格 - self.transfer_table.setItem(i, 3, QtWidgets.QTableWidgetItem(f"{progress}%")) - self.transfer_table.setItem(i, 4, QtWidgets.QTableWidgetItem(status)) - break - - def cancel_transfer_task(self, task_id): - """取消传输任务""" - # 查找任务 - for i, task in enumerate(self.transfer_tasks): - if task["id"] == task_id: - # 取消线程任务 - if task.get("threaded_task"): - task["threaded_task"].cancel() - - # 如果是下载任务,删除临时文件 - if task["type"] == "下载" and task.get("file_path") and os.path.exists(task["file_path"]): - try: - os.remove(task["file_path"]) - # 也检查是否有最终文件存在(如果下载已完成但未清理) - final_path = task["file_path"].replace(".123pan", "") - if os.path.exists(final_path): - os.remove(final_path) - except Exception as e: - print(f"删除文件失败: {e}") - - # 更新任务状态 - task["status"] = "已取消" - task["progress"] = 0 - self.transfer_table.setItem(i, 3, QtWidgets.QTableWidgetItem("0%")) - self.transfer_table.setItem(i, 4, QtWidgets.QTableWidgetItem("已取消")) - - # 移除取消按钮 - widget = self.transfer_table.cellWidget(i, 5) - if widget: - widget.setVisible(False) - - # 从活动任务列表中移除 - if task_id in self.active_tasks: - del self.active_tasks[task_id] - - break - - def remove_transfer_task(self, task_id): - """移除传输任务""" - # 查找任务 - for i, task in enumerate(self.transfer_tasks): - if task["id"] == task_id: - # 从列表中移除 - self.transfer_tasks.pop(i) - # 从表格中移除 - self.transfer_table.removeRow(i) - # 从活动任务列表中移除 - if task_id in self.active_tasks: - del self.active_tasks[task_id] - break - - def format_file_size(self, size): - """格式化文件大小""" - if size > 1073741824: - return f"{round(size / 1073741824, 2)} GB" - elif size > 1048576: - return f"{round(size / 1048576, 2)} MB" - elif size > 1024: - return f"{round(size / 1024, 2)} KB" - else: - return f"{size} B" - - def get_selected_detail(self): - row = self.prompt_selected_row() - if row is None: - return None, None - try: - # 直接使用行索引作为文件索引,更可靠 - if not self.pan or row < 0 or row >= len(self.pan.list): - self._show_error("无效的选择行") - return None, None - return row, self.pan.list[row] - except Exception as e: - self._show_error(f"获取选中文件失败: {str(e)}") - return None, None - - def on_download(self): - file_index, file_detail = self.get_selected_detail() - if file_detail is None: - return - - # 获取设置 - ask_location = ConfigManager.get_setting("askDownloadLocation", True) - default_path = ConfigManager.get_setting("defaultDownloadPath", - os.path.join(os.path.expanduser("~"), "Downloads")) - - download_dir = default_path - if ask_location: - download_dir = QtWidgets.QFileDialog.getExistingDirectory( - self, "选择下载文件夹", default_path - ) - if not download_dir: - return - - file_name = file_detail.get("FileName", "未知文件") - file_size = file_detail.get("Size", 0) - - # 添加传输任务 - task_id = self.add_transfer_task("下载", file_name, file_size) - - self.status.showMessage("正在解析下载链接...") - task = ThreadedTask(self._task_get_download_and_stream, file_index, download_dir, task_id) - - # 保存任务对象引用 - for i, t in enumerate(self.transfer_tasks): - if t["id"] == task_id: - self.transfer_tasks[i]["threaded_task"] = task - break - - self.active_tasks[task_id] = task - - task.signals.progress.connect(lambda p, tid=task_id: ( - self.status.showMessage(f"下载进度: {p}%", 2000), - self.update_transfer_task(tid, p, "下载中") - )) - def on_task_finished(tid): - if tid in self.active_tasks: - del self.active_tasks[tid] - - task.signals.result.connect(lambda r, tid=task_id: ( - self.status.showMessage("下载完成: " + str(r), 5000), - self.update_transfer_task(tid, 100, "已完成"), - on_task_finished(tid) - )) - task.signals.error.connect(lambda e, tid=task_id: ( - self._show_error("下载失败: " + e), - self.update_transfer_task(tid, 0, "失败"), - on_task_finished(tid) - )) - task.signals.finished.connect(lambda tid=task_id: on_task_finished(tid)) - self.threadpool.start(task) - - def _task_get_download_and_stream(self, file_index, download_dir, task_id, signals=None, task=None): - file_detail = self.pan.list[file_index] - if file_detail["Type"] == 1: - redirect_url = self.pan.link_by_fileDetail(file_detail, showlink=False) - else: - redirect_url = self.pan.link_by_number(file_index, showlink=False) - if isinstance(redirect_url, int): - raise RuntimeError("获取下载链接失败,返回码: " + str(redirect_url)) - if file_detail["Type"] == 1: - fname = file_detail["FileName"] + ".zip" - else: - fname = file_detail["FileName"] - out_path = os.path.join(download_dir, fname) - temp = out_path + ".123pan" - - # 保存文件路径到任务对象 - for i, t in enumerate(self.transfer_tasks): - if t["id"] == task_id: - self.transfer_tasks[i]["file_path"] = temp - break - - if os.path.exists(out_path): - reply = QtWidgets.QMessageBox.question(None, "文件已存在", f"{fname} 已存在,是否覆盖?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) - if reply == QtWidgets.QMessageBox.StandardButton.No: - return "已取消" - with requests.get(redirect_url, stream=True, timeout=30) as r: - r.raise_for_status() - total = int(r.headers.get("Content-Length", 0) or 0) - done = 0 - with open(temp, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - # 检查是否被取消 - if task and task.is_cancelled: - f.close() - # 删除临时文件 - if os.path.exists(temp): - os.remove(temp) - return "已取消" - if chunk: - f.write(chunk) - done += len(chunk) - if total and signals: - signals.progress.emit(int(done * 100 / total)) - if task and task.is_cancelled: - # 删除临时文件 - if os.path.exists(temp): - os.remove(temp) - return "已取消" - os.replace(temp, out_path) - return out_path - - def on_showlink(self): - file_index, file_detail = self.get_selected_detail() - if file_detail is None: - return - try: - # 直接调用获取链接,不使用线程,避免参数传递问题 - url = self._task_get_link(file_index) - self._after_get_link(url) - except Exception as e: - self._show_error(f"获取链接失败: {str(e)}") - - def _task_get_link(self, file_index, signals=None, task=None): - try: - url = self.pan.link_by_number(file_index, showlink=False) - return url - except Exception as e: - return f"获取链接失败: {str(e)}" - - def _after_get_link(self, url): - if isinstance(url, int): - self._show_error("获取链接失败,返回码: " + str(url)) - return - dlg = QtWidgets.QDialog(self) - dlg.setWindowTitle("下载链接") - dlg.resize(700, 140) - v = QtWidgets.QVBoxLayout(dlg) - te = QtWidgets.QTextEdit() - te.setReadOnly(True) - te.setPlainText(url) - v.addWidget(te) - h = QtWidgets.QHBoxLayout() - btn_copy = QtWidgets.QPushButton("复制到剪贴板") - btn_copy.clicked.connect(lambda: QtWidgets.QApplication.clipboard().setText(url)) - btn_close = QtWidgets.QPushButton("关闭") - btn_close.clicked.connect(dlg.accept) - h.addStretch() - h.addWidget(btn_copy) - h.addWidget(btn_close) - v.addLayout(h) - dlg.exec() - - def on_upload(self): - if not self.pan: - QtWidgets.QMessageBox.information(self, "提示", "请先登录。") - return - path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "选择要上传的文件", os.path.expanduser("~")) - if not path: - return - fname = os.path.basename(path) - file_size = os.path.getsize(path) - same = [i for i in self.pan.list if i.get("FileName") == fname] - dup_choice = 1 - if same: - text, ok = QtWidgets.QInputDialog.getText(self, "同名文件", "检测到同名文件,输入行为:1 覆盖;2 保留两者;0 取消(默认1)", text="1") - if not ok: - return - if text.strip() not in ("0", "1", "2"): - QtWidgets.QMessageBox.information(self, "提示", "无效的选择,已取消") - return - if text.strip() == "0": - return - dup_choice = int(text.strip()) - - # 添加传输任务 - task_id = self.add_transfer_task("上传", fname, file_size) - - task = ThreadedTask(self._task_upload_file, path, dup_choice, task_id) - - # 保存任务对象引用 - for i, t in enumerate(self.transfer_tasks): - if t["id"] == task_id: - self.transfer_tasks[i]["threaded_task"] = task - break - - self.active_tasks[task_id] = task - - def on_task_finished(tid): - if tid in self.active_tasks: - del self.active_tasks[tid] - - task.signals.progress.connect(lambda p, tid=task_id: ( - self.status.showMessage(f"上传进度: {p}%", 2000), - self.update_transfer_task(tid, p, "上传中") - )) - task.signals.result.connect(lambda r, tid=task_id: ( - self.status.showMessage("上传完成", 3000), - self.update_transfer_task(tid, 100, "已完成"), - self.refresh_file_list(reset_page=True), - on_task_finished(tid) - )) - task.signals.error.connect(lambda e, tid=task_id: ( - self._show_error("上传失败: " + e), - self.update_transfer_task(tid, 0, "失败"), - on_task_finished(tid) - )) - task.signals.finished.connect(lambda tid=task_id: on_task_finished(tid)) - self.threadpool.start(task) - - def _task_upload_file(self, file_path, dup_choice, task_id, signals=None, task=None): - file_path = file_path.replace('"', "").replace("\\", "/") - file_name = os.path.basename(file_path) - if not os.path.exists(file_path): - raise RuntimeError("文件不存在") - if os.path.isdir(file_path): - raise RuntimeError("不支持文件夹上传") - fsize = os.path.getsize(file_path) - - # 检查是否被取消 - if task and task.is_cancelled: - return "已取消" - - md5 = hashlib.md5() - with open(file_path, "rb") as f: - while True: - data = f.read(64 * 1024) - if not data: - break - md5.update(data) - # 检查是否被取消 - if task and task.is_cancelled: - return "已取消" - readable_hash = md5.hexdigest() - - # 检查是否被取消 - if task and task.is_cancelled: - return "已取消" - list_up_request = { - "driveId": 0, - "etag": readable_hash, - "fileName": file_name, - "parentFileId": self.pan.parent_file_id, - "size": fsize, - "type": 0, - "duplicate": 0, - } - url = "https://www.123pan.com/b/api/file/upload_request" - headers = self.pan.header_logined.copy() - res = requests.post(url, headers=headers, data=list_up_request, timeout=30) - res_json = res.json() - code = res_json.get("code", -1) - if code == 5060: - list_up_request["duplicate"] = dup_choice - res = requests.post(url, headers=headers, data=json.dumps(list_up_request), timeout=30) - res_json = res.json() - code = res_json.get("code", -1) - if code != 0: - raise RuntimeError("上传请求失败: " + json.dumps(res_json, ensure_ascii=False)) - data = res_json["data"] - if data.get("Reuse"): - return "复用上传成功" - bucket = data["Bucket"] - storage_node = data["StorageNode"] - upload_key = data["Key"] - upload_id = data["UploadId"] - up_file_id = data["FileId"] - block_size = 5242880 - total_sent = 0 - part_number = 1 - with open(file_path, "rb") as f: - while True: - block = f.read(block_size) - if not block: - break - get_link_data = { - "bucket": bucket, - "key": upload_key, - "partNumberEnd": part_number + 1, - "partNumberStart": part_number, - "uploadId": upload_id, - "StorageNode": storage_node, - } - get_link_url = "https://www.123pan.com/b/api/file/s3_repare_upload_parts_batch" - get_link_res = requests.post(get_link_url, headers=headers, data=json.dumps(get_link_data), timeout=30) - get_link_res_json = get_link_res.json() - if get_link_res_json.get("code", -1) != 0: - raise RuntimeError("获取上传链接失败: " + json.dumps(get_link_res_json, ensure_ascii=False)) - upload_url = get_link_res_json["data"]["presignedUrls"][str(part_number)] - requests.put(upload_url, data=block, timeout=60) - total_sent += len(block) - if signals and fsize: - signals.progress.emit(int(total_sent * 100 / fsize)) - part_number += 1 - uploaded_list_url = "https://www.123pan.com/b/api/file/s3_list_upload_parts" - uploaded_comp_data = {"bucket": bucket, "key": upload_key, "uploadId": upload_id, "storageNode": storage_node} - requests.post(uploaded_list_url, headers=headers, data=json.dumps(uploaded_comp_data), timeout=30) - compmultipart_up_url = "https://www.123pan.com/b/api/file/s3_complete_multipart_upload" - requests.post(compmultipart_up_url, headers=headers, data=json.dumps(uploaded_comp_data), timeout=30) - if fsize > 64 * 1024 * 1024: - time.sleep(3) - close_up_session_url = "https://www.123pan.com/b/api/file/upload_complete" - close_up_session_data = {"fileId": up_file_id} - close_res = requests.post(close_up_session_url, headers=headers, data=json.dumps(close_up_session_data), timeout=30) - cr = close_res.json() - if cr.get("code", -1) != 0: - raise RuntimeError("上传完成确认失败: " + json.dumps(cr, ensure_ascii=False)) - return up_file_id - - def on_mkdir(self): - if not self.pan: - QtWidgets.QMessageBox.information(self, "提示", "请先登录。") - return - name, ok = QtWidgets.QInputDialog.getText(self, "新建文件夹", "请输入文件夹名称:") - if not ok or not name.strip(): - return - res = self.pan.mkdir(name.strip(), remakedir=False) - self.status.showMessage("创建完成", 3000) - self.refresh_file_list(reset_page=True) - - def on_delete(self): - file_index, file_detail = self.get_selected_detail() - if file_detail is None: - return - r = QtWidgets.QMessageBox.question(self, "删除确认", f"确认将 '{file_detail['FileName']}' 删除?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) - if r == QtWidgets.QMessageBox.StandardButton.No: - return - try: - self.pan.delete_file(file_index, by_num=True, operation=True) - self.status.showMessage("删除请求已发送", 3000) - self.refresh_file_list(reset_page=True) - except Exception as e: - self._show_error("删除失败: " + str(e)) - - def on_share(self): - file_index, file_detail = self.get_selected_detail() - if file_detail is None: - return - pwd, ok = QtWidgets.QInputDialog.getText(self, "分享", "提取码(留空则没有提取码):") - if not ok: - return - file_id_list = str(file_detail["FileId"]) - data = { - "driveId": 0, - "expiration": "2099-12-12T08:00:00+08:00", - "fileIdList": file_id_list, - "shareName": "123云盘分享", - "sharePwd": pwd or "", - "event": "shareCreate" - } - headers = self.pan.header_logined.copy() - try: - r = requests.post("https://www.123pan.com/a/api/share/create", headers=headers, data=json.dumps(data), timeout=30) - jr = r.json() - if jr.get("code", -1) != 0: - self._show_error("分享失败: " + jr.get("message", str(jr))) - return - share_key = jr["data"]["ShareKey"] - share_url = "https://www.123pan.com/s/" + share_key - QtWidgets.QMessageBox.information(self, "分享链接", f"{share_url}\n提取码:{pwd or '(无)'}") - except Exception as e: - self._show_error("分享异常: " + str(e)) - - def _show_error(self, msg): - QtWidgets.QMessageBox.critical(self, "错误", msg) - self.status.showMessage(msg, 8000) +import sys +from PyQt6 import QtWidgets +from main_window import MainWindow - def closeEvent(self, event): - try: - if self.pan and getattr(self.pan, "user_name", "") and getattr(self.pan, "password", ""): - self.pan.save_file() - except Exception: - pass - event.accept() def main(): app = QtWidgets.QApplication(sys.argv) - w = MainWindow() - w.show() + window = MainWindow() + window.show() sys.exit(app.exec()) -if __name__ == "__main__": - main() +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/api.py b/src/api.py new file mode 100644 index 0000000..5e96f82 --- /dev/null +++ b/src/api.py @@ -0,0 +1,752 @@ +# https://github.com/123panNextGen/123pan +# src/api.py + +import os +import json +import hashlib +import requests +import time +import random +import re +import uuid +from log import get_logger +from config import ConfigManager + +logger = get_logger(__name__) + + +class Pan123: + """123云盘API客户端类""" + + def __init__( + self, + readfile=True, + user_name="", + pass_word="", + authorization="", + input_pwd=False, + ): + + self.all_device_type = [ + "MI-ONE PLUS", "MI-ONE C1", "MI-ONE", "2012051", "2012053", "2012052", "2012061", "2012062", "2013012", + "2013021", "2012121", "2013061", "2013062", "2013063", "2014215", "2014218", "2014216", "2014719", + "2014716", "2014726", "2015015", "2015561", "2015562", "2015911", "2015201", "2015628", "2015105", + "2015711", "2016070", "2016089", "MDE2", "MDT2", "MCE16", "MCT1", "M1804D2SE", "M1804D2ST", "M1804D2SC", + "M1803E1A", "M1803E1T", "M1803E1C", "M1807E8S", "M1807E8A", "M1805E2A", "M1808D2TE", "M1808D2TT", + "M1808D2TC", "M1808D2TG", "M1902F1A", "M1902F1T", "M1902F1C", "M1902F1G", "M1908F1XE", "M1903F2A", + "M1903F2G", "M1903F10G", "M1903F11G", "M1904F3BG", "M2001J2E", "M2001J2G", "M2001J2I", "M2001J1E", + "M2001J1G", "M2002J9E", "M2002J9G", "M2002J9S", "M2002J9R", "M2007J1SC", "M2007J3SY", "M2007J3SP", + "M2007J3SG", "M2007J3SI", "M2007J17G", "M2007J17I", "M2102J2SC", "M2011K2C", "M2011K2G", "M2102K1AC", + "M2102K1C", "M2102K1G", "M2101K9C", "M2101K9G", "M2101K9R", "M2101K9AG", "M2101K9AI", "2107119DC", + "2109119DG", "2109119DI", "M2012K11G", "M2012K11AI", "M2012K11I", "21081111RG", "2107113SG", "2107113SI", + "2107113SR", "21091116I", "21091116UI", "2201123C", "2201123G", "2112123AC", "2112123AG", "2201122C", + "2201122G", "2207122MC", "2203129G", "2203129I", "2206123SC", "2206122SC", "2203121C", "22071212AG", + "22081212UG", "22081212R", "A201XM", "2211133C", "2211133G", "2210132C", "2210132G", "2304FPN6DC", + "2304FPN6DG", "2210129SG", "2306EPN60G", "2306EPN60R", "XIG04", "23078PND5G", "23088PND5R", "A301XM", + "23127PN0CC", "23127PN0CG", "23116PN5BC", "2311BPN23C", "24031PN0DC", "24030PN60G", "24053PY09I", + "2406APNFAG", "XIG06", "2407FPN8EG", "2407FPN8ER", "A402XM", "2014616", "2014619", "2014618", "2014617", + "2015011", "2015021", "2015022", "2015501", "2015211", "2015212", "2015213", "MCE8", "MCT8", "M1910F4G", + "M1910F4S", "M2002F4LG", "2016080", "MDE5", "MDT5", "MDE5S", "M1803D5XE", "M1803D5XA", "M1803D5XT", + "M1803D5XC", "M1810E5E", "M1810E5A", "M1810E5GG", "2106118C", "M2011J18C", "22061218C", "2308CPXD0C", + "24072PX77C", "2405CPX3DC", "2405CPX3DG", "2016001", "2016002", "2016007", "MDE40", "MDT4", "MDI40", + "M1804E4A", "M1804E4T", "M1804E4C", "M1904F3BC", "M1904F3BT", "M1906F9SC", "M1910F4E", "2109119BC", + "2109119BC", "2209129SC", "23046PNC9C", "24053PY09C", "M1901F9E", "M1901F9T", "MDG2", "MDI2", "M1804D2SG", + "M1804D2SI", "M1805D1SG", "M1906F9SH", "M1906F9SI", "A0101", "2015716", "MCE91", "M1806D9W", "M1806D9E", + "M1806D9PE", "21051182C", "21051182G", "M2105K81AC", "M2105K81C", "22081281AC", "23043RP34C", "23043RP34G", + "23043RP34I", "23046RP50C", "2307BRPDCC", "24018RPACC", "24018RPACG", "2013022", "2013023", "2013029", + "2013028", "2014011", "2014501", "2014813", "2014112", "2014811", "2014812", "2014821", "2014817", + "2014818", "2014819", "2014502", "2014512", "2014816", "2015811", "2015812", "2015810", "2015817", + "2015818", "2015816", "2016030", "2016031", "2016032", "2016037", "2016036", "2016035", "2016033", + "2016090", "2016060", "2016111", "2016112", "2016117", "2016116", "MAE136", "MAT136", "MAG138", "MAI132", + "MDE1", "MDT1", "MDG1", "MDI1", "MEE7", "MET7", "MEG7", "MCE3B", "MCT3B", "MCG3B", "MCI3B", "M1804C3DE", + "M1804C3DT", "M1804C3DC", "M1804C3DG", "M1804C3DI", "M1805D1SE", "M1805D1ST", "M1805D1SC", "M1805D1SI", + "M1804C3CE", "M1804C3CT", "M1804C3CC", "M1804C3CG", "M1804C3CI", "M1810F6LE", "M1810F6LT", "M1810F6LG", + "M1810F6LI", "M1903C3EE", "M1903C3ET", "M1903C3EC", "M1903C3EG", "M1903C3EI", "M1908C3IE", "M1908C3IC", + "M1908C3IG", "M1908C3II", "M1908C3KE", "M1908C3KG", "M1908C3KI", "M2001C3K3I", "M2004J19C", "M2004J19G", + "M2004J19I", "M2004J19AG", "M2006C3LC", "M2006C3LG", "M2006C3LVG", "M2006C3LI", "M2006C3LII", "M2006C3MG", + "M2006C3MT", "M2006C3MNG", "M2006C3MII", "M2010J19SG", "M2010J19SI", "M2010J19SR", "M2010J19ST", + "M2010J19SY", "M2010J19SL", "21061119AG", "21061119AL", "21061119BI", "21061119DG", "21121119SG", + "21121119VL", "22011119TI", "22011119UY", "22041219G", "22041219I", "22041219NY", "220333QAG", "220333QBI", + "220333QNY", "220333QL", "220233L2C", "220233L2G", "220233L2I", "22071219AI", "23053RN02A", "23053RN02I", + "23053RN02L", "23053RN02Y", "23077RABDC", "23076RN8DY", "23076RA4BR", "XIG03", "A401XM", "23076RN4BI", + "23076RA4BC", "22120RN86C", "22120RN86G", "22120RN86H", "2212ARNC4L", "22126RN91Y", "2404ARN45A", + "2404ARN45I", "24049RN28L", "24040RN64Y", "2406ERN9CI", "23106RN0DA", "2311DRN14I", "23100RN82L", + "23108RN04Y", "23124RN87C", "23124RN87I", "23124RN87G", "2409BRN2CA", "2409BRN2CI", "2409BRN2CL", + "2409BRN2CY", "2411DRN47C", "2014018", "2013121", "2014017", "2013122", "2014022", "2014021", "2014715", + "2014712", "2014915", "2014912", "2014916", "2014911", "2014910", "2015052", "2015051", "2015712", + "2015055", "2015056", "2015617", "2015611", "2015112", "2015116", "2015161", "2016050", "2016051", + "2016101", "2016130", "2016100", "MBE6A5", "MBT6A5", "MEI7", "MEE7S", "MET7S", "MEC7S", "M1803E7SG", + "MEI7S", "MDE6", "MDT6", "MDG6", "MDI6", "MDE6S", "MDT6S", "MDG6S", "MDI6S", "M1806E7TG", "M1806E7TI", + "M1901F7E", "M1901F7T", "M1901F7C", "M1901F7G", "M1901F7I", "M1901F7BE", "M1901F7S", "M1908C3JE", + "M1908C3JC", "M1908C3JG", "M1908C3JI", "M1908C3XG", "M1908C3JGG", "M1906G7E", "M1906G7T", "M1906G7G", + "M1906G7I", "M2010J19SC", "M2007J22C", "M2003J15SS", "M2003J15SI", "M2003J15SG", "M2007J22G", "M2007J22R", + "M2007J17C", "M2003J6A1G", "M2003J6A1R", "M2003J6A1I", "M2003J6B1I", "M2003J6B2G", "M2101K7AG", "M2101K7AI", + "M2101K7BG", "M2101K7BI", "M2101K7BNY", "M2101K7BL", "M2103K19C", "M2103K19I", "M2103K19G", "M2103K19Y", + "M2104K19J", "22021119KR", "A101XM", "M2101K6G", "M2101K6T", "M2101K6R", "M2101K6P", "M2101K6I", + "M2104K10AC", "2109106A1I", "21121119SC", "2201117TG", "2201117TI", "2201117TL", "2201117TY", "21091116AC", + "21091116AI", "22041219C", "2201117SG", "2201117SI", "2201117SL", "2201117SY", "22087RA4DI", "22031116BG", + "21091116C", "2201116TG", "2201116TI", "2201116SC", "2201116SG", "2201116SR", "2201116SI", "21091116UC", + "21091116UG", "22041216C", "22041216UC", "22095RA98C", "23021RAAEG", "23027RAD4I", "23028RA60L", + "23021RAA2Y", "22101317C", "22111317G", "22111317I", "23076RA4BC", "2303CRA44A", "2303ERA42L", "23030RAC7Y", + "2209116AG", "22101316C", "22101316G", "22101316I", "22101316UCP", "22101316UG", "22101316UP", "22101316UC", + "22101320C", "23054RA19C", "23049RAD8C", "23129RAA4G", "23129RA5FL", "23124RA7EO", "2312DRAABC", + "2312DRAABI", "2312DRAABG", "23117RA68G", "2312DRA50C", "2312DRA50G", "2312DRA50I", "XIG05", "23090RA98C", + "23090RA98G", "23090RA98I", "24040RA98R", "2406ERN9CC", "2311FRAFDC", "24094RAD4C", "24094RAD4G", + "24094RAD4I", "24090RA29C", "24090RA29G", "24090RA29I", "24115RA8EC", "24115RA8EG", "24115RA8EI", + "M2004J7AC", "M2004J7BC", "M2003J15SC", "24069RA21C", "M1903F10A", "M1903F10C", "M1903F10I", "M1903F11A", + "M1903F11C", "M1903F11I", "M1903F11A", "M2001G7AE", "M2001G7AC", "M2001G7AC", "M1912G7BE", "M1912G7BC", + "M2001J11C", "M2001J11C", "M2006J10C", "M2007J3SC", "M2012K11AC", "M2012K11C", "M2012K10C", "22021211RC", + "22041211AC", "22011211C", "21121210C", "22081212C", "22041216I", "23013RK75C", "22127RK46C", "22122RK93C", + "23078RKD5C", "23113RKC6C", "23117RK66C", "2311DRK48C", "2407FRK8EC", "2016020", "2016021", "M1803E6E", + "M1803E6T", "M1803E6C", "M1803E6G", "M1803E6I", "M1810F6G", "M1810F6I", "M1903C3GG", "M1903C3GI", + "220733SG", "220733SH", "220733SL", "220733SFG", "220733SFH", "23028RN4DG", "23028RN4DH", "23026RN54G", + "23028RNCAG", "23028RNCAH", "23129RN51X", "23129RN51H", "2312CRNCCL", "24048RN6CG", "24048RN6CI", + "24044RN32L", "2409BRN2CG", "22081283C", "22081283G", "23073RPBFC", "23073RPBFG", "23073RPBFL", + "2405CRPFDC", "2405CRPFDG", "2405CRPFDI", "2405CRPFDL", "24074RPD2C", "24074RPD2G", "24074RPD2I", + "24075RP89G", "24076RP19G", "24076RP19I", "M1805E10A", "M2004J11G", "M2012K11AG", "M2104K10I", "22021211RG", + "22021211RI", "21121210G", "23049PCD8G", "23049PCD8I", "23013PC75G", "24069PC21G", "24069PC21I", + "23113RKC6G", "M1912G7BI", "M2007J20CI", "M2007J20CG", "M2007J20CT", "M2102J20SG", "M2102J20SI", + "21061110AG", "2201116PG", "2201116PI", "22041216G", "22041216UG", "22111317PG", "22111317PI", "22101320G", + "22101320I", "23122PCD1G", "23122PCD1I", "2311DRK48G", "2311DRK48I", "2312FRAFDI", "M2004J19PI", + "M2003J6CI", "M2010J19CG", "M2010J19CT", "M2010J19CI", "M2103K19PG", "M2103K19PI", "22041219PG", + "22041219PI", "2201117PG", "2201117PI", "21091116AG", "22031116AI", "22071219CG", "22071219CI", + "2207117BPG", "2404APC5FG", "2404APC5FI", "23128PC33I", "24066PC95I", "2312FPCA6G", "23076PC4BI", + "M2006C3MI", "211033MI", "220333QPG", "220333QPI", "220733SPH", "2305EPCC4G", "2302EPCC4H", "22127PC95G", + "22127PC95H", "2312BPC51X", "2312BPC51H", "2310FPCA4G", "2310FPCA4I", "2405CPCFBG", "24074PCD2I", "FYJ01QP", + "21051191C" + ] + self.all_os_versions = [ + "Android_7.1.2", "Android_8.0.0", "Android_8.1.0", "Android_9.0", "Android_10", "Android_11", "Android_12", + "Android_13", "Android_6.0.1", "Android_5.1.1", "Android_4.4.4", "Android_4.3", "Android_4.2.2", + "Android_4.1.2", + ] + # 随机生成设备信息 + self.devicetype = random.choice(self.all_device_type) + self.osversion = random.choice(self.all_os_versions) + + self.cookies = None + self.recycle_list = None + self.list = [] + self.total = 0 + self.parent_file_name_list = [] + self.all_file = False + self.file_page = 0 + self.file_list = [] + self.dir_list = [] + self.name_dict = {} + if readfile: + self.read_ini(user_name, pass_word, input_pwd, authorization) + else: + if user_name == "" or pass_word == "": + raise Exception("用户名或密码为空") + self.user_name = user_name + self.password = pass_word + self.authorization = authorization + self.header_logined = { + "user-agent": "123pan/v2.4.0(" + self.osversion + ";Xiaomi)", + "authorization": self.authorization, + "accept-encoding": "gzip", + "content-type": "application/json", + "osversion": self.osversion, + "loginuuid": str(uuid.uuid4().hex), + "platform": "android", + "devicetype": self.devicetype, + "devicename": "Xiaomi", + "host": "www.123pan.com", + "app-version": "61", + "x-app-version": "2.4.0" + } + self.parent_file_id = 0 # 路径,文件夹的id,0为根目录 + self.parent_file_list = [0] + res_code_getdir = self.get_dir()[0] + if res_code_getdir != 0: + self.login() + self.get_dir() + + def login(self): + """登录123云盘账户并获取授权令牌""" + data = {"type": 1, "passport": self.user_name, "password": self.password} + login_res = requests.post( + "https://www.123pan.com/b/api/user/sign_in", + headers=self.header_logined, + data=data, + ) + + res_sign = login_res.json() + res_code_login = res_sign["code"] + if res_code_login != 200: + logger.error("code = 1 Error:" + str(res_code_login)) + logger.error(res_sign.get("message", "")) + return res_code_login + set_cookies = login_res.headers.get("Set-Cookie", "") + set_cookies_list = {} + + for cookie in set_cookies.split(';'): + if '=' in cookie: + key, value = cookie.strip().split('=', 1) + set_cookies_list[key] = value + else: + set_cookies_list[cookie.strip()] = None + + self.cookies = set_cookies_list + + token = res_sign["data"]["token"] + self.authorization = "Bearer " + token + self.header_logined["authorization"] = self.authorization + self.save_file() + return res_code_login + + def save_file(self): + """将账户信息保存到配置文件""" + try: + config = ConfigManager.load_config() + config.update({ + "userName": self.user_name, + "passWord": self.password, + "authorization": self.authorization, + "deviceType": self.devicetype, + "osVersion": self.osversion, + }) + ConfigManager.save_config(config) + logger.info("账号已保存") + except Exception as e: + logger.error("保存账号失败:", e) + + def get_dir(self, save=True): + """获取当前目录下的文件列表""" + return self.get_dir_by_id(self.parent_file_id, save) + + def get_dir_by_id(self, file_id, save=True, all=False, limit=100): + """按文件夹ID获取文件列表(支持分页) + + Args: + file_id: 文件夹ID + save: 是否保存结果到列表 + all: 是否强制获取所有文件 + limit: 每页限制数量 + """ + get_pages = 3 + res_code_getdir = 0 + page = self.file_page * get_pages + 1 + lenth_now = len(self.list) + if all: + # 强制获取所有文件 + page = 1 + lenth_now = 0 + lists = [] + + total = -1 + times = 0 + while (lenth_now < total or total == -1) and (times < get_pages or all): + base_url = "https://www.123pan.com/api/file/list/new" + params = { + "driveId": 0, + "limit": limit, + "next": 0, + "orderBy": "file_id", + "orderDirection": "desc", + "parentFileId": str(file_id), + "trashed": False, + "SearchData": "", + "Page": str(page), + "OnlyLookAbnormalFile": 0, + } + try: + a = requests.get(base_url, headers=self.header_logined, params=params, timeout=30) + except Exception: + logger.error("连接失败") + return -1, [] + text = a.json() + res_code_getdir = text["code"] + if res_code_getdir != 0: + logger.error("code = 2 Error:" + str(res_code_getdir)) + logger.error(text.get("message", "")) + return res_code_getdir, [] + lists_page = text["data"]["InfoList"] + lists += lists_page + total = text["data"]["Total"] + lenth_now += len(lists_page) + page += 1 + times += 1 + if times % 5 == 0: + logger.warning("警告:文件夹内文件过多:" + str(lenth_now) + "/" + str(total)) + logger.info("为防止对服务器造成影响,暂停3秒") + time.sleep(3) + + if lenth_now < total: + logger.warning("文件夹内文件过多:" + str(lenth_now) + "/" + str(total)) + self.all_file = False + else: + self.all_file = True + self.total = total + self.file_page += 1 + if save: + self.list = self.list + lists + + return res_code_getdir, lists + + def show(self): + """显示文件列表信息到日志""" + if not self.all_file: + logger.info(f"获取了{len(self.list)}/{self.total}个文件") + else: + logger.info(f"获取全部{len(self.list)}个文件") + + def link_by_number(self, file_number, showlink=True): + """按编号获取文件下载链接""" + file_detail = self.list[file_number] + return self.link_by_fileDetail(file_detail, showlink) + + def link_by_fileDetail(self, file_detail, showlink=True): + """按文件详情获取下载链接""" + type_detail = file_detail["Type"] + + if type_detail == 1: + down_request_url = "https://www.123pan.com/a/api/file/batch_download_info" + down_request_data = {"fileIdList": [{"fileId": int(file_detail["FileId"])}]} + + else: + down_request_url = "https://www.123pan.com/a/api/file/download_info" + down_request_data = { + "driveId": 0, + "etag": file_detail["Etag"], + "fileId": file_detail["FileId"], + "s3keyFlag": file_detail["S3KeyFlag"], + "type": file_detail["Type"], + "fileName": file_detail["FileName"], + "size": file_detail["Size"], + } + + link_res = requests.post( + down_request_url, + headers=self.header_logined, + data=json.dumps(down_request_data), + timeout=10 + ) + link_res_json = link_res.json() + res_code_download = link_res_json["code"] + if res_code_download != 0: + logger.error("获取下载链接失败,返回码: " + str(res_code_download)) + logger.error(link_res_json.get("message", "")) + return res_code_download + down_load_url = link_res.json()["data"]["DownloadUrl"] + next_to_get = requests.get(down_load_url, timeout=10, allow_redirects=False).text + url_pattern = re.compile(r"href='(https?://[^']+)'") + redirect_url = url_pattern.findall(next_to_get)[0] + if showlink: + logger.info(f"获取下载链接成功: {redirect_url}") + + return redirect_url + + def download(self, file_number, download_path="download"): + """下载文件""" + file_detail = self.list[file_number] + if file_detail["Type"] == 1: + logger.info("开始下载") + file_name = file_detail["FileName"] + ".zip" + else: + file_name = file_detail["FileName"] # 文件名 + + down_load_url = self.link_by_number(file_number, showlink=False) + if type(down_load_url) == int: + return + self.download_from_url(down_load_url, file_name, download_path) + + def download_from_url(self, url, file_name, download_path="download"): + """从URL下载文件""" + if not os.path.exists(download_path): + logger.info("创建下载目录") + os.makedirs(download_path) + + file_path = os.path.join(download_path, file_name) + temp_path = file_path + ".123pan" + + # 如果临时文件存在,删除它(防止之前的不完整下载) + if os.path.exists(temp_path): + os.remove(temp_path) + + down = requests.get(url, stream=True, timeout=10) + file_size = int(down.headers.get("Content-Length", 0) or 0) + + # 以.123pan后缀下载,下载完成重命名,防止下载中断 + with open(temp_path, "wb") as f: + for chunk in down.iter_content(8192): + if chunk: + f.write(chunk) + + os.rename(temp_path, file_path) + + def get_all_things(self, id): + """获取文件夹内所有内容""" + self.dir_list.remove(id) + all_list = self.get_dir_by_id(id, save=False)[1] + + for i in all_list: + if i["Type"] == 0: + self.file_list.append(i) + else: + self.dir_list.append(i["FileId"]) + self.name_dict[i["FileId"]] = i["FileName"] + + for i in self.dir_list: + self.get_all_things(i) + + def download_dir(self, file_detail, download_path_root="download"): + """下载文件夹""" + self.name_dict[file_detail["FileId"]] = file_detail["FileName"] + if file_detail["Type"] != 1: + logger.warning("不是文件夹") + return + + all_list = self.get_dir_by_id(file_detail["FileId"], save=False, all=True, limit=100)[1] + for i in all_list[::-1]: + if i["Type"] == 0: # 直接开始下载 + AbsPath = i["AbsPath"] + for key, value in self.name_dict.items(): + AbsPath = AbsPath.replace(str(key), value) + download_path = download_path_root + AbsPath + download_path = download_path.replace("/" + str(i["FileId"]), "") + self.download_from_url(i["DownloadUrl"], i["FileName"], download_path) + + else: + self.download_dir(i, download_path_root) + + def recycle(self): + """获取回收站列表""" + recycle_id = 0 + url = ( + "https://www.123pan.com/a/api/file/list/new?driveId=0&limit=100&next=0" + "&orderBy=fileId&orderDirection=desc&parentFileId=" + + str(recycle_id) + + "&trashed=true&&Page=1" + ) + recycle_res = requests.get(url, headers=self.header_logined, timeout=10) + json_recycle = recycle_res.json() + recycle_list = json_recycle["data"]["InfoList"] + self.recycle_list = recycle_list + + def delete_file(self, file, by_num=True, operation=True): + """删除或恢复文件""" + if by_num: + if not str(file).isdigit(): + raise ValueError("文件索引必须是数字") + if 0 <= file < len(self.list): + file_detail = self.list[file] + else: + raise IndexError("文件索引超出范围") + else: + if file in self.list: + file_detail = file + else: + raise ValueError("文件不存在") + data_delete = { + "driveId": 0, + "fileTrashInfoList": file_detail, + "operation": operation, + } + delete_res = requests.post( + "https://www.123pan.com/a/api/file/trash", + data=json.dumps(data_delete), + headers=self.header_logined, + timeout=10 + ) + dele_json = delete_res.json() + print(dele_json) + message = dele_json.get("message", "") + print(message) + + def share(self, file_id_list, share_pwd=""): + """分享文件""" + if not file_id_list: + raise ValueError("文件ID列表为空") + data = { + "driveId": 0, + "expiration": "2099-12-12T08:00:00+08:00", + "fileIdList": file_id_list, + "shareName": "123云盘分享", + "sharePwd": share_pwd or "", + "event": "shareCreate" + } + share_res = requests.post( + "https://www.123pan.com/a/api/share/create", + headers=self.header_logined, + data=json.dumps(data), + timeout=10 + ) + share_res_json = share_res.json() + if share_res_json.get("code", -1) != 0: + raise RuntimeError(f"分享失败: {share_res_json.get('message', '')}") + share_key = share_res_json["data"]["ShareKey"] + share_url = "https://www.123pan.com/s/" + share_key + return share_url + + def up_load(self, file_path): + """上传文件""" + file_path = file_path.replace('"', "").replace("\\", "/") + file_name = os.path.basename(file_path) + if not os.path.exists(file_path): + raise FileNotFoundError("文件不存在") + if os.path.isdir(file_path): + raise IsADirectoryError("不支持文件夹上传") + fsize = os.path.getsize(file_path) + readable_hash = self._compute_file_md5(file_path) + + list_up_request = { + "driveId": 0, + "etag": readable_hash, + "fileName": file_name, + "parentFileId": self.parent_file_id, + "size": fsize, + "type": 0, + "duplicate": 0, + } + + up_res = requests.post( + "https://www.123pan.com/b/api/file/upload_request", + headers=self.header_logined, + data=list_up_request, + timeout=10 + ) + up_res_json = up_res.json() + res_code_up = up_res_json.get("code", -1) + if res_code_up == 5060: + # 同名文件处理由调用者在GUI中处理 + raise RuntimeError("同名文件存在") + if res_code_up != 0: + raise RuntimeError(f"上传请求失败: {up_res_json}") + if up_res_json["data"].get("Reuse", False): + return up_file_id + + bucket = up_res_json["data"]["Bucket"] + storage_node = up_res_json["data"]["StorageNode"] + upload_key = up_res_json["data"]["Key"] + upload_id = up_res_json["data"]["UploadId"] + up_file_id = up_res_json["data"]["FileId"] # 上传文件的fileId,完成上传后需要用到 + + # 获取已将上传的分块 + start_data = { + "bucket": bucket, + "key": upload_key, + "uploadId": upload_id, + "storageNode": storage_node, + } + start_res = requests.post( + "https://www.123pan.com/b/api/file/s3_list_upload_parts", + headers=self.header_logined, + data=json.dumps(start_data), + timeout=10 + ) + start_res_json = start_res.json() + res_code_up = start_res_json.get("code", -1) + if res_code_up != 0: + raise RuntimeError(f"获取传输列表失败: {start_res_json}") + + # 分块,每一块取一次链接,依次上传 + block_size = 5242880 + with open(file_path, "rb") as f: + part_number_start = 1 + put_size = 0 + while True: + data = f.read(block_size) + put_size = put_size + len(data) + + if not data: + break + get_link_data = { + "bucket": bucket, + "key": upload_key, + "partNumberEnd": part_number_start + 1, + "partNumberStart": part_number_start, + "uploadId": upload_id, + "StorageNode": storage_node, + } + + get_link_url = ( + "https://www.123pan.com/b/api/file/s3_repare_upload_parts_batch" + ) + get_link_res = requests.post( + get_link_url, + headers=self.header_logined, + data=json.dumps(get_link_data), + timeout=10 + ) + get_link_res_json = get_link_res.json() + res_code_up = get_link_res_json.get("code", -1) + if res_code_up != 0: + raise RuntimeError(f"获取链接失败: {get_link_res_json}") + upload_url = get_link_res_json["data"]["presignedUrls"][ + str(part_number_start) + ] + requests.put(upload_url, data=data, timeout=10) + + part_number_start = part_number_start + 1 + + + uploaded_list_url = "https://www.123pan.com/b/api/file/s3_list_upload_parts" + uploaded_comp_data = { + "bucket": bucket, + "key": upload_key, + "uploadId": upload_id, + "storageNode": storage_node, + } + requests.post( + uploaded_list_url, + headers=self.header_logined, + data=json.dumps(uploaded_comp_data), + timeout=10 + ) + compmultipart_up_url = ( + "https://www.123pan.com/b/api/file/s3_complete_multipart_upload" + ) + requests.post( + compmultipart_up_url, + headers=self.header_logined, + data=json.dumps(uploaded_comp_data), + timeout=10 + ) + + if fsize > 64 * 1024 * 1024: + time.sleep(3) + close_up_session_url = "https://www.123pan.com/b/api/file/upload_complete" + close_up_session_data = {"fileId": up_file_id} + close_up_session_res = requests.post( + close_up_session_url, + headers=self.header_logined, + data=json.dumps(close_up_session_data), + timeout=10 + ) + close_res_json = close_up_session_res.json() + res_code_up = close_res_json.get("code", -1) + if res_code_up != 0: + raise RuntimeError(f"上传完成确认失败: {close_res_json}") + return up_file_id + + def cd(self, dir_num): + """进入文件夹""" + if dir_num == "..": + if len(self.parent_file_list) > 1: + self.all_file = False + self.file_page = 0 + self.parent_file_list.pop() + self.parent_file_id = self.parent_file_list[-1] + self.list = [] + self.parent_file_name_list.pop() + self.get_dir() + else: + raise RuntimeError("已经是根目录") + return + if dir_num == "/": + self.all_file = False + self.file_page = 0 + self.parent_file_id = 0 + self.parent_file_list = [0] + self.list = [] + self.parent_file_name_list = [] + self.get_dir() + return + if not str(dir_num).isdigit(): + raise ValueError("文件夹编号必须是数字") + dir_num = int(dir_num) - 1 + if dir_num > (len(self.list) - 1) or dir_num < 0: + raise IndexError("文件夹编号超出范围") + if self.list[dir_num]["Type"] != 1: + raise TypeError("选中项不是文件夹") + self.all_file = False + self.file_page = 0 + self.parent_file_id = self.list[dir_num]["FileId"] + self.parent_file_list.append(self.parent_file_id) + self.parent_file_name_list.append(self.list[dir_num]["FileName"]) + self.list = [] + self.get_dir() + + def cdById(self, file_id): + """按ID进入文件夹""" + self.all_file = False + self.file_page = 0 + self.list = [] + self.parent_file_id = file_id + self.parent_file_list.append(self.parent_file_id) + self.get_dir() + self.show() + + def read_ini( + self, + user_name, + pass_word, + input_pwd, + authorization="", + ): + """从配置文件读取账号信息""" + try: + config = ConfigManager.load_config() + deviceType = config.get("deviceType", "") + osVersion = config.get("osVersion", "") + if deviceType: + self.devicetype = deviceType + if osVersion: + self.osversion = osVersion + user_name = config.get("userName", user_name) + pass_word = config.get("passWord", pass_word) + authorization = config.get("authorization", authorization) + except Exception as e: + logger.error(f"获取配置失败: {e}") + if user_name == "" or pass_word == "": + raise Exception("无法从配置获取账号信息") + + self.user_name = user_name + self.password = pass_word + self.authorization = authorization + + def mkdir(self, dirname, remakedir=False): + """创建文件夹""" + if not remakedir: + for i in self.list: + if i["FileName"] == dirname: + logger.info("文件夹已存在") + return i["FileId"] + + url = "https://www.123pan.com/a/api/file/upload_request" + data_mk = { + "driveId": 0, + "etag": "", + "fileName": dirname, + "parentFileId": self.parent_file_id, + "size": 0, + "type": 1, + "duplicate": 1, + "NotReuse": True, + "event": "newCreateFolder", + "operateType": 1, + } + res_mk = requests.post( + url, + headers=self.header_logined, + data=json.dumps(data_mk), + timeout=10 + ) + try: + res_json = res_mk.json() + except json.decoder.JSONDecodeError: + logger.error("创建失败") + logger.error(res_mk.text) + return + code_mkdir = res_json.get("code", -1) + + if code_mkdir == 0: + logger.info(f"创建成功: {res_json['data']['FileId']}") + self.get_dir() + return res_json["data"]["Info"]["FileId"] + logger.error(f"创建失败: {res_json}") + return + + @staticmethod + def _compute_file_md5(file_path): + """计算文件MD5值""" + md5 = hashlib.md5() + with open(file_path, "rb") as f: + while True: + data = f.read(64 * 1024) + if not data: + break + md5.update(data) + return md5.hexdigest() diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..1f35f3a --- /dev/null +++ b/src/config.py @@ -0,0 +1,73 @@ +# https://github.com/123panNextGen/123pan +# src/config.py + +import os +import json +import platform +from log import get_logger + +logger = get_logger(__name__) + +# 配置文件路径 +if platform.system() == 'Windows': + CONFIG_DIR = os.path.join(os.environ.get('APPDATA', ''), 'Qxyz17', '123pan') +else: + CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.config', 'Qxyz17', '123pan') +CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.json') + + +class ConfigManager: + """配置管理类""" + + @staticmethod + def ensure_config_dir(): + """确保配置目录存在""" + if not os.path.exists(CONFIG_DIR): + os.makedirs(CONFIG_DIR, exist_ok=True) + + @staticmethod + def load_config(): + """加载配置""" + ConfigManager.ensure_config_dir() + default_config = { + "userName": "", + "passWord": "", + "authorization": "", + "deviceType": "", + "osVersion": "", + "settings": { + "defaultDownloadPath": os.path.join(os.path.expanduser("~"), "Downloads"), + "askDownloadLocation": True + } + } + + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + # 确保新版本配置兼容性 + if "settings" not in config: + config["settings"] = default_config["settings"] + return config + except Exception as e: + logger.error(f"加载配置失败: {e}") + return default_config + return default_config + + @staticmethod + def save_config(config): + """保存配置""" + try: + ConfigManager.ensure_config_dir() + with open(CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + logger.error(f"保存配置失败: {e}") + return False + + @staticmethod + def get_setting(key, default=None): + """获取特定设置""" + config = ConfigManager.load_config() + return config.get("settings", {}).get(key, default) diff --git a/src/log.py b/src/log.py index 2bab00e..6a92107 100644 --- a/src/log.py +++ b/src/log.py @@ -1,3 +1,6 @@ +# https://github.com/123panNextGen/123pan +# src/log.py + import logging import platform import os @@ -33,13 +36,4 @@ def get_logger(name: str = "123pan"): logger.addHandler(file_handler) logger.addHandler(console_handler) - return logger - -''' -调用方法:在其他文件中 -from log import get_logger -logger = get_logger(__name__) -然后需要时(例子↓) -logger.info("xxx") -发布版本时把debug级别的那一行注释掉改为info的那一行 -''' \ No newline at end of file + return logger \ No newline at end of file diff --git a/src/main_window.py b/src/main_window.py new file mode 100644 index 0000000..a2e5845 --- /dev/null +++ b/src/main_window.py @@ -0,0 +1,1586 @@ +"""主窗口模块""" +from PyQt6 import QtCore, QtGui, QtWidgets +import os +import json +import hashlib +from log import get_logger +from config import ConfigManager +from ui_widgets import SidebarButton, LoginDialog, SettingsDialog +from api import Pan123 +from threading_utils import ThreadedTask + +logger = get_logger(__name__) + +class MainWindow(QtWidgets.QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("123云盘") + self.resize(980, 620) + self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint) + + self.pan = None + self.threadpool = QtCore.QThreadPool.globalInstance() + # 设置线程池的最大线程数,允许同时下载多个文件 + self.threadpool.setMaxThreadCount(64) + + # 应用123云盘主题 + self.apply_blue_white_theme() + + # 中央布局 + central = QtWidgets.QWidget() + self.setCentralWidget(central) + main_layout = QtWidgets.QHBoxLayout(central) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # 创建侧边栏 + self.sidebar = QtWidgets.QWidget() + self.sidebar.setMinimumWidth(200) + self.sidebar.setMaximumWidth(200) + self.sidebar.setStyleSheet( + "background-color: rgba(255, 255, 255, 0.95);" + "border-right: 1px solid rgba(0, 0, 0, 0.05);" + "border-radius: 0;" + ) + sidebar_layout = QtWidgets.QVBoxLayout(self.sidebar) + sidebar_layout.setContentsMargins(10, 20, 10, 10) + sidebar_layout.setSpacing(8) + sidebar_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + + # 侧边栏标题 + sidebar_title = QtWidgets.QLabel("功能菜单") + sidebar_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + sidebar_title.setStyleSheet( + "font-size: 20px; font-weight: bold; color: #334155; margin-bottom: 20px;" + "padding: 10px 0;" + ) + sidebar_layout.addWidget(sidebar_title) + + # 侧边栏按钮组 + self.sidebar_buttons = [] + self.sidebar_animations = {} + self.sidebar_original_geoms = {} + + # 文件页按钮 + self.btn_files = SidebarButton("📁 文件") + self.btn_files.setMinimumHeight(50) + self.btn_files.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: rgba(59, 130, 246, 0.9);" + "color: white; border-radius: 12px;" + "border: none;" + ) + sidebar_layout.addWidget(self.btn_files) + self.sidebar_buttons.append(self.btn_files) + + # 传输页按钮 + self.btn_transfer = SidebarButton("🔄 传输") + self.btn_transfer.setMinimumHeight(50) + self.btn_transfer.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: transparent; color: #334155;" + "border-radius: 12px;" + "border: none;" + ) + sidebar_layout.addWidget(self.btn_transfer) + self.sidebar_buttons.append(self.btn_transfer) + + # 为侧边栏按钮添加悬停和点击事件,实现动画效果 + for btn in self.sidebar_buttons: + btn.entered.connect(lambda b=btn: self.on_sidebar_button_hover(b)) + btn.left.connect(lambda b=btn: self.on_sidebar_button_leave(b)) + btn.pressed.connect(lambda b=btn: self.on_sidebar_button_pressed(b)) + btn.released.connect(lambda b=btn: self.on_sidebar_button_released(b)) + + # 保存按钮的原始位置 + QtCore.QTimer.singleShot(100, lambda b=btn: self.save_original_position(b)) + + sidebar_layout.addStretch() + main_layout.addWidget(self.sidebar) + + # 创建右侧内容区域 + right_content = QtWidgets.QWidget() + right_layout = QtWidgets.QVBoxLayout(right_content) + right_layout.setContentsMargins(10, 10, 10, 10) + right_layout.setSpacing(8) + + # 顶部横向按钮栏(左上角为设置按钮) + toolbar_h = QtWidgets.QHBoxLayout() + toolbar_h.setSpacing(6) + + # 设置按钮(左上角齿轮图标) + self.btn_settings = QtWidgets.QPushButton("⚙️") + self.btn_settings.setToolTip("设置") + self.btn_settings.setMinimumHeight(36) + self.btn_settings.setMinimumWidth(45) + self.btn_settings.setMaximumHeight(36) + self.btn_settings.setMaximumWidth(45) + self.btn_settings.setStyleSheet( + "font-size: 20px;" + "background-color: transparent;" + "border: none;" + "border-radius: 8px;" + ) + self.btn_settings.setObjectName("btn_settings") + toolbar_h.addWidget(self.btn_settings) + + # 操作按钮(横向排列) + self.btn_refresh = QtWidgets.QPushButton("刷新") + self.btn_more = QtWidgets.QPushButton("更多") + self.btn_up = QtWidgets.QPushButton("上级") + self.btn_delete = QtWidgets.QPushButton("删除") + self.btn_download = QtWidgets.QPushButton("下载") + self.btn_share = QtWidgets.QPushButton("分享") + self.btn_link = QtWidgets.QPushButton("显示链接") + self.btn_upload = QtWidgets.QPushButton("上传文件") + self.btn_mkdir = QtWidgets.QPushButton("新建文件夹") + + # 设置按钮最小宽度统一外观 + btns = [self.btn_refresh, self.btn_more, self.btn_up, self.btn_download, self.btn_link, + self.btn_upload, self.btn_mkdir, self.btn_delete, self.btn_share] + + # 为每个按钮添加动画效果 + self.button_animations = {} + for b in btns: + b.setMinimumHeight(30) + b.setMinimumWidth(110) + toolbar_h.addWidget(b) + + # 为按钮添加悬停和点击事件,实现动画效果 + b.enterEvent = lambda event, btn=b: self.on_button_hover(btn) + b.leaveEvent = lambda event, btn=b: self.on_button_leave(btn) + b.pressed.connect(lambda btn=b: self.on_button_pressed(btn)) + b.released.connect(lambda btn=b: self.on_button_released(btn)) + + # 初始化按钮动画 + animation = QtCore.QPropertyAnimation(b, b"geometry") + animation.setDuration(100) + self.button_animations[b] = animation + + toolbar_h.addStretch() + right_layout.addLayout(toolbar_h) + + # 路径栏 + self.path_widget = QtWidgets.QWidget() + path_h = QtWidgets.QHBoxLayout(self.path_widget) + path_h.addWidget(QtWidgets.QLabel("路径:")) + self.lbl_path = QtWidgets.QLabel("/") + font = self.lbl_path.font() + font.setBold(True) + self.lbl_path.setFont(font) + path_h.addWidget(self.lbl_path) + path_h.addStretch() + right_layout.addWidget(self.path_widget) + + # 创建页面堆栈 + self.page_stack = QtWidgets.QStackedWidget() + + # 文件页面 + self.files_page = QtWidgets.QWidget() + files_layout = QtWidgets.QVBoxLayout(self.files_page) + files_layout.setContentsMargins(0, 0, 0, 0) + + # 文件列表区域(包含表格和加载动画) + file_list_widget = QtWidgets.QWidget() + file_list_layout = QtWidgets.QVBoxLayout(file_list_widget) + file_list_layout.setContentsMargins(0, 0, 0, 0) + + # 文件列表表格 + self.table = QtWidgets.QTableWidget(0, 5) + self.table.setHorizontalHeaderLabels(["", "编号", "名称", "类型", "大小"]) + self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) + self.table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.table.doubleClicked.connect(self.on_table_double) + self.table.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.on_table_context_menu) + self.table.verticalHeader().setVisible(False) + self.table.horizontalHeader().setStretchLastSection(True) + file_list_layout.addWidget(self.table, stretch=1) + + # 加载动画布局 + self.loading_widget = QtWidgets.QWidget() + loading_layout = QtWidgets.QVBoxLayout(self.loading_widget) + loading_layout.setContentsMargins(0, 0, 0, 0) + loading_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + # 加载标签 + self.loading_label = QtWidgets.QLabel() + self.loading_label.setText("正在加载...") + font = self.loading_label.font() + font.setPointSize(14) + self.loading_label.setFont(font) + self.loading_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + loading_layout.addWidget(self.loading_label) + + # 旋转动画 + self.loading_spinner = QtWidgets.QLabel() + self.loading_spinner.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + # 创建一个简单的旋转动画 + self.spinner_timer = QtCore.QTimer() + self.spinner_angle = 0 + self.spinner_timer.timeout.connect(self.update_spinner) + self.spinner_timer.start(50) # 每50毫秒更新一次 + + loading_layout.addWidget(self.loading_spinner) + + # 初始隐藏加载动画 + self.loading_widget.setVisible(False) + file_list_layout.addWidget(self.loading_widget) + + files_layout.addWidget(file_list_widget, stretch=1) + + # 传输任务管理 + self.transfer_tasks = [] + self.next_task_id = 0 + self.active_tasks = {} # 保存活动任务的引用,用于取消 + + # 传输页面 + self.transfer_page = QtWidgets.QWidget() + transfer_layout = QtWidgets.QVBoxLayout(self.transfer_page) + transfer_layout.setContentsMargins(0, 0, 0, 0) + + # 传输页面内容 + transfer_title = QtWidgets.QLabel("传输任务") + transfer_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + transfer_title.setStyleSheet("font-size: 24px; font-weight: bold; color: #334155; margin: 20px 0;") + transfer_layout.addWidget(transfer_title) + + self.transfer_table = QtWidgets.QTableWidget(0, 6) + self.transfer_table.setHorizontalHeaderLabels(["类型", "文件名", "大小", "进度", "状态", "操作"]) + self.transfer_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) + self.transfer_table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.transfer_table.verticalHeader().setVisible(False) + self.transfer_table.horizontalHeader().setStretchLastSection(True) + # 设置列宽 + self.transfer_table.setColumnWidth(0, 80) + self.transfer_table.setColumnWidth(2, 120) + self.transfer_table.setColumnWidth(3, 100) + self.transfer_table.setColumnWidth(4, 100) + self.transfer_table.setColumnWidth(5, 80) + transfer_layout.addWidget(self.transfer_table, stretch=1) + + # 添加页面到堆栈 + self.page_stack.addWidget(self.files_page) + self.page_stack.addWidget(self.transfer_page) + + right_layout.addWidget(self.page_stack, stretch=1) + main_layout.addWidget(right_content, stretch=1) + + # 状态栏显示简短提示/进度 + self.status = self.statusBar() + self.status.showMessage("准备就绪") + + # 信号连接 + self.btn_settings.clicked.connect(self.on_settings) + self.btn_refresh.clicked.connect(lambda: self.refresh_file_list(reset_page=True)) + self.btn_more.clicked.connect(lambda: self.refresh_file_list(reset_page=False)) + self.btn_up.clicked.connect(self.on_up) + self.btn_download.clicked.connect(self.on_download) + self.btn_link.clicked.connect(self.on_showlink) + self.btn_upload.clicked.connect(self.on_upload) + self.btn_mkdir.clicked.connect(self.on_mkdir) + self.btn_delete.clicked.connect(self.on_delete) + self.btn_share.clicked.connect(self.on_share) + + # 侧边栏按钮信号 + self.btn_files.clicked.connect(lambda: self.switch_page(0)) + self.btn_transfer.clicked.connect(lambda: self.switch_page(1)) + + # 初始化默认页面 + self.switch_page(0) + + # 启动登录流程 + self.startup_login_flow() + + def apply_blue_white_theme(self): + """ + 123云盘主题样式表 - iOS 26 Liquid Glass 液态毛玻璃效果 + """ + style = """ + /* 全局样式 */ + QWidget { + background-color: rgba(255, 255, 255, 0.8); + color: #1E293B; + font-family: "SF Pro Display", "Segoe UI", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial; + font-size: 13px; + } + + /* 主窗口 */ + QMainWindow { + background-color: rgba(245, 245, 247, 0.95); + } + + /* 表格样式 - 液态毛玻璃效果(模拟) */ + QTableWidget { + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 12px; + padding: 8px; + gridline-color: rgba(0, 0, 0, 0.05); + } + + /* 表格行样式 */ + QTableWidget::item { + padding: 10px 6px; + border: none; + background-color: transparent; + border-radius: 6px; + } + + /* 表格行悬停效果 */ + QTableWidget::item:hover { + background-color: rgba(59, 130, 246, 0.1); + } + + /* 表格行选中效果 */ + QTableWidget::item:selected { + background-color: rgba(59, 130, 246, 0.9); + color: #FFFFFF; + } + + /* 表头样式 */ + QHeaderView::section { + background-color: rgba(255, 255, 255, 0.95); + color: #334155; + padding: 12px 16px; + border: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + font-weight: 600; + text-align: left; + border-radius: 8px 8px 0 0; + } + + QHeaderView { + background-color: transparent; + border: none; + } + + /* 按钮样式 - 液态毛玻璃效果(模拟) */ + QPushButton { + background-color: rgba(255, 255, 255, 0.95); + color: #3B82F6; + border: 1px solid rgba(59, 130, 246, 0.4); + border-radius: 12px; + padding: 10px 18px; + font-weight: 500; + font-size: 14px; + } + + QPushButton:hover { + background-color: rgba(255, 255, 255, 0.98); + border-color: rgba(59, 130, 246, 0.6); + } + + QPushButton:pressed { + background-color: rgba(230, 240, 255, 0.95); + border-color: rgba(59, 130, 246, 0.8); + } + + QPushButton:disabled { + background-color: rgba(240, 240, 245, 0.8); + border-color: rgba(148, 163, 184, 0.4); + color: rgba(148, 163, 184, 0.8); + } + + /* 输入控件样式 - 液态毛玻璃效果(模拟) */ + QLineEdit, QTextEdit, QComboBox { + background-color: rgba(255, 255, 255, 0.95); + border: 1px solid rgba(0, 0, 0, 0.08); + padding: 10px 14px; + border-radius: 12px; + } + + QLineEdit:focus, QTextEdit:focus, QComboBox:focus { + border-color: rgba(59, 130, 246, 0.6); + } + + /* 状态栏样式 - 液态毛玻璃效果(模拟) */ + QStatusBar { + background-color: rgba(255, 255, 255, 0.95); + color: #334155; + padding: 8px 16px; + border-top: 1px solid rgba(0, 0, 0, 0.05); + } + + /* 菜单样式 - 液态毛玻璃效果(模拟) */ + QMenu { + background-color: rgba(255, 255, 255, 0.98); + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 12px; + padding: 8px 0; + } + + QMenu::item { + padding: 10px 24px; + background-color: transparent; + border: none; + border-radius: 8px; + margin: 2px 8px; + } + + QMenu::item:selected { + background-color: rgba(59, 130, 246, 0.15); + color: #3B82F6; + } + + /* 滚动条样式 - 液态毛玻璃效果(模拟) */ + QScrollBar { + background-color: rgba(255, 255, 255, 0.7); + border-radius: 10px; + width: 10px; + height: 10px; + } + + QScrollBar::handle { + background-color: rgba(59, 130, 246, 0.6); + border-radius: 10px; + min-width: 24px; + min-height: 24px; + } + + QScrollBar::handle:hover { + background-color: rgba(59, 130, 246, 0.8); + } + + QScrollBar::add-line, QScrollBar::sub-line { + background-color: transparent; + } + + /* 对话框样式 - 液态毛玻璃效果(模拟) */ + QDialog { + background-color: rgba(255, 255, 255, 0.98); + border: 1px solid rgba(255, 255, 255, 0.9); + border-radius: 16px; + } + + /* 分组框样式 - 液态毛玻璃效果(模拟) */ + QGroupBox { + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 12px; + margin-top: 16px; + padding: 16px; + } + + QGroupBox::title { + color: #334155; + font-weight: 600; + subcontrol-origin: margin; + subcontrol-position: top left; + padding: 0 12px; + } + + /* 复选框样式 - 液态毛玻璃效果(模拟) */ + QCheckBox { + spacing: 8px; + } + + QCheckBox::indicator { + width: 20px; + height: 20px; + border: 2px solid rgba(59, 130, 246, 0.6); + border-radius: 6px; + background-color: rgba(255, 255, 255, 0.95); + } + + QCheckBox::indicator:checked { + background-color: rgba(59, 130, 246, 0.95); + border-color: rgba(59, 130, 246, 0.95); + } + + /* 标签样式 */ + QLabel { + color: #334155; + } + + /* 路径标签 */ + QLabel#lbl_path { + font-weight: 600; + color: #3B82F6; + font-size: 14px; + } + + /* 加载动画标签 */ + QLabel#loading_label { + color: #3B82F6; + } + + /* 设置按钮特殊样式 */ + QPushButton#btn_settings { + background-color: transparent; + border: none; + border-radius: 8px; + font-size: 18px; + padding: 6px; + color: #3B82F6; + } + + QPushButton#btn_settings:hover { + background-color: rgba(59, 130, 246, 0.1); + } + """ + self.setStyleSheet(style) + + def on_settings(self): + """打开设置对话框""" + dlg = SettingsDialog(self) + if dlg.exec() == QtWidgets.QDialog.DialogCode.Accepted: + settings = dlg.get_settings() + # 保存设置到配置文件 + config = ConfigManager.load_config() + config["settings"] = settings + ConfigManager.save_config(config) + QtWidgets.QMessageBox.information(self, "设置", "设置已保存") + + def startup_login_flow(self): + cfg_loaded = False + config = ConfigManager.load_config() + if config.get("userName") and config.get("passWord"): + try: + self.pan = Pan123(readfile=True, input_pwd=False) + res_code = self.pan.get_dir(save=False)[0] + if res_code == 0: + cfg_loaded = True + else: + cfg_loaded = False + except Exception: + cfg_loaded = False + + if not cfg_loaded: + dlg = LoginDialog(self) + if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted: + QtWidgets.QMessageBox.information(self, "提示", "未登录,程序将退出。") + QtCore.QTimer.singleShot(0, self.close) + return + self.pan = dlg.get_pan() + + self.refresh_file_list(reset_page=True) + + def prompt_selected_row(self): + rows = self.table.selectionModel().selectedRows() + if not rows: + QtWidgets.QMessageBox.information(self, "提示", "请先选择一项。") + return None + return rows[0].row() + + def get_file_icon(self, file_detail): + """根据文件类型获取图标""" + file_type = file_detail.get("Type", 0) + file_name = file_detail.get("FileName", "") + + # 创建一个32x32的图标 + pixmap = QtGui.QPixmap(32, 32) + pixmap.fill(QtCore.Qt.GlobalColor.transparent) + painter = QtGui.QPainter(pixmap) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + + if file_type == 1: # 文件夹 + # 绘制文件夹图标 + painter.setBrush(QtGui.QColor(255, 193, 7)) + painter.setPen(QtGui.QColor(255, 152, 0)) + # 文件夹主体 + painter.drawRect(6, 10, 20, 16) + # 文件夹盖子 + painter.drawRect(6, 6, 16, 8) + else: # 文件 + # 根据文件扩展名选择图标颜色 + ext = os.path.splitext(file_name)[1].lower() + colors = { + ".txt": QtGui.QColor(25, 118, 210), + ".pdf": QtGui.QColor(211, 47, 47), + ".doc": QtGui.QColor(33, 150, 243), + ".docx": QtGui.QColor(33, 150, 243), + ".xls": QtGui.QColor(76, 175, 80), + ".xlsx": QtGui.QColor(76, 175, 80), + ".ppt": QtGui.QColor(255, 193, 7), + ".pptx": QtGui.QColor(255, 193, 7), + ".jpg": QtGui.QColor(156, 39, 176), + ".jpeg": QtGui.QColor(156, 39, 176), + ".png": QtGui.QColor(156, 39, 176), + ".gif": QtGui.QColor(156, 39, 176), + ".mp3": QtGui.QColor(94, 53, 177), + ".mp4": QtGui.QColor(233, 30, 99), + ".zip": QtGui.QColor(121, 85, 72), + ".rar": QtGui.QColor(121, 85, 72), + ".7z": QtGui.QColor(121, 85, 72), + } + + color = colors.get(ext, QtGui.QColor(100, 116, 139)) + painter.setBrush(color) + painter.setPen(color.darker(120)) + + # 绘制文件图标 + painter.drawRect(6, 8, 20, 20) + # 绘制文件顶部的横线 + painter.setBrush(color.darker(120)) + painter.drawRect(6, 8, 20, 4) + + painter.end() + return QtGui.QIcon(pixmap) + + def populate_table(self): + if not self.pan: + return + self.table.setRowCount(0) + + # 逐行添加,使用定时器实现动画效果 + for i, item in enumerate(self.pan.list): + # 使用定时器延迟添加,实现逐行出现的效果 + QtCore.QTimer.singleShot(i * 30, lambda idx=i: self._add_row(idx)) + + names = getattr(self.pan, "parent_file_name_list", []) + path = "/" + "/".join(names) if names else "/" + self.lbl_path.setText(path) + + def _add_row(self, index): + """添加行,逐行显示""" + if index >= len(self.pan.list): + return + + item = self.pan.list[index] + row = self.table.rowCount() + self.table.insertRow(row) + + # 添加文件图标 + icon = self.get_file_icon(item) + icon_item = QtWidgets.QTableWidgetItem() + icon_item.setIcon(icon) + self.table.setItem(row, 0, icon_item) + + # 设置列宽,图标列不需要太宽 + self.table.setColumnWidth(0, 40) + + # 编号 + self.table.setItem(row, 1, QtWidgets.QTableWidgetItem(str(index + 1))) + + # 文件名 + name_item = QtWidgets.QTableWidgetItem(item.get("FileName", "")) + # 文件夹使用粗体 + if item.get("Type", 0) == 1: + font = name_item.font() + font.setBold(True) + name_item.setFont(font) + self.table.setItem(row, 2, name_item) + + # 文件类型 + typ = "文件夹" if item.get("Type", 0) == 1 else "文件" + self.table.setItem(row, 3, QtWidgets.QTableWidgetItem(typ)) + + # 文件大小 + size = item.get("Size", 0) + if size > 1073741824: + s = f"{round(size / 1073741824, 2)} GB" + elif size > 1048576: + s = f"{round(size / 1048576, 2)} MB" + else: + s = f"{round(size / 1024, 2)} KB" + self.table.setItem(row, 4, QtWidgets.QTableWidgetItem(s)) + + def update_spinner(self): + """更新旋转动画""" + self.spinner_angle = (self.spinner_angle + 10) % 360 + pixmap = QtGui.QPixmap(32, 32) + pixmap.fill(QtCore.Qt.GlobalColor.transparent) + painter = QtGui.QPainter(pixmap) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + + # 绘制旋转圆环 + pen = QtGui.QPen(QtGui.QColor(59, 130, 246), 3) + pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) + painter.setPen(pen) + rect = QtCore.QRect(4, 4, 24, 24) + painter.drawArc(rect, (90 - self.spinner_angle) * 16, 180 * 16) + + painter.end() + self.loading_spinner.setPixmap(pixmap) + + def refresh_file_list(self, reset_page=True): + if not self.pan: + QtWidgets.QMessageBox.information(self, "提示", "尚未初始化,请先登录。") + return + if reset_page: + self.pan.all_file = False + self.pan.file_page = 0 + self.pan.list = [] + + # 显示加载动画 + self.table.setVisible(False) + self.loading_widget.setVisible(True) + self.status.showMessage("正在获取目录...") + + task = ThreadedTask(self._task_get_dir) + task.signals.result.connect(self._after_get_dir) + task.signals.error.connect(lambda e: self._show_error("获取目录失败: " + e)) + self.threadpool.start(task) + + def _task_get_dir(self, signals=None, task=None): + code, _ = self.pan.get_dir(save=True) + return code + + def _after_get_dir(self, code): + # 隐藏加载动画,显示表格 + self.loading_widget.setVisible(False) + self.table.setVisible(True) + + if code != 0: + self.status.showMessage(f"获取目录返回码: {code}", 5000) + else: + self.status.showMessage("目录获取完成", 3000) + self.populate_table() + + def on_table_double(self, index): + row = index.row() + typ_item = self.table.item(row, 3) + if typ_item and typ_item.text() == "文件夹": + try: + # 保存要进入的文件夹编号 + self.target_folder_num = str(row + 1) + # 添加淡出动画 + self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") + self.fade_animation.setDuration(200) + self.fade_animation.setStartValue(1.0) + self.fade_animation.setEndValue(0.0) + self.fade_animation.finished.connect(self._after_fade_out_enter_folder) + self.fade_animation.start() + except Exception as e: + self._show_error("进入文件夹失败: " + str(e)) + else: + ret = QtWidgets.QMessageBox.question(self, "下载", "是否下载所选文件?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) + if ret == QtWidgets.QMessageBox.StandardButton.Yes: + self.on_download() + + def _after_fade_out_enter_folder(self): + """淡出动画完成后执行的操作 - 进入文件夹""" + try: + self.pan.cd(self.target_folder_num) + self.populate_table() + # 添加淡入动画 + self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") + self.fade_animation.setDuration(200) + self.fade_animation.setStartValue(0.0) + self.fade_animation.setEndValue(1.0) + self.fade_animation.start() + except Exception as e: + self._show_error("进入文件夹失败: " + str(e)) + + def on_button_hover(self, button): + """按钮悬停效果 - 修复动画冲突""" + # 停止当前正在运行的动画 + if button in self.button_animations: + self.button_animations[button].stop() + + # 保存原始位置,用于恢复 + if not hasattr(self, 'button_original_geoms'): + self.button_original_geoms = {} + if button not in self.button_original_geoms: + self.button_original_geoms[button] = button.geometry() + + # 创建放大动画 + scale_animation = QtCore.QPropertyAnimation(button, b"geometry") + current_geom = button.geometry() + original_geom = self.button_original_geoms[button] + # 基于原始位置计算新位置,避免累积误差 + new_geom = QtCore.QRect( + original_geom.x() - 2, + original_geom.y() - 2, + original_geom.width() + 4, + original_geom.height() + 4 + ) + scale_animation.setStartValue(current_geom) + scale_animation.setEndValue(new_geom) + scale_animation.setDuration(150) + scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) + scale_animation.start() + + # 保存动画引用 + self.button_animations[button] = scale_animation + + def on_button_leave(self, button): + """按钮离开效果 - 修复动画冲突""" + # 停止当前正在运行的动画 + if button in self.button_animations: + self.button_animations[button].stop() + + # 恢复到原始位置 + if hasattr(self, 'button_original_geoms') and button in self.button_original_geoms: + # 创建恢复动画 + scale_animation = QtCore.QPropertyAnimation(button, b"geometry") + current_geom = button.geometry() + original_geom = self.button_original_geoms[button] + scale_animation.setStartValue(current_geom) + scale_animation.setEndValue(original_geom) + scale_animation.setDuration(150) + scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) + scale_animation.start() + + # 保存动画引用 + self.button_animations[button] = scale_animation + + def on_button_pressed(self, button): + """按钮按下效果 - 修复动画冲突""" + # 停止当前正在运行的动画 + if button in self.button_animations: + self.button_animations[button].stop() + + # 创建按下动画 + scale_animation = QtCore.QPropertyAnimation(button, b"geometry") + current_geom = button.geometry() + # 基于当前位置轻微缩小 + new_geom = QtCore.QRect( + current_geom.x() + 1, + current_geom.y() + 1, + current_geom.width() - 2, + current_geom.height() - 2 + ) + scale_animation.setStartValue(current_geom) + scale_animation.setEndValue(new_geom) + scale_animation.setDuration(100) + scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.InQuad) + scale_animation.start() + + # 保存动画引用 + self.button_animations[button] = scale_animation + + def on_button_released(self, button): + """按钮释放效果 - 修复动画冲突""" + # 停止当前正在运行的动画 + if button in self.button_animations: + self.button_animations[button].stop() + + # 恢复到原始放大状态(如果是悬停中)或原始状态 + scale_animation = QtCore.QPropertyAnimation(button, b"geometry") + current_geom = button.geometry() + + if hasattr(self, 'button_original_geoms') and button in self.button_original_geoms: + # 检查鼠标是否仍然在按钮上 + if button.underMouse(): + # 恢复到悬停放大状态 + original_geom = self.button_original_geoms[button] + new_geom = QtCore.QRect( + original_geom.x() - 2, + original_geom.y() - 2, + original_geom.width() + 4, + original_geom.height() + 4 + ) + else: + # 恢复到原始状态 + new_geom = self.button_original_geoms[button] + + scale_animation.setStartValue(current_geom) + scale_animation.setEndValue(new_geom) + scale_animation.setDuration(100) + scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) + scale_animation.start() + + # 保存动画引用 + self.button_animations[button] = scale_animation + + def on_table_context_menu(self, pos): + row = self.table.indexAt(pos).row() + if row < 0: + return + menu = QtWidgets.QMenu() + a_download = menu.addAction("下载") + a_link = menu.addAction("显示链接") + a_delete = menu.addAction("删除") + a_share = menu.addAction("分享") + action = menu.exec(self.table.viewport().mapToGlobal(pos)) + self.table.selectRow(row) + if action == a_download: + self.on_download() + elif action == a_link: + self.on_showlink() + elif action == a_delete: + self.on_delete() + elif action == a_share: + self.on_share() + + def on_up(self): + if not self.pan: + return + try: + # 添加淡出动画 + self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") + self.fade_animation.setDuration(200) + self.fade_animation.setStartValue(1.0) + self.fade_animation.setEndValue(0.0) + self.fade_animation.finished.connect(self._after_fade_out_up) + self.fade_animation.start() + except Exception as e: + self._show_error("返回上级失败: " + str(e)) + + def _after_fade_out_up(self): + """淡出动画完成后执行的操作 - 返回上级""" + try: + self.pan.cd("..") + self.populate_table() + # 添加淡入动画 + self.fade_animation = QtCore.QPropertyAnimation(self.table, b"windowOpacity") + self.fade_animation.setDuration(200) + self.fade_animation.setStartValue(0.0) + self.fade_animation.setEndValue(1.0) + self.fade_animation.start() + except Exception as e: + self._show_error("返回上级失败: " + str(e)) + + def save_original_position(self, button): + """保存按钮的原始位置""" + self.sidebar_original_geoms[button] = button.geometry() + + def switch_page(self, page_index): + """切换页面""" + # 切换堆栈页面 + self.page_stack.setCurrentIndex(page_index) + + # 更新按钮样式 + for i, btn in enumerate(self.sidebar_buttons): + if i == page_index: + btn.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: rgba(59, 130, 246, 0.9);" + "color: white; border-radius: 12px;" + "border: none;" + ) + else: + btn.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: transparent; color: #334155;" + "border-radius: 12px;" + "border: none;" + ) + + # 根据页面显示/隐藏路径栏和相关按钮 + if page_index == 0: # 文件页面 + self.path_widget.setVisible(True) + self.btn_refresh.setVisible(True) + self.btn_more.setVisible(True) + self.btn_up.setVisible(True) + self.btn_delete.setVisible(True) + self.btn_download.setVisible(True) + self.btn_share.setVisible(True) + self.btn_link.setVisible(True) + self.btn_upload.setVisible(True) + self.btn_mkdir.setVisible(True) + else: # 传输页面 + self.path_widget.setVisible(False) + self.btn_refresh.setVisible(False) + self.btn_more.setVisible(False) + self.btn_up.setVisible(False) + self.btn_delete.setVisible(False) + self.btn_download.setVisible(False) + self.btn_share.setVisible(False) + self.btn_link.setVisible(False) + self.btn_upload.setVisible(False) + self.btn_mkdir.setVisible(False) + + def on_sidebar_button_hover(self, button): + """侧边栏按钮悬停效果""" + # 停止当前正在运行的动画 + if button in self.sidebar_animations: + self.sidebar_animations[button].stop() + + # 获取原始位置 + if button not in self.sidebar_original_geoms: + self.save_original_position(button) + original_geom = self.sidebar_original_geoms[button] + + # 创建缩放动画 + scale_animation = QtCore.QPropertyAnimation(button, b"geometry") + scale_animation.setStartValue(button.geometry()) + scale_animation.setEndValue(QtCore.QRect( + original_geom.x() - 5, + original_geom.y() - 2, + original_geom.width() + 10, + original_geom.height() + 4 + )) + scale_animation.setDuration(150) + scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) + scale_animation.start() + + # 保存动画引用 + self.sidebar_animations[button] = scale_animation + + def on_sidebar_button_leave(self, button): + """侧边栏按钮离开效果""" + # 停止当前正在运行的动画 + if button in self.sidebar_animations: + self.sidebar_animations[button].stop() + + # 获取原始位置 + if button not in self.sidebar_original_geoms: + self.save_original_position(button) + original_geom = self.sidebar_original_geoms[button] + + # 创建恢复动画 + scale_animation = QtCore.QPropertyAnimation(button, b"geometry") + scale_animation.setStartValue(button.geometry()) + scale_animation.setEndValue(original_geom) + scale_animation.setDuration(150) + scale_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutQuad) + scale_animation.start() + + # 保存动画引用 + self.sidebar_animations[button] = scale_animation + + def on_sidebar_button_pressed(self, button): + """侧边栏按钮按下效果""" + # 改变背景色 + button.setStyleSheet( + button.styleSheet().replace( + "background-color: rgba(59, 130, 246, 0.9);", + "background-color: rgba(37, 99, 235, 0.9);" + ).replace( + "background-color: transparent;", + "background-color: rgba(59, 130, 246, 0.1);" + ) + ) + + def on_sidebar_button_released(self, button): + """侧边栏按钮释放效果""" + # 恢复背景色 + if button == self.btn_files: + if self.page_stack.currentIndex() == 0: + button.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: rgba(59, 130, 246, 0.9);" + "color: white; border-radius: 12px;" + "border: none;" + ) + else: + button.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: transparent; color: #334155;" + "border-radius: 12px;" + "border: none;" + ) + elif button == self.btn_transfer: + if self.page_stack.currentIndex() == 1: + button.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: rgba(59, 130, 246, 0.9);" + "color: white; border-radius: 12px;" + "border: none;" + ) + else: + button.setStyleSheet( + "font-size: 16px; text-align: left; padding-left: 20px;" + "background-color: transparent; color: #334155;" + "border-radius: 12px;" + "border: none;" + ) + + def add_transfer_task(self, task_type, file_name, file_size): + """添加传输任务到列表和表格""" + task_id = self.next_task_id + self.next_task_id += 1 + + # 创建任务对象 + task = { + "id": task_id, + "type": task_type, # "下载" 或 "上传" + "file_name": file_name, + "file_size": file_size, + "progress": 0, + "status": "等待中", + "file_path": "", # 用于保存下载文件路径,便于取消时删除 + "threaded_task": None # 保存线程任务引用 + } + + # 添加到任务列表 + self.transfer_tasks.append(task) + + # 添加到表格 + row = self.transfer_table.rowCount() + self.transfer_table.insertRow(row) + + # 设置表格内容 + self.transfer_table.setItem(row, 0, QtWidgets.QTableWidgetItem(task_type)) + self.transfer_table.setItem(row, 1, QtWidgets.QTableWidgetItem(file_name)) + self.transfer_table.setItem(row, 2, QtWidgets.QTableWidgetItem(self.format_file_size(file_size))) + self.transfer_table.setItem(row, 3, QtWidgets.QTableWidgetItem("0%")) + self.transfer_table.setItem(row, 4, QtWidgets.QTableWidgetItem("等待中")) + + # 添加取消按钮 + cancel_btn = QtWidgets.QPushButton("取消") + cancel_btn.setStyleSheet( + "background-color: rgba(239, 68, 68, 0.1);" + "color: #EF4444;" + "border: 1px solid rgba(239, 68, 68, 0.3);" + "border-radius: 8px;" + "padding: 4px 12px;" + "font-size: 12px;" + ) + cancel_btn.clicked.connect(lambda _, tid=task_id: self.cancel_transfer_task(tid)) + self.transfer_table.setCellWidget(row, 5, cancel_btn) + + return task_id + + def update_transfer_task(self, task_id, progress, status): + """更新传输任务的进度和状态""" + # 查找任务 + for i, task in enumerate(self.transfer_tasks): + if task["id"] == task_id: + # 更新任务对象 + task["progress"] = progress + task["status"] = status + + # 更新表格 + self.transfer_table.setItem(i, 3, QtWidgets.QTableWidgetItem(f"{progress}%")) + self.transfer_table.setItem(i, 4, QtWidgets.QTableWidgetItem(status)) + break + + def cancel_transfer_task(self, task_id): + """取消传输任务""" + # 查找任务 + for i, task in enumerate(self.transfer_tasks): + if task["id"] == task_id: + # 取消线程任务 + if task.get("threaded_task"): + task["threaded_task"].cancel() + + # 如果是下载任务,删除临时文件 + if task["type"] == "下载" and task.get("file_path") and os.path.exists(task["file_path"]): + try: + os.remove(task["file_path"]) + # 也检查是否有最终文件存在(如果下载已完成但未清理) + final_path = task["file_path"].replace(".123pan", "") + if os.path.exists(final_path): + os.remove(final_path) + except Exception as e: + print(f"删除文件失败: {e}") + + # 更新任务状态 + task["status"] = "已取消" + task["progress"] = 0 + self.transfer_table.setItem(i, 3, QtWidgets.QTableWidgetItem("0%")) + self.transfer_table.setItem(i, 4, QtWidgets.QTableWidgetItem("已取消")) + + # 移除取消按钮 + widget = self.transfer_table.cellWidget(i, 5) + if widget: + widget.setVisible(False) + + # 从活动任务列表中移除 + if task_id in self.active_tasks: + del self.active_tasks[task_id] + + break + + def remove_transfer_task(self, task_id): + """移除传输任务""" + # 查找任务 + for i, task in enumerate(self.transfer_tasks): + if task["id"] == task_id: + # 从列表中移除 + self.transfer_tasks.pop(i) + # 从表格中移除 + self.transfer_table.removeRow(i) + # 从活动任务列表中移除 + if task_id in self.active_tasks: + del self.active_tasks[task_id] + break + + def format_file_size(self, size): + """格式化文件大小""" + if size > 1073741824: + return f"{round(size / 1073741824, 2)} GB" + elif size > 1048576: + return f"{round(size / 1048576, 2)} MB" + elif size > 1024: + return f"{round(size / 1024, 2)} KB" + else: + return f"{size} B" + + def get_selected_detail(self): + row = self.prompt_selected_row() + if row is None: + return None, None + try: + # 直接使用行索引作为文件索引,更可靠 + if not self.pan or row < 0 or row >= len(self.pan.list): + self._show_error("无效的选择行") + return None, None + return row, self.pan.list[row] + except Exception as e: + self._show_error(f"获取选中文件失败: {str(e)}") + return None, None + + def on_download(self): + file_index, file_detail = self.get_selected_detail() + if file_detail is None: + return + + # 获取设置 + ask_location = ConfigManager.get_setting("askDownloadLocation", True) + default_path = ConfigManager.get_setting("defaultDownloadPath", + os.path.join(os.path.expanduser("~"), "Downloads")) + + download_dir = default_path + if ask_location: + download_dir = QtWidgets.QFileDialog.getExistingDirectory( + self, "选择下载文件夹", default_path + ) + if not download_dir: + return + + file_name = file_detail.get("FileName", "未知文件") + file_size = file_detail.get("Size", 0) + + # 添加传输任务 + task_id = self.add_transfer_task("下载", file_name, file_size) + + self.status.showMessage("正在解析下载链接...") + task = ThreadedTask(self._task_get_download_and_stream, file_index, download_dir, task_id) + + # 保存任务对象引用 + for i, t in enumerate(self.transfer_tasks): + if t["id"] == task_id: + self.transfer_tasks[i]["threaded_task"] = task + break + + self.active_tasks[task_id] = task + + task.signals.progress.connect(lambda p, tid=task_id: ( + self.status.showMessage(f"下载进度: {p}%", 2000), + self.update_transfer_task(tid, p, "下载中") + )) + def on_task_finished(tid): + if tid in self.active_tasks: + del self.active_tasks[tid] + + task.signals.result.connect(lambda r, tid=task_id: ( + self.status.showMessage("下载完成: " + str(r), 5000), + self.update_transfer_task(tid, 100, "已完成"), + on_task_finished(tid) + )) + task.signals.error.connect(lambda e, tid=task_id: ( + self._show_error("下载失败: " + e), + self.update_transfer_task(tid, 0, "失败"), + on_task_finished(tid) + )) + task.signals.finished.connect(lambda tid=task_id: on_task_finished(tid)) + self.threadpool.start(task) + + def _task_get_download_and_stream(self, file_index, download_dir, task_id, signals=None, task=None): + file_detail = self.pan.list[file_index] + if file_detail["Type"] == 1: + redirect_url = self.pan.link_by_fileDetail(file_detail, showlink=False) + else: + redirect_url = self.pan.link_by_number(file_index, showlink=False) + if isinstance(redirect_url, int): + raise RuntimeError("获取下载链接失败,返回码: " + str(redirect_url)) + if file_detail["Type"] == 1: + fname = file_detail["FileName"] + ".zip" + else: + fname = file_detail["FileName"] + out_path = os.path.join(download_dir, fname) + temp = out_path + ".123pan" + + # 保存文件路径到任务对象 + for i, t in enumerate(self.transfer_tasks): + if t["id"] == task_id: + self.transfer_tasks[i]["file_path"] = temp + break + + if os.path.exists(out_path): + reply = QtWidgets.QMessageBox.question(None, "文件已存在", f"{fname} 已存在,是否覆盖?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) + if reply == QtWidgets.QMessageBox.StandardButton.No: + return "已取消" + with requests.get(redirect_url, stream=True, timeout=30) as r: + r.raise_for_status() + total = int(r.headers.get("Content-Length", 0) or 0) + done = 0 + with open(temp, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + # 检查是否被取消 + if task and task.is_cancelled: + f.close() + # 删除临时文件 + if os.path.exists(temp): + os.remove(temp) + return "已取消" + if chunk: + f.write(chunk) + done += len(chunk) + if total and signals: + signals.progress.emit(int(done * 100 / total)) + if task and task.is_cancelled: + # 删除临时文件 + if os.path.exists(temp): + os.remove(temp) + return "已取消" + os.replace(temp, out_path) + return out_path + + def on_showlink(self): + file_index, file_detail = self.get_selected_detail() + if file_detail is None: + return + try: + # 直接调用获取链接,不使用线程,避免参数传递问题 + url = self._task_get_link(file_index) + self._after_get_link(url) + except Exception as e: + self._show_error(f"获取链接失败: {str(e)}") + + def _task_get_link(self, file_index, signals=None, task=None): + try: + url = self.pan.link_by_number(file_index, showlink=False) + return url + except Exception as e: + return f"获取链接失败: {str(e)}" + + def _after_get_link(self, url): + if isinstance(url, int): + self._show_error("获取链接失败,返回码: " + str(url)) + return + dlg = QtWidgets.QDialog(self) + dlg.setWindowTitle("下载链接") + dlg.resize(700, 140) + v = QtWidgets.QVBoxLayout(dlg) + te = QtWidgets.QTextEdit() + te.setReadOnly(True) + te.setPlainText(url) + v.addWidget(te) + h = QtWidgets.QHBoxLayout() + btn_copy = QtWidgets.QPushButton("复制到剪贴板") + btn_copy.clicked.connect(lambda: QtWidgets.QApplication.clipboard().setText(url)) + btn_close = QtWidgets.QPushButton("关闭") + btn_close.clicked.connect(dlg.accept) + h.addStretch() + h.addWidget(btn_copy) + h.addWidget(btn_close) + v.addLayout(h) + dlg.exec() + + def on_upload(self): + if not self.pan: + QtWidgets.QMessageBox.information(self, "提示", "请先登录。") + return + path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "选择要上传的文件", os.path.expanduser("~")) + if not path: + return + fname = os.path.basename(path) + file_size = os.path.getsize(path) + same = [i for i in self.pan.list if i.get("FileName") == fname] + dup_choice = 1 + if same: + text, ok = QtWidgets.QInputDialog.getText(self, "同名文件", "检测到同名文件,输入行为:1 覆盖;2 保留两者;0 取消(默认1)", text="1") + if not ok: + return + if text.strip() not in ("0", "1", "2"): + QtWidgets.QMessageBox.information(self, "提示", "无效的选择,已取消") + return + if text.strip() == "0": + return + dup_choice = int(text.strip()) + + # 添加传输任务 + task_id = self.add_transfer_task("上传", fname, file_size) + + task = ThreadedTask(self._task_upload_file, path, dup_choice, task_id) + + # 保存任务对象引用 + for i, t in enumerate(self.transfer_tasks): + if t["id"] == task_id: + self.transfer_tasks[i]["threaded_task"] = task + break + + self.active_tasks[task_id] = task + + def on_task_finished(tid): + if tid in self.active_tasks: + del self.active_tasks[tid] + + task.signals.progress.connect(lambda p, tid=task_id: ( + self.status.showMessage(f"上传进度: {p}%", 2000), + self.update_transfer_task(tid, p, "上传中") + )) + task.signals.result.connect(lambda r, tid=task_id: ( + self.status.showMessage("上传完成", 3000), + self.update_transfer_task(tid, 100, "已完成"), + self.refresh_file_list(reset_page=True), + on_task_finished(tid) + )) + task.signals.error.connect(lambda e, tid=task_id: ( + self._show_error("上传失败: " + e), + self.update_transfer_task(tid, 0, "失败"), + on_task_finished(tid) + )) + task.signals.finished.connect(lambda tid=task_id: on_task_finished(tid)) + self.threadpool.start(task) + + def _task_upload_file(self, file_path, dup_choice, task_id, signals=None, task=None): + file_path = file_path.replace('"', "").replace("\\", "/") + file_name = os.path.basename(file_path) + if not os.path.exists(file_path): + raise RuntimeError("文件不存在") + if os.path.isdir(file_path): + raise RuntimeError("不支持文件夹上传") + fsize = os.path.getsize(file_path) + + # 检查是否被取消 + if task and task.is_cancelled: + return "已取消" + + md5 = hashlib.md5() + with open(file_path, "rb") as f: + while True: + data = f.read(64 * 1024) + if not data: + break + md5.update(data) + # 检查是否被取消 + if task and task.is_cancelled: + return "已取消" + readable_hash = md5.hexdigest() + + # 检查是否被取消 + if task and task.is_cancelled: + return "已取消" + list_up_request = { + "driveId": 0, + "etag": readable_hash, + "fileName": file_name, + "parentFileId": self.pan.parent_file_id, + "size": fsize, + "type": 0, + "duplicate": 0, + } + url = "https://www.123pan.com/b/api/file/upload_request" + headers = self.pan.header_logined.copy() + res = requests.post(url, headers=headers, data=list_up_request, timeout=30) + res_json = res.json() + code = res_json.get("code", -1) + if code == 5060: + list_up_request["duplicate"] = dup_choice + res = requests.post(url, headers=headers, data=json.dumps(list_up_request), timeout=30) + res_json = res.json() + code = res_json.get("code", -1) + if code != 0: + raise RuntimeError("上传请求失败: " + json.dumps(res_json, ensure_ascii=False)) + data = res_json["data"] + if data.get("Reuse"): + return "复用上传成功" + bucket = data["Bucket"] + storage_node = data["StorageNode"] + upload_key = data["Key"] + upload_id = data["UploadId"] + up_file_id = data["FileId"] + block_size = 5242880 + total_sent = 0 + part_number = 1 + with open(file_path, "rb") as f: + while True: + block = f.read(block_size) + if not block: + break + get_link_data = { + "bucket": bucket, + "key": upload_key, + "partNumberEnd": part_number + 1, + "partNumberStart": part_number, + "uploadId": upload_id, + "StorageNode": storage_node, + } + get_link_url = "https://www.123pan.com/b/api/file/s3_repare_upload_parts_batch" + get_link_res = requests.post(get_link_url, headers=headers, data=json.dumps(get_link_data), timeout=30) + get_link_res_json = get_link_res.json() + if get_link_res_json.get("code", -1) != 0: + raise RuntimeError("获取上传链接失败: " + json.dumps(get_link_res_json, ensure_ascii=False)) + upload_url = get_link_res_json["data"]["presignedUrls"][str(part_number)] + requests.put(upload_url, data=block, timeout=60) + total_sent += len(block) + if signals and fsize: + signals.progress.emit(int(total_sent * 100 / fsize)) + part_number += 1 + uploaded_list_url = "https://www.123pan.com/b/api/file/s3_list_upload_parts" + uploaded_comp_data = {"bucket": bucket, "key": upload_key, "uploadId": upload_id, "storageNode": storage_node} + requests.post(uploaded_list_url, headers=headers, data=json.dumps(uploaded_comp_data), timeout=30) + compmultipart_up_url = "https://www.123pan.com/b/api/file/s3_complete_multipart_upload" + requests.post(compmultipart_up_url, headers=headers, data=json.dumps(uploaded_comp_data), timeout=30) + if fsize > 64 * 1024 * 1024: + time.sleep(3) + close_up_session_url = "https://www.123pan.com/b/api/file/upload_complete" + close_up_session_data = {"fileId": up_file_id} + close_res = requests.post(close_up_session_url, headers=headers, data=json.dumps(close_up_session_data), timeout=30) + cr = close_res.json() + if cr.get("code", -1) != 0: + raise RuntimeError("上传完成确认失败: " + json.dumps(cr, ensure_ascii=False)) + return up_file_id + + def on_mkdir(self): + if not self.pan: + QtWidgets.QMessageBox.information(self, "提示", "请先登录。") + return + name, ok = QtWidgets.QInputDialog.getText(self, "新建文件夹", "请输入文件夹名称:") + if not ok or not name.strip(): + return + res = self.pan.mkdir(name.strip(), remakedir=False) + self.status.showMessage("创建完成", 3000) + self.refresh_file_list(reset_page=True) + + def on_delete(self): + file_index, file_detail = self.get_selected_detail() + if file_detail is None: + return + r = QtWidgets.QMessageBox.question(self, "删除确认", f"确认将 '{file_detail['FileName']}' 删除?", QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) + if r == QtWidgets.QMessageBox.StandardButton.No: + return + try: + self.pan.delete_file(file_index, by_num=True, operation=True) + self.status.showMessage("删除请求已发送", 3000) + self.refresh_file_list(reset_page=True) + except Exception as e: + self._show_error("删除失败: " + str(e)) + + def on_share(self): + file_index, file_detail = self.get_selected_detail() + if file_detail is None: + return + pwd, ok = QtWidgets.QInputDialog.getText(self, "分享", "提取码(留空则没有提取码):") + if not ok: + return + file_id_list = str(file_detail["FileId"]) + data = { + "driveId": 0, + "expiration": "2099-12-12T08:00:00+08:00", + "fileIdList": file_id_list, + "shareName": "123云盘分享", + "sharePwd": pwd or "", + "event": "shareCreate" + } + headers = self.pan.header_logined.copy() + try: + r = requests.post("https://www.123pan.com/a/api/share/create", headers=headers, data=json.dumps(data), timeout=30) + jr = r.json() + if jr.get("code", -1) != 0: + self._show_error("分享失败: " + jr.get("message", str(jr))) + return + share_key = jr["data"]["ShareKey"] + share_url = "https://www.123pan.com/s/" + share_key + QtWidgets.QMessageBox.information(self, "分享链接", f"{share_url}\n提取码:{pwd or '(无)'}") + except Exception as e: + self._show_error("分享异常: " + str(e)) + + def _show_error(self, msg): + QtWidgets.QMessageBox.critical(self, "错误", msg) + self.status.showMessage(msg, 8000) + + def closeEvent(self, event): + try: + if self.pan and getattr(self.pan, "user_name", "") and getattr(self.pan, "password", ""): + self.pan.save_file() + except Exception: + pass + event.accept() + +def main(): + app = QtWidgets.QApplication(sys.argv) + w = MainWindow() + w.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() + diff --git a/src/threading_utils.py b/src/threading_utils.py new file mode 100644 index 0000000..107751a --- /dev/null +++ b/src/threading_utils.py @@ -0,0 +1,45 @@ +"""多线程任务模块""" +from PyQt6 import QtCore + + +class WorkerSignals(QtCore.QObject): + """工作线程信号""" + finished = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + result = QtCore.pyqtSignal(object) + progress = QtCore.pyqtSignal(int) + log = QtCore.pyqtSignal(str) + cancel = QtCore.pyqtSignal() + + +class ThreadedTask(QtCore.QRunnable): + """线程任务""" + + def __init__(self, fn, *args, **kwargs): + super().__init__() + self.fn = fn + self.args = args + self.kwargs = kwargs + self.signals = WorkerSignals() + self.is_cancelled = False + + @QtCore.pyqtSlot() + def run(self): + """运行任务""" + try: + if self.is_cancelled: + return + res = self.fn(*self.args, **self.kwargs, signals=self.signals, task=self) + if not self.is_cancelled: + self.signals.result.emit(res) + except Exception as e: + if not self.is_cancelled: + self.signals.error.emit(str(e)) + finally: + if not self.is_cancelled: + self.signals.finished.emit() + + def cancel(self): + """取消任务""" + self.is_cancelled = True + self.signals.cancel.emit() diff --git a/src/ui_widgets.py b/src/ui_widgets.py new file mode 100644 index 0000000..57c1b6b --- /dev/null +++ b/src/ui_widgets.py @@ -0,0 +1,178 @@ +# https://github.com/123panNextGen/123pan +# src/ui_widgets.py + +from PyQt6 import QtCore, QtGui, QtWidgets +import os +from config import ConfigManager + + +class SidebarButton(QtWidgets.QPushButton): + """侧栏按钮,支持hover事件""" + entered = QtCore.pyqtSignal() + left = QtCore.pyqtSignal() + + def enterEvent(self, event): + """鼠标进入事件""" + super().enterEvent(event) + self.entered.emit() + + def leaveEvent(self, event): + """鼠标离开事件""" + super().leaveEvent(event) + self.left.emit() + + +class SettingsDialog(QtWidgets.QDialog): + """设置对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("设置") + self.setModal(True) + self.resize(500, 200) + self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint) + + layout = QtWidgets.QVBoxLayout(self) + + # 下载设置组 + download_group = QtWidgets.QGroupBox("下载设置") + download_layout = QtWidgets.QVBoxLayout() + + # 默认下载路径 + path_layout = QtWidgets.QHBoxLayout() + path_layout.addWidget(QtWidgets.QLabel("默认下载路径:")) + self.le_download_path = QtWidgets.QLineEdit() + self.le_download_path.setReadOnly(True) + path_layout.addWidget(self.le_download_path, 1) + self.btn_browse = QtWidgets.QPushButton("浏览...") + self.btn_browse.clicked.connect(self.browse_download_path) + path_layout.addWidget(self.btn_browse) + download_layout.addLayout(path_layout) + + # 下载前询问 + self.cb_ask_location = QtWidgets.QCheckBox("每次下载前询问保存位置") + download_layout.addWidget(self.cb_ask_location) + + download_group.setLayout(download_layout) + layout.addWidget(download_group) + + # 按钮 + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() + self.btn_save = QtWidgets.QPushButton("保存") + self.btn_cancel = QtWidgets.QPushButton("取消") + button_layout.addWidget(self.btn_save) + button_layout.addWidget(self.btn_cancel) + layout.addLayout(button_layout) + + # 连接信号 + self.btn_save.clicked.connect(self.accept) + self.btn_cancel.clicked.connect(self.reject) + + # 加载当前设置 + self.load_settings() + + def load_settings(self): + """加载当前设置""" + default_path = ConfigManager.get_setting("defaultDownloadPath", + os.path.join(os.path.expanduser("~"), "Downloads")) + ask_location = ConfigManager.get_setting("askDownloadLocation", True) + + self.le_download_path.setText(default_path) + self.cb_ask_location.setChecked(ask_location) + + def browse_download_path(self): + """浏览下载路径""" + path = QtWidgets.QFileDialog.getExistingDirectory( + self, "选择默认下载路径", self.le_download_path.text() + ) + if path: + self.le_download_path.setText(path) + + def get_settings(self): + """获取设置的参数""" + return { + "defaultDownloadPath": self.le_download_path.text(), + "askDownloadLocation": self.cb_ask_location.isChecked() + } + + +class LoginDialog(QtWidgets.QDialog): + """登录对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("登录123云盘") + self.setModal(True) + self.resize(420, 150) + self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint) + + layout = QtWidgets.QVBoxLayout(self) + + form = QtWidgets.QFormLayout() + self.le_user = QtWidgets.QLineEdit() + self.le_pass = QtWidgets.QLineEdit() + self.le_pass.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) + form.addRow("用户名:", self.le_user) + form.addRow("密码:", self.le_pass) + layout.addLayout(form) + + h = QtWidgets.QHBoxLayout() + h.addStretch() + self.btn_ok = QtWidgets.QPushButton("登录") + self.btn_cancel = QtWidgets.QPushButton("取消") + h.addWidget(self.btn_ok) + h.addWidget(self.btn_cancel) + layout.addLayout(h) + + self.btn_ok.clicked.connect(self.on_ok) + self.btn_cancel.clicked.connect(self.reject) + + self.pan = None + self.login_error = None + + # 从配置文件中加载用户名 + config = ConfigManager.load_config() + self.le_user.setText(config.get("userName", "")) + + def on_ok(self): + """登录处理""" + from api import Pan123 + + user = self.le_user.text().strip() + pwd = self.le_pass.text() + if not user or not pwd: + QtWidgets.QMessageBox.information(self, "提示", "请输入用户名和密码。") + return + QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.WaitCursor) + try: + # 构造123pan并登录 + try: + self.pan = Pan123(readfile=False, user_name=user, pass_word=pwd, input_pwd=False) + except Exception: + self.pan = Pan123(readfile=False, user_name=user, pass_word=pwd, input_pwd=False) + if not getattr(self.pan, "authorization", None): + code = self.pan.login() + if code != 200 and code != 0: + self.login_error = f"登录失败,返回码: {code}" + QtWidgets.QApplication.restoreOverrideCursor() + QtWidgets.QMessageBox.critical(self, "登录失败", self.login_error) + return + except Exception as e: + self.login_error = str(e) + QtWidgets.QApplication.restoreOverrideCursor() + QtWidgets.QMessageBox.critical(self, "登录异常", "登录时发生异常:\n" + str(e)) + return + finally: + QtWidgets.QApplication.restoreOverrideCursor() + + try: + if hasattr(self.pan, "save_file"): + self.pan.save_file() + except Exception: + pass + self.accept() + + def get_pan(self): + """获取登录成功的Pan对象""" + return self.pan From b3a344d4f563022c1ce18e0f647c6f0479db8a00 Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sun, 1 Feb 2026 18:10:03 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E4=BF=AE=E6=94=B9action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8587295..b53b050 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,9 +73,10 @@ jobs: with: path: artifacts - - name: Publish Release + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - files: | - artifacts/123pan-windows/* - artifacts/123pan-linux/* + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + files: all_binaries/* + generate_release_notes: true From a038f2b0534529282badfff9c731968a36e9ef3b Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sun, 1 Feb 2026 18:38:55 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- src/main_window.py | 249 ++++++++++++++++++++++++++++++++++++++++- src/threading_utils.py | 4 +- src/ui_widgets.py | 61 +++++++++- 4 files changed, 308 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ea4053b..b5be05d 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,7 @@ --- ## 待开发功能 -- [ ] 退出登录 -- [ ] 文件拖拽上传 -- [ ] 拖拽上传功能 +? ## 🤝 贡献指南 diff --git a/src/main_window.py b/src/main_window.py index a2e5845..fc58b01 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,16 +1,110 @@ -"""主窗口模块""" +# https://github.com/123panNextGen/123pan +# src/main.window.py + from PyQt6 import QtCore, QtGui, QtWidgets import os import json import hashlib +import requests +import sys +import time from log import get_logger from config import ConfigManager -from ui_widgets import SidebarButton, LoginDialog, SettingsDialog +from ui_widgets import SidebarButton, LoginDialog, SettingsDialog, AboutDialog from api import Pan123 from threading_utils import ThreadedTask logger = get_logger(__name__) + +class DropAreaTableWidget(QtWidgets.QTableWidget): + """支持拖拽上传的表格控件""" + files_dropped = QtCore.pyqtSignal(list) # 信号:文件路径列表 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setAcceptDrops(True) + self.is_drag_over = False + # 设置 viewport 也接受拖拽 + self.viewport().setAcceptDrops(True) + self.viewport().installEventFilter(self) + + def eventFilter(self, obj, event): + """事件过滤器,捕获 viewport 的拖拽事件""" + if obj == self.viewport(): + if event.type() == QtCore.QEvent.Type.DragEnter: + return self.dragEnterEvent(event) or True + elif event.type() == QtCore.QEvent.Type.DragLeave: + self.dragLeaveEvent(event) + return True + elif event.type() == QtCore.QEvent.Type.DragMove: + if event.mimeData().hasUrls(): + has_files = any( + os.path.isfile(url.toLocalFile()) + for url in event.mimeData().urls() + ) + if has_files: + event.acceptProposedAction() + return True + elif event.type() == QtCore.QEvent.Type.Drop: + return self.dropEvent(event) or True + return super().eventFilter(obj, event) + + def dragEnterEvent(self, event): + """处理拖进事件""" + if event.mimeData().hasUrls(): + # 检查是否有文件 + has_files = any( + os.path.isfile(url.toLocalFile()) + for url in event.mimeData().urls() + ) + if has_files: + event.acceptProposedAction() + self.is_drag_over = True + # 高亮显示表格 + self.setStyleSheet(self.styleSheet() + + "\nQTableWidget { background-color: rgba(59, 130, 246, 0.15); border: 2px dashed rgba(59, 130, 246, 0.5); }") + return True + else: + event.ignore() + else: + event.ignore() + return False + + def dragLeaveEvent(self, event): + """处理拖出事件""" + if self.is_drag_over: + self.is_drag_over = False + # 恢复原样式 + style = self.styleSheet() + # 移除高亮样式 + style = style.replace("\nQTableWidget { background-color: rgba(59, 130, 246, 0.15); border: 2px dashed rgba(59, 130, 246, 0.5); }", "") + self.setStyleSheet(style) + + def dropEvent(self, event): + """处理放下事件""" + # 恢复原样式 + if self.is_drag_over: + self.is_drag_over = False + style = self.styleSheet() + style = style.replace("\nQTableWidget { background-color: rgba(59, 130, 246, 0.15); border: 2px dashed rgba(59, 130, 246, 0.5); }", "") + self.setStyleSheet(style) + + files = [] + for url in event.mimeData().urls(): + file_path = url.toLocalFile() + if os.path.isfile(file_path): + files.append(file_path) + + if files: + logger.info(f"拖拽上传文件: {files}") + self.files_dropped.emit(files) + event.acceptProposedAction() + return True + else: + event.ignore() + return False + class MainWindow(QtWidgets.QMainWindow): def __init__(self): super().__init__() @@ -124,6 +218,22 @@ def __init__(self): self.btn_settings.setObjectName("btn_settings") toolbar_h.addWidget(self.btn_settings) + # 退出登陆按钮 + self.btn_logout = QtWidgets.QPushButton("🚪") + self.btn_logout.setToolTip("退出登陆") + self.btn_logout.setMinimumHeight(36) + self.btn_logout.setMinimumWidth(45) + self.btn_logout.setMaximumHeight(36) + self.btn_logout.setMaximumWidth(45) + self.btn_logout.setStyleSheet( + "font-size: 20px;" + "background-color: transparent;" + "border: none;" + "border-radius: 8px;" + ) + self.btn_logout.setObjectName("btn_logout") + toolbar_h.addWidget(self.btn_logout) + # 操作按钮(横向排列) self.btn_refresh = QtWidgets.QPushButton("刷新") self.btn_more = QtWidgets.QPushButton("更多") @@ -135,6 +245,22 @@ def __init__(self): self.btn_upload = QtWidgets.QPushButton("上传文件") self.btn_mkdir = QtWidgets.QPushButton("新建文件夹") + # 关于按钮 + self.btn_about = QtWidgets.QPushButton("ℹ️") + self.btn_about.setToolTip("关于") + self.btn_about.setMinimumHeight(36) + self.btn_about.setMinimumWidth(45) + self.btn_about.setMaximumHeight(36) + self.btn_about.setMaximumWidth(45) + self.btn_about.setStyleSheet( + "font-size: 20px;" + "background-color: transparent;" + "border: none;" + "border-radius: 8px;" + ) + self.btn_about.setObjectName("btn_about") + toolbar_h.addWidget(self.btn_about) + # 设置按钮最小宽度统一外观 btns = [self.btn_refresh, self.btn_more, self.btn_up, self.btn_download, self.btn_link, self.btn_upload, self.btn_mkdir, self.btn_delete, self.btn_share] @@ -185,8 +311,8 @@ def __init__(self): file_list_layout = QtWidgets.QVBoxLayout(file_list_widget) file_list_layout.setContentsMargins(0, 0, 0, 0) - # 文件列表表格 - self.table = QtWidgets.QTableWidget(0, 5) + # 文件列表表格(支持拖拽上传) + self.table = DropAreaTableWidget(0, 5) self.table.setHorizontalHeaderLabels(["", "编号", "名称", "类型", "大小"]) self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) self.table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) @@ -195,6 +321,8 @@ def __init__(self): self.table.customContextMenuRequested.connect(self.on_table_context_menu) self.table.verticalHeader().setVisible(False) self.table.horizontalHeader().setStretchLastSection(True) + # 连接拖拽上传信号 + self.table.files_dropped.connect(self.on_files_dropped) file_list_layout.addWidget(self.table, stretch=1) # 加载动画布局 @@ -272,6 +400,7 @@ def __init__(self): # 信号连接 self.btn_settings.clicked.connect(self.on_settings) + self.btn_logout.clicked.connect(self.on_logout) self.btn_refresh.clicked.connect(lambda: self.refresh_file_list(reset_page=True)) self.btn_more.clicked.connect(lambda: self.refresh_file_list(reset_page=False)) self.btn_up.clicked.connect(self.on_up) @@ -289,9 +418,12 @@ def __init__(self): # 初始化默认页面 self.switch_page(0) + + # 关于按钮信号 + self.btn_about.clicked.connect(self.on_about) + # 启动登录流程 self.startup_login_flow() - def apply_blue_white_theme(self): """ 123云盘主题样式表 - iOS 26 Liquid Glass 液态毛玻璃效果 @@ -531,7 +663,114 @@ def on_settings(self): config["settings"] = settings ConfigManager.save_config(config) QtWidgets.QMessageBox.information(self, "设置", "设置已保存") + + def on_logout(self): + """退出登陆""" + reply = QtWidgets.QMessageBox.question( + self, "退出登陆", "确定要退出登陆吗?", + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No + ) + if reply == QtWidgets.QMessageBox.StandardButton.Yes: + # 清除配置文件中的登陆信息 + config = ConfigManager.load_config() + config["userName"] = "" + config["passWord"] = "" + config["authorization"] = "" + ConfigManager.save_config(config) + + # 清空当前登陆状态 + self.pan = None + + # 显示登陆对话框 + dlg = LoginDialog(self) + if dlg.exec() != QtWidgets.QDialog.DialogCode.Accepted: + QtWidgets.QMessageBox.information(self, "提示", "未登录,程序将退出。") + QtCore.QTimer.singleShot(0, self.close) + return + self.pan = dlg.get_pan() + self.refresh_file_list(reset_page=True) + QtWidgets.QMessageBox.information(self, "提示", "登陆成功") + + def on_files_dropped(self, files): + """处理拖拽上传的文件""" + logger.info(f"收到拖拽上传请求,文件数: {len(files)}") + if not self.pan: + logger.warning("未登录,无法上传") + QtWidgets.QMessageBox.information(self, "提示", "请先登录。") + return + + # 逐个上传文件 + for file_path in files: + self._upload_single_file(file_path) + + def _upload_single_file(self, file_path): + """上传单个文件""" + logger.info(f"准备上传文件: {file_path}") + if not os.path.isfile(file_path): + logger.warning(f"文件不存在: {file_path}") + return + + fname = os.path.basename(file_path) + file_size = os.path.getsize(file_path) + logger.info(f"文件信息 - 名称: {fname}, 大小: {file_size} 字节") + same = [i for i in self.pan.list if i.get("FileName") == fname] + dup_choice = 1 + + if same: + text, ok = QtWidgets.QInputDialog.getText( + self, "同名文件", + f"检测到同名文件: {fname}\n输入行为: 1 覆盖; 2 保留两者; 0 取消(默认1)", + text="1" + ) + if not ok: + return + if text.strip() not in ("0", "1", "2"): + QtWidgets.QMessageBox.information(self, "提示", "无效的选择,已取消") + return + if text.strip() == "0": + return + dup_choice = int(text.strip()) + + # 添加传输任务 + task_id = self.add_transfer_task("上传", fname, file_size) + + task = ThreadedTask(self._task_upload_file, file_path, dup_choice, task_id) + + # 保存任务对象引用 + for i, t in enumerate(self.transfer_tasks): + if t["id"] == task_id: + self.transfer_tasks[i]["threaded_task"] = task + break + + self.active_tasks[task_id] = task + + def on_task_finished(tid): + if tid in self.active_tasks: + del self.active_tasks[tid] + + task.signals.progress.connect(lambda p, tid=task_id: ( + self.status.showMessage(f"上传进度: {p}%", 2000), + self.update_transfer_task(tid, p, "上传中") + )) + task.signals.result.connect(lambda r, tid=task_id: ( + self.status.showMessage("上传完成", 3000), + self.update_transfer_task(tid, 100, "已完成"), + on_task_finished(tid), + self.refresh_file_list(reset_page=False) + )) + task.signals.error.connect(lambda e, tid=task_id: ( + self.status.showMessage(f"上传出错: {e}", 3000), + self.update_transfer_task(tid, 0, "失败"), + on_task_finished(tid) + )) + + self.threadpool.start(task) + def on_about(self): + """打开关于对话框""" + dlg = AboutDialog(self) + dlg.exec() + def startup_login_flow(self): cfg_loaded = False config = ConfigManager.load_config() diff --git a/src/threading_utils.py b/src/threading_utils.py index 107751a..1a1675f 100644 --- a/src/threading_utils.py +++ b/src/threading_utils.py @@ -1,4 +1,6 @@ -"""多线程任务模块""" +# https://github.com/123panNextGen/123pan +# src/threading_utils.py + from PyQt6 import QtCore diff --git a/src/ui_widgets.py b/src/ui_widgets.py index 57c1b6b..51cace9 100644 --- a/src/ui_widgets.py +++ b/src/ui_widgets.py @@ -1,7 +1,7 @@ # https://github.com/123panNextGen/123pan # src/ui_widgets.py -from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6 import QtCore, QtWidgets import os from config import ConfigManager @@ -176,3 +176,62 @@ def on_ok(self): def get_pan(self): """获取登录成功的Pan对象""" return self.pan + + +class AboutDialog(QtWidgets.QDialog): + """关于对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("关于123pan") + self.setModal(True) + self.resize(450, 350) + self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint) + + layout = QtWidgets.QVBoxLayout(self) + + # 应用标题 + title = QtWidgets.QLabel("123pan") + title_font = title.font() + title_font.setPointSize(24) + title_font.setBold(True) + title.setFont(title_font) + title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + layout.addWidget(title) + + # 分隔线 + separator1 = QtWidgets.QFrame() + separator1.setFrameShape(QtWidgets.QFrame.Shape.HLine) + layout.addWidget(separator1) + + # 版本信息 + version_label = QtWidgets.QLabel("版本: 2.4.0") + version_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + layout.addWidget(version_label) + + # 描述 + description = QtWidgets.QLabel("123pan是一款基于Python开发的高效下载辅助工具,通过模拟安卓客户端协议,帮助用户绕过123云盘的自用下载流量限制,实现无阻碍下载体验。") + description.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + description.setWordWrap(True) + layout.addWidget(description) + + + # 分隔线 + separator3 = QtWidgets.QFrame() + separator3.setFrameShape(QtWidgets.QFrame.Shape.HLine) + layout.addWidget(separator3) + + # 项目信息 + project_info = QtWidgets.QLabel( + "GitHub: https://github.com/123panNextGen/123pan\n" + "By Qxyz17 xhdndmm \n" + "Apache License v2.0" + ) + project_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + project_info.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse) + layout.addWidget(project_info) + + # 关闭按钮 + btn_close = QtWidgets.QPushButton("关闭") + btn_close.clicked.connect(self.accept) + layout.addWidget(btn_close) From 9514e30fef8259cd6217b63ad147c87ac9c645f3 Mon Sep 17 00:00:00 2001 From: xhdndmm Date: Sun, 1 Feb 2026 18:45:01 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20v2.3.0!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- doc/img/image.png | Bin 0 -> 124829 bytes src/ui_widgets.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 doc/img/image.png diff --git a/README.md b/README.md index b5be05d..126000f 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,15 @@ Python Version Downloads
- +
+ + --- ## 📖 项目介绍 -[项目地址](https://github.com/Qxyz17/123pan) - 123pan是一款基于Python开发的高效下载辅助工具,通过模拟安卓客户端协议,帮助用户绕过123云盘的自用下载流量限制,实现无阻碍下载体验。 --- diff --git a/doc/img/image.png b/doc/img/image.png new file mode 100644 index 0000000000000000000000000000000000000000..c7ab5b4b5415c46c4b41e9a6c99267e63b042265 GIT binary patch literal 124829 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%0Ap!DJp?#Yo_alaub|+7M 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