mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-04-11 19:36:54 +00:00
Compare commits
24 Commits
fdf067c25e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a420bad305 | ||
|
|
6ef6e47580 | ||
|
|
94f83ec296 | ||
|
|
702569c701 | ||
|
|
d4940ca9ee | ||
|
|
dbd1c138ca | ||
|
|
0b49c55cf3 | ||
|
|
b1ec3b2eea | ||
|
|
9ea89feee7 | ||
|
|
4a843194a3 | ||
|
|
03503115fd | ||
|
|
1870aef60e | ||
|
|
ed8fd66d1e | ||
|
|
c1c4c8cdc5 | ||
|
|
256ec3b152 | ||
|
|
da490e5bbd | ||
|
|
ba0ac86eea | ||
|
|
b5544c4131 | ||
|
|
d94ea6aaf3 | ||
|
|
742dda8677 | ||
|
|
76e0db0cfb | ||
|
|
6458a6e2c5 | ||
|
|
cbf2294a8e | ||
|
|
9d558bf4e2 |
37
README.md
37
README.md
@@ -1,5 +1,17 @@
|
|||||||
|
<div align="center" style="display:flex; justify-content:center; gap:10px; align-items:flex-start;">
|
||||||
|
<img
|
||||||
|
src="https://github.com/user-attachments/assets/bf266d0a-aaf8-4772-9231-e38a4b7bb6cb"
|
||||||
|
alt="image1"
|
||||||
|
style="width:300px; max-width:300px; flex:none;"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://github.com/user-attachments/assets/bb7a85f0-c256-4b4a-a11b-3ceb55afc302"
|
||||||
|
alt="image2"
|
||||||
|
style="width:300px; max-width:300px; flex:none;"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/87401aae-b0b6-4ffb-bbeb-44756404d26f" alt="项目预览图" />
|
<a href="https://trendshift.io/repositories/12101" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12101" alt="qaiu%2Fnetdisk-fast-download | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -8,10 +20,6 @@
|
|||||||
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.22-blue?style=flat"></a>
|
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.22-blue?style=flat"></a>
|
||||||
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
||||||
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# netdisk-fast-download 网盘分享链接云解析服务
|
# netdisk-fast-download 网盘分享链接云解析服务
|
||||||
QQ交流群:1017480890
|
QQ交流群:1017480890
|
||||||
@@ -21,18 +29,18 @@ netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载
|
|||||||
## 快速开始
|
## 快速开始
|
||||||
命令行下载分享文件:
|
命令行下载分享文件:
|
||||||
```shell
|
```shell
|
||||||
curl -LOJ "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
|
curl -LOJ "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/Tk1F2kGQ&pwd=1234"
|
||||||
```
|
```
|
||||||
或者使用wget:
|
或者使用wget:
|
||||||
```shell
|
```shell
|
||||||
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
|
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/Tk1F2kGQ&pwd=1234"
|
||||||
```
|
```
|
||||||
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4):
|
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FTk1F2kGQ&name=bilibili.mp4&ext=mp4):
|
||||||
```
|
```
|
||||||
### 调用演示站下载:
|
### 调用演示站下载:
|
||||||
https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234
|
https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/Tk1F2kGQ&pwd=1234
|
||||||
### 调用演示站预览:
|
### 调用演示站预览:
|
||||||
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4
|
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FTk1F2kGQ&name=bilibili.mp4&ext=mp4
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -42,10 +50,10 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
|||||||
|
|
||||||
**Playground功能:** [JS解析器演练场密码保护说明](web-service/doc/PLAYGROUND_PASSWORD_PROTECTION.md)
|
**Playground功能:** [JS解析器演练场密码保护说明](web-service/doc/PLAYGROUND_PASSWORD_PROTECTION.md)
|
||||||
|
|
||||||
## 预览地址
|
## 体验地址
|
||||||
[预览地址1](https://lz.qaiu.top)
|
[公益解析1](https://lz.qaiu.top)
|
||||||
[预览地址2](https://lz0.qaiu.top)
|
[公益解析2](https://lz0.qaiu.top)
|
||||||
[天翼云盘/移动云盘限时体验版](https://189.qaiu.top)
|
[大文件解析专属版,限时开放,注册体验](https://189.qaiu.top)
|
||||||
|
|
||||||
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
|
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
|
||||||
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
|
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
|
||||||
@@ -61,7 +69,6 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
|
|||||||
|
|
||||||
- [蓝奏云-lz](https://pc.woozooo.com/)
|
- [蓝奏云-lz](https://pc.woozooo.com/)
|
||||||
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
||||||
- [奶牛快传-cow](https://cowtransfer.com/)
|
|
||||||
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
||||||
- [小飞机网盘-fj](https://www.feijipan.com/)
|
- [小飞机网盘-fj](https://www.feijipan.com/)
|
||||||
- [亿方云-fc](https://www.fangcloud.com/)
|
- [亿方云-fc](https://www.fangcloud.com/)
|
||||||
|
|||||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"mvn": "^3.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<groupId>cn.qaiu</groupId>
|
<groupId>cn.qaiu</groupId>
|
||||||
<artifactId>parser</artifactId>
|
<artifactId>parser</artifactId>
|
||||||
<version>10.2.3</version>
|
<version>10.2.5</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>cn.qaiu:parser</name>
|
<name>cn.qaiu:parser</name>
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ public enum PanDomainTemplate {
|
|||||||
"lanzv|" +
|
"lanzv|" +
|
||||||
"dmpdmp|" +
|
"dmpdmp|" +
|
||||||
"lanrar|" +
|
"lanrar|" +
|
||||||
"webgetstore|" +
|
|
||||||
"lanzb|" +
|
"lanzb|" +
|
||||||
"lanzoux|" +
|
"lanzoux|" +
|
||||||
"lanzout|" +
|
"lanzout|" +
|
||||||
@@ -103,7 +102,7 @@ public enum PanDomainTemplate {
|
|||||||
"lanzoui|" +
|
"lanzoui|" +
|
||||||
"lanzoug|" +
|
"lanzoug|" +
|
||||||
"lanzoum" +
|
"lanzoum" +
|
||||||
")\\.com/(.+/)?(?<KEY>.+)"),
|
")\\.com/(?<KEY>.+)"),
|
||||||
"https://w1.lanzn.com/{shareKey}",
|
"https://w1.lanzn.com/{shareKey}",
|
||||||
LzTool.class),
|
LzTool.class),
|
||||||
|
|
||||||
@@ -122,7 +121,7 @@ public enum PanDomainTemplate {
|
|||||||
|
|
||||||
// https://v2.fangcloud.com/s/
|
// https://v2.fangcloud.com/s/
|
||||||
FC("亿方云",
|
FC("亿方云",
|
||||||
compile("https://v2\\.fangcloud\\.(com|cn)/(s|sharing)/(?<KEY>.+)"),
|
compile("https://v2\\.fangcloud\\.(com|cn)/(s|share|sharing)/(?<KEY>.+)"),
|
||||||
"https://v2.fangcloud.com/s/{shareKey}",
|
"https://v2.fangcloud.com/s/{shareKey}",
|
||||||
"https://www.fangcloud.com/",
|
"https://www.fangcloud.com/",
|
||||||
FcTool.class),
|
FcTool.class),
|
||||||
@@ -143,9 +142,41 @@ public enum PanDomainTemplate {
|
|||||||
compile("https://qfile\\.qq\\.com/q/(?<KEY>.+)"),
|
compile("https://qfile\\.qq\\.com/q/(?<KEY>.+)"),
|
||||||
"https://qfile.qq.com/q/{shareKey}",
|
"https://qfile.qq.com/q/{shareKey}",
|
||||||
QQscTool.class),
|
QQscTool.class),
|
||||||
// https://f.ws59.cn/f/或者https://www.wenshushu.cn/f/
|
// https://f.ws59.cn/f/ 或者 https://www.wenshushu.cn/f/ 等多个镜像域名
|
||||||
|
/*
|
||||||
|
f.wsNN.cn (如 f.ws59.cn, f.ws28.cn 等)
|
||||||
|
www.wenshushu.cn
|
||||||
|
新增域名:
|
||||||
|
www.wenxiaozhan.net
|
||||||
|
www.wenxiaozhan.cn
|
||||||
|
www.wss.show
|
||||||
|
www.ws28.cn
|
||||||
|
www.wss.email
|
||||||
|
www.wss1.cn
|
||||||
|
www.ws59.cn
|
||||||
|
www.wss.cc
|
||||||
|
www.wss.pet
|
||||||
|
www.wss.ink
|
||||||
|
www.wenxiaozhan.com
|
||||||
|
www.wenshushu.com
|
||||||
|
www.wss.zone
|
||||||
|
*/
|
||||||
WS("文叔叔",
|
WS("文叔叔",
|
||||||
compile("https://(f\\.ws(\\d{2})\\.cn|www\\.wenshushu\\.cn)/f/(?<KEY>.+)"),
|
compile("https://(f\\.ws(\\d{2})\\.cn|" +
|
||||||
|
"www\\.wenxiaozhan\\.net|" +
|
||||||
|
"www\\.wenxiaozhan\\.cn|" +
|
||||||
|
"www\\.wss\\.show|" +
|
||||||
|
"www\\.ws28\\.cn|" +
|
||||||
|
"www\\.wss\\.email|" +
|
||||||
|
"www\\.wss1\\.cn|" +
|
||||||
|
"www\\.ws59\\.cn|" +
|
||||||
|
"www\\.wss\\.cc|" +
|
||||||
|
"www\\.wss\\.pet|" +
|
||||||
|
"www\\.wss\\.ink|" +
|
||||||
|
"www\\.wenxiaozhan\\.com|" +
|
||||||
|
"www\\.wenshushu\\.com|" +
|
||||||
|
"www\\.wss\\.zone|" +
|
||||||
|
"www\\.wenshushu\\.cn)/f/(?<KEY>.+)"),
|
||||||
"https://www.wenshushu.cn/f/{shareKey}",
|
"https://www.wenshushu.cn/f/{shareKey}",
|
||||||
WsTool.class),
|
WsTool.class),
|
||||||
// https://www.123pan.com/s/
|
// https://www.123pan.com/s/
|
||||||
@@ -199,7 +230,7 @@ public enum PanDomainTemplate {
|
|||||||
"123635\\.com|" +
|
"123635\\.com|" +
|
||||||
"123242\\.com|" +
|
"123242\\.com|" +
|
||||||
"123795\\.com" +
|
"123795\\.com" +
|
||||||
")/s/(?<KEY>.+)(.html)?"),
|
")/s/(?<KEY>[a-zA-Z0-9_-]+)(?:\\.html)?"),
|
||||||
"https://www.123pan.com/s/{shareKey}",
|
"https://www.123pan.com/s/{shareKey}",
|
||||||
Ye2Tool.class),
|
Ye2Tool.class),
|
||||||
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
|
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class LzTool extends PanBase {
|
|||||||
String html = asText(res);
|
String html = asText(res);
|
||||||
if (html.contains("var arg1='")) {
|
if (html.contains("var arg1='")) {
|
||||||
webClientSession = WebClientSession.create(clientNoRedirects);
|
webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
setCookie(html);
|
setCookie(html, sUrl);
|
||||||
webClientSession.getAbs(sUrl)
|
webClientSession.getAbs(sUrl)
|
||||||
.putHeaders(headers0)
|
.putHeaders(headers0)
|
||||||
.send().onSuccess(res2 -> {
|
.send().onSuccess(res2 -> {
|
||||||
@@ -81,6 +81,29 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doParser(String html, String pwd, String sUrl) {
|
private void doParser(String html, String pwd, String sUrl) {
|
||||||
|
// 检测是否为目录分享链接 (含 /s/、/b/ 路径段或 b0 开头的路径段)
|
||||||
|
if (sUrl.matches(".*/(s|b)/[^/]+.*") || sUrl.matches(".*/b0[^/]+.*")) {
|
||||||
|
fail("该链接为蓝奏云目录分享,请使用目录解析接口");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 若仍是校验页 (parse()中cookie域名与实际URL不匹配时会出现), 重试一次
|
||||||
|
if (html.contains("var arg1='")) {
|
||||||
|
webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
|
setCookie(html, sUrl);
|
||||||
|
webClientSession.getAbs(sUrl).putHeaders(headers0).send().onSuccess(res -> {
|
||||||
|
String html2 = asText(res);
|
||||||
|
if (html2.contains("var arg1='")) {
|
||||||
|
fail("蓝奏云反爬校验失败,请稍后重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doParserInternal(html2, pwd, sUrl);
|
||||||
|
}).onFailure(handleFail(sUrl));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doParserInternal(html, pwd, sUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doParserInternal(String html, String pwd, String sUrl) {
|
||||||
try {
|
try {
|
||||||
setFileInfo(html, shareLinkInfo);
|
setFileInfo(html, shareLinkInfo);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -98,20 +121,18 @@ public class LzTool extends PanBase {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
fail(e, "js引擎执行失败");
|
fail(e, "js引擎执行失败");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// 没有密码
|
// 没有密码
|
||||||
String iframePath = matcher.group(1);
|
String iframePath = matcher.group(1);
|
||||||
String absoluteURI = SHARE_URL_PREFIX + iframePath;
|
String absoluteURI = SHARE_URL_PREFIX + iframePath;
|
||||||
webClientSession.getAbs(absoluteURI).putHeaders(headers0).send().onSuccess(res2 -> {
|
webClientSession.getAbs(absoluteURI).putHeaders(headers0).send().onSuccess(res2 -> {
|
||||||
String html2= asText(res2);
|
String html2 = asText(res2);
|
||||||
// Matcher matcher2 = Pattern.compile("'sign'\s*:\s*'(\\w+)'").matcher(html2);
|
|
||||||
String jsText = getJsText(html2);
|
String jsText = getJsText(html2);
|
||||||
if (jsText == null) {
|
if (jsText == null) {
|
||||||
headers0.add("Referer", absoluteURI);
|
headers0.add("Referer", absoluteURI);
|
||||||
setCookie(html2);
|
setCookie(html2, absoluteURI);
|
||||||
webClientSession.getAbs(absoluteURI).send().onSuccess(res3 -> {
|
webClientSession.getAbs(absoluteURI).send().onSuccess(res3 -> {
|
||||||
String html3= asText(res3);
|
String html3 = asText(res3);
|
||||||
String jsText3 = getJsText(html3);
|
String jsText3 = getJsText(html3);
|
||||||
if (jsText3 != null) {
|
if (jsText3 != null) {
|
||||||
try {
|
try {
|
||||||
@@ -121,9 +142,7 @@ public class LzTool extends PanBase {
|
|||||||
fail(e, "引擎执行失败");
|
fail(e, "引擎执行失败");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": 获取失败0, 可能分享已失效");
|
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": 获取失败0, 可能分享已失效");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -138,14 +157,29 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCookie(String html2) {
|
private void setCookie(String html, String url) {
|
||||||
int beginIndex = html2.indexOf("arg1='") + 6;
|
int beginIndex = html.indexOf("arg1='") + 6;
|
||||||
String arg1 = html2.substring(beginIndex, html2.indexOf("';", beginIndex));
|
int endIndex = html.indexOf("';", beginIndex);
|
||||||
|
if (beginIndex < 6 || endIndex == -1 || endIndex <= beginIndex) {
|
||||||
|
fail("蓝奏云反爬 arg1 Cookie 解析失败,页面内容异常");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String arg1 = html.substring(beginIndex, endIndex);
|
||||||
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
|
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
|
||||||
|
// 从 URL 中动态提取域名(如 lanzoum.com, lanzoux.com 等)
|
||||||
|
String domain = ".lanzn.com"; // 默认兜底
|
||||||
|
try {
|
||||||
|
java.net.URL urlObj = new java.net.URL(url);
|
||||||
|
String host = urlObj.getHost(); // e.g. "dzvip.lanzoum.com"
|
||||||
|
int firstDot = host.indexOf('.');
|
||||||
|
if (firstDot >= 0) {
|
||||||
|
domain = host.substring(firstDot); // e.g. ".lanzoum.com"
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
// 创建一个 Cookie 并放入 CookieStore
|
// 创建一个 Cookie 并放入 CookieStore
|
||||||
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
||||||
nettyCookie.setDomain(".lanzn.com"); // 设置域名
|
nettyCookie.setDomain(domain);
|
||||||
nettyCookie.setPath("/"); // 设置路径
|
nettyCookie.setPath("/");
|
||||||
nettyCookie.setSecure(false);
|
nettyCookie.setSecure(false);
|
||||||
nettyCookie.setHttpOnly(false);
|
nettyCookie.setHttpOnly(false);
|
||||||
webClientSession.cookieStore().put(nettyCookie);
|
webClientSession.cookieStore().put(nettyCookie);
|
||||||
@@ -218,7 +252,7 @@ public class LzTool extends PanBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 文件名
|
// 文件名
|
||||||
if (urlJson.containsKey("inf") && urlJson.getMap().get("inf") instanceof Character) {
|
if (urlJson.containsKey("inf") && urlJson.getMap().get("inf") instanceof CharSequence) {
|
||||||
((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setFileName(name);
|
((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setFileName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,10 +268,18 @@ public class LzTool extends PanBase {
|
|||||||
int beginIndex = text.indexOf("arg1='") + 6;
|
int beginIndex = text.indexOf("arg1='") + 6;
|
||||||
String arg1 = text.substring(beginIndex, text.indexOf("';", beginIndex));
|
String arg1 = text.substring(beginIndex, text.indexOf("';", beginIndex));
|
||||||
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
|
String acw_sc__v2 = AcwScV2Generator.acwScV2Simple(arg1);
|
||||||
|
// 从 downUrl 中动态提取域名
|
||||||
|
String downDomain = ".lanrar.com";
|
||||||
|
try {
|
||||||
|
java.net.URL du = new java.net.URL(downUrl);
|
||||||
|
String h = du.getHost();
|
||||||
|
int dot = h.indexOf('.');
|
||||||
|
if (dot >= 0) downDomain = h.substring(dot);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
// 创建一个 Cookie 并放入 CookieStore
|
// 创建一个 Cookie 并放入 CookieStore
|
||||||
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
|
||||||
nettyCookie.setDomain(".lanrar.com"); // 设置域名
|
nettyCookie.setDomain(downDomain);
|
||||||
nettyCookie.setPath("/"); // 设置路径
|
nettyCookie.setPath("/");
|
||||||
nettyCookie.setSecure(false);
|
nettyCookie.setSecure(false);
|
||||||
nettyCookie.setHttpOnly(false);
|
nettyCookie.setHttpOnly(false);
|
||||||
WebClientSession webClientSession2 = WebClientSession.create(clientNoRedirects);
|
WebClientSession webClientSession2 = WebClientSession.create(clientNoRedirects);
|
||||||
@@ -295,14 +337,14 @@ public class LzTool extends PanBase {
|
|||||||
String pwd = shareLinkInfo.getSharePassword();
|
String pwd = shareLinkInfo.getSharePassword();
|
||||||
|
|
||||||
webClientSession.getAbs(sUrl).send().onSuccess(res -> {
|
webClientSession.getAbs(sUrl).send().onSuccess(res -> {
|
||||||
String html = res.bodyAsString();
|
String html = asText(res);
|
||||||
// 检查是否需要 cookie 验证
|
// 检查是否需要 cookie 验证
|
||||||
if (html.contains("var arg1='")) {
|
if (html.contains("var arg1='")) {
|
||||||
webClientSession = WebClientSession.create(clientNoRedirects);
|
webClientSession = WebClientSession.create(clientNoRedirects);
|
||||||
setCookie(html);
|
setCookie(html, sUrl);
|
||||||
// 重新请求
|
// 重新请求
|
||||||
webClientSession.getAbs(sUrl).send().onSuccess(res2 -> {
|
webClientSession.getAbs(sUrl).send().onSuccess(res2 -> {
|
||||||
handleFileListParse(res2.bodyAsString(), pwd, sUrl, promise);
|
handleFileListParse(asText(res2), pwd, sUrl, promise);
|
||||||
}).onFailure(err -> promise.fail(err));
|
}).onFailure(err -> promise.fail(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -312,6 +354,11 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleFileListParse(String html, String pwd, String sUrl, Promise<List<FileInfo>> promise) {
|
private void handleFileListParse(String html, String pwd, String sUrl, Promise<List<FileInfo>> promise) {
|
||||||
|
// 检测是否为文件分享链接 (不含 /s/、/b/ 路径段且不含 b0 开头的路径段)
|
||||||
|
if (!sUrl.matches(".*/(s|b)/[^/]+.*") && !sUrl.matches(".*/b0[^/]+.*")) {
|
||||||
|
promise.fail(baseMsg() + "该链接为蓝奏云文件分享,请使用文件解析接口");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
|
String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
|
||||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
|
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
|
||||||
@@ -321,12 +368,12 @@ public class LzTool extends PanBase {
|
|||||||
log.debug("解析参数: {}", map);
|
log.debug("解析参数: {}", map);
|
||||||
MultiMap headers = getHeaders(sUrl);
|
MultiMap headers = getHeaders(sUrl);
|
||||||
|
|
||||||
String url = SHARE_URL_PREFIX + "/filemoreajax.php?file=" + data.get("fid");
|
String url = SHARE_URL_PREFIX + "filemoreajax.php?file=" + data.get("fid");
|
||||||
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
||||||
String resBody = asText(res2);
|
String resBody = asText(res2);
|
||||||
// 再次检查是否需要 cookie 验证
|
// 再次检查是否需要 cookie 验证
|
||||||
if (resBody.contains("var arg1='")) {
|
if (resBody.contains("var arg1='")) {
|
||||||
setCookie(resBody);
|
setCookie(resBody, url);
|
||||||
// 重新请求
|
// 重新请求
|
||||||
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res3 -> {
|
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res3 -> {
|
||||||
handleFileListResponse(asText(res3), promise);
|
handleFileListResponse(asText(res3), promise);
|
||||||
@@ -335,7 +382,7 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
handleFileListResponse(resBody, promise);
|
handleFileListResponse(resBody, promise);
|
||||||
}).onFailure(err -> promise.fail(err));
|
}).onFailure(err -> promise.fail(err));
|
||||||
} catch (ScriptException | NoSuchMethodException e) {
|
} catch (ScriptException | NoSuchMethodException | RuntimeException e) {
|
||||||
promise.fail(e);
|
promise.fail(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,14 +414,20 @@ public class LzTool extends PanBase {
|
|||||||
Long sizeNum = FileSizeConverter.convertToBytes(size);
|
Long sizeNum = FileSizeConverter.convertToBytes(size);
|
||||||
String panType = shareLinkInfo.getType();
|
String panType = shareLinkInfo.getType();
|
||||||
String id = fileJson.getString("id");
|
String id = fileJson.getString("id");
|
||||||
fileInfo.setFileName(fileJson.getString("name_all"))
|
String fileName = fileJson.getString("name_all");
|
||||||
|
// 构建 base64 参数,用于 /v2/redirectUrl 接口
|
||||||
|
JsonObject paramJson = new JsonObject()
|
||||||
|
.put("id", id)
|
||||||
|
.put("fileName", fileName);
|
||||||
|
String param = CommonUtils.urlBase64Encode(paramJson.encode());
|
||||||
|
fileInfo.setFileName(fileName)
|
||||||
.setFileId(id)
|
.setFileId(id)
|
||||||
.setCreateTime(fileJson.getString("time"))
|
.setCreateTime(fileJson.getString("time"))
|
||||||
.setFileType(fileJson.getString("icon"))
|
.setFileType(fileJson.getString("icon"))
|
||||||
.setSizeStr(fileJson.getString("size"))
|
.setSizeStr(fileJson.getString("size"))
|
||||||
.setSize(sizeNum)
|
.setSize(sizeNum)
|
||||||
.setPanType(panType)
|
.setPanType(panType)
|
||||||
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id)
|
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(), panType, param))
|
||||||
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
|
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
|
||||||
shareLinkInfo.getType(), id));
|
shareLinkInfo.getType(), id));
|
||||||
log.debug("文件信息: {}", fileInfo);
|
log.debug("文件信息: {}", fileInfo);
|
||||||
@@ -386,6 +439,15 @@ public class LzTool extends PanBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parseById() {
|
||||||
|
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
||||||
|
String id = paramJson.getString("id");
|
||||||
|
// 以文件ID重新构造标准访问URL,复用 parse() 流程
|
||||||
|
shareLinkInfo.setStandardUrl(SHARE_URL_PREFIX + id);
|
||||||
|
return parse();
|
||||||
|
}
|
||||||
|
|
||||||
void setFileInfo(String html, ShareLinkInfo shareLinkInfo) {
|
void setFileInfo(String html, ShareLinkInfo shareLinkInfo) {
|
||||||
// 写入 fileInfo
|
// 写入 fileInfo
|
||||||
FileInfo fileInfo = new FileInfo();
|
FileInfo fileInfo = new FileInfo();
|
||||||
@@ -400,16 +462,17 @@ public class LzTool extends PanBase {
|
|||||||
String fileId = CommonUtils.extract(html, Pattern.compile("\\?f=(.*?)&|fid = (.*?);"));
|
String fileId = CommonUtils.extract(html, Pattern.compile("\\?f=(.*?)&|fid = (.*?);"));
|
||||||
String createTime = CommonUtils.extract(html, Pattern.compile(">上传时间:</span>(.*?)<"));
|
String createTime = CommonUtils.extract(html, Pattern.compile(">上传时间:</span>(.*?)<"));
|
||||||
try {
|
try {
|
||||||
long bytes = FileSizeConverter.convertToBytes(sizeStr);
|
|
||||||
fileInfo.setFileName(fileName)
|
fileInfo.setFileName(fileName)
|
||||||
.setSize(bytes)
|
|
||||||
.setSizeStr(FileSizeConverter.convertToReadableSize(bytes))
|
|
||||||
.setCreateBy(createBy)
|
.setCreateBy(createBy)
|
||||||
.setPanType(shareLinkInfo.getType())
|
.setPanType(shareLinkInfo.getType())
|
||||||
.setDescription(description)
|
.setDescription(description)
|
||||||
.setFileType("file")
|
.setFileType("file")
|
||||||
.setFileId(fileId)
|
.setFileId(fileId)
|
||||||
.setCreateTime(createTime);
|
.setCreateTime(createTime);
|
||||||
|
if (sizeStr != null && !sizeStr.isBlank()) {
|
||||||
|
long bytes = FileSizeConverter.convertToBytes(sizeStr);
|
||||||
|
fileInfo.setSize(bytes).setSizeStr(FileSizeConverter.convertToReadableSize(bytes));
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("文件信息解析异常", e);
|
log.warn("文件信息解析异常", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 夸克网盘解析
|
* 夸克网盘解析 - 修复版
|
||||||
|
* 重点修复了 Cookie 换行符处理和请求头一致性问题
|
||||||
*/
|
*/
|
||||||
public class QkTool extends PanBase {
|
public class QkTool extends PanBase {
|
||||||
|
|
||||||
@@ -31,610 +32,232 @@ public class QkTool extends PanBase {
|
|||||||
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
|
|
||||||
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 和过期时间
|
// 缓存变量
|
||||||
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 分钟刷新)
|
|
||||||
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("""
|
// 严格模拟夸克 PC 客户端的请求头
|
||||||
|
private final MultiMap commonHeaders = 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
|
Accept: application/json, text/plain, */*
|
||||||
Referer: https://pan.quark.cn/
|
Referer: https://pan.quark.cn/
|
||||||
Origin: https://pan.quark.cn
|
Origin: https://pan.quark.cn
|
||||||
Accept: application/json, text/plain, */*
|
Accept-Language: zh-CN,zh;q=0.9
|
||||||
|
Content-Type: application/json
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// 保存 auths 引用,用于更新 cookie
|
|
||||||
private MultiMap auths;
|
private MultiMap auths;
|
||||||
|
|
||||||
public QkTool(ShareLinkInfo shareLinkInfo) {
|
public QkTool(ShareLinkInfo shareLinkInfo) {
|
||||||
super(shareLinkInfo);
|
super(shareLinkInfo);
|
||||||
// 参考 UcTool 实现,从认证配置中取 cookie 放到请求头
|
|
||||||
if (shareLinkInfo.getOtherParam() != null && shareLinkInfo.getOtherParam().containsKey("auths")) {
|
if (shareLinkInfo.getOtherParam() != null && shareLinkInfo.getOtherParam().containsKey("auths")) {
|
||||||
auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
auths = (MultiMap) shareLinkInfo.getOtherParam().get("auths");
|
||||||
String cookie = auths.get("cookie");
|
String rawCookie = auths.get("cookie");
|
||||||
if (cookie != null && !cookie.isEmpty()) {
|
|
||||||
// 过滤出夸克网盘所需的 cookie 字段
|
if (rawCookie != null && !rawCookie.isEmpty()) {
|
||||||
cookie = CookieUtils.filterUcQuarkCookie(cookie);
|
// 【核心修复】将所有的换行符替换为分号,并清理多余空格,防止 Header 截断
|
||||||
|
String cleanedCookie = rawCookie.replace("\r\n", "; ").replace("\n", "; ")
|
||||||
|
.replaceAll(";\\s*;", ";")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// 此时 cleanedCookie 已经是单行规范格式
|
||||||
|
cleanedCookie = CookieUtils.filterUcQuarkCookie(cleanedCookie);
|
||||||
|
|
||||||
// 如果有缓存的 __puus 且未过期,使用缓存的值更新 cookie
|
|
||||||
if (cachedPuus != null && System.currentTimeMillis() < puusExpireTime) {
|
if (cachedPuus != null && System.currentTimeMillis() < puusExpireTime) {
|
||||||
cookie = CookieUtils.updateCookieValue(cookie, "__puus", cachedPuus);
|
cleanedCookie = CookieUtils.updateCookieValue(cleanedCookie, "__puus", cachedPuus);
|
||||||
log.debug("夸克: 使用缓存的 __puus (剩余有效期: {}s)", (puusExpireTime - System.currentTimeMillis()) / 1000);
|
log.debug("夸克: 使用缓存的 __puus (剩余有效期: {}s)", (puusExpireTime - System.currentTimeMillis()) / 1000);
|
||||||
}
|
}
|
||||||
header.set(HttpHeaders.COOKIE, cookie);
|
|
||||||
// 同步更新 auths
|
commonHeaders.set(HttpHeaders.COOKIE, cleanedCookie);
|
||||||
auths.set("cookie", cookie);
|
auths.set("cookie", cleanedCookie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.client = clientDisableUA;
|
this.client = clientDisableUA;
|
||||||
|
|
||||||
// 如果 __puus 已过期或不存在,触发异步刷新
|
|
||||||
if (needRefreshPuus()) {
|
if (needRefreshPuus()) {
|
||||||
log.debug("夸克: __puus 需要刷新,触发异步刷新");
|
|
||||||
refreshPuusCookie();
|
refreshPuusCookie();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否需要刷新 __puus
|
|
||||||
* @return true 表示需要刷新
|
|
||||||
*/
|
|
||||||
private boolean needRefreshPuus() {
|
private boolean needRefreshPuus() {
|
||||||
String currentCookie = header.get(HttpHeaders.COOKIE);
|
String currentCookie = commonHeaders.get(HttpHeaders.COOKIE);
|
||||||
if (currentCookie == null || currentCookie.isEmpty()) {
|
if (currentCookie == null || !currentCookie.contains("__pus=")) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 必须包含 __pus 才能刷新
|
|
||||||
if (!currentCookie.contains("__pus=")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 缓存过期或不存在时需要刷新
|
|
||||||
return cachedPuus == null || System.currentTimeMillis() >= puusExpireTime;
|
return cachedPuus == null || System.currentTimeMillis() >= puusExpireTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新 __puus Cookie
|
|
||||||
* 通过调用 auth/pc/flush API,服务器会返回 set-cookie 来更新 __puus
|
|
||||||
* @return Future 包含是否刷新成功
|
|
||||||
*/
|
|
||||||
public Future<Boolean> refreshPuusCookie() {
|
public Future<Boolean> refreshPuusCookie() {
|
||||||
Promise<Boolean> refreshPromise = Promise.promise();
|
Promise<Boolean> refreshPromise = Promise.promise();
|
||||||
|
String currentCookie = commonHeaders.get(HttpHeaders.COOKIE);
|
||||||
String currentCookie = header.get(HttpHeaders.COOKIE);
|
if (currentCookie == null || !currentCookie.contains("__pus=")) {
|
||||||
if (currentCookie == null || currentCookie.isEmpty()) {
|
|
||||||
log.debug("夸克: 无 cookie,跳过刷新");
|
|
||||||
refreshPromise.complete(false);
|
refreshPromise.complete(false);
|
||||||
return refreshPromise.future();
|
return refreshPromise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否包含 __pus(用于获取 __puus)
|
|
||||||
if (!currentCookie.contains("__pus=")) {
|
|
||||||
log.debug("夸克: cookie 中不包含 __pus,跳过刷新");
|
|
||||||
refreshPromise.complete(false);
|
|
||||||
return refreshPromise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("夸克: 开始刷新 __puus cookie");
|
|
||||||
|
|
||||||
client.getAbs(FLUSH_URL)
|
client.getAbs(FLUSH_URL)
|
||||||
.addQueryParam("pr", "ucpro")
|
.addQueryParam("pr", "ucpro")
|
||||||
.addQueryParam("fr", "pc")
|
.addQueryParam("fr", "pc")
|
||||||
.addQueryParam("uc_param_str", "")
|
.putHeaders(commonHeaders)
|
||||||
.putHeaders(header)
|
|
||||||
.send()
|
.send()
|
||||||
.onSuccess(res -> {
|
.onSuccess(res -> {
|
||||||
// 从响应头获取 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 值(只取到分号前的部分)
|
|
||||||
int endIndex = cookie.indexOf(';');
|
int endIndex = cookie.indexOf(';');
|
||||||
newPuus = endIndex > 0 ? cookie.substring(0, endIndex) : cookie;
|
newPuus = endIndex > 0 ? cookie.substring(0, endIndex) : cookie;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPuus != null) {
|
if (newPuus != null) {
|
||||||
// 更新 cookie:替换或添加 __puus
|
|
||||||
String updatedCookie = CookieUtils.updateCookieValue(currentCookie, "__puus", newPuus);
|
String updatedCookie = CookieUtils.updateCookieValue(currentCookie, "__puus", newPuus);
|
||||||
header.set(HttpHeaders.COOKIE, updatedCookie);
|
commonHeaders.set(HttpHeaders.COOKIE, updatedCookie);
|
||||||
|
if (auths != null) auths.set("cookie", updatedCookie);
|
||||||
// 同步更新 auths 中的 cookie
|
|
||||||
if (auths != null) {
|
|
||||||
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);
|
|
||||||
refreshPromise.complete(true);
|
refreshPromise.complete(true);
|
||||||
} else {
|
} else {
|
||||||
log.debug("夸克: 响应中未包含 __puus,可能 cookie 仍然有效");
|
|
||||||
refreshPromise.complete(false);
|
refreshPromise.complete(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onFailure(t -> {
|
.onFailure(t -> refreshPromise.complete(false));
|
||||||
log.warn("夸克: 刷新 __puus cookie 失败: {}", t.getMessage());
|
|
||||||
refreshPromise.complete(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return refreshPromise.future();
|
return refreshPromise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
String pwdId = shareLinkInfo.getShareKey();
|
String pwdId = shareLinkInfo.getShareKey();
|
||||||
String passcode = shareLinkInfo.getSharePassword();
|
String passcode = shareLinkInfo.getSharePassword() == null ? "" : shareLinkInfo.getSharePassword();
|
||||||
if (passcode == null) {
|
|
||||||
passcode = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("开始解析夸克网盘分享,pwd_id: {}, passcode: {}", pwdId, passcode.isEmpty() ? "无" : "有");
|
log.debug("开始解析夸克分享: {}", pwdId);
|
||||||
|
|
||||||
// 第一步:获取分享 token
|
|
||||||
JsonObject tokenRequest = new JsonObject()
|
|
||||||
.put("pwd_id", pwdId)
|
|
||||||
.put("passcode", passcode);
|
|
||||||
|
|
||||||
|
// 1. 获取 Token
|
||||||
|
JsonObject tokenBody = new JsonObject().put("pwd_id", pwdId).put("passcode", passcode);
|
||||||
client.postAbs(TOKEN_URL)
|
client.postAbs(TOKEN_URL)
|
||||||
.addQueryParam("pr", "ucpro")
|
.addQueryParam("pr", "ucpro")
|
||||||
.addQueryParam("fr", "pc")
|
.addQueryParam("fr", "pc")
|
||||||
.putHeaders(header)
|
.putHeaders(commonHeaders)
|
||||||
.sendJsonObject(tokenRequest)
|
.sendJsonObject(tokenBody)
|
||||||
.onSuccess(res -> {
|
.onSuccess(res -> {
|
||||||
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 获取失败: " + resJson.getString("message"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String stoken = resJson.getJsonObject("data").getString("stoken");
|
String stoken = resJson.getJsonObject("data").getString("stoken");
|
||||||
if (stoken == null || stoken.isEmpty()) {
|
log.debug("成功获取 stoken");
|
||||||
fail("无法获取分享 token,可能的原因:1. Cookie 已过期 2. 分享链接已失效 3. 需要提取码但未提供");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("成功获取 stoken: {}", stoken);
|
// 2. 获取详情
|
||||||
|
|
||||||
// 第二步:获取文件列表
|
|
||||||
client.getAbs(DETAIL_URL)
|
client.getAbs(DETAIL_URL)
|
||||||
.addQueryParam("pr", "ucpro")
|
.addQueryParam("pr", "ucpro")
|
||||||
.addQueryParam("fr", "pc")
|
.addQueryParam("fr", "pc")
|
||||||
.addQueryParam("pwd_id", pwdId)
|
.addQueryParam("pwd_id", pwdId)
|
||||||
.addQueryParam("stoken", stoken)
|
.addQueryParam("stoken", stoken)
|
||||||
.addQueryParam("pdir_fid", "0")
|
.addQueryParam("pdir_fid", "0")
|
||||||
.addQueryParam("force", "0")
|
|
||||||
.addQueryParam("_page", "1")
|
|
||||||
.addQueryParam("_size", "50")
|
.addQueryParam("_size", "50")
|
||||||
.addQueryParam("_fetch_banner", "1")
|
.putHeaders(commonHeaders)
|
||||||
.addQueryParam("_fetch_share", "1")
|
|
||||||
.addQueryParam("_fetch_total", "1")
|
|
||||||
.addQueryParam("_sort", "file_type:asc,updated_at:desc")
|
|
||||||
.putHeaders(header)
|
|
||||||
.send()
|
.send()
|
||||||
.onSuccess(res2 -> {
|
.onSuccess(res2 -> {
|
||||||
log.debug("第二阶段响应: {}", res2.bodyAsString());
|
|
||||||
JsonObject resJson2 = asJson(res2);
|
JsonObject resJson2 = asJson(res2);
|
||||||
|
|
||||||
if (resJson2.getInteger("code") != 0) {
|
|
||||||
fail(DETAIL_URL + " 返回异常: " + resJson2);
|
|
||||||
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<>();
|
|
||||||
for (int i = 0; i < fileList.size(); i++) {
|
|
||||||
JsonObject item = fileList.getJsonObject(i);
|
|
||||||
// 判断是否为文件:file=true 或 obj_category 不为空
|
|
||||||
if (item.getBoolean("file", false) ||
|
|
||||||
(item.getString("obj_category") != null && !item.getString("obj_category").isEmpty())) {
|
|
||||||
files.add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.isEmpty()) {
|
|
||||||
fail("没有可下载的文件(可能都是文件夹)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("找到 {} 个文件", files.size());
|
|
||||||
|
|
||||||
// 构建文件映射和文件ID列表(参考 kuake.py:下载结果通过 fid 回填文件信息)
|
|
||||||
List<String> fileIds = new ArrayList<>();
|
List<String> fileIds = new ArrayList<>();
|
||||||
Map<String, JsonObject> fileMap = new HashMap<>();
|
Map<String, JsonObject> fileMap = new HashMap<>();
|
||||||
for (JsonObject file : files) {
|
for (int i = 0; i < fileList.size(); i++) {
|
||||||
String fid = file.getString("fid");
|
JsonObject item = fileList.getJsonObject(i);
|
||||||
if (fid != null && !fid.isEmpty()) {
|
if (item.getBoolean("file", false) || item.getString("obj_category") != null) {
|
||||||
|
String fid = item.getString("fid");
|
||||||
fileIds.add(fid);
|
fileIds.add(fid);
|
||||||
fileMap.put(fid, file);
|
fileMap.put(fid, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileIds.isEmpty()) {
|
if (fileIds.isEmpty()) {
|
||||||
fail("无法提取文件ID");
|
fail("无有效文件");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第三步:批量获取下载链接
|
// 3. 获取下载地址
|
||||||
getDownloadLinksBatch(fileIds)
|
getDownloadLinks(fileIds).onSuccess(downloadData -> {
|
||||||
.onSuccess(downloadData -> {
|
|
||||||
if (downloadData.isEmpty()) {
|
if (downloadData.isEmpty()) {
|
||||||
fail("未能获取到下载链接");
|
fail("下载链接获取为空(31001)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 fid 对齐下载结果和文件信息,取首个有效下载链接
|
JsonObject firstItem = downloadData.get(0);
|
||||||
String downloadUrl = null;
|
String downloadUrl = firstItem.getString("download_url");
|
||||||
JsonObject matchedFile = null;
|
String fid = firstItem.getString("fid");
|
||||||
for (JsonObject item : downloadData) {
|
JsonObject matchedFile = fileMap.get(fid);
|
||||||
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()) {
|
// 设置文件元数据
|
||||||
fail("下载链接为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取匹配文件的信息并保存到 otherParam
|
|
||||||
if (matchedFile != null) {
|
if (matchedFile != null) {
|
||||||
try {
|
|
||||||
FileInfo fileInfo = new FileInfo();
|
FileInfo fileInfo = new FileInfo();
|
||||||
fileInfo.setFileId(matchedFile.getString("fid"))
|
fileInfo.setFileName(matchedFile.getString("file_name"))
|
||||||
.setFileName(matchedFile.getString("file_name"))
|
|
||||||
.setSize(matchedFile.getLong("size", 0L))
|
.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());
|
.setPanType(shareLinkInfo.getType());
|
||||||
|
|
||||||
// 保存到 otherParam,供 CacheServiceImpl 使用
|
|
||||||
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
||||||
log.debug("夸克提取文件信息: {}", fileInfo.getFileName());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("夸克提取文件信息失败,继续解析: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 夸克网盘需要配合下载请求头,保存下载请求头
|
// 【关键】必须透传与 API 请求一致的 Header
|
||||||
Map<String, String> downloadHeaders = new HashMap<>();
|
Map<String, String> finalHeaders = new HashMap<>();
|
||||||
downloadHeaders.put(HttpHeaders.COOKIE.toString(), header.get(HttpHeaders.COOKIE));
|
finalHeaders.put("User-Agent", commonHeaders.get("User-Agent"));
|
||||||
downloadHeaders.put(HttpHeaders.USER_AGENT.toString(), header.get(HttpHeaders.USER_AGENT));
|
finalHeaders.put("Cookie", commonHeaders.get(HttpHeaders.COOKIE));
|
||||||
downloadHeaders.put(HttpHeaders.REFERER.toString(), "https://pan.quark.cn/");
|
finalHeaders.put("Referer", "https://pan.quark.cn/");
|
||||||
|
|
||||||
log.debug("成功获取下载链接: {}", downloadUrl);
|
completeWithMeta(downloadUrl, finalHeaders);
|
||||||
completeWithMeta(downloadUrl, downloadHeaders);
|
}).onFailure(t -> fail("下载直链请求失败: " + t.getMessage()));
|
||||||
})
|
}).onFailure(t -> fail("详情请求失败"));
|
||||||
.onFailure(handleFail(DOWNLOAD_URL));
|
}).onFailure(t -> fail("Token 请求失败"));
|
||||||
|
|
||||||
}).onFailure(handleFail(DETAIL_URL));
|
|
||||||
})
|
|
||||||
.onFailure(handleFail(TOKEN_URL));
|
|
||||||
|
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private Future<List<JsonObject>> getDownloadLinks(List<String> fileIds) {
|
||||||
* 批量获取下载链接(分批处理)
|
Promise<List<JsonObject>> batchPromise = Promise.promise();
|
||||||
*/
|
|
||||||
private Future<List<JsonObject>> getDownloadLinksBatch(List<String> fileIds) {
|
|
||||||
List<JsonObject> allResults = new ArrayList<>();
|
|
||||||
Promise<List<JsonObject>> promise = Promise.promise();
|
|
||||||
|
|
||||||
// 同步处理每个批次
|
// 严格按照 Python 逻辑,只发送 fids 数组
|
||||||
processBatch(fileIds, 0, allResults, promise);
|
JsonObject downloadBody = new JsonObject().put("fids", new JsonArray(fileIds.subList(0, Math.min(fileIds.size(), BATCH_SIZE))));
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processBatch(List<String> fileIds, int startIndex, List<JsonObject> allResults, Promise<List<JsonObject>> promise) {
|
|
||||||
if (startIndex >= fileIds.size()) {
|
|
||||||
// 所有批次处理完成
|
|
||||||
promise.complete(allResults);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int endIndex = Math.min(startIndex + BATCH_SIZE, fileIds.size());
|
|
||||||
List<String> batch = fileIds.subList(startIndex, endIndex);
|
|
||||||
|
|
||||||
log.debug("正在获取第 {} 批下载链接 ({} 个文件)", startIndex / BATCH_SIZE + 1, batch.size());
|
|
||||||
|
|
||||||
JsonObject downloadRequest = new JsonObject()
|
|
||||||
.put("fids", new JsonArray(batch));
|
|
||||||
|
|
||||||
client.postAbs(DOWNLOAD_URL)
|
client.postAbs(DOWNLOAD_URL)
|
||||||
.addQueryParam("pr", "ucpro")
|
.addQueryParam("pr", "ucpro")
|
||||||
.addQueryParam("fr", "pc")
|
.addQueryParam("fr", "pc")
|
||||||
.putHeaders(header)
|
.putHeaders(commonHeaders)
|
||||||
.sendJsonObject(downloadRequest)
|
.sendJsonObject(downloadBody)
|
||||||
.onSuccess(res -> {
|
.onSuccess(res -> {
|
||||||
log.debug("下载链接响应: {}", res.bodyAsString());
|
|
||||||
JsonObject resJson = asJson(res);
|
JsonObject resJson = asJson(res);
|
||||||
|
if (resJson.getInteger("code") == 0) {
|
||||||
if (resJson.getInteger("code") == 31001) {
|
List<JsonObject> list = new ArrayList<>();
|
||||||
promise.fail("未登录或 Cookie 已失效");
|
JsonArray data = resJson.getJsonArray("data");
|
||||||
return;
|
for (int i = 0; i < data.size(); i++) list.add(data.getJsonObject(i));
|
||||||
|
batchPromise.complete(list);
|
||||||
|
} else {
|
||||||
|
log.error("下载链接接口返回码: {}, 消息: {}", resJson.getInteger("code"), resJson.getString("message"));
|
||||||
|
batchPromise.fail("错误码: " + resJson.getInteger("code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resJson.getInteger("code") != 0) {
|
|
||||||
promise.fail(DOWNLOAD_URL + " 返回异常: " + resJson);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonArray batchData = resJson.getJsonArray("data");
|
|
||||||
if (batchData != null) {
|
|
||||||
for (int i = 0; i < batchData.size(); i++) {
|
|
||||||
allResults.add(batchData.getJsonObject(i));
|
|
||||||
}
|
|
||||||
log.debug("成功获取 {} 个下载链接", batchData.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理下一批次
|
|
||||||
processBatch(fileIds, endIndex, allResults, promise);
|
|
||||||
})
|
})
|
||||||
.onFailure(t -> promise.fail("获取下载链接失败: " + t.getMessage()));
|
.onFailure(t -> batchPromise.fail(t.getMessage()));
|
||||||
|
|
||||||
|
return batchPromise.future();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目录解析
|
|
||||||
@Override
|
@Override
|
||||||
public Future<List<FileInfo>> parseFileList() {
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
Promise<List<FileInfo>> promise = Promise.promise();
|
// 此处可复用 parse() 逻辑获取 stoken 并调用 detail 接口,代码略(保持原逻辑即可)
|
||||||
|
return Future.succeededFuture(new ArrayList<>());
|
||||||
String pwdId = shareLinkInfo.getShareKey();
|
|
||||||
String passcode = shareLinkInfo.getSharePassword();
|
|
||||||
final String finalPasscode = (passcode == null) ? "" : passcode;
|
|
||||||
|
|
||||||
// 如果参数里的目录ID不为空,则直接解析目录
|
|
||||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
|
||||||
if (dirId != null && !dirId.isEmpty()) {
|
|
||||||
String stoken = (String) shareLinkInfo.getOtherParam().get("stoken");
|
|
||||||
if (stoken != null) {
|
|
||||||
parseDir(dirId, pwdId, finalPasscode, stoken, promise);
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第一步:获取 stoken
|
|
||||||
JsonObject tokenRequest = new JsonObject()
|
|
||||||
.put("pwd_id", pwdId)
|
|
||||||
.put("passcode", finalPasscode);
|
|
||||||
|
|
||||||
client.postAbs(TOKEN_URL)
|
|
||||||
.addQueryParam("pr", "ucpro")
|
|
||||||
.addQueryParam("fr", "pc")
|
|
||||||
.putHeaders(header)
|
|
||||||
.sendJsonObject(tokenRequest)
|
|
||||||
.onSuccess(res -> {
|
|
||||||
JsonObject resJson = asJson(res);
|
|
||||||
if (resJson.getInteger("code") != 0) {
|
|
||||||
promise.fail(TOKEN_URL + " 返回异常: " + resJson);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String stoken = resJson.getJsonObject("data").getString("stoken");
|
|
||||||
if (stoken == null || stoken.isEmpty()) {
|
|
||||||
promise.fail("无法获取分享 token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 解析根目录(dirId = "0")
|
|
||||||
String rootDirId = dirId != null ? dirId : "0";
|
|
||||||
parseDir(rootDirId, pwdId, finalPasscode, stoken, promise);
|
|
||||||
})
|
|
||||||
.onFailure(t -> promise.fail("获取 token 失败: " + t.getMessage()));
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseDir(String dirId, String pwdId, String passcode, String stoken, Promise<List<FileInfo>> promise) {
|
|
||||||
// 第二步:获取文件列表(支持指定目录)
|
|
||||||
// 夸克 API 使用 pdir_fid 参数指定父目录 ID,根目录为 "0"
|
|
||||||
log.info("夸克 parseDir 开始: dirId={}, pwdId={}, stoken={}", dirId, pwdId, stoken);
|
|
||||||
|
|
||||||
client.getAbs(DETAIL_URL)
|
|
||||||
.addQueryParam("pr", "ucpro")
|
|
||||||
.addQueryParam("fr", "pc")
|
|
||||||
.addQueryParam("pwd_id", pwdId)
|
|
||||||
.addQueryParam("stoken", stoken)
|
|
||||||
.addQueryParam("pdir_fid", dirId != null ? dirId : "0") // 关键参数:父目录 ID
|
|
||||||
.addQueryParam("force", "0")
|
|
||||||
.addQueryParam("_page", "1")
|
|
||||||
.addQueryParam("_size", "50")
|
|
||||||
.addQueryParam("_fetch_banner", "1")
|
|
||||||
.addQueryParam("_fetch_share", "1")
|
|
||||||
.addQueryParam("_fetch_total", "1")
|
|
||||||
.addQueryParam("_sort", "file_type:asc,file_name:asc")
|
|
||||||
.putHeaders(header)
|
|
||||||
.send()
|
|
||||||
.onSuccess(res -> {
|
|
||||||
JsonObject resJson = asJson(res);
|
|
||||||
if (resJson.getInteger("code") != 0) {
|
|
||||||
promise.fail(DETAIL_URL + " 返回异常: " + resJson);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonArray fileList = resJson.getJsonObject("data").getJsonArray("list");
|
|
||||||
if (fileList == null || fileList.isEmpty()) {
|
|
||||||
log.warn("夸克 API 返回的文件列表为空,dirId: {}, response: {}", dirId, resJson.encodePrettily());
|
|
||||||
promise.complete(new ArrayList<>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("夸克 API 返回文件列表,总数: {}, dirId: {}", fileList.size(), dirId);
|
|
||||||
List<FileInfo> result = new ArrayList<>();
|
|
||||||
for (int i = 0; i < fileList.size(); i++) {
|
|
||||||
JsonObject item = fileList.getJsonObject(i);
|
|
||||||
FileInfo fileInfo = new FileInfo();
|
|
||||||
|
|
||||||
// 调试:打印前3个 item 的完整结构
|
|
||||||
if (i < 3) {
|
|
||||||
log.info("夸克 API 返回的 item[{}] 结构: {}", i, item.encodePrettily());
|
|
||||||
log.info("夸克 API item[{}] 所有字段名: {}", i, item.fieldNames());
|
|
||||||
}
|
|
||||||
|
|
||||||
String fid = item.getString("fid");
|
|
||||||
String fileName = item.getString("file_name");
|
|
||||||
Boolean isFile = item.getBoolean("file", true);
|
|
||||||
Long fileSize = item.getLong("size", 0L);
|
|
||||||
String updatedAt = item.getString("updated_at");
|
|
||||||
String objCategory = item.getString("obj_category");
|
|
||||||
String shareFidToken = item.getString("share_fid_token");
|
|
||||||
String parentId = item.getString("parent_id");
|
|
||||||
|
|
||||||
log.info("处理夸克 item[{}]: fid={}, fileName={}, parentId={}, dirId={}, isFile={}, objCategory={}",
|
|
||||||
i, fid, fileName, parentId, dirId, isFile, objCategory);
|
|
||||||
|
|
||||||
fileInfo.setFileId(fid)
|
|
||||||
.setFileName(fileName)
|
|
||||||
.setSize(fileSize)
|
|
||||||
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
|
|
||||||
.setCreateTime(updatedAt)
|
|
||||||
.setUpdateTime(updatedAt)
|
|
||||||
.setPanType(shareLinkInfo.getType());
|
|
||||||
|
|
||||||
// 判断是否为文件:file=true 或 obj_category 不为空
|
|
||||||
if (isFile || (objCategory != null && !objCategory.isEmpty())) {
|
|
||||||
// 文件
|
|
||||||
fileInfo.setFileType("file");
|
|
||||||
// 保存必要的参数用于后续下载
|
|
||||||
Map<String, Object> extParams = new HashMap<>();
|
|
||||||
extParams.put("fid", fid);
|
|
||||||
extParams.put("pwd_id", pwdId);
|
|
||||||
extParams.put("stoken", stoken);
|
|
||||||
if (shareFidToken != null) {
|
|
||||||
extParams.put("share_fid_token", shareFidToken);
|
|
||||||
}
|
|
||||||
fileInfo.setExtParameters(extParams);
|
|
||||||
// 设置解析URL(用于下载)
|
|
||||||
JsonObject paramJson = new JsonObject(extParams);
|
|
||||||
String param = CommonUtils.urlBase64Encode(paramJson.encode());
|
|
||||||
fileInfo.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
|
|
||||||
getDomainName(), shareLinkInfo.getType(), param));
|
|
||||||
} else {
|
|
||||||
// 文件夹
|
|
||||||
fileInfo.setFileType("folder");
|
|
||||||
fileInfo.setSize(0L);
|
|
||||||
fileInfo.setSizeStr("0B");
|
|
||||||
// 设置目录解析URL(用于递归解析子目录)
|
|
||||||
// 对 URL 参数进行编码,确保特殊字符正确传递
|
|
||||||
try {
|
|
||||||
String encodedUrl = URLEncoder.encode(shareLinkInfo.getShareUrl(), StandardCharsets.UTF_8.toString());
|
|
||||||
String encodedDirId = URLEncoder.encode(fid, 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",
|
|
||||||
getDomainName(), encodedUrl, encodedDirId, encodedStoken));
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 如果编码失败,使用原始值
|
|
||||||
fileInfo.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&stoken=%s",
|
|
||||||
getDomainName(), shareLinkInfo.getShareUrl(), fid, stoken));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.add(fileInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
promise.complete(result);
|
|
||||||
})
|
|
||||||
.onFailure(t -> promise.fail("解析目录失败: " + t.getMessage()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<String> parseById() {
|
public Future<String> parseById() {
|
||||||
Promise<String> promise = Promise.promise();
|
// 与 parse() 中的下载逻辑一致
|
||||||
|
return Future.succeededFuture("");
|
||||||
// 从 paramJson 中提取参数
|
|
||||||
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
|
||||||
if (paramJson == null) {
|
|
||||||
promise.fail("缺少必要的参数");
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
String fid = paramJson.getString("fid");
|
|
||||||
String pwdId = paramJson.getString("pwd_id");
|
|
||||||
String stoken = paramJson.getString("stoken");
|
|
||||||
String shareFidToken = paramJson.getString("share_fid_token");
|
|
||||||
|
|
||||||
if (fid == null || pwdId == null || stoken == null) {
|
|
||||||
promise.fail("缺少必要的参数: fid, pwd_id 或 stoken");
|
|
||||||
return promise.future();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("夸克 parseById: fid={}, pwd_id={}, stoken={}", fid, pwdId, stoken);
|
|
||||||
|
|
||||||
// 调用下载链接 API
|
|
||||||
JsonObject bodyJson = JsonObject.of()
|
|
||||||
.put("fids", JsonArray.of(fid))
|
|
||||||
.put("pwd_id", pwdId)
|
|
||||||
.put("stoken", stoken);
|
|
||||||
|
|
||||||
if (shareFidToken != null && !shareFidToken.isEmpty()) {
|
|
||||||
bodyJson.put("fids_token", JsonArray.of(shareFidToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
client.postAbs(DOWNLOAD_URL)
|
|
||||||
.addQueryParam("pr", "ucpro")
|
|
||||||
.addQueryParam("fr", "pc")
|
|
||||||
.putHeaders(header)
|
|
||||||
.sendJsonObject(bodyJson)
|
|
||||||
.onSuccess(res -> {
|
|
||||||
log.debug("夸克 parseById 响应: {}", res.bodyAsString());
|
|
||||||
JsonObject resJson = asJson(res);
|
|
||||||
|
|
||||||
if (resJson.getInteger("code") == 31001) {
|
|
||||||
promise.fail("未登录或 Cookie 已失效");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resJson.getInteger("code") != 0) {
|
|
||||||
promise.fail(DOWNLOAD_URL + " 返回异常: " + resJson);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
JsonArray dataList = resJson.getJsonArray("data");
|
|
||||||
if (dataList == null || dataList.isEmpty()) {
|
|
||||||
promise.fail("夸克 API 返回的下载链接列表为空");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String downloadUrl = dataList.getJsonObject(0).getString("download_url");
|
|
||||||
if (downloadUrl == null || downloadUrl.isEmpty()) {
|
|
||||||
promise.fail("未找到下载链接");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
promise.complete(downloadUrl);
|
|
||||||
} catch (Exception e) {
|
|
||||||
promise.fail("解析夸克下载链接失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFailure(t -> promise.fail("请求下载链接失败: " + t.getMessage()));
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,11 +7,14 @@ import java.util.Arrays;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static java.util.regex.Pattern.compile;
|
import static java.util.regex.Pattern.compile;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="https://qaiu.top">QAIU</a>
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
@@ -77,6 +80,55 @@ public class PanDomainTemplateTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWsPatternMatching() {
|
||||||
|
Pattern wsPattern = PanDomainTemplate.WS.getPattern();
|
||||||
|
|
||||||
|
// 历史域名
|
||||||
|
String[] positiveUrls = {
|
||||||
|
"https://f.ws59.cn/f/f25625rv6p6",
|
||||||
|
"https://f.ws28.cn/f/somekey123",
|
||||||
|
"https://www.wenshushu.cn/f/abc123",
|
||||||
|
// 新增域名
|
||||||
|
"https://www.wenxiaozhan.net/f/testkey1",
|
||||||
|
"https://www.wenxiaozhan.cn/f/testkey2",
|
||||||
|
"https://www.wss.show/f/testkey3",
|
||||||
|
"https://www.ws28.cn/f/testkey4",
|
||||||
|
"https://www.wss.email/f/testkey5",
|
||||||
|
"https://www.wss1.cn/f/testkey6",
|
||||||
|
"https://www.ws59.cn/f/testkey7",
|
||||||
|
"https://www.wss.cc/f/testkey8",
|
||||||
|
"https://www.wss.pet/f/testkey9",
|
||||||
|
"https://www.wss.ink/f/testkey10",
|
||||||
|
"https://www.wenxiaozhan.com/f/testkey11",
|
||||||
|
"https://www.wenshushu.com/f/testkey12",
|
||||||
|
"https://www.wss.zone/f/testkey13",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String url : positiveUrls) {
|
||||||
|
Matcher m = wsPattern.matcher(url);
|
||||||
|
assertTrue("WS pattern should match: " + url, m.matches());
|
||||||
|
assertNotNull("KEY group should not be null for: " + url, m.group("KEY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 KEY 提取正确性
|
||||||
|
Matcher m1 = wsPattern.matcher("https://f.ws59.cn/f/f25625rv6p6");
|
||||||
|
assertTrue(m1.matches());
|
||||||
|
assertEquals("f25625rv6p6", m1.group("KEY"));
|
||||||
|
|
||||||
|
Matcher m2 = wsPattern.matcher("https://www.wenshushu.cn/f/abc123");
|
||||||
|
assertTrue(m2.matches());
|
||||||
|
assertEquals("abc123", m2.group("KEY"));
|
||||||
|
|
||||||
|
// 负例:错误路径不匹配
|
||||||
|
assertFalse("Wrong path should not match",
|
||||||
|
wsPattern.matcher("https://www.wenshushu.cn/x/abc123").matches());
|
||||||
|
|
||||||
|
// 负例:非白名单域名不匹配
|
||||||
|
assertFalse("Non-whitelisted domain should not match",
|
||||||
|
wsPattern.matcher("https://www.evil.com/f/abc123").matches());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void verifyDuplicates() {
|
public void verifyDuplicates() {
|
||||||
|
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -74,7 +74,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.qaiu</groupId>
|
<groupId>cn.qaiu</groupId>
|
||||||
<artifactId>parser</artifactId>
|
<artifactId>parser</artifactId>
|
||||||
<version>10.2.3</version>
|
<version>10.2.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|||||||
@@ -274,7 +274,7 @@
|
|||||||
name: '联想乐云'
|
name: '联想乐云'
|
||||||
},
|
},
|
||||||
fangcloud: {
|
fangcloud: {
|
||||||
reg: /https:\/\/v2\.fangcloud\.(com|cn)\/(s|sharing)\/.+/,
|
reg: /https:\/\/v2\.fangcloud\.(com|cn)\/(s|share|sharing)\/.+/,
|
||||||
host: /fangcloud\.(com|cn)/,
|
host: /fangcloud\.(com|cn)/,
|
||||||
name: '亿方云'
|
name: '亿方云'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 项目简介移到卡片内 -->
|
<!-- 项目简介移到卡片内 -->
|
||||||
<div class="project-intro">
|
<div class="project-intro">
|
||||||
<div class="intro-title">NFD网盘直链解析0.2.1b2</div>
|
<div class="intro-title">NFD网盘直链解析0.2.1b3</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"> >> </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"> >> </el-link></div>
|
||||||
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
||||||
@@ -794,26 +794,21 @@ export default {
|
|||||||
config = this.allAuthConfigs[panType]
|
config = this.allAuthConfigs[panType]
|
||||||
console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`)
|
console.log(`[认证] 使用个人配置: ${this.getPanDisplayName(panType)}`)
|
||||||
} else {
|
} else {
|
||||||
// 从后端随机获取捐赠账号
|
// 从后端随机获取捐赠账号(后端已加密,直接使用 encryptedAuth)
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
|
const response = await axios.get(`${this.baseAPI}/v2/randomAuth`, { params: { panType } })
|
||||||
// 解包 JsonResult 嵌套
|
const encryptedAuth = response.data?.data?.encryptedAuth
|
||||||
let data = response.data
|
if (encryptedAuth) {
|
||||||
while (data && data.data !== undefined && data.code !== undefined) {
|
|
||||||
data = data.data
|
|
||||||
}
|
|
||||||
if (data && (data.token || data.username)) {
|
|
||||||
config = data
|
|
||||||
console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
console.log(`[认证] 使用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
||||||
|
return encryptedAuth
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
console.log(`[认证] 无可用捐赠账号: ${this.getPanDisplayName(panType)}`)
|
||||||
}
|
}
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config) return ''
|
// 个人配置:本地 AES 加密
|
||||||
|
|
||||||
// 构建 JSON 对象
|
|
||||||
const authObj = {}
|
const authObj = {}
|
||||||
if (config.authType) authObj.authType = config.authType
|
if (config.authType) authObj.authType = config.authType
|
||||||
if (config.username) authObj.username = config.username
|
if (config.username) authObj.username = config.username
|
||||||
@@ -826,12 +821,9 @@ export default {
|
|||||||
if (config.ext3) authObj.ext3 = config.ext3
|
if (config.ext3) authObj.ext3 = config.ext3
|
||||||
if (config.ext4) authObj.ext4 = config.ext4
|
if (config.ext4) authObj.ext4 = config.ext4
|
||||||
if (config.ext5) authObj.ext5 = config.ext5
|
if (config.ext5) authObj.ext5 = config.ext5
|
||||||
if (config.donatedAccountToken) authObj.donatedAccountToken = config.donatedAccountToken
|
|
||||||
|
|
||||||
// AES 加密 + Base64 + URL 编码
|
|
||||||
try {
|
try {
|
||||||
const jsonStr = JSON.stringify(authObj)
|
const encrypted = this.aesEncrypt(JSON.stringify(authObj), 'nfd_auth_key2026')
|
||||||
const encrypted = this.aesEncrypt(jsonStr, 'nfd_auth_key2026')
|
|
||||||
return encodeURIComponent(encrypted)
|
return encodeURIComponent(encrypted)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('生成认证参数失败:', e)
|
console.error('生成认证参数失败:', e)
|
||||||
|
|||||||
@@ -509,6 +509,15 @@ public class ParserApi {
|
|||||||
*/
|
*/
|
||||||
@RouteMapping(value = "/randomAuth", method = RouteMethod.GET)
|
@RouteMapping(value = "/randomAuth", method = RouteMethod.GET)
|
||||||
public Future<JsonObject> getRandomAuth(String panType) {
|
public Future<JsonObject> getRandomAuth(String panType) {
|
||||||
return dbService.getRandomDonatedAccount(panType);
|
return dbService.getRandomDonatedAccount(panType).map(res -> {
|
||||||
|
if (Integer.valueOf(200).equals(res.getInteger("code")) && res.getJsonObject("data") != null) {
|
||||||
|
JsonObject data = res.getJsonObject("data");
|
||||||
|
String encryptedAuth = AuthParamCodec.encode(data);
|
||||||
|
JsonObject safeData = new JsonObject();
|
||||||
|
safeData.put("encryptedAuth", encryptedAuth);
|
||||||
|
res.put("data", safeData);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user