mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
优化,添加115接口文档
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
## 删除用户云下载任务
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:----------------------------------|
|
||||||
|
| 接口名称 | 删除用户云下载任务 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /del_task |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
删除当前授权用户的指定云下载任务,可选择同时删除对应源文件。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/offline/del_task
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------------|:-------|:-----|:-------|:---------------------------|:-------------|
|
||||||
|
| info_hash | string | 是 | - | 需删除的任务Hash | `<info_hash>` |
|
||||||
|
| del_source_file | int | 否 | 0 | 是否删除源文件,1=删除 0=不删除 | 0 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/offline/del_task' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--form-string 'info_hash=<info_hash>' \
|
||||||
|
--form-string 'del_source_file=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:--------|:----------------------|
|
||||||
|
| state | boolean | 操作结果状态 |
|
||||||
|
| message | string | 返回信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object[] | 返回数据,成功时为空数组 |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:-------|:---------|:-------------------------|
|
||||||
|
| 990002 | 参数错误 | 检查 `info_hash` 参数 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 未传 `del_source_file` 时默认不删除源文件。
|
||||||
|
- `del_source_file=1` 会删除任务对应的源文件,操作前需确认影响范围。
|
||||||
|
|
||||||
|
### 性能与安全说明
|
||||||
|
|
||||||
|
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||||
|
- 接口按应用、用户和接口维度执行频率限制。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 删除结果以响应中的 `state`、`code` 和 `message` 字段为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
## 添加云下载BT任务
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:----------------------------------|
|
||||||
|
| 接口名称 | 添加云下载BT任务 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /add_task_bt |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
根据已解析的BT种子信息添加云下载BT任务。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/offline/add_task_bt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------------|:-------|:-----|:-------|:----------------------------------|:----------------|
|
||||||
|
| info_hash | string | 是 | - | BT任务Hash | `<info_hash>` |
|
||||||
|
| wanted | string | 是 | - | 选中下载的文件索引,使用半角逗号分隔 | `<file_indexes>` |
|
||||||
|
| save_path | string | 是 | - | BT任务文件保存路径 | `A/B` |
|
||||||
|
| torrent_sha1 | string | 是 | - | BT种子SHA1 | `<torrent_sha1>` |
|
||||||
|
| pick_code | string | 是 | - | BT种子文件提取码 | `<pick_code>` |
|
||||||
|
| wp_path_id | string | 否 | 0 | 保存目标文件夹ID | 0 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/offline/add_task_bt' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--form-string 'info_hash=<info_hash>' \
|
||||||
|
--form-string 'wanted=<file_indexes>' \
|
||||||
|
--form-string 'save_path=A/B' \
|
||||||
|
--form-string 'torrent_sha1=<torrent_sha1>' \
|
||||||
|
--form-string 'pick_code=<pick_code>' \
|
||||||
|
--form-string 'wp_path_id=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:--------|:----------------------|
|
||||||
|
| state | boolean | 操作结果状态 |
|
||||||
|
| message | string | 返回信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object[] | 返回数据,成功时为空数组 |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:--------|:---------------------|:-------------------------------|
|
||||||
|
| 20018 | 文件不存在或已删除 | 检查提取码、文件归属和种子SHA1 |
|
||||||
|
| 91006 | 存储空间不足 | 扩充存储空间后重试 |
|
||||||
|
| 990002 | 参数错误 | 检查必填参数 |
|
||||||
|
| 1000012 | 云下载配额已用完 | 购买配额或获得更多配额后重试 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- `wanted` 可以为 `0`,但不能是空字符串。
|
||||||
|
- 种子文件需属于当前授权用户、位于指定存储区域,并且文件SHA1与 `torrent_sha1` 一致。
|
||||||
|
- 不传 `wp_path_id` 时默认保存到根目录;`save_path` 是相对于 `wp_path_id` 所在文件夹的路径。例如,`wp_path_id` 不传或传云下载文件夹ID且 `save_path=A/B` 时,最终路径为根目录下的 `A/B/`。
|
||||||
|
- 添加任务前会检查当前授权账号的剩余存储空间;空间不足时不扣减云下载配额。
|
||||||
|
- 云下载服务返回状态码 `10007` 或 `10010` 时,接口统一返回错误码 `1000012`。
|
||||||
|
|
||||||
|
### 性能与安全说明
|
||||||
|
|
||||||
|
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||||
|
- 接口按应用、用户和接口维度执行频率限制。
|
||||||
|
- 种子文件必须属于当前授权账号,并且文件SHA1与 `torrent_sha1` 一致。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 任务处理结果以响应中的 `data` 字段为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
## 添加云下载链接任务
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:----------------------------------|
|
||||||
|
| 接口名称 | 添加云下载链接任务 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /add_task_urls |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
批量添加云下载链接任务。多个链接使用换行符分隔,支持HTTP(S)、FTP、磁力链和电驴链接。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/offline/add_task_urls
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-----------|:-------|:-----|:-------|:----------------------------------------|:----------------|
|
||||||
|
| urls | string | 是 | - | 云下载链接,多个链接使用换行符分隔 | `<download_url>` |
|
||||||
|
| wp_path_id | string | 否 | 0 | 保存目标文件夹ID;不传或传0时保存到根目录 | 0 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/offline/add_task_urls' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--form-string 'urls=<download_url>' \
|
||||||
|
--form-string 'wp_path_id=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-----------------|:---------|:-------------------------------|
|
||||||
|
| state | boolean | 操作结果状态 |
|
||||||
|
| message | string | 返回信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object[] | 各链接任务的添加结果 |
|
||||||
|
| data[].state | boolean | 链接任务添加状态 |
|
||||||
|
| data[].code | int | 链接任务状态码 |
|
||||||
|
| data[].message | string | 链接任务状态描述 |
|
||||||
|
| data[].info_hash | string | 链接任务SHA1,仅任务成功时返回 |
|
||||||
|
| data[].url | string | 链接任务URL |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:--------|:---------------------------|:-----------------------------|
|
||||||
|
| 91006 | 存储空间不足 | 扩充存储空间后重试 |
|
||||||
|
| 980004 | 操作失败 | 稍后重试 |
|
||||||
|
| 990002 | 参数错误 | 检查必填参数 |
|
||||||
|
| 1000011 | 链接数量超过115个 | 将链接拆分为每批不超过115个 |
|
||||||
|
| 1000012 | 云下载配额已用完 | 购买配额或获得更多配额后重试 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"code": 0,
|
||||||
|
"message": "",
|
||||||
|
"info_hash": "",
|
||||||
|
"url": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 服务端按换行符拆分并清理链接,单次最多提交115个链接。
|
||||||
|
- 添加任务前会检查当前授权账号的剩余存储空间;空间不足时不扣减云下载配额。
|
||||||
|
- 云下载服务返回状态码 `10007` 或 `10010` 时,接口统一返回错误码 `1000012`。
|
||||||
|
|
||||||
|
### 性能与安全说明
|
||||||
|
|
||||||
|
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||||
|
- 接口按应用、用户和接口维度执行频率限制。
|
||||||
|
- 本接口限制单批链接数量,避免不受控的批量请求。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 任务处理结果以响应中的 `data` 字段为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
## 清空云下载任务
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:----------------------------------|
|
||||||
|
| 接口名称 | 清空云下载任务 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /clear_task |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
按指定类型清空当前授权用户的云下载任务。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/offline/clear_task
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------|:-----|:-----|:-------|:-----------------------|:----------|
|
||||||
|
| flag | int | 是 | - | 清空任务类型,见下方枚举表格 | 1 |
|
||||||
|
|
||||||
|
#### 请求参数中的 flag 参数枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:---|:-----------------------------|:-----|
|
||||||
|
| 0 | 清空已完成任务 | - |
|
||||||
|
| 1 | 清空全部任务 | - |
|
||||||
|
| 2 | 清空失败任务 | - |
|
||||||
|
| 3 | 清空进行中任务 | - |
|
||||||
|
| 4 | 清空已完成任务并删除对应源文件 | - |
|
||||||
|
| 5 | 清空全部任务并删除对应源文件 | - |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/offline/clear_task' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--form-string 'flag=1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:--------|:-----------------|
|
||||||
|
| state | boolean | 操作结果状态 |
|
||||||
|
| message | string | 返回信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object[] | 返回数据,成功时为空数组 |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:-------|:---------|:--------------------------|
|
||||||
|
| 990002 | 参数错误 | 检查 `flag` 是否为0至5的整数 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- `flag=4` 或 `flag=5` 会同时删除对应源文件,操作前需确认清理范围。
|
||||||
|
|
||||||
|
### 性能与安全说明
|
||||||
|
|
||||||
|
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||||
|
- 接口按应用、用户和接口维度执行频率限制。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 清空结果以响应中的 `state`、`code` 和 `message` 字段为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
## 获取云下载配额信息
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:----------------------------------|
|
||||||
|
| 接口名称 | 获取云下载配额信息 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /get_quota_info |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
获取当前授权用户各类云下载配额的使用情况和过期明细。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/offline/get_quota_info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
无。
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/offline/get_quota_info' \
|
||||||
|
-H 'Authorization: Bearer <access_token>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------------------------------------------|:---------|:---------------------|
|
||||||
|
| state | boolean | 状态,true表示成功 |
|
||||||
|
| message | string | 返回信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 云下载配额数据 |
|
||||||
|
| data.package | object[] | 配额类型列表 |
|
||||||
|
| data.package[].surplus | int | 该类型剩余配额 |
|
||||||
|
| data.package[].used | int | 该类型已用配额 |
|
||||||
|
| data.package[].count | int | 该类型总配额 |
|
||||||
|
| data.package[].name | string | 该类型配额名称 |
|
||||||
|
| data.package[].expire_info | object[] | 该类型配额过期明细 |
|
||||||
|
| data.package[].expire_info[].surplus | int | 明细项剩余配额 |
|
||||||
|
| data.package[].expire_info[].expire_time | int | 明细项过期时间 |
|
||||||
|
| data.count | int | 用户总配额数量 |
|
||||||
|
| data.surplus | int | 用户总剩余配额数量 |
|
||||||
|
| data.used | int | 用户总已用配额数量 |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:-------|:---------|:-------------|
|
||||||
|
| 990002 | 参数错误 | 检查授权信息 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"package": [],
|
||||||
|
"count": 0,
|
||||||
|
"surplus": 0,
|
||||||
|
"used": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能与安全说明
|
||||||
|
|
||||||
|
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||||
|
- 接口按应用、用户和接口维度执行频率限制。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 云下载配额以响应中的 `data` 字段为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
## 获取用户云下载任务列表
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:----------------------------------|
|
||||||
|
| 接口名称 | 获取用户云下载任务列表 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /get_task_list |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
分页获取当前授权用户的云下载任务列表。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/offline/get_task_list
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------|:-----|:-----|:-------|:---------|:----------|
|
||||||
|
| page | int | 否 | 1 | 页码 | 1 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/offline/get_task_list' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--data-urlencode 'page=1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------------------|:---------|:--------------------------------------|
|
||||||
|
| state | boolean | 状态,true表示成功 |
|
||||||
|
| message | string | 返回信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 分页任务数据 |
|
||||||
|
| data.page | int | 当前页码 |
|
||||||
|
| data.page_count | int | 总页数 |
|
||||||
|
| data.count | int | 任务总数 |
|
||||||
|
| data.tasks | object[] | 云下载任务列表 |
|
||||||
|
| data.tasks[].info_hash | string | 任务SHA1 |
|
||||||
|
| data.tasks[].add_time | int | 任务添加时间戳 |
|
||||||
|
| data.tasks[].percentDone | int | 任务下载进度 |
|
||||||
|
| data.tasks[].size | int | 任务总大小,单位为字节 |
|
||||||
|
| data.tasks[].name | string | 任务名称 |
|
||||||
|
| data.tasks[].last_update | int | 任务最后更新时间戳 |
|
||||||
|
| data.tasks[].file_id | string | 任务源文件或文件夹ID |
|
||||||
|
| data.tasks[].delete_file_id | string | 删除任务并删除源文件时需传递的文件或文件夹ID |
|
||||||
|
| data.tasks[].status | int | 任务状态,见下方枚举表格 |
|
||||||
|
| data.tasks[].url | string | 链接任务URL |
|
||||||
|
| data.tasks[].wp_path_id | string | 任务源文件所在父文件夹ID |
|
||||||
|
| data.tasks[].def2 | int | 视频清晰度,见下方枚举表格 |
|
||||||
|
| data.tasks[].play_long | int | 视频时长 |
|
||||||
|
| data.tasks[].can_appeal | int | 是否可以申诉 |
|
||||||
|
|
||||||
|
#### 响应的 data.tasks[].status 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:---|:---------|:-----|
|
||||||
|
| -1 | 下载失败 | - |
|
||||||
|
| 0 | 分配中 | - |
|
||||||
|
| 1 | 下载中 | - |
|
||||||
|
| 2 | 下载成功 | - |
|
||||||
|
|
||||||
|
#### 响应的 data.tasks[].def2 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:----|:------|:-----|
|
||||||
|
| 1 | 标清 | - |
|
||||||
|
| 2 | 高清 | - |
|
||||||
|
| 3 | 超清 | - |
|
||||||
|
| 4 | 1080P | - |
|
||||||
|
| 5 | 4K | - |
|
||||||
|
| 100 | 原画 | - |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:-------|:---------|:---------------|
|
||||||
|
| 990002 | 参数错误 | 检查授权信息 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"page": 1,
|
||||||
|
"page_count": 0,
|
||||||
|
"count": 0,
|
||||||
|
"tasks": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 未传 `page` 或参数值为空时,默认查询第1页。
|
||||||
|
|
||||||
|
### 性能与安全说明
|
||||||
|
|
||||||
|
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||||
|
- 接口按应用、用户和接口维度执行频率限制。
|
||||||
|
- 任务列表按 `page` 分页,实际返回数量以响应为准。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 云下载任务状态和字段以响应中的 `data` 字段为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
## 解析BT种子
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:----------------------------------|
|
||||||
|
| 接口名称 | 解析BT种子 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /torrent |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
解析已上传的BT种子文件,返回种子任务信息和文件列表。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/offline/torrent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------------|:-------|:-----|:-------|:---------------|:----------------|
|
||||||
|
| torrent_sha1 | string | 是 | - | BT种子文件SHA1 | `<torrent_sha1>` |
|
||||||
|
| pick_code | string | 是 | - | BT种子文件提取码 | `<pick_code>` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/offline/torrent' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--form-string 'torrent_sha1=<torrent_sha1>' \
|
||||||
|
--form-string 'pick_code=<pick_code>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-------------------------------|:---------|:---------------------|
|
||||||
|
| state | boolean | 状态,true表示成功 |
|
||||||
|
| message | string | 返回信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 种子解析结果 |
|
||||||
|
| data.file_size | int | 任务大小 |
|
||||||
|
| data.torrent_name | string | 任务名称 |
|
||||||
|
| data.file_count | int | 文件数量 |
|
||||||
|
| data.info_hash | string | 任务SHA1 |
|
||||||
|
| data.torrent_filelist | object[] | 文件列表 |
|
||||||
|
| data.torrent_filelist[].size | int | 文件大小 |
|
||||||
|
| data.torrent_filelist[].path | string | 文件路径 |
|
||||||
|
| data.torrent_filelist[].wanted | int | 文件是否默认选中 |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:-------|:---------------------|:---------------------------------|
|
||||||
|
| 20018 | 文件不存在或已删除 | 检查提取码、文件归属和种子SHA1 |
|
||||||
|
| 990002 | 参数错误 | 检查必填参数 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"file_size": 0,
|
||||||
|
"torrent_name": "",
|
||||||
|
"file_count": 0,
|
||||||
|
"info_hash": "",
|
||||||
|
"torrent_filelist": [
|
||||||
|
{
|
||||||
|
"size": 0,
|
||||||
|
"path": "",
|
||||||
|
"wanted": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 种子文件需属于当前授权用户、位于指定存储区域,并且文件SHA1与 `torrent_sha1` 一致。
|
||||||
|
- 现有开放平台文档建议先将种子文件上传至“云下载/种子文件”文件夹,但该目录不是硬性要求。
|
||||||
|
|
||||||
|
### 性能与安全说明
|
||||||
|
|
||||||
|
- 用户身份由 Bearer access_token 解析,请求参数不能指定115账号。
|
||||||
|
- 接口按应用、用户和接口维度执行频率限制。
|
||||||
|
- 种子文件必须属于当前授权账号,并且文件SHA1与 `torrent_sha1` 一致。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 种子解析结果以响应中的 `data` 字段为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
## 开发者商业价值转化:推广产品得收益
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------|:--------------------|
|
||||||
|
| 文档名称 | 开发者商业价值转化:推广产品得收益 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
|
||||||
|
## 一、简介
|
||||||
|
|
||||||
|
为帮助开发者实现商业价值转化,115生活开放平台推出“推广产品得收益”计划,通过“推广有奖、收益共享”的方式,与开发者共同构建可持续发展的开放生态。
|
||||||
|
|
||||||
|
开发者接入标准化服务接口后,可以在用户需要升级使用权限、扩充长期存储空间或增加云下载配额时,引导用户购买对应的 115 增值服务,并基于用户实际购买的产品获取相应推广收益。
|
||||||
|
|
||||||
|
## 二、服务形式说明
|
||||||
|
|
||||||
|
### 1. 标准化服务接口
|
||||||
|
|
||||||
|
标准化服务接口覆盖以下核心场景。
|
||||||
|
|
||||||
|
#### 1.1. VIP 服务
|
||||||
|
|
||||||
|
适用于用户对应功能使用权限不足的场景:
|
||||||
|
|
||||||
|
- 视频播放权限升级:解决非 VIP 用户不支持在线预览视频、年费VIP以下用户不支持视频 4K 超轻转码等权限限制。
|
||||||
|
- 大文件上传权限升级:解决月费VIP以下用户不支持上传 115GB 大文件等权限限制。
|
||||||
|
|
||||||
|
开发者可以引导用户升级至更高的 VIP 类型,以获取对应使用权限。
|
||||||
|
|
||||||
|
#### 1.2. 长期存储空间扩容服务
|
||||||
|
|
||||||
|
适用于用户存储空间不足的场景,可解决因空间容量不足导致文件上传、复制及添加云下载失败等问题。
|
||||||
|
|
||||||
|
开发者可以引导用户购买 VIP 服务以获取更多长期存储空间,或单独购买长期存储空间进行扩容。
|
||||||
|
|
||||||
|
#### 1.3. 云下载配额
|
||||||
|
|
||||||
|
适用于用户云下载配额不足的场景,可解决因服务配额不足导致添加云下载失败等问题。
|
||||||
|
|
||||||
|
开发者可以引导用户升级至更高的 VIP 类型以获取更多云下载配额,或单独购买云下载配额。
|
||||||
|
|
||||||
|
### 2. 收益获取模式
|
||||||
|
|
||||||
|
用户通过开发者应用进入购买页面并成功购买以下任一服务产品后,开发者可以获得对应推广收益:
|
||||||
|
|
||||||
|
- 115生活 VIP 服务,包括月费VIP、年费VIP等。
|
||||||
|
- 长期存储空间扩容。
|
||||||
|
- 云下载配额。
|
||||||
|
|
||||||
|
VIP 服务购买页面示例:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
长期存储空间扩容购买页面示例:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 三、接入流程
|
||||||
|
|
||||||
|
### 1. 成为开发者
|
||||||
|
|
||||||
|
已成为 115 生活开发者的用户可以直接进行接口接入;未注册的开发者,请先参考[接入流程](../接入指南/接入流程.md)完成注册。
|
||||||
|
|
||||||
|
### 2. 接口接入
|
||||||
|
|
||||||
|
开发者可以调用“获取产品列表地址”接口取得购买页面地址,并在对应业务场景中引导用户访问。
|
||||||
|
|
||||||
|
#### 接口信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------|:------------|
|
||||||
|
| 接口名称 | 获取产品列表地址 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /vip/qr_url |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
该接口用于获取 115 生活开放平台增值服务产品列表地址。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/vip/qr_url
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------------------|:-------|:---|:-----|:---------------------------|:--------------------------------------------------------|
|
||||||
|
| default_product_id | int | 否 | null | 打开产品列表时默认选中的产品 ID | 月费:`5`;年费:`1`;尝鲜 1 天:`101`;长期VIP(至尊版):`24072401` |
|
||||||
|
| open_device | string | 是 | - | 设备号 | `DEVICE_ID_PLACEHOLDER` |
|
||||||
|
| hide_title | int | 否 | 0 | 是否隐藏购买页推荐人信息:0-不隐藏;1-隐藏 | `0` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/vip/qr_url' \
|
||||||
|
-H 'Authorization: Bearer ACCESS_TOKEN_PLACEHOLDER' \
|
||||||
|
--data-urlencode 'default_product_id=1' \
|
||||||
|
--data-urlencode 'open_device=DEVICE_ID_PLACEHOLDER' \
|
||||||
|
--data-urlencode 'hide_title=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------|:--------|:--------------|
|
||||||
|
| state | boolean | 状态码,true 表示成功 |
|
||||||
|
| message | string | 响应信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.qrcode_url | string | 开放平台产品列表地址 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"qrcode_url": "PRODUCT_LIST_URL_PLACEHOLDER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 接口根据 `access_token` 识别授权信息,调用方无需额外传入 115 账号和 AppID。
|
||||||
|
- `open_device` 不能为空;未传或传入空值时,接口返回参数错误。
|
||||||
|
- `default_product_id` 未传或为空时,服务端不指定默认产品。
|
||||||
|
- `hide_title` 大于 0 时,服务端会将该参数传递给产品列表服务。
|
||||||
|
- `access_token` 属于敏感凭证,不得写入公开仓库、客户端日志或公开沟通内容。
|
||||||
|
|
||||||
|
### 3. 场景触发与收益
|
||||||
|
|
||||||
|
完成接口接入后,开发者可以在用户使用权限不足、长期存储空间不足或云下载配额不足时触发对应引导。用户完成购买后,开发者可以获得对应推广收益。
|
||||||
|
|
||||||
|
## 四、收益管理与结算
|
||||||
|
|
||||||
|
### 1. 收益查看与提现
|
||||||
|
|
||||||
|
开发者可以登录“115生活-生活-联盟”,或直接访问[115联盟](https://union.115.com/),查看收益明细等推广收益情况并进行提现操作。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 2. 规则说明
|
||||||
|
|
||||||
|
收益计算方式、结算方式和提现流程等详情,参见[联盟规则](https://union.115.com/?ac=help&i=10)。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----------------|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
|
| 2026年08月05日(周三) 17:30:01 | 恢复业务说明、接入流程及示例图片 |
|
||||||
|
| 2026年08月19日(周三) 11:00:08 | 长期VIP默认产品名称改为至尊版 |
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
## 删除或清空回收站
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:--------------------------------|
|
||||||
|
| 接口名称 | 删除或清空回收站 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /del |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
批量彻底删除回收站中的文件(夹),或在不传 `tid` 时清空回收站。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/rb/del
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----|:-------|:---|:----|:--------------------------------|:------|
|
||||||
|
| tid | string | 否 | "" | 需要删除的回收站ID,多个ID用半角逗号分隔,最多 1150 个;不传时清空回收站 | 1,2,3,4 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/rb/del' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'tid=1,2,3,4'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:---------|:--------------|
|
||||||
|
| state | boolean | 状态码,true 表示成功 |
|
||||||
|
| message | string | 错误信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | string[] | 响应数据 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||||
|
- 不传 `tid` 会清空当前授权用户的整个回收站,操作后无法恢复。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
## 删除文件
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:--------------------------------|
|
||||||
|
| 接口名称 | 删除文件 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /delete |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
批量删除文件(夹),删除操作异步执行并将目标移入回收站。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/ufile/delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-------|:---|:----|:-----------------------|:-------------------------------------------|
|
||||||
|
| file_ids | string | 是 | - | 需要删除的文件(夹)ID,多个ID用半角逗号分隔 | 3073323042143855813,3073323042143855822 |
|
||||||
|
| parent_id | string | 否 | 0 | 待删除文件(夹)所在的父目录ID | 3073311192547189943 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/ufile/delete' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'file_ids=3073323042143855813,3073323042143855822' \
|
||||||
|
--form-string 'parent_id=3073311192547189943'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:---------|:-----------------|
|
||||||
|
| state | boolean | 状态码,true 表示请求已受理 |
|
||||||
|
| message | string | 错误信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | string[] | 响应数据 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
## 回收站列表
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:--------------------------------|
|
||||||
|
| 接口名称 | 回收站列表 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /list |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
分页获取当前授权用户的回收站文件(夹)列表。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/rb/list
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------|:----|:---|:----|:--------|:------|
|
||||||
|
| limit | int | 否 | 30 | 单页记录数 | 最大 200 |
|
||||||
|
| offset | int | 否 | 0 | 数据显示偏移量 | 0 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/rb/list' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--data-urlencode 'limit=30' \
|
||||||
|
--data-urlencode 'offset=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------------|:--------|:-------------------------------|
|
||||||
|
| state | boolean | 状态码,true 表示成功 |
|
||||||
|
| message | string | 错误信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.offset | int | 数据显示偏移量 |
|
||||||
|
| data.limit | int | 单页记录数 |
|
||||||
|
| data.count | string | 回收站文件(夹)总数 |
|
||||||
|
| data.rb_pass | int | 是否设置回收站密码:1-是,0-否 |
|
||||||
|
| data.{回收站ID} | object | 以回收站ID为键的文件(夹)信息 |
|
||||||
|
| data.{回收站ID}.id | string | 回收站ID |
|
||||||
|
| data.{回收站ID}.file_name | string | 文件(夹)名称 |
|
||||||
|
| data.{回收站ID}.type | string | 类型:1-文件,2-文件夹 |
|
||||||
|
| data.{回收站ID}.file_size | string | 文件大小,单位为字节 |
|
||||||
|
| data.{回收站ID}.dtime | string | 删除时间 |
|
||||||
|
| data.{回收站ID}.thumb_url | string | 缩略图地址 |
|
||||||
|
| data.{回收站ID}.status | string | 还原状态:-1-还原中,0-正常 |
|
||||||
|
| data.{回收站ID}.cid | int | 原文件(夹)的父目录ID |
|
||||||
|
| data.{回收站ID}.parent_name | string | 原文件(夹)的父目录名称 |
|
||||||
|
| data.{回收站ID}.pick_code | string | 文件提取码 |
|
||||||
|
| data.{回收站ID}.isv | int | 是否为视频文件,按文件类型返回 |
|
||||||
|
| data.{回收站ID}.def2 | int | 视频清晰度,按文件类型返回 |
|
||||||
|
| data.{回收站ID}.ico | string | 文件扩展名,按文件类型返回 |
|
||||||
|
| data.{回收站ID}.muc | string | 音频封面地址,按文件类型返回 |
|
||||||
|
| data.{回收站ID}.d_img | string | 文档缩略图地址,按文件类型返回 |
|
||||||
|
| data.{回收站ID}.play_long | int | 音视频时长,按文件类型返回 |
|
||||||
|
| data.{回收站ID}.sha1 | string | 文件 SHA1 值,按文件类型返回 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"offset": 0,
|
||||||
|
"limit": 30,
|
||||||
|
"count": "0",
|
||||||
|
"rb_pass": 0,
|
||||||
|
"3074054555277845747": {
|
||||||
|
"id": "3074054555277845747",
|
||||||
|
"file_name": "",
|
||||||
|
"type": "1",
|
||||||
|
"file_size": "0",
|
||||||
|
"dtime": "",
|
||||||
|
"thumb_url": "",
|
||||||
|
"status": "0",
|
||||||
|
"cid": 0,
|
||||||
|
"parent_name": "",
|
||||||
|
"pick_code": "",
|
||||||
|
"isv": 0,
|
||||||
|
"def2": 0,
|
||||||
|
"ico": "",
|
||||||
|
"muc": "",
|
||||||
|
"d_img": "",
|
||||||
|
"play_long": 0,
|
||||||
|
"sha1": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
|
| 2026年08月05日(周三) 17:15:28 | 补充单页记录数上限 |
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
## 回收站还原
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:--------------------------------|
|
||||||
|
| 接口名称 | 回收站还原 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /revert |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
批量还原回收站中的文件(夹)。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/rb/revert
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----|:-------|:---|:----|:---------------------------|:--------------|
|
||||||
|
| tid | string | 是 | - | 需要还原的回收站ID,多个ID用半角逗号分隔,最多 1150 个 | 111,222,333,444 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/rb/revert' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'tid=111,222,333,444'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:---------|:--------------|
|
||||||
|
| state | boolean | 状态码,true 表示成功 |
|
||||||
|
| message | string | 错误信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | string[] | 响应数据 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
## 文件(夹)更新
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:--------------------------------|
|
||||||
|
| 接口名称 | 文件(夹)更新 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /update |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
更新文件(夹)名称或星标状态。`file_name` 与 `star` 至少传入一个。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/ufile/update
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:---------|:-------|:---|:----|:----------------------|:------------------|
|
||||||
|
| file_id | string | 是 | - | 需要更新的文件(夹)ID | 3073323042143855813 |
|
||||||
|
| file_name | string | 否 | null | 新的文件(夹)名称,文件夹名称限制 255 字节 | 新的名字 |
|
||||||
|
| star | int | 否 | null | 是否星标:1-星标,0-取消星标 | 1 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/ufile/update' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'file_id=3073323042143855813' \
|
||||||
|
--form-string 'file_name=新的名字' \
|
||||||
|
--form-string 'star=1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:---------------|:--------|:------------------|
|
||||||
|
| state | boolean | 状态码,true 表示成功 |
|
||||||
|
| message | string | 错误信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.file_name | string | 更新后的文件(夹)名称 |
|
||||||
|
| data.star | int | 更新后的星标状态:1-星标,0-取消星标 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"file_name": "新的名字",
|
||||||
|
"star": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 上传流程
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:---------|:------------------------------|
|
||||||
|
| 文档名称 | 上传流程 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
| 适用场景 | 115开放平台文件上传 |
|
||||||
|
|
||||||
|
### 文档说明
|
||||||
|
|
||||||
|
本文档说明 115 开放平台的文件秒传、普通上传和断点续传流程。
|
||||||
|
|
||||||
|
## 流程概览
|
||||||
|
|
||||||
|
1. 请求「文件上传」接口初始化上传。
|
||||||
|
2. 若响应中 `status=2`,表示秒传成功,上传流程结束。
|
||||||
|
3. 若响应提示需要二次认证,按 `sign_check` 指定的字节范围计算大写 SHA1,然后携带 `sign_key` 和 `sign_val` 重新请求「文件上传」接口。
|
||||||
|
4. 若 `status=1`,携带初始化响应中的 `bucket`、`object`、`callback` 及「获取上传凭证」接口返回的凭证,向对象存储上传文件。
|
||||||
|
5. 需要续传时,携带初始化响应中的 `pick_code` 及待上传文件信息请求「断点续传」接口,获取新的对象存储上传参数。
|
||||||
|
6. 对象存储返回上传成功后,普通上传或断点续传完成。
|
||||||
|
|
||||||
|
## 相关接口
|
||||||
|
|
||||||
|
- [文件上传](文件上传.md)
|
||||||
|
- [获取上传凭证](获取上传凭证.md)
|
||||||
|
- [断点续传](断点续传.md)
|
||||||
|
- [阿里云 OSS 上传文件说明](https://help.aliyun.com/zh/oss/user-guide/upload-objects-to-oss/)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 对象存储上传不请求 `proapi.115.com`,应使用「获取上传凭证」和上传调度接口返回的域名、对象标识、临时凭证与回调参数发起请求。
|
||||||
|
- `sign_check` 的起止字节均在 SHA1 计算范围内。例如 `0-99` 表示计算共 100 字节的内容。
|
||||||
|
- 调用开放平台接口时必须携带 `Authorization: Bearer access_token`,并妥善保管临时上传凭证。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
## 文件上传
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:---------|:------------------------------|
|
||||||
|
| 接口名称 | 文件上传 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /upload/init |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
初始化断点续传调度,完成秒传判定、二次认证调度或返回对象存储上传参数。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/upload/init
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-----|:---|:----|:---------------------------|:--------------------|
|
||||||
|
| file_name | string | 是 | - | 文件名 | 图片.jpg |
|
||||||
|
| file_size | int | 是 | - | 文件大小,单位为字节 | 5335 |
|
||||||
|
| target | string | 是 | - | 文件上传目标 | `U_1_0`,格式为 `U_1_<文件夹ID>` |
|
||||||
|
| fileid | string | 是 | - | 文件 SHA1 值 | `<FILE_SHA1>` |
|
||||||
|
| preid | string | 否 | "" | 文件前 128 KiB 内容的 SHA1 值 | `<PREID_SHA1>` |
|
||||||
|
| path | string | 否 | "" | 上传路径 | "" |
|
||||||
|
| pick_code | string | 否 | "" | 上传任务唯一标识,用于续传 | `<PICK_CODE>` |
|
||||||
|
| topupload | int | 否 | "" | 上传调度文件类型标记,见下方枚举表格 | 0 |
|
||||||
|
| sign_key | string | 否 | "" | 二次认证标识 | `<SIGN_KEY>` |
|
||||||
|
| sign_val | string | 否 | "" | 根据 `sign_check` 计算的大写 SHA1 值 | `<SIGN_VAL>` |
|
||||||
|
|
||||||
|
#### 请求参数中的 topupload 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:---|:---------------------------|:---|
|
||||||
|
| -1 | 没有上传调度文件类型标记 | - |
|
||||||
|
| 0 | 单文件上传任务,记录一条独立上传记录 | - |
|
||||||
|
| 1 | 文件夹任务的第一个子文件,记录一次文件夹上传 | - |
|
||||||
|
| 2 | 文件夹任务的其他子文件,不单独记录上传记录 | - |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/upload/init' \
|
||||||
|
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
|
||||||
|
-F 'file_name=图片.jpg' \
|
||||||
|
-F 'file_size=5335' \
|
||||||
|
-F 'target=U_1_0' \
|
||||||
|
-F 'fileid=<FILE_SHA1>' \
|
||||||
|
-F 'topupload=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------------------------|:------|:----------------------------------------|
|
||||||
|
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object | 上传调度数据 |
|
||||||
|
| data.status | int | 上传状态:1-非秒传 2-秒传 |
|
||||||
|
| data.code | int | 上传调度状态码 |
|
||||||
|
| data.pick_code | string | 上传任务唯一标识,用于续传 |
|
||||||
|
| data.target | string | 文件上传目标 |
|
||||||
|
| data.bucket | string | 对象存储 bucket |
|
||||||
|
| data.object | string | OSS 对象标识 |
|
||||||
|
| data.callback | object | 上传完成回调数据 |
|
||||||
|
| data.callback.callback | string | 上传完成回调信息 |
|
||||||
|
| data.callback.callback_var | string | 上传完成回调参数 |
|
||||||
|
| data.sign_key | string | 本次二次认证的 SHA1 标识 |
|
||||||
|
| data.sign_check | string | 二次认证所需本地文件 SHA1 计算的字节范围 |
|
||||||
|
| data.file_id | string | 秒传成功时新增文件 ID |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"status": 1,
|
||||||
|
"code": 0,
|
||||||
|
"pick_code": "",
|
||||||
|
"target": "U_1_0",
|
||||||
|
"bucket": "",
|
||||||
|
"object": "",
|
||||||
|
"callback": {
|
||||||
|
"callback": "",
|
||||||
|
"callback_var": ""
|
||||||
|
},
|
||||||
|
"sign_key": "",
|
||||||
|
"sign_check": "",
|
||||||
|
"file_id": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- `target` 必须匹配 `U_1_<数字文件夹ID>`;`U_1_0` 表示网盘根目录。
|
||||||
|
- 非 VIP 用户单文件大小不得超过 5 GiB;尝鲜VIP、体验VIP用户单文件大小不得超过 15 GiB。
|
||||||
|
- 上传前会检查用户剩余空间和盗播上传封禁状态。
|
||||||
|
- 二次认证调度结果见下表。
|
||||||
|
|
||||||
|
| code | status | 说明 | 后续处理 |
|
||||||
|
|:-----|:-------|:---------|:------|
|
||||||
|
| 700 | 6 | 签名认证后失败 | 按 `sign_check` 截取包含起止字节的文件内容计算大写 SHA1,再传入 `sign_key` 和 `sign_val` |
|
||||||
|
| 701 | 7 | 需要认证签名 | 按 `sign_check` 截取包含起止字节的文件内容计算大写 SHA1,再传入 `sign_key` 和 `sign_val` |
|
||||||
|
| 702 | 8 | 签名认证失败 | 按 `sign_check` 截取包含起止字节的文件内容计算大写 SHA1,再传入 `sign_key` 和 `sign_val` |
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `sign_check` 格式为 `起始字节-结束字节`,起止字节均包含在 SHA1 计算范围内;例如 `2392148-2392298` 需计算该范围内文件内容的 SHA1。
|
||||||
|
- 省略 `topupload` 时,服务端会将其归一化为空字符串;显式传入枚举值时会归一化为整数。
|
||||||
|
- 请妥善保管 `access_token`、上传凭证与回调数据,不要在日志或客户端可见信息中输出。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
## 断点续传
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:---------|:------------------------------|
|
||||||
|
| 接口名称 | 断点续传 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /upload/resume |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
根据已有上传任务和待上传文件信息,获取断点续传所需的对象存储上传参数。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/upload/resume
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-----|:---|:----|:------------------|:--------------------|
|
||||||
|
| file_size | int | 是 | - | 文件大小,单位为字节 | 5335 |
|
||||||
|
| target | string | 是 | - | 文件上传目标 | `U_1_0`,格式为 `U_1_<文件夹ID>` |
|
||||||
|
| fileid | string | 是 | - | 文件 SHA1 值 | `<FILE_SHA1>` |
|
||||||
|
| pick_code | string | 是 | - | 上传任务唯一标识 | `<PICK_CODE>` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/upload/resume' \
|
||||||
|
-H 'Authorization: Bearer <ACCESS_TOKEN>' \
|
||||||
|
-F 'file_size=5335' \
|
||||||
|
-F 'target=U_1_0' \
|
||||||
|
-F 'fileid=<FILE_SHA1>' \
|
||||||
|
-F 'pick_code=<PICK_CODE>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------------------------|:------|:----------------------|
|
||||||
|
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object | 续传调度数据 |
|
||||||
|
| data.version | string | 上传接口版本 |
|
||||||
|
| data.target | string | 文件上传目标 |
|
||||||
|
| data.pick_code | string | 上传任务唯一标识 |
|
||||||
|
| data.bucket | string | 对象存储 bucket |
|
||||||
|
| data.object | string | OSS 对象标识 |
|
||||||
|
| data.callback | object | 上传完成回调数据 |
|
||||||
|
| data.callback.callback | string | 上传完成回调信息 |
|
||||||
|
| data.callback.callback_var | string | 上传完成回调参数 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"version": "",
|
||||||
|
"target": "U_1_0",
|
||||||
|
"pick_code": "",
|
||||||
|
"bucket": "",
|
||||||
|
"object": "",
|
||||||
|
"callback": {
|
||||||
|
"callback": "",
|
||||||
|
"callback_var": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- `target` 必须匹配 `U_1_<数字文件夹ID>`;`U_1_0` 表示网盘根目录。
|
||||||
|
- 非 VIP 用户单文件大小不得超过 5 GiB;尝鲜VIP、体验VIP用户单文件大小不得超过 15 GiB。
|
||||||
|
- 续传前会检查用户剩余空间和盗播上传封禁状态。
|
||||||
|
- 上游调度结果的 `status` 只有为 1 或 2 时才按成功响应返回续传参数。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `pick_code` 必须来自原上传初始化调度响应,并与当前文件信息匹配。
|
||||||
|
- 请妥善保管 `access_token`、上传凭证与回调数据,不要在日志或客户端可见信息中输出。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
## 获取上传凭证
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:---------|:------------------------------|
|
||||||
|
| 接口名称 | 获取上传凭证 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /upload/get_token |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
获取对象存储上传域名和临时上传凭证。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/upload/get_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
无
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/upload/get_token' \
|
||||||
|
-H 'Authorization: Bearer <ACCESS_TOKEN>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-------------------|:------|:---------------|
|
||||||
|
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object | 上传凭证数据 |
|
||||||
|
| data.endpoint | string | 上传域名 |
|
||||||
|
| data.AccessKeySecret | string | 临时上传凭证密钥 |
|
||||||
|
| data.SecurityToken | string | 临时安全令牌 |
|
||||||
|
| data.Expiration | string | 上传凭证过期时间 |
|
||||||
|
| data.AccessKeyId | string | 临时上传凭证 ID |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"endpoint": "",
|
||||||
|
"AccessKeySecret": "",
|
||||||
|
"SecurityToken": "",
|
||||||
|
"Expiration": "",
|
||||||
|
"AccessKeyId": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 网页版文档中的密钥字段名存在拼写误差,服务端实际返回字段为 `AccessKeySecret`。
|
||||||
|
- 上传凭证为敏感信息,仅用于当前上传流程;不要写入日志、持久化存储或对外暴露。
|
||||||
|
- 服务端通过当前 `access_token` 识别用户,不接收用户账号请求参数。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
## 文件复制
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 文件复制 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /copy |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
将一个或多个文件、文件夹复制到指定目录。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/ufile/copy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:--------|:-------|:---|:----|:------------------------------------|:--------------------|
|
||||||
|
| pid | string | 否 | "0" | 目标目录ID,根目录ID为 `0` | 1054251402869818368 |
|
||||||
|
| file_id | string | 是 | - | 待复制的文件或文件夹ID,多个ID使用半角逗号分隔 | 2323423573680609857 |
|
||||||
|
| nodupli | int | 否 | 0 | 目标目录是否不允许同名:0-允许,1-不允许 | 1 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/ufile/copy' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'pid=1054251402869818368' \
|
||||||
|
--form-string 'file_id=2323423573680609857' \
|
||||||
|
--form-string 'nodupli=1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:---------|:--------------------|
|
||||||
|
| state | boolean | 接口状态,true表示成功 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object[] | 响应数据 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `pid` 不传或传 `0` 时复制到根目录。
|
||||||
|
- 接口受文件操作频率限制;触发限制时返回异常。
|
||||||
|
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
## 文件搜索
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 文件搜索 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /search |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
根据文件名搜索文件或文件夹,支持按目录、文件标签、时间范围和文件类型筛选。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/ufile/search
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:------------|:-------|:---|:----|:----------------------------------------------------------|:---------|
|
||||||
|
| search_value | string | 否 | "" | 搜索关键词;与 `file_label` 至少传一个,最多取前40个字符 | "文件" |
|
||||||
|
| limit | int | 否 | 20 | 单页记录数;`offset + limit` 最大不超过10000 | 20 |
|
||||||
|
| offset | int | 否 | 0 | 数据显示偏移量 | 0 |
|
||||||
|
| file_label | string | 否 | "" | 文件标签;与 `search_value` 至少传一个 | "1" |
|
||||||
|
| cid | int | 否 | 0 | 目标目录ID;`-1` 表示不返回任何列表内容 | 0 |
|
||||||
|
| gte_day | string | 否 | "" | 搜索结果匹配的开始日期 | 2020-11-19 |
|
||||||
|
| lte_day | string | 否 | "" | 搜索结果匹配的结束日期 | 2020-11-20 |
|
||||||
|
| fc | int | 否 | 0 | 显示类型:1-只显示文件夹,2-只显示文件,0-全部 | 0 |
|
||||||
|
| type | int | 否 | 0 | 一级筛选大分类,见下方枚举表格 | 1 |
|
||||||
|
| suffix | string | 否 | "" | 一级筛选选择“其他”时填写的后缀名 | "pdf" |
|
||||||
|
|
||||||
|
#### 请求参数中的 type 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:--|:----|:---|
|
||||||
|
| 1 | 文档 | - |
|
||||||
|
| 2 | 图片 | - |
|
||||||
|
| 3 | 音频 | - |
|
||||||
|
| 4 | 视频 | - |
|
||||||
|
| 5 | 压缩包 | - |
|
||||||
|
| 6 | 应用 | - |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/ufile/search' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--data-urlencode 'search_value=文件' \
|
||||||
|
--data-urlencode 'limit=20' \
|
||||||
|
--data-urlencode 'offset=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-------------------|:---------|:----------------------------------------|
|
||||||
|
| count | int | 符合条件的文件或文件夹总数 |
|
||||||
|
| data | object[] | 文件或文件夹列表 |
|
||||||
|
| data[].file_id | string | 文件或文件夹ID |
|
||||||
|
| data[].user_id | string | 115账号 |
|
||||||
|
| data[].sha1 | string | 文件SHA-1值 |
|
||||||
|
| data[].file_name | string | 文件或文件夹名称 |
|
||||||
|
| data[].file_size | string | 文件大小 |
|
||||||
|
| data[].user_ptime | string | 上传时间 |
|
||||||
|
| data[].user_utime | string | 更新时间 |
|
||||||
|
| data[].pick_code | string | 文件提取码 |
|
||||||
|
| data[].parent_id | string | 父目录ID |
|
||||||
|
| data[].area_id | string | 文件状态:1-正常,7-已删除(回收站),120-彻底删除 |
|
||||||
|
| data[].is_private | int | 文件是否隐藏:0-未隐藏,1-已隐藏 |
|
||||||
|
| data[].file_category | string | 文件属性:1-文件,0-文件夹 |
|
||||||
|
| data[].ico | string | 文件后缀 |
|
||||||
|
| limit | int | 分页数量 |
|
||||||
|
| offset | int | 偏移量 |
|
||||||
|
| state | boolean | 接口状态,true表示成功 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"count": 0,
|
||||||
|
"data": [],
|
||||||
|
"limit": 20,
|
||||||
|
"offset": 0,
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `search_value` 与 `file_label` 至少传一个;搜索关键词最多取前40个字符。
|
||||||
|
- 当 `offset + limit` 超过10000时,服务端按 `offset=0`、`limit=115` 查询。
|
||||||
|
- 搜索结果会过滤不属于正常区域的文件;当前账号未开启隐藏文件展示时,也会过滤隐藏文件。
|
||||||
|
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
## 文件移动
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 文件移动 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /move |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
将一个或多个文件、文件夹移动到指定目录。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/ufile/move
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:---------|:-------|:---|:----|:------------------------------------|:----------------------------------------|
|
||||||
|
| file_ids | string | 是 | - | 待移动的文件或文件夹ID,多个ID使用半角逗号分隔 | 3073323042143855813,3073323042143855822 |
|
||||||
|
| to_cid | string | 否 | "0" | 目标目录ID;`0` 表示根目录,非 `0` 时必须指向正常可用的目录 | 3073311192547189943 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/ufile/move' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'file_ids=3073323042143855813,3073323042143855822' \
|
||||||
|
--form-string 'to_cid=3073311192547189943'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:---------|:--------------------|
|
||||||
|
| state | boolean | 接口状态,true表示成功 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object[] | 响应数据 |
|
||||||
|
|
||||||
|
#### 响应的 code 字段(错误码)说明
|
||||||
|
|
||||||
|
| 错误码 | 说明 | 解决方案 |
|
||||||
|
|:-------|:-----------------------------------|:-----------------------------|
|
||||||
|
| 20009 | 目标目录不存在或目标ID不是目录 | 确认 to_cid 指向存在的目录 |
|
||||||
|
| 20018 | 目标目录不在正常区域或已经删除 | 选择正常可用的目标目录 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `to_cid` 不传或传 `0` 时移动到根目录。
|
||||||
|
- `to_cid` 非 `0` 时,目标必须是正常可用的目录;目标不存在、不是目录或已经删除时,移动失败。
|
||||||
|
- 接口受文件操作频率限制;触发限制时返回异常。
|
||||||
|
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:---------------------------------|:---------------------------------------|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
|
| 2026年08月14日(周五) 15:38:44 | 补充移动目标目录有效性约束及错误码 |
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
## 新建文件夹
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 新建文件夹 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /add |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
在指定父目录下新建文件夹。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/folder/add
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:---------|:-------|:---|:----|:---------------------------|:--------------------|
|
||||||
|
| pid | string | 否 | "0" | 父目录ID,根目录ID为 `0` | 3073323042143855813 |
|
||||||
|
| file_name | string | 是 | - | 文件夹名称,最多255个字符 | 新建文件夹名称 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/folder/add' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'pid=3073323042143855813' \
|
||||||
|
--form-string 'file_name=新建文件夹名称'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:---------------|:--------|:----------------------|
|
||||||
|
| state | boolean | 接口状态,true表示成功 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.file_name | string | 新建的文件夹名称 |
|
||||||
|
| data.file_id | string | 新建的文件夹ID |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"file_name": "",
|
||||||
|
"file_id": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `pid` 不传或传 `0` 时在根目录下新建文件夹。
|
||||||
|
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
## 按ID获取
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 按ID获取 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /get_info |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
根据文件或文件夹ID获取详情。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/folder/get_info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:---------|:-------|:---|:----|:----------|:--------------------|
|
||||||
|
| file_id | string | 是 | - | 文件或文件夹ID | 1288444975268439877 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/folder/get_info' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--data-urlencode 'file_id=1288444975268439877'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------------|:---------|:----------------------------------------|
|
||||||
|
| state | boolean | 接口状态,true表示成功 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object | 文件或文件夹详情 |
|
||||||
|
| data.count | int | 包含的文件总数 |
|
||||||
|
| data.size | string | 文件或文件夹总大小 |
|
||||||
|
| data.size_byte | int | 文件或文件夹总大小,单位为字节 |
|
||||||
|
| data.folder_count | int | 包含的文件夹总数 |
|
||||||
|
| data.play_long | int | 视频时长;`-1` 表示正在统计,其他数值单位为秒 |
|
||||||
|
| data.show_play_long | int | 是否开启展示视频时长 |
|
||||||
|
| data.ptime | string | 上传时间 |
|
||||||
|
| data.utime | string | 修改时间 |
|
||||||
|
| data.file_name | string | 文件或文件夹名称 |
|
||||||
|
| data.pick_code | string | 文件提取码 |
|
||||||
|
| data.sha1 | string | 文件SHA-1值 |
|
||||||
|
| data.file_id | string | 文件或文件夹ID |
|
||||||
|
| data.is_mark | string | 是否星标 |
|
||||||
|
| data.open_time | int | 文件或文件夹最近打开时间 |
|
||||||
|
| data.file_category | string | 文件属性:1-文件,0-文件夹 |
|
||||||
|
| data.paths | object[] | 文件或文件夹所在路径 |
|
||||||
|
| data.paths[].file_id | string | 父目录ID |
|
||||||
|
| data.paths[].file_name | string | 父目录名称 |
|
||||||
|
| data.paths[].iss | int | 父目录共享状态标识 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"count": 0,
|
||||||
|
"size": "",
|
||||||
|
"size_byte": 0,
|
||||||
|
"folder_count": 0,
|
||||||
|
"play_long": 0,
|
||||||
|
"show_play_long": 0,
|
||||||
|
"ptime": "",
|
||||||
|
"utime": "",
|
||||||
|
"file_name": "",
|
||||||
|
"pick_code": "",
|
||||||
|
"sha1": "",
|
||||||
|
"file_id": "",
|
||||||
|
"is_mark": "",
|
||||||
|
"open_time": 0,
|
||||||
|
"file_category": "",
|
||||||
|
"paths": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 文件或文件夹已进入回收站或被彻底删除时,接口返回异常。
|
||||||
|
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
## 按路径获取
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 按路径获取 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /get_info |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
根据文件或文件夹路径获取详情。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/folder/get_info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----|:-------|:---|:----|:----------------------------------------------------------------|:-------------------|
|
||||||
|
| path | string | 是 | - | 文件路径,支持 `/`、`>` 两种分隔符;路径需以分隔符开头,并用同一分隔符分隔目录层级 | /a/b/c.png 或 >a>b>c |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/folder/get_info' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'path=/a/b/c.png'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------------|:---------|:----------------------------------------|
|
||||||
|
| state | boolean | 接口状态,true表示成功 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object | 文件或文件夹详情 |
|
||||||
|
| data.count | int | 包含的文件总数 |
|
||||||
|
| data.size | string | 文件或文件夹总大小 |
|
||||||
|
| data.size_byte | int | 文件或文件夹总大小,单位为字节 |
|
||||||
|
| data.folder_count | int | 包含的文件夹总数 |
|
||||||
|
| data.play_long | int | 视频时长;`-1` 表示正在统计,其他数值单位为秒 |
|
||||||
|
| data.show_play_long | int | 是否开启展示视频时长 |
|
||||||
|
| data.ptime | string | 上传时间 |
|
||||||
|
| data.utime | string | 修改时间 |
|
||||||
|
| data.file_name | string | 文件或文件夹名称 |
|
||||||
|
| data.pick_code | string | 文件提取码 |
|
||||||
|
| data.sha1 | string | 文件SHA-1值 |
|
||||||
|
| data.file_id | string | 文件或文件夹ID |
|
||||||
|
| data.is_mark | string | 是否星标 |
|
||||||
|
| data.open_time | int | 文件或文件夹最近打开时间 |
|
||||||
|
| data.file_category | string | 文件属性:1-文件,0-文件夹 |
|
||||||
|
| data.paths | object[] | 文件或文件夹所在路径 |
|
||||||
|
| data.paths[].file_id | string | 父目录ID |
|
||||||
|
| data.paths[].file_name | string | 父目录名称 |
|
||||||
|
| data.paths[].iss | int | 父目录共享状态标识 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"count": 0,
|
||||||
|
"size": "",
|
||||||
|
"size_byte": 0,
|
||||||
|
"folder_count": 0,
|
||||||
|
"play_long": 0,
|
||||||
|
"show_play_long": 0,
|
||||||
|
"ptime": "",
|
||||||
|
"utime": "",
|
||||||
|
"file_name": "",
|
||||||
|
"pick_code": "",
|
||||||
|
"sha1": "",
|
||||||
|
"file_id": "",
|
||||||
|
"is_mark": "",
|
||||||
|
"open_time": 0,
|
||||||
|
"file_category": "",
|
||||||
|
"paths": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 文件或文件夹已进入回收站或被彻底删除时,接口返回异常。
|
||||||
|
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
## 获取文件下载地址
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:--------------------------------|
|
||||||
|
| 接口名称 | 获取文件下载地址 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /downurl |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
根据文件提取码获取文件下载地址。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/ufile/downurl
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:---------|:-------|:---|:----|:------|:-----------------|
|
||||||
|
| pick_code | string | 是 | - | 文件提取码,多个提取码用半角逗号分隔 | dtctprlmfkl4exiok |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/ufile/downurl' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--form-string 'pick_code=dtctprlmfkl4exiok'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:---------------------------|:--------|:---------------------------|
|
||||||
|
| state | boolean | 状态码,true 表示成功 |
|
||||||
|
| message | string | 错误信息 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| errno | int | 下载地址获取失败时返回的错误码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.{文件ID} | object | 以文件ID为键的文件下载信息 |
|
||||||
|
| data.{文件ID}.file_name | string | 文件名 |
|
||||||
|
| data.{文件ID}.file_size | int | 文件大小,单位为字节 |
|
||||||
|
| data.{文件ID}.pick_code | string | 文件提取码 |
|
||||||
|
| data.{文件ID}.sha1 | string | 文件 SHA1 值 |
|
||||||
|
| data.{文件ID}.url | object | 下载地址信息 |
|
||||||
|
| data.{文件ID}.url.url | string | 文件下载地址 |
|
||||||
|
| data.can_appeal | boolean | 文件违规时是否可以申诉,按错误场景返回 |
|
||||||
|
| data.want_appeal_id | string | 文件违规时的申诉标识,按错误场景返回 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"2323423573680609857": {
|
||||||
|
"file_name": "",
|
||||||
|
"file_size": 0,
|
||||||
|
"pick_code": "",
|
||||||
|
"sha1": "",
|
||||||
|
"url": {
|
||||||
|
"url": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 由开放平台授权流程获取,并通过 `Authorization` 请求头传递。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
## 获取文件列表
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 获取文件列表 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /files |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
获取指定目录中的文件和文件夹列表,支持分页、排序以及按文件类型、后缀名和星标状态筛选。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/ufile/files
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-----------|:-------|:---|:----------|:-------------------------------------------------------|:----------|
|
||||||
|
| cid | string | 否 | "0" | 目录ID,对应 `parent_id`;根目录ID为 `0` | "0" |
|
||||||
|
| type | int | 否 | 0 | 文件类型,见下方枚举表格 | 1 |
|
||||||
|
| limit | int | 否 | 20 | 查询数量,最大1150 | 20 |
|
||||||
|
| offset | int | 否 | 0 | 查询起始位置 | 0 |
|
||||||
|
| suffix | string | 否 | "" | 文件后缀名 | "pdf" |
|
||||||
|
| asc | int | 否 | 0 | 排序方向:1-升序,0-降序 | 0 |
|
||||||
|
| o | string | 否 | user_ptime | 排序字段,见下方枚举表格 | file_name |
|
||||||
|
| custom_order | int | 否 | 0 | 排序模式,见下方枚举表格 | 0 |
|
||||||
|
| stdir | int | 否 | 0 | 筛选文件时是否显示文件夹:1-显示,0-不显示 | 1 |
|
||||||
|
| star | int | 否 | 0 | 星标筛选:1-仅显示星标文件,0-全部 | 0 |
|
||||||
|
| cur | int | 否 | 0 | 是否只显示当前文件夹内的文件:1-是,0-否 | 1 |
|
||||||
|
| show_dir | int | 否 | 0 | 是否显示目录:1-是,0-否 | 0 |
|
||||||
|
|
||||||
|
#### 请求参数中的 type 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:--|:----|:---|
|
||||||
|
| 1 | 文档 | - |
|
||||||
|
| 2 | 图片 | - |
|
||||||
|
| 3 | 音频 | - |
|
||||||
|
| 4 | 视频 | - |
|
||||||
|
| 5 | 压缩包 | - |
|
||||||
|
| 6 | 应用 | - |
|
||||||
|
| 7 | 书籍 | - |
|
||||||
|
|
||||||
|
#### 请求参数中的 o 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:-----------|:-------|:---|
|
||||||
|
| file_name | 文件名 | - |
|
||||||
|
| file_size | 文件大小 | - |
|
||||||
|
| user_ptime | 上传时间 | 默认值 |
|
||||||
|
| user_utime | 更新时间 | - |
|
||||||
|
| file_type | 文件类型 | - |
|
||||||
|
|
||||||
|
#### 请求参数中的 custom_order 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:--|:----------------------|:---|
|
||||||
|
| 0 | 使用记忆排序,自定义排序失效 | 默认值 |
|
||||||
|
| 1 | 使用自定义排序,不使用记忆排序 | - |
|
||||||
|
| 2 | 使用自定义排序,非文件夹置顶 | - |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/ufile/files' \
|
||||||
|
-H 'Authorization: Bearer access_token' \
|
||||||
|
--data-urlencode 'cid=0' \
|
||||||
|
--data-urlencode 'limit=20' \
|
||||||
|
--data-urlencode 'offset=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------------|:---------|:----------------------------------------------------------|
|
||||||
|
| data | object[] | 文件和文件夹列表 |
|
||||||
|
| data[].fid | string | 文件或文件夹ID |
|
||||||
|
| data[].aid | string | 文件状态:1-正常,7-已删除(回收站),120-彻底删除 |
|
||||||
|
| data[].pid | string | 父目录ID |
|
||||||
|
| data[].fc | string | 文件分类:0-文件夹,1-文件 |
|
||||||
|
| data[].fn | string | 文件或文件夹名称 |
|
||||||
|
| data[].fco | string | 文件夹封面 |
|
||||||
|
| data[].ism | string | 是否星标,1表示星标 |
|
||||||
|
| data[].isp | int | 是否加密,1表示加密 |
|
||||||
|
| data[].pc | string | 文件提取码 |
|
||||||
|
| data[].upt | int | 修改时间 |
|
||||||
|
| data[].uet | int | 修改时间 |
|
||||||
|
| data[].uppt | int | 上传时间 |
|
||||||
|
| data[].cm | int | 特殊目录标识 |
|
||||||
|
| data[].fdesc | string | 文件备注 |
|
||||||
|
| data[].ispl | int | 是否统计文件夹下视频时长 |
|
||||||
|
| data[].fl | object[] | 文件标签 |
|
||||||
|
| data[].fl[].id | string | 文件标签ID |
|
||||||
|
| data[].fl[].name | string | 文件标签名称 |
|
||||||
|
| data[].fl[].sort | string | 文件标签排序 |
|
||||||
|
| data[].fl[].color | string | 文件标签颜色 |
|
||||||
|
| data[].fl[].is_default | int | 文件标签类型:0-最近使用,1-非最近使用,2-默认标签 |
|
||||||
|
| data[].fl[].update_time | int | 文件标签更新时间 |
|
||||||
|
| data[].fl[].create_time | int | 文件标签创建时间 |
|
||||||
|
| data[].sha1 | string | 文件SHA-1值 |
|
||||||
|
| data[].fs | int | 文件大小,单位为字节 |
|
||||||
|
| data[].fta | string | 文件状态:0或2-未上传完成,1-已上传完成 |
|
||||||
|
| data[].ico | string | 文件后缀名 |
|
||||||
|
| data[].fatr | string | 音频长度 |
|
||||||
|
| data[].isv | int | 是否为视频 |
|
||||||
|
| data[].def | int | 视频清晰度:1-标清,2-高清,3-超清,4-1080P,5-4K,100-原画 |
|
||||||
|
| data[].def2 | int | 视频清晰度:1-标清,2-高清,3-超清,4-1080P,5-4K,100-原画 |
|
||||||
|
| data[].play_long | int | 音视频时长,单位为秒 |
|
||||||
|
| data[].v_img | string | 视频缩略图地址 |
|
||||||
|
| data[].thumb | string | 图片缩略图地址 |
|
||||||
|
| data[].uo | string | 原图地址 |
|
||||||
|
| count | int | 当前目录文件数量 |
|
||||||
|
| sys_count | int | 系统文件夹数量 |
|
||||||
|
| offset | int | 偏移量 |
|
||||||
|
| limit | int | 分页数量 |
|
||||||
|
| aid | string | 文件状态:1-正常,7-已删除(回收站),120-彻底删除 |
|
||||||
|
| cid | int | 父目录ID |
|
||||||
|
| is_asc | int | 排序方向:1-升序,0-降序 |
|
||||||
|
| min_size | int | 最小文件大小筛选值 |
|
||||||
|
| max_size | int | 最大文件大小筛选值 |
|
||||||
|
| sys_dir | string | 系统目录 |
|
||||||
|
| hide_data | string | 是否返回文件数据 |
|
||||||
|
| record_open_time | string | 是否记录文件夹打开时间 |
|
||||||
|
| star | int | 是否星标:1-星标,0-未星标 |
|
||||||
|
| type | int | 一级筛选大分类,见请求参数中的 `type` 字段枚举 |
|
||||||
|
| suffix | string | 一级筛选选择“其他”时填写的后缀名 |
|
||||||
|
| path | object[] | 父目录树 |
|
||||||
|
| path[].name | string | 父目录名称 |
|
||||||
|
| path[].aid | int | 父目录文件状态 |
|
||||||
|
| path[].cid | int | 父目录ID |
|
||||||
|
| path[].pid | int | 上级父目录ID |
|
||||||
|
| path[].isp | int | 父目录是否加密 |
|
||||||
|
| path[].p_cid | string | 父目录路径标识 |
|
||||||
|
| path[].fv | string | 父目录属性 |
|
||||||
|
| cur | int | 是否只显示当前文件夹内的文件 |
|
||||||
|
| stdir | int | 筛选文件时是否显示文件夹 |
|
||||||
|
| fields | string | 指定返回字段 |
|
||||||
|
| order | string | 实际使用的排序字段 |
|
||||||
|
| state | boolean | 接口状态,true表示成功 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [],
|
||||||
|
"count": 0,
|
||||||
|
"sys_count": 0,
|
||||||
|
"offset": 0,
|
||||||
|
"limit": 20,
|
||||||
|
"aid": "1",
|
||||||
|
"cid": 0,
|
||||||
|
"is_asc": 0,
|
||||||
|
"min_size": 0,
|
||||||
|
"max_size": 0,
|
||||||
|
"sys_dir": "",
|
||||||
|
"hide_data": "",
|
||||||
|
"record_open_time": "",
|
||||||
|
"star": 0,
|
||||||
|
"type": 0,
|
||||||
|
"suffix": "",
|
||||||
|
"path": [],
|
||||||
|
"cur": 0,
|
||||||
|
"stdir": 0,
|
||||||
|
"fields": "",
|
||||||
|
"order": "user_ptime",
|
||||||
|
"state": true,
|
||||||
|
"code": 0,
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `limit` 最大为1150。
|
||||||
|
- 当 `cid` 指向不存在或已删除的目录时,接口返回异常;当目录为加密目录时,不返回目录内容。
|
||||||
|
- `user_id` 由服务端根据 access token 获取,无需传入。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
## 用户信息
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:---------|:------------------------------|
|
||||||
|
| 接口名称 | 用户信息 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /user/info |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
获取当前授权用户的基本信息、网盘空间信息、VIP 等级信息以及第三方畅用权益信息。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/user/info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
无
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/user/info' \
|
||||||
|
-H 'Authorization: Bearer <ACCESS_TOKEN>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------------------------------|:------|:----------------------------------------|
|
||||||
|
| state | boolean | 状态码,是表示成功,否表示异常 |
|
||||||
|
| message | string | 异常信息 |
|
||||||
|
| code | int | 异常码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.user_id | int | 用户115账号 |
|
||||||
|
| data.user_name | string | 用户名称 |
|
||||||
|
| data.user_face_s | string | 小尺寸用户头像 |
|
||||||
|
| data.user_face_m | string | 中尺寸用户头像 |
|
||||||
|
| data.user_face_l | string | 大尺寸用户头像 |
|
||||||
|
| data.rt_space_info | object | 用户实时空间信息 |
|
||||||
|
| data.rt_space_info.all_total | object | 用户总空间 |
|
||||||
|
| data.rt_space_info.all_total.size | int | 用户总空间大小,单位为字节 |
|
||||||
|
| data.rt_space_info.all_total.size_format | string | 用户总空间大小,格式化文本 |
|
||||||
|
| data.rt_space_info.all_remain | object | 用户剩余空间 |
|
||||||
|
| data.rt_space_info.all_remain.size | int | 用户剩余空间大小,单位为字节 |
|
||||||
|
| data.rt_space_info.all_remain.size_format | string | 用户剩余空间大小,格式化文本 |
|
||||||
|
| data.rt_space_info.all_use | object | 用户已使用空间 |
|
||||||
|
| data.rt_space_info.all_use.size | int | 用户已使用空间大小,单位为字节 |
|
||||||
|
| data.rt_space_info.all_use.size_format | string | 用户已使用空间大小,格式化文本 |
|
||||||
|
| data.vip_info | object | 用户 VIP 等级信息 |
|
||||||
|
| data.vip_info.level_name | string | VIP 等级名称,见下方枚举表格 |
|
||||||
|
| data.vip_info.expire | int | VIP 过期时间戳,无 VIP 时为 0 |
|
||||||
|
| data.vip_info.tp_rights | object | 第三方畅用权益信息 |
|
||||||
|
| data.vip_info.tp_rights.is_tp_rights | int | 是否具有当前应用的第三方畅用权益:0-否 1-是 |
|
||||||
|
| data.vip_info.tp_rights.tp_rights_time | int | 第三方畅用权益过期时间戳,无有效权益时为 0 |
|
||||||
|
|
||||||
|
#### 响应的 data.vip_info.level_name 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:----------|:---|:---|
|
||||||
|
| 原石会员 | 原石用户 | 无有效 VIP 等级时的服务端固定返回值 |
|
||||||
|
| 尝鲜VIP | 尝鲜VIP | - |
|
||||||
|
| 体验VIP | 体验VIP | - |
|
||||||
|
| 月费VIP | 月费VIP | - |
|
||||||
|
| 年费VIP | 年费VIP | - |
|
||||||
|
| 长期VIP(高级版) | 长期VIP(高级版) | - |
|
||||||
|
| 长期VIP(特级版) | 长期VIP(特级版) | - |
|
||||||
|
| 长期VIP(超级版) | 长期VIP(超级版) | - |
|
||||||
|
| 长期VIP(至尊版) | 长期VIP(至尊版) | - |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"user_id": 0,
|
||||||
|
"user_name": "",
|
||||||
|
"user_face_s": "",
|
||||||
|
"user_face_m": "",
|
||||||
|
"user_face_l": "",
|
||||||
|
"rt_space_info": {
|
||||||
|
"all_total": {
|
||||||
|
"size": 0,
|
||||||
|
"size_format": ""
|
||||||
|
},
|
||||||
|
"all_remain": {
|
||||||
|
"size": 0,
|
||||||
|
"size_format": ""
|
||||||
|
},
|
||||||
|
"all_use": {
|
||||||
|
"size": 0,
|
||||||
|
"size_format": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vip_info": {
|
||||||
|
"expire": 0,
|
||||||
|
"level_name": "原石会员",
|
||||||
|
"tp_rights": {
|
||||||
|
"is_tp_rights": 0,
|
||||||
|
"tp_rights_time": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 决定当前用户和开放应用,接口不接收用户账号请求参数。
|
||||||
|
- `rt_space_info` 为实时空间数据,`all_use` 由总空间减去剩余空间计算得出。
|
||||||
|
- 请妥善保管 `access_token`,不要在日志或客户端可见信息中输出。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
|
| 2026年08月18日(周二) 14:40:25 | 更新四种长期VIP等级名称枚举 |
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
## 提交视频转码
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:--------------------------------|
|
||||||
|
| 接口名称 | 提交视频转码 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /video_push |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
按 VIP 等级或消耗枫币提交视频加速转码。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/video/video_push
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-------|:---|:----|:----------------------------------------|:-----------------|
|
||||||
|
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||||
|
| op | string | 是 | - | 加速转码方式,`vip_push`:按 VIP 等级加速;`pay_push`:消耗枫币加速 | vip_push |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/video/video_push' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
-F 'pick_code=b53gu6z3hvqji8wrm' \
|
||||||
|
-F 'op=vip_push'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:------|:---------|:----------------------|
|
||||||
|
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||||
|
| message | string | 返回信息,成功时为空字符串 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object[] | 响应数据,成功时为空数组 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 仅支持为当前授权用户所属的视频文件提交加速转码。
|
||||||
|
- 已完成转码的视频无需重复提交。
|
||||||
|
- 两种加速转码方式均要求当前用户具备 VIP 权益;`pay_push` 在枫币余额不足时提交失败。
|
||||||
|
- 提交成功后,系统通知转码服务重新计算排队信息,并在 3 小时内记录已加速状态。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||||
|
- 请勿对同一视频重复提交加速转码。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
## 获取视频在线播放地址
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:--------------------------------|
|
||||||
|
| 接口名称 | 获取视频在线播放地址 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /play |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
获取指定视频的在线播放地址、清晰度、音轨及文件基础信息。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/video/play
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-------|:---|:----|:----------|:-----------------|
|
||||||
|
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/video/play' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--data-urlencode 'pick_code=b53gu6z3hvqji8wrm'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:---------------------------------|:---------|:----------------------------------------|
|
||||||
|
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||||
|
| message | string | 返回信息,成功时为空字符串 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 视频播放及文件数据 |
|
||||||
|
| data.file_id | string | 文件ID |
|
||||||
|
| data.parent_id | string | 文件父目录ID |
|
||||||
|
| data.file_name | string | 文件名称 |
|
||||||
|
| data.file_size | string | 文件大小,单位为字节 |
|
||||||
|
| data.file_sha1 | string | 文件哈希值 |
|
||||||
|
| data.file_type | string | 文件类型 |
|
||||||
|
| data.is_private | string | 文件是否加密隐藏,0:否;1:是 |
|
||||||
|
| data.play_long | string | 视频时长 |
|
||||||
|
| data.user_def | int | 记忆的清晰度,1:标清;2:高清;3:超清;4:1080P;5:4K;100:原画 |
|
||||||
|
| data.user_rotate | int | 记忆的视频旋转角度,取值为 0、90、180、270 |
|
||||||
|
| data.user_turn | int | 视频翻转方向,0:不翻转;1:水平翻转;2:垂直翻转 |
|
||||||
|
| data.multitrack_list | object | 多音轨列表,键为音轨序号 |
|
||||||
|
| data.multitrack_list.*.title | string | 音轨标题 |
|
||||||
|
| data.multitrack_list.*.is_selected | string | 音轨是否为上次选中,1:是 |
|
||||||
|
| data.multitrack_list.*.sync_time | string | 音轨同步时间 |
|
||||||
|
| data.definition_list | object | 清晰度列表,键为清晰度值,值为清晰度名称 |
|
||||||
|
| data.definition_list_new | object | 新版清晰度列表,键为清晰度值,值为清晰度名称 |
|
||||||
|
| data.video_url | object[] | 各清晰度的播放地址信息 |
|
||||||
|
| data.video_url[].url | string | 播放地址 |
|
||||||
|
| data.video_url[].height | int | 视频高度 |
|
||||||
|
| data.video_url[].width | int | 视频宽度 |
|
||||||
|
| data.video_url[].definition | int | 视频清晰度 |
|
||||||
|
| data.video_url[].title | string | 视频清晰度名称 |
|
||||||
|
| data.video_url[].definition_n | int | 新版视频清晰度 |
|
||||||
|
| data.video_push_state | boolean | 视频尚未完成转码时是否已提交加速转码,仅对应失败响应返回 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"file_id": "",
|
||||||
|
"parent_id": "",
|
||||||
|
"file_name": "",
|
||||||
|
"file_size": "0",
|
||||||
|
"file_sha1": "",
|
||||||
|
"file_type": "",
|
||||||
|
"is_private": "0",
|
||||||
|
"play_long": "0",
|
||||||
|
"user_def": 0,
|
||||||
|
"user_rotate": 0,
|
||||||
|
"user_turn": 0,
|
||||||
|
"multitrack_list": {},
|
||||||
|
"definition_list": {},
|
||||||
|
"definition_list_new": {},
|
||||||
|
"video_url": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 切换音轨时,在返回的播放地址后增加整型参数 `audio_track`,参数值取 `multitrack_list` 对应的键;音轨下标从 `0` 开始。
|
||||||
|
- 年费VIP以下用户不支持播放 4K 视频;选择 4K 清晰度时会返回引导升级的视频地址。
|
||||||
|
- 接口不返回下载地址字段 `down_url`。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||||
|
- 播放地址具有时效性,请勿缓存或向无关方披露。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
## 获取视频播放进度
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:--------------------------------|
|
||||||
|
| 接口名称 | 获取视频播放进度 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /history |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
获取指定视频已记录的播放进度。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/video/history
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-------|:---|:----|:----------|:-----------------|
|
||||||
|
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/video/history' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--data-urlencode 'pick_code=b53gu6z3hvqji8wrm'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-------------|:--------|:-----------------------------|
|
||||||
|
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||||
|
| message | string | 返回信息,成功时为空字符串 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 播放进度数据;没有记录时为空数组 |
|
||||||
|
| data.add_time | int | 记录添加时间,Unix 时间戳 |
|
||||||
|
| data.file_id | string | 文件ID |
|
||||||
|
| data.file_name | string | 文件名称 |
|
||||||
|
| data.hash | string | 文件哈希值 |
|
||||||
|
| data.pick_code | string | 文件提取码 |
|
||||||
|
| data.time | int | 已播放时长,单位为秒 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"add_time": 0,
|
||||||
|
"file_id": "",
|
||||||
|
"file_name": "",
|
||||||
|
"hash": "",
|
||||||
|
"pick_code": "b53gu6z3hvqji8wrm",
|
||||||
|
"time": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 接口只返回当前 `pick_code` 对应的单条播放进度记录。
|
||||||
|
- 视频是否播放完毕可通过文件列表中的 `played_end` 字段查看,`1` 表示已播放完毕。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||||
|
- `pick_code` 对应的文件不存在或不在有效文件区域时,接口返回失败。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
## 视频字幕列表
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:--------------------------------|
|
||||||
|
| 接口名称 | 视频字幕列表 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /subtitle |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
获取指定视频的自动载入字幕和可用字幕列表。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/video/subtitle
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-------|:---|:----|:----------|:-----------------|
|
||||||
|
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://proapi.115.com/open/video/subtitle' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
--data-urlencode 'pick_code=b53gu6z3hvqji8wrm'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:----------------------------|:---------|:----------------------------|
|
||||||
|
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||||
|
| message | string | 返回信息,成功时为空字符串 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.autoload | object | 默认自动载入的字幕;无可用字幕时为空数组 |
|
||||||
|
| data.autoload.sid | string | 字幕标识 |
|
||||||
|
| data.autoload.language | string | 字幕语言 |
|
||||||
|
| data.autoload.title | string | 字幕标题 |
|
||||||
|
| data.autoload.url | string | 字幕文件地址 |
|
||||||
|
| data.autoload.type | string | 字幕文件类型 |
|
||||||
|
| data.autoload.key | string | 内置字幕键,仅内置字幕返回 |
|
||||||
|
| data.autoload.sha1 | string | 字幕文件哈希值 |
|
||||||
|
| data.autoload.file_id | string | 外挂或内嵌字幕文件ID |
|
||||||
|
| data.autoload.file_name | string | 外挂或内嵌字幕文件名 |
|
||||||
|
| data.autoload.pick_code | string | 外挂或内嵌字幕文件提取码 |
|
||||||
|
| data.autoload.caption_map_id | string | 内嵌字幕映射ID |
|
||||||
|
| data.autoload.is_caption_map | int | 是否为内嵌字幕,0:否;1:是 |
|
||||||
|
| data.autoload.sync_time | float | 字幕同步时间 |
|
||||||
|
| data.autoload.from | int | 记忆字幕来源标识 |
|
||||||
|
| data.autoload.user_sub | int | 是否为用户记忆字幕,1:是 |
|
||||||
|
| data.list | object[] | 字幕列表 |
|
||||||
|
| data.list[].sid | string | 字幕标识 |
|
||||||
|
| data.list[].language | string | 字幕语言 |
|
||||||
|
| data.list[].title | string | 字幕标题 |
|
||||||
|
| data.list[].url | string | 字幕文件地址 |
|
||||||
|
| data.list[].type | string | 字幕文件类型 |
|
||||||
|
| data.list[].key | string | 内置字幕键,仅内置字幕返回 |
|
||||||
|
| data.list[].sha1 | string | 字幕文件哈希值 |
|
||||||
|
| data.list[].file_id | string | 外挂或内嵌字幕文件ID |
|
||||||
|
| data.list[].file_name | string | 外挂或内嵌字幕文件名 |
|
||||||
|
| data.list[].pick_code | string | 外挂或内嵌字幕文件提取码 |
|
||||||
|
| data.list[].caption_map_id | string | 内嵌字幕映射ID |
|
||||||
|
| data.list[].is_caption_map | int | 是否为内嵌字幕,0:否;1:是 |
|
||||||
|
| data.list[].sync_time | float | 字幕同步时间 |
|
||||||
|
| data.list[].from | int | 记忆字幕来源标识 |
|
||||||
|
| data.list[].user_sub | int | 是否为用户记忆字幕,1:是 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"autoload": [],
|
||||||
|
"list": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- 用户记忆字幕不在列表中时会被前置;其余字幕按同目录同名外挂字幕、内嵌字幕、内置字幕、其他外挂字幕的顺序组合。
|
||||||
|
- 存在用户记忆字幕时优先将其作为自动载入字幕;否则依次选择内嵌字幕、同名外挂字幕或内置字幕。
|
||||||
|
- 字幕项来源不同,部分来源专属字段可能不返回。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||||
|
- 字幕文件地址具有时效性,请以本次接口返回值为准。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
## 记忆视频播放进度
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-------------|:--------------------------------|
|
||||||
|
| 接口名称 | 记忆视频播放进度 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /history |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
记录指定视频的播放进度及是否播放完毕。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://proapi.115.com/open/video/history
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer access_token
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----------|:-------|:---|:----|:-------------------|:-----------------|
|
||||||
|
| pick_code | string | 是 | - | 视频文件提取码 | b53gu6z3hvqji8wrm |
|
||||||
|
| time | int | 否 | 0 | 视频播放进度,单位为秒 | 10 |
|
||||||
|
| watch_end | int | 否 | 0 | 是否播放完毕,0:否;1:是 | 0 |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://proapi.115.com/open/video/history' \
|
||||||
|
-H 'Authorization: Bearer <access_token>' \
|
||||||
|
-F 'pick_code=b53gu6z3hvqji8wrm' \
|
||||||
|
-F 'time=10' \
|
||||||
|
-F 'watch_end=0'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:------|:---------|:----------------------|
|
||||||
|
| state | boolean | 操作结果状态,true:成功;false:失败 |
|
||||||
|
| message | string | 返回信息,成功时为空字符串 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object[] | 响应数据,成功时为空数组 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": true,
|
||||||
|
"message": "",
|
||||||
|
"code": 0,
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
- `time` 和 `watch_end` 均不传时,两个参数均按 `0` 处理,即播放进度为 0 秒且未播放完毕。
|
||||||
|
- 非本人文件不写入播放进度,但接口按操作成功返回。
|
||||||
|
- 同一视频未播放完毕的进度在 5 秒内重复上报时不重复持久化;标记为播放完毕时会立即持久化。
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 通过 `Authorization` 请求头传递,请将请求示例中的占位符替换为实际访问令牌。
|
||||||
|
- `pick_code` 必须属于当前授权用户可记录播放进度的视频文件。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 开发须知
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 文档名称 | 开发须知 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
为共同建设开放、共赢的合作生态并保障平台服务的可持续发展,开发者在接入平台服务前,须认真阅读并严格遵循《115生活开放平台开发者协议》的相关要求,确保合规运营、维护平台秩序。
|
||||||
|
|
||||||
|
基于平台与用户权益保护原则,平台会持续监测开发者的服务使用行为。如发现违反平台规范的行为,平台将视情节采取包括但不限于服务限流、接口冻结、资质回收等限制措施,并保留依法追责的权利。
|
||||||
|
|
||||||
|
开发者严禁实施包括但不限于以下行为:
|
||||||
|
|
||||||
|
1. **数据隐私违规行为**:侵害用户数据隐私安全,包括未经用户授权或未明确用途,违规收集、下载、存储、传播、加工用户存储数据等。开发者需要确保用户数据的获取与使用全程透明、可追溯。
|
||||||
|
2. **商业利益侵犯行为**:损害 115 科技的商业利益,包括多人共享开发者账号及会员权益、开展竞争关系业务、未经授权获取平台相关服务运营数据等。
|
||||||
|
3. **不当使用行为**:违规或未按要求使用 API 服务,包括违反国家相关政策法规、侵犯第三方合法权益、调用非公开接口、实际用途与申请信息不符等。
|
||||||
|
|
||||||
|
## 限流说明
|
||||||
|
|
||||||
|
为确保系统安全并保障服务稳定运行,115生活开放平台对所有 API 实施频率控制策略。出于安全防护需要,相关策略细则暂不公开,平台会持续动态优化该机制。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# 授权错误码
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 文档名称 | 授权错误码 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
|
||||||
|
| 错误码 | 描述 | 建议 |
|
||||||
|
|:-------|:------------------------------------|:-------------------------------------------------------------|
|
||||||
|
| 40100000 | 参数缺失 | - |
|
||||||
|
| 40101017 | 用户验证失败 | - |
|
||||||
|
| 40110000 | 请求异常,需要重试 | - |
|
||||||
|
| 40140100 | `client_id` 错误 | - |
|
||||||
|
| 40140101 | `code_challenge` 必填 | - |
|
||||||
|
| 40140102 | `code_challenge_method` 必须是 `sha256`、`sha1`、`md5` 之一 | - |
|
||||||
|
| 40140103 | `sign` 必填 | - |
|
||||||
|
| 40140104 | `sign` 签名失败 | - |
|
||||||
|
| 40140105 | 生成二维码失败 | - |
|
||||||
|
| 40140106 | AppID 无效 | - |
|
||||||
|
| 40140107 | 应用不存在 | - |
|
||||||
|
| 40140108 | 应用未审核通过 | - |
|
||||||
|
| 40140109 | 应用已被停用 | - |
|
||||||
|
| 40140110 | 应用已过期 | - |
|
||||||
|
| 40140111 | AppSecret 错误 | - |
|
||||||
|
| 40140112 | `code_verifier` 长度要求为 43 至 128 位 | - |
|
||||||
|
| 40140113 | `code_verifier` 验证失败 | - |
|
||||||
|
| 40140114 | `refresh_token` 格式错误(防篡改) | - |
|
||||||
|
| 40140115 | `refresh_token` 签名校验失败(防篡改) | - |
|
||||||
|
| 40140116 | `refresh_token` 无效(已解除授权) | 重新授权。终态错误,重试不会成功;继续重试将被标记为永久失效,见 `40140137` |
|
||||||
|
| 40140117 | `access_token` 刷新太频繁 | - |
|
||||||
|
| 40140118 | 开发者认证已过期 | - |
|
||||||
|
| 40140119 | `refresh_token` 已过期 | 重新授权。终态错误,重试不会成功;继续重试将被标记为永久失效,见 `40140137` |
|
||||||
|
| 40140120 | `refresh_token` 检验失败(防篡改) | 调用 `/open/refreshToken` 后会重新生成 `refresh_token`,检查本地是否已更新其值。终态错误,持续用旧值重试将被标记为永久失效,见 `40140137` |
|
||||||
|
| 40140121 | `access_token` 刷新失败 | 重试 |
|
||||||
|
| 40140122 | 超出授权应用数量上限 | - |
|
||||||
|
| 40140123 | `access_token` 格式错误(防篡改) | - |
|
||||||
|
| 40140124 | `access_token` 签名校验失败(防篡改) | - |
|
||||||
|
| 40140125 | `access_token` 无效(已过期、已解除授权或授权缓存不存在) | 调用 `/open/refreshToken` 获取新凭证;若 `refresh_token` 无效或已过期,重新授权 |
|
||||||
|
| 40140126 | `access_token` 与当前授权记录不匹配 | 重新读取刷新后保存的最新凭证,禁止使用原 `access_token` 重试;没有可用新凭证时调用 `/open/refreshToken` |
|
||||||
|
| 40140127 | `response_type` 错误 | - |
|
||||||
|
| 40140128 | `redirect_uri` 缺少协议 | - |
|
||||||
|
| 40140129 | `redirect_uri` 缺少域名 | - |
|
||||||
|
| 40140130 | 没有配置重定向域名 | 到应用管理中配置域名 |
|
||||||
|
| 40140131 | `redirect_uri` 域名不合法 | 需要与应用管理中配置的应用域名一致 |
|
||||||
|
| 40140132 | `grant_type` 错误 | - |
|
||||||
|
| 40140133 | `client_secret` 验证失败 | - |
|
||||||
|
| 40140134 | 授权码 `code` 验证失败 | - |
|
||||||
|
| 40140135 | `client_id` 验证失败 | - |
|
||||||
|
| 40140136 | `redirect_uri` 验证失败(防 MITM 攻击) | - |
|
||||||
|
| 40140137 | `refresh_token` 已失效,请停止重试并重新授权(终态) | 同一 `refresh_token` 连续多次以 `40140116`/`40140119`/`40140120` 失败后被服务端标记为永久失效。收到后必须停止用该令牌重试,引导用户重新授权;重新授权产生的新令牌不受影响 |
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
|
| 2026年08月10日(周一) 10:23:15 | 修正 `40140125`、`40140126` 的原因及处理建议 |
|
||||||
|
| 2026年08月24日(周一) 10:16:52 | 新增终态错误码 `40140137`,补充 `40140116`/`40140119`/`40140120` 的永久失效判定说明 |
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
## 刷新access_token
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 刷新access_token |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /refreshToken |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
该接口用于通过 `refresh_token` 获取新的 `access_token` 和 `refresh_token`。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://passportapi.115.com/open/refreshToken
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
OAuth 2.0 刷新凭证
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------------|:-------|:---|:----|:---------------------|:----------------------------|
|
||||||
|
| refresh_token | string | 是 | - | 用于刷新访问凭证的刷新凭证 | `REFRESH_TOKEN_PLACEHOLDER` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://passportapi.115.com/open/refreshToken' \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode 'refresh_token=REFRESH_TOKEN_PLACEHOLDER'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-------------------|:-------|:------------------------------------|
|
||||||
|
| state | int | 状态码 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| message | string | 响应信息 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.access_token | string | 新的 `access_token`,同时刷新有效期 |
|
||||||
|
| data.refresh_token | string | 新的 `refresh_token`,其有效期不延长、不改变 |
|
||||||
|
| data.expires_in | int | `access_token` 有效期,单位为秒 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": 1,
|
||||||
|
"code": 0,
|
||||||
|
"message": "",
|
||||||
|
"data": {
|
||||||
|
"access_token": "ACCESS_TOKEN_PLACEHOLDER",
|
||||||
|
"refresh_token": "REFRESH_TOKEN_PLACEHOLDER",
|
||||||
|
"expires_in": 7200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `access_token` 有效期为 7200 秒,调用方应以响应中的 `expires_in` 计算刷新时间。
|
||||||
|
- 同一授权在 60 秒内重复刷新会触发频率控制;多进程或多节点调用方应确保同一授权同一时间只有一个刷新请求。
|
||||||
|
- 调用后会同时生成新的 `access_token` 和 `refresh_token`。调用方必须将两者作为一组原子保存,并停止使用刷新前的旧凭证;刷新凭证本身的有效期不延长、不改变。
|
||||||
|
- 收到 `40140125` 或 `40140126` 时,不要使用原 `access_token` 重复重试。应先读取已保存的最新凭证;没有可用新凭证时,再调用本接口刷新。
|
||||||
|
- `40140116`、`40140119`、`40140120` 是终态错误,用同一个 `refresh_token` 重试永远不会成功。同一 `refresh_token` 连续多次以这三种原因失败会被服务端标记为永久失效,之后每次刷新都直接返回 `40140137`。客户端收到这三种错误或 `40140137` 时必须停止重试,引导用户重新授权;后台常驻程序(如 NAS 同步任务)尤其要实现该逻辑,避免长期无效轮询。
|
||||||
|
- `access_token` 和 `refresh_token` 属于敏感凭证,不得写入公开仓库、客户端日志或公开沟通内容。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
|
| 2026年08月10日(周一) 10:23:15 | 修正 `access_token` 有效期示例,补充凭证轮换及并发刷新说明 |
|
||||||
|
| 2026年08月24日(周一) 10:16:52 | 新增 `refresh_token` 永久失效判定规则与终态错误码 `40140137` 说明 |
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
## 获取access_token
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 获取access_token |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /deviceCodeToToken |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
该接口用于在用户确认手机扫码授权后,使用设备码和 PKCE 原始校验值换取 `access_token`。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://passportapi.115.com/open/deviceCodeToToken
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
OAuth 2.0 + PKCE
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-----------|:-------|:---|:----|:--------------------------------|:---------------------------------------------------------------|
|
||||||
|
| uid | string | 是 | - | 二维码 ID/设备码 | `DEVICE_CODE_PLACEHOLDER` |
|
||||||
|
| code_verifier | string | 是 | - | 计算 `code_challenge` 时使用的原始随机字符串 | `IGKN6CJanWxCDPDhHZJrhswQdlcPBGLqExkhyujysXaQ4fJKBk_6dlPJo47s` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://passportapi.115.com/open/deviceCodeToToken' \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode 'uid=DEVICE_CODE_PLACEHOLDER' \
|
||||||
|
--data-urlencode 'code_verifier=IGKN6CJanWxCDPDhHZJrhswQdlcPBGLqExkhyujysXaQ4fJKBk_6dlPJo47s'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-------------------|:-------|:--------------------------------|
|
||||||
|
| state | int | 状态码 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| message | string | 响应信息 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.access_token | string | 访问资源接口的凭证 |
|
||||||
|
| data.refresh_token | string | 刷新 `access_token` 的凭证,有效期 1 年 |
|
||||||
|
| data.expires_in | int | `access_token` 有效期,单位为秒 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": 1,
|
||||||
|
"code": 0,
|
||||||
|
"message": "",
|
||||||
|
"data": {
|
||||||
|
"access_token": "ACCESS_TOKEN_PLACEHOLDER",
|
||||||
|
"refresh_token": "REFRESH_TOKEN_PLACEHOLDER",
|
||||||
|
"expires_in": 7200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `code_verifier` 必须与生成 `code_challenge` 时使用的原始值一致。
|
||||||
|
- `access_token` 和 `refresh_token` 属于敏感凭证,不得写入公开仓库、客户端日志或公开沟通内容。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
## 获取设备码和二维码内容
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 获取设备码和二维码内容 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /authDeviceCode |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
该接口用于 OAuth 2.0 + PKCE 手机扫码授权流程的第一步,获取设备码和二维码内容。此模式适用于无后端服务的第三方客户端,无需提供 AppSecret。
|
||||||
|
|
||||||
|
第三方客户端需要根据响应中的 `data.qrcode` 生成二维码,供 115 客户端扫码授权。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://passportapi.115.com/open/authDeviceCode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
OAuth 2.0 + PKCE
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:---------------------|:-------|:---|:----|:--------------------------------|:-----------------------------------------------------------|
|
||||||
|
| client_id | string | 是 | - | AppID | `YOUR_APP_ID` |
|
||||||
|
| code_challenge | string | 是 | - | PKCE 挑战码 | `THHodGWg-FZfv8XYz7QArNGIK_aVomSHPldlSOTUtkw` |
|
||||||
|
| code_challenge_method | string | 是 | - | `code_challenge` 的哈希算法,见下方枚举表格 | `sha256` |
|
||||||
|
|
||||||
|
#### 请求参数中的 code_challenge_method 字段枚举
|
||||||
|
|
||||||
|
| 值 | 说明 | 备注 |
|
||||||
|
|:-------|:---------------------------|:---|
|
||||||
|
| md5 | 使用 MD5 计算挑战码 | - |
|
||||||
|
| sha1 | 使用 SHA-1 计算挑战码 | - |
|
||||||
|
| sha256 | 使用 SHA-256 计算挑战码,推荐使用 | - |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://passportapi.115.com/open/authDeviceCode' \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode 'client_id=YOUR_APP_ID' \
|
||||||
|
--data-urlencode 'code_challenge=THHodGWg-FZfv8XYz7QArNGIK_aVomSHPldlSOTUtkw' \
|
||||||
|
--data-urlencode 'code_challenge_method=sha256'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-----------|:-----|:------------------------------------------|
|
||||||
|
| state | int | 状态码 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| message | string | 响应信息 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.uid | string | 设备码,轮询二维码状态时使用 |
|
||||||
|
| data.time | int | 校验时间戳,轮询二维码状态时使用 |
|
||||||
|
| data.qrcode | string | 二维码内容,第三方客户端需要据此生成设备二维码 |
|
||||||
|
| data.sign | string | 校验签名,轮询二维码状态时使用 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": 1,
|
||||||
|
"code": 0,
|
||||||
|
"message": "",
|
||||||
|
"data": {
|
||||||
|
"uid": "DEVICE_CODE_PLACEHOLDER",
|
||||||
|
"time": 0,
|
||||||
|
"qrcode": "QRCODE_CONTENT_PLACEHOLDER",
|
||||||
|
"sign": "SIGN_PLACEHOLDER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `code_verifier` 为长度 43 至 128 位的随机字符串。
|
||||||
|
- `code_challenge` 的计算方式为 `url_safe(base64_encode(hash(code_verifier)))`。哈希结果按二进制数据参与 Base64 编码,算法需要与 `code_challenge_method` 一致。
|
||||||
|
- AppID 等应用凭证请使用实际应用配置,不要在公开仓库或日志中记录敏感凭证。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
## 轮询二维码状态
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 轮询二维码状态 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /get/status/ |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
该接口用于长轮询设备二维码的扫码和授权状态。当二维码状态没有变化时,接口不会立即响应,直到请求超时或状态发生变化。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://qrcodeapi.115.com/get/status/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
设备码参数校验
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:----|:-------|:---|:----|:------------------------------------|:------------------------|
|
||||||
|
| uid | string | 是 | - | 二维码 ID/设备码,从 `/open/authDeviceCode` 的 `data.uid` 获取 | `DEVICE_CODE_PLACEHOLDER` |
|
||||||
|
| time | int | 是 | - | 校验时间戳,从 `/open/authDeviceCode` 的 `data.time` 获取 | `0` |
|
||||||
|
| sign | string | 是 | - | 校验签名,从 `/open/authDeviceCode` 的 `data.sign` 获取 | `SIGN_PLACEHOLDER` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://qrcodeapi.115.com/get/status/' \
|
||||||
|
--data-urlencode 'uid=DEVICE_CODE_PLACEHOLDER' \
|
||||||
|
--data-urlencode 'time=0' \
|
||||||
|
--data-urlencode 'sign=SIGN_PLACEHOLDER'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:------------|:-------|:-------------------------------------------|
|
||||||
|
| state | int | 轮询状态:0-二维码无效,结束轮询;1-继续轮询 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| message | string | 响应信息 |
|
||||||
|
| data | object | 响应数据;115 客户端扫码或输入设备码后才有值 |
|
||||||
|
| data.msg | string | 操作提示 |
|
||||||
|
| data.status | int | 二维码状态:1-扫码成功,等待确认;2-确认登录或授权,结束轮询 |
|
||||||
|
| data.version | string | 版本信息 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": 1,
|
||||||
|
"code": 0,
|
||||||
|
"message": "",
|
||||||
|
"data": {
|
||||||
|
"msg": "OPERATION_MESSAGE_PLACEHOLDER",
|
||||||
|
"status": 1,
|
||||||
|
"version": "VERSION_PLACEHOLDER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `state=0` 表示二维码无效,应结束轮询;`state=1` 表示继续轮询。
|
||||||
|
- `data.status=1` 表示扫码成功并等待用户确认;`data.status=2` 表示用户已确认登录或授权,应结束轮询并进入换取访问凭证的步骤。
|
||||||
|
- 长轮询超时不等同于授权失败,客户端可以按照自身网络策略重新发起请求。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
## 用授权码换取access_token
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 用授权码换取access_token |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /authCodeToToken |
|
||||||
|
| 请求方法 | POST |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
该接口用于通过授权码换取 `access_token`。建议在开发者服务端调用,避免泄露 AppSecret。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://passportapi.115.com/open/authCodeToToken
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
POST
|
||||||
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
OAuth 2.0 授权码模式
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-------------|:-------|:---|:----|:------------------------------------|:------------------------------|
|
||||||
|
| client_id | string | 是 | - | AppID | `YOUR_APP_ID` |
|
||||||
|
| client_secret | string | 是 | - | AppSecret | `YOUR_APP_SECRET` |
|
||||||
|
| code | string | 是 | - | 请求授权接口重定向返回的授权码 | `AUTHORIZATION_CODE_PLACEHOLDER` |
|
||||||
|
| redirect_uri | string | 是 | - | 与请求授权时传入的 `redirect_uri` 一致,用于防止 MITM 和 CSRF 攻击 | `https://foo.com?state=123456` |
|
||||||
|
| grant_type | string | 是 | - | 授权类型,固定为 `authorization_code` | `authorization_code` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl 'https://passportapi.115.com/open/authCodeToToken' \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode 'client_id=YOUR_APP_ID' \
|
||||||
|
--data-urlencode 'client_secret=YOUR_APP_SECRET' \
|
||||||
|
--data-urlencode 'code=AUTHORIZATION_CODE_PLACEHOLDER' \
|
||||||
|
--data-urlencode 'redirect_uri=https://foo.com?state=123456' \
|
||||||
|
--data-urlencode 'grant_type=authorization_code'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:-------------------|:-------|:--------------------------------|
|
||||||
|
| state | int | 状态码:0-失败;1-成功 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| message | string | 响应信息 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| data.access_token | string | 访问资源接口的凭证 |
|
||||||
|
| data.refresh_token | string | 刷新 `access_token` 的凭证,有效期 1 年 |
|
||||||
|
| data.expires_in | int | `access_token` 有效期,单位为秒 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": 1,
|
||||||
|
"code": 0,
|
||||||
|
"message": "",
|
||||||
|
"data": {
|
||||||
|
"access_token": "ACCESS_TOKEN_PLACEHOLDER",
|
||||||
|
"refresh_token": "REFRESH_TOKEN_PLACEHOLDER",
|
||||||
|
"expires_in": 7200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 必须在服务端安全保存并使用 AppSecret,不得在客户端代码、公开仓库或日志中泄露。
|
||||||
|
- `redirect_uri` 必须与请求授权时传入的值一致。
|
||||||
|
- `access_token` 和 `refresh_token` 属于敏感凭证,应按照敏感数据规范存储。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
## 请求授权
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 接口名称 | 请求授权 |
|
||||||
|
| 接口版本 | v1.0 |
|
||||||
|
| 接口路径 | /authorize |
|
||||||
|
| 请求方法 | GET |
|
||||||
|
| 接口状态 | 生产环境 |
|
||||||
|
|
||||||
|
### 接口说明
|
||||||
|
|
||||||
|
该接口用于发起 OAuth 2.0 授权码模式授权,建议由开发者服务端参与授权流程。
|
||||||
|
|
||||||
|
用户未登录时,接口会重定向到登录页面;用户已登录时,接口会自动完成授权并重定向到 `redirect_uri` 指定的地址。
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
|
||||||
|
```
|
||||||
|
https://passportapi.115.com/open/authorize
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求方式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET
|
||||||
|
```
|
||||||
|
|
||||||
|
### 认证方式
|
||||||
|
|
||||||
|
```
|
||||||
|
OAuth 2.0 授权码模式
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 默认值 | 说明 | 约束/示例 |
|
||||||
|
|:-----------|:-------|:---|:----|:-----------------------------------------------|:---------------------|
|
||||||
|
| client_id | string | 是 | - | AppID | `YOUR_APP_ID` |
|
||||||
|
| redirect_uri | string | 是 | - | 授权完成后的重定向地址;接口会附加授权码 `code`,并原样附加请求中的 `state` | `https://foo.com/bar` |
|
||||||
|
| response_type | string | 是 | - | 授权模式,固定为 `code` | `code` |
|
||||||
|
| state | string | 否 | "" | 防止 CSRF 攻击的随机值,重定向时原样返回 | `123456` |
|
||||||
|
|
||||||
|
### 请求示例
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -G 'https://passportapi.115.com/open/authorize' \
|
||||||
|
--data-urlencode 'client_id=YOUR_APP_ID' \
|
||||||
|
--data-urlencode 'redirect_uri=https://foo.com/bar' \
|
||||||
|
--data-urlencode 'response_type=code' \
|
||||||
|
--data-urlencode 'state=123456'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应字段说明
|
||||||
|
|
||||||
|
接口调用成功时会重定向到 `redirect_uri`,并通过查询参数返回授权码 `code` 和请求中携带的 `state`。接口调用失败时返回以下字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
|:--------|:-------|:------------------|
|
||||||
|
| state | int | 状态码:0-失败;1-成功 |
|
||||||
|
| code | int | 错误码 |
|
||||||
|
| data | object | 响应数据 |
|
||||||
|
| message | string | 响应信息 |
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"state": 0,
|
||||||
|
"code": 40140127,
|
||||||
|
"data": {},
|
||||||
|
"message": "response_type 错误"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- `redirect_uri` 需要先在[115生活开放平台](https://open.115.com/)的应用管理中配置域名,并在请求时进行 URL 编码。
|
||||||
|
- 强烈建议传入随机 `state`,并在换取 `access_token` 前验证重定向返回的 `state` 与请求值一致,以防止 CSRF 攻击。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 接入流程
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 文档名称 | 接入流程 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
|
||||||
|
## 1. 注册“115生活”账号
|
||||||
|
|
||||||
|
在使用 115 开放平台服务之前,开发者需要先通过[“115生活”官网](https://115.com/)注册“115生活”账号并登录,完成实名认证。
|
||||||
|
|
||||||
|
## 2. 提交入驻申请
|
||||||
|
|
||||||
|
访问[115生活开放平台](https://open.115.com/),按照页面流程填写相关信息。
|
||||||
|
|
||||||
|
### 2.1 选择开发者身份类型
|
||||||
|
|
||||||
|
开发者可以申请成为“个人开发者”或“企业开发者”。点击页面上方的“切换申请类型”,可以切换身份类型。
|
||||||
|
|
||||||
|
### 2.2 签署协议
|
||||||
|
|
||||||
|
认真阅读并确认相关协议内容;如无异议,勾选“同意”并点击“下一步”。
|
||||||
|
|
||||||
|
### 2.3 填写入驻资料
|
||||||
|
|
||||||
|
按照页面指引填写个人信息、API 对接需求、应用场景等入驻资料,然后点击“下一步”。
|
||||||
|
|
||||||
|
### 2.4 填写认证信息
|
||||||
|
|
||||||
|
根据所选择的开发者身份类型,按照页面指引填写并上传身份认证资料:
|
||||||
|
|
||||||
|
- **个人开发者**:姓名、身份证信息(证件号码、有效期)、证件照片(身份证件正反面照片、本人手持身份证照片)。
|
||||||
|
- **企业开发者**:企业负责人或法人的姓名、联系方式、身份证信息(证件号码、有效期)、证件照片(身份证件正反面照片、加盖公章的营业执照影印件)、统一社会信用代码。
|
||||||
|
|
||||||
|
### 2.5 提交入驻申请
|
||||||
|
|
||||||
|
确认上述信息填写无误后,点击“提交验证”提交入驻申请。平台将在 7 个工作日内完成审核。
|
||||||
|
|
||||||
|
## 3. 创建应用
|
||||||
|
|
||||||
|
入驻申请通过后,进入 115 生活开放平台管理页面,选择“应用管理”,点击“创建应用”,按照页面指引填写应用信息、应用域名、应用描述、接口信息等内容。确认信息填写无误后提交应用申请,平台将在 7 个工作日内完成审核。
|
||||||
|
|
||||||
|
应用审核通过后,开发者可以获取该应用的 AppID、AppKey 和 AppSecret 等接入凭证。请妥善保存这些凭证,不要在客户端代码、公开仓库或沟通内容中泄露。
|
||||||
|
|
||||||
|
## 4. 接口调试
|
||||||
|
|
||||||
|
按照开放平台接口文档完成接入和调用后,即可正式使用开放平台能力。
|
||||||
|
|
||||||
|
开放平台 API 基础域名:`https://proapi.115.com/`
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 更新记录
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 文档名称 | 更新记录 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
|
||||||
|
## 更新内容
|
||||||
|
|
||||||
|
| 更新模块 | 更新内容 | 更新时间 |
|
||||||
|
|:-----------|:-------------------------|:--------------|
|
||||||
|
| 增值服务产品 | 新增增值服务产品,获得推广收益 | 2025年4月17日周四 |
|
||||||
|
| 视频播放、云下载 | 新增视频播放、云下载接口 | 2025年4月3日周四 |
|
||||||
|
| 接入授权 | 支持 H5 账号密码/短信授权 | 2025年4月2日周三 |
|
||||||
|
| 基本框架 | 新增开放平台文档接口 | 2025年1月22日周三 |
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 概述
|
||||||
|
|
||||||
|
### 基本信息
|
||||||
|
|
||||||
|
| 属性 | 内容 |
|
||||||
|
|:-----------|:---------------------------------|
|
||||||
|
| 文档名称 | 概述 |
|
||||||
|
| 文档版本 | v1.0 |
|
||||||
|
|
||||||
|
## 115生活简介
|
||||||
|
|
||||||
|
“115生活”是一款面向个人用户的数字生活平台,提供海量数据的安全存储、多端同步与快速访问。用户不仅可以便捷地管理和使用各类数字资源,还能使用多维社交、生活服务等多元化功能。
|
||||||
|
|
||||||
|
## 115生活开放平台能力说明
|
||||||
|
|
||||||
|
115生活开放平台提供“115生活”数据存储、同步、管理等功能的 API 服务。开发者通过对接 API,可以将“115生活”的存储能力集成到自己的应用中。
|
||||||
|
|
||||||
|
目前已开放以下能力:
|
||||||
|
|
||||||
|
- **用户管理能力**:用户授权与信息查询等。
|
||||||
|
- **文件管理能力**:获取文件列表,查看文件属性,以及文件上传、下载、搜索、移动、删除等。
|
||||||
|
- **视频管理能力**:视频文件的在线转码与播放等。
|
||||||
|
- **云下载服务**:获取云下载任务列表、配额信息,以及添加、删除下载任务等。
|
||||||
|
- **商业价值转化**:开发者参与“推广产品得收益”计划,可基于用户实际购买的产品获取相应推广收益。
|
||||||
|
|
||||||
|
### 修改历史
|
||||||
|
|
||||||
|
| 修改时间 | 修改说明 |
|
||||||
|
|:-----------------------------|:-----|
|
||||||
|
| 2025年04月01日(周二) 00:00:00 | 创建文档 |
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 115 开放平台文档(离线版)
|
||||||
|
|
||||||
|
> 来源:https://open.115.com/doc/ (115生活开放平台开发者文档)
|
||||||
|
> 抓取时间:2026-09-05,共 42 篇 Markdown 文档,目录结构与官网一致。
|
||||||
|
> 目录索引:[tree-index.json](tree-index.json)(官网原始目录树,程序化遍历可用)。
|
||||||
|
|
||||||
|
## 文档目录
|
||||||
|
|
||||||
|
- [概述](115开放平台/简介/概述.md)
|
||||||
|
- [更新记录](115开放平台/简介/更新记录.md)
|
||||||
|
- [接入流程](115开放平台/接入指南/接入流程.md)
|
||||||
|
- [开发须知](115开放平台/接入指南/开发须知.md)
|
||||||
|
- [授权错误码](115开放平台/接入指南/授权错误码.md)
|
||||||
|
- **接入授权/**
|
||||||
|
- **手机扫码授权PKCE模式/**
|
||||||
|
- [获取设备码和二维码内容](115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取设备码和二维码内容.md)
|
||||||
|
- [轮询二维码状态](115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/轮询二维码状态.md)
|
||||||
|
- [获取access_token](115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取access_token.md)
|
||||||
|
- **授权码模式/**
|
||||||
|
- [请求授权](115开放平台/接入指南/接入授权/授权码模式/请求授权.md)
|
||||||
|
- [用授权码换取access_token](115开放平台/接入指南/接入授权/授权码模式/用授权码换取access_token.md)
|
||||||
|
- [刷新access_token](115开放平台/接入指南/接入授权/刷新access_token.md)
|
||||||
|
- [开发者商业价值转化:推广产品得收益](115开放平台/API列表/开发者商业价值转化:推广产品得收益.md)
|
||||||
|
- **用户管理/**
|
||||||
|
- [用户信息](115开放平台/API列表/用户管理/用户信息.md)
|
||||||
|
- **文件管理/**
|
||||||
|
- **文件上传/**
|
||||||
|
- [上传流程](115开放平台/API列表/文件管理/文件上传/上传流程.md)
|
||||||
|
- [获取上传凭证](115开放平台/API列表/文件管理/文件上传/获取上传凭证.md)
|
||||||
|
- [文件上传](115开放平台/API列表/文件管理/文件上传/文件上传.md)
|
||||||
|
- [断点续传](115开放平台/API列表/文件管理/文件上传/断点续传.md)
|
||||||
|
- [新建文件夹](115开放平台/API列表/文件管理/新建文件夹.md)
|
||||||
|
- [获取文件列表](115开放平台/API列表/文件管理/获取文件列表.md)
|
||||||
|
- **获取文件(夹)详情/**
|
||||||
|
- [按ID获取](115开放平台/API列表/文件管理/获取文件(夹)详情/按ID获取.md)
|
||||||
|
- [按路径获取](115开放平台/API列表/文件管理/获取文件(夹)详情/按路径获取.md)
|
||||||
|
- [文件搜索](115开放平台/API列表/文件管理/文件搜索.md)
|
||||||
|
- [文件复制](115开放平台/API列表/文件管理/文件复制.md)
|
||||||
|
- [文件移动](115开放平台/API列表/文件管理/文件移动.md)
|
||||||
|
- [获取文件下载地址](115开放平台/API列表/文件管理/获取文件下载地址.md)
|
||||||
|
- [文件(夹)更新](115开放平台/API列表/文件管理/文件(夹)更新.md)
|
||||||
|
- [删除文件](115开放平台/API列表/文件管理/删除文件.md)
|
||||||
|
- [回收站列表](115开放平台/API列表/文件管理/回收站列表.md)
|
||||||
|
- [回收站还原](115开放平台/API列表/文件管理/回收站还原.md)
|
||||||
|
- [删除或清空回收站](115开放平台/API列表/文件管理/删除或清空回收站.md)
|
||||||
|
- **视频播放/**
|
||||||
|
- [记忆视频播放进度](115开放平台/API列表/视频播放/记忆视频播放进度.md)
|
||||||
|
- [视频字幕列表](115开放平台/API列表/视频播放/视频字幕列表.md)
|
||||||
|
- [获取视频播放进度](115开放平台/API列表/视频播放/获取视频播放进度.md)
|
||||||
|
- [获取视频在线播放地址](115开放平台/API列表/视频播放/获取视频在线播放地址.md)
|
||||||
|
- [提交视频转码](115开放平台/API列表/视频播放/提交视频转码.md)
|
||||||
|
- **云下载/**
|
||||||
|
- [解析BT种子](115开放平台/API列表/云下载/解析BT种子.md)
|
||||||
|
- [获取用户云下载任务列表](115开放平台/API列表/云下载/获取用户云下载任务列表.md)
|
||||||
|
- [获取云下载配额信息](115开放平台/API列表/云下载/获取云下载配额信息.md)
|
||||||
|
- [清空云下载任务](115开放平台/API列表/云下载/清空云下载任务.md)
|
||||||
|
- [添加云下载链接任务](115开放平台/API列表/云下载/添加云下载链接任务.md)
|
||||||
|
- [删除用户云下载任务](115开放平台/API列表/云下载/删除用户云下载任务.md)
|
||||||
|
- [添加云下载BT任务](115开放平台/API列表/云下载/添加云下载BT任务.md)
|
||||||
|
|
||||||
|
## API 接口速查
|
||||||
|
|
||||||
|
主要 API 域名与认证方式(详见各文档):
|
||||||
|
|
||||||
|
- 开放 API 基础地址:`https://proapi.115.com/open/...`
|
||||||
|
- 认证方式:`Authorization: Bearer access_token`
|
||||||
|
- OAuth 授权相关文档见 `115开放平台/接入指南/接入授权/`
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
{
|
||||||
|
"name": "doc",
|
||||||
|
"path": "/doc/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "115开放平台",
|
||||||
|
"path": "/doc/115开放平台/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "简介",
|
||||||
|
"path": "/doc/115开放平台/简介/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "概述.md",
|
||||||
|
"path": "/doc/115开放平台/简介/概述.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "更新记录.md",
|
||||||
|
"path": "/doc/115开放平台/简介/更新记录.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "接入指南",
|
||||||
|
"path": "/doc/115开放平台/接入指南/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "接入流程.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入流程.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "开发须知.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/开发须知.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "授权错误码.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/授权错误码.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "接入授权",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "手机扫码授权PKCE模式",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "获取设备码和二维码内容.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取设备码和二维码内容.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "轮询二维码状态.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/轮询二维码状态.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取access_token.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/手机扫码授权PKCE模式/获取access_token.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "授权码模式",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/授权码模式/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "请求授权.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/授权码模式/请求授权.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "用授权码换取access_token.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/授权码模式/用授权码换取access_token.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "刷新access_token.md",
|
||||||
|
"path": "/doc/115开放平台/接入指南/接入授权/刷新access_token.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "API列表",
|
||||||
|
"path": "/doc/115开放平台/API列表/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "开发者商业价值转化:推广产品得收益.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/开发者商业价值转化:推广产品得收益.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "用户管理",
|
||||||
|
"path": "/doc/115开放平台/API列表/用户管理/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "用户信息.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/用户管理/用户信息.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文件管理",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "文件上传",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件上传/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "上传流程.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件上传/上传流程.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取上传凭证.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件上传/获取上传凭证.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文件上传.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件上传/文件上传.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "断点续传.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件上传/断点续传.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "新建文件夹.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/新建文件夹.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取文件列表.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/获取文件列表.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取文件(夹)详情",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/获取文件(夹)详情/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "按ID获取.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/获取文件(夹)详情/按ID获取.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "按路径获取.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/获取文件(夹)详情/按路径获取.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文件搜索.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件搜索.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文件复制.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件复制.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文件移动.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件移动.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取文件下载地址.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/获取文件下载地址.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "文件(夹)更新.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/文件(夹)更新.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "删除文件.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/删除文件.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "回收站列表.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/回收站列表.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "回收站还原.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/回收站还原.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "删除或清空回收站.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/文件管理/删除或清空回收站.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "视频播放",
|
||||||
|
"path": "/doc/115开放平台/API列表/视频播放/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "记忆视频播放进度.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/视频播放/记忆视频播放进度.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "视频字幕列表.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/视频播放/视频字幕列表.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取视频播放进度.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/视频播放/获取视频播放进度.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取视频在线播放地址.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/视频播放/获取视频在线播放地址.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "提交视频转码.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/视频播放/提交视频转码.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "云下载",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "解析BT种子.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/解析BT种子.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取用户云下载任务列表.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/获取用户云下载任务列表.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "获取云下载配额信息.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/获取云下载配额信息.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "清空云下载任务.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/清空云下载任务.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "添加云下载链接任务.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/添加云下载链接任务.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "删除用户云下载任务.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/删除用户云下载任务.md",
|
||||||
|
"type": "file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "添加云下载BT任务.md",
|
||||||
|
"path": "/doc/115开放平台/API列表/云下载/添加云下载BT任务.md",
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "directory"
|
||||||
|
}
|
||||||
@@ -124,6 +124,7 @@ type StrmUploadTask struct {
|
|||||||
FileName string `gorm:"size:512" json:"file_name"`
|
FileName string `gorm:"size:512" json:"file_name"`
|
||||||
LocalPath string `gorm:"size:1024" json:"local_path"` // 本地源文件
|
LocalPath string `gorm:"size:1024" json:"local_path"` // 本地源文件
|
||||||
RemotePath string `gorm:"size:1024" json:"remote_path"` // 远端目标路径
|
RemotePath string `gorm:"size:1024" json:"remote_path"` // 远端目标路径
|
||||||
|
RemoteRef string `gorm:"size:1024" json:"remote_ref"` // 远端同名旧文件引用(115 文件 ID;上传覆盖前先删除旧文件,WebDAV/OpenList 直接覆盖无需删除)
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Status string `gorm:"size:16;index" json:"status"`
|
Status string `gorm:"size:16;index" json:"status"`
|
||||||
Error string `gorm:"size:1024" json:"error"`
|
Error string `gorm:"size:1024" json:"error"`
|
||||||
|
|||||||
@@ -901,6 +901,53 @@ func (r *StrmDirCacheRepository) Set(ctx context.Context, syncPathID, dirID, pat
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetBatch 批量 upsert 目录缓存(dirID → 相对路径)。单个事务内先查出已存在
|
||||||
|
// 行再分流更新/插入,替代同步流程逐目录单条 Set,避免首次全量同步上万目录时
|
||||||
|
// 的 SQLite 写锁竞争。同一 dirID 的重复项以 map 语义取最后一次写入。
|
||||||
|
func (r *StrmDirCacheRepository) SetBatch(ctx context.Context, syncPathID string, paths map[string]string) error {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
ids := make([]string, 0, len(paths))
|
||||||
|
for dirID := range paths {
|
||||||
|
ids = append(ids, dirID)
|
||||||
|
}
|
||||||
|
var existing []model.StrmDirCache
|
||||||
|
if err := tx.Where("sync_path_id = ? AND dir_id IN ?", syncPathID, ids).Find(&existing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existingRowID := make(map[string]string, len(existing))
|
||||||
|
for _, row := range existing {
|
||||||
|
existingRowID[row.DirID] = row.ID
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
var creates []model.StrmDirCache
|
||||||
|
for dirID, path := range paths {
|
||||||
|
if rowID, ok := existingRowID[dirID]; ok {
|
||||||
|
if err := tx.Model(&model.StrmDirCache{}).Where("id = ?", rowID).Updates(map[string]any{
|
||||||
|
"path": path,
|
||||||
|
"updated_at": now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
creates = append(creates, model.StrmDirCache{
|
||||||
|
SyncPathID: syncPathID,
|
||||||
|
DirID: dirID,
|
||||||
|
Path: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(creates) > 0 {
|
||||||
|
return tx.CreateInBatches(creates, 100).Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (r *StrmDirCacheRepository) DeleteBySyncPathID(ctx context.Context, syncPathID string) error {
|
func (r *StrmDirCacheRepository) DeleteBySyncPathID(ctx context.Context, syncPathID string) error {
|
||||||
return withSQLiteBusyRetry(ctx, func() error {
|
return withSQLiteBusyRetry(ctx, func() error {
|
||||||
return r.db.WithContext(ctx).Unscoped().Where("sync_path_id = ?", syncPathID).Delete(&model.StrmDirCache{}).Error
|
return r.db.WithContext(ctx).Unscoped().Where("sync_path_id = ?", syncPathID).Delete(&model.StrmDirCache{}).Error
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ type FileEntry struct {
|
|||||||
MTime int64 `json:"mtime,omitempty"`
|
MTime int64 `json:"mtime,omitempty"`
|
||||||
// PickCode is 115-specific; other providers use ID directly.
|
// PickCode is 115-specific; other providers use ID directly.
|
||||||
PickCode string `json:"pick_code,omitempty"`
|
PickCode string `json:"pick_code,omitempty"`
|
||||||
|
// Sha1 is 115-specific content hash(大写 hex,目录/未完成文件可能为空或占位符)。
|
||||||
|
// 其他网盘不提供,留空时调用方退回大小比对。用于元数据"是否同一文件"的精确判定。
|
||||||
|
Sha1 string `json:"sha1,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DirectLink is a resolved playback target.
|
// DirectLink is a resolved playback target.
|
||||||
@@ -72,6 +75,15 @@ type Provider interface {
|
|||||||
Resolve(ctx context.Context, fileRef string) (*DirectLink, error)
|
Resolve(ctx context.Context, fileRef string) (*DirectLink, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchResolver is implemented by providers that can resolve several file
|
||||||
|
// references in fewer API calls(115 的 downurl 接口支持逗号分隔多个 pick_code,
|
||||||
|
// 批量换取可显著降低下载队列的换链请求量)。返回以原始引用为键的直链 map;
|
||||||
|
// 解析失败的引用不在结果中,err 汇报批量机制本身的失败,调用方应据此对缺失
|
||||||
|
// 项回退到逐个 Resolve。
|
||||||
|
type BatchResolver interface {
|
||||||
|
ResolveBatch(ctx context.Context, fileRefs []string) (map[string]*DirectLink, error)
|
||||||
|
}
|
||||||
|
|
||||||
// MutableProvider is implemented by cloud bridges that support safe folder
|
// MutableProvider is implemented by cloud bridges that support safe folder
|
||||||
// management through their official API or standard WebDAV methods.
|
// management through their official API or standard WebDAV methods.
|
||||||
type MutableProvider interface {
|
type MutableProvider interface {
|
||||||
|
|||||||
@@ -81,7 +81,12 @@ func Test115OpenAPIListPaginates(t *testing.T) {
|
|||||||
t.Fatalf("unexpected path %s", r.URL.Path)
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||||
}
|
}
|
||||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||||
count := 100
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
// 首页返回满页,之后返回 1 条:驱动按 offset/limit 翻页直到短页
|
||||||
|
count := limit
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
count = 1
|
count = 1
|
||||||
}
|
}
|
||||||
@@ -97,11 +102,12 @@ func Test115OpenAPIListPaginates(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("list: %v", err)
|
t.Fatalf("list: %v", err)
|
||||||
}
|
}
|
||||||
if len(entries) != 101 {
|
// List 使用文档上限 limit=1150:首页 1150 条 + 短页 1 条
|
||||||
t.Fatalf("entries = %d, want 101", len(entries))
|
if len(entries) != 1151 {
|
||||||
|
t.Fatalf("entries = %d, want 1151", len(entries))
|
||||||
}
|
}
|
||||||
if entries[100].ID != "100" || entries[100].PickCode != "pick100" {
|
if entries[1150].ID != "1150" || entries[1150].PickCode != "pick1150" {
|
||||||
t.Fatalf("last entry wrong: %#v", entries[100])
|
t.Fatalf("last entry wrong: %#v", entries[1150])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ func (p *openAPI115Provider) Ping(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *openAPI115Provider) List(ctx context.Context, dirID string) ([]FileEntry, error) {
|
func (p *openAPI115Provider) List(ctx context.Context, dirID string) ([]FileEntry, error) {
|
||||||
// 115 开放平台列表接口按 offset/limit 分页,这里循环取完整个目录
|
// 115 开放平台列表接口按 offset/limit 分页,这里循环取完整个目录;
|
||||||
const pageSize = 100
|
// limit 上限 1150(官方文档《获取文件列表》),取上限减少大目录翻页次数
|
||||||
|
const pageSize = 1150
|
||||||
var out []FileEntry
|
var out []FileEntry
|
||||||
for offset := 0; ; offset += pageSize {
|
for offset := 0; ; offset += pageSize {
|
||||||
files, _, err := p.c.GetFsList(ctx, dirID, offset, pageSize)
|
files, _, err := p.c.GetFsList(ctx, dirID, offset, pageSize)
|
||||||
@@ -72,6 +73,7 @@ func (p *openAPI115Provider) List(ctx context.Context, dirID string) ([]FileEntr
|
|||||||
Size: f.FileSize,
|
Size: f.FileSize,
|
||||||
MTime: f.ModifiedAt(),
|
MTime: f.ModifiedAt(),
|
||||||
PickCode: f.PickCode,
|
PickCode: f.PickCode,
|
||||||
|
Sha1: f.Sha1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(files) < pageSize {
|
if len(files) < pageSize {
|
||||||
@@ -105,6 +107,19 @@ func (p *openAPI115Provider) ResolveWithUA(ctx context.Context, fileRef, ua stri
|
|||||||
return &DirectLink{URL: url, Proxy: false, Headers: map[string]string{"User-Agent": bound}}, nil
|
return &DirectLink{URL: url, Proxy: false, Headers: map[string]string{"User-Agent": bound}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveBatch 批量换取直链(downurl 支持逗号分隔多 pick_code,一次请求覆盖
|
||||||
|
// 整批下载任务的换链)。返回 pickcode → 直链,未解析成功的引用不在结果中;
|
||||||
|
// err 非 nil 表示批量过程部分/全部失败,调用方对缺失项回退到逐个 Resolve。
|
||||||
|
// 下载队列统一使用默认 UA,与单个换取的防盗链绑定语义一致。
|
||||||
|
func (p *openAPI115Provider) ResolveBatch(ctx context.Context, fileRefs []string) (map[string]*DirectLink, error) {
|
||||||
|
urls, err := p.c.GetDownloadURLsBatch(ctx, fileRefs, "")
|
||||||
|
out := make(map[string]*DirectLink, len(urls))
|
||||||
|
for pc, u := range urls {
|
||||||
|
out[pc] = &DirectLink{URL: u, Proxy: false, Headers: map[string]string{"User-Agent": cloud115.DefaultUA}}
|
||||||
|
}
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
// OpenClient 暴露底层客户端(token 刷新用)。
|
// OpenClient 暴露底层客户端(token 刷新用)。
|
||||||
func (p *openAPI115Provider) OpenClient() *cloud115.OpenClient { return p.c }
|
func (p *openAPI115Provider) OpenClient() *cloud115.OpenClient { return p.c }
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -472,3 +473,65 @@ func TestFsListRefreshContinue(t *testing.T) {
|
|||||||
t.Fatalf("want 1 file, got %d", len(files))
|
t.Fatalf("want 1 file, got %d", len(files))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetDownloadURLsBatch 验证批量换链:多个 pick_code 合并为一次逗号分隔
|
||||||
|
// 请求;响应按文件 ID 为键、以条目内 pick_code 映射回请求侧;已缓存的
|
||||||
|
// pick_code 不再发起请求;缺失项(空 URL)不出现在结果中。
|
||||||
|
func TestGetDownloadURLsBatch(t *testing.T) {
|
||||||
|
var mu sync.Mutex
|
||||||
|
var requests []string
|
||||||
|
mockAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/open/ufile/downurl" {
|
||||||
|
t.Errorf("unexpected path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
pc := r.PostFormValue("pick_code")
|
||||||
|
mu.Lock()
|
||||||
|
requests = append(requests, pc)
|
||||||
|
mu.Unlock()
|
||||||
|
w.Write([]byte(`{"state":true,"data":{
|
||||||
|
"111":{"pick_code":"batch-pc-a","url":{"url":"https://cdn/a.mkv"}},
|
||||||
|
"222":{"pick_code":"batch-pc-b","url":{"url":"https://cdn/b.jpg"}},
|
||||||
|
"333":{"pick_code":"batch-pc-empty","url":{"url":""}}}}`))
|
||||||
|
})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ClearDownloadURLCache("batch-pc-a")
|
||||||
|
ClearDownloadURLCache("batch-pc-b")
|
||||||
|
ClearDownloadURLCache("batch-pc-empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
c := NewOpenClient("100195125", "at1", "rt1")
|
||||||
|
urls, err := c.GetDownloadURLsBatch(context.Background(),
|
||||||
|
[]string{"batch-pc-a", "batch-pc-b", "batch-pc-empty", "", "batch-pc-a"}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("batch downurl: %v", err)
|
||||||
|
}
|
||||||
|
if urls["batch-pc-a"] != "https://cdn/a.mkv" || urls["batch-pc-b"] != "https://cdn/b.jpg" {
|
||||||
|
t.Fatalf("bad urls: %#v", urls)
|
||||||
|
}
|
||||||
|
if _, ok := urls["batch-pc-empty"]; ok {
|
||||||
|
t.Fatalf("empty-url entry should be absent: %#v", urls)
|
||||||
|
}
|
||||||
|
if _, ok := urls[""]; ok {
|
||||||
|
t.Fatalf("empty pickcode should be absent: %#v", urls)
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
if len(requests) != 1 || requests[0] != "batch-pc-a,batch-pc-b,batch-pc-empty" {
|
||||||
|
mu.Unlock()
|
||||||
|
t.Fatalf("unexpected downurl requests: %v", requests)
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
// 第二次调用全部命中缓存:不再发任何请求
|
||||||
|
urls2, err := c.GetDownloadURLsBatch(context.Background(), []string{"batch-pc-a", "batch-pc-b"}, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cached batch downurl: %v", err)
|
||||||
|
}
|
||||||
|
if urls2["batch-pc-a"] != "https://cdn/a.mkv" {
|
||||||
|
t.Fatalf("cached url lost: %#v", urls2)
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if len(requests) != 1 {
|
||||||
|
t.Fatalf("cache hit should not issue requests, got %v", requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 115 开放平台删除类 API:元数据覆盖上传前清理远端旧文件。
|
||||||
|
package cloud115
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteFiles 批量删除 115 文件(官方接口 POST /open/ufile/delete)。
|
||||||
|
// 删除为异步执行,文件移入回收站。parentID 为待删除文件所在父目录 ID
|
||||||
|
//(可选提示,空串省略)。fileIDs 中的空项自动忽略,全为空时直接返回成功。
|
||||||
|
func (c *OpenClient) DeleteFiles(ctx context.Context, parentID string, fileIDs ...string) error {
|
||||||
|
ids := make([]string, 0, len(fileIDs))
|
||||||
|
for _, id := range fileIDs {
|
||||||
|
if id = strings.TrimSpace(id); id != "" {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
params := map[string]string{"file_ids": strings.Join(ids, ",")}
|
||||||
|
if parentID = strings.TrimSpace(parentID); parentID != "" {
|
||||||
|
params["parent_id"] = parentID
|
||||||
|
}
|
||||||
|
resp, err := c.doAuthJSON(ctx, "POST", ProAPIBase+"/open/ufile/delete", params, 2)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// doJSON 已把 state=false 转为错误返回,这里兜底防御响应外壳异常
|
||||||
|
if !resp.State {
|
||||||
|
return NewOpenAPIResponseError(resp.Code, resp.Errno, resp.Message, resp.Error, "115 删除文件失败")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -225,6 +225,62 @@ func (c *OpenClient) GetDownloadURLWithUA(ctx context.Context, pickCode, ua stri
|
|||||||
return first.URL.URL, nil
|
return first.URL.URL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downurlBatchSize 单次批量换取直链的 pick_code 数上限。官方 /open/ufile/downurl
|
||||||
|
// 支持逗号分隔多个 pick_code,批量可大幅降低元数据下载的换链请求量;大小取
|
||||||
|
// 保守值,减小单个违规/异常文件导致整批失败的爆炸半径。
|
||||||
|
const downurlBatchSize = 10
|
||||||
|
|
||||||
|
// GetDownloadURLsBatch 批量获取下载直链(pickcode → URL)。先查进程内缓存,
|
||||||
|
// 仅对未命中的 pick_code 分片发起批量请求;单个分片失败时返回已解析的部分与
|
||||||
|
// 错误,调用方对缺失项回退到逐个 GetDownloadURLWithUA。UA 语义与单个换取
|
||||||
|
// 一致:直链绑定换取时的 UA,后续下载必须携带同一 UA。
|
||||||
|
func (c *OpenClient) GetDownloadURLsBatch(ctx context.Context, pickCodes []string, ua string) (map[string]string, error) {
|
||||||
|
ua = strings.TrimSpace(ua)
|
||||||
|
out := make(map[string]string, len(pickCodes))
|
||||||
|
seen := make(map[string]struct{}, len(pickCodes))
|
||||||
|
missing := make([]string, 0, len(pickCodes))
|
||||||
|
for _, pc := range pickCodes {
|
||||||
|
pc = strings.TrimSpace(pc)
|
||||||
|
if pc == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, dup := seen[pc]; dup {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pc] = struct{}{}
|
||||||
|
if cached := GetDownloadURLCache(pc, ua); cached != "" {
|
||||||
|
out[pc] = cached
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missing = append(missing, pc)
|
||||||
|
}
|
||||||
|
for start := 0; start < len(missing); start += downurlBatchSize {
|
||||||
|
end := start + downurlBatchSize
|
||||||
|
if end > len(missing) {
|
||||||
|
end = len(missing)
|
||||||
|
}
|
||||||
|
chunk := missing[start:end]
|
||||||
|
params := map[string]string{"pick_code": strings.Join(chunk, ",")}
|
||||||
|
resp, err := c.doAuthJSONWithUA(ctx, "POST", ProAPIBase+"/open/ufile/downurl", params, 1, ua)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
var data map[string]downloadURLData
|
||||||
|
if err := json.Unmarshal(resp.Data, &data); err != nil {
|
||||||
|
return out, fmt.Errorf("115: 解析下载地址失败:%w", err)
|
||||||
|
}
|
||||||
|
// 响应以文件 ID 为键,条目内的 pick_code 用于映射回请求侧
|
||||||
|
for _, item := range data {
|
||||||
|
if item.PickCode == "" || item.URL.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
SetDownloadURLCache(item.PickCode, item.URL.URL, ua)
|
||||||
|
out[item.PickCode] = item.URL.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 授权(设备码扫码) ──────────────────────────────────────────────────────
|
// ─── 授权(设备码扫码) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
// QrCodeScanStatus 扫码状态。
|
// QrCodeScanStatus 扫码状态。
|
||||||
|
|||||||
+111
-14
@@ -52,16 +52,28 @@ func (s *StrmService) downloadWorker(ctx context.Context) {
|
|||||||
sleepContext(ctx, 2*time.Second)
|
sleepContext(ctx, 2*time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var wg sync.WaitGroup
|
// 处于 WAF 冷却的 115 任务先退回,剩余任务在派发前按账号批量换链:
|
||||||
|
// downurl 支持逗号分隔多个 pick_code,整批任务一次请求即可完成解析,
|
||||||
|
// 显著减少全局 QPS 限流下的换链请求量。
|
||||||
|
runnable := make([]*model.StrmDownloadTask, 0, len(tasks))
|
||||||
for i := range tasks {
|
for i := range tasks {
|
||||||
|
task := &tasks[i]
|
||||||
|
if task.Provider == model.StrmProvider115 && s.wafCooldownLeft() > 0 {
|
||||||
|
s.requeueDownloadTask(task)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
runnable = append(runnable, task)
|
||||||
|
}
|
||||||
|
if len(runnable) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resolved := s.batchResolve115Links(ctx, runnable)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := range runnable {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(i int) {
|
go func(i int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
task := &tasks[i]
|
task := runnable[i]
|
||||||
if task.Provider == model.StrmProvider115 && s.wafCooldownLeft() > 0 {
|
|
||||||
s.requeueDownloadTask(task)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !s.acquireDownloadSlot(ctx, task.Provider) {
|
if !s.acquireDownloadSlot(ctx, task.Provider) {
|
||||||
s.requeueDownloadTask(task)
|
s.requeueDownloadTask(task)
|
||||||
return
|
return
|
||||||
@@ -76,7 +88,7 @@ func (s *StrmService) downloadWorker(ctx context.Context) {
|
|||||||
s.downloadTaskFailWithRetry(task, "任务执行异常中断")
|
s.downloadTaskFailWithRetry(task, "任务执行异常中断")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
s.processDownloadTask(ctx, task)
|
s.processDownloadTask(ctx, task, resolved)
|
||||||
completed = true
|
completed = true
|
||||||
})
|
})
|
||||||
}(i)
|
}(i)
|
||||||
@@ -85,6 +97,69 @@ func (s *StrmService) downloadWorker(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dlResolveKey 构造批量换链结果 map 的键(按账号隔离,避免极端情况下不同
|
||||||
|
// 账号的引用串扰)。
|
||||||
|
func dlResolveKey(accountID, fileRef string) string {
|
||||||
|
return accountID + "|" + fileRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// batchResolve115Links 在派发执行前对 115 下载任务做批量换链。官方 downurl
|
||||||
|
// 接口支持逗号分隔多个 pick_code(文档《获取文件下载地址》),按账号把整批
|
||||||
|
// 任务的 pickcode 合并换取,减少 QPS 限流下的换链请求量。解析结果写入
|
||||||
|
// pickcode 直链缓存供任务执行时命中;批量失败只记日志并触发风控冷却判定,
|
||||||
|
// 未解析成功的任务在执行时回退到逐个 Resolve,不影响任务本身。
|
||||||
|
func (s *StrmService) batchResolve115Links(ctx context.Context, tasks []*model.StrmDownloadTask) map[string]*cloud.DirectLink {
|
||||||
|
byAcct := map[string][]string{}
|
||||||
|
seenRef := map[string]map[string]struct{}{}
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.Provider != model.StrmProvider115 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ref := strings.TrimSpace(task.RemoteRef)
|
||||||
|
if ref == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seenRef[task.AccountID] == nil {
|
||||||
|
seenRef[task.AccountID] = map[string]struct{}{}
|
||||||
|
}
|
||||||
|
if _, dup := seenRef[task.AccountID][ref]; dup {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenRef[task.AccountID][ref] = struct{}{}
|
||||||
|
byAcct[task.AccountID] = append(byAcct[task.AccountID], ref)
|
||||||
|
}
|
||||||
|
resolved := map[string]*cloud.DirectLink{}
|
||||||
|
for acctID, refs := range byAcct {
|
||||||
|
acct, err := s.repo.StrmAccount.FindByID(ctx, acctID)
|
||||||
|
if err != nil || acct == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
provider, err := s.providerFor(ctx, acct)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
batch, ok := provider.(cloud.BatchResolver)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
links, err := batch.ResolveBatch(ctx, refs)
|
||||||
|
if err != nil {
|
||||||
|
if is115Blocked(err) {
|
||||||
|
s.triggerWAFCooldown()
|
||||||
|
}
|
||||||
|
s.log.Warn("batch resolve 115 download links failed; fall back to per-task resolve",
|
||||||
|
zap.String("account_id", acctID), zap.Int("refs", len(refs)), zap.Error(err))
|
||||||
|
}
|
||||||
|
for ref, link := range links {
|
||||||
|
if link == nil || link.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resolved[dlResolveKey(acctID, ref)] = link
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
// requeueDownloadTask 把已认领但未实际执行的任务退回 pending,避免长期停留在 running。
|
// requeueDownloadTask 把已认领但未实际执行的任务退回 pending,避免长期停留在 running。
|
||||||
// 退回时必须设置 NextTryAt(WAF 冷却剩余时间):claim 只过滤 next_try_at
|
// 退回时必须设置 NextTryAt(WAF 冷却剩余时间):claim 只过滤 next_try_at
|
||||||
// 已过期的任务,不设会让同一批任务被立刻再认领,形成 claim/requeue
|
// 已过期的任务,不设会让同一批任务被立刻再认领,形成 claim/requeue
|
||||||
@@ -104,7 +179,9 @@ func (s *StrmService) requeueDownloadTask(task *model.StrmDownloadTask) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StrmService) processDownloadTask(ctx context.Context, task *model.StrmDownloadTask) {
|
// processDownloadTask 处理单个下载任务:解析直链(优先使用批量换链预取的
|
||||||
|
// 结果,未命中时逐个 Resolve)→ 下载 → 落盘。
|
||||||
|
func (s *StrmService) processDownloadTask(ctx context.Context, task *model.StrmDownloadTask, resolved map[string]*cloud.DirectLink) {
|
||||||
cleanPath := sanitizeLocalPath(task.LocalPath)
|
cleanPath := sanitizeLocalPath(task.LocalPath)
|
||||||
if cleanPath != "" && cleanPath != task.LocalPath {
|
if cleanPath != "" && cleanPath != task.LocalPath {
|
||||||
task.LocalPath = cleanPath
|
task.LocalPath = cleanPath
|
||||||
@@ -137,13 +214,16 @@ func (s *StrmService) processDownloadTask(ctx context.Context, task *model.StrmD
|
|||||||
s.downloadTaskFailWithRetry(task, err.Error())
|
s.downloadTaskFailWithRetry(task, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
link, err := provider.Resolve(ctx, task.RemoteRef)
|
link, ok := resolved[dlResolveKey(task.AccountID, task.RemoteRef)]
|
||||||
if err != nil {
|
if !ok || link == nil || link.URL == "" {
|
||||||
if is115Blocked(err) {
|
link, err = provider.Resolve(ctx, task.RemoteRef)
|
||||||
s.triggerWAFCooldown()
|
if err != nil {
|
||||||
|
if is115Blocked(err) {
|
||||||
|
s.triggerWAFCooldown()
|
||||||
|
}
|
||||||
|
s.downloadTaskFailWithRetry(task, "解析下载地址失败:"+err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
s.downloadTaskFailWithRetry(task, "解析下载地址失败:"+err.Error())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if err := downloadToFile(ctx, link, task.LocalPath, s.http); err != nil {
|
if err := downloadToFile(ctx, link, task.LocalPath, s.http); err != nil {
|
||||||
// 直链失效(403/404/410 等):清掉缓存让下一轮重新换取
|
// 直链失效(403/404/410 等):清掉缓存让下一轮重新换取
|
||||||
@@ -286,6 +366,20 @@ func (s *StrmService) processUpload115(ctx context.Context, task *model.StrmUplo
|
|||||||
finish(model.StrmTaskFailed, "该网盘不支持元数据上传")
|
finish(model.StrmTaskFailed, "该网盘不支持元数据上传")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 以本地为准:网盘端已有同名但内容不同的旧元数据时,先删除旧文件再上传。
|
||||||
|
// 115 的上传接口不保证同名覆盖,直接上传可能产生同名重复文件;删除失败则
|
||||||
|
// 任务重试(旧文件 ID 失效的场景会在下次同步后自动修复)。
|
||||||
|
if task.RemoteRef != "" {
|
||||||
|
open115, ok := provider.(cloud.OpenAPI115Provider)
|
||||||
|
if !ok {
|
||||||
|
finish(model.StrmTaskFailed, "该网盘不支持删除远端旧元数据")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := open115.OpenClient().DeleteFiles(ctx, task.RemotePath, task.RemoteRef); err != nil {
|
||||||
|
s.uploadTaskFailWithRetry(task, "删除网盘旧元数据失败:"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
f, err := os.Open(task.LocalPath)
|
f, err := os.Open(task.LocalPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.uploadTaskFailWithRetry(task, "打开本地文件失败:"+err.Error())
|
s.uploadTaskFailWithRetry(task, "打开本地文件失败:"+err.Error())
|
||||||
@@ -765,12 +859,15 @@ func (s *StrmService) wafCooldownLeft() time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// is115Blocked 判断错误是否来自 115 的风控/限流(WAF 405 拦截页或限流错误码)。
|
// is115Blocked 判断错误是否来自 115 的风控/限流(WAF 405 拦截页或限流错误码)。
|
||||||
|
// 覆盖两层文案:HTTP 层(doJSON 的"接口触发频控/安全拦截(HTTP 405)")与
|
||||||
|
// 业务错误码层(OpenAPIError 的"115 接口错误(406/770004)")。
|
||||||
func is115Blocked(err error) bool {
|
func is115Blocked(err error) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
msg := strings.ToLower(err.Error())
|
msg := strings.ToLower(err.Error())
|
||||||
return strings.Contains(msg, "115 接口返回 http 405") ||
|
return strings.Contains(msg, "115 接口返回 http 405") ||
|
||||||
|
strings.Contains(msg, "115 接口触发频控/安全拦截") ||
|
||||||
strings.Contains(msg, "访问被阻断") ||
|
strings.Contains(msg, "访问被阻断") ||
|
||||||
strings.Contains(msg, "request has been blocked") ||
|
strings.Contains(msg, "request has been blocked") ||
|
||||||
strings.Contains(msg, "115 接口错误(770004") ||
|
strings.Contains(msg, "115 接口错误(770004") ||
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/truewhile/MeBox/internal/model"
|
"github.com/truewhile/MeBox/internal/model"
|
||||||
"github.com/truewhile/MeBox/internal/repository"
|
"github.com/truewhile/MeBox/internal/repository"
|
||||||
|
"github.com/truewhile/MeBox/internal/service/cloud"
|
||||||
|
"github.com/truewhile/MeBox/internal/service/cloud115"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +25,7 @@ func TestIs115Blocked(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{errors.New("115 接口返回 HTTP 405:<!doctypehtml>...访问被阻断"), true},
|
{errors.New("115 接口返回 HTTP 405:<!doctypehtml>...访问被阻断"), true},
|
||||||
{errors.New("115 接口返回 HTTP 405"), true},
|
{errors.New("115 接口返回 HTTP 405"), true},
|
||||||
|
{errors.New("115 接口触发频控/安全拦截(HTTP 405):阿里云 WAF 拦截页"), true},
|
||||||
{errors.New("115 接口错误(770004):访问频率过高"), true},
|
{errors.New("115 接口错误(770004):访问频率过高"), true},
|
||||||
{errors.New("115 接口错误(406):达到访问上限"), true},
|
{errors.New("115 接口错误(406):达到访问上限"), true},
|
||||||
{errors.New("下载失败:http 403"), false},
|
{errors.New("下载失败:http 403"), false},
|
||||||
@@ -164,3 +172,189 @@ func TestRequeueDownloadTask(t *testing.T) {
|
|||||||
t.Fatalf("task not requeued: %+v", got)
|
t.Fatalf("task not requeued: %+v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestProcessUpload115DeletesStaleRemoteMetaFirst 验证 115 覆盖上传语义(以本地为准):
|
||||||
|
// 任务携带网盘旧文件 ID 时,必须先调用 /open/ufile/delete 删除旧元数据再上传本地文件,
|
||||||
|
// 避免 115 出现同名重复文件;删除请求应携带 file_ids 与父目录 cid。
|
||||||
|
func TestProcessUpload115DeletesStaleRemoteMetaFirst(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
localFile := filepath.Join(localDir, "movie.nfo")
|
||||||
|
if err := os.WriteFile(localFile, []byte("local-nfo-data"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acct := &model.StrmAccount{Name: "fake115", Provider: "cloud115", Config: "{}", Enabled: true}
|
||||||
|
if err := svc.repo.StrmAccount.Create(context.Background(), acct); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
var calls []string
|
||||||
|
deleteForm := map[string]string{}
|
||||||
|
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_ = r.ParseForm()
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/open/ufile/delete":
|
||||||
|
calls = append(calls, "delete")
|
||||||
|
deleteForm["file_ids"] = r.FormValue("file_ids")
|
||||||
|
deleteForm["parent_id"] = r.FormValue("parent_id")
|
||||||
|
w.Write([]byte(`{"state":true,"data":[]}`))
|
||||||
|
case "/open/upload/init":
|
||||||
|
calls = append(calls, "upload")
|
||||||
|
// 返回秒传成功,跳过 OSS 真实上传
|
||||||
|
w.Write([]byte(`{"state":true,"data":{"status":2,"file_id":"new-1","pick_code":"new-pc-1","callback":null}}`))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected 115 api path %s", r.URL.Path)
|
||||||
|
w.Write([]byte(`{"state":false,"message":"unexpected path"}`))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer api.Close()
|
||||||
|
oldPro := cloud115.ProAPIBase
|
||||||
|
cloud115.ProAPIBase = api.URL
|
||||||
|
defer func() { cloud115.ProAPIBase = oldPro }()
|
||||||
|
|
||||||
|
task := &model.StrmUploadTask{
|
||||||
|
Base: model.Base{ID: "up-del-1"},
|
||||||
|
SyncPathID: "p1",
|
||||||
|
AccountID: acct.ID,
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
FileName: "movie.nfo",
|
||||||
|
LocalPath: localFile,
|
||||||
|
RemotePath: "777",
|
||||||
|
RemoteRef: "old-file-1",
|
||||||
|
Status: model.StrmTaskRunning,
|
||||||
|
}
|
||||||
|
svc.processUpload115(context.Background(), task)
|
||||||
|
|
||||||
|
if task.Status != model.StrmTaskDone {
|
||||||
|
t.Fatalf("upload task should succeed, status = %s, error = %s", task.Status, task.Error)
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if len(calls) != 2 || calls[0] != "delete" || calls[1] != "upload" {
|
||||||
|
t.Fatalf("expected delete before upload, got calls = %v", calls)
|
||||||
|
}
|
||||||
|
if deleteForm["file_ids"] != "old-file-1" {
|
||||||
|
t.Fatalf("delete file_ids = %q, want old-file-1", deleteForm["file_ids"])
|
||||||
|
}
|
||||||
|
if deleteForm["parent_id"] != "777" {
|
||||||
|
t.Fatalf("delete parent_id = %q, want 777", deleteForm["parent_id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBatchResolve115Links 验证下载队列的批量换链:同账号多个 115 任务的
|
||||||
|
// pickcode 合并为一次 downurl 请求(官方接口支持逗号分隔多 pick_code),
|
||||||
|
// 重复引用去重、非 115 任务不参与、直链携带绑定 UA。
|
||||||
|
func TestBatchResolve115Links(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
acct := &model.StrmAccount{Name: "fake115", Provider: model.StrmProvider115, Config: "{}", Enabled: true}
|
||||||
|
if err := svc.repo.StrmAccount.Create(context.Background(), acct); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
var requests []string
|
||||||
|
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/open/ufile/downurl":
|
||||||
|
_ = r.ParseForm()
|
||||||
|
mu.Lock()
|
||||||
|
requests = append(requests, r.PostFormValue("pick_code"))
|
||||||
|
mu.Unlock()
|
||||||
|
w.Write([]byte(`{"state":true,"data":{
|
||||||
|
"111":{"pick_code":"q-pc-a","url":{"url":"http://cdn/a.mkv"}},
|
||||||
|
"222":{"pick_code":"q-pc-b","url":{"url":"http://cdn/b.jpg"}}}}`))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected 115 api path %s", r.URL.Path)
|
||||||
|
w.Write([]byte(`{"state":false,"message":"unexpected path"}`))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer api.Close()
|
||||||
|
oldPro := cloud115.ProAPIBase
|
||||||
|
cloud115.ProAPIBase = api.URL
|
||||||
|
defer func() { cloud115.ProAPIBase = oldPro }()
|
||||||
|
defer cloud115.ClearDownloadURLCache("q-pc-a")
|
||||||
|
defer cloud115.ClearDownloadURLCache("q-pc-b")
|
||||||
|
|
||||||
|
tasks := []*model.StrmDownloadTask{
|
||||||
|
{SyncPathID: "p1", AccountID: acct.ID, Provider: model.StrmProvider115, RemoteRef: "q-pc-a"},
|
||||||
|
{SyncPathID: "p1", AccountID: acct.ID, Provider: model.StrmProvider115, RemoteRef: "q-pc-a"}, // 重复引用
|
||||||
|
{SyncPathID: "p1", AccountID: acct.ID, Provider: model.StrmProvider115, RemoteRef: "q-pc-b"},
|
||||||
|
{SyncPathID: "p1", AccountID: acct.ID, Provider: model.StrmProviderOpenList, RemoteRef: "ol-ref"},
|
||||||
|
{SyncPathID: "p1", AccountID: acct.ID, Provider: model.StrmProvider115, RemoteRef: " "}, // 空引用
|
||||||
|
}
|
||||||
|
resolved := svc.batchResolve115Links(context.Background(), tasks)
|
||||||
|
|
||||||
|
if got := resolved[dlResolveKey(acct.ID, "q-pc-a")]; got == nil || got.URL != "http://cdn/a.mkv" {
|
||||||
|
t.Fatalf("missing/bad link for q-pc-a: %+v", resolved)
|
||||||
|
}
|
||||||
|
if got := resolved[dlResolveKey(acct.ID, "q-pc-b")]; got == nil || got.URL != "http://cdn/b.jpg" {
|
||||||
|
t.Fatalf("missing/bad link for q-pc-b: %+v", resolved)
|
||||||
|
}
|
||||||
|
if got := resolved[dlResolveKey(acct.ID, "q-pc-a")].Headers["User-Agent"]; got != cloud115.DefaultUA {
|
||||||
|
t.Fatalf("link UA = %q, want default bound UA", got)
|
||||||
|
}
|
||||||
|
if _, ok := resolved[dlResolveKey(acct.ID, "ol-ref")]; ok {
|
||||||
|
t.Fatal("non-115 task should not be batch resolved")
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if len(requests) != 1 || requests[0] != "q-pc-a,q-pc-b" {
|
||||||
|
t.Fatalf("expected single batched downurl request, got %v", requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProcessDownloadTaskUsesPreResolvedLink 验证任务执行时优先使用批量换链
|
||||||
|
// 预取的直链:downurl 接口保持失败,若任务仍走逐个换链则必然失败。
|
||||||
|
func TestProcessDownloadTaskUsesPreResolvedLink(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
acct := &model.StrmAccount{Name: "fake115", Provider: model.StrmProvider115, Config: "{}", Enabled: true}
|
||||||
|
if err := svc.repo.StrmAccount.Create(context.Background(), acct); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer api.Close()
|
||||||
|
oldPro := cloud115.ProAPIBase
|
||||||
|
cloud115.ProAPIBase = api.URL
|
||||||
|
defer func() { cloud115.ProAPIBase = oldPro }()
|
||||||
|
|
||||||
|
contentSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Header.Get("User-Agent") != cloud115.DefaultUA {
|
||||||
|
t.Errorf("download UA = %q, want bound default UA", r.Header.Get("User-Agent"))
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte("nfo-content"))
|
||||||
|
}))
|
||||||
|
defer contentSrv.Close()
|
||||||
|
|
||||||
|
task := &model.StrmDownloadTask{
|
||||||
|
SyncPathID: "p1",
|
||||||
|
AccountID: acct.ID,
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
FileName: "movie.nfo",
|
||||||
|
RemoteRef: "q-pc-pre",
|
||||||
|
LocalPath: filepath.Join(localDir, "movie.nfo"),
|
||||||
|
Status: model.StrmTaskRunning,
|
||||||
|
}
|
||||||
|
if err := svc.repo.StrmDownload.Create(context.Background(), task); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resolved := map[string]*cloud.DirectLink{
|
||||||
|
dlResolveKey(acct.ID, "q-pc-pre"): {URL: contentSrv.URL, Headers: map[string]string{"User-Agent": cloud115.DefaultUA}},
|
||||||
|
}
|
||||||
|
svc.processDownloadTask(context.Background(), task, resolved)
|
||||||
|
|
||||||
|
if task.Status != model.StrmTaskDone {
|
||||||
|
t.Fatalf("task should be done via pre-resolved link, status = %s, error = %s", task.Status, task.Error)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(task.LocalPath)
|
||||||
|
if err != nil || string(data) != "nfo-content" {
|
||||||
|
t.Fatalf("downloaded file mismatch: err = %v, data = %q", err, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ var StrmSettingDefs = map[string]struct {
|
|||||||
StrmSettingMinVideoSizeMB: {Default: "0", Label: "最小视频大小(MB)", Kind: "number", Help: "小于该大小的视频文件不生成 STRM,0 表示不限"},
|
StrmSettingMinVideoSizeMB: {Default: "0", Label: "最小视频大小(MB)", Kind: "number", Help: "小于该大小的视频文件不生成 STRM,0 表示不限"},
|
||||||
StrmSettingAddPath: {Default: "1", Label: "STRM 链接 path 参数", Kind: "choice", Choices: []string{"1", "2", "3"}, Help: "1=附带完整远端路径 2=仅文件名 3=不带 path"},
|
StrmSettingAddPath: {Default: "1", Label: "STRM 链接 path 参数", Kind: "choice", Choices: []string{"1", "2", "3"}, Help: "1=附带完整远端路径 2=仅文件名 3=不带 path"},
|
||||||
StrmSettingDownloadMeta: {Default: "true", Label: "下载元数据", Kind: "bool", Help: "同步时把远端 nfo/图片/字幕下载到本地输出目录"},
|
StrmSettingDownloadMeta: {Default: "true", Label: "下载元数据", Kind: "bool", Help: "同步时把远端 nfo/图片/字幕下载到本地输出目录"},
|
||||||
StrmSettingUploadMeta: {Default: "false", Label: "上传元数据", Kind: "bool", Help: "同步时把本地元数据上传到远端(需网盘支持写入)"},
|
StrmSettingUploadMeta: {Default: "false", Label: "上传元数据", Kind: "bool", Help: "同步时把本地元数据上传到远端;本地与网盘元数据不同时以本地为准覆盖(需网盘支持写入)"},
|
||||||
StrmSettingDeleteDir: {Default: "false", Label: "清理空目录", Kind: "bool", Help: "清理远端已删除的多余 .strm/元数据后,删除空目录"},
|
StrmSettingDeleteDir: {Default: "false", Label: "清理空目录", Kind: "bool", Help: "清理远端已删除的多余 .strm/元数据后,删除空目录"},
|
||||||
Strm115RelayKeySetting: {Default: "", Label: "115 中继授权共享密钥", Kind: "text", Help: "QMediaSync/MQFamily 中继授权的共享 AES 密钥(OAUTH_RELAY_ENCRYPTION_KEY);不配置则中继授权不可用"},
|
Strm115RelayKeySetting: {Default: "", Label: "115 中继授权共享密钥", Kind: "text", Help: "QMediaSync/MQFamily 中继授权的共享 AES 密钥(OAUTH_RELAY_ENCRYPTION_KEY);不配置则中继授权不可用"},
|
||||||
StrmSettingDownloadThreads: {Default: "6", Label: "下载队列线程数", Kind: "number", Help: "OpenList/CloudDrive2 元数据下载并发数(115 独立限速为 3)"},
|
StrmSettingDownloadThreads: {Default: "6", Label: "下载队列线程数", Kind: "number", Help: "OpenList/CloudDrive2 元数据下载并发数(115 独立限速为 3)"},
|
||||||
|
|||||||
+189
-35
@@ -41,7 +41,9 @@ type strmSyncState struct {
|
|||||||
lastProgressFlush time.Time // 上次进度落库时间
|
lastProgressFlush time.Time // 上次进度落库时间
|
||||||
seenVideo map[string]bool // "v:"+去掉扩展名的相对路径 → 远端存在该视频
|
seenVideo map[string]bool // "v:"+去掉扩展名的相对路径 → 远端存在该视频
|
||||||
seenMeta map[string]bool // "m:"+相对路径 → 远端存在该元数据
|
seenMeta map[string]bool // "m:"+相对路径 → 远端存在该元数据
|
||||||
remoteMeta map[string]int64 // 远端元数据大小(上传比对用)
|
remoteMeta map[string]int64 // 远端元数据大小(上传比对用)
|
||||||
|
remoteMetaRef map[string]string // "m:"+相对路径 → 远端元数据文件引用(115 文件 ID,覆盖上传前删除旧文件用)
|
||||||
|
remoteMetaSha1 map[string]string // "m:"+相对路径 → 远端元数据内容 SHA1(115 列表返回;上传/下载精确比对用,其他网盘为空)
|
||||||
seenMetaTarget map[string]cloud.FileEntry
|
seenMetaTarget map[string]cloud.FileEntry
|
||||||
seenVideoTarget map[string]cloud.FileEntry
|
seenVideoTarget map[string]cloud.FileEntry
|
||||||
activeDownloadPaths map[string]bool // 本地已在排队/进行的下载任务路径(内存去重)
|
activeDownloadPaths map[string]bool // 本地已在排队/进行的下载任务路径(内存去重)
|
||||||
@@ -50,6 +52,7 @@ type strmSyncState struct {
|
|||||||
pendingUploads []*model.StrmUploadTask
|
pendingUploads []*model.StrmUploadTask
|
||||||
dirCache sync.Map // dirID (string) -> relativePath (string)
|
dirCache sync.Map // dirID (string) -> relativePath (string)
|
||||||
dirPathToID map[string]string // relativePath (string) -> dirID(115 上传父目录寻址用,walk 后构建)
|
dirPathToID map[string]string // relativePath (string) -> dirID(115 上传父目录寻址用,walk 后构建)
|
||||||
|
dirCacheDirty map[string]string // 待批量落库的目录缓存(dirID → 相对路径),避免逐目录单条 upsert
|
||||||
|
|
||||||
scanIncomplete atomic.Bool // 远端目录树/文件列表本次扫描不完整 → 禁止增量 prune 误删本地文件
|
scanIncomplete atomic.Bool // 远端目录树/文件列表本次扫描不完整 → 禁止增量 prune 误删本地文件
|
||||||
}
|
}
|
||||||
@@ -201,6 +204,8 @@ func (s *StrmService) runSync(ctx context.Context, p *model.StrmSyncPath, rec *m
|
|||||||
seenVideo: map[string]bool{},
|
seenVideo: map[string]bool{},
|
||||||
seenMeta: map[string]bool{},
|
seenMeta: map[string]bool{},
|
||||||
remoteMeta: map[string]int64{},
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
|
remoteMetaSha1: map[string]string{},
|
||||||
seenMetaTarget: map[string]cloud.FileEntry{},
|
seenMetaTarget: map[string]cloud.FileEntry{},
|
||||||
seenVideoTarget: map[string]cloud.FileEntry{},
|
seenVideoTarget: map[string]cloud.FileEntry{},
|
||||||
}
|
}
|
||||||
@@ -331,6 +336,11 @@ func (st *strmSyncState) run() error {
|
|||||||
// 多个目录列表请求的网络往返彼此重叠,大幅缩短大目录树同步耗时。
|
// 多个目录列表请求的网络往返彼此重叠,大幅缩短大目录树同步耗时。
|
||||||
const strmScanWorkers = 8
|
const strmScanWorkers = 8
|
||||||
|
|
||||||
|
// strmProcessWorkers 115 平铺拉取后本地文件分类处理(生成 strm/元数据入队)
|
||||||
|
// 的 worker 数。本地磁盘 I/O 是大库同步的尾部瓶颈,输出目录在网络挂载上尤甚;
|
||||||
|
// processRemoteFile 的共享状态均由 st.mu 保护,可安全并发。
|
||||||
|
const strmProcessWorkers = 8
|
||||||
|
|
||||||
// walkRemote 并发广度优先遍历网盘目录树。
|
// walkRemote 并发广度优先遍历网盘目录树。
|
||||||
// 多个 worker 并行执行 List(受全局 115 令牌桶限流约束),子目录动态
|
// 多个 worker 并行执行 List(受全局 115 令牌桶限流约束),子目录动态
|
||||||
// 入队;任一目录失败则取消其余 worker 并返回错误(与旧串行版语义一致)。
|
// 入队;任一目录失败则取消其余 worker 并返回错误(与旧串行版语义一致)。
|
||||||
@@ -509,8 +519,8 @@ func (st *strmSyncState) isMetaExt(ext string) bool {
|
|||||||
|
|
||||||
// cleanDirRel 对 115 扁平化拉取的目录相对路径逐段套用目录级文件名清洗,
|
// cleanDirRel 对 115 扁平化拉取的目录相对路径逐段套用目录级文件名清洗,
|
||||||
// 确保与 walkRemote / joinLocalRel(sanitizeRelativePath)使用同一套清洗规则。
|
// 确保与 walkRemote / joinLocalRel(sanitizeRelativePath)使用同一套清洗规则。
|
||||||
// 若不清洗,目录名中的冒号等非法字符会直达 rel,而 seenVideo/seenMeta 的 key
|
// 若不清洗,目录名中的冒号等非法字符会直达 rel,而 seenVideo/remoteMeta 的 key
|
||||||
// 与磁盘实际路径不一致,导致 pruneLocal 误删已下载的 strm / 元数据。
|
// 与磁盘实际路径不一致,导致 pruneLocal 误删已下载的 strm、上传误传或重复下载。
|
||||||
// 空 rel(根目录)原样返回。
|
// 空 rel(根目录)原样返回。
|
||||||
func cleanDirRel(rel string) string {
|
func cleanDirRel(rel string) string {
|
||||||
if rel == "" {
|
if rel == "" {
|
||||||
@@ -530,6 +540,34 @@ func cleanDirRel(rel string) string {
|
|||||||
return strings.Join(out, "/")
|
return strings.Join(out, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deferDirCacheSave 暂存一条目录缓存写入,由 flushDirCacheSave 统一批量落库。
|
||||||
|
// 首次全量同步可能有上万个目录,逐目录单条 upsert 会造成明显的 SQLite 写锁
|
||||||
|
// 竞争;内存 dirCache(sync.Map)始终即时可用,落库仅服务于下次增量预加载。
|
||||||
|
func (st *strmSyncState) deferDirCacheSave(dirID, relPath string) {
|
||||||
|
st.mu.Lock()
|
||||||
|
if st.dirCacheDirty == nil {
|
||||||
|
st.dirCacheDirty = map[string]string{}
|
||||||
|
}
|
||||||
|
st.dirCacheDirty[dirID] = relPath
|
||||||
|
st.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushDirCacheSave 把暂存的目录缓存一次性批量落库;失败仅记日志(缓存缺失
|
||||||
|
// 只影响下次增量的目录解析提速,正确性由"重新向 115 获取"兜底)。
|
||||||
|
func (st *strmSyncState) flushDirCacheSave() {
|
||||||
|
st.mu.Lock()
|
||||||
|
dirty := st.dirCacheDirty
|
||||||
|
st.dirCacheDirty = nil
|
||||||
|
st.mu.Unlock()
|
||||||
|
if len(dirty) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := st.s.repo.StrmDirCache.SetBatch(st.ctx, st.p.ID, dirty); err != nil {
|
||||||
|
st.s.log.Warn("batch save strm dir cache failed",
|
||||||
|
zap.Error(err), zap.Int("count", len(dirty)), zap.String("path_id", st.p.ID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// walk115Flat 使用 115 开放平台扁平化分页批量拉取机制与目录拓扑缓存(参考 QMediaSync)。
|
// walk115Flat 使用 115 开放平台扁平化分页批量拉取机制与目录拓扑缓存(参考 QMediaSync)。
|
||||||
// 极大地降低 API 请求次数并支持毫秒级/秒级增量同步。
|
// 极大地降低 API 请求次数并支持毫秒级/秒级增量同步。
|
||||||
func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
||||||
@@ -721,7 +759,7 @@ func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
|||||||
// 解析相对路径
|
// 解析相对路径
|
||||||
relPath := cleanDirRel(detail.RelativePath(rootCID))
|
relPath := cleanDirRel(detail.RelativePath(rootCID))
|
||||||
st.dirCache.Store(pid, relPath)
|
st.dirCache.Store(pid, relPath)
|
||||||
_ = st.s.repo.StrmDirCache.Set(ctx, st.p.ID, pid, relPath)
|
st.deferDirCacheSave(pid, relPath)
|
||||||
|
|
||||||
// 顺便解析并缓存 detail.Paths 中包含的中间各层级目录
|
// 顺便解析并缓存 detail.Paths 中包含的中间各层级目录
|
||||||
for _, ancestor := range detail.Paths {
|
for _, ancestor := range detail.Paths {
|
||||||
@@ -742,7 +780,7 @@ func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
|||||||
}
|
}
|
||||||
ancestorRel := cleanDirRel(subDetail.RelativePath(rootCID))
|
ancestorRel := cleanDirRel(subDetail.RelativePath(rootCID))
|
||||||
st.dirCache.Store(ancestor.FileId, ancestorRel)
|
st.dirCache.Store(ancestor.FileId, ancestorRel)
|
||||||
_ = st.s.repo.StrmDirCache.Set(ctx, st.p.ID, ancestor.FileId, ancestorRel)
|
st.deferDirCacheSave(ancestor.FileId, ancestorRel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,6 +795,9 @@ func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
pwg.Wait()
|
pwg.Wait()
|
||||||
|
// 目录解析阶段结束即批量落库已解析的缓存:失败路径也保留部分成果,
|
||||||
|
// 下次同步可少解析一批目录。
|
||||||
|
st.flushDirCacheSave()
|
||||||
if firstErr != nil {
|
if firstErr != nil {
|
||||||
// 目录树解析失败会导致 rel 塌缩,若继续处理会让大量本地文件
|
// 目录树解析失败会导致 rel 塌缩,若继续处理会让大量本地文件
|
||||||
// 被错误判定为"云端不存在"而重复下载/上传,并可能误删本地文件。
|
// 被错误判定为"云端不存在"而重复下载/上传,并可能误删本地文件。
|
||||||
@@ -767,10 +808,57 @@ func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
|||||||
|
|
||||||
st.updateSyncMessage(fmt.Sprintf("正在生成 STRM 与同步文件 (共 %d 个)...", len(allFiles)))
|
st.updateSyncMessage(fmt.Sprintf("正在生成 STRM 与同步文件 (共 %d 个)...", len(allFiles)))
|
||||||
|
|
||||||
// 5. 分类处理所有文件
|
// 5. 分类处理所有文件。本地磁盘 I/O(Stat/读内容比对/写盘)远慢于列表
|
||||||
|
// 拉取,串行消化是大库同步的尾部瓶颈(输出目录在网络挂载上尤甚);
|
||||||
|
// processRemoteFile 的共享状态均由 st.mu 保护(walkRemote 已并发调用),
|
||||||
|
// 这里用有界 worker 池并行处理。rel 构建依赖 dirCache 且需在父目录缺失
|
||||||
|
// 时整体中止,保留在生产者侧串行完成。
|
||||||
|
type strmFileTask struct {
|
||||||
|
file cloud115.RemoteFile
|
||||||
|
rel string
|
||||||
|
}
|
||||||
|
fileCh := make(chan strmFileTask)
|
||||||
|
var (
|
||||||
|
procWg sync.WaitGroup
|
||||||
|
procErrMu sync.Mutex
|
||||||
|
procErr error
|
||||||
|
)
|
||||||
|
for i := 0; i < strmProcessWorkers; i++ {
|
||||||
|
procWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer procWg.Done()
|
||||||
|
if err := helper.Recover(st.s.log, "strm.sync.walk115.process", func() error {
|
||||||
|
for t := range fileCh {
|
||||||
|
// 中止(ctx 取消)后排空队列即可,不再产生任何写操作
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := cloud.FileEntry{
|
||||||
|
ID: t.file.FileId,
|
||||||
|
Name: t.file.FileName,
|
||||||
|
IsDir: false,
|
||||||
|
Size: t.file.FileSize,
|
||||||
|
MTime: t.file.Utime,
|
||||||
|
PickCode: t.file.PickCode,
|
||||||
|
Sha1: t.file.Sha1,
|
||||||
|
}
|
||||||
|
st.processRemoteFile(entry, t.rel)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
procErrMu.Lock()
|
||||||
|
if procErr == nil {
|
||||||
|
procErr = err
|
||||||
|
}
|
||||||
|
procErrMu.Unlock()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
feed:
|
||||||
for _, f := range allFiles {
|
for _, f := range allFiles {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
break
|
||||||
}
|
}
|
||||||
cleanName := cleanEntryName(f.FileName, false)
|
cleanName := cleanEntryName(f.FileName, false)
|
||||||
var rel string
|
var rel string
|
||||||
@@ -783,21 +871,27 @@ func (st *strmSyncState) walk115Flat(open115 *cloud115.OpenClient) error {
|
|||||||
// 父目录不在目录缓存,无法还原真实相对路径。若继续用塌缩后的
|
// 父目录不在目录缓存,无法还原真实相对路径。若继续用塌缩后的
|
||||||
// 根路径处理,该文件会被错误判定,导致重复下载/上传或误删本地文件。
|
// 根路径处理,该文件会被错误判定,导致重复下载/上传或误删本地文件。
|
||||||
// 目录树不完整时宁可中止本次同步,也不带着损坏的 rel 继续执行。
|
// 目录树不完整时宁可中止本次同步,也不带着损坏的 rel 继续执行。
|
||||||
return fmt.Errorf("115: 文件 %s 的父目录未解析成功,目录树不完整,中止同步以防误删/误传", cleanName)
|
procErrMu.Lock()
|
||||||
|
if procErr == nil {
|
||||||
|
procErr = fmt.Errorf("115: 文件 %s 的父目录未解析成功,目录树不完整,中止同步以防误删/误传", cleanName)
|
||||||
|
}
|
||||||
|
procErrMu.Unlock()
|
||||||
|
cancel()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry := cloud.FileEntry{
|
select {
|
||||||
ID: f.FileId,
|
case fileCh <- strmFileTask{file: f, rel: rel}:
|
||||||
Name: f.FileName,
|
case <-ctx.Done():
|
||||||
IsDir: false,
|
break feed
|
||||||
Size: f.FileSize,
|
|
||||||
MTime: f.Utime,
|
|
||||||
PickCode: f.PickCode,
|
|
||||||
}
|
}
|
||||||
st.processRemoteFile(entry, rel)
|
|
||||||
}
|
}
|
||||||
|
close(fileCh)
|
||||||
return nil
|
procWg.Wait()
|
||||||
|
if procErr != nil {
|
||||||
|
return procErr
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleVideo 生成/更新 .strm 文件。
|
// handleVideo 生成/更新 .strm 文件。
|
||||||
@@ -933,11 +1027,46 @@ func (st *strmSyncState) strmPathParam(rel string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordRemoteMeta 记录远端存在的元数据索引及文件大小。
|
// usableSha1 归一化远端内容哈希:115 对目录/未完成文件可能返回空串或占位符 "-",均视为不可用。
|
||||||
|
func usableSha1(sha string) string {
|
||||||
|
sha = strings.TrimSpace(sha)
|
||||||
|
if sha == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return sha
|
||||||
|
}
|
||||||
|
|
||||||
|
// localSha1Matches 计算本地文件 SHA1 并与远端哈希做大小写不敏感比对
|
||||||
|
// (115 列表返回大写 hex,本地计算为小写)。读取/哈希失败按"视为同一文件"
|
||||||
|
// 处理,避免瞬时读文件错误触发大规模重复上传/下载。
|
||||||
|
func (st *strmSyncState) localSha1Matches(path, remoteSha1 string) bool {
|
||||||
|
local, err := cloud115.FileSHA1(path)
|
||||||
|
if err != nil {
|
||||||
|
st.s.log.Warn("strm 计算本地元数据 SHA1 失败,按同一文件处理",
|
||||||
|
zap.String("path", path), zap.Error(err))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.EqualFold(local, remoteSha1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordRemoteMeta 记录远端存在的元数据索引、文件大小、文件引用及内容 SHA1。
|
||||||
func (st *strmSyncState) recordRemoteMeta(entry cloud.FileEntry, rel string) {
|
func (st *strmSyncState) recordRemoteMeta(entry cloud.FileEntry, rel string) {
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
|
if st.remoteMeta == nil {
|
||||||
|
st.remoteMeta = map[string]int64{}
|
||||||
|
}
|
||||||
|
if st.remoteMetaRef == nil {
|
||||||
|
st.remoteMetaRef = map[string]string{}
|
||||||
|
}
|
||||||
|
if st.remoteMetaSha1 == nil {
|
||||||
|
st.remoteMetaSha1 = map[string]string{}
|
||||||
|
}
|
||||||
st.seenMeta["m:"+rel] = true
|
st.seenMeta["m:"+rel] = true
|
||||||
st.remoteMeta["m:"+rel] = entry.Size
|
st.remoteMeta["m:"+rel] = entry.Size
|
||||||
|
st.remoteMetaRef["m:"+rel] = entry.ID
|
||||||
|
if sha := usableSha1(entry.Sha1); sha != "" {
|
||||||
|
st.remoteMetaSha1["m:"+rel] = sha
|
||||||
|
}
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,7 +1100,8 @@ func (st *strmSyncState) flushPendingUploads() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleMeta 元数据入下载队列(本地已存在且大小一致则跳过)。
|
// handleMeta 元数据入下载队列。本地已存在时:开启上传元数据则一律跳过(以本地
|
||||||
|
// 为准);否则大小与 SHA1(115 提供)均一致视为同一文件跳过,内容不同则下载覆盖。
|
||||||
func (st *strmSyncState) handleMeta(entry cloud.FileEntry, rel, ext string) {
|
func (st *strmSyncState) handleMeta(entry cloud.FileEntry, rel, ext string) {
|
||||||
st.recordRemoteMeta(entry, rel)
|
st.recordRemoteMeta(entry, rel)
|
||||||
|
|
||||||
@@ -993,9 +1123,22 @@ func (st *strmSyncState) handleMeta(entry cloud.FileEntry, rel, ext string) {
|
|||||||
st.seenMetaTarget[target] = entry
|
st.seenMetaTarget[target] = entry
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
|
|
||||||
if info, err := os.Stat(target); err == nil && info.Size() == entry.Size {
|
if info, err := os.Stat(target); err == nil {
|
||||||
st.touchProgress()
|
// 开启上传元数据时以本地为准:本地已存在的元数据不再用远端版本覆盖,
|
||||||
return
|
// 与网盘版本的差异交给上传队列把本地文件推回网盘,避免下载/上传
|
||||||
|
// 两个队列互相覆盖形成回环。
|
||||||
|
if st.cfg.UploadMeta {
|
||||||
|
st.touchProgress()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 同名同大小:115 提供远端 SHA1 时做内容级比对,网盘更新了同大小
|
||||||
|
// 元数据也能被下载到本地;无哈希(其他网盘/列表未返回)或哈希一致
|
||||||
|
// 视为同一文件跳过。
|
||||||
|
remoteSha := usableSha1(entry.Sha1)
|
||||||
|
if info.Size() == entry.Size && (remoteSha == "" || st.localSha1Matches(target, remoteSha)) {
|
||||||
|
st.touchProgress()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
if st.activeDownloadPaths == nil {
|
if st.activeDownloadPaths == nil {
|
||||||
@@ -1135,6 +1278,8 @@ func (st *strmSyncState) walkLocalSource() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// scanLocalMetaForUpload 扫描本地元数据,与远端比对后入上传队列。
|
// scanLocalMetaForUpload 扫描本地元数据,与远端比对后入上传队列。
|
||||||
|
// 以本地为准:网盘端不存在、同名不同大小、或同名同大小但 SHA1 不同(115 提供
|
||||||
|
// 远端哈希时做内容级比对)均入队覆盖上传;同名同大小同内容视为同一文件跳过。
|
||||||
func (st *strmSyncState) scanLocalMetaForUpload() error {
|
func (st *strmSyncState) scanLocalMetaForUpload() error {
|
||||||
defer st.flushPendingUploads()
|
defer st.flushPendingUploads()
|
||||||
if st.activeUploadPaths == nil {
|
if st.activeUploadPaths == nil {
|
||||||
@@ -1174,12 +1319,22 @@ func (st *strmSyncState) scanLocalMetaForUpload() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
_, exists := st.remoteMeta["m:"+rel]
|
remoteSize, exists := st.remoteMeta["m:"+rel]
|
||||||
|
remoteRef := st.remoteMetaRef["m:"+rel]
|
||||||
|
remoteSha1 := st.remoteMetaSha1["m:"+rel]
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
if exists {
|
if exists && remoteSize == info.Size() {
|
||||||
// 网盘端已存在该元数据文件,跳过上传
|
// 网盘端同名同大小:候选同一文件。115 提供远端 SHA1 时做内容级比对,
|
||||||
return nil
|
// 识别"同大小不同内容"(如 nfo 改一个字符长度不变)避免漏传;
|
||||||
|
// 无哈希(其他网盘/列表未返回)视为同一文件,保持大小比对兜底。
|
||||||
|
if remoteSha1 == "" || st.localSha1Matches(path, remoteSha1) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 入队上传(以本地为准):网盘端不存在;或同名但大小不同(必然内容不同);
|
||||||
|
// 或同名同大小但 SHA1 不同(精确比对发现的同大小不同内容)。
|
||||||
|
// 115 的上传接口不保证同名覆盖,任务携带远端旧文件 ID(RemoteRef),
|
||||||
|
// 由上传端先删旧文件再上传;WebDAV/OpenList 的 PutFile 本身即覆盖上传。
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
if st.activeUploadPaths != nil && st.activeUploadPaths[path] {
|
if st.activeUploadPaths != nil && st.activeUploadPaths[path] {
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
@@ -1200,6 +1355,9 @@ func (st *strmSyncState) scanLocalMetaForUpload() error {
|
|||||||
Size: info.Size(),
|
Size: info.Size(),
|
||||||
Status: model.StrmTaskPending,
|
Status: model.StrmTaskPending,
|
||||||
}
|
}
|
||||||
|
if exists && st.p.Provider == model.StrmProvider115 {
|
||||||
|
task.RemoteRef = remoteRef
|
||||||
|
}
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
st.pendingUploads = append(st.pendingUploads, task)
|
st.pendingUploads = append(st.pendingUploads, task)
|
||||||
shouldFlush := len(st.pendingUploads) >= 100
|
shouldFlush := len(st.pendingUploads) >= 100
|
||||||
@@ -1260,10 +1418,11 @@ func (st *strmSyncState) taskExists(kind, syncPathID, localPath string) bool {
|
|||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// pruneLocal 清理本地多余 .strm 与元数据(远端已不存在),可选删除空目录。
|
// pruneLocal 清理本地多余 .strm(远端已不存在的视频),可选删除空目录。
|
||||||
|
// 元数据文件(nfo/图片/字幕等)一律保留:本地刮削结果不因网盘端缺失而被删除。
|
||||||
func (st *strmSyncState) pruneLocal() error {
|
func (st *strmSyncState) pruneLocal() error {
|
||||||
// 增量同步保护:本次远端扫描不完整(目录详情解析失败 / 文件父路径降级)时,
|
// 增量同步保护:本次远端扫描不完整(目录详情解析失败 / 文件父路径降级)时,
|
||||||
// seenVideo/seenMeta 覆盖不全,按"远端不存在"清理会误删刚下载或已存在的本地文件,
|
// seenVideo 覆盖不全,按"远端不存在"清理会误删刚下载或已存在的本地 .strm,
|
||||||
// 进而触发"下次增量重新下载"的循环。此时跳过清理,仅做进度落库。
|
// 进而触发"下次增量重新下载"的循环。此时跳过清理,仅做进度落库。
|
||||||
if st.syncType == model.StrmSyncTypeIncremental && st.scanIncomplete.Load() {
|
if st.syncType == model.StrmSyncTypeIncremental && st.scanIncomplete.Load() {
|
||||||
st.s.log.Warn("strm 增量同步跳过清理:本次远端扫描不完整,prune 已禁用",
|
st.s.log.Warn("strm 增量同步跳过清理:本次远端扫描不完整,prune 已禁用",
|
||||||
@@ -1295,16 +1454,11 @@ func (st *strmSyncState) pruneLocal() error {
|
|||||||
rel = filepath.ToSlash(rel)
|
rel = filepath.ToSlash(rel)
|
||||||
ext := strings.ToLower(filepath.Ext(rel))
|
ext := strings.ToLower(filepath.Ext(rel))
|
||||||
remove := false
|
remove := false
|
||||||
switch {
|
if ext == ".strm" {
|
||||||
case ext == ".strm":
|
|
||||||
relSansExt := rel[:len(rel)-len(ext)]
|
relSansExt := rel[:len(rel)-len(ext)]
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
remove = !st.seenVideo["v:"+relSansExt]
|
remove = !st.seenVideo["v:"+relSansExt]
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
case st.isMetaExt(ext) && st.cfg.DownloadMeta && !st.cfg.UploadMeta && st.p.Provider != model.StrmProviderLocal:
|
|
||||||
st.mu.Lock()
|
|
||||||
remove = !st.seenMeta["m:"+rel]
|
|
||||||
st.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
if remove {
|
if remove {
|
||||||
if err := os.Remove(path); err == nil {
|
if err := os.Remove(path); err == nil {
|
||||||
|
|||||||
@@ -344,14 +344,19 @@ func TestSanitizePathWithSpecialChars(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestScanLocalMetaForUpload 验证元数据上传比对逻辑:网盘已存在跳过,网盘不存在才入队上传。
|
// TestScanLocalMetaForUpload 验证元数据上传比对逻辑:网盘同名同大小(同一文件)跳过,
|
||||||
|
// 网盘不存在或同名不同大小(内容不同的元数据文件)以本地为准入队上传。
|
||||||
func TestScanLocalMetaForUpload(t *testing.T) {
|
func TestScanLocalMetaForUpload(t *testing.T) {
|
||||||
svc := testStrmService(t)
|
svc := testStrmService(t)
|
||||||
localDir := t.TempDir()
|
localDir := t.TempDir()
|
||||||
|
|
||||||
// 本地有 2 个元数据:poster.jpg 和 fanart.jpg
|
// 本地有 3 个元数据:
|
||||||
|
// poster.jpg — 网盘已有同名同大小 → 同一文件,跳过
|
||||||
|
// fanart.jpg — 网盘没有 → 上传
|
||||||
|
// tvshow.nfo — 网盘已有同名但大小不同(内容不同的元数据文件)→ 以本地为准覆盖上传
|
||||||
writeFile(t, filepath.Join(localDir, "动漫", "poster.jpg"), "poster-data")
|
writeFile(t, filepath.Join(localDir, "动漫", "poster.jpg"), "poster-data")
|
||||||
writeFile(t, filepath.Join(localDir, "动漫", "fanart.jpg"), "fanart-data")
|
writeFile(t, filepath.Join(localDir, "动漫", "fanart.jpg"), "fanart-data")
|
||||||
|
writeFile(t, filepath.Join(localDir, "动漫", "tvshow.nfo"), "local-nfo-data")
|
||||||
|
|
||||||
p := &model.StrmSyncPath{
|
p := &model.StrmSyncPath{
|
||||||
Base: model.Base{ID: "test-path-upload"},
|
Base: model.Base{ID: "test-path-upload"},
|
||||||
@@ -363,35 +368,47 @@ func TestScanLocalMetaForUpload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
st := &strmSyncState{
|
st := &strmSyncState{
|
||||||
s: svc,
|
s: svc,
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
p: p,
|
p: p,
|
||||||
cfg: &strmPathConfig{UploadMeta: true, MetaExt: []string{"jpg", "nfo"}},
|
cfg: &strmPathConfig{UploadMeta: true, MetaExt: []string{"jpg", "nfo"}},
|
||||||
rec: &model.StrmSyncRecord{},
|
rec: &model.StrmSyncRecord{},
|
||||||
seenMeta: map[string]bool{},
|
seenMeta: map[string]bool{},
|
||||||
remoteMeta: map[string]int64{},
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟远端已存在 poster.jpg
|
// 模拟远端已存在 poster.jpg(与本地同一文件)和 tvshow.nfo(与本地不同)
|
||||||
st.remoteMeta["m:动漫/poster.jpg"] = 1000
|
st.remoteMeta["m:动漫/poster.jpg"] = int64(len("poster-data"))
|
||||||
|
st.remoteMeta["m:动漫/tvshow.nfo"] = 999
|
||||||
|
|
||||||
if err := st.scanLocalMetaForUpload(); err != nil {
|
if err := st.scanLocalMetaForUpload(); err != nil {
|
||||||
t.Fatalf("scanLocalMetaForUpload failed: %v", err)
|
t.Fatalf("scanLocalMetaForUpload failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 此时应该只有 fanart.jpg 入队上传,poster.jpg 被跳过
|
// fanart.jpg(网盘缺失)与 tvshow.nfo(网盘版本不同)入队,poster.jpg 跳过
|
||||||
tasks, _, err := svc.repo.StrmUpload.List(context.Background(), "", 1, 10)
|
tasks, _, err := svc.repo.StrmUpload.List(context.Background(), "", 1, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(tasks) != 1 {
|
if len(tasks) != 2 {
|
||||||
t.Fatalf("expected 1 upload task (fanart.jpg), got %d", len(tasks))
|
t.Fatalf("expected 2 upload tasks (fanart.jpg, tvshow.nfo), got %d", len(tasks))
|
||||||
}
|
}
|
||||||
if tasks[0].FileName != "fanart.jpg" {
|
got := map[string]model.StrmUploadTask{}
|
||||||
t.Errorf("expected upload task for fanart.jpg, got %s", tasks[0].FileName)
|
for _, task := range tasks {
|
||||||
|
got[task.FileName] = task
|
||||||
|
}
|
||||||
|
if _, ok := got["fanart.jpg"]; !ok {
|
||||||
|
t.Errorf("expected upload task for fanart.jpg, got %v", taskNames(tasks))
|
||||||
|
}
|
||||||
|
if _, ok := got["tvshow.nfo"]; !ok {
|
||||||
|
t.Errorf("expected upload task for tvshow.nfo (local wins), got %v", taskNames(tasks))
|
||||||
|
}
|
||||||
|
if _, ok := got["poster.jpg"]; ok {
|
||||||
|
t.Errorf("poster.jpg (same size on remote) should be skipped")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再次扫描:fanart.jpg 已经在队列中,应自动去重,不重复入队
|
// 再次扫描:已入队的文件自动去重,不重复入队
|
||||||
if err := st.scanLocalMetaForUpload(); err != nil {
|
if err := st.scanLocalMetaForUpload(); err != nil {
|
||||||
t.Fatalf("second scanLocalMetaForUpload failed: %v", err)
|
t.Fatalf("second scanLocalMetaForUpload failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -399,8 +416,313 @@ func TestScanLocalMetaForUpload(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if len(tasks) != 2 {
|
||||||
|
t.Fatalf("expected still 2 upload tasks after dedup, got %d", len(tasks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanLocalMetaForUpload115CarriesRemoteRef 验证 115 覆盖上传时任务携带
|
||||||
|
// 网盘旧文件 ID(供上传前删除旧文件,避免同名重复),网盘无同名文件时不携带。
|
||||||
|
func TestScanLocalMetaForUpload115CarriesRemoteRef(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
|
||||||
|
// movie.nfo 网盘已有同名但大小不同(需要删除旧文件后覆盖上传)
|
||||||
|
writeFile(t, filepath.Join(localDir, "movie.nfo"), "local-nfo-data")
|
||||||
|
// fresh.nfo 网盘没有(普通上传,不携带 ref)
|
||||||
|
writeFile(t, filepath.Join(localDir, "fresh.nfo"), "fresh-nfo")
|
||||||
|
|
||||||
|
p := &model.StrmSyncPath{
|
||||||
|
Base: model.Base{ID: "test-path-upload-115"},
|
||||||
|
AccountID: "acct-1",
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
RemotePath: "100",
|
||||||
|
LocalPath: localDir,
|
||||||
|
UploadMeta: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
st := &strmSyncState{
|
||||||
|
s: svc,
|
||||||
|
ctx: context.Background(),
|
||||||
|
p: p,
|
||||||
|
cfg: &strmPathConfig{UploadMeta: true, MetaExt: []string{"jpg", "nfo"}},
|
||||||
|
rec: &model.StrmSyncRecord{},
|
||||||
|
seenMeta: map[string]bool{},
|
||||||
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
|
}
|
||||||
|
st.remoteMeta["m:movie.nfo"] = 1
|
||||||
|
st.remoteMetaRef["m:movie.nfo"] = "file-42"
|
||||||
|
|
||||||
|
if err := st.scanLocalMetaForUpload(); err != nil {
|
||||||
|
t.Fatalf("scanLocalMetaForUpload failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, _, err := svc.repo.StrmUpload.List(context.Background(), "", 1, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(tasks) != 2 {
|
||||||
|
t.Fatalf("expected 2 upload tasks, got %d", len(tasks))
|
||||||
|
}
|
||||||
|
for _, task := range tasks {
|
||||||
|
switch task.FileName {
|
||||||
|
case "movie.nfo":
|
||||||
|
if task.RemoteRef != "file-42" {
|
||||||
|
t.Errorf("movie.nfo upload task should carry remote ref %q, got %q", "file-42", task.RemoteRef)
|
||||||
|
}
|
||||||
|
if task.RemotePath != "100" {
|
||||||
|
t.Errorf("movie.nfo upload task remote path = %q, want parent cid %q", task.RemotePath, "100")
|
||||||
|
}
|
||||||
|
case "fresh.nfo":
|
||||||
|
if task.RemoteRef != "" {
|
||||||
|
t.Errorf("fresh.nfo (not on remote) should not carry remote ref, got %q", task.RemoteRef)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected upload task %s", task.FileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func taskNames(tasks []model.StrmUploadTask) []string {
|
||||||
|
names := make([]string, 0, len(tasks))
|
||||||
|
for _, task := range tasks {
|
||||||
|
names = append(names, task.FileName)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPruneLocalKeepsLocalMeta 验证清理规则:远端已删除的视频 .strm 仍会被清理,
|
||||||
|
// 但本地元数据一律保留(即使开启"下载元数据"且未开启"上传元数据"、网盘端没有
|
||||||
|
// 该元数据,也不再删除本地刮削好的 nfo/图片/字幕)。
|
||||||
|
func TestPruneLocalKeepsLocalMeta(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
|
||||||
|
// 阿凡达.strm(对应视频已被网盘删除 → 应清理)+ 阿凡达.nfo(网盘没有 → 保留)
|
||||||
|
writeFile(t, filepath.Join(localDir, "电影", "阿凡达.strm"), "http://test.local:8096/x")
|
||||||
|
writeFile(t, filepath.Join(localDir, "电影", "阿凡达.nfo"), "<local scraped meta/>")
|
||||||
|
writeFile(t, filepath.Join(localDir, "电影", "poster.jpg"), "local-poster")
|
||||||
|
|
||||||
|
p := &model.StrmSyncPath{
|
||||||
|
Base: model.Base{ID: "prune-meta-path"},
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
RemotePath: "0",
|
||||||
|
LocalPath: localDir,
|
||||||
|
DownloadMeta: true,
|
||||||
|
UploadMeta: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
st := &strmSyncState{
|
||||||
|
s: svc,
|
||||||
|
ctx: context.Background(),
|
||||||
|
p: p,
|
||||||
|
cfg: &strmPathConfig{DownloadMeta: true, UploadMeta: false, MetaExt: []string{"nfo", "jpg"}},
|
||||||
|
rec: &model.StrmSyncRecord{},
|
||||||
|
syncType: model.StrmSyncTypeFull,
|
||||||
|
seenVideo: map[string]bool{},
|
||||||
|
seenMeta: map[string]bool{},
|
||||||
|
remoteMeta: map[string]int64{},
|
||||||
|
}
|
||||||
|
// 本次远端扫描既没有看到视频,也没有看到任何元数据
|
||||||
|
if err := st.pruneLocal(); err != nil {
|
||||||
|
t.Fatalf("pruneLocal failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(localDir, "电影", "阿凡达.strm")); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("orphan .strm should be pruned, stat err = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(localDir, "电影", "阿凡达.nfo")); err != nil {
|
||||||
|
t.Fatalf("local meta must be kept even when missing on remote: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(localDir, "电影", "poster.jpg")); err != nil {
|
||||||
|
t.Fatalf("local poster must be kept even when missing on remote: %v", err)
|
||||||
|
}
|
||||||
|
if st.rec.Pruned != 1 {
|
||||||
|
t.Fatalf("expected 1 pruned (strm only), got %d", st.rec.Pruned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleMetaKeepsLocalWhenUploadEnabled 验证下载侧规则:开启"上传元数据"时
|
||||||
|
// 以本地为准——本地已存在的元数据(即使与网盘大小不同)不再入下载队列被网盘版本
|
||||||
|
// 覆盖;本地不存在的元数据仍正常入队下载。
|
||||||
|
func TestHandleMetaKeepsLocalWhenUploadEnabled(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
|
||||||
|
// 本地已有 test.nfo(大小 50,与网盘版本大小 100 不同)
|
||||||
|
writeFile(t, filepath.Join(localDir, "test.nfo"), strings.Repeat("L", 50))
|
||||||
|
|
||||||
|
p := &model.StrmSyncPath{
|
||||||
|
Base: model.Base{ID: "meta-local-wins-path"},
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
RemotePath: "0",
|
||||||
|
LocalPath: localDir,
|
||||||
|
DownloadMeta: true,
|
||||||
|
UploadMeta: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
st := &strmSyncState{
|
||||||
|
s: svc,
|
||||||
|
ctx: context.Background(),
|
||||||
|
p: p,
|
||||||
|
cfg: &strmPathConfig{DownloadMeta: true, UploadMeta: true, MetaExt: []string{"nfo"}},
|
||||||
|
rec: &model.StrmSyncRecord{},
|
||||||
|
seenMeta: map[string]bool{},
|
||||||
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
|
seenMetaTarget: map[string]cloud.FileEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网盘版本与本地不同:本地存在 → 不入下载队列(以本地为准)
|
||||||
|
entry := cloud.FileEntry{ID: "r1", Name: "test.nfo", Size: 100, PickCode: "pc1"}
|
||||||
|
st.handleMeta(entry, "test.nfo", ".nfo")
|
||||||
|
// 网盘独有:本地不存在 → 正常入下载队列
|
||||||
|
missing := cloud.FileEntry{ID: "r2", Name: "absent.nfo", Size: 200, PickCode: "pc2"}
|
||||||
|
st.handleMeta(missing, "absent.nfo", ".nfo")
|
||||||
|
st.flushPendingDownloads()
|
||||||
|
|
||||||
|
tasks, _, err := svc.repo.StrmDownload.List(context.Background(), "", 1, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if len(tasks) != 1 {
|
if len(tasks) != 1 {
|
||||||
t.Fatalf("expected still 1 upload task after dedup, got %d", len(tasks))
|
t.Fatalf("expected only 1 download task (absent.nfo), got %d", len(tasks))
|
||||||
|
}
|
||||||
|
if tasks[0].FileName != "absent.nfo" {
|
||||||
|
t.Fatalf("expected download task for absent.nfo, got %s", tasks[0].FileName)
|
||||||
|
}
|
||||||
|
if st.rec.NewMeta != 1 {
|
||||||
|
t.Fatalf("expected NewMeta = 1, got %d", st.rec.NewMeta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanLocalMetaForUploadSha1Identity 验证 115 远端 SHA1 可用时按内容精确比对:
|
||||||
|
// 同名同大小同内容(大小写不敏感)跳过;同名同大小不同内容以本地为准覆盖上传,
|
||||||
|
// 且任务携带网盘旧文件 ID。纯大小比对无法识别"同大小不同内容"(如 nfo 改一个字符)。
|
||||||
|
func TestScanLocalMetaForUploadSha1Identity(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
|
||||||
|
// same.nfo / diff.nfo 本地与网盘大小均相同:
|
||||||
|
// same.nfo 网盘内容与本地一致(SHA1 相同)→ 同一文件,跳过;
|
||||||
|
// diff.nfo 网盘上是另一个同大小文件(SHA1 不同)→ 以本地为准覆盖上传。
|
||||||
|
writeFile(t, filepath.Join(localDir, "same.nfo"), "same-content")
|
||||||
|
writeFile(t, filepath.Join(localDir, "diff.nfo"), "diff-content")
|
||||||
|
otherFile := filepath.Join(localDir, "other.tmp")
|
||||||
|
writeFile(t, otherFile, "other-content!")
|
||||||
|
|
||||||
|
sameSha, err := cloud115.FileSHA1(filepath.Join(localDir, "same.nfo"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
otherSha, err := cloud115.FileSHA1(otherFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &model.StrmSyncPath{
|
||||||
|
Base: model.Base{ID: "test-path-upload-sha1"},
|
||||||
|
AccountID: "acct-1",
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
RemotePath: "100",
|
||||||
|
LocalPath: localDir,
|
||||||
|
UploadMeta: true,
|
||||||
|
}
|
||||||
|
st := &strmSyncState{
|
||||||
|
s: svc,
|
||||||
|
ctx: context.Background(),
|
||||||
|
p: p,
|
||||||
|
cfg: &strmPathConfig{UploadMeta: true, MetaExt: []string{"nfo"}},
|
||||||
|
rec: &model.StrmSyncRecord{},
|
||||||
|
seenMeta: map[string]bool{},
|
||||||
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
|
remoteMetaSha1: map[string]string{},
|
||||||
|
}
|
||||||
|
// 115 返回大写 SHA1,本地计算为小写:同时验证大小写不敏感比对
|
||||||
|
st.remoteMeta["m:same.nfo"] = int64(len("same-content"))
|
||||||
|
st.remoteMetaSha1["m:same.nfo"] = strings.ToUpper(sameSha)
|
||||||
|
st.remoteMeta["m:diff.nfo"] = int64(len("diff-content"))
|
||||||
|
st.remoteMetaSha1["m:diff.nfo"] = strings.ToUpper(otherSha)
|
||||||
|
st.remoteMetaRef["m:diff.nfo"] = "old-diff-1"
|
||||||
|
|
||||||
|
if err := st.scanLocalMetaForUpload(); err != nil {
|
||||||
|
t.Fatalf("scanLocalMetaForUpload failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, _, err := svc.repo.StrmUpload.List(context.Background(), "", 1, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Fatalf("expected 1 upload task (diff.nfo), got %d: %v", len(tasks), taskNames(tasks))
|
||||||
|
}
|
||||||
|
if tasks[0].FileName != "diff.nfo" {
|
||||||
|
t.Fatalf("expected upload task for diff.nfo, got %s", tasks[0].FileName)
|
||||||
|
}
|
||||||
|
if tasks[0].RemoteRef != "old-diff-1" {
|
||||||
|
t.Fatalf("diff.nfo task should carry remote ref %q, got %q", "old-diff-1", tasks[0].RemoteRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleMetaSha1Identity 验证下载侧(未开启上传时镜像网盘元数据):
|
||||||
|
// 同名同大小同 SHA1 跳过;网盘更新了同大小元数据(SHA1 不同)仍会下载;
|
||||||
|
// 网盘未返回哈希时退回大小比对。
|
||||||
|
func TestHandleMetaSha1Identity(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
|
||||||
|
writeFile(t, filepath.Join(localDir, "same.nfo"), "identical-data") // 与网盘内容一致
|
||||||
|
writeFile(t, filepath.Join(localDir, "stale.nfo"), "stale-content!!") // 网盘已更新为同大小新内容
|
||||||
|
writeFile(t, filepath.Join(localDir, "nohash.nfo"), "nohash-content") // 网盘未返回 SHA1
|
||||||
|
otherFile := filepath.Join(localDir, "other.tmp")
|
||||||
|
writeFile(t, otherFile, "totally-newdata")
|
||||||
|
|
||||||
|
sameSha, err := cloud115.FileSHA1(filepath.Join(localDir, "same.nfo"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
otherSha, err := cloud115.FileSHA1(otherFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &model.StrmSyncPath{
|
||||||
|
Base: model.Base{ID: "meta-sha1-path"},
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
RemotePath: "0",
|
||||||
|
LocalPath: localDir,
|
||||||
|
DownloadMeta: true,
|
||||||
|
UploadMeta: false,
|
||||||
|
}
|
||||||
|
st := &strmSyncState{
|
||||||
|
s: svc,
|
||||||
|
ctx: context.Background(),
|
||||||
|
p: p,
|
||||||
|
cfg: &strmPathConfig{DownloadMeta: true, UploadMeta: false, MetaExt: []string{"nfo"}},
|
||||||
|
rec: &model.StrmSyncRecord{},
|
||||||
|
seenMeta: map[string]bool{},
|
||||||
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
|
remoteMetaSha1: map[string]string{},
|
||||||
|
seenMetaTarget: map[string]cloud.FileEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
st.handleMeta(cloud.FileEntry{ID: "r1", Name: "same.nfo", Size: int64(len("identical-data")), Sha1: strings.ToUpper(sameSha), PickCode: "pc1"}, "same.nfo", ".nfo")
|
||||||
|
st.handleMeta(cloud.FileEntry{ID: "r2", Name: "stale.nfo", Size: int64(len("stale-content!!")), Sha1: strings.ToUpper(otherSha), PickCode: "pc2"}, "stale.nfo", ".nfo")
|
||||||
|
st.handleMeta(cloud.FileEntry{ID: "r3", Name: "nohash.nfo", Size: int64(len("nohash-content")), PickCode: "pc3"}, "nohash.nfo", ".nfo")
|
||||||
|
st.flushPendingDownloads()
|
||||||
|
|
||||||
|
tasks, _, err := svc.repo.StrmDownload.List(context.Background(), "", 1, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Fatalf("expected only 1 download task (stale.nfo), got %d", len(tasks))
|
||||||
|
}
|
||||||
|
if tasks[0].FileName != "stale.nfo" {
|
||||||
|
t.Fatalf("expected download task for stale.nfo, got %s", tasks[0].FileName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +994,7 @@ func TestWalk115FlatAbortsOnDirResolveFailure(t *testing.T) {
|
|||||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/open/ufile/files":
|
case "/open/ufile/files":
|
||||||
w.Write([]byte(`{"state":true,"count":1,"data":[{"fid":"100","pid":"999","fc":1,"fn":"movie.mkv","pc":"pc1","upt":1700000000,"fs":1024}]}`))
|
w.Write([]byte(`{"state":true,"count":1,"data":[{"fid":"100","pid":"999","fc":"1","fn":"movie.mkv","pc":"pc1","upt":1700000000,"fs":1024}]}`))
|
||||||
case "/open/folder/get_info":
|
case "/open/folder/get_info":
|
||||||
w.Write([]byte(`{"state":false,"code":40140123,"message":"access_token 格式错误"}`))
|
w.Write([]byte(`{"state":false,"code":40140123,"message":"access_token 格式错误"}`))
|
||||||
default:
|
default:
|
||||||
@@ -716,3 +1038,126 @@ func TestWalk115FlatAbortsOnDirResolveFailure(t *testing.T) {
|
|||||||
t.Fatalf("expected no .strm written after abort, got %d", strmCount)
|
t.Fatalf("expected no .strm written after abort, got %d", strmCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWalk115FlatConcurrentProcessing 验证 115 平铺拉取后文件分类处理走并发
|
||||||
|
// worker 池:同一父目录下多个视频的 strm 全部生成,且解析出的目录拓扑缓存
|
||||||
|
// 通过 SetBatch 批量落库,供下次增量同步预加载(省掉重复的 get_info 调用)。
|
||||||
|
func TestWalk115FlatConcurrentProcessing(t *testing.T) {
|
||||||
|
svc := testStrmService(t)
|
||||||
|
localDir := t.TempDir()
|
||||||
|
acct := &model.StrmAccount{Name: "fake115", Provider: "cloud115", Config: "{}", Enabled: true}
|
||||||
|
if err := svc.repo.StrmAccount.Create(context.Background(), acct); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p := &model.StrmSyncPath{
|
||||||
|
Base: model.Base{ID: "flat-path"},
|
||||||
|
AccountID: acct.ID,
|
||||||
|
Provider: model.StrmProvider115,
|
||||||
|
RemotePath: "0",
|
||||||
|
LocalPath: localDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/open/ufile/files":
|
||||||
|
w.Write([]byte(`{"state":true,"count":3,"data":[
|
||||||
|
{"fid":"101","pid":"999","fc":"1","fn":"m1.mkv","pc":"pc1","upt":1700000001,"fs":1024},
|
||||||
|
{"fid":"102","pid":"999","fc":"1","fn":"m2.mkv","pc":"pc2","upt":1700000002,"fs":2048},
|
||||||
|
{"fid":"103","pid":"999","fc":"1","fn":"m3.mkv","pc":"pc3","upt":1700000003,"fs":4096}]}`))
|
||||||
|
case "/open/folder/get_info":
|
||||||
|
w.Write([]byte(`{"state":true,"data":{"file_id":"999","file_name":"Movies","file_category":"0",
|
||||||
|
"paths":[{"file_id":"0","file_name":"根目录"},{"file_id":"999","file_name":"Movies"}]}}`))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer api.Close()
|
||||||
|
oldPro := cloud115.ProAPIBase
|
||||||
|
cloud115.ProAPIBase = api.URL
|
||||||
|
defer func() { cloud115.ProAPIBase = oldPro }()
|
||||||
|
|
||||||
|
oc := cloud115.NewOpenClient("app", "at", "rt")
|
||||||
|
st := &strmSyncState{
|
||||||
|
s: svc,
|
||||||
|
ctx: context.Background(),
|
||||||
|
p: p,
|
||||||
|
provider: cloud.NewOpenAPI115("app", "at", "rt"),
|
||||||
|
cfg: &strmPathConfig{VideoExt: []string{"mkv"}, MetaExt: []string{"nfo"}, AddPath: 1},
|
||||||
|
rec: &model.StrmSyncRecord{},
|
||||||
|
syncType: model.StrmSyncTypeFull,
|
||||||
|
dirCache: sync.Map{},
|
||||||
|
seenVideo: map[string]bool{},
|
||||||
|
seenMeta: map[string]bool{},
|
||||||
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
|
remoteMetaSha1: map[string]string{},
|
||||||
|
seenMetaTarget: map[string]cloud.FileEntry{},
|
||||||
|
seenVideoTarget: map[string]cloud.FileEntry{},
|
||||||
|
}
|
||||||
|
if err := st.walk115Flat(oc); err != nil {
|
||||||
|
t.Fatalf("walk115Flat: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3 个视频的 strm 全部生成
|
||||||
|
if st.rec.NewStrm != 3 {
|
||||||
|
t.Fatalf("expected 3 strm created, got %d", st.rec.NewStrm)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"m1.strm", "m2.strm", "m3.strm"} {
|
||||||
|
if _, err := os.Stat(filepath.Join(localDir, "Movies", name)); err != nil {
|
||||||
|
t.Fatalf("strm %s missing: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目录拓扑缓存已批量落库
|
||||||
|
rows, err := svc.repo.StrmDirCache.ListBySyncPathID(context.Background(), p.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rows) != 1 || rows[0].DirID != "999" || rows[0].Path != "Movies" {
|
||||||
|
t.Fatalf("dir cache rows = %#v, want one row for dir 999 -> Movies", rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增量同步复用目录缓存:不再发起 get_info 调用
|
||||||
|
var infoCalls int
|
||||||
|
api.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/open/ufile/files":
|
||||||
|
w.Write([]byte(`{"state":true,"count":3,"data":[
|
||||||
|
{"fid":"101","pid":"999","fc":"1","fn":"m1.mkv","pc":"pc1","upt":1700000001,"fs":1024},
|
||||||
|
{"fid":"102","pid":"999","fc":"1","fn":"m2.mkv","pc":"pc2","upt":1700000002,"fs":2048},
|
||||||
|
{"fid":"103","pid":"999","fc":"1","fn":"m3.mkv","pc":"pc3","upt":1700000003,"fs":4096}]}`))
|
||||||
|
case "/open/folder/get_info":
|
||||||
|
infoCalls++
|
||||||
|
w.Write([]byte(`{"state":true,"data":{"file_id":"999","file_name":"Movies","file_category":"0","paths":[]}}`))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
st2 := &strmSyncState{
|
||||||
|
s: svc,
|
||||||
|
ctx: context.Background(),
|
||||||
|
p: p,
|
||||||
|
provider: cloud.NewOpenAPI115("app", "at", "rt"),
|
||||||
|
cfg: st.cfg,
|
||||||
|
rec: &model.StrmSyncRecord{},
|
||||||
|
syncType: model.StrmSyncTypeIncremental,
|
||||||
|
dirCache: sync.Map{},
|
||||||
|
seenVideo: map[string]bool{},
|
||||||
|
seenMeta: map[string]bool{},
|
||||||
|
remoteMeta: map[string]int64{},
|
||||||
|
remoteMetaRef: map[string]string{},
|
||||||
|
remoteMetaSha1: map[string]string{},
|
||||||
|
seenMetaTarget: map[string]cloud.FileEntry{},
|
||||||
|
seenVideoTarget: map[string]cloud.FileEntry{},
|
||||||
|
}
|
||||||
|
if err := st2.walk115Flat(oc); err != nil {
|
||||||
|
t.Fatalf("incremental walk115Flat: %v", err)
|
||||||
|
}
|
||||||
|
if infoCalls != 0 {
|
||||||
|
t.Fatalf("incremental sync should reuse dir cache, got %d get_info calls", infoCalls)
|
||||||
|
}
|
||||||
|
if st2.rec.NewStrm != 0 || st2.rec.Skipped != 3 {
|
||||||
|
t.Fatalf("incremental sync should skip all, new = %d skipped = %d", st2.rec.NewStrm, st2.rec.Skipped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user