mirror of
https://github.com/Bao-qing/123pan.git
synced 2026-02-12 13:00:31 +08:00
分离出core和cli,更新文档,打包脚本
This commit is contained in:
parent
18fcfc2c01
commit
cb737c0d95
|
|
@ -1,86 +0,0 @@
|
||||||
// ==UserScript==
|
|
||||||
// @name 123云盘下载辅助
|
|
||||||
// @namespace https://github.com/Bao-qing/123pan
|
|
||||||
// @version 0.3
|
|
||||||
// @description 123 Cloud Drive Unlimited Flow
|
|
||||||
// @match https://www.123pan.com/*
|
|
||||||
// @match https://www.123pan.cn/*
|
|
||||||
// @match https://www.123865.com/*
|
|
||||||
// @match https://www.123684.com/*
|
|
||||||
// @grant none
|
|
||||||
// @author Qing
|
|
||||||
// ==/UserScript==
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
// 重写 XMLHttpRequest
|
|
||||||
const originalXHR = window.XMLHttpRequest;
|
|
||||||
|
|
||||||
function newXHR() {
|
|
||||||
const realXHR = new originalXHR();
|
|
||||||
|
|
||||||
realXHR.open = function (method, url, async, user, password) {
|
|
||||||
this._url = url; // 记录请求的 URL
|
|
||||||
return originalXHR.prototype.open.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
realXHR.setRequestHeader = function (header, value) {
|
|
||||||
let headers = {
|
|
||||||
"user-agent": "123pan/v2.4.0(Android_7.1.2;Xiaomi)",
|
|
||||||
//"loginuuid": generateUUIDHex(),
|
|
||||||
"platform": "android",
|
|
||||||
"app-version": "61",
|
|
||||||
"x-app-version": "2.4.0"
|
|
||||||
}
|
|
||||||
// 如果header在列表中,则修改
|
|
||||||
if (header.toLowerCase() in headers) {
|
|
||||||
value = headers[header.toLowerCase()];
|
|
||||||
} else {
|
|
||||||
console.log('header:', header);
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalXHR.prototype.setRequestHeader.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 拦截响应内容,修改 DownloadUrl以适应网页端下载
|
|
||||||
realXHR.send = function () {
|
|
||||||
const xhrInstance = this;
|
|
||||||
this.addEventListener('readystatechange', function () {
|
|
||||||
let origin_url;
|
|
||||||
let new_url_no_redirect;
|
|
||||||
let base64data;
|
|
||||||
if (xhrInstance.readyState === 4 && xhrInstance.status === 200) {
|
|
||||||
// 解析响应的 JSON
|
|
||||||
let responseText = xhrInstance.responseText;
|
|
||||||
let responseJSON = JSON.parse(responseText);
|
|
||||||
console.log('Original Response:', responseJSON);
|
|
||||||
|
|
||||||
// 修改 DownloadUrl
|
|
||||||
if (responseJSON.data && responseJSON.data.DownloadUrl) {
|
|
||||||
origin_url = responseJSON.data.DownloadUrl;
|
|
||||||
new_url_no_redirect = origin_url + "&auto_redirect=0";
|
|
||||||
base64data = btoa(new_url_no_redirect);
|
|
||||||
responseJSON.data.DownloadUrl = "https://web-pro2.123952.com/download-v2/?params=" + base64data + "&is_s3=0";
|
|
||||||
console.log('Modified DownloadUrl:', responseJSON.data.DownloadUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将修改后的 JSON 转为字符串
|
|
||||||
let modifiedResponseText = JSON.stringify(responseJSON);
|
|
||||||
|
|
||||||
// 使用 defineProperty 重写 responseText
|
|
||||||
Object.defineProperty(xhrInstance, 'responseText', {
|
|
||||||
get: function () {
|
|
||||||
return modifiedResponseText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log('Modified Response:', modifiedResponseText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return originalXHR.prototype.send.apply(this, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
return realXHR;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.XMLHttpRequest = newXHR;
|
|
||||||
})();
|
|
||||||
371
README.md
371
README.md
|
|
@ -1,18 +1,43 @@
|
||||||
# 123Pan 下载工具
|
# 1、123Pan Cli工具
|
||||||
|
|
||||||
123 网盘命令行工具,支持列出文件、下载、上传、分享、删除、创建目录及回收站管理。Android 客户端或 Web 协议,便于在本地批量管理与下载文件。
|
123 网盘命令行工具,支持列出文件、下载、上传、分享、删除、创建目录及回收站管理。
|
||||||
|
|
||||||
安卓客户端协议不受下载流量限制(推荐使用)。
|
<!-- TOC -->
|
||||||
|
* [1、123Pan Cli工具](#1123pan-cli工具)
|
||||||
|
* [1.1 特性](#11-特性)
|
||||||
|
* [1.2 脚本环境要求](#12-脚本环境要求)
|
||||||
|
* [1.3 安装与运行](#13-安装与运行)
|
||||||
|
* [1.3.1 脚本运行](#131-脚本运行)
|
||||||
|
* [1.3.2 下载release版](#132-下载release版)
|
||||||
|
* [1.4 配置文件(JSON)](#14-配置文件json)
|
||||||
|
* [1.5 常用命令(交互式)](#15-常用命令交互式)
|
||||||
|
* [2、123Pan接口模块(pan123_core.py)](#2123pan接口模块pan123_corepy)
|
||||||
|
* [2.1 核心类:`Pan123Core`](#21-核心类pan123core)
|
||||||
|
* [2.1.1 属性说明](#211-属性说明)
|
||||||
|
* [2.1.2 方法清单](#212-方法清单)
|
||||||
|
* [2.1.2.1 (1)登录操作](#2121-1登录操作)
|
||||||
|
* [2.1.2.2 (2)配置管理](#2122-2配置管理)
|
||||||
|
* [2.1.2.3 (3)目录操作](#2123-3目录操作)
|
||||||
|
* [2.1.2.4 (4)文件操作](#2124-4文件操作)
|
||||||
|
* [2.1.2.5 (5)用户信息](#2125-5用户信息)
|
||||||
|
* [2.2 工具类:`Pan123Tool`](#22-工具类pan123tool)
|
||||||
|
* [2.2.1 属性说明](#221-属性说明)
|
||||||
|
* [2.2.2 方法清单](#222-方法清单)
|
||||||
|
* [2.2.2.1 (1)配置管理](#2221-1配置管理)
|
||||||
|
* [2.2.2.2 (2)文件下载](#2222-2文件下载)
|
||||||
|
* [2.2.2.3 (3)文件上传](#2223-3文件上传)
|
||||||
|
* [2.3 全局配置参数](#23-全局配置参数)
|
||||||
|
* [2.3.1 协议相关](#231-协议相关)
|
||||||
|
* [2.3.2 设备伪装](#232-设备伪装)
|
||||||
|
* [2.4 错误码说明](#24-错误码说明)
|
||||||
|
* [2.5 典型使用示例](#25-典型使用示例)
|
||||||
|
* [3、下载说明](#3下载说明)
|
||||||
|
* [4、注意事项](#4注意事项)
|
||||||
|
* [5、免责声明](#5免责声明)
|
||||||
|
<!-- TOC -->
|
||||||
|
|
||||||
123download.js 是网页端下载油猴脚本最初版本,仍然可以使用,仅保留最基本的解锁下载功能,不再更新。
|
## 1.1 特性
|
||||||
|
|
||||||
可以参考其他项目:
|
|
||||||
|
|
||||||
[123云盘解锁 (@QingJ)](https://greasyfork.org/zh-CN/scripts/519353-123%E4%BA%91%E7%9B%98%E8%A7%A3%E9%94%81)
|
|
||||||
|
|
||||||
[123 云盘会员青春版 (@hmjz100)](https://greasyfork.org/zh-CN/scripts/513528-123-%E4%BA%91%E7%9B%98%E4%BC%9A%E5%91%98%E9%9D%92%E6%98%A5%E7%89%88)
|
|
||||||
|
|
||||||
## 特性
|
|
||||||
- 登录 / 登出
|
- 登录 / 登出
|
||||||
- 列出当前目录文件(ls)
|
- 列出当前目录文件(ls)
|
||||||
- 切换目录(cd)与刷新(refresh / re)
|
- 切换目录(cd)与刷新(refresh / re)
|
||||||
|
|
@ -26,27 +51,35 @@
|
||||||
- 协议切换(protocol android|web)
|
- 协议切换(protocol android|web)
|
||||||
- 支持保存配置到 JSON 文件(authorization、device/os、protocol 等)
|
- 支持保存配置到 JSON 文件(authorization、device/os、protocol 等)
|
||||||
|
|
||||||
## 脚本环境要求
|
## 1.2 脚本环境要求
|
||||||
- Python 3.7+
|
|
||||||
|
- Python 3
|
||||||
- 依赖库:requests
|
- 依赖库:requests
|
||||||
安装:
|
安装:
|
||||||
```bash
|
```bash
|
||||||
pip install requests
|
pip install requests
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装与运行
|
## 1.3 安装与运行
|
||||||
### 脚本运行
|
|
||||||
|
### 1.3.1 脚本运行
|
||||||
|
|
||||||
1. 克隆或下载本仓库到本地。
|
1. 克隆或下载本仓库到本地。
|
||||||
2. 进入项目目录。
|
2. 进入项目目录。
|
||||||
3. 运行脚本:
|
3. 运行脚本:
|
||||||
```bash
|
```bash
|
||||||
python 123pan.py
|
python pan123_cli.py
|
||||||
```
|
```
|
||||||
启动后会提示输入用户名 / 密码,或自动读取配置文件(默认 `123pan_config.json` 或 `123pan.txt`,脚本内部根据传入参数使用该文件)。
|
启动后会提示输入用户名 / 密码,或自动读取配置文件(默认 `123pan_config.json`,脚本内部根据传入参数使用该文件)。
|
||||||
### 下载release版
|
|
||||||
根据系统下载对应的 release 版本,解压后运行 `123pan.exe`(Windows)或 `123pan`(Linux)。
|
### 1.3.2 下载release版
|
||||||
## 配置文件(JSON)
|
|
||||||
|
根据系统下载对应的 release 版本,解压后运行 `123pan.exe`(Windows)或 `123pan`(Linux)。
|
||||||
|
|
||||||
|
## 1.4 配置文件(JSON)
|
||||||
|
|
||||||
脚本会读取并保存一个配置文件(示例 `123pan_config.json`),保存登录状态与偏好,格式示例:
|
脚本会读取并保存一个配置文件(示例 `123pan_config.json`),保存登录状态与偏好,格式示例:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"userName": "your_username",
|
"userName": "your_username",
|
||||||
|
|
@ -57,72 +90,37 @@
|
||||||
"protocol": "android"
|
"protocol": "android"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:保存密码或 token 到本地会有安全风险,请在可信环境下使用并妥善保护该文件。
|
注意:保存密码或 token 到本地会有安全风险,请在可信环境下使用并妥善保护该文件。
|
||||||
|
|
||||||
## 常用命令(交互式)
|
## 1.5 常用命令(交互式)
|
||||||
|
|
||||||
在脚本交互提示符中输入命令,部分带参数:
|
在脚本交互提示符中输入命令,部分带参数:
|
||||||
|
|
||||||
- 直接输入编号
|
| 指令 | 用法示例 | 功能说明 |
|
||||||
- 若编号对应文件夹 → 进入该文件夹
|
|-----------------------------|----------------------------------------|----------------------------------|
|
||||||
- 若编号对应文件 → 直接下载该文件
|
| 直接输入编号 | `3` | 若编号对应文件夹 → 进入该文件夹;若为文件 → 直接下载该文件 |
|
||||||
|
| ls | `ls` | 显示当前目录的文件与文件夹列表 |
|
||||||
- ls
|
| cd [编号|..|/] | `cd 3`、`cd ..`、`cd /` | 切换目录:进入指定编号的文件夹、返回上级、返回根目录 |
|
||||||
- 显示当前目录的文件与文件夹列表。
|
| mkdir [名称] | `mkdir test` | 在当前目录创建文件夹 |
|
||||||
|
| upload [路径] | `upload C:\Users\you\Desktop\file.txt` | 上传文件到当前目录(仅支持单个文件) |
|
||||||
- cd [编号|..|/]
|
| rm [编号] | `rm 2` | 删除当前列表中指定编号的文件/文件夹(移入回收站) |
|
||||||
- 切换目录:
|
| share [编号 ...] | `share 2 4` | 为指定文件创建一个或多个分享链接,可设置提取码(可为空) |
|
||||||
- cd 3 —— 进入当前列表第3项(如果是文件夹)。
|
| link [编号] | `link 3` | 获取指定文件的直链地址 |
|
||||||
- cd .. —— 返回上级。
|
| download / d [编号] | `download 5` 或 `d 5` | 下载指定编号的文件或文件夹(文件夹将递归下载) |
|
||||||
- cd / —— 返回根目录。
|
| recycle | `recycle` | 查看回收站内容,可恢复指定编号项或输入 clear 清空回收站 |
|
||||||
|
| refresh / re | `refresh` 或 `re` | 刷新当前目录列表 |
|
||||||
- mkdir [名称]
|
| reload | `reload` | 重新加载配置文件并刷新目录 |
|
||||||
- 在当前目录创建文件夹。例如:mkdir test
|
| login / logout | `login`、`logout` | 手动登录或登出(清除授权信息) |
|
||||||
|
| clearaccount | clearaccount | 清除已登录账号(包括用户名和密码) |
|
||||||
- upload [路径]
|
| more | `more` | 当目录分页未加载完时,继续加载更多内容 |
|
||||||
- 上传文件到当前目录。例如:upload C:\Users\you\Desktop\file.txt
|
| protocol [android|web] | `protocol web` | 切换通信协议(如 android/web),并可选择保存配置 |
|
||||||
- 仅支持文件,暂不支持目录批量上传。
|
| exit | `exit` | 退出程序 |
|
||||||
|
|
||||||
- rm [编号]
|
|
||||||
- 删除当前列表中的文件/文件夹(会移动到回收站)。例如:rm 2
|
|
||||||
|
|
||||||
- share [编号 ...]
|
|
||||||
- 为一个或多个文件创建分享链接,例如:share 2 4
|
|
||||||
- 程序会提示输入提取码(可留空)。
|
|
||||||
|
|
||||||
- link [编号]
|
|
||||||
- 获取文件直链。例如:link 3
|
|
||||||
|
|
||||||
- download / d [编号]
|
|
||||||
- 下载指定编号的文件或文件夹(如果是文件夹,会递归下载)。
|
|
||||||
- 例如:download 5
|
|
||||||
|
|
||||||
- recycle
|
|
||||||
- 显示回收站内容,并可恢复或清空。
|
|
||||||
- 可输入编号恢复,或输入 clear 清空回收站。
|
|
||||||
|
|
||||||
- refresh / re
|
|
||||||
- 刷新当前目录列表。
|
|
||||||
|
|
||||||
- reload
|
|
||||||
- 重新加载配置文件并刷新目录。
|
|
||||||
|
|
||||||
- login / logout
|
|
||||||
- login:手动登录(使用当前配置或提示输入)。
|
|
||||||
- logout:登出并清除授权信息(保存配置时会写入空 token)。
|
|
||||||
|
|
||||||
- more
|
|
||||||
- 如果当前目录分页未加载完,输入 more 继续加载更多文件。
|
|
||||||
|
|
||||||
- protocol [android|web]
|
|
||||||
- 切换协议。例:protocol web
|
|
||||||
- 切换后会重新初始化请求头,并可选择保存到配置文件。
|
|
||||||
|
|
||||||
- exit
|
|
||||||
- 退出程序。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
交互示例:
|
交互示例:
|
||||||
|
|
||||||
```
|
```
|
||||||
/> cd demo
|
/> cd demo
|
||||||
无效命令,使用 '..' 返回上级,'/' 返回根目录,或输入文件夹编号
|
无效命令,使用 '..' 返回上级,'/' 返回根目录,或输入文件夹编号
|
||||||
|
|
@ -152,15 +150,226 @@
|
||||||
/demo1/test>
|
/demo1/test>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 下载说明
|
# 2、123Pan接口模块(pan123_core.py)
|
||||||
|
|
||||||
|
以下是基于代码实现的 **123pan 网盘 API**,按类结构分类说明:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.1 核心类:`Pan123Core`
|
||||||
|
|
||||||
|
负责与 123pan 服务器的通信,管理认证状态、目录浏览、文件操作等核心逻辑。
|
||||||
|
|
||||||
|
#### 2.1.1 属性说明
|
||||||
|
|
||||||
|
| 属性名 | 类型 | 描述 |
|
||||||
|
|-----------------|------------|-----------------------------|
|
||||||
|
| `user_name` | str | 登录账号(手机号/用户名) |
|
||||||
|
| `password` | str | 登录密码 |
|
||||||
|
| `authorization` | str | 认证 Token(登录后自动填充) |
|
||||||
|
| `protocol` | str | 请求协议(`"android"` 或 `"web"`) |
|
||||||
|
| `cwd_id` | int | 当前工作目录 ID(0 表示根目录) |
|
||||||
|
| `file_list` | List[dict] | 当前目录文件列表 |
|
||||||
|
| `nick_name` | str | 当前用户昵称 |
|
||||||
|
| `uid` | int | 当前用户 UID |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.1.2 方法清单
|
||||||
|
|
||||||
|
##### 2.1.2.1 (1)登录操作
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|-------------------|------|--------|----------------------------|
|
||||||
|
| `login()` | 无 | Result | 使用 `user_name/password` 登录 |
|
||||||
|
| `logout()` | 无 | Result | 登出并清除 Token |
|
||||||
|
| `check_login()` | 无 | Result | 检查当前 Token 是否有效 |
|
||||||
|
| `clear_account()` | 无 | Result | 清除账号信息(不保存配置) |
|
||||||
|
|
||||||
|
##### 2.1.2.2 (2)配置管理
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|--------------------------|-----------------------------------|--------|----------------------|
|
||||||
|
| `load_config(cfg)` | `cfg`: 包含账号信息的字典(见下方配置参数) | Result | 加载配置并更新实例状态 |
|
||||||
|
| `get_current_config()` | 无 | dict | 获取当前配置(账号、Token、协议等) |
|
||||||
|
| `set_protocol(protocol)` | `protocol`: `"android"` 或 `"web"` | Result | 切换请求协议 |
|
||||||
|
|
||||||
|
##### 2.1.2.3 (3)目录操作
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|-----------------------------------------------|---------------------------------------------------------|--------|--------------|
|
||||||
|
| `list_dir(parent_id=None, page=1, limit=100)` | `parent_id`: 父目录 ID<br>`page`: 页码<br>`limit`: 单页数量 | Result | 获取单页文件列表 |
|
||||||
|
| `list_dir_all(parent_id=None, limit=100)` | 同上 | Result | 获取全部文件(自动翻页) |
|
||||||
|
| `mkdir(name)` | `name`: 目录名 | Result | 在当前目录创建子目录 |
|
||||||
|
| `cd(folder_index)` | `folder_index`: `file_list` 中的目标文件夹下标 | Result | 进入目标文件夹 |
|
||||||
|
| `cd_up()` | 无 | Result | 返回上级目录 |
|
||||||
|
| `cd_root()` | 无 | Result | 返回根目录 |
|
||||||
|
| `trash(file_data, delete=True)` | `file_data`: 文件信息字典<br>`delete`: 是否删除(True=删除,False=恢复) | Result | 删除或恢复文件 |
|
||||||
|
| `list_recycle()` | 无 | Result | 获取回收站文件列表 |
|
||||||
|
|
||||||
|
##### 2.1.2.4 (4)文件操作
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|-------------------------------------------------------------------------|---------------------------------------------------------------------------------|--------|-----------------|
|
||||||
|
| `upload_file(file_path, duplicate=0, on_progress=None)` | `file_path`: 本地文件路径<br>`duplicate`: 冲突策略(0=报错,1=覆盖,2=保留)<br>`on_progress`: 进度回调 | Result | 上传文件(支持秒传和分块上传) |
|
||||||
|
| `get_download_url(index)` | `index`: `file_list` 中的目标文件下标 | Result | 获取文件直链(自动处理重定向) |
|
||||||
|
| `share(file_ids, share_pwd="", expiration="2099-12-12T08:00:00+08:00")` | `file_ids`: 文件 ID 列表<br>`share_pwd`: 提取码<br>`expiration`: 过期时间 | Result | 创建分享链接 |
|
||||||
|
|
||||||
|
##### 2.1.2.5 (5)用户信息
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|-------------------|------|--------|------------------------|
|
||||||
|
| `get_user_info()` | 无 | Result | 获取当前用户信息(UID、昵称、空间用量等) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 工具类:`Pan123Tool`
|
||||||
|
|
||||||
|
基于 `Pan123Core` 提供文件交互功能(依赖文件系统操作)。
|
||||||
|
|
||||||
|
#### 2.2.1 属性说明
|
||||||
|
|
||||||
|
| 属性名 | 类型 | 描述 |
|
||||||
|
|---------------|------------|-----------------------------------|
|
||||||
|
| `core` | Pan123Core | 关联的核心实例 |
|
||||||
|
| `config_file` | str | 配置文件路径(默认 `"123pan_config.json"`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2.2.2 方法清单
|
||||||
|
|
||||||
|
##### 2.2.2.1 (1)配置管理
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|---------------------------|------|--------|------------|
|
||||||
|
| `load_config_from_file()` | 无 | Result | 从文件加载配置 |
|
||||||
|
| `save_config_to_file()` | 无 | Result | 将当前配置保存到文件 |
|
||||||
|
|
||||||
|
##### 2.2.2.2 (2)文件下载
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|--------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------|--------|
|
||||||
|
| `download_file(index, save_dir="download", on_progress=None, overwrite=False, skip_existing=False)` | `index`: 文件列表下标<br>`save_dir`: 保存路径<br>`on_progress`: 进度回调<br>`overwrite`: 是否覆盖<br>`skip_existing`: 是否跳过已存在文件 | Result | 下载单个文件 |
|
||||||
|
| `download_directory(directory, save_dir="download", on_progress=None, overwrite=False, skip_existing=False)` | `directory`: 目录信息字典<br>其他参数同上 | Result | 递归下载目录 |
|
||||||
|
|
||||||
|
##### 2.2.2.3 (3)文件上传
|
||||||
|
|
||||||
|
| 方法名 | 参数说明 | 返回值类型 | 功能描述 |
|
||||||
|
|---------------------------------------------------------|----------------------------|--------|-------------------|
|
||||||
|
| `upload_file(file_path, duplicate=0, on_progress=None)` | 同 `Pan123Core.upload_file` | Result | 上传文件(与 Core 方法一致) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 全局配置参数
|
||||||
|
|
||||||
|
#### 2.3.1 协议相关
|
||||||
|
|
||||||
|
| 参数名 | 默认值 | 描述 |
|
||||||
|
|-----------------------|----------------------------|-----------------|
|
||||||
|
| `API_BASE_URL` | `"https://www.123pan.com"` | API 根地址 |
|
||||||
|
| `TIMEOUT_DEFAULT` | `15` | 默认请求超时时间(秒) |
|
||||||
|
| `UPLOAD_CHUNK_SIZE` | `5*1024*1024` | 分块上传单块大小(5MB) |
|
||||||
|
| `DOWNLOAD_CHUNK_SIZE` | `8192` | 下载流式读取单块大小(8KB) |
|
||||||
|
|
||||||
|
#### 2.3.2 设备伪装
|
||||||
|
|
||||||
|
| 参数名 | 默认值 | 描述 |
|
||||||
|
|----------------|-----|-------------------|
|
||||||
|
| `DEVICE_TYPES` | 见代码 | 可选 Android 设备型号列表 |
|
||||||
|
| `OS_VERSIONS` | 见代码 | 可选 Android 系统版本列表 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 错误码说明
|
||||||
|
|
||||||
|
| 错误码 | 含义 | 可能触发场景 |
|
||||||
|
|------|--------|----------------------------|
|
||||||
|
| 0 | 操作成功 | 所有接口成功时返回 |
|
||||||
|
| -1 | 网络请求失败 | 连接超时、SSL 错误等 |
|
||||||
|
| 5060 | 文件名冲突 | 上传时 `duplicate=0` 且目标文件已存在 |
|
||||||
|
| 1 | 本地文件冲突 | 下载时目标文件已存在 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 典型使用示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pan123_core import Pan123Core, Pan123Tool, Pan123EventType, format_size
|
||||||
|
|
||||||
|
# 初始化核心对象(Android 协议)
|
||||||
|
core = Pan123Core(
|
||||||
|
user_name="13800138000",
|
||||||
|
password="your_password",
|
||||||
|
protocol=Pan123Core.PROTOCOL_ANDROID
|
||||||
|
)
|
||||||
|
|
||||||
|
# 登录
|
||||||
|
result = core.login()
|
||||||
|
if result["code"] != 0:
|
||||||
|
raise Exception("登录失败")
|
||||||
|
|
||||||
|
# 创建工具类实例
|
||||||
|
tool = Pan123Tool(core)
|
||||||
|
|
||||||
|
|
||||||
|
# 下载文件
|
||||||
|
def _download_progress(data) -> None:
|
||||||
|
if data.get("type") == Pan123EventType.DOWNLOAD_PROGRESS:
|
||||||
|
downloaded = data.get("downloaded", 0)
|
||||||
|
total = data.get("total", 0)
|
||||||
|
speed = data.get("speed", 0)
|
||||||
|
if total > 0:
|
||||||
|
pct = downloaded / total * 100
|
||||||
|
print(
|
||||||
|
f"\r进度: {pct:.1f}% | {format_size(downloaded)}/{format_size(total)} | {format_size(int(speed))}/s",
|
||||||
|
end=" ",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
elif data.get("type") == Pan123EventType.DOWNLOAD_START_FILE:
|
||||||
|
print(f"开始下载: {data.get('file_name', '未知文件')} ({format_size(data.get('file_size', 0))})")
|
||||||
|
elif data.get("type") == Pan123EventType.DOWNLOAD_START_DIRECTORY:
|
||||||
|
print(f"开始下载目录: {data.get('dir_name', '未知目录')}")
|
||||||
|
else:
|
||||||
|
print(json.dumps(data, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
result = tool.download_file(
|
||||||
|
index=0,
|
||||||
|
save_dir="downloads",
|
||||||
|
on_progress=lambda e: print(f"下载进度: {e['percent']:.2f}%")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 上传文件
|
||||||
|
result = core.upload_file(
|
||||||
|
file_path="local_file.txt",
|
||||||
|
duplicate=1, # 覆盖已有文件
|
||||||
|
on_progress=None # ...
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建分享链接
|
||||||
|
result = core.share(
|
||||||
|
file_ids=[12345],
|
||||||
|
share_pwd="123456",
|
||||||
|
expiration="2026-12-31T23:59:59+08:00"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3、下载说明
|
||||||
|
|
||||||
- 下载到脚本所在目录的 `download` 文件夹,下载过程中使用临时后缀 `.123pan`,下载完成后会重命名为原文件名。
|
- 下载到脚本所在目录的 `download` 文件夹,下载过程中使用临时后缀 `.123pan`,下载完成后会重命名为原文件名。
|
||||||
- 如果文件已存在,会提示覆盖 / 跳过 / 全部覆盖 / 全部跳过等选项。
|
- 如果文件已存在,会提示覆盖 / 跳过 / 全部覆盖 / 全部跳过等选项。
|
||||||
|
|
||||||
## 注意事项
|
# 4、注意事项
|
||||||
|
|
||||||
- 本工具用于学习与自用场景,请勿用于违法用途。对任何滥用造成的后果,本人概不负责。
|
- 本工具用于学习与自用场景,请勿用于违法用途。对任何滥用造成的后果,本人概不负责。
|
||||||
- 模拟客户端协议可能存在账号或服务端策略风险,请谨慎使用。
|
- 模拟客户端协议可能存在账号或服务端策略风险,请谨慎使用。
|
||||||
- 建议不要在公用或不受信任的机器上保存明文密码或授权信息。
|
- 建议不要在公用或不受信任的机器上保存明文密码或授权信息。
|
||||||
|
|
||||||
## 免责声明
|
# 5、免责声明
|
||||||
|
|
||||||
本工具用于学习场景,请勿用于违法用途。对任何滥用造成的后果,作者概不负责。
|
本工具用于学习场景,请勿用于违法用途。对任何滥用造成的后果,作者概不负责。
|
||||||
|
任何未经允许的api调用都是不被官方允许的,对于因此产生的账号风险、数据损失等后果自负。
|
||||||
2
pack.sh
2
pack.sh
|
|
@ -1 +1 @@
|
||||||
pyinstaller -F 123pan.py --icon favicon.ico --clean --noconfirm
|
pyinstaller -F pan123_cli.py --icon favicon.ico --clean --noconfirm
|
||||||
103
pan123_cli.py
103
pan123_cli.py
|
|
@ -2,11 +2,12 @@
|
||||||
123pan 控制台交互界面 —— 仅负责用户 IO,所有业务调用 Pan123Core。
|
123pan 控制台交互界面 —— 仅负责用户 IO,所有业务调用 Pan123Core。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import List
|
from typing import Dict
|
||||||
|
|
||||||
from pan123_core import Pan123Core, format_size, make_result
|
from pan123_core import Pan123Core, Pan123Tool, Pan123EventType, format_size
|
||||||
|
|
||||||
|
|
||||||
# ──────────────── 颜色工具 ────────────────
|
# ──────────────── 颜色工具 ────────────────
|
||||||
|
|
@ -50,7 +51,9 @@ class Pan123CLI:
|
||||||
exit - 退出程序"""
|
exit - 退出程序"""
|
||||||
|
|
||||||
def __init__(self, config_file: str = "123pan_config.json"):
|
def __init__(self, config_file: str = "123pan_config.json"):
|
||||||
self.core = Pan123Core(config_file=config_file)
|
self.config_file: str = config_file
|
||||||
|
self.core = Pan123Core()
|
||||||
|
self.tool = Pan123Tool(self.core)
|
||||||
self._download_mode: int = 0 # 0=询问, 3=全部覆盖, 4=全部跳过
|
self._download_mode: int = 0 # 0=询问, 3=全部覆盖, 4=全部跳过
|
||||||
|
|
||||||
# ──────────────── 启动 ────────────────
|
# ──────────────── 启动 ────────────────
|
||||||
|
|
@ -62,11 +65,36 @@ class Pan123CLI:
|
||||||
os.system("")
|
os.system("")
|
||||||
|
|
||||||
self._print_banner()
|
self._print_banner()
|
||||||
self._init_login()
|
if not self._init_login():
|
||||||
|
print(colored("无法登录", Color.RED))
|
||||||
|
a = input("输入1重新输入账号和密码,输入2清除登录信息,其他键退出: ")
|
||||||
|
if a == "1":
|
||||||
|
user_name = input("请输入用户名: ")
|
||||||
|
password = input("请输入密码: ")
|
||||||
|
if not user_name or not password:
|
||||||
|
print("用户名和密码不能为空,程序退出")
|
||||||
|
return
|
||||||
|
self.core.load_config({
|
||||||
|
"userName": user_name,
|
||||||
|
"passWord": password,
|
||||||
|
"authorization": ""
|
||||||
|
})
|
||||||
|
self.save_config()
|
||||||
|
return self.run()
|
||||||
|
if a == "2":
|
||||||
|
self._do_clear_account()
|
||||||
|
return self.run()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.save_config()
|
||||||
|
self.core.refresh() # 加载文件列表
|
||||||
|
self.core.get_user_info()
|
||||||
|
self._show_files()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
prompt = colored(f"{self.core.cwd_path}>", Color.RED) + " "
|
prompt = colored(f"{self.core.cwd_path}>", Color.RED) + " "
|
||||||
|
print(colored(f'用户:{self.core.nick_name}', Color.GREEN))
|
||||||
command = input(prompt).strip()
|
command = input(prompt).strip()
|
||||||
if not command:
|
if not command:
|
||||||
continue
|
continue
|
||||||
|
|
@ -82,26 +110,39 @@ class Pan123CLI:
|
||||||
|
|
||||||
def _print_banner(self) -> None:
|
def _print_banner(self) -> None:
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("123网盘客户端".center(56))
|
print("123网盘CLI客户端".center(56))
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
def _init_login(self) -> None:
|
def _init_login(self) -> bool:
|
||||||
"""尝试加载配置 -> 尝试访问目录 -> 必要时登录"""
|
"""尝试加载配置 -> 尝试访问目录 -> 必要时登录"""
|
||||||
self.core.load_config()
|
res = self.load_config()
|
||||||
|
r = self.core.init_login_state()
|
||||||
|
if r["code"] < 0:
|
||||||
|
print(colored("登录失败", Color.YELLOW))
|
||||||
|
print(r["message"])
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
r = self.core.refresh()
|
def load_config(self) -> Dict:
|
||||||
if r["code"] != 0:
|
"""加载配置"""
|
||||||
# 需要登录
|
try:
|
||||||
if not self.core.user_name:
|
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||||
self.core.user_name = input("请输入用户名: ")
|
cfg = json.load(f)
|
||||||
if not self.core.password:
|
except FileNotFoundError:
|
||||||
self.core.password = input("请输入密码: ")
|
user_name = input("请输入用户名: ")
|
||||||
lr = self.core.login()
|
password = input("请输入密码: ")
|
||||||
self._print_result(lr)
|
cfg = {
|
||||||
if lr["code"] == 0:
|
"userName": user_name,
|
||||||
self.core.refresh()
|
"passWord": password,
|
||||||
|
"authorization": ""
|
||||||
|
}
|
||||||
|
return self.core.load_config(cfg)
|
||||||
|
|
||||||
self._show_files()
|
def save_config(self) -> None:
|
||||||
|
"""保存配置"""
|
||||||
|
cfg = self.core.get_current_config()
|
||||||
|
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
# ──────────────── 命令分发 ────────────────
|
# ──────────────── 命令分发 ────────────────
|
||||||
|
|
||||||
|
|
@ -185,6 +226,7 @@ class Pan123CLI:
|
||||||
|
|
||||||
def _do_logout(self) -> None:
|
def _do_logout(self) -> None:
|
||||||
r = self.core.logout()
|
r = self.core.logout()
|
||||||
|
self.save_config()
|
||||||
self._print_result(r)
|
self._print_result(r)
|
||||||
|
|
||||||
def _do_clear_account(self) -> None:
|
def _do_clear_account(self) -> None:
|
||||||
|
|
@ -192,6 +234,7 @@ class Pan123CLI:
|
||||||
confirm = input("确定要清除已登录账号信息吗?(y/N): ").strip().lower()
|
confirm = input("确定要清除已登录账号信息吗?(y/N): ").strip().lower()
|
||||||
if confirm == 'y':
|
if confirm == 'y':
|
||||||
r = self.core.clear_account()
|
r = self.core.clear_account()
|
||||||
|
self.save_config()
|
||||||
self._print_result(r)
|
self._print_result(r)
|
||||||
else:
|
else:
|
||||||
print("操作已取消")
|
print("操作已取消")
|
||||||
|
|
@ -275,7 +318,7 @@ class Pan123CLI:
|
||||||
return
|
return
|
||||||
r = self.core.get_download_url(int(arg) - 1)
|
r = self.core.get_download_url(int(arg) - 1)
|
||||||
if r["code"] == 0:
|
if r["code"] == 0:
|
||||||
print(f"文件直链: {r['data']['url']}")
|
print(f"文件直链: \n{r['data']['url']}")
|
||||||
else:
|
else:
|
||||||
self._print_result(r)
|
self._print_result(r)
|
||||||
|
|
||||||
|
|
@ -292,7 +335,7 @@ class Pan123CLI:
|
||||||
|
|
||||||
overwrite = self._download_mode == 3
|
overwrite = self._download_mode == 3
|
||||||
skip = self._download_mode == 4
|
skip = self._download_mode == 4
|
||||||
r = self.core.download_file(
|
r = self.tool.download_file(
|
||||||
idx,
|
idx,
|
||||||
on_progress=self._download_progress,
|
on_progress=self._download_progress,
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
|
|
@ -311,7 +354,7 @@ class Pan123CLI:
|
||||||
return
|
return
|
||||||
elif choice == "3":
|
elif choice == "3":
|
||||||
self._download_mode = 3
|
self._download_mode = 3
|
||||||
r = self.core.download_file(idx, on_progress=self._download_progress, overwrite=True)
|
r = self.tool.download_file(idx, on_progress=self._download_progress, overwrite=True)
|
||||||
|
|
||||||
print() # 换行
|
print() # 换行
|
||||||
self._print_result(r)
|
self._print_result(r)
|
||||||
|
|
@ -378,7 +421,11 @@ class Pan123CLI:
|
||||||
# ──────────────── 进度回调 ────────────────
|
# ──────────────── 进度回调 ────────────────
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _download_progress(downloaded: int, total: int, speed: float) -> None:
|
def _download_progress(data) -> None:
|
||||||
|
if data.get("type") == Pan123EventType.DOWNLOAD_PROGRESS:
|
||||||
|
downloaded = data.get("downloaded", 0)
|
||||||
|
total = data.get("total", 0)
|
||||||
|
speed = data.get("speed", 0)
|
||||||
if total > 0:
|
if total > 0:
|
||||||
pct = downloaded / total * 100
|
pct = downloaded / total * 100
|
||||||
print(
|
print(
|
||||||
|
|
@ -386,9 +433,17 @@ class Pan123CLI:
|
||||||
end=" ",
|
end=" ",
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
|
elif data.get("type") == Pan123EventType.DOWNLOAD_START_FILE:
|
||||||
|
print(f"开始下载: {data.get('file_name', '未知文件')} ({format_size(data.get('file_size', 0))})")
|
||||||
|
elif data.get("type") == Pan123EventType.DOWNLOAD_START_DIRECTORY:
|
||||||
|
print(f"开始下载目录: {data.get('dir_name', '未知目录')}")
|
||||||
|
else:
|
||||||
|
print(json.dumps(data, indent=2))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _upload_progress(uploaded: int, total: int) -> None:
|
def _upload_progress(data) -> None:
|
||||||
|
uploaded = data.get("uploaded", 0)
|
||||||
|
total = data.get("total", 0)
|
||||||
if total > 0:
|
if total > 0:
|
||||||
pct = uploaded / total * 100
|
pct = uploaded / total * 100
|
||||||
print(f"\r上传进度: {pct:.1f}%", end="", flush=True)
|
print(f"\r上传进度: {pct:.1f}%", end="", flush=True)
|
||||||
|
|
|
||||||
765
pan123_core.py
765
pan123_core.py
|
|
@ -1,12 +1,9 @@
|
||||||
"""
|
"""
|
||||||
123pan 网盘内核模块 —— 纯业务逻辑,无任何 IO(print / input)。
|
123pan 网盘内核模块
|
||||||
|
|
||||||
可直接移植到 GUI / Web / API 等上层应用。
|
|
||||||
所有公开方法统一返回 Result 字典::
|
所有公开方法统一返回 Result 字典::
|
||||||
|
|
||||||
{
|
{
|
||||||
"code": int, # 0 = 成功,非 0 = 失败(含 API 原始错误码)
|
"code": int, # 0 = 成功,小于 0 = 失败 大于 0 = 警告
|
||||||
"message": str, # 人类可读的结果描述
|
"message": str, # 结果描述
|
||||||
"data": Any # 业务数据,失败时为 None
|
"data": Any # 业务数据,失败时为 None
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
@ -18,11 +15,11 @@ import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
# 全局常量 —— URL / 端点 / 超时 / 分块 / 设备信息
|
# 全局常量 —— URL / 端点 / 超时 / 分块 / 设备信息
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -48,7 +45,7 @@ URL_DOWNLOAD_INFO = "/a/api/file/download_info"
|
||||||
"""单文件下载信息接口"""
|
"""单文件下载信息接口"""
|
||||||
|
|
||||||
URL_BATCH_DOWNLOAD = "/a/api/file/batch_download_info"
|
URL_BATCH_DOWNLOAD = "/a/api/file/batch_download_info"
|
||||||
"""批量(文件夹)下载信息接口"""
|
"""批量(文件夹)下载信息接口 服务端会进行打包下载"""
|
||||||
|
|
||||||
URL_UPLOAD_REQUEST = "/b/api/file/upload_request"
|
URL_UPLOAD_REQUEST = "/b/api/file/upload_request"
|
||||||
"""上传请求接口(含创建目录)"""
|
"""上传请求接口(含创建目录)"""
|
||||||
|
|
@ -65,6 +62,12 @@ URL_UPLOAD_COMPLETE = "/b/api/file/upload_complete"
|
||||||
URL_MKDIR = "/a/api/file/upload_request"
|
URL_MKDIR = "/a/api/file/upload_request"
|
||||||
"""创建目录接口(复用 upload_request,type=1)"""
|
"""创建目录接口(复用 upload_request,type=1)"""
|
||||||
|
|
||||||
|
URL_USER_INFO = "/b/api/user/info"
|
||||||
|
"""获取用户信息接口"""
|
||||||
|
|
||||||
|
URL_DETAILS = "/b/api/restful/goapi/v1/file/details"
|
||||||
|
"""获取文件夹详情接口"""
|
||||||
|
|
||||||
SHARE_URL_TEMPLATE = "{base}/s/{key}"
|
SHARE_URL_TEMPLATE = "{base}/s/{key}"
|
||||||
"""分享链接模板,{base} = API_BASE_URL,{key} = ShareKey"""
|
"""分享链接模板,{base} = API_BASE_URL,{key} = ShareKey"""
|
||||||
|
|
||||||
|
|
@ -155,6 +158,15 @@ WEB_USER_AGENT = (
|
||||||
WEB_APP_VERSION = "3"
|
WEB_APP_VERSION = "3"
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 事件类型 ────────────────────────────────────────────────
|
||||||
|
@dataclass
|
||||||
|
class Pan123EventType:
|
||||||
|
DOWNLOAD_START_FILE = "download_start_file"
|
||||||
|
DOWNLOAD_START_DIRECTORY = "download_start_directory"
|
||||||
|
DOWNLOAD_PROGRESS: str = "download_progress"
|
||||||
|
UPLOAD_PROGRESS: str = "upload_progress"
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
# 工具函数
|
# 工具函数
|
||||||
# ════════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════════
|
||||||
|
|
@ -163,7 +175,7 @@ def make_result(code: int = CODE_OK, message: str = "ok", data: Any = None) -> D
|
||||||
"""构造统一返回结构。
|
"""构造统一返回结构。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
code: 状态码,0 表示成功,非 0 表<EFBFBD><EFBFBD>失败。
|
code: 状态码,0 表示成功,小于 0 表示失败,大于 0 表示成功但有警告信息
|
||||||
message: 人类可读的结果描述。
|
message: 人类可读的结果描述。
|
||||||
data: 业务数据,失败时通常为 None。
|
data: 业务数据,失败时通常为 None。
|
||||||
|
|
||||||
|
|
@ -226,9 +238,8 @@ ProgressCallback = Optional[Callable[..., None]]
|
||||||
class Pan123Core:
|
class Pan123Core:
|
||||||
"""123 网盘内核类。
|
"""123 网盘内核类。
|
||||||
|
|
||||||
提供登录、目录浏览、上传、下载、分享、删除、回收站等纯逻辑接口。
|
提供登录、目录浏览、上传、下载链接、分享、删除、回收站等纯逻辑接口。
|
||||||
不做任何 print / input,所有结果通过 ``make_result`` 统一返回,
|
所有结果通过 ``make_result`` 统一返回,
|
||||||
方便上层(CLI / GUI / Web)自行处理展示与交互。
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
user_name (str): 登录用户名 / 手机号。
|
user_name (str): 登录用户名 / 手机号。
|
||||||
|
|
@ -236,8 +247,8 @@ class Pan123Core:
|
||||||
authorization (str): Bearer Token,登录后自动填充。
|
authorization (str): Bearer Token,登录后自动填充。
|
||||||
protocol (str): 请求协议,"android" 或 "web"。
|
protocol (str): 请求协议,"android" 或 "web"。
|
||||||
config_file (str): 配置文件路径。
|
config_file (str): 配置文件路径。
|
||||||
device_type (str): Android 设备型号。
|
device_type (str): Android 设备型号。 留空则随机选取 DEVICE_TYPES 中的一个。
|
||||||
os_version (str): Android 系统版本。
|
os_version (str): Android 系统版本。 留空则随机选取 OS_VERSIONS 中的一个。
|
||||||
cwd_id (int): 当前工作目录 FileId(0 = 根目录)。
|
cwd_id (int): 当前工作目录 FileId(0 = 根目录)。
|
||||||
cwd_stack (List[int]): 目录 ID 导航栈。
|
cwd_stack (List[int]): 目录 ID 导航栈。
|
||||||
cwd_name_stack (List[str]): 目录名称导航栈。
|
cwd_name_stack (List[str]): 目录名称导航栈。
|
||||||
|
|
@ -246,6 +257,13 @@ class Pan123Core:
|
||||||
all_loaded (bool): 当前目录是否已全部加载。
|
all_loaded (bool): 当前目录是否已全部加载。
|
||||||
cookies (Optional[Dict]): 登录后保存的 Cookie。
|
cookies (Optional[Dict]): 登录后保存的 Cookie。
|
||||||
headers (Dict[str, str]): 当前使用的请求头。
|
headers (Dict[str, str]): 当前使用的请求头。
|
||||||
|
|
||||||
|
nick_name (str): 当前用户昵称(获取用户信息时填充)。
|
||||||
|
uid (int): 当前用户 UID(获取用户信息时填充)。
|
||||||
|
|
||||||
|
:note:
|
||||||
|
流程:初始化内核实例 -> 加载配置(可以初始化时提供)-> 初始化登录状态(self.init_login_state()) -> 进行目录浏览 / 上传 / 下载等操作 -> 需要时保存配置
|
||||||
|
配置说明:如果传入了authorization,会先使用它尝试获取用户信息来验证登录状态,如果无效则根据提供的用户名和密码重新登录;如果未传入authorization,则直接根据用户名和密码登录。登录成功后会更新authorization属性。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ── 协议常量 ──────────────────────────────────────────────
|
# ── 协议常量 ──────────────────────────────────────────────
|
||||||
|
|
@ -258,9 +276,9 @@ class Pan123Core:
|
||||||
password: str = "",
|
password: str = "",
|
||||||
authorization: str = "",
|
authorization: str = "",
|
||||||
protocol: str = PROTOCOL_ANDROID,
|
protocol: str = PROTOCOL_ANDROID,
|
||||||
config_file: str = "123pan.txt",
|
|
||||||
device_type: str = "",
|
device_type: str = "",
|
||||||
os_version: str = "",
|
os_version: str = "",
|
||||||
|
# config_file: str = "123pan_config.json",
|
||||||
):
|
):
|
||||||
"""初始化内核实例。
|
"""初始化内核实例。
|
||||||
|
|
||||||
|
|
@ -272,6 +290,7 @@ class Pan123Core:
|
||||||
config_file: 配置文件路径,用于持久化账号和 Token。
|
config_file: 配置文件路径,用于持久化账号和 Token。
|
||||||
device_type: 指定 Android 设备型号,为空则随机选取。
|
device_type: 指定 Android 设备型号,为空则随机选取。
|
||||||
os_version: 指定 Android 系统版本,为空则随机选取。
|
os_version: 指定 Android 系统版本,为空则随机选取。
|
||||||
|
use_config_file: 是否在初始化时自动从配置文件加载账号信息和 Token,默认为 False,以避免内核直接依赖文件系统
|
||||||
"""
|
"""
|
||||||
# 账号信息
|
# 账号信息
|
||||||
self.user_name: str = user_name
|
self.user_name: str = user_name
|
||||||
|
|
@ -285,7 +304,7 @@ class Pan123Core:
|
||||||
self.login_uuid: str = uuid.uuid4().hex
|
self.login_uuid: str = uuid.uuid4().hex
|
||||||
|
|
||||||
# 配置文件
|
# 配置文件
|
||||||
self.config_file: str = config_file
|
# self.config_file: str = config_file
|
||||||
|
|
||||||
# 目录导航状态
|
# 目录导航状态
|
||||||
self.cwd_id: int = 0
|
self.cwd_id: int = 0
|
||||||
|
|
@ -305,6 +324,10 @@ class Pan123Core:
|
||||||
self.headers: Dict[str, str] = {}
|
self.headers: Dict[str, str] = {}
|
||||||
self._build_headers()
|
self._build_headers()
|
||||||
|
|
||||||
|
# 运行参数
|
||||||
|
self.nick_name = None
|
||||||
|
self.uid = None
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 请求头构建
|
# 请求头构建
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
@ -364,10 +387,13 @@ class Pan123Core:
|
||||||
# 配置持久化
|
# 配置持久化
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self, cfg: Dict) -> Dict[str, Any]:
|
||||||
"""从配置文件加载账号信息、Token 及协议设置。
|
"""从配置加载账号信息、Token 及协议设置。仅更新cfg中存在的字段,
|
||||||
|
|
||||||
会自<EFBFBD><EFBFBD><EFBFBD>重建 headers 并同步 authorization。
|
会自动重建 headers 并同步 authorization。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cfg: { userName: str, passWord: str, authorization: str, deviceType: str, osVersion: str, protocol: str }
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Result 字典::
|
Result 字典::
|
||||||
|
|
@ -375,11 +401,7 @@ class Pan123Core:
|
||||||
成功: {"code": 0, "message": "配置加载成功", "data": {配置内容 dict}}
|
成功: {"code": 0, "message": "配置加载成功", "data": {配置内容 dict}}
|
||||||
失败: {"code": -1, "message": "错误描述", "data": None}
|
失败: {"code": -1, "message": "错误描述", "data": None}
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(self.config_file):
|
|
||||||
return make_result(-1, "配置文件不存在")
|
|
||||||
try:
|
try:
|
||||||
with open(self.config_file, "r", encoding="utf-8") as f:
|
|
||||||
cfg = json.load(f)
|
|
||||||
self.user_name = cfg.get("userName", self.user_name)
|
self.user_name = cfg.get("userName", self.user_name)
|
||||||
self.password = cfg.get("passWord", self.password)
|
self.password = cfg.get("passWord", self.password)
|
||||||
self.authorization = cfg.get("authorization", self.authorization)
|
self.authorization = cfg.get("authorization", self.authorization)
|
||||||
|
|
@ -392,16 +414,22 @@ class Pan123Core:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return make_result(-1, f"加载配置失败: {e}")
|
return make_result(-1, f"加载配置失败: {e}")
|
||||||
|
|
||||||
def save_config(self) -> Dict[str, Any]:
|
def get_current_config(self) -> Dict[str, Any]:
|
||||||
"""将当前账号信息、Token 及协议设置保存到配置文件。
|
"""获取当前账号信息、Token 及协议设置的字典表示。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Result 字典::
|
当前配置的字典,例如::
|
||||||
|
|
||||||
成功: {"code": 0, "message": "配置已保存", "data": {配置内容 dict}}
|
{
|
||||||
失败: {"code": -1, "message": "错误描述", "data": None}
|
"userName": str,
|
||||||
|
"passWord": str,
|
||||||
|
"authorization": str,
|
||||||
|
"deviceType": str,
|
||||||
|
"osVersion": str,
|
||||||
|
"protocol": str,
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
cfg = {
|
return {
|
||||||
"userName": self.user_name,
|
"userName": self.user_name,
|
||||||
"passWord": self.password,
|
"passWord": self.password,
|
||||||
"authorization": self.authorization,
|
"authorization": self.authorization,
|
||||||
|
|
@ -409,12 +437,6 @@ class Pan123Core:
|
||||||
"osVersion": self.os_version,
|
"osVersion": self.os_version,
|
||||||
"protocol": self.protocol,
|
"protocol": self.protocol,
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
with open(self.config_file, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
|
||||||
return make_result(CODE_OK, "配置已保存", cfg)
|
|
||||||
except Exception as e:
|
|
||||||
return make_result(-1, f"保存配置失败: {e}")
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 统一网络请求
|
# 统一网络请求
|
||||||
|
|
@ -445,7 +467,7 @@ class Pan123Core:
|
||||||
Result 字典::
|
Result 字典::
|
||||||
|
|
||||||
成功: {"code": 0, "message": "ok", "data": {API 原始响应 JSON}}
|
成功: {"code": 0, "message": "ok", "data": {API 原始响应 JSON}}
|
||||||
失败: {"code": <API错误码或-1>, "message": "错误描述", "data": {API响应} | None}
|
失败: {"code": <0, "message": "错误描述", "data": {API响应} | None}
|
||||||
"""
|
"""
|
||||||
url = f"{API_BASE_URL}{path}" if path.startswith("/") else path
|
url = f"{API_BASE_URL}{path}" if path.startswith("/") else path
|
||||||
try:
|
try:
|
||||||
|
|
@ -458,14 +480,46 @@ class Pan123Core:
|
||||||
)
|
)
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
api_code = data.get("code", -1)
|
api_code = data.get("code", -1)
|
||||||
# 123pan 登录成功返回 200,其余接口成功返回 0
|
# 123pan 登录成功/退出登录 成功返回 code 200,其余接口成功返回 0
|
||||||
if api_code not in (CODE_OK, CODE_LOGIN_OK):
|
if api_code not in (CODE_OK, CODE_LOGIN_OK):
|
||||||
return make_result(api_code, data.get("message", "未知错误"), data)
|
return make_result(-3, data.get("message", "未知错误"), data)
|
||||||
return make_result(CODE_OK, "ok", data)
|
return make_result(CODE_OK, "ok", data)
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
return make_result(-1, f"请求失败: {e}")
|
return make_result(-1, f"请求失败: {e}")
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return make_result(-1, "响应 JSON 解析错误")
|
return make_result(-2, "响应 JSON 解析错误")
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# 用户信息
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def get_user_info(self) -> Dict[str, Any]:
|
||||||
|
"""获取当前登录用户的信息。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result:
|
||||||
|
成功: {"code": 0, "message": "ok", "data": {用户信息 dict}}
|
||||||
|
失败: {"code": <错误码>, "message": "错误描述", "data": None}
|
||||||
|
data: {
|
||||||
|
"UID": ,
|
||||||
|
"Nickname": "",
|
||||||
|
"SpaceUsed": ,
|
||||||
|
"SpacePermanent": ,
|
||||||
|
"SpaceTemp": 0,
|
||||||
|
"FileCount": ,
|
||||||
|
"SpaceTempExpr": "",
|
||||||
|
"Mail": "",
|
||||||
|
"Passport": ,
|
||||||
|
"HeadImage": "",
|
||||||
|
......
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
user_info_res = self._request("GET", URL_USER_INFO)
|
||||||
|
if user_info_res["code"] != CODE_OK:
|
||||||
|
return make_result(user_info_res["code"], f"获取用户信息失败: {user_info_res['message']}")
|
||||||
|
self.nick_name = user_info_res["data"]["data"].get("Nickname", "")
|
||||||
|
self.uid = user_info_res["data"]["data"].get("UID", None)
|
||||||
|
return make_result(CODE_OK, "ok", user_info_res["data"]["data"])
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 登录 / 登出
|
# 登录 / 登出
|
||||||
|
|
@ -478,7 +532,7 @@ class Pan123Core:
|
||||||
Result 字典::
|
Result 字典::
|
||||||
|
|
||||||
成功: {"code": 0, "message": "登录成功", "data": None}
|
成功: {"code": 0, "message": "登录成功", "data": None}
|
||||||
失败: {"code": -1|<API码>, "message": "错误描述", "data": None}
|
失败: {"code": -1, "message": "错误描述", "data": None}
|
||||||
"""
|
"""
|
||||||
if not self.user_name or not self.password:
|
if not self.user_name or not self.password:
|
||||||
return make_result(-1, "用户名和密码不能为空")
|
return make_result(-1, "用户名和密码不能为空")
|
||||||
|
|
@ -494,7 +548,8 @@ class Pan123Core:
|
||||||
self.authorization = f"Bearer {token}"
|
self.authorization = f"Bearer {token}"
|
||||||
self._build_headers()
|
self._build_headers()
|
||||||
self._sync_authorization()
|
self._sync_authorization()
|
||||||
self.save_config()
|
# 为避免内核依赖文件系统,登录成功后不自动保存配置到文件,由上层调用者决定何时保存。
|
||||||
|
# self.save_config_to_file()
|
||||||
return make_result(CODE_OK, "登录成功")
|
return make_result(CODE_OK, "登录成功")
|
||||||
|
|
||||||
def logout(self) -> Dict[str, Any]:
|
def logout(self) -> Dict[str, Any]:
|
||||||
|
|
@ -508,11 +563,11 @@ class Pan123Core:
|
||||||
self.authorization = ""
|
self.authorization = ""
|
||||||
self._sync_authorization()
|
self._sync_authorization()
|
||||||
self.cookies = None
|
self.cookies = None
|
||||||
self.save_config()
|
# self.save_config_to_file()
|
||||||
return make_result(CODE_OK, "已登出")
|
return make_result(CODE_OK, "已登出")
|
||||||
|
|
||||||
def clear_account(self) -> Dict[str, Any]:
|
def clear_account(self) -> Dict[str, Any]:
|
||||||
"""清除已登录账号:清除用户名、密码、authorization 和 cookies,并保存配置。
|
"""清除已登录账号:清除用户名、密码、authorization 和 cookies,不保存配置,但重建请求头。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Result 字典::
|
Result 字典::
|
||||||
|
|
@ -524,13 +579,76 @@ class Pan123Core:
|
||||||
self.authorization = ""
|
self.authorization = ""
|
||||||
self._sync_authorization()
|
self._sync_authorization()
|
||||||
self.cookies = None
|
self.cookies = None
|
||||||
self.save_config()
|
# self.save_config_to_file()
|
||||||
return make_result(CODE_OK, "账号信息已清除")
|
return make_result(CODE_OK, "账号信息已清除")
|
||||||
|
|
||||||
|
def check_login(self) -> Dict[str, Any]:
|
||||||
|
"""检查当前登录状态是否有效。
|
||||||
|
|
||||||
|
通过尝试获取根目录列表来验证 Token 是否有效。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result 字典::
|
||||||
|
|
||||||
|
成功: {"code": 0, "message": "登录状态有效", "data": None}
|
||||||
|
失败: {"code": -1, "message": "登录状态无效: 错误描述", "data": None}
|
||||||
|
"""
|
||||||
|
result = self.get_user_info()
|
||||||
|
if result["code"] == CODE_OK:
|
||||||
|
return make_result(CODE_OK, "登录状态有效")
|
||||||
|
return make_result(-1, f"登录状态无效: {result['message']}")
|
||||||
|
|
||||||
|
def init_login_state(self) -> Dict[str, Any]:
|
||||||
|
"""根据提供的配置初始化登录状态。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cfg: 包含账号信息和 Token 的配置字典,结构同 get_current_config() 的返回值。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result:
|
||||||
|
{"code": Num, "message": "..."}
|
||||||
|
"""
|
||||||
|
# 直接获取目录列表来验证登录状态和 Token 是否有效
|
||||||
|
is_valid = self.check_login()
|
||||||
|
if is_valid["code"] == CODE_OK:
|
||||||
|
return make_result(CODE_OK, "登录状态初始化成功")
|
||||||
|
else:
|
||||||
|
# 登录状态无效,重新登录
|
||||||
|
if not self.user_name or not self.password:
|
||||||
|
return make_result(-1, "登录状态无效,且用户名或密码缺失,无法重新登录")
|
||||||
|
login_result = self.login()
|
||||||
|
if login_result["code"] == CODE_OK:
|
||||||
|
return make_result(CODE_OK, "登录状态无效,重新登录成功")
|
||||||
|
else:
|
||||||
|
return make_result(-2, f"登录状态无效,重新登录失败: {login_result['message']}")
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 目录浏览
|
# 目录浏览
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def get_folder_details(self, folder_id: int) -> Dict[str, Any]:
|
||||||
|
"""获取指定文件夹的详情信息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_id: 目标文件夹的 FileId。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result 字典::
|
||||||
|
|
||||||
|
成功: {"code": 0, "message": "ok", "data": {文件夹详情 dict}}
|
||||||
|
失败: {"code": <错误码>, "message": "错误描述", "data": None}
|
||||||
|
"""
|
||||||
|
# 要传递一个包含 folder_id 的列表,但接口只返回第一个文件夹的详情
|
||||||
|
data = {"file_ids": [folder_id]}
|
||||||
|
res = self._request("POST", URL_DETAILS, json_data=data)
|
||||||
|
if res["code"] != CODE_OK:
|
||||||
|
return make_result(-1, f"获取文件夹详情失败: {res['message']}", res["data"])
|
||||||
|
details = res["data"]["data"]
|
||||||
|
if not details:
|
||||||
|
return make_result(-2, "文件夹详情数据为空", res["data"])
|
||||||
|
return make_result(CODE_OK, "ok", details)
|
||||||
|
|
||||||
|
|
||||||
def list_dir(
|
def list_dir(
|
||||||
self,
|
self,
|
||||||
parent_id: Optional[int] = None,
|
parent_id: Optional[int] = None,
|
||||||
|
|
@ -735,7 +853,7 @@ class Pan123Core:
|
||||||
Result 字典::
|
Result 字典::
|
||||||
|
|
||||||
成功: {"code": 0, "message": "ok", "data": {API 响应}}
|
成功: {"code": 0, "message": "ok", "data": {API 响应}}
|
||||||
失败: {"code": -1|<API码>, "message": "...", "data": ...}
|
失败: {"code": -1, "message": "...", "data": ...}
|
||||||
"""
|
"""
|
||||||
if not name:
|
if not name:
|
||||||
return make_result(-1, "目录名不能为空")
|
return make_result(-1, "目录名不能为空")
|
||||||
|
|
@ -914,14 +1032,26 @@ class Pan123Core:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Result 字典::
|
Result 字典::
|
||||||
|
来自 self.get_item_download_url() 的结果:
|
||||||
成功: {"code": 0, "message": "ok", "data": {"url": "https://..."}}
|
成功: {"code": 0, "message": "ok", "data": {"url": "https://..."}}
|
||||||
失败: {"code": -1, "message": "...", "data": None}
|
失败: {"code": -1, "message": "...", "data": None}
|
||||||
"""
|
"""
|
||||||
if not (0 <= index < len(self.file_list)):
|
if not (0 <= index < len(self.file_list)):
|
||||||
return make_result(-1, "无效的文件编号")
|
return make_result(-1, "无效的文件编号")
|
||||||
item = self.file_list[index]
|
item = self.file_list[index]
|
||||||
|
return self.get_item_download_url(item)
|
||||||
|
|
||||||
|
def get_item_download_url(self, item: Dict) -> Dict[str, Any]:
|
||||||
|
"""获取单个文件或文件夹的真实下载链接。
|
||||||
|
Args:
|
||||||
|
item: 文件信息字典,文件夹(Type = 1)需包含 "FileId"
|
||||||
|
文件(Type = 0)需包含 "FileId", "Etag", "S3KeyFlag", "Type", "FileName", "Size"。可以来自 file_list 中的条目或手动构造的 dict。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result 字典::
|
||||||
|
成功: {"code": 0, "message": "ok", "data": {"url": "https://..."}}
|
||||||
|
失败: {"code": -1, "message": "...", "data": None}
|
||||||
|
"""
|
||||||
# 文件夹走批量下载接口,文件走单文件接口
|
# 文件夹走批量下载接口,文件走单文件接口
|
||||||
if item["Type"] == 1:
|
if item["Type"] == 1:
|
||||||
api_path = URL_BATCH_DOWNLOAD
|
api_path = URL_BATCH_DOWNLOAD
|
||||||
|
|
@ -946,7 +1076,12 @@ class Pan123Core:
|
||||||
|
|
||||||
# 跟随重定向获取真实下载链接
|
# 跟随重定向获取真实下载链接
|
||||||
try:
|
try:
|
||||||
resp = requests.get(download_url, allow_redirects=False, timeout=TIMEOUT_DEFAULT)
|
# 直接请求会报错证书错误
|
||||||
|
# 此服务器无法证明它是 user-app-free-download-cdn.123295.com;它的安全证书来自 *.123pan.cn。这可能是由错误配置或者有攻击者截获你的连接而导致的。
|
||||||
|
# 关闭 SSL 验证以避免下载链接获取失败
|
||||||
|
# 仅在获取下载链接时关闭验证
|
||||||
|
requests.packages.urllib3.disable_warnings()
|
||||||
|
resp = requests.get(download_url, allow_redirects=False, timeout=TIMEOUT_DEFAULT, verify=False)
|
||||||
if resp.status_code == 302:
|
if resp.status_code == 302:
|
||||||
location = resp.headers.get("Location")
|
location = resp.headers.get("Location")
|
||||||
if location:
|
if location:
|
||||||
|
|
@ -959,138 +1094,142 @@ class Pan123Core:
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
return make_result(-1, f"获取真实下载链接失败: {e}")
|
return make_result(-1, f"获取真实下载链接失败: {e}")
|
||||||
|
|
||||||
def download_file(
|
# 文件交互,方法已移至 Pan123Tool
|
||||||
self,
|
# def download_file(
|
||||||
index: int,
|
# self,
|
||||||
save_dir: str = "download",
|
# index: int,
|
||||||
on_progress: ProgressCallback = None,
|
# save_dir: str = "download",
|
||||||
overwrite: bool = False,
|
# on_progress: ProgressCallback = None,
|
||||||
skip_existing: bool = False,
|
# overwrite: bool = False,
|
||||||
) -> Dict[str, Any]:
|
# skip_existing: bool = False,
|
||||||
"""下载 file_list 中指定下标的文件到本地。
|
# ) -> Dict[str, Any]:
|
||||||
|
# """下载 file_list 中指定下标的文件到本地。
|
||||||
|
#
|
||||||
|
# 如果目标是文件夹,则自动递归调用 download_directory()。
|
||||||
|
# 下载过程中使用 ".123pan" 临时文件,完成后重命名。
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# index: file_list 中的 0-based 下标。
|
||||||
|
# save_dir: 本地保存目录路径,不存在会自动创建。
|
||||||
|
# on_progress: 下载进度回调函数,签名:
|
||||||
|
# (downloaded_bytes: int, total_bytes: int, speed_bps: float) -> None
|
||||||
|
# overwrite: True = 覆盖已存在的同名文件。
|
||||||
|
# skip_existing: True = 跳过已存在的同名文件。
|
||||||
|
#
|
||||||
|
# Returns:
|
||||||
|
# Result 字典::
|
||||||
|
#
|
||||||
|
# 成功: {"code": 0, "message": "下载完成", "data": {"path": "本地文件路径"}}
|
||||||
|
# 冲突: {"code": 1, "message": "文件已存在", "data": {"path": "...", "conflict": True}}
|
||||||
|
# 跳过: {"code": 0, "message": "文件已存在,已跳过", "data": {"path": "..."}}
|
||||||
|
# 失败: {"code": -1, "message": "...", "data": None}
|
||||||
|
# """
|
||||||
|
# if not (0 <= index < len(self.file_list)):
|
||||||
|
# return make_result(-1, "无效的文件编号")
|
||||||
|
# item = self.file_list[index]
|
||||||
|
#
|
||||||
|
# # 文件夹递归下载
|
||||||
|
# if item["Type"] == 1:
|
||||||
|
# return self.download_directory(item, save_dir, on_progress, overwrite, skip_existing)
|
||||||
|
#
|
||||||
|
# # 获取下载链接
|
||||||
|
# r = self.get_download_url(index)
|
||||||
|
# if r["code"] != CODE_OK:
|
||||||
|
# return r
|
||||||
|
# url = r["data"]["url"]
|
||||||
|
#
|
||||||
|
# file_name = item["FileName"]
|
||||||
|
# os.makedirs(save_dir, exist_ok=True)
|
||||||
|
# full_path = os.path.join(save_dir, file_name)
|
||||||
|
#
|
||||||
|
# # 文件冲突处理
|
||||||
|
# if os.path.exists(full_path):
|
||||||
|
# if skip_existing:
|
||||||
|
# return make_result(CODE_OK, "文件已存在,已跳过", {"path": full_path})
|
||||||
|
# if not overwrite:
|
||||||
|
# return make_result(CODE_CONFLICT, "文件已存在", {"path": full_path, "conflict": True})
|
||||||
|
# os.remove(full_path)
|
||||||
|
#
|
||||||
|
# # 使用临时文件下载
|
||||||
|
# temp_path = full_path + ".123pan"
|
||||||
|
# try:
|
||||||
|
# resp = requests.get(url, stream=True, timeout=TIMEOUT_DOWNLOAD)
|
||||||
|
# total = int(resp.headers.get("Content-Length", 0))
|
||||||
|
# downloaded = 0
|
||||||
|
# start = time.time()
|
||||||
|
# with open(temp_path, "wb") as f:
|
||||||
|
# for chunk in resp.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
|
||||||
|
# if chunk:
|
||||||
|
# f.write(chunk)
|
||||||
|
# downloaded += len(chunk)
|
||||||
|
# if on_progress:
|
||||||
|
# elapsed = time.time() - start
|
||||||
|
# speed = downloaded / elapsed if elapsed > 0 else 0.0
|
||||||
|
# # on_progress(
|
||||||
|
#
|
||||||
|
# )
|
||||||
|
# os.rename(temp_path, full_path)
|
||||||
|
# return make_result(CODE_OK, "下载完成", {"path": full_path})
|
||||||
|
# except Exception as e:
|
||||||
|
# if os.path.exists(temp_path):
|
||||||
|
# os.remove(temp_path)
|
||||||
|
# return make_result(-1, f"下载失败: {e}")
|
||||||
|
|
||||||
如果目标是文件夹,则自动递归调用 download_directory()。
|
# 文件交互,方法已移至 Pan123Tool,Pan123Core 仅保留获取下载链接的功能,目录下载逻辑也移至工具类以避免内核依赖文件系统。
|
||||||
下载过程中使用 ".123pan" 临时文件,完成后重命名。
|
# def download_directory(
|
||||||
|
# self,
|
||||||
Args:
|
# directory: Dict,
|
||||||
index: file_list 中的 0-based 下标。
|
# save_dir: str = "download",
|
||||||
save_dir: 本地保存目录路径,不存在会自动创建。
|
# on_progress: ProgressCallback = None,
|
||||||
on_progress: 下载进度回调函数,签名:
|
# overwrite: bool = False,
|
||||||
(downloaded_bytes: int, total_bytes: int, speed_bps: float) -> None
|
# skip_existing: bool = False,
|
||||||
overwrite: True = 覆盖已存在的同名文件。
|
# ) -> Dict[str, Any]:
|
||||||
skip_existing: True = 跳过已存在的同名文件。
|
# """递归下载整个目录到本地。
|
||||||
|
#
|
||||||
Returns:
|
# Args:
|
||||||
Result 字典::
|
# directory: 文件夹信息字典(需包含 "FileId"、"FileName"、"Type" 字段)。
|
||||||
|
# save_dir: 本地保存根目录路径。
|
||||||
成功: {"code": 0, "message": "下载完成", "data": {"path": "本地文件路径"}}
|
# on_progress: 下载进度回调函数(同 download_file)。
|
||||||
冲突: {"code": 1, "message": "文件已存在", "data": {"path": "...", "conflict": True}}
|
# overwrite: True = 覆盖已存在文件。
|
||||||
跳过: {"code": 0, "message": "文件已存在,已跳过", "data": {"path": "..."}}
|
# skip_existing: True = 跳过已存在文件。
|
||||||
失败: {"code": -1, "message": "...", "data": None}
|
#
|
||||||
"""
|
# Returns:
|
||||||
if not (0 <= index < len(self.file_list)):
|
# Result 字典::
|
||||||
return make_result(-1, "无效的文件编号")
|
#
|
||||||
item = self.file_list[index]
|
# 成功: {"code": 0, "message": "文件夹下载完成", "data": {"path": "本地目录路径"}}
|
||||||
|
# 部分失败: {"code": -1, "message": "部分文件下载失败: ...", "data": {"path": "..."}}
|
||||||
# 文件夹递归下载
|
# 失败: {"code": <错误码>, "message": "...", "data": None}
|
||||||
if item["Type"] == 1:
|
# """
|
||||||
return self.download_directory(item, save_dir, on_progress, overwrite, skip_existing)
|
# if directory["Type"] != 1:
|
||||||
|
# return make_result(-1, "不是文件夹")
|
||||||
# 获取下载链接
|
#
|
||||||
r = self.get_download_url(index)
|
# target_dir = os.path.join(save_dir, directory["FileName"])
|
||||||
if r["code"] != CODE_OK:
|
# os.makedirs(target_dir, exist_ok=True)
|
||||||
return r
|
#
|
||||||
url = r["data"]["url"]
|
# r = self.list_dir_all(parent_id=directory["FileId"])
|
||||||
|
# if r["code"] != CODE_OK:
|
||||||
file_name = item["FileName"]
|
# return r
|
||||||
os.makedirs(save_dir, exist_ok=True)
|
#
|
||||||
full_path = os.path.join(save_dir, file_name)
|
# items = r["data"]["items"]
|
||||||
|
# if not items:
|
||||||
# 文件冲突处理
|
# return make_result(CODE_OK, "文件夹为空", {"path": target_dir})
|
||||||
if os.path.exists(full_path):
|
#
|
||||||
if skip_existing:
|
# errors: List[str] = []
|
||||||
return make_result(CODE_OK, "文件已存在,已跳过", {"path": full_path})
|
# for item in items:
|
||||||
if not overwrite:
|
# if item["Type"] == 1:
|
||||||
return make_result(CODE_CONFLICT, "文件已存在", {"path": full_path, "conflict": True})
|
# sub = self.download_directory(item, target_dir, on_progress, overwrite, skip_existing)
|
||||||
os.remove(full_path)
|
# else:
|
||||||
|
# # 临时替换 file_list 以复用 download_file 逻辑
|
||||||
# 使用临时文件下载
|
# orig_list = self.file_list
|
||||||
temp_path = full_path + ".123pan"
|
# self.file_list = [item]
|
||||||
try:
|
# sub = self.download_file(0, target_dir, on_progress, overwrite, skip_existing)
|
||||||
resp = requests.get(url, stream=True, timeout=TIMEOUT_DOWNLOAD)
|
# self.file_list = orig_list
|
||||||
total = int(resp.headers.get("Content-Length", 0))
|
# if sub["code"] != CODE_OK:
|
||||||
downloaded = 0
|
# errors.append(f"{item['FileName']}: {sub['message']}")
|
||||||
start = time.time()
|
#
|
||||||
with open(temp_path, "wb") as f:
|
# if errors:
|
||||||
for chunk in resp.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
|
# return make_result(-1, f"部分文件下载失败: {'; '.join(errors)}", {"path": target_dir})
|
||||||
if chunk:
|
# return make_result(CODE_OK, "文件夹下载完成", {"path": target_dir})
|
||||||
f.write(chunk)
|
|
||||||
downloaded += len(chunk)
|
|
||||||
if on_progress:
|
|
||||||
elapsed = time.time() - start
|
|
||||||
speed = downloaded / elapsed if elapsed > 0 else 0.0
|
|
||||||
on_progress(downloaded, total, speed)
|
|
||||||
os.rename(temp_path, full_path)
|
|
||||||
return make_result(CODE_OK, "下载完成", {"path": full_path})
|
|
||||||
except Exception as e:
|
|
||||||
if os.path.exists(temp_path):
|
|
||||||
os.remove(temp_path)
|
|
||||||
return make_result(-1, f"下载失败: {e}")
|
|
||||||
|
|
||||||
def download_directory(
|
|
||||||
self,
|
|
||||||
directory: Dict,
|
|
||||||
save_dir: str = "download",
|
|
||||||
on_progress: ProgressCallback = None,
|
|
||||||
overwrite: bool = False,
|
|
||||||
skip_existing: bool = False,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""递归下载整个目录到本地。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
directory: 文件夹信息字典(需包含 "FileId"、"FileName"、"Type" 字段)。
|
|
||||||
save_dir: 本地保存根目录路径。
|
|
||||||
on_progress: 下载进度回调函数(同 download_file)。
|
|
||||||
overwrite: True = 覆盖已存在文件。
|
|
||||||
skip_existing: True = 跳过已存在文件。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Result 字典::
|
|
||||||
|
|
||||||
成功: {"code": 0, "message": "文件夹下载完成", "data": {"path": "本地目录路径"}}
|
|
||||||
部分失败: {"code": -1, "message": "部分文件下载失败: ...", "data": {"path": "..."}}
|
|
||||||
失败: {"code": <错误码>, "message": "...", "data": None}
|
|
||||||
"""
|
|
||||||
if directory["Type"] != 1:
|
|
||||||
return make_result(-1, "不是文件夹")
|
|
||||||
|
|
||||||
target_dir = os.path.join(save_dir, directory["FileName"])
|
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
|
||||||
|
|
||||||
r = self.list_dir_all(parent_id=directory["FileId"])
|
|
||||||
if r["code"] != CODE_OK:
|
|
||||||
return r
|
|
||||||
|
|
||||||
items = r["data"]["items"]
|
|
||||||
if not items:
|
|
||||||
return make_result(CODE_OK, "文件夹为空", {"path": target_dir})
|
|
||||||
|
|
||||||
errors: List[str] = []
|
|
||||||
for item in items:
|
|
||||||
if item["Type"] == 1:
|
|
||||||
sub = self.download_directory(item, target_dir, on_progress, overwrite, skip_existing)
|
|
||||||
else:
|
|
||||||
# 临时替换 file_list 以复用 download_file 逻辑
|
|
||||||
orig_list = self.file_list
|
|
||||||
self.file_list = [item]
|
|
||||||
sub = self.download_file(0, target_dir, on_progress, overwrite, skip_existing)
|
|
||||||
self.file_list = orig_list
|
|
||||||
if sub["code"] != CODE_OK:
|
|
||||||
errors.append(f"{item['FileName']}: {sub['message']}")
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
return make_result(-1, f"部分文件下载失败: {'; '.join(errors)}", {"path": target_dir})
|
|
||||||
return make_result(CODE_OK, "文件夹下载完成", {"path": target_dir})
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 上传
|
# 上传
|
||||||
|
|
@ -1235,7 +1374,12 @@ class Pan123Core:
|
||||||
|
|
||||||
uploaded += len(chunk)
|
uploaded += len(chunk)
|
||||||
if on_progress:
|
if on_progress:
|
||||||
on_progress(uploaded, total_size)
|
on_progress({
|
||||||
|
"type": Pan123EventType.UPLOAD_PROGRESS,
|
||||||
|
"uploaded": uploaded,
|
||||||
|
"total": total_size,
|
||||||
|
"percent": uploaded / total_size * 100,
|
||||||
|
})
|
||||||
part_number += 1
|
part_number += 1
|
||||||
|
|
||||||
# 步骤 3: 通知服务端合并所有分块
|
# 步骤 3: 通知服务端合并所有分块
|
||||||
|
|
@ -1281,5 +1425,266 @@ class Pan123Core:
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
self._build_headers()
|
self._build_headers()
|
||||||
self._sync_authorization()
|
self._sync_authorization()
|
||||||
self.save_config()
|
# self.save_config_to_file()
|
||||||
return make_result(CODE_OK, f"已切换到 {protocol} 协议")
|
return make_result(CODE_OK, f"已切换到 {protocol} 协议")
|
||||||
|
|
||||||
|
|
||||||
|
class Pan123Tool:
|
||||||
|
"""123pan 工具类,提供更高层次的文件交互方法,依赖 Pan123Core 实现具体 API 调用。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
core: Pan123Core 实例,负责 API 请求和状态管理。
|
||||||
|
config_file: 配置文件路径,默认为 "123pan_config.json",用于保存和加载账号信息、Token 及协议设置。
|
||||||
|
|
||||||
|
:note
|
||||||
|
Pan123Tool 主要负责文件下载、上传、目录操作等依赖文件系统的功能,而 Pan123Core 负责 API 请求、认证和状态管理。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, core: Pan123Core, config_file: str = "123pan_config.json"):
|
||||||
|
self.core = core
|
||||||
|
self.config_file = config_file
|
||||||
|
|
||||||
|
def load_config_from_file(self) -> Dict[str, Any]:
|
||||||
|
"""从配置文件加载账号信息、Token 及协议设置。
|
||||||
|
|
||||||
|
会自动重建 headers 并同步 authorization。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result 字典::
|
||||||
|
|
||||||
|
成功: {"code": 0, "message": "配置加载成功", "data": {配置内容 dict}}
|
||||||
|
失败: {"code": -1, "message": "错误描述", "data": None}
|
||||||
|
"""
|
||||||
|
if not os.path.exists(self.config_file):
|
||||||
|
return make_result(-1, "配置文件不存在")
|
||||||
|
try:
|
||||||
|
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
return self.core.load_config(cfg)
|
||||||
|
except Exception as e:
|
||||||
|
return make_result(-1, f"加载配置失败: {e}")
|
||||||
|
|
||||||
|
def save_config_to_file(self) -> Dict[str, Any]:
|
||||||
|
"""将当前账号信息、Token 及协议设置保存到配置文件。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result 字典::
|
||||||
|
|
||||||
|
成功: {"code": 0, "message": "配置已保存", "data": {配置内容 dict}}
|
||||||
|
失败: {"code": -1, "message": "错误描述", "data": None}
|
||||||
|
"""
|
||||||
|
cfg = {
|
||||||
|
"userName": self.core.user_name,
|
||||||
|
"passWord": self.core.password,
|
||||||
|
"authorization": self.core.authorization,
|
||||||
|
"deviceType": self.core.device_type,
|
||||||
|
"osVersion": self.core.os_version,
|
||||||
|
"protocol": self.core.protocol,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
with open(self.config_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||||
|
return make_result(CODE_OK, "配置已保存", cfg)
|
||||||
|
except Exception as e:
|
||||||
|
return make_result(-1, f"保存配置失败: {e}")
|
||||||
|
|
||||||
|
def download_file(
|
||||||
|
self,
|
||||||
|
index: int,
|
||||||
|
save_dir: str = "download",
|
||||||
|
on_progress: ProgressCallback = None,
|
||||||
|
overwrite: bool = False,
|
||||||
|
skip_existing: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""下载 file_list 中指定下标的文件到本地。
|
||||||
|
|
||||||
|
如果目标是文件夹,则自动递归调用 download_directory()。
|
||||||
|
下载过程中使用 ".123pan" 临时文件,完成后重命名。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index: file_list 中的 0-based 下标。
|
||||||
|
save_dir: 本地保存目录路径,不存在会自动创建。
|
||||||
|
on_progress: 下载进度回调函数,签名:
|
||||||
|
(downloaded_bytes: int, total_bytes: int, speed_bps: float) -> None
|
||||||
|
overwrite: True = 覆盖已存在的同名文件。
|
||||||
|
skip_existing: True = 跳过已存在的同名文件。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result 字典:: 来自 download_url() 或 download_directory() 的结果:
|
||||||
|
成功: {"code": 0, "message": "下载完成", "data": {"path": "本地文件路径"}}
|
||||||
|
冲突: {"code": 1, "message": "文件已存在", "data": {"path": "...", "conflict": True}}
|
||||||
|
跳过: {"code": 0, "message": "文件已存在,已跳过", "data": {"path": "..."}}
|
||||||
|
失败: {"code": -1, "message": "...", "data": None}
|
||||||
|
"""
|
||||||
|
if not (0 <= index < len(self.core.file_list)):
|
||||||
|
return make_result(-1, "无效的文件编号")
|
||||||
|
item = self.core.file_list[index]
|
||||||
|
return self.download_item(item, save_dir, on_progress, overwrite, skip_existing)
|
||||||
|
|
||||||
|
def download_item(
|
||||||
|
self,
|
||||||
|
item: Dict,
|
||||||
|
save_dir: str = "download",
|
||||||
|
on_progress: ProgressCallback = None,
|
||||||
|
overwrite: bool = False,
|
||||||
|
skip_existing: bool = False,
|
||||||
|
):
|
||||||
|
"""下载单个文件或文件夹项,自动区分类型并处理。
|
||||||
|
Args:
|
||||||
|
item: 文件信息字典,文件夹(Type = 1)需包含 "FileId"
|
||||||
|
文件(Type = 0)需包含 "FileId", "Etag", "S3KeyFlag", "Type", "FileName", "Size"。
|
||||||
|
save_dir: 本地保存目录路径,不存在会自动创建。
|
||||||
|
on_progress: 下载进度回调函数,签名:
|
||||||
|
(downloaded_bytes: int, total_bytes: int, speed_bps: float) -> None
|
||||||
|
overwrite: True = 覆盖已存在的同名文件。
|
||||||
|
skip_existing: True = 跳过已存在的同名文件。
|
||||||
|
Returns:
|
||||||
|
Result 字典:: 来自 download_url() 或 download_directory() 的结果:
|
||||||
|
成功: {"code": 0, "message": "下载完成", "data": {"path": "本地文件路径"}}
|
||||||
|
冲突: {"code": 1, "message": "文件已存在", "data": {"path": "...", "conflict": True}}
|
||||||
|
跳过: {"code": 0, "message": "文件已存在,已跳过", "data": {"path": "..."}}
|
||||||
|
失败: {"code": -1, "message": "...", "data": None}
|
||||||
|
"""
|
||||||
|
# 文件夹递归下载
|
||||||
|
if item["Type"] == 1:
|
||||||
|
return self.download_directory(item, save_dir, on_progress, overwrite, skip_existing)
|
||||||
|
|
||||||
|
# 获取下载链接
|
||||||
|
r = self.core.get_item_download_url(item)
|
||||||
|
if r["code"] != CODE_OK:
|
||||||
|
return r
|
||||||
|
url = r["data"]["url"]
|
||||||
|
file_name = item["FileName"]
|
||||||
|
return self.download_url(url, file_name, save_dir, on_progress, overwrite, skip_existing)
|
||||||
|
|
||||||
|
def download_url(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
file_name: str,
|
||||||
|
save_dir: str = "download",
|
||||||
|
on_progress: ProgressCallback = None,
|
||||||
|
overwrite: bool = False,
|
||||||
|
skip_existing: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""根据下载链接下载文件到本地,支持进度回调和冲突处理。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 真实下载链接。
|
||||||
|
file_name: 保存的文件名(不含路径)。
|
||||||
|
save_dir: 本地保存目录路径,不存在会自动创建。
|
||||||
|
on_progress: 下载进度回调函数,签名:
|
||||||
|
(downloaded_bytes: int, total_bytes: int, speed_bps: float) -> None
|
||||||
|
overwrite: True = 覆盖已存在的同名文件。
|
||||||
|
skip_existing: True = 跳过已存在的同名文件。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Result 字典::
|
||||||
|
成功: {"code": 0, "message": "下载完成", "data": {"path": "本地文件路径"}}
|
||||||
|
冲突: {"code": 1, "message": "文件已存在", "data": {"path": "...", "conflict": True}}
|
||||||
|
跳过: {"code": 0, "message": "文件已存在,已跳过", "data": {"path": "..."}}
|
||||||
|
失败: {"code": -1, "message": "...", "data": None}
|
||||||
|
"""
|
||||||
|
|
||||||
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
full_path = os.path.join(save_dir, file_name)
|
||||||
|
|
||||||
|
# 文件冲突处理
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
if skip_existing:
|
||||||
|
return make_result(CODE_OK, "文件已存在,已跳过", {"path": full_path})
|
||||||
|
if not overwrite:
|
||||||
|
return make_result(CODE_CONFLICT, "文件已存在", {"path": full_path, "conflict": True})
|
||||||
|
os.remove(full_path)
|
||||||
|
|
||||||
|
# TODO: 可以考虑断点续传
|
||||||
|
# 使用临时文件下载
|
||||||
|
temp_path = full_path + ".123pan"
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, stream=True, timeout=TIMEOUT_DOWNLOAD)
|
||||||
|
total = int(resp.headers.get("Content-Length", 0))
|
||||||
|
downloaded = 0
|
||||||
|
start = time.time()
|
||||||
|
with open(temp_path, "wb") as f:
|
||||||
|
for chunk in resp.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
downloaded += len(chunk)
|
||||||
|
if on_progress:
|
||||||
|
elapsed = time.time() - start
|
||||||
|
speed = downloaded / elapsed if elapsed > 0 else 0.0
|
||||||
|
on_progress({
|
||||||
|
"type": Pan123EventType.DOWNLOAD_PROGRESS,
|
||||||
|
"downloaded": downloaded,
|
||||||
|
"total": total,
|
||||||
|
"speed": speed,
|
||||||
|
})
|
||||||
|
os.rename(temp_path, full_path)
|
||||||
|
return make_result(CODE_OK, "下载完成", {"path": full_path})
|
||||||
|
except Exception as e:
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
|
return make_result(-1, f"下载失败: {e}")
|
||||||
|
|
||||||
|
def download_directory(
|
||||||
|
self,
|
||||||
|
directory: Dict,
|
||||||
|
save_dir: str = "download",
|
||||||
|
on_progress: ProgressCallback = None,
|
||||||
|
overwrite: bool = False,
|
||||||
|
skip_existing: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""递归下载整个目录到本地。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: 文件夹信息字典(需包含 "FileId"、"FileName"、"Type" 字段)。
|
||||||
|
save_dir: 本地保存根目录路径。
|
||||||
|
on_progress: 下载进度回调函数(同 download_file)。
|
||||||
|
overwrite: True = 覆盖已存在文件。
|
||||||
|
skip_existing: True = 跳过已存在文件。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
成功: {"code": 0, "message": "文件夹下载完成", "data": {"path": "本地目录路径"}}
|
||||||
|
部分失败: {"code": -1, "message": "部分文件下载失败: ...", "data": {"path": "..."}}
|
||||||
|
失败: {"code": <错误码>, "message": "...", "data": None}
|
||||||
|
"""
|
||||||
|
if directory["Type"] != 1:
|
||||||
|
return make_result(-1, "不是文件夹")
|
||||||
|
|
||||||
|
target_dir = os.path.join(save_dir, directory["FileName"])
|
||||||
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
|
||||||
|
r = self.core.list_dir_all(parent_id=directory["FileId"])
|
||||||
|
if r["code"] != CODE_OK:
|
||||||
|
return r
|
||||||
|
|
||||||
|
items = r["data"]["items"]
|
||||||
|
if not items:
|
||||||
|
return make_result(CODE_OK, "文件夹为空", {"path": target_dir})
|
||||||
|
|
||||||
|
errors: List[str] = []
|
||||||
|
for item in items:
|
||||||
|
if item["Type"] == 1:
|
||||||
|
# 递归下载子目录
|
||||||
|
if on_progress:
|
||||||
|
on_progress({
|
||||||
|
"type": Pan123EventType.DOWNLOAD_START_DIRECTORY,
|
||||||
|
"file_name": item["FileName"],
|
||||||
|
"dir_name": item["FileName"],
|
||||||
|
"message": f"正在下载目录: {item['FileName']}",
|
||||||
|
})
|
||||||
|
sub = self.download_directory(item, target_dir, on_progress, overwrite, skip_existing)
|
||||||
|
else:
|
||||||
|
if on_progress:
|
||||||
|
on_progress({
|
||||||
|
"type": Pan123EventType.DOWNLOAD_START_FILE,
|
||||||
|
"file_name": item["FileName"],
|
||||||
|
"file_size": item["Size"],
|
||||||
|
"message": f"正在下载文件: {item['FileName']}",
|
||||||
|
})
|
||||||
|
sub = self.download_item(item, target_dir, on_progress, overwrite, skip_existing)
|
||||||
|
if sub["code"] != CODE_OK:
|
||||||
|
errors.append(f"{item['FileName']}: {sub['message']}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return make_result(-1, f"部分文件下载失败: {'; '.join(errors)}", {"path": target_dir})
|
||||||
|
return make_result(CODE_OK, "文件夹下载完成", {"path": target_dir})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user