diff --git a/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java b/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java index 8c783a3..e999b9a 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/LeTool.java @@ -15,6 +15,7 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.Random; 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 DEFAULT_FILE_TYPE = "file"; private static final int FILE_TYPE_DIRECTORY = 0; // 目录类型 + private static final Random RANDOM = new Random(); private static final MultiMap HEADERS; @@ -100,8 +102,8 @@ public class LeTool extends PanBase { } String fileId = fileInfoJson.getString("fileId"); - // 根据文件ID获取跳转链接 - getDownURL(dataKey, fileId); + // 根据文件ID获取跳转链接(随机选择方式,失败自动fallback) + getDownURLWithFallback(dataKey, fileId); } } else { fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg")); @@ -260,8 +262,8 @@ public class LeTool extends PanBase { String shareId = paramJson.getString("shareId"); String fileId = paramJson.getString("fileId"); - // 调用获取下载链接 - getDownURLForById(shareId, fileId, parsePromise); + // 调用获取下载链接(随机选择方式,失败自动fallback) + getDownURLWithFallbackForById(shareId, fileId, parsePromise); } catch (Exception e) { parsePromise.fail("解析参数失败: " + e.getMessage()); @@ -304,14 +306,22 @@ public class LeTool extends PanBase { }).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 promise) { String uuid = UUID.randomUUID().toString(); JsonArray fileIds = JsonArray.of(fileId); - String apiUrl2 = API_URL_PREFIX + "packageDownloadWithFileIds"; + String apiUrl = API_URL_PREFIX + "packageDownloadWithFileIds"; // {"fileIds":[123],"shareId":"xxx","browserId":"uuid"} - client.postAbs(apiUrl2) + client.postAbs(apiUrl) .putHeaders(HEADERS) - .sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", key, "browserId", uuid)) + .sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", shareId, "browserId", uuid)) .onSuccess(res -> { JsonObject resJson = asJson(res); if (resJson.containsKey("result")) { @@ -320,20 +330,107 @@ public class LeTool extends PanBase { // 获取重定向链接跳转链接 String downloadUrl = dataJson.getString("downloadUrl"); if (downloadUrl == null) { - fail("Result JSON数据异常: downloadUrl不存在"); + promise.fail("Result JSON数据异常: downloadUrl不存在"); return; } // 获取重定向链接跳转链接 clientNoRedirects.getAbs(downloadUrl).send() .onSuccess(res2 -> promise.complete(res2.headers().get("Location"))) - .onFailure(handleFail(downloadUrl)); + .onFailure(err -> promise.fail(err)); } else { - fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg")); + promise.fail(resJson.getString("errcode") + ": " + resJson.getString("errmsg")); } } 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 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 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 promise) { + boolean useDirect = RANDOM.nextBoolean(); + log.info("乐云下载方式选择(parseById): shareId={}, fileId={}, method={}", shareId, fileId, useDirect ? "directDownload" : "packageDownloadWithFileIds"); + + Promise 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); + } } /** diff --git a/web-service/src/main/resources/app-dev.yml b/web-service/src/main/resources/app-dev.yml index dab8357..b5e3892 100644 --- a/web-service/src/main/resources/app-dev.yml +++ b/web-service/src/main/resources/app-dev.yml @@ -74,28 +74,54 @@ cache: type: h2db # 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大 defaultDuration: 5 - # 具体网盘的缓存配置,如果不加配置则不缓存,每次请求都会请求网盘API,格式:网盘标识: 时长 + # 具体网盘的缓存配置(单位:分钟) + # - 配置 key 且有值(如 le: 2879):使用指定时长 + # - 配置 key 但无值(如 fc:):使用上面的 defaultDuration + # - 未配置的 key:不缓存,每次都请求网盘API + # 格式:网盘标识: 时长 duration: - ce: 5 - cow: 5 - ec: 5 - fc: - fj: 20 - iz: 20 - le: 2879 - lz: 20 - qq: 9999999 - qqw: 30 - ws: 10 - ye: -1 - mne: 30 - mqq: 30 - mkg: 30 - p115: 30 - ct: 30 - qishui_music: 5 - baidu_photo: 5 - migu: 5 + # ---- 网盘类 ---- + ce: 5 # Cloudreve + cow: 5 # 奶牛快传 + ct: 30 # 城通网盘 + ec: 5 # 移动云空间 + fc: # 亿方云 + fj: 20 # 小飞机网盘 + fs: # 飞书云盘 + iz: 20 # 蓝奏云优享 + kd: # 可道云 + le: 2879 # 联想乐云 + lz: 20 # 蓝奏云 + other: # 其他网盘 + p115: 30 # 115网盘 + pdb: # Dropbox + pcx: # 超星云盘(需要 referer 头) + pgd: # Google Drive + pic: # iCloud + pod: # OneDrive + 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: # 酷狗音乐分享2(share/*.html) + mkws: # 酷我音乐分享 + mmgs: # 咪咕音乐分享短链 + mne: 30 # 网易云音乐 + mnes: # 网易云音乐分享短链 + mqq: 30 # QQ音乐 + mqqs: # QQ音乐分享短链 + qishui_music: 5 # 汽水音乐 # httpClient静态代理服务器配置(外网代理) proxy: