更新 夸克解析、小飞机解析,前端版本号

This commit is contained in:
q
2026-02-22 19:15:15 +08:00
parent 5f9da47513
commit fdf067c25e
7 changed files with 1297 additions and 257 deletions

1
.gitignore vendored
View File

@@ -56,7 +56,6 @@ test-filelist.java
*.temp *.temp
*.log *.log
*.bak *.bak
**/secret.yml
*.swp *.swp
*.swo *.swo
*~ *~

View File

@@ -0,0 +1,55 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* 蓝奏云优享解析器选择器
* 根据配置的鉴权方式选择不同的解析器:
* - 如果配置了 username 和 password则使用 IzToolWithAuth (支持大文件)
* - 否则使用 IzTool (免登录,仅支持小文件)
*/
public class IzSelectorTool implements IPanTool {
private final IPanTool selectedTool;
public IzSelectorTool(ShareLinkInfo shareLinkInfo) {
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
// 检查是否配置了账号密码
if (auths.contains("username") && auths.contains("password")) {
String username = auths.get("username");
String password = auths.get("password");
if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) {
// 使用 IzToolWithAuth (账密登录,支持大文件)
this.selectedTool = new IzToolWithAuth(shareLinkInfo);
return;
}
}
}
// 无认证信息或认证信息无效,使用免登录版本(仅支持小文件)
this.selectedTool = new IzTool(shareLinkInfo);
}
@Override
public Future<String> parse() {
return selectedTool.parse();
}
@Override
public Future<List<FileInfo>> parseFileList() {
return selectedTool.parseFileList();
}
@Override
public Future<String> parseById() {
return selectedTool.parseById();
}
}

View File

@@ -5,35 +5,50 @@ import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase; import cn.qaiu.parser.PanBase;
import cn.qaiu.util.AESUtils; import cn.qaiu.util.AESUtils;
import cn.qaiu.util.AcwScV2Generator; import cn.qaiu.util.AcwScV2Generator;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter; import cn.qaiu.util.FileSizeConverter;
import io.netty.handler.codec.http.cookie.DefaultCookie; import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.MultiMap; import io.vertx.core.MultiMap;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClientSession; import io.vertx.ext.web.client.WebClientSession;
import io.vertx.uritemplate.UriTemplate; import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/** /**
* 蓝奏云优享 * 蓝奏云优享
* v019b22 *
*/ */
public class IzTool extends PanBase { public class IzTool extends PanBase {
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects); private static final String API_URL0 = "https://api.ilanzou.com/";
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/"; private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" + private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
"&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60"; "&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
private static final String LOGIN_URL = API_URL_PREFIX +
"login?uuid={uuid}&devType=6&devCode={uuid}&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken=&extra=2";
// https://api.ilanzou.com/proved/user/info/map?devType=3&devModel=Chrome&uuid=TInRHH3QzRaMo-Ajl2PkJ&extra=2&timestamp=EC2C6E7F45EB21338A17A7621E0BB437
private static final String TOKEN_VERIFY_URL = API_URL0 +
"proved/user/info/map?devType=6&devModel=Chrome&uuid={uuid}&extra=2&timestamp={ts}";
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" + private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
"&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}&shareId={dataKey}"; "&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}&shareId={dataKey}";
// downloadId=x&enable=1&devType=6&uuid=x&timestamp=x&auth=x&shareId=lGFndCM
private static final String SECOND_REQUEST_URL_VIP = API_URL_PREFIX + "file/redirect?uuid={uuid}&devType=6&devCode={uuid}" +
"&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken={appToken}&enable=1&downloadId={fidEncode}&auth={auth}";
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" + private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}"; "={uuid}&extra=2&timestamp={ts}";
@@ -42,16 +57,15 @@ public class IzTool extends PanBase {
"={uuid}&extra=2&timestamp={ts}&shareId={shareId}&folderId" + "={uuid}&extra=2&timestamp={ts}&shareId={shareId}&folderId" +
"={folderId}&offset=1&limit=60"; "={folderId}&offset=1&limit=60";
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs)); WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
String uuid = UUID.randomUUID().toString();
private static final MultiMap header; private static final MultiMap header;
static { static {
header = MultiMap.caseInsensitiveMultiMap(); header = MultiMap.caseInsensitiveMultiMap();
header.set("Accept", "application/json, text/plain, */*"); header.set("Accept", "application/json, text/plain, */*");
header.set("Accept-Encoding", "gzip, deflate, br, zstd"); header.set("Accept-Encoding", "gzip, deflate");
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"); header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
header.set("Cache-Control", "no-cache"); header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive"); header.set("Connection", "keep-alive");
@@ -69,38 +83,59 @@ public class IzTool extends PanBase {
header.set("sec-ch-ua-mobile", "?0"); header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "\"Windows\""); header.set("sec-ch-ua-platform", "\"Windows\"");
} }
public IzTool(ShareLinkInfo shareLinkInfo) { public IzTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo); super(shareLinkInfo);
} }
private void setCookie(String html) { String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
int beginIndex = html.indexOf("arg1='") + 6;
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex)); public static String token = null;
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1); public static boolean authFlag = true;
// 创建一个 Cookie 并放入 CookieStore
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
nettyCookie.setPath("/"); // 设置路径
nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie);
}
public Future<String> parse() { public Future<String> parse() {
String shareId = shareLinkInfo.getShareKey();
// 24.5.12 ilanzou改规则无需计算shareId String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey)); long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
// 第一次请求 获取文件信息 // 检查并输出认证状态
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60 if (shareLinkInfo.getOtherParam().containsKey("auths")) {
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword()); log.info("文件解析检测到认证信息: isTempAuth={}, authFlag={}, token={}",
isTempAuth, authFlag, token != null ? "已登录(" + token.substring(0, Math.min(10, token.length())) + "...)" : "未登录");
// 如果需要认证但还没有token先执行登录
if ((isTempAuth || authFlag) && token == null) {
log.info("文件解析需要登录,开始执行登录流程...");
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
return login(tsEncode, auths)
.compose(v -> {
log.info("文件解析预登录成功,继续解析流程");
return parseWithAuth(shareId, tsEncode);
})
.onFailure(err -> {
log.warn("文件解析预登录失败: {},尝试使用免登录模式", err.getMessage());
// 登录失败,继续使用免登录模式
});
} else if (token != null) {
log.info("文件解析使用已有token: {}...", token.substring(0, Math.min(10, token.length())));
}
} else {
log.debug("文件解析无认证信息,使用免登录模式");
}
return parseWithAuth(shareId, tsEncode);
}
private Future<String> parseWithAuth(String shareId, String tsEncode) {
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL)) webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid) .setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode) .setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res .send().onSuccess(r0 -> { // 忽略res
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
// 第一次请求 获取文件信息 // 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60 // POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
webClientSession.postAbs(UriTemplate.of(url)) webClientSession.postAbs(UriTemplate.of(url))
@@ -121,36 +156,43 @@ public class IzTool extends PanBase {
.setTemplateParam("uuid", uuid) .setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode) .setTemplateParam("ts", tsEncode)
.send().onSuccess(res2 -> { .send().onSuccess(res2 -> {
handleParseResponse(asText(res2), shareId); processFirstResponse(res2);
}).onFailure(handleFail(FIRST_REQUEST_URL)); }).onFailure(handleFail("请求1-重试"));
return; return;
} }
handleParseResponse(resBody, shareId); processFirstResponse(res);
}).onFailure(handleFail(FIRST_REQUEST_URL)); }).onFailure(handleFail("请求1"));
});
}).onFailure(handleFail("请求1"));
return promise.future(); return promise.future();
} }
private void handleParseResponse(String resBody, String shareId) { /**
JsonObject resJson; * 设置 cookie
try { */
resJson = new JsonObject(resBody); private void setCookie(String html) {
} catch (Exception e) { int beginIndex = html.indexOf("arg1='") + 6;
fail(FIRST_REQUEST_URL + " 解析JSON失败: " + resBody); String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
return; String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
} // 创建一个 Cookie 并放入 CookieStore
if (resJson.isEmpty()) { DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
fail(FIRST_REQUEST_URL + " 返回内容为空"); nettyCookie.setDomain(".ilanzou.com"); // 设置域名
return; nettyCookie.setPath("/"); // 设置路径
} nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie);
}
/**
* 处理第一次请求的响应
*/
private void processFirstResponse(HttpResponse<Buffer> res) {
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 200) { if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson); fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return; return;
} }
if (resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) { if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson); fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return; return;
@@ -167,30 +209,251 @@ public class IzTool extends PanBase {
promise.complete(fileList.getInteger("folderId").toString()); promise.complete(fileList.getInteger("folderId").toString());
return; return;
} }
// 提取文件信息
extractFileInfo(fileList, fileInfo);
getDownURL(resJson);
}
private void getDownURL(JsonObject resJson) {
String dataKey = shareLinkInfo.getShareKey();
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
String fileId = fileInfo.getString("fileIds"); String fileId = fileInfo.getString("fileIds");
String userId = fileInfo.getString("userId"); String userId = fileInfo.getString("userId");
// 其他参数 // 其他参数
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|"); long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId); String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs); String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 第二次请求
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL)) // 检查是否有认证信息
.setTemplateParam("fidEncode", fidEncode) if (shareLinkInfo.getOtherParam().containsKey("auths")) {
.setTemplateParam("uuid", uuid) // 检查是否为临时认证(临时认证每次都尝试登录)
.setTemplateParam("ts", tsEncode) boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
.setTemplateParam("auth", auth) // 如果是临时认证或者是后台配置且authFlag为true则尝试使用认证
.setTemplateParam("shareId", shareId) if (isTempAuth || authFlag) {
.putHeaders(header).send().onSuccess(res2 -> { log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag);
MultiMap headers = res2.headers(); HttpRequest<Buffer> httpRequest =
if (!headers.contains("Location")) { webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + headers); .setTemplateParam("fidEncode", fidEncode)
return; .setTemplateParam("uuid", uuid)
} .setTemplateParam("ts", tsEncode2)
promise.complete(headers.get("Location")); .setTemplateParam("auth", auth)
}).onFailure(handleFail(SECOND_REQUEST_URL)); .setTemplateParam("dataKey", dataKey);
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (token == null) {
// 执行登录
login(tsEncode2, auths).onFailure(failRes-> {
log.warn("登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
// 验证token
webClientSession.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header).send().onSuccess(res -> {
// log.info("res: {}",asJson(res));
if (asJson(res).getInteger("code") != 200) {
login(tsEncode2, auths).onFailure(failRes -> {
log.warn("重新登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}
}).onFailure(handleFail("Token验证"));
}
} else {
// authFlag 为 false使用免登录解析
log.debug("authFlag=false使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
} else {
// 没有认证信息,使用免登录解析
log.debug("无认证信息,使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
} }
private Future<Void> login(String tsEncode2, MultiMap auths) {
Promise<Void> promise1 = Promise.promise();
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
.setTemplateParam("uuid",uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header)
.sendJsonObject(JsonObject.of("loginName", auths.get("username"), "loginPwd", auths.get("password")))
.onSuccess(res2->{
JsonObject json = asJson(res2);
if (json.getInteger("code") == 200) {
token = json.getJsonObject("data").getString("appToken");
header.set("appToken", token);
log.info("登录成功 token: {}", token);
promise1.complete();
} else {
// 检查是否为临时认证
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
if (isTempAuth) {
// 临时认证失败,直接返回错误,不影响后台配置的认证
log.warn("临时认证失败: {}", json.getString("msg"));
promise1.fail("临时认证失败: " + json.getString("msg"));
} else {
// 后台配置的认证失败设置authFlag并返回失败让下次请求使用免登陆解析
log.warn("后台配置认证失败: {}, authFlag将设为false请重新解析", json.getString("msg"));
authFlag = false;
promise1.fail("认证失败: " + json.getString("msg") + ", 请重新解析将使用免登陆模式");
}
}
}).onFailure(err -> {
log.error("登录请求异常: {}", err.getMessage());
promise1.fail("登录请求异常: " + err.getMessage());
});
return promise1.future();
}
/**
* 从接口返回数据中提取文件信息
*/
private void extractFileInfo(JsonObject fileList, JsonObject shareInfo) {
try {
// 文件名
String fileName = fileList.getString("fileName");
shareLinkInfo.getOtherParam().put("fileName", fileName);
// 文件大小 (KB -> Bytes)
Long fileSize = fileList.getLong("fileSize", 0L) * 1024;
shareLinkInfo.getOtherParam().put("fileSize", fileSize);
shareLinkInfo.getOtherParam().put("fileSizeFormat", FileSizeConverter.convertToReadableSize(fileSize));
// 文件图标
String fileIcon = fileList.getString("fileIcon");
if (StringUtils.isNotBlank(fileIcon)) {
shareLinkInfo.getOtherParam().put("fileIcon", fileIcon);
}
// 文件ID
Long fileId = fileList.getLong("fileId");
if (fileId != null) {
shareLinkInfo.getOtherParam().put("fileId", fileId.toString());
}
// 文件类型 (1=文件, 2=目录)
Integer fileType = fileList.getInteger("fileType", 1);
shareLinkInfo.getOtherParam().put("fileType", fileType == 1 ? "file" : "folder");
// 下载次数
Integer downloads = fileList.getInteger("fileDownloads", 0);
shareLinkInfo.getOtherParam().put("downloadCount", downloads);
// 点赞数
Integer likes = fileList.getInteger("fileLikes", 0);
shareLinkInfo.getOtherParam().put("likeCount", likes);
// 评论数
Integer comments = fileList.getInteger("fileComments", 0);
shareLinkInfo.getOtherParam().put("commentCount", comments);
// 评分
Double stars = fileList.getDouble("fileStars", 0.0);
shareLinkInfo.getOtherParam().put("stars", stars);
// 更新时间
String updateTime = fileList.getString("updTime");
if (StringUtils.isNotBlank(updateTime)) {
shareLinkInfo.getOtherParam().put("updateTime", updateTime);
}
// 创建时间
String createTime = null;
// 分享信息
if (shareInfo != null) {
// 分享ID
Integer shareId = shareInfo.getInteger("shareId");
if (shareId != null) {
shareLinkInfo.getOtherParam().put("shareId", shareId.toString());
}
// 上传时间
String addTime = shareInfo.getString("addTime");
if (StringUtils.isNotBlank(addTime)) {
shareLinkInfo.getOtherParam().put("createTime", addTime);
createTime = addTime;
}
// 预览次数
Integer previewNum = shareInfo.getInteger("previewNum", 0);
shareLinkInfo.getOtherParam().put("previewCount", previewNum);
// 用户信息
JsonObject userMap = shareInfo.getJsonObject("map");
if (userMap != null) {
String userName = userMap.getString("userName");
if (StringUtils.isNotBlank(userName)) {
shareLinkInfo.getOtherParam().put("userName", userName);
}
// VIP信息
Integer isVip = userMap.getInteger("isVip", 0);
shareLinkInfo.getOtherParam().put("isVip", isVip == 1);
}
}
// 创建 FileInfo 对象并存入 otherParam
FileInfo fileInfoObj = new FileInfo()
.setPanType(shareLinkInfo.getType())
.setFileName(fileName)
.setFileId(fileList.getLong("fileId") != null ? fileList.getLong("fileId").toString() : null)
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setFileType(fileType == 1 ? "file" : "folder")
.setFileIcon(fileList.getString("fileIcon"))
.setDownloadCount(downloads)
.setCreateTime(createTime)
.setUpdateTime(updateTime);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfoObj);
log.debug("提取文件信息成功: fileName={}, fileSize={}, downloads={}",
fileName, fileSize, downloads);
} catch (Exception e) {
log.warn("提取文件信息失败: {}", e.getMessage());
}
}
private void down(HttpResponse<Buffer> res2) {
MultiMap headers = res2.headers();
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) {
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误");
return;
}
promise.complete(headers.get("Location"));
}
// 目录解析
@Override @Override
public Future<List<FileInfo>> parseFileList() { public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise(); Promise<List<FileInfo>> promise = Promise.promise();
@@ -205,11 +468,7 @@ public class IzTool extends PanBase {
return promise.future(); return promise.future();
} }
parse().onSuccess(id -> { parse().onSuccess(id -> {
if (id != null && id.matches("^[a-zA-Z0-9]+$")) { parserDir(id, shareId, promise);
parserDir(id, shareId, promise);
} else {
promise.fail("解析目录ID失败");
}
}).onFailure(failRes -> { }).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage()); log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes); promise.fail(failRes);
@@ -218,6 +477,22 @@ public class IzTool extends PanBase {
} }
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) { private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
if (id != null && (id.startsWith("http://") || id.startsWith("https://"))) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(id)
.setFileId(id)
.setFileType("file")
.setParserUrl(id)
.setPanType(shareLinkInfo.getType());
List<FileInfo> result = new ArrayList<>();
result.add(fileInfo);
promise.complete(result);
return;
}
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode); log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860 // 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
// 拿到目录ID // 拿到目录ID
@@ -228,103 +503,134 @@ public class IzTool extends PanBase {
.setTemplateParam("ts", tsEncode) .setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id) .setTemplateParam("folderId", id)
.send().onSuccess(res -> { .send().onSuccess(res -> {
JsonObject jsonObject; String resBody = asText(res);
try { // 检查是否包含 cookie 验证
jsonObject = asJson(res); if (resBody.contains("var arg1='")) {
} catch (Exception e) { log.debug("目录解析需要 cookie 验证,重新创建 session");
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString()); webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(resBody);
// 重新请求目录列表
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res2 -> {
processDirResponse(res2, shareId, promise);
}).onFailure(err -> {
log.error("目录解析重试失败: {}", err.getMessage());
promise.fail("目录解析失败: " + err.getMessage());
});
return; return;
} }
// System.out.println(jsonObject.encodePrettily()); processDirResponse(res, shareId, promise);
JsonArray list = jsonObject.getJsonArray("list"); }).onFailure(err -> {
ArrayList<FileInfo> result = new ArrayList<>(); log.error("目录解析请求失败: {}", err.getMessage());
list.forEach(item->{ promise.fail("目录解析失败: " + err.getMessage());
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 回传用到的参数
//"fidEncode", paramJson.getString("fidEncode"))
//"uuid", paramJson.getString("uuid"))
//"ts", paramJson.getString("ts"))
//"auth", paramJson.getString("auth"))
//"shareId", paramJson.getString("shareId"))
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode,
"auth", auth,
"shareId", shareId);
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
String param = new String(encode);
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
promise.complete(result);
}).onFailure(failRes -> {
log.error("解析目录请求失败: {}", failRes.getMessage());
promise.fail(failRes);
}); });
} }
/**
* 处理目录解析响应
*/
private void processDirResponse(HttpResponse<Buffer> res, String shareId, Promise<List<FileInfo>> promise) {
try {
JsonObject jsonObject = asJson(res);
log.debug("目录解析响应: {}", jsonObject.encodePrettily());
if (!jsonObject.containsKey("list")) {
log.error("目录解析响应缺少 list 字段: {}", jsonObject);
promise.fail("目录解析失败: 响应格式错误");
return;
}
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 其他参数 - 每个文件使用新的时间戳
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 回传用到的参数
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId);
String param = CommonUtils.urlBase64Encode(entries.encode());
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
promise.complete(result);
} catch (Exception e) {
log.error("处理目录响应异常: {}", e.getMessage(), e);
promise.fail("目录解析失败: " + e.getMessage());
}
}
@Override @Override
public Future<String> parseById() { public Future<String> parseById() {
// 第二次请求 JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson"); // 使用免登录接口
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL)) webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", paramJson.getString("fidEncode")) .setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid")) .setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts")) .setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth")) .setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("shareId", paramJson.getString("shareId")) .setTemplateParam("dataKey", paramJson.getString("shareId"))
.putHeaders(header).send().onSuccess(res2 -> { .send().onSuccess(this::down).onFailure(handleFail("parseById"));
MultiMap headers = res2.headers();
if (!headers.contains("Location")) {
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res2.headers());
return;
}
promise.complete(headers.get("Location"));
}).onFailure(handleFail(SECOND_REQUEST_URL));
return promise.future(); return promise.future();
} }
public static void resetToken() {
token = null;
authFlag = true;
}
} }

View File

@@ -0,0 +1,658 @@
package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
import cn.qaiu.util.AESUtils;
import cn.qaiu.util.AcwScV2Generator;
import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.FileSizeConverter;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
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 io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClientSession;
import io.vertx.uritemplate.UriTemplate;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 蓝奏云优享 - 需要登录版本(支持大文件)
*/
public class IzToolWithAuth extends PanBase {
private static final String API_URL0 = "https://api.ilanzou.com/";
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" +
"&uuid={uuid}&extra=2&timestamp={ts}&shareId={shareId}&type=0&offset=1&limit=60";
private static final String LOGIN_URL = API_URL_PREFIX +
"login?uuid={uuid}&devType=6&devCode={uuid}&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken=&extra=2";
// https://api.ilanzou.com/proved/user/info/map?devType=3&devModel=Chrome&uuid=TInRHH3QzRaMo-Ajl2PkJ&extra=2&timestamp=EC2C6E7F45EB21338A17A7621E0BB437
private static final String TOKEN_VERIFY_URL = API_URL0 +
"proved/user/info/map?devType=6&devModel=Chrome&uuid={uuid}&extra=2&timestamp={ts}";
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" +
"&devType=6&uuid={uuid}&timestamp={ts}&auth={auth}&shareId={dataKey}";
private static final String SECOND_REQUEST_URL_VIP = API_URL_PREFIX + "file/redirect?uuid={uuid}&devType=6&devCode={uuid}" +
"&devModel=chrome&devVersion=127&appVersion=&timestamp={ts}&appToken={appToken}&enable=1&downloadId={fidEncode}&auth={auth}";
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}";
private static final String FILE_LIST_URL = API_URL_PREFIX + "/share/list?devType=6&devModel=Chrome&uuid" +
"={uuid}&extra=2&timestamp={ts}&shareId={shareId}&folderId" +
"={folderId}&offset=1&limit=60";
WebClientSession webClientSession = WebClientSession.create(clientNoRedirects);
private static final MultiMap header;
static {
header = MultiMap.caseInsensitiveMultiMap();
header.set("Accept", "application/json, text/plain, */*");
header.set("Accept-Encoding", "gzip, deflate");
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
header.set("Cache-Control", "no-cache");
header.set("Connection", "keep-alive");
header.set("Content-Length", "0");
header.set("DNT", "1");
header.set("Host", "api.ilanzou.com");
header.set("Origin", "https://www.ilanzou.com/");
header.set("Pragma", "no-cache");
header.set("Referer", "https://www.ilanzou.com/");
header.set("Sec-Fetch-Dest", "empty");
header.set("Sec-Fetch-Mode", "cors");
header.set("Sec-Fetch-Site", "cross-site");
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
header.set("sec-ch-ua-mobile", "?0");
header.set("sec-ch-ua-platform", "\"Windows\"");
}
public IzToolWithAuth(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
String uuid = UUID.randomUUID().toString().toLowerCase(); // 也可以使用 UUID.randomUUID().toString()
public static String token = null;
public static boolean authFlag = true;
public Future<String> parse() {
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
webClientSession.postAbs(UriTemplate.of(VIP_REQUEST_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(r0 -> { // 忽略res
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
// 第一次请求 获取文件信息
// POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
webClientSession.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res -> {
String resBody = asText(res);
// 检查是否包含 cookie 验证
if (resBody.contains("var arg1='")) {
webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(resBody);
// 重新请求
webClientSession.postAbs(UriTemplate.of(url))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.send().onSuccess(res2 -> {
processFirstResponse(res2);
}).onFailure(handleFail("请求1-重试"));
return;
}
processFirstResponse(res);
}).onFailure(handleFail("请求1"));
}).onFailure(handleFail("请求1"));
return promise.future();
}
/**
* 设置 cookie
*/
private void setCookie(String html) {
int beginIndex = html.indexOf("arg1='") + 6;
String arg1 = html.substring(beginIndex, html.indexOf("';", beginIndex));
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
// 创建一个 Cookie 并放入 CookieStore
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(".ilanzou.com"); // 设置域名
nettyCookie.setPath("/"); // 设置路径
nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie);
}
/**
* 处理第一次请求的响应
*/
private void processFirstResponse(HttpResponse<Buffer> res) {
JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 200) {
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
return;
}
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
return;
}
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
// 如果是目录返回目录ID
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
return;
}
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
if (fileList.getInteger("fileType") == 2) {
promise.complete(fileList.getInteger("folderId").toString());
return;
}
// 提取文件信息
extractFileInfo(fileList, fileInfo);
getDownURL(resJson);
}
private void getDownURL(JsonObject resJson) {
String dataKey = shareLinkInfo.getShareKey();
// 文件Id
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
String fileId = fileInfo.getString("fileIds");
String userId = fileInfo.getString("userId");
// 其他参数
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 检查是否有认证信息
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
// 检查是否为临时认证(临时认证每次都尝试登录)
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
// 如果是临时认证或者是后台配置且authFlag为true则尝试使用认证
if (isTempAuth || authFlag) {
log.debug("尝试使用认证信息解析, isTempAuth={}, authFlag={}", isTempAuth, authFlag);
HttpRequest<Buffer> httpRequest =
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey);
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
if (token == null) {
// 执行登录
login(tsEncode2, auths).onFailure(failRes-> {
log.warn("登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
// 验证token
webClientSession.postAbs(UriTemplate.of(TOKEN_VERIFY_URL))
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header).send().onSuccess(res -> {
// log.info("res: {}",asJson(res));
if (asJson(res).getInteger("code") != 200) {
login(tsEncode2, auths).onFailure(failRes -> {
log.warn("重新登录失败: {}", failRes.getMessage());
fail(failRes.getMessage());
}).onSuccess(r-> {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
});
} else {
httpRequest.setTemplateParam("appToken", header.get("appToken"))
.putHeaders(header);
httpRequest.send().onSuccess(this::down).onFailure(handleFail("请求2"));
}
}).onFailure(handleFail("Token验证"));
}
} else {
// authFlag 为 false使用免登录解析
log.debug("authFlag=false使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
} else {
// 没有认证信息,使用免登录解析
log.debug("无认证信息,使用免登录解析");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", fidEncode)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode2)
.setTemplateParam("auth", auth)
.setTemplateParam("dataKey", dataKey).send()
.onSuccess(this::down).onFailure(handleFail("请求2"));
}
}
private Future<Void> login(String tsEncode2, MultiMap auths) {
Promise<Void> promise1 = Promise.promise();
webClientSession.postAbs(UriTemplate.of(LOGIN_URL))
.setTemplateParam("uuid",uuid)
.setTemplateParam("ts", tsEncode2)
.putHeaders(header)
.sendJsonObject(JsonObject.of("loginName", auths.get("username"), "loginPwd", auths.get("password")))
.onSuccess(res2->{
JsonObject json = asJson(res2);
if (json.getInteger("code") == 200) {
token = json.getJsonObject("data").getString("appToken");
header.set("appToken", token);
log.info("登录成功 token: {}", token);
promise1.complete();
} else {
// 检查是否为临时认证
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
if (isTempAuth) {
// 临时认证失败,直接返回错误,不影响后台配置的认证
log.warn("临时认证失败: {}", json.getString("msg"));
promise1.fail("临时认证失败: " + json.getString("msg"));
} else {
// 后台配置的认证失败设置authFlag并返回失败让下次请求使用免登陆解析
log.warn("后台配置认证失败: {}, authFlag将设为false请重新解析", json.getString("msg"));
authFlag = false;
promise1.fail("认证失败: " + json.getString("msg") + ", 请重新解析将使用免登陆模式");
}
}
}).onFailure(err -> {
log.error("登录请求异常: {}", err.getMessage());
promise1.fail("登录请求异常: " + err.getMessage());
});
return promise1.future();
}
/**
* 从接口返回数据中提取文件信息
*/
private void extractFileInfo(JsonObject fileList, JsonObject shareInfo) {
try {
// 文件名
String fileName = fileList.getString("fileName");
shareLinkInfo.getOtherParam().put("fileName", fileName);
// 文件大小 (KB -> Bytes)
Long fileSize = fileList.getLong("fileSize", 0L) * 1024;
shareLinkInfo.getOtherParam().put("fileSize", fileSize);
shareLinkInfo.getOtherParam().put("fileSizeFormat", FileSizeConverter.convertToReadableSize(fileSize));
// 文件图标
String fileIcon = fileList.getString("fileIcon");
if (StringUtils.isNotBlank(fileIcon)) {
shareLinkInfo.getOtherParam().put("fileIcon", fileIcon);
}
// 文件ID
Long fileId = fileList.getLong("fileId");
if (fileId != null) {
shareLinkInfo.getOtherParam().put("fileId", fileId.toString());
}
// 文件类型 (1=文件, 2=目录)
Integer fileType = fileList.getInteger("fileType", 1);
shareLinkInfo.getOtherParam().put("fileType", fileType == 1 ? "file" : "folder");
// 下载次数
Integer downloads = fileList.getInteger("fileDownloads", 0);
shareLinkInfo.getOtherParam().put("downloadCount", downloads);
// 点赞数
Integer likes = fileList.getInteger("fileLikes", 0);
shareLinkInfo.getOtherParam().put("likeCount", likes);
// 评论数
Integer comments = fileList.getInteger("fileComments", 0);
shareLinkInfo.getOtherParam().put("commentCount", comments);
// 评分
Double stars = fileList.getDouble("fileStars", 0.0);
shareLinkInfo.getOtherParam().put("stars", stars);
// 更新时间
String updateTime = fileList.getString("updTime");
if (StringUtils.isNotBlank(updateTime)) {
shareLinkInfo.getOtherParam().put("updateTime", updateTime);
}
// 创建时间
String createTime = null;
// 分享信息
if (shareInfo != null) {
// 分享ID
Integer shareId = shareInfo.getInteger("shareId");
if (shareId != null) {
shareLinkInfo.getOtherParam().put("shareId", shareId.toString());
}
// 上传时间
String addTime = shareInfo.getString("addTime");
if (StringUtils.isNotBlank(addTime)) {
shareLinkInfo.getOtherParam().put("createTime", addTime);
createTime = addTime;
}
// 预览次数
Integer previewNum = shareInfo.getInteger("previewNum", 0);
shareLinkInfo.getOtherParam().put("previewCount", previewNum);
// 用户信息
JsonObject userMap = shareInfo.getJsonObject("map");
if (userMap != null) {
String userName = userMap.getString("userName");
if (StringUtils.isNotBlank(userName)) {
shareLinkInfo.getOtherParam().put("userName", userName);
}
// VIP信息
Integer isVip = userMap.getInteger("isVip", 0);
shareLinkInfo.getOtherParam().put("isVip", isVip == 1);
}
}
// 创建 FileInfo 对象并存入 otherParam
FileInfo fileInfoObj = new FileInfo()
.setPanType(shareLinkInfo.getType())
.setFileName(fileName)
.setFileId(fileList.getLong("fileId") != null ? fileList.getLong("fileId").toString() : null)
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setFileType(fileType == 1 ? "file" : "folder")
.setFileIcon(fileList.getString("fileIcon"))
.setDownloadCount(downloads)
.setCreateTime(createTime)
.setUpdateTime(updateTime);
shareLinkInfo.getOtherParam().put("fileInfo", fileInfoObj);
log.debug("提取文件信息成功: fileName={}, fileSize={}, downloads={}",
fileName, fileSize, downloads);
} catch (Exception e) {
log.warn("提取文件信息失败: {}", e.getMessage());
}
}
private void down(HttpResponse<Buffer> res2) {
MultiMap headers = res2.headers();
if (!headers.contains("Location") || StringUtils.isBlank(headers.get("Location"))) {
fail("找不到下载链接可能服务器已被禁止或者配置的认证信息有误");
return;
}
promise.complete(headers.get("Location"));
}
// 目录解析
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise();
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
// 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) {
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
parserDir(dirId, shareId, promise);
return promise.future();
}
parse().onSuccess(id -> {
parserDir(id, shareId, promise);
}).onFailure(failRes -> {
log.error("解析目录失败: {}", failRes.getMessage());
promise.fail(failRes);
});
return promise.future();
}
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
if (id != null && (id.startsWith("http://") || id.startsWith("https://"))) {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(id)
.setFileId(id)
.setFileType("file")
.setParserUrl(id)
.setPanType(shareLinkInfo.getType());
List<FileInfo> result = new ArrayList<>();
result.add(fileInfo);
promise.complete(result);
return;
}
long nowTs = System.currentTimeMillis();
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
// 检查是否需要登录(有认证信息且需要使用认证)
if (shareLinkInfo.getOtherParam().containsKey("auths")) {
boolean isTempAuth = shareLinkInfo.getOtherParam().containsKey("__TEMP_AUTH_ADDED");
log.debug("目录解析检查认证: isTempAuth={}, authFlag={}, token={}", isTempAuth, authFlag, token != null ? "已有" : "null");
if ((isTempAuth || authFlag) && token == null) {
MultiMap auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
log.info("目录解析需要登录,开始执行登录...");
// 先登录获取 token
login(tsEncode, auths)
.onFailure(err -> {
log.warn("目录解析登录失败,使用免登录模式: {}", err.getMessage());
// 登录失败,继续使用免登录
requestDirList(id, shareId, tsEncode, promise);
})
.onSuccess(r -> {
log.info("目录解析登录成功token={}, 使用 VIP 模式", token != null ? token.substring(0, 10) + "..." : "null");
requestDirList(id, shareId, tsEncode, promise);
});
return;
} else if (token != null) {
log.debug("目录解析已有 token直接使用 VIP 模式");
} else {
log.debug("目录解析: authFlag=false 或为临时认证但已失败,使用免登录模式");
}
} else {
log.debug("目录解析无认证信息,使用免登录模式");
}
// 无需登录或已登录,直接请求
requestDirList(id, shareId, tsEncode, promise);
}
/**
* 请求目录列表
*/
private void requestDirList(String id, String shareId, String tsEncode, Promise<List<FileInfo>> promise) {
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res -> {
String resBody = asText(res);
// 检查是否包含 cookie 验证
if (resBody.contains("var arg1='")) {
log.debug("目录解析需要 cookie 验证,重新创建 session");
webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(resBody);
// 重新请求目录列表
webClientSession.postAbs(UriTemplate.of(FILE_LIST_URL))
.putHeaders(header)
.setTemplateParam("shareId", shareId)
.setTemplateParam("uuid", uuid)
.setTemplateParam("ts", tsEncode)
.setTemplateParam("folderId", id)
.send().onSuccess(res2 -> {
processDirResponse(res2, shareId, promise);
}).onFailure(err -> {
log.error("目录解析重试失败: {}", err.getMessage());
promise.fail("目录解析失败: " + err.getMessage());
});
return;
}
processDirResponse(res, shareId, promise);
}).onFailure(err -> {
log.error("目录解析请求失败: {}", err.getMessage());
promise.fail("目录解析失败: " + err.getMessage());
});
}
/**
* 处理目录解析响应
*/
private void processDirResponse(HttpResponse<Buffer> res, String shareId, Promise<List<FileInfo>> promise) {
try {
JsonObject jsonObject = asJson(res);
log.debug("目录解析响应: {}", jsonObject.encodePrettily());
if (!jsonObject.containsKey("list")) {
log.error("目录解析响应缺少 list 字段: {}", jsonObject);
promise.fail("目录解析失败: 响应格式错误");
return;
}
JsonArray list = jsonObject.getJsonArray("list");
ArrayList<FileInfo> result = new ArrayList<>();
list.forEach(item->{
JsonObject fileJson = (JsonObject) item;
FileInfo fileInfo = new FileInfo();
// 映射已知字段
String fileId = fileJson.getString("fileId");
String userId = fileJson.getString("userId");
// 其他参数 - 每个文件使用新的时间戳
long nowTs2 = System.currentTimeMillis();
String tsEncode2 = AESUtils.encrypt2HexIz(Long.toString(nowTs2));
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs2);
// 回传用到的参数(包含 token
JsonObject entries = JsonObject.of(
"fidEncode", fidEncode,
"uuid", uuid,
"ts", tsEncode2,
"auth", auth,
"shareId", shareId,
"appToken", token != null ? token : "");
String param = CommonUtils.urlBase64Encode(entries.encode());
if (fileJson.getInteger("fileType") == 2) {
// 如果是目录
fileInfo.setFileName(fileJson.getString("name"))
.setFileId(fileJson.getString("folderId"))
.setCreateTime(fileJson.getString("updTime"))
.setFileType("folder")
.setSize(0L)
.setSizeStr("0B")
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
// 设置目录解析的URL
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
result.add(fileInfo);
return;
}
long fileSize = fileJson.getLong("fileSize") * 1024;
fileInfo.setFileName(fileJson.getString("fileName"))
.setFileId(fileId)
.setCreateTime(fileJson.getString("createTime"))
.setFileType("file")
.setSize(fileSize)
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
.setCreateBy(fileJson.getLong("userId").toString())
.setDownloadCount(fileJson.getInteger("fileDownloads"))
.setCreateTime(fileJson.getString("updTime"))
.setFileIcon(fileJson.getString("fileIcon"))
.setPanType(shareLinkInfo.getType())
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param))
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
shareLinkInfo.getType(), param));
result.add(fileInfo);
});
promise.complete(result);
} catch (Exception e) {
log.error("处理目录响应异常: {}", e.getMessage(), e);
promise.fail("目录解析失败: " + e.getMessage());
}
}
@Override
public Future<String> parseById() {
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
String appToken = paramJson.getString("appToken", "");
// 如果有 token使用 VIP 接口
if (StringUtils.isNotBlank(appToken)) {
log.debug("parseById 使用 VIP 接口, appToken={}", appToken.substring(0, Math.min(10, appToken.length())) + "...");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL_VIP))
.putHeaders(header)
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("appToken", appToken)
.send().onSuccess(this::down).onFailure(handleFail("parseById-VIP"));
} else {
// 无 token使用免登录接口
log.debug("parseById 使用免登录接口");
webClientSession.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
.putHeaders(header)
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
.setTemplateParam("uuid", paramJson.getString("uuid"))
.setTemplateParam("ts", paramJson.getString("ts"))
.setTemplateParam("auth", paramJson.getString("auth"))
.setTemplateParam("dataKey", paramJson.getString("shareId"))
.send().onSuccess(this::down).onFailure(handleFail("parseById"));
}
return promise.future();
}
public static void resetToken() {
token = null;
authFlag = true;
}
}

View File

@@ -25,24 +25,24 @@ import java.util.Map;
* 夸克网盘解析 * 夸克网盘解析
*/ */
public class QkTool extends PanBase { public class QkTool extends PanBase {
public static final String SHARE_URL_PREFIX = "https://pan.quark.cn/s/"; public static final String SHARE_URL_PREFIX = "https://pan.quark.cn/s/";
private static final String TOKEN_URL = "https://drive-pc.quark.cn/1/clouddrive/share/sharepage/token"; private static final String TOKEN_URL = "https://drive-pc.quark.cn/1/clouddrive/share/sharepage/token";
private static final String DETAIL_URL = "https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail"; private static final String DETAIL_URL = "https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail";
private static final String DOWNLOAD_URL = "https://drive-pc.quark.cn/1/clouddrive/file/download"; private static final String DOWNLOAD_URL = "https://drive-pc.quark.cn/1/clouddrive/file/download";
// Cookie 刷新 API // Cookie 刷新 API
private static final String FLUSH_URL = "https://drive-pc.quark.cn/1/clouddrive/auth/pc/flush"; private static final String FLUSH_URL = "https://drive-pc.quark.cn/1/clouddrive/auth/pc/flush";
private static final int BATCH_SIZE = 15; // 批量获取下载链接的批次大小 private static final int BATCH_SIZE = 15; // 批量获取下载链接的批次大小
// 静态变量:缓存 __puus cookie 和过期时间 // 静态变量:缓存 __puus cookie 和过期时间
private static volatile String cachedPuus = null; private static volatile String cachedPuus = null;
private static volatile long puusExpireTime = 0; private static volatile long puusExpireTime = 0;
// __puus 有效期,默认 55 分钟(服务器实际 1 小时过期,提前 5 分钟刷新) // __puus 有效期,默认 55 分钟(服务器实际 1 小时过期,提前 5 分钟刷新)
private static final long PUUS_TTL_MS = 55 * 60 * 1000L; private static final long PUUS_TTL_MS = 55 * 60 * 1000L;
private final MultiMap header = HeaderUtils.parseHeaders(""" private final MultiMap header = HeaderUtils.parseHeaders("""
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch
Content-Type: application/json;charset=UTF-8 Content-Type: application/json;charset=UTF-8
@@ -63,7 +63,7 @@ public class QkTool extends PanBase {
if (cookie != null && !cookie.isEmpty()) { if (cookie != null && !cookie.isEmpty()) {
// 过滤出夸克网盘所需的 cookie 字段 // 过滤出夸克网盘所需的 cookie 字段
cookie = CookieUtils.filterUcQuarkCookie(cookie); cookie = CookieUtils.filterUcQuarkCookie(cookie);
// 如果有缓存的 __puus 且未过期,使用缓存的值更新 cookie // 如果有缓存的 __puus 且未过期,使用缓存的值更新 cookie
if (cachedPuus != null && System.currentTimeMillis() < puusExpireTime) { if (cachedPuus != null && System.currentTimeMillis() < puusExpireTime) {
cookie = CookieUtils.updateCookieValue(cookie, "__puus", cachedPuus); cookie = CookieUtils.updateCookieValue(cookie, "__puus", cachedPuus);
@@ -75,14 +75,14 @@ public class QkTool extends PanBase {
} }
} }
this.client = clientDisableUA; this.client = clientDisableUA;
// 如果 __puus 已过期或不存在,触发异步刷新 // 如果 __puus 已过期或不存在,触发异步刷新
if (needRefreshPuus()) { if (needRefreshPuus()) {
log.debug("夸克: __puus 需要刷新,触发异步刷新"); log.debug("夸克: __puus 需要刷新,触发异步刷新");
refreshPuusCookie(); refreshPuusCookie();
} }
} }
/** /**
* 判断是否需要刷新 __puus * 判断是否需要刷新 __puus
* @return true 表示需要刷新 * @return true 表示需要刷新
@@ -107,23 +107,23 @@ public class QkTool extends PanBase {
*/ */
public Future<Boolean> refreshPuusCookie() { public Future<Boolean> refreshPuusCookie() {
Promise<Boolean> refreshPromise = Promise.promise(); Promise<Boolean> refreshPromise = Promise.promise();
String currentCookie = header.get(HttpHeaders.COOKIE); String currentCookie = header.get(HttpHeaders.COOKIE);
if (currentCookie == null || currentCookie.isEmpty()) { if (currentCookie == null || currentCookie.isEmpty()) {
log.debug("夸克: 无 cookie跳过刷新"); log.debug("夸克: 无 cookie跳过刷新");
refreshPromise.complete(false); refreshPromise.complete(false);
return refreshPromise.future(); return refreshPromise.future();
} }
// 检查是否包含 __pus用于获取 __puus // 检查是否包含 __pus用于获取 __puus
if (!currentCookie.contains("__pus=")) { if (!currentCookie.contains("__pus=")) {
log.debug("夸克: cookie 中不包含 __pus跳过刷新"); log.debug("夸克: cookie 中不包含 __pus跳过刷新");
refreshPromise.complete(false); refreshPromise.complete(false);
return refreshPromise.future(); return refreshPromise.future();
} }
log.debug("夸克: 开始刷新 __puus cookie"); log.debug("夸克: 开始刷新 __puus cookie");
client.getAbs(FLUSH_URL) client.getAbs(FLUSH_URL)
.addQueryParam("pr", "ucpro") .addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc") .addQueryParam("fr", "pc")
@@ -134,7 +134,7 @@ public class QkTool extends PanBase {
// 从响应头获取 set-cookie // 从响应头获取 set-cookie
List<String> setCookies = res.cookies(); List<String> setCookies = res.cookies();
String newPuus = null; String newPuus = null;
for (String cookie : setCookies) { for (String cookie : setCookies) {
if (cookie.startsWith("__puus=")) { if (cookie.startsWith("__puus=")) {
// 提取 __puus 值(只取到分号前的部分) // 提取 __puus 值(只取到分号前的部分)
@@ -143,21 +143,21 @@ public class QkTool extends PanBase {
break; break;
} }
} }
if (newPuus != null) { if (newPuus != null) {
// 更新 cookie替换或添加 __puus // 更新 cookie替换或添加 __puus
String updatedCookie = CookieUtils.updateCookieValue(currentCookie, "__puus", newPuus); String updatedCookie = CookieUtils.updateCookieValue(currentCookie, "__puus", newPuus);
header.set(HttpHeaders.COOKIE, updatedCookie); header.set(HttpHeaders.COOKIE, updatedCookie);
// 同步更新 auths 中的 cookie // 同步更新 auths 中的 cookie
if (auths != null) { if (auths != null) {
auths.set("cookie", updatedCookie); auths.set("cookie", updatedCookie);
} }
// 更新静态缓存 // 更新静态缓存
cachedPuus = newPuus; cachedPuus = newPuus;
puusExpireTime = System.currentTimeMillis() + PUUS_TTL_MS; puusExpireTime = System.currentTimeMillis() + PUUS_TTL_MS;
log.info("夸克: __puus cookie 刷新成功,有效期至: {}ms", puusExpireTime); log.info("夸克: __puus cookie 刷新成功,有效期至: {}ms", puusExpireTime);
refreshPromise.complete(true); refreshPromise.complete(true);
} else { } else {
@@ -169,7 +169,7 @@ public class QkTool extends PanBase {
log.warn("夸克: 刷新 __puus cookie 失败: {}", t.getMessage()); log.warn("夸克: 刷新 __puus cookie 失败: {}", t.getMessage());
refreshPromise.complete(false); refreshPromise.complete(false);
}); });
return refreshPromise.future(); return refreshPromise.future();
} }
@@ -180,14 +180,14 @@ public class QkTool extends PanBase {
if (passcode == null) { if (passcode == null) {
passcode = ""; passcode = "";
} }
log.debug("开始解析夸克网盘分享pwd_id: {}, passcode: {}", pwdId, passcode.isEmpty() ? "" : ""); log.debug("开始解析夸克网盘分享pwd_id: {}, passcode: {}", pwdId, passcode.isEmpty() ? "" : "");
// 第一步:获取分享 token // 第一步:获取分享 token
JsonObject tokenRequest = new JsonObject() JsonObject tokenRequest = new JsonObject()
.put("pwd_id", pwdId) .put("pwd_id", pwdId)
.put("passcode", passcode); .put("passcode", passcode);
client.postAbs(TOKEN_URL) client.postAbs(TOKEN_URL)
.addQueryParam("pr", "ucpro") .addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc") .addQueryParam("fr", "pc")
@@ -196,20 +196,20 @@ public class QkTool extends PanBase {
.onSuccess(res -> { .onSuccess(res -> {
log.debug("第一阶段响应: {}", res.bodyAsString()); log.debug("第一阶段响应: {}", res.bodyAsString());
JsonObject resJson = asJson(res); JsonObject resJson = asJson(res);
if (resJson.getInteger("code") != 0) { if (resJson.getInteger("code") != 0) {
fail(TOKEN_URL + " 返回异常: " + resJson); fail(TOKEN_URL + " 返回异常: " + resJson);
return; return;
} }
String stoken = resJson.getJsonObject("data").getString("stoken"); String stoken = resJson.getJsonObject("data").getString("stoken");
if (stoken == null || stoken.isEmpty()) { if (stoken == null || stoken.isEmpty()) {
fail("无法获取分享 token可能的原因1. Cookie 已过期 2. 分享链接已失效 3. 需要提取码但未提供"); fail("无法获取分享 token可能的原因1. Cookie 已过期 2. 分享链接已失效 3. 需要提取码但未提供");
return; return;
} }
log.debug("成功获取 stoken: {}", stoken); log.debug("成功获取 stoken: {}", stoken);
// 第二步:获取文件列表 // 第二步:获取文件列表
client.getAbs(DETAIL_URL) client.getAbs(DETAIL_URL)
.addQueryParam("pr", "ucpro") .addQueryParam("pr", "ucpro")
@@ -229,85 +229,102 @@ public class QkTool extends PanBase {
.onSuccess(res2 -> { .onSuccess(res2 -> {
log.debug("第二阶段响应: {}", res2.bodyAsString()); log.debug("第二阶段响应: {}", res2.bodyAsString());
JsonObject resJson2 = asJson(res2); JsonObject resJson2 = asJson(res2);
if (resJson2.getInteger("code") != 0) { if (resJson2.getInteger("code") != 0) {
fail(DETAIL_URL + " 返回异常: " + resJson2); fail(DETAIL_URL + " 返回异常: " + resJson2);
return; return;
} }
JsonArray fileList = resJson2.getJsonObject("data").getJsonArray("list"); JsonArray fileList = resJson2.getJsonObject("data").getJsonArray("list");
if (fileList == null || fileList.isEmpty()) { if (fileList == null || fileList.isEmpty()) {
fail("未找到文件"); fail("未找到文件");
return; return;
} }
// 过滤出文件(排除文件夹) // 过滤出文件(排除文件夹)
List<JsonObject> files = new ArrayList<>(); List<JsonObject> files = new ArrayList<>();
for (int i = 0; i < fileList.size(); i++) { for (int i = 0; i < fileList.size(); i++) {
JsonObject item = fileList.getJsonObject(i); JsonObject item = fileList.getJsonObject(i);
// 判断是否为文件file=true 或 obj_category 不为空 // 判断是否为文件file=true 或 obj_category 不为空
if (item.getBoolean("file", false) || if (item.getBoolean("file", false) ||
(item.getString("obj_category") != null && !item.getString("obj_category").isEmpty())) { (item.getString("obj_category") != null && !item.getString("obj_category").isEmpty())) {
files.add(item); files.add(item);
} }
} }
if (files.isEmpty()) { if (files.isEmpty()) {
fail("没有可下载的文件(可能都是文件夹)"); fail("没有可下载的文件(可能都是文件夹)");
return; return;
} }
log.debug("找到 {} 个文件", files.size()); log.debug("找到 {} 个文件", files.size());
// 提取第一个文件信息并保存到 otherParam // 构建文件映射和文件ID列表参考 kuake.py下载结果通过 fid 回填文件信息
try {
JsonObject firstFile = files.get(0);
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(firstFile.getString("fid"))
.setFileName(firstFile.getString("file_name"))
.setSize(firstFile.getLong("size", 0L))
.setSizeStr(FileSizeConverter.convertToReadableSize(firstFile.getLong("size", 0L)))
.setFileType(firstFile.getBoolean("file", true) ? "file" : "folder")
.setCreateTime(firstFile.getString("updated_at"))
.setUpdateTime(firstFile.getString("updated_at"))
.setPanType(shareLinkInfo.getType());
// 保存到 otherParam供 CacheServiceImpl 使用
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
log.debug("夸克提取文件信息: {}", fileInfo.getFileName());
} catch (Exception e) {
log.warn("夸克提取文件信息失败,继续解析: {}", e.getMessage());
}
// 提取文件ID列表
List<String> fileIds = new ArrayList<>(); List<String> fileIds = new ArrayList<>();
Map<String, JsonObject> fileMap = new HashMap<>();
for (JsonObject file : files) { for (JsonObject file : files) {
String fid = file.getString("fid"); String fid = file.getString("fid");
if (fid != null && !fid.isEmpty()) { if (fid != null && !fid.isEmpty()) {
fileIds.add(fid); fileIds.add(fid);
fileMap.put(fid, file);
} }
} }
if (fileIds.isEmpty()) { if (fileIds.isEmpty()) {
fail("无法提取文件ID"); fail("无法提取文件ID");
return; return;
} }
// 第三步:批量获取下载链接 // 第三步:批量获取下载链接
getDownloadLinksBatch(fileIds, stoken) getDownloadLinksBatch(fileIds)
.onSuccess(downloadData -> { .onSuccess(downloadData -> {
if (downloadData.isEmpty()) { if (downloadData.isEmpty()) {
fail("未能获取到下载链接"); fail("未能获取到下载链接");
return; return;
} }
// 获取第一个文件的下载链接 // 按 fid 对齐下载结果和文件信息,取首个有效下载链接
String downloadUrl = downloadData.get(0).getString("download_url"); String downloadUrl = null;
JsonObject matchedFile = null;
for (JsonObject item : downloadData) {
String fid = item.getString("fid");
String currentUrl = item.getString("download_url");
if (currentUrl != null && !currentUrl.isEmpty() && fid != null) {
JsonObject fileMeta = fileMap.get(fid);
if (fileMeta != null) {
downloadUrl = currentUrl;
matchedFile = fileMeta;
break;
}
}
}
if (downloadUrl == null || downloadUrl.isEmpty()) { if (downloadUrl == null || downloadUrl.isEmpty()) {
fail("下载链接为空"); fail("下载链接为空");
return; return;
} }
// 提取匹配文件的信息并保存到 otherParam
if (matchedFile != null) {
try {
FileInfo fileInfo = new FileInfo();
fileInfo.setFileId(matchedFile.getString("fid"))
.setFileName(matchedFile.getString("file_name"))
.setSize(matchedFile.getLong("size", 0L))
.setSizeStr(FileSizeConverter.convertToReadableSize(matchedFile.getLong("size", 0L)))
.setFileType(matchedFile.getBoolean("file", true) ? "file" : "folder")
.setCreateTime(matchedFile.getString("updated_at"))
.setUpdateTime(matchedFile.getString("updated_at"))
.setPanType(shareLinkInfo.getType());
// 保存到 otherParam供 CacheServiceImpl 使用
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
log.debug("夸克提取文件信息: {}", fileInfo.getFileName());
} catch (Exception e) {
log.warn("夸克提取文件信息失败,继续解析: {}", e.getMessage());
}
}
// 夸克网盘需要配合下载请求头,保存下载请求头 // 夸克网盘需要配合下载请求头,保存下载请求头
Map<String, String> downloadHeaders = new HashMap<>(); Map<String, String> downloadHeaders = new HashMap<>();
downloadHeaders.put(HttpHeaders.COOKIE.toString(), header.get(HttpHeaders.COOKIE)); downloadHeaders.put(HttpHeaders.COOKIE.toString(), header.get(HttpHeaders.COOKIE));
@@ -318,28 +335,28 @@ public class QkTool extends PanBase {
completeWithMeta(downloadUrl, downloadHeaders); completeWithMeta(downloadUrl, downloadHeaders);
}) })
.onFailure(handleFail(DOWNLOAD_URL)); .onFailure(handleFail(DOWNLOAD_URL));
}).onFailure(handleFail(DETAIL_URL)); }).onFailure(handleFail(DETAIL_URL));
}) })
.onFailure(handleFail(TOKEN_URL)); .onFailure(handleFail(TOKEN_URL));
return promise.future(); return promise.future();
} }
/** /**
* 批量获取下载链接(分批处理) * 批量获取下载链接(分批处理)
*/ */
private Future<List<JsonObject>> getDownloadLinksBatch(List<String> fileIds, String stoken) { private Future<List<JsonObject>> getDownloadLinksBatch(List<String> fileIds) {
List<JsonObject> allResults = new ArrayList<>(); List<JsonObject> allResults = new ArrayList<>();
Promise<List<JsonObject>> promise = Promise.promise(); Promise<List<JsonObject>> promise = Promise.promise();
// 同步处理每个批次 // 同步处理每个批次
processBatch(fileIds, stoken, 0, allResults, promise); processBatch(fileIds, 0, allResults, promise);
return promise.future(); return promise.future();
} }
private void processBatch(List<String> fileIds, String stoken, int startIndex, List<JsonObject> allResults, Promise<List<JsonObject>> promise) { private void processBatch(List<String> fileIds, int startIndex, List<JsonObject> allResults, Promise<List<JsonObject>> promise) {
if (startIndex >= fileIds.size()) { if (startIndex >= fileIds.size()) {
// 所有批次处理完成 // 所有批次处理完成
promise.complete(allResults); promise.complete(allResults);
@@ -382,7 +399,7 @@ public class QkTool extends PanBase {
} }
// 处理下一批次 // 处理下一批次
processBatch(fileIds, stoken, endIndex, allResults, promise); processBatch(fileIds, endIndex, allResults, promise);
}) })
.onFailure(t -> promise.fail("获取下载链接失败: " + t.getMessage())); .onFailure(t -> promise.fail("获取下载链接失败: " + t.getMessage()));
} }
@@ -391,11 +408,11 @@ public class QkTool extends PanBase {
@Override @Override
public Future<List<FileInfo>> parseFileList() { public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> promise = Promise.promise(); Promise<List<FileInfo>> promise = Promise.promise();
String pwdId = shareLinkInfo.getShareKey(); String pwdId = shareLinkInfo.getShareKey();
String passcode = shareLinkInfo.getSharePassword(); String passcode = shareLinkInfo.getSharePassword();
final String finalPasscode = (passcode == null) ? "" : passcode; final String finalPasscode = (passcode == null) ? "" : passcode;
// 如果参数里的目录ID不为空则直接解析目录 // 如果参数里的目录ID不为空则直接解析目录
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId"); String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
if (dirId != null && !dirId.isEmpty()) { if (dirId != null && !dirId.isEmpty()) {
@@ -405,12 +422,12 @@ public class QkTool extends PanBase {
return promise.future(); return promise.future();
} }
} }
// 第一步:获取 stoken // 第一步:获取 stoken
JsonObject tokenRequest = new JsonObject() JsonObject tokenRequest = new JsonObject()
.put("pwd_id", pwdId) .put("pwd_id", pwdId)
.put("passcode", finalPasscode); .put("passcode", finalPasscode);
client.postAbs(TOKEN_URL) client.postAbs(TOKEN_URL)
.addQueryParam("pr", "ucpro") .addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc") .addQueryParam("fr", "pc")
@@ -432,7 +449,7 @@ public class QkTool extends PanBase {
parseDir(rootDirId, pwdId, finalPasscode, stoken, promise); parseDir(rootDirId, pwdId, finalPasscode, stoken, promise);
}) })
.onFailure(t -> promise.fail("获取 token 失败: " + t.getMessage())); .onFailure(t -> promise.fail("获取 token 失败: " + t.getMessage()));
return promise.future(); return promise.future();
} }
@@ -440,7 +457,7 @@ public class QkTool extends PanBase {
// 第二步:获取文件列表(支持指定目录) // 第二步:获取文件列表(支持指定目录)
// 夸克 API 使用 pdir_fid 参数指定父目录 ID根目录为 "0" // 夸克 API 使用 pdir_fid 参数指定父目录 ID根目录为 "0"
log.info("夸克 parseDir 开始: dirId={}, pwdId={}, stoken={}", dirId, pwdId, stoken); log.info("夸克 parseDir 开始: dirId={}, pwdId={}, stoken={}", dirId, pwdId, stoken);
client.getAbs(DETAIL_URL) client.getAbs(DETAIL_URL)
.addQueryParam("pr", "ucpro") .addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc") .addQueryParam("fr", "pc")
@@ -462,26 +479,26 @@ public class QkTool extends PanBase {
promise.fail(DETAIL_URL + " 返回异常: " + resJson); promise.fail(DETAIL_URL + " 返回异常: " + resJson);
return; return;
} }
JsonArray fileList = resJson.getJsonObject("data").getJsonArray("list"); JsonArray fileList = resJson.getJsonObject("data").getJsonArray("list");
if (fileList == null || fileList.isEmpty()) { if (fileList == null || fileList.isEmpty()) {
log.warn("夸克 API 返回的文件列表为空dirId: {}, response: {}", dirId, resJson.encodePrettily()); log.warn("夸克 API 返回的文件列表为空dirId: {}, response: {}", dirId, resJson.encodePrettily());
promise.complete(new ArrayList<>()); promise.complete(new ArrayList<>());
return; return;
} }
log.info("夸克 API 返回文件列表,总数: {}, dirId: {}", fileList.size(), dirId); log.info("夸克 API 返回文件列表,总数: {}, dirId: {}", fileList.size(), dirId);
List<FileInfo> result = new ArrayList<>(); List<FileInfo> result = new ArrayList<>();
for (int i = 0; i < fileList.size(); i++) { for (int i = 0; i < fileList.size(); i++) {
JsonObject item = fileList.getJsonObject(i); JsonObject item = fileList.getJsonObject(i);
FileInfo fileInfo = new FileInfo(); FileInfo fileInfo = new FileInfo();
// 调试打印前3个 item 的完整结构 // 调试打印前3个 item 的完整结构
if (i < 3) { if (i < 3) {
log.info("夸克 API 返回的 item[{}] 结构: {}", i, item.encodePrettily()); log.info("夸克 API 返回的 item[{}] 结构: {}", i, item.encodePrettily());
log.info("夸克 API item[{}] 所有字段名: {}", i, item.fieldNames()); log.info("夸克 API item[{}] 所有字段名: {}", i, item.fieldNames());
} }
String fid = item.getString("fid"); String fid = item.getString("fid");
String fileName = item.getString("file_name"); String fileName = item.getString("file_name");
Boolean isFile = item.getBoolean("file", true); Boolean isFile = item.getBoolean("file", true);
@@ -490,10 +507,10 @@ public class QkTool extends PanBase {
String objCategory = item.getString("obj_category"); String objCategory = item.getString("obj_category");
String shareFidToken = item.getString("share_fid_token"); String shareFidToken = item.getString("share_fid_token");
String parentId = item.getString("parent_id"); String parentId = item.getString("parent_id");
log.info("处理夸克 item[{}]: fid={}, fileName={}, parentId={}, dirId={}, isFile={}, objCategory={}", log.info("处理夸克 item[{}]: fid={}, fileName={}, parentId={}, dirId={}, isFile={}, objCategory={}",
i, fid, fileName, parentId, dirId, isFile, objCategory); i, fid, fileName, parentId, dirId, isFile, objCategory);
fileInfo.setFileId(fid) fileInfo.setFileId(fid)
.setFileName(fileName) .setFileName(fileName)
.setSize(fileSize) .setSize(fileSize)
@@ -501,7 +518,7 @@ public class QkTool extends PanBase {
.setCreateTime(updatedAt) .setCreateTime(updatedAt)
.setUpdateTime(updatedAt) .setUpdateTime(updatedAt)
.setPanType(shareLinkInfo.getType()); .setPanType(shareLinkInfo.getType());
// 判断是否为文件file=true 或 obj_category 不为空 // 判断是否为文件file=true 或 obj_category 不为空
if (isFile || (objCategory != null && !objCategory.isEmpty())) { if (isFile || (objCategory != null && !objCategory.isEmpty())) {
// 文件 // 文件
@@ -518,7 +535,7 @@ public class QkTool extends PanBase {
// 设置解析URL用于下载 // 设置解析URL用于下载
JsonObject paramJson = new JsonObject(extParams); JsonObject paramJson = new JsonObject(extParams);
String param = CommonUtils.urlBase64Encode(paramJson.encode()); String param = CommonUtils.urlBase64Encode(paramJson.encode());
fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
getDomainName(), shareLinkInfo.getType(), param)); getDomainName(), shareLinkInfo.getType(), param));
} else { } else {
// 文件夹 // 文件夹
@@ -531,18 +548,18 @@ public class QkTool extends PanBase {
String encodedUrl = URLEncoder.encode(shareLinkInfo.getShareUrl(), StandardCharsets.UTF_8.toString()); String encodedUrl = URLEncoder.encode(shareLinkInfo.getShareUrl(), StandardCharsets.UTF_8.toString());
String encodedDirId = URLEncoder.encode(fid, StandardCharsets.UTF_8.toString()); String encodedDirId = URLEncoder.encode(fid, StandardCharsets.UTF_8.toString());
String encodedStoken = URLEncoder.encode(stoken, StandardCharsets.UTF_8.toString()); String encodedStoken = URLEncoder.encode(stoken, StandardCharsets.UTF_8.toString());
fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&stoken=%s", fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&stoken=%s",
getDomainName(), encodedUrl, encodedDirId, encodedStoken)); getDomainName(), encodedUrl, encodedDirId, encodedStoken));
} catch (Exception e) { } catch (Exception e) {
// 如果编码失败,使用原始值 // 如果编码失败,使用原始值
fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&stoken=%s", fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&stoken=%s",
getDomainName(), shareLinkInfo.getShareUrl(), fid, stoken)); getDomainName(), shareLinkInfo.getShareUrl(), fid, stoken));
} }
} }
result.add(fileInfo); result.add(fileInfo);
} }
promise.complete(result); promise.complete(result);
}) })
.onFailure(t -> promise.fail("解析目录失败: " + t.getMessage())); .onFailure(t -> promise.fail("解析目录失败: " + t.getMessage()));
@@ -551,36 +568,36 @@ public class QkTool extends PanBase {
@Override @Override
public Future<String> parseById() { public Future<String> parseById() {
Promise<String> promise = Promise.promise(); Promise<String> promise = Promise.promise();
// 从 paramJson 中提取参数 // 从 paramJson 中提取参数
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson"); JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
if (paramJson == null) { if (paramJson == null) {
promise.fail("缺少必要的参数"); promise.fail("缺少必要的参数");
return promise.future(); return promise.future();
} }
String fid = paramJson.getString("fid"); String fid = paramJson.getString("fid");
String pwdId = paramJson.getString("pwd_id"); String pwdId = paramJson.getString("pwd_id");
String stoken = paramJson.getString("stoken"); String stoken = paramJson.getString("stoken");
String shareFidToken = paramJson.getString("share_fid_token"); String shareFidToken = paramJson.getString("share_fid_token");
if (fid == null || pwdId == null || stoken == null) { if (fid == null || pwdId == null || stoken == null) {
promise.fail("缺少必要的参数: fid, pwd_id 或 stoken"); promise.fail("缺少必要的参数: fid, pwd_id 或 stoken");
return promise.future(); return promise.future();
} }
log.debug("夸克 parseById: fid={}, pwd_id={}, stoken={}", fid, pwdId, stoken); log.debug("夸克 parseById: fid={}, pwd_id={}, stoken={}", fid, pwdId, stoken);
// 调用下载链接 API // 调用下载链接 API
JsonObject bodyJson = JsonObject.of() JsonObject bodyJson = JsonObject.of()
.put("fids", JsonArray.of(fid)) .put("fids", JsonArray.of(fid))
.put("pwd_id", pwdId) .put("pwd_id", pwdId)
.put("stoken", stoken); .put("stoken", stoken);
if (shareFidToken != null && !shareFidToken.isEmpty()) { if (shareFidToken != null && !shareFidToken.isEmpty()) {
bodyJson.put("fids_token", JsonArray.of(shareFidToken)); bodyJson.put("fids_token", JsonArray.of(shareFidToken));
} }
client.postAbs(DOWNLOAD_URL) client.postAbs(DOWNLOAD_URL)
.addQueryParam("pr", "ucpro") .addQueryParam("pr", "ucpro")
.addQueryParam("fr", "pc") .addQueryParam("fr", "pc")
@@ -589,17 +606,17 @@ public class QkTool extends PanBase {
.onSuccess(res -> { .onSuccess(res -> {
log.debug("夸克 parseById 响应: {}", res.bodyAsString()); log.debug("夸克 parseById 响应: {}", res.bodyAsString());
JsonObject resJson = asJson(res); JsonObject resJson = asJson(res);
if (resJson.getInteger("code") == 31001) { if (resJson.getInteger("code") == 31001) {
promise.fail("未登录或 Cookie 已失效"); promise.fail("未登录或 Cookie 已失效");
return; return;
} }
if (resJson.getInteger("code") != 0) { if (resJson.getInteger("code") != 0) {
promise.fail(DOWNLOAD_URL + " 返回异常: " + resJson); promise.fail(DOWNLOAD_URL + " 返回异常: " + resJson);
return; return;
} }
try { try {
JsonArray dataList = resJson.getJsonArray("data"); JsonArray dataList = resJson.getJsonArray("data");
if (dataList == null || dataList.isEmpty()) { if (dataList == null || dataList.isEmpty()) {
@@ -617,7 +634,7 @@ public class QkTool extends PanBase {
} }
}) })
.onFailure(t -> promise.fail("请求下载链接失败: " + t.getMessage())); .onFailure(t -> promise.fail("请求下载链接失败: " + t.getMessage()));
return promise.future(); return promise.future();
} }
} }

View File

@@ -64,7 +64,7 @@
</div> </div>
<!-- 项目简介移到卡片内 --> <!-- 项目简介移到卡片内 -->
<div class="project-intro"> <div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.2.1</div> <div class="intro-title">NFD网盘直链解析0.2.1b2</div>
<div class="intro-desc"> <div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间QQ邮箱云盘QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div> <div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间QQ邮箱云盘QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div> <div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
@@ -957,7 +957,8 @@ export default {
} else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') { } else if (panType === 'fj' || panType === 'lz' || panType === 'iz' || panType === 'le') {
// 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证 // 小飞机、蓝奏、优享、联想乐云:提示大文件需要认证
const hasAuth = this.allAuthConfigs[panType]?.cookie || const hasAuth = this.allAuthConfigs[panType]?.cookie ||
this.allAuthConfigs[panType]?.username this.allAuthConfigs[panType]?.username ||
(this.donateAccountCounts.active[panType.toUpperCase()] || 0) > 0
if (!hasAuth) { if (!hasAuth) {
this.$message.info({ this.$message.info({
message: `${panName}的大文件解析需要配置认证信息,请在"配置认证"中添加`, message: `${panName}的大文件解析需要配置认证信息,请在"配置认证"中添加`,

View File

@@ -0,0 +1,4 @@
# This file contains sensitive information and should not be committed to version control.
# It is used to store the encryption key for the application.
encrypt:
key: "nfd_secret_key_32bytes_2026_abcd"