mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 12:23:03 +00:00
feat: 添加 WPS 云文档/WPS 云盘解析支持 (closes #133)
- 新增 PwpsTool 解析器,支持 WPS 云文档直链获取 - 调用 WPS API: https://www.kdocs.cn/api/office/file/{shareKey}/download - 前端添加 kdocs.cn 链接识别规则 - 前端预览功能优化:WPS 云文档直接使用原分享链接预览 - 后端预览接口特殊处理:判断 shareKey 以 pwps: 开头自动重定向 - 支持提取文件名和有效期信息 - 更新 README 文档,添加 WPS 云文档支持说明 Parser 模块设计: - 遵循开放封闭原则,易于扩展新网盘 - 只需实现 IPanTool 接口和注册枚举即可 - 支持自定义域名解析和责任链模式 技术特性: - 免登录获取下载直链 - 支持在线预览(利用 WPS 原生功能) - 文件大小限制:10M(免费版)/2G(会员版) - 初始空间:5G(免费版)
This commit is contained in:
@@ -7,7 +7,6 @@ NFD 解析器模块:聚合各类网盘/分享页解析,统一输出文件列
|
||||
- 模块版本:10.1.17
|
||||
|
||||
## 依赖(Maven Central)
|
||||
- Maven(无需额外仓库配置):
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>cn.qaiu</groupId>
|
||||
|
||||
@@ -266,6 +266,12 @@ public enum PanDomainTemplate {
|
||||
compile("https://pan-yz\\.cldisk\\.com/external/m/file/(?<KEY>\\w+)"),
|
||||
"https://pan-yz.cldisk.com/external/m/file/{shareKey}",
|
||||
PcxTool.class),
|
||||
// WPS:分享格式:https://www.kdocs.cn/l/ck0azivLlDi3 ;API格式:https://www.kdocs.cn/api/office/file/{shareKey}/download
|
||||
// 响应:{download_url: "https://hwc-bj.ag.kdocs.cn/api/xx",url: "",fize: 0,fver: 0,store: ""}
|
||||
PWPS("WPS",
|
||||
compile("https://www\\.kdocs\\.cn/l/(?<KEY>.+)"),
|
||||
"https://www.kdocs.cn/l/{shareKey}",
|
||||
PwpsTool.class),
|
||||
// =====================音乐类解析 分享链接标志->MxxS (单歌曲/普通音质)==========================
|
||||
// http://163cn.tv/xxx
|
||||
MNES("网易云音乐分享",
|
||||
|
||||
63
parser/src/main/java/cn/qaiu/parser/impl/PwpsTool.java
Normal file
63
parser/src/main/java/cn/qaiu/parser/impl/PwpsTool.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
/**
|
||||
* <a href="https://www.kdocs.cn/">WPS云文档</a>
|
||||
* 分享格式:https://www.kdocs.cn/l/ck0azivLlDi3
|
||||
* API格式:https://www.kdocs.cn/api/office/file/{shareKey}/download
|
||||
* 响应:{download_url: "https://hwc-bj.ag.kdocs.cn/api/xx",url: "",fize: 0,fver: 0,store: ""}
|
||||
*/
|
||||
public class PwpsTool extends PanBase {
|
||||
private static final String API_URL_TEMPLATE = "https://www.kdocs.cn/api/office/file/%s/download";
|
||||
|
||||
public PwpsTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
final String shareKey = shareLinkInfo.getShareKey();
|
||||
|
||||
// 构建API URL
|
||||
String apiUrl = String.format(API_URL_TEMPLATE, shareKey);
|
||||
|
||||
// 发送GET请求到WPS API
|
||||
client.getAbs(apiUrl)
|
||||
.send()
|
||||
.onSuccess(res -> {
|
||||
try {
|
||||
JsonObject resJson = asJson(res);
|
||||
|
||||
// 检查响应是否包含download_url字段
|
||||
if (resJson.containsKey("download_url")) {
|
||||
String downloadUrl = resJson.getString("download_url");
|
||||
|
||||
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||
log.info("WPS云文档解析成功: shareKey={}, downloadUrl={}", shareKey, downloadUrl);
|
||||
promise.complete(downloadUrl);
|
||||
} else {
|
||||
fail("download_url字段为空");
|
||||
}
|
||||
} else {
|
||||
// 检查是否有错误信息
|
||||
if (resJson.containsKey("error") || resJson.containsKey("msg")) {
|
||||
String errorMsg = resJson.getString("error", resJson.getString("msg", "未知错误"));
|
||||
fail("API返回错误: {}", errorMsg);
|
||||
} else {
|
||||
fail("响应中未找到download_url字段, 响应内容: {}", resJson.encodePrettily());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
fail(e, "解析响应JSON失败");
|
||||
}
|
||||
})
|
||||
.onFailure(handleFail(apiUrl));
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
156
parser/src/test/java/cn/qaiu/parser/impl/WpsPanTest.java
Normal file
156
parser/src/test/java/cn/qaiu/parser/impl/WpsPanTest.java
Normal file
@@ -0,0 +1,156 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import cn.qaiu.parser.ParserCreate;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* WPS 云文档解析测试
|
||||
*/
|
||||
public class WpsPanTest {
|
||||
|
||||
@Test
|
||||
public void testWpsDownload() throws InterruptedException {
|
||||
System.out.println("======= WPS 云文档解析测试 =======");
|
||||
|
||||
// 测试链接:reset_navicat_mac
|
||||
String wpsUrl = "https://www.kdocs.cn/l/ck0azivLlDi3";
|
||||
|
||||
System.out.println("测试链接: " + wpsUrl);
|
||||
System.out.println("文件名称: reset_navicat_mac");
|
||||
System.out.println();
|
||||
|
||||
// 使用 ParserCreate 方式创建解析器
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(wpsUrl);
|
||||
|
||||
System.out.println("解析器类型: " + parserCreate.getShareLinkInfo().getType());
|
||||
System.out.println("网盘名称: " + parserCreate.getShareLinkInfo().getPanName());
|
||||
System.out.println("分享Key: " + parserCreate.getShareLinkInfo().getShareKey());
|
||||
System.out.println("标准URL: " + parserCreate.getShareLinkInfo().getStandardUrl());
|
||||
System.out.println();
|
||||
|
||||
System.out.println("开始解析下载链接...");
|
||||
|
||||
// 创建工具并解析
|
||||
parserCreate.createTool()
|
||||
.parse()
|
||||
.onSuccess(downloadUrl -> {
|
||||
System.out.println("✓ 解析成功!");
|
||||
System.out.println("下载直链: " + downloadUrl);
|
||||
System.out.println();
|
||||
|
||||
// 解析文件信息
|
||||
JsonObject fileInfo = getFileInfo(downloadUrl);
|
||||
System.out.println("文件信息: " + fileInfo.encodePrettily());
|
||||
System.out.println();
|
||||
})
|
||||
.onFailure(error -> {
|
||||
System.err.println("✗ 解析失败!");
|
||||
System.err.println("错误信息: " + error.getMessage());
|
||||
error.printStackTrace();
|
||||
});
|
||||
|
||||
// 等待异步结果
|
||||
System.out.println("等待解析结果...");
|
||||
TimeUnit.SECONDS.sleep(10);
|
||||
|
||||
System.out.println("======= 测试结束 =======");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWpsWithShareKey() throws InterruptedException {
|
||||
System.out.println("======= WPS 云文档解析测试 (使用 shareKey) =======");
|
||||
|
||||
String shareKey = "ck0azivLlDi3";
|
||||
|
||||
System.out.println("分享Key: " + shareKey);
|
||||
System.out.println();
|
||||
|
||||
// 使用 fromType + shareKey 方式
|
||||
ParserCreate parserCreate = ParserCreate.fromType("pwps")
|
||||
.shareKey(shareKey);
|
||||
|
||||
System.out.println("解析器类型: " + parserCreate.getShareLinkInfo().getType());
|
||||
System.out.println("网盘名称: " + parserCreate.getShareLinkInfo().getPanName());
|
||||
System.out.println("标准URL: " + parserCreate.getShareLinkInfo().getStandardUrl());
|
||||
System.out.println();
|
||||
|
||||
System.out.println("开始解析下载链接...");
|
||||
|
||||
// 创建工具并解析
|
||||
parserCreate.createTool()
|
||||
.parse()
|
||||
.onSuccess(downloadUrl -> {
|
||||
System.out.println("✓ 解析成功!");
|
||||
System.out.println("下载直链: " + downloadUrl);
|
||||
System.out.println();
|
||||
|
||||
// 解析文件信息
|
||||
JsonObject fileInfo = getFileInfo(downloadUrl);
|
||||
System.out.println("文件信息: " + fileInfo.encodePrettily());
|
||||
System.out.println();
|
||||
})
|
||||
.onFailure(error -> {
|
||||
System.err.println("✗ 解析失败!");
|
||||
System.err.println("错误信息: " + error.getMessage());
|
||||
error.printStackTrace();
|
||||
});
|
||||
|
||||
// 等待异步结果
|
||||
System.out.println("等待解析结果...");
|
||||
TimeUnit.SECONDS.sleep(10);
|
||||
|
||||
System.out.println("======= 测试结束 =======");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 WPS 下载直链中提取文件信息
|
||||
* 示例链接: https://hwc-bj.ag.kdocs.cn/api/object/xxx/compatible?response-content-disposition=attachment%3Bfilename%2A%3Dutf-8%27%27reset_navicat_mac.sh&AccessKeyId=xxx&Expires=1760928746&Signature=xxx
|
||||
*
|
||||
* @param downloadUrl WPS 下载直链
|
||||
* @return JSON 格式的文件信息 {fileName: "reset_navicat_mac.sh", expire: "2025-10-20 10:45:46"}
|
||||
*/
|
||||
private JsonObject getFileInfo(String downloadUrl) {
|
||||
String fileName = "未知文件";
|
||||
String expireTime = "未知";
|
||||
|
||||
try {
|
||||
// 1. 提取文件名 - 从 response-content-disposition 参数中提取
|
||||
// 格式: attachment%3Bfilename%2A%3Dutf-8%27%27reset_navicat_mac.sh
|
||||
// 解码后: attachment;filename*=utf-8''reset_navicat_mac.sh
|
||||
Pattern fileNamePattern = Pattern.compile("filename[^=]*=(?:utf-8'')?([^&]+)");
|
||||
Matcher fileNameMatcher = fileNamePattern.matcher(URLDecoder.decode(downloadUrl, StandardCharsets.UTF_8));
|
||||
if (fileNameMatcher.find()) {
|
||||
fileName = fileNameMatcher.group(1);
|
||||
// 再次解码(可能被双重编码)
|
||||
fileName = URLDecoder.decode(fileName, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// 2. 提取有效期 - 从 Expires 参数中提取 Unix timestamp
|
||||
Pattern expiresPattern = Pattern.compile("[?&]Expires=([0-9]+)");
|
||||
Matcher expiresMatcher = expiresPattern.matcher(downloadUrl);
|
||||
if (expiresMatcher.find()) {
|
||||
long timestamp = Long.parseLong(expiresMatcher.group(1));
|
||||
// 转换为日期格式 yyyy-MM-dd HH:mm:ss
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
expireTime = sdf.format(new Date(timestamp * 1000L)); // Unix timestamp 是秒,需要转毫秒
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("解析文件信息失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return JsonObject.of("fileName", fileName, "expire", expireTime);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user