Merge pull request #136 from qaiu/copilot/add-ce4tool-parser

Add Cloudreve 4.x API support with Ce4Tool parser
This commit is contained in:
qaiu
2025-11-13 18:18:30 +08:00
committed by GitHub
3 changed files with 321 additions and 26 deletions

View File

@@ -0,0 +1,73 @@
package cn.qaiu.vx.core.verticle.conf;
import io.vertx.core.json.JsonObject;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.impl.JsonUtil;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
/**
* Converter and mapper for {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf}.
* NOTE: This class has been automatically generated from the {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf} original class using Vert.x codegen.
*/
public class HttpProxyConfConverter {
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpProxyConf obj) {
for (java.util.Map.Entry<String, Object> member : json) {
switch (member.getKey()) {
case "password":
if (member.getValue() instanceof String) {
obj.setPassword((String)member.getValue());
}
break;
case "port":
if (member.getValue() instanceof Number) {
obj.setPort(((Number)member.getValue()).intValue());
}
break;
case "preProxyOptions":
if (member.getValue() instanceof JsonObject) {
obj.setPreProxyOptions(new io.vertx.core.net.ProxyOptions((io.vertx.core.json.JsonObject)member.getValue()));
}
break;
case "timeout":
if (member.getValue() instanceof Number) {
obj.setTimeout(((Number)member.getValue()).intValue());
}
break;
case "username":
if (member.getValue() instanceof String) {
obj.setUsername((String)member.getValue());
}
break;
}
}
}
static void toJson(HttpProxyConf obj, JsonObject json) {
toJson(obj, json.getMap());
}
static void toJson(HttpProxyConf obj, java.util.Map<String, Object> json) {
if (obj.getPassword() != null) {
json.put("password", obj.getPassword());
}
if (obj.getPort() != null) {
json.put("port", obj.getPort());
}
if (obj.getPreProxyOptions() != null) {
json.put("preProxyOptions", obj.getPreProxyOptions().toJson());
}
if (obj.getTimeout() != null) {
json.put("timeout", obj.getTimeout());
}
if (obj.getUsername() != null) {
json.put("username", obj.getUsername());
}
}
}

View File

@@ -0,0 +1,136 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import java.net.URL;
/**
* <a href="https://github.com/cloudreve/Cloudreve">Cloudreve 4.x 自建网盘解析</a> <br>
* Cloudreve 4.x API 版本解析器 <br>
* 此解析器专门处理Cloudreve 4.x版本的API使用新的下载流程
*/
public class Ce4Tool extends PanBase {
// Cloudreve 4.x uses /api/v3/ prefix for most APIs
private static final String FILE_URL_API_PATH = "/api/v3/file/url";
private static final String SHARE_API_PATH = "/api/v3/share/info/";
public Ce4Tool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
public Future<String> parse() {
String key = shareLinkInfo.getShareKey();
String pwd = shareLinkInfo.getSharePassword();
try {
URL url = new URL(shareLinkInfo.getShareUrl());
String baseUrl = url.getProtocol() + "://" + url.getHost();
// 获取分享信息
getShareInfo(baseUrl, key, pwd);
} catch (Exception e) {
fail(e, "URL解析错误");
}
return promise.future();
}
/**
* 获取Cloudreve 4.x分享信息
*/
private void getShareInfo(String baseUrl, String key, String pwd) {
String shareApiUrl = baseUrl + SHARE_API_PATH + key;
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl);
if (pwd != null && !pwd.isEmpty()) {
httpRequest.addQueryParam("password", pwd);
}
httpRequest.send().onSuccess(res -> {
try {
if (res.statusCode() == 200) {
JsonObject jsonObject = asJson(res);
if (jsonObject.containsKey("code")) {
int code = jsonObject.getInteger("code");
if (code == 0) {
// 成功,获取文件信息和下载链接
JsonObject data = jsonObject.getJsonObject("data");
if (data != null) {
// 获取文件路径,如果没有则使用默认路径
String filePath = "/";
if (data.containsKey("path")) {
filePath = data.getString("path");
}
// 对于4.x需要通过 POST /api/v3/file/url 获取下载链接
getDownloadUrl(baseUrl, filePath);
} else {
fail("分享信息获取失败: data字段为空");
}
} else {
// 错误码,可能是密码错误或分享失效
String msg = jsonObject.getString("msg", "未知错误");
fail("分享验证失败: {}", msg);
}
} else {
// 响应格式不符合预期
fail("响应格式不符合Cloudreve 4.x规范");
}
} else {
// HTTP错误
fail("获取分享信息失败: HTTP {}", res.statusCode());
}
} catch (Exception e) {
fail(e, "解析分享信息响应失败");
}
}).onFailure(handleFail(shareApiUrl));
}
/**
* 通过 POST /api/v3/file/url 获取下载链接 (Cloudreve 4.x API)
*/
private void getDownloadUrl(String baseUrl, String filePath) {
String fileUrlApi = baseUrl + FILE_URL_API_PATH;
// 准备Cloudreve 4.x的请求体
JsonObject requestBody = new JsonObject()
.put("uris", new JsonArray().add(filePath))
.put("download", true);
clientSession.postAbs(fileUrlApi)
.putHeader("Content-Type", "application/json")
.sendJsonObject(requestBody)
.onSuccess(res -> {
try {
if (res.statusCode() == 200) {
JsonObject jsonObject = asJson(res);
if (jsonObject.containsKey("urls")) {
JsonArray urls = jsonObject.getJsonArray("urls");
if (urls != null && urls.size() > 0) {
JsonObject urlObj = urls.getJsonObject(0);
String downloadUrl = urlObj.getString("url");
if (downloadUrl != null && !downloadUrl.isEmpty()) {
promise.complete(downloadUrl);
} else {
fail("下载链接为空");
}
} else {
fail("下载链接列表为空");
}
} else {
fail("响应中不包含urls字段: {}", jsonObject.encodePrettily());
}
} else {
fail("获取下载链接失败: HTTP {}", res.statusCode());
}
} catch (Exception e) {
fail(e, "解析下载链接响应失败");
}
}).onFailure(handleFail(fileUrlApi));
}
}

View File

@@ -15,6 +15,7 @@ import java.net.URL;
* <a href="https://pan.huang1111.cn">huang1111</a> <br>
* <a href="https://pan.seeoss.com">看见存储</a> <br>
* <a href="https://dav.yiandrive.com">亿安云盘</a> <br>
* Cloudreve 3.x 解析器会自动检测版本并在4.x时转发到Ce4Tool
*/
public class CeTool extends PanBase {
@@ -22,6 +23,9 @@ public class CeTool extends PanBase {
// api/v3/share/info/g31PcQ?password=qaiu
private static final String SHARE_API_PATH = "/api/v3/share/info/";
private static final String PING_API_V3_PATH = "/api/v3/site/ping";
private static final String PING_API_V4_PATH = "/api/v4/site/ping";
public CeTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
@@ -31,39 +35,121 @@ public class CeTool extends PanBase {
public Future<String> parse() {
String key = shareLinkInfo.getShareKey();
String pwd = shareLinkInfo.getSharePassword();
// https://pan.huang1111.cn/s/wDz5TK
// https://pan.huang1111.cn/s/y12bI6 -> https://pan.huang1111
// .cn/api/v3/share/download/y12bI6?path=undefined%2Fundefined;
// 类型解析 -> /ce/pan.huang1111.cn_s_wDz5TK
// parser接口 -> /parser?url=https://pan.huang1111.cn/s/wDz5TK
try {
// // 处理URL
URL url = new URL(shareLinkInfo.getShareUrl());
String downloadApiUrl = url.getProtocol() + "://" + url.getHost() + DOWNLOAD_API_PATH + key + "?path" +
"=undefined/undefined;";
String shareApiUrl = url.getProtocol() + "://" + url.getHost() + SHARE_API_PATH + key;
// 设置cookie
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl);
if (pwd != null) {
httpRequest.addQueryParam("password", pwd);
}
// 获取下载链接
httpRequest.send().onSuccess(res -> {
try {
if (res.statusCode() == 200 && res.bodyAsJsonObject().containsKey("code")) {
getDownURL(downloadApiUrl);
} else {
nextParser();
}
} catch (Exception e) {
nextParser();
}
}).onFailure(handleFail(shareApiUrl));
String baseUrl = url.getProtocol() + "://" + url.getHost();
// 先检测API版本
detectVersionAndParse(baseUrl, key, pwd);
} catch (Exception e) {
fail(e, "URL解析错误");
}
return promise.future();
}
/**
* 检测Cloudreve版本并选择合适的解析器
* 先调用 /api/v3/site/ping 判断哪个API 如果/v3 或者/v4 能查询到json响应可以判断是哪个版本
* 不然返回404说明不是ce盘直接nextParser
*/
private void detectVersionAndParse(String baseUrl, String key, String pwd) {
String pingUrlV3 = baseUrl + PING_API_V3_PATH;
// 先尝试v3 ping
clientSession.getAbs(pingUrlV3).send().onSuccess(res -> {
if (res.statusCode() == 200) {
try {
asJson(res);
// v3 ping成功可能是3.x或4.x尝试3.x的download API来判断
String shareApiUrl = baseUrl + SHARE_API_PATH + key;
String downloadApiUrl = baseUrl + DOWNLOAD_API_PATH + key + "?path=undefined/undefined;";
checkIfV3(shareApiUrl, downloadApiUrl, pwd);
} catch (Exception e) {
// JSON解析失败尝试v4 ping
tryV4Ping(baseUrl, key, pwd);
}
} else if (res.statusCode() == 404) {
// v3 ping不存在尝试v4
tryV4Ping(baseUrl, key, pwd);
} else {
// 其他错误不是Cloudreve盘
nextParser();
}
}).onFailure(t -> {
// 网络错误或不可达尝试v4 ping
tryV4Ping(baseUrl, key, pwd);
});
}
private void tryV4Ping(String baseUrl, String key, String pwd) {
String pingUrlV4 = baseUrl + PING_API_V4_PATH;
clientSession.getAbs(pingUrlV4).send().onSuccess(res -> {
if (res.statusCode() == 200) {
try {
asJson(res);
// v4 ping成功使用Ce4Tool
delegateToCe4Tool();
} catch (Exception e) {
// JSON解析失败不是Cloudreve盘
nextParser();
}
} else {
// v4 ping失败不是Cloudreve盘
nextParser();
}
}).onFailure(t -> {
// 网络错误,尝试下一个解析器
nextParser();
});
}
/**
* 检查是否是3.x版本通过尝试调用3.x的API
*/
private void checkIfV3(String shareApiUrl, String downloadApiUrl, String pwd) {
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl);
if (pwd != null) {
httpRequest.addQueryParam("password", pwd);
}
httpRequest.send().onSuccess(res -> {
try {
if (res.statusCode() == 200 && res.bodyAsJsonObject().containsKey("code")) {
// share API成功尝试download API
clientSession.putAbs(downloadApiUrl).send().onSuccess(res2 -> {
if (res2.statusCode() == 200 || res2.statusCode() == 400) {
// 3.x版本的download API存在
getDownURL(downloadApiUrl);
} else if (res2.statusCode() == 404 || res2.statusCode() == 405) {
// download API不存在说明是4.x
delegateToCe4Tool();
} else {
// 其他错误可能是4.x
delegateToCe4Tool();
}
}).onFailure(t -> {
// 请求失败尝试4.x
delegateToCe4Tool();
});
} else {
nextParser();
}
} catch (Exception e) {
nextParser();
}
}).onFailure(t -> {
nextParser();
});
}
/**
* 转发到Ce4Tool处理4.x版本
*/
private void delegateToCe4Tool() {
log.debug("检测到Cloudreve 4.x转发到Ce4Tool处理");
new Ce4Tool(shareLinkInfo).parse().onComplete(promise);
}
private void getDownURL(String shareApiUrl) {