feat: 乐云 directDownload 接口支持 & 缓存配置补充完善

- 新增 directDownload (GET) 接口,比 packageDownloadWithFileIds 少一次请求
- 每次随机选择下载方式,失败自动 fallback 到另一种
- 统一所有下载方法的 Promise 参数传递
- 添加 HTTP 状态码日志便于调试
- 优化 app-dev.yml 缓存配置注释,补充所有缺失的网盘类型
This commit is contained in:
yukaidi
2026-06-05 22:46:25 +08:00
parent 0fd78defcb
commit bca4da4b6c
2 changed files with 157 additions and 34 deletions

View File

@@ -15,6 +15,7 @@ import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -24,6 +25,7 @@ public class LeTool extends PanBase {
private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/mshare/api/clouddiskapi/share/public/v1/"; private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/mshare/api/clouddiskapi/share/public/v1/";
private static final String DEFAULT_FILE_TYPE = "file"; private static final String DEFAULT_FILE_TYPE = "file";
private static final int FILE_TYPE_DIRECTORY = 0; // 目录类型 private static final int FILE_TYPE_DIRECTORY = 0; // 目录类型
private static final Random RANDOM = new Random();
private static final MultiMap HEADERS; private static final MultiMap HEADERS;
@@ -100,8 +102,8 @@ public class LeTool extends PanBase {
} }
String fileId = fileInfoJson.getString("fileId"); String fileId = fileInfoJson.getString("fileId");
// 根据文件ID获取跳转链接 // 根据文件ID获取跳转链接随机选择方式失败自动fallback
getDownURL(dataKey, fileId); getDownURLWithFallback(dataKey, fileId);
} }
} else { } else {
fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg")); fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg"));
@@ -260,8 +262,8 @@ public class LeTool extends PanBase {
String shareId = paramJson.getString("shareId"); String shareId = paramJson.getString("shareId");
String fileId = paramJson.getString("fileId"); String fileId = paramJson.getString("fileId");
// 调用获取下载链接 // 调用获取下载链接随机选择方式失败自动fallback
getDownURLForById(shareId, fileId, parsePromise); getDownURLWithFallbackForById(shareId, fileId, parsePromise);
} catch (Exception e) { } catch (Exception e) {
parsePromise.fail("解析参数失败: " + e.getMessage()); parsePromise.fail("解析参数失败: " + e.getMessage());
@@ -304,14 +306,22 @@ public class LeTool extends PanBase {
}).onFailure(err -> promise.fail(err)); }).onFailure(err -> promise.fail(err));
} }
private void getDownURL(String key, String fileId) { /**
* 通过 packageDownloadWithFileIds 接口获取下载链接
* 需要两步:先获取 downloadUrl再请求 302 跳转
*
* @param shareId 分享ID
* @param fileId 文件ID
* @param promise 完成时会写入此 promise
*/
private void getDownURL(String shareId, String fileId, Promise<String> promise) {
String uuid = UUID.randomUUID().toString(); String uuid = UUID.randomUUID().toString();
JsonArray fileIds = JsonArray.of(fileId); JsonArray fileIds = JsonArray.of(fileId);
String apiUrl2 = API_URL_PREFIX + "packageDownloadWithFileIds"; String apiUrl = API_URL_PREFIX + "packageDownloadWithFileIds";
// {"fileIds":[123],"shareId":"xxx","browserId":"uuid"} // {"fileIds":[123],"shareId":"xxx","browserId":"uuid"}
client.postAbs(apiUrl2) client.postAbs(apiUrl)
.putHeaders(HEADERS) .putHeaders(HEADERS)
.sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", key, "browserId", uuid)) .sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", shareId, "browserId", uuid))
.onSuccess(res -> { .onSuccess(res -> {
JsonObject resJson = asJson(res); JsonObject resJson = asJson(res);
if (resJson.containsKey("result")) { if (resJson.containsKey("result")) {
@@ -320,20 +330,107 @@ public class LeTool extends PanBase {
// 获取重定向链接跳转链接 // 获取重定向链接跳转链接
String downloadUrl = dataJson.getString("downloadUrl"); String downloadUrl = dataJson.getString("downloadUrl");
if (downloadUrl == null) { if (downloadUrl == null) {
fail("Result JSON数据异常: downloadUrl不存在"); promise.fail("Result JSON数据异常: downloadUrl不存在");
return; return;
} }
// 获取重定向链接跳转链接 // 获取重定向链接跳转链接
clientNoRedirects.getAbs(downloadUrl).send() clientNoRedirects.getAbs(downloadUrl).send()
.onSuccess(res2 -> promise.complete(res2.headers().get("Location"))) .onSuccess(res2 -> promise.complete(res2.headers().get("Location")))
.onFailure(handleFail(downloadUrl)); .onFailure(err -> promise.fail(err));
} else { } else {
fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg")); promise.fail(resJson.getString("errcode") + ": " + resJson.getString("errmsg"));
} }
} else { } else {
fail("Result JSON数据异常: result字段不存在"); promise.fail("Result JSON数据异常: result字段不存在");
} }
}).onFailure(handleFail(apiUrl2)); }).onFailure(err -> promise.fail(err));
}
/**
* 通过 directDownload 接口获取下载链接
* 相比 packageDownloadWithFileIds 少一次请求直接返回302
*
* @param shareId 分享ID
* @param fileId 文件ID
* @param promise 完成时会写入此 promise
*/
private void getDownURLDirect(String shareId, String fileId, Promise<String> promise) {
String uuid = UUID.randomUUID().toString();
String apiUrl = API_URL_PREFIX + "directDownload"
+ "?shareId=" + shareId
+ "&fileId=" + fileId
+ "&browserId=" + uuid;
clientNoRedirects.getAbs(apiUrl)
.putHeaders(HEADERS)
.send()
.onSuccess(res -> {
String location = res.headers().get("Location");
if (location != null && !location.isEmpty()) {
promise.complete(location);
} else {
log.warn("directDownload 返回非302响应: shareId={}, fileId={}, statusCode={}", shareId, fileId, res.statusCode());
promise.fail("directDownload 未返回有效的 Location, statusCode=" + res.statusCode());
}
})
.onFailure(err -> {
log.warn("directDownload 请求失败: shareId={}, fileId={}, error={}", shareId, fileId, err.getMessage());
promise.fail(err);
});
}
/**
* 随机选择下载方式并带 fallback用于 parse
* 先随机选择 directDownload 或 packageDownloadWithFileIds失败则尝试另一个
*/
private void getDownURLWithFallback(String shareId, String fileId) {
boolean useDirect = RANDOM.nextBoolean();
log.info("乐云下载方式选择: shareId={}, fileId={}, method={}", shareId, fileId, useDirect ? "directDownload" : "packageDownloadWithFileIds");
Promise<String> fallbackPromise = Promise.promise();
fallbackPromise.future().onSuccess(url -> {
promise.complete(url);
}).onFailure(err -> {
log.warn("乐云第一种下载方式失败,尝试另一种: {}", err.getMessage());
if (useDirect) {
getDownURL(shareId, fileId, promise);
} else {
getDownURLDirect(shareId, fileId, promise);
}
});
if (useDirect) {
getDownURLDirect(shareId, fileId, fallbackPromise);
} else {
getDownURL(shareId, fileId, fallbackPromise);
}
}
/**
* 随机选择下载方式并带 fallback用于 parseById
* 先随机选择 directDownload 或 packageDownloadWithFileIds失败则尝试另一个
*/
private void getDownURLWithFallbackForById(String shareId, String fileId, Promise<String> promise) {
boolean useDirect = RANDOM.nextBoolean();
log.info("乐云下载方式选择(parseById): shareId={}, fileId={}, method={}", shareId, fileId, useDirect ? "directDownload" : "packageDownloadWithFileIds");
Promise<String> fallbackPromise = Promise.promise();
fallbackPromise.future().onSuccess(url -> {
promise.complete(url);
}).onFailure(err -> {
log.warn("乐云第一种下载方式失败,尝试另一种: {}", err.getMessage());
if (useDirect) {
getDownURLForById(shareId, fileId, promise);
} else {
getDownURLDirect(shareId, fileId, promise);
}
});
if (useDirect) {
getDownURLDirect(shareId, fileId, fallbackPromise);
} else {
getDownURLForById(shareId, fileId, fallbackPromise);
}
} }
/** /**

View File

@@ -74,28 +74,54 @@ cache:
type: h2db type: h2db
# 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大 # 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大
defaultDuration: 5 defaultDuration: 5
# 具体网盘的缓存配置如果不加配置则不缓存每次请求都会请求网盘API格式网盘标识: 时长 # 具体网盘的缓存配置(单位:分钟)
# - 配置 key 且有值(如 le: 2879使用指定时长
# - 配置 key 但无值(如 fc:):使用上面的 defaultDuration
# - 未配置的 key不缓存每次都请求网盘API
# 格式:网盘标识: 时长
duration: duration:
ce: 5 # ---- 网盘类 ----
cow: 5 ce: 5 # Cloudreve
ec: 5 cow: 5 # 奶牛快传
fc: ct: 30 # 城通网盘
fj: 20 ec: 5 # 移动云空间
iz: 20 fc: # 亿方云
le: 2879 fj: 20 # 小飞机网盘
lz: 20 fs: # 飞书云盘
qq: 9999999 iz: 20 # 蓝奏云优享
qqw: 30 kd: # 可道云
ws: 10 le: 2879 # 联想乐云
ye: -1 lz: 20 # 蓝奏云
mne: 30 other: # 其他网盘
mqq: 30 p115: 30 # 115网盘
mkg: 30 pdb: # Dropbox
p115: 30 pcx: # 超星云盘(需要 referer 头)
ct: 30 pgd: # Google Drive
qishui_music: 5 pic: # iCloud
baidu_photo: 5 pod: # OneDrive
migu: 5 pvyy: # 微雨云存储
pwps: # WPS云文档
qk: # 夸克网盘
qq: 9999999 # QQ邮箱中转站 (iwx.mail.qq.com/ftn/download)
qqsc: # QQ闪传 (qfile.qq.com)
qqw: 30 # QQ邮箱云盘 (wx.mail.qq.com/s)
uc: # UC网盘
ws: 10 # 文叔叔
ye: -1 # 123网盘
# ---- 音乐类 ----
baidu_photo: 5 # 百度网盘相册
migu: 5 # 咪咕音乐
mkg: 30 # 酷狗音乐
mkgs: # 酷狗音乐分享短链
mkgs2: # 酷狗音乐分享2share/*.html
mkws: # 酷我音乐分享
mmgs: # 咪咕音乐分享短链
mne: 30 # 网易云音乐
mnes: # 网易云音乐分享短链
mqq: 30 # QQ音乐
mqqs: # QQ音乐分享短链
qishui_music: 5 # 汽水音乐
# httpClient静态代理服务器配置(外网代理) # httpClient静态代理服务器配置(外网代理)
proxy: proxy: