Compare commits

...

34 Commits

Author SHA1 Message Date
q
3bbe16a07d 面向云代理会话的 VS Code 中的检查点 2026-04-18 16:40:21 +08:00
qaiu
cc9d0a4b30 更新 README.md 2026-04-15 05:16:47 +08:00
qaiu
696ef832f8 更新 README.md 2026-04-13 08:03:04 +08:00
qaiu
442f9d1d2e Merge pull request #176 from qaiu/copilot/optimize-pandomain-template
fix(PanDomainTemplate): 修复现有网盘域名模板正则表达式中的多处缺陷
2026-04-12 19:45:06 +08:00
qaiu
1a949725f3 更新 README.md 2026-04-12 19:33:44 +08:00
qaiu
7c14f3437b 更新 README.md 2026-04-12 19:32:20 +08:00
qaiu
bc402da365 更新 README.md 2026-04-12 19:30:47 +08:00
qaiu
b95b474660 更新 README.md 2026-04-12 19:29:10 +08:00
qaiu
691a3770d9 更新 README.md 2026-04-12 19:28:06 +08:00
copilot-swe-agent[bot]
49ec54a3b5 refactor(tests): 改善测试注释说明,增强可读性
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/5523822b-ffe2-4e95-ac13-fd3f0dc41970

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-12 11:19:51 +00:00
qaiu
2fc15f437e 更新 README.md 2026-04-12 19:18:15 +08:00
qaiu
190f6ca7ab 更新 README.md 2026-04-12 19:17:44 +08:00
qaiu
c683fd27d4 更新 README.md 2026-04-12 19:17:18 +08:00
copilot-swe-agent[bot]
d815cc1010 fix(PanDomainTemplate): 优化现有网盘域名模板正则表达式
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/5523822b-ffe2-4e95-ac13-fd3f0dc41970

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-12 11:17:06 +00:00
copilot-swe-agent[bot]
fd84ff1200 Initial plan 2026-04-12 11:05:17 +00:00
qaiu
a420bad305 Update README.md 2026-04-09 16:46:29 +08:00
qaiu
6ef6e47580 Update README.md 2026-04-07 08:39:41 +08:00
qaiu
94f83ec296 Fix duplicate Trendshift badge in README
Removed duplicate Trendshift badge from README.
2026-04-07 08:23:44 +08:00
qaiu
702569c701 Add Trendshift badge to README
Added Trendshift badge to README for repository tracking.
2026-04-07 08:22:40 +08:00
qaiu
d4940ca9ee fixed: 123-YePan: Fix regex pattern for share key extraction 2026-04-07 08:20:06 +08:00
qaiu
dbd1c138ca Merge pull request #173 from qaiu/copilot/identify-yifang-cloud-new-format
feat: recognize new Fangcloud /share/ URL format
2026-04-05 17:59:54 +08:00
copilot-swe-agent[bot]
0b49c55cf3 feat: recognize new Fangcloud /share/ URL format in addition to /sharing/ and /s/
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/dc483348-3899-4448-80ce-c2352e6bc23e

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-05 08:20:46 +00:00
copilot-swe-agent[bot]
b1ec3b2eea Initial plan 2026-04-05 08:16:46 +00:00
qaiu
9ea89feee7 更新 README.md 2026-04-04 20:16:58 +08:00
qaiu
4a843194a3 Merge pull request #171 from qaiu/copilot/update-ws-domain-recognition
feat(WS): 扩展文叔叔域名匹配 + 补充单元测试
2026-03-18 12:21:31 +08:00
copilot-swe-agent[bot]
03503115fd feat: 文叔叔(WS)域名扩展 + 单元测试补充
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-03-18 02:18:53 +00:00
copilot-swe-agent[bot]
1870aef60e Initial plan 2026-03-18 02:11:33 +00:00
qaiu
ed8fd66d1e 更新 README.md 2026-03-16 20:15:36 +08:00
qaiu
c1c4c8cdc5 更新 README.md 2026-03-16 20:14:57 +08:00
q
256ec3b152 Fixed: Lz parser return filename error. 2026-03-07 13:45:26 +08:00
q
da490e5bbd Merge branch 'main' of github.com:qaiu/netdisk-fast-download 2026-03-06 10:39:49 +08:00
q
ba0ac86eea LzToooool 2026-03-06 10:38:11 +08:00
qaiu
b5544c4131 更新 README.md 2026-02-28 21:31:20 +08:00
qaiu
d94ea6aaf3 更新 README.md 2026-02-25 01:42:46 +08:00
8 changed files with 733 additions and 108 deletions

View File

@@ -1,38 +1,51 @@
<p align="center">
<img src="https://github.com/user-attachments/assets/87401aae-b0b6-4ffb-bbeb-44756404d26f" alt="项目预览图" />
</p>
# 一款网盘分享链接云解析快速下载服务
QQ交流群1017480890
<p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a>
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></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://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
<p align="center">
<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>
<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>
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链可广泛应用于各类下载站资源站个人博客图床APP下载更新视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
# netdisk-fast-download 网盘分享链接云解析服务
QQ交流群1017480890
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等支持加密分享以及部分网盘文件夹分享。
[官方文档](https://nfd-parser.github.io/)
[API接入](https://nfdparser.apifox.cn/)
[公益解析lz站](https://lz.qaiu.top)
[公益解析lz0站](https://lz0.qaiu.top)
[专业版189站注册体验](https://189.qaiu.top)
## 快速开始
命令行下载分享文件:
```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:
```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,15 +55,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)
## 预览地址
[预览地址1](https://lz.qaiu.top)
[预览地址2](https://lz0.qaiu.top)
[天翼云盘/移动云盘限时体验版](https://189.qaiu.top)
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
**小飞机解析有IP限制多数云服务商的大陆IP会被拦截可以自行配置代理和本程序无关**
**注意: 请不要过度依赖lz.qaiu.top预览地址服务建议本地搭建或者云服务器自行搭建。解析次数过多IP会被部分网盘厂商限制不推荐做公共解析。**
**注意⚠小飞机解析有IP限制多数云服务商的大陆IP会被拦截可以自行配置代理和本程序无关**
**注意⚠️收到很多用户反馈,小飞机近期封号频繁,请尽可能选择其他网盘分享**
**注意⚠️请不要过度依赖 lz.qaiu.top建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制遇到解析失败的分享链接不要着急提issues请先检查分享是否有效** [lz站](https://lz.qaiu.top) 和 [lz0](https://lz0.qaiu.top) 不支持大文件,请使用 [189站](https://189.qaiu.top) 注册体验。
## 网盘支持情况:
> 20230905 奶牛云直链做了防盗链需加入请求头Referer: https://cowtransfer.com/
@@ -61,7 +69,6 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [蓝奏云-lz](https://pc.woozooo.com/)
- [蓝奏云优享-iz](https://www.ilanzou.com/)
- [奶牛快传-cow](https://cowtransfer.com/)
- [移动云云空间-ec](https://www.ecpan.cn/web)
- [小飞机网盘-fj](https://www.feijipan.com/)
- [亿方云-fc](https://www.fangcloud.com/)

View File

@@ -12,7 +12,7 @@
<groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId>
<version>10.2.3</version>
<version>10.2.5</version>
<packaging>jar</packaging>
<name>cn.qaiu:parser</name>

View File

@@ -68,42 +68,43 @@ public enum PanDomainTemplate {
t-is.cn
*/
LZ("蓝奏云",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(" +
"lanzoul|" +
"lanzouh|" +
"lanosso|" +
"lanpv|" +
"bakstotre|" +
"lanzouo|" +
"lanzov|" +
"lanpw|" +
"ulanzou|" +
"lanzouf|" +
"lanzn|" +
"lanzouj|" +
"lanzouk|" +
"lanzouq|" +
"lanzouv|" +
"lanzoue|" +
"lanzouw|" +
"lanzoub|" +
"lanzouu|" +
"lanwp|" +
"lanzouy|" +
"lanzoup|" +
"woozooo|" +
"lanzv|" +
"dmpdmp|" +
"lanrar|" +
"webgetstore|" +
"lanzb|" +
"lanzoux|" +
"lanzout|" +
"lanzouc|" +
"lanzoui|" +
"lanzoug|" +
"lanzoum" +
")\\.com/(.+/)?(?<KEY>.+)"),
compile("https://(?:[a-zA-Z\\d-]+\\.)?(?:" +
"(?:lanzoul|" +
"lanzouh|" +
"lanosso|" +
"lanpv|" +
"bakstotre|" +
"lanzouo|" +
"lanzov|" +
"lanpw|" +
"ulanzou|" +
"lanzouf|" +
"lanzn|" +
"lanzouj|" +
"lanzouk|" +
"lanzouq|" +
"lanzouv|" +
"lanzoue|" +
"lanzouw|" +
"lanzoub|" +
"lanzouu|" +
"lanwp|" +
"lanzouy|" +
"lanzoup|" +
"woozooo|" +
"lanzv|" +
"dmpdmp|" +
"lanrar|" +
"webgetstore|" +
"lanzb|" +
"lanzoux|" +
"lanzout|" +
"lanzouc|" +
"lanzoui|" +
"lanzoug|" +
"lanzoum)\\.com" +
"|t-is\\.cn" +
")/(?<KEY>.+)"),
"https://w1.lanzn.com/{shareKey}",
LzTool.class),
@@ -116,13 +117,13 @@ public enum PanDomainTemplate {
// https://lecloud.lenovo.com/share/
LE("联想乐云",
compile("https://lecloud?\\.lenovo\\.com/share/(?<KEY>.+)"),
compile("https://lecloud\\.lenovo\\.com/share/(?<KEY>.+)"),
"https://lecloud.lenovo.com/share/{shareKey}",
LeTool.class),
// https://v2.fangcloud.com/s/
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://www.fangcloud.com/",
FcTool.class),
@@ -143,9 +144,41 @@ public enum PanDomainTemplate {
compile("https://qfile\\.qq\\.com/q/(?<KEY>.+)"),
"https://qfile.qq.com/q/{shareKey}",
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("文叔叔",
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}",
WsTool.class),
// https://www.123pan.com/s/
@@ -199,7 +232,7 @@ public enum PanDomainTemplate {
"123635\\.com|" +
"123242\\.com|" +
"123795\\.com" +
")/s/(?<KEY>.+)(.html)?"),
")/s/(?<KEY>[a-zA-Z0-9_-]+)(?:\\.html)?"),
"https://www.123pan.com/s/{shareKey}",
Ye2Tool.class),
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
@@ -210,7 +243,7 @@ public enum PanDomainTemplate {
EcTool.class),
// https://cowtransfer.com/s/
COW("奶牛快传",
compile("https://(.*)cowtransfer\\.com/s/(?<KEY>.+)"),
compile("https://(?:[a-zA-Z\\d-]+\\.)?cowtransfer\\.com/s/(?<KEY>.+)"),
"https://cowtransfer.com/s/{shareKey}",
CowTool.class),
CT("城通网盘",
@@ -233,7 +266,7 @@ public enum PanDomainTemplate {
PodTool.class),
// 404网盘 https://drive.google.com/file/d/xxx/view?usp=sharing
PGD("GoogleDrive",
compile("https://drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?"),
compile("https://(?:[a-zA-Z\\d-]+\\.)?drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?"),
"https://drive.google.com/file/d/{shareKey}/view?usp=sharing",
PgdTool.class),
// iCloud https://www.icloud.com.cn/iclouddrive/xxx#fonts
@@ -243,11 +276,11 @@ public enum PanDomainTemplate {
PicTool.class),
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/emqx-5.0.26-el7-amd64.tar.gz?rlkey=3uoi4bxz5mv93jmlaws0nlol1&e=8&st=fe0lclc2&dl=0
PDB("dropbox",
compile("https://www.dropbox.com/scl/fi/(?<KEY>\\w+)/.+?rlkey=(?<PWD>\\w+).*"),
compile("https://www\\.dropbox\\.com/scl/fi/(?<KEY>\\w+)/.+?rlkey=(?<PWD>\\w+).*"),
"https://www.dropbox.com/scl/fi/{shareKey}/?rlkey={pwd}&dl=0",
PdbTool.class),
P115("115网盘",
compile("https://(115|anxia).com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?([&#].*)?"),
compile("https://(115|anxia)\\.com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?([&#].*)?"),
"https://115.com/s/{shareKey}?password={pwd}",
P115Tool.class),
// 链接https://www.yunpan.com/surl_yD7wz4VgU9v提取码fc70
@@ -288,7 +321,7 @@ public enum PanDomainTemplate {
MnesTool.class),
// https://music.163.com/#/song?id=xxx
MNE("网易云音乐歌曲详情",
compile("https://(y.)?music\\.163\\.com/(#|m/)?song\\?id=(?<KEY>.+)(&.*)?"),
compile("https://(y\\.)?music\\.163\\.com/(?:#/|m/)?song\\?id=(?<KEY>.+)(&.*)?"),
"https://music.163.com/#/song?id={shareKey}",
MnesTool.MneTool.class),
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
@@ -309,7 +342,7 @@ public enum PanDomainTemplate {
MkgsTool.class),
// https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4"
MKGS2("酷狗音乐分享2",
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/share/(?<KEY>.+).html.*"),
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/share/(?<KEY>.+)\\.html.*"),
"https://www.kugou.com/share/{shareKey}.html",
MkgsTool.Mkgs2Tool.class),
// https://www.kugou.com/mixsong/2bi8Fe9CSV3

View File

@@ -64,7 +64,7 @@ public class LzTool extends PanBase {
String html = asText(res);
if (html.contains("var arg1='")) {
webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(html);
setCookie(html, sUrl);
webClientSession.getAbs(sUrl)
.putHeaders(headers0)
.send().onSuccess(res2 -> {
@@ -81,6 +81,29 @@ public class LzTool extends PanBase {
}
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 {
setFileInfo(html, shareLinkInfo);
} catch (Exception e) {
@@ -98,20 +121,18 @@ public class LzTool extends PanBase {
} catch (Exception e) {
fail(e, "js引擎执行失败");
}
}
else {
} else {
// 没有密码
String iframePath = matcher.group(1);
String absoluteURI = SHARE_URL_PREFIX + iframePath;
webClientSession.getAbs(absoluteURI).putHeaders(headers0).send().onSuccess(res2 -> {
String html2= asText(res2);
// Matcher matcher2 = Pattern.compile("'sign'\s*:\s*'(\\w+)'").matcher(html2);
String html2 = asText(res2);
String jsText = getJsText(html2);
if (jsText == null) {
headers0.add("Referer", absoluteURI);
setCookie(html2);
setCookie(html2, absoluteURI);
webClientSession.getAbs(absoluteURI).send().onSuccess(res3 -> {
String html3= asText(res3);
String html3 = asText(res3);
String jsText3 = getJsText(html3);
if (jsText3 != null) {
try {
@@ -120,10 +141,8 @@ public class LzTool extends PanBase {
} catch (ScriptException | NoSuchMethodException e) {
fail(e, "引擎执行失败");
}
} else {
} else {
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": 获取失败0, 可能分享已失效");
return;
}
});
} else {
@@ -138,14 +157,29 @@ public class LzTool extends PanBase {
}
}
private void setCookie(String html2) {
int beginIndex = html2.indexOf("arg1='") + 6;
String arg1 = html2.substring(beginIndex, html2.indexOf("';", beginIndex));
private void setCookie(String html, String url) {
int beginIndex = html.indexOf("arg1='") + 6;
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);
// 从 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
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(".lanzn.com"); // 设置域名
nettyCookie.setPath("/"); // 设置路径
nettyCookie.setDomain(domain);
nettyCookie.setPath("/");
nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
webClientSession.cookieStore().put(nettyCookie);
@@ -218,7 +252,7 @@ public class LzTool extends PanBase {
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);
}
@@ -234,10 +268,18 @@ public class LzTool extends PanBase {
int beginIndex = text.indexOf("arg1='") + 6;
String arg1 = text.substring(beginIndex, text.indexOf("';", beginIndex));
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
DefaultCookie nettyCookie = new DefaultCookie("acw_sc__v2", acw_sc__v2);
nettyCookie.setDomain(".lanrar.com"); // 设置域名
nettyCookie.setPath("/"); // 设置路径
nettyCookie.setDomain(downDomain);
nettyCookie.setPath("/");
nettyCookie.setSecure(false);
nettyCookie.setHttpOnly(false);
WebClientSession webClientSession2 = WebClientSession.create(clientNoRedirects);
@@ -295,14 +337,14 @@ public class LzTool extends PanBase {
String pwd = shareLinkInfo.getSharePassword();
webClientSession.getAbs(sUrl).send().onSuccess(res -> {
String html = res.bodyAsString();
String html = asText(res);
// 检查是否需要 cookie 验证
if (html.contains("var arg1='")) {
webClientSession = WebClientSession.create(clientNoRedirects);
setCookie(html);
setCookie(html, sUrl);
// 重新请求
webClientSession.getAbs(sUrl).send().onSuccess(res2 -> {
handleFileListParse(res2.bodyAsString(), pwd, sUrl, promise);
handleFileListParse(asText(res2), pwd, sUrl, promise);
}).onFailure(err -> promise.fail(err));
return;
}
@@ -312,6 +354,11 @@ public class LzTool extends PanBase {
}
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 {
String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
@@ -321,12 +368,12 @@ public class LzTool extends PanBase {
log.debug("解析参数: {}", map);
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 -> {
String resBody = asText(res2);
// 再次检查是否需要 cookie 验证
if (resBody.contains("var arg1='")) {
setCookie(resBody);
setCookie(resBody, url);
// 重新请求
webClientSession.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res3 -> {
handleFileListResponse(asText(res3), promise);
@@ -335,7 +382,7 @@ public class LzTool extends PanBase {
}
handleFileListResponse(resBody, promise);
}).onFailure(err -> promise.fail(err));
} catch (ScriptException | NoSuchMethodException e) {
} catch (ScriptException | NoSuchMethodException | RuntimeException e) {
promise.fail(e);
}
}
@@ -367,14 +414,20 @@ public class LzTool extends PanBase {
Long sizeNum = FileSizeConverter.convertToBytes(size);
String panType = shareLinkInfo.getType();
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)
.setCreateTime(fileJson.getString("time"))
.setFileType(fileJson.getString("icon"))
.setSizeStr(fileJson.getString("size"))
.setSize(sizeNum)
.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(),
shareLinkInfo.getType(), id));
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) {
// 写入 fileInfo
FileInfo fileInfo = new FileInfo();
@@ -400,16 +462,17 @@ public class LzTool extends PanBase {
String fileId = CommonUtils.extract(html, Pattern.compile("\\?f=(.*?)&|fid = (.*?);"));
String createTime = CommonUtils.extract(html, Pattern.compile(">上传时间:</span>(.*?)<"));
try {
long bytes = FileSizeConverter.convertToBytes(sizeStr);
fileInfo.setFileName(fileName)
.setSize(bytes)
.setSizeStr(FileSizeConverter.convertToReadableSize(bytes))
.setCreateBy(createBy)
.setPanType(shareLinkInfo.getType())
.setDescription(description)
.setFileType("file")
.setFileId(fileId)
.setCreateTime(createTime);
if (sizeStr != null && !sizeStr.isBlank()) {
long bytes = FileSizeConverter.convertToBytes(sizeStr);
fileInfo.setSize(bytes).setSizeStr(FileSizeConverter.convertToReadableSize(bytes));
}
} catch (Exception e) {
log.warn("文件信息解析异常", e);
}

View File

@@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""
飞书公开分享 直链解析 + 批量下载 (aria2/Motrix)
支持: 单文件链接 / 文件夹链接(递归子目录)
用法:
python feishu_dl.py <链接> # 推送到 Motrix
python feishu_dl.py <链接> -d D:/Downloads # 指定下载目录
python feishu_dl.py <链接> --list # 仅列出文件,不下载
python feishu_dl.py <链接> --aria2c # 输出 aria2c 命令行
"""
import sys, os, re, json, uuid, ssl, gzip, argparse
import http.cookiejar
import urllib.request, urllib.error
from urllib.parse import unquote, quote
# ─── Motrix aria2 RPC 默认配置 ──────────────────────────
ARIA2_RPC_URL = "http://localhost:16800/jsonrpc"
ARIA2_SECRET = "motrix"
# ────────────────────────────────────────────────────────
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36")
# 飞书 obj_type 映射 (type=12 上传文件可下载, type=0 文件夹可递归)
OBJ_TYPES = {
0: "📁 文件夹", 2: "📝 旧版文档", 3: "📊 表格", 8: "🧠 思维导图",
11: "📽 幻灯片", 12: "📄 文件", 22: "📝 新版文档", 30: "📋 画板",
44: "📊 多维表格", 84: "📑 知识库", 123: "❓ 未知", 124: "❓ 未知",
}
# v3 列表 API 支持的 obj_type
LIST_OBJ_TYPES = [0, 2, 22, 44, 3, 30, 8, 11, 12, 84, 123, 124]
# ─── 网络工具 ────────────────────────────────────────────
def _ctx():
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def make_opener(jar):
return urllib.request.build_opener(
urllib.request.HTTPSHandler(context=_ctx()),
urllib.request.HTTPCookieProcessor(jar),
)
def decode_body(resp):
data = resp.read()
if resp.headers.get("Content-Encoding") == "gzip":
data = gzip.decompress(data)
return data.decode("utf-8", errors="replace")
def cookie_string(jar):
return "; ".join(f"{c.name}={c.value}" for c in jar)
def human_size(n):
for u in ("B", "KB", "MB", "GB"):
if n < 1024: return f"{n:.1f} {u}"
n /= 1024
return f"{n:.1f} TB"
# ─── 飞书核心 API ────────────────────────────────────────
def parse_share_url(url):
"""返回 (tenant, token, link_type:'file'|'folder')"""
m = re.match(r'https://([^.]+)\.feishu\.cn/file/([A-Za-z0-9_-]+)', url)
if m: return m.group(1), m.group(2), "file"
m = re.match(r'https://([^.]+)\.feishu\.cn/drive/folder/([A-Za-z0-9_-]+)', url)
if m: return m.group(1), m.group(2), "folder"
return None, None, None
def fetch_session(share_url):
"""访问分享页拿匿名 session cookie"""
jar = http.cookiejar.CookieJar()
opener = make_opener(jar)
req = urllib.request.Request(share_url)
req.add_header("User-Agent", UA)
req.add_header("Accept", "text/html,*/*")
opener.open(req, timeout=15).read()
return jar
def list_folder(tenant, folder_token, jar, page_label=""):
"""
v3 API 列出文件夹内容 (单页)
GET /space/api/explorer/v3/children/list/?token=xxx&length=50&...
"""
base = f"https://{tenant}.feishu.cn"
params = ["length=50", "asc=1", "rank=5", f"token={folder_token}"]
for t in LIST_OBJ_TYPES:
params.append(f"obj_type={t}")
if page_label:
params.append(f"last_label={quote(page_label, safe='')}")
url = f"{base}/space/api/explorer/v3/children/list/?{'&'.join(params)}"
opener = make_opener(jar)
req = urllib.request.Request(url)
req.add_header("User-Agent", UA)
req.add_header("Accept", "application/json, text/plain, */*")
req.add_header("Referer", f"{base}/drive/folder/{folder_token}")
resp = opener.open(req, timeout=15)
data = json.loads(decode_body(resp))
if data.get("code") != 0:
raise RuntimeError(f"API error: {data.get('msg')}")
d = data["data"]
nodes = d.get("entities", {}).get("nodes", {})
node_list = d.get("node_list", [])
items = []
for nid in node_list:
node = nodes.get(nid, {})
obj_type = node.get("type", -1)
obj_token = node.get("obj_token", "")
name = node.get("name", "unknown")
extra = node.get("extra", {})
try: size = int(extra.get("size", "0"))
except: size = 0
items.append({
"name": name, "obj_token": obj_token, "type": obj_type,
"size": size, "url": node.get("url", ""),
"is_folder": obj_type == 0,
"type_name": OBJ_TYPES.get(obj_type, f"❓ type={obj_type}"),
})
# 排除文件夹自身节点
items = [it for it in items if it["obj_token"] != folder_token]
return items, d.get("has_more", False), d.get("last_label", "")
def list_folder_all(tenant, folder_token, jar):
"""分页获取文件夹全部内容"""
all_items, label = [], ""
while True:
items, has_more, label = list_folder(tenant, folder_token, jar, label)
all_items.extend(items)
if not has_more: break
return all_items
def walk_folder(tenant, folder_token, jar, prefix="", depth=0):
"""递归遍历, 返回扁平列表 [{..., path:"a/b/file.txt"}]"""
if depth > 10: # 防止无限递归
return []
items = list_folder_all(tenant, folder_token, jar)
result = []
for it in items:
if it["is_folder"]:
sub = walk_folder(tenant, it["obj_token"], jar,
prefix=f"{prefix}{it['name']}/", depth=depth+1)
result.extend(sub)
else:
it["path"] = f"{prefix}{it['name']}"
result.append(it)
return result
def probe_file(tenant, obj_token, jar, referer):
"""Range 探测文件名 + 大小 (只取1字节)"""
dl_url = f"https://{tenant}.feishu.cn/space/api/box/stream/download/all/{obj_token}"
opener = make_opener(jar)
req = urllib.request.Request(dl_url)
req.add_header("User-Agent", UA)
req.add_header("Referer", referer)
req.add_header("Range", "bytes=0-0")
resp = opener.open(req, timeout=15)
cd = resp.headers.get("Content-Disposition", "")
cr = resp.headers.get("Content-Range", "")
resp.read()
filename = ""
m = re.search(r"filename\*=UTF-8''(.+?)(?:;|$)", cd)
if m: filename = unquote(m.group(1).strip())
if not filename:
m = re.search(r'filename="?([^";]+)"?', cd)
if m: filename = unquote(m.group(1).strip())
total = 0
m = re.search(r'/(\d+)', cr)
if m: total = int(m.group(1))
return filename, total
# ─── aria2 RPC ───────────────────────────────────────────
def aria2_add(dl_url, cs, referer, filename, out_dir=None):
opts = {"header": [f"Cookie: {cs}", f"Referer: {referer}", f"User-Agent: {UA}"]}
if filename: opts["out"] = filename
if out_dir: opts["dir"] = out_dir
payload = json.dumps({
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
"method": "aria2.addUri",
"params": [f"token:{ARIA2_SECRET}", [dl_url], opts],
}).encode()
req = urllib.request.Request(ARIA2_RPC_URL, data=payload,
headers={"Content-Type": "application/json"})
resp = urllib.request.urlopen(req, timeout=10)
return json.loads(resp.read().decode()).get("result", "")
def push_one(dl_url, cs, referer, filename, out_dir, quiet=False):
try:
gid = aria2_add(dl_url, cs, referer, filename, out_dir)
if gid:
if not quiet: print(f" [✓] GID={gid} {filename}")
return True
except urllib.error.URLError:
if not quiet:
print(f" [✗] Motrix 未启动, RPC: {ARIA2_RPC_URL}")
except Exception as e:
if not quiet: print(f" [✗] {e}")
return False
def print_aria2c(dl_url, cs, referer, filename, out_dir):
print(f'aria2c --header="Cookie: {cs}" \\')
print(f' --header="Referer: {referer}" \\')
print(f' --header="User-Agent: {UA}" \\')
if filename: print(f' -o "{filename}" \\')
if out_dir: print(f' -d "{out_dir}" \\')
print(f' "{dl_url}"')
# ─── 主流程 ──────────────────────────────────────────────
def handle_file(tenant, token, jar, args):
share_url = f"https://{tenant}.feishu.cn/file/{token}"
dl_url = f"https://{tenant}.feishu.cn/space/api/box/stream/download/all/{token}"
cs = cookie_string(jar)
print(f"[2/3] 探测文件 ...")
filename, size = probe_file(tenant, token, jar, share_url)
print(f" {filename} ({human_size(size)})")
if args.list: return
if args.aria2c:
print_aria2c(dl_url, cs, share_url, filename, args.dir); return
print(f"[3/3] 推送到 Motrix ...")
if not push_one(dl_url, cs, share_url, filename, args.dir):
print(f"\n 降级输出 aria2c 命令:\n")
print_aria2c(dl_url, cs, share_url, filename, args.dir)
def handle_folder(tenant, token, jar, args):
base = f"https://{tenant}.feishu.cn"
cs = cookie_string(jar)
print(f"[2/4] 递归扫描文件夹 ...")
all_files = walk_folder(tenant, token, jar)
downloadable = [f for f in all_files if f["type"] == 12]
skipped = [f for f in all_files if f["type"] != 12]
print(f"{len(all_files)} 项: "
f"{len(downloadable)} 可下载, {len(skipped)} 在线文档(跳过)")
if skipped:
print(f"\n ⏭ 跳过的在线文档:")
for f in skipped:
print(f" {f['type_name']} {f['path']}")
if not downloadable:
print("\n 没有可下载的文件"); return
# 探测真实文件名和大小
print(f"\n[3/4] 探测文件信息 ...")
total_size = 0
for f in downloadable:
referer = f.get("url", f"{base}/drive/folder/{token}")
try:
real_name, size = probe_file(tenant, f["obj_token"], jar, referer)
f["real_name"] = real_name or f["name"]
f["size"] = size or f["size"]
except:
f["real_name"] = f["name"]
total_size += f["size"]
# 打印文件列表
print(f"\n {''*60}")
print(f" {'#':>3} {'文件名':<35} {'大小':>10} 路径")
print(f" {''*60}")
for i, f in enumerate(downloadable):
sz = human_size(f["size"]) if f["size"] else " ?"
print(f" {i+1:>3} {f['real_name']:<35} {sz:>10} {f['path']}")
print(f" {''*60}")
print(f" 合计: {len(downloadable)} 文件, {human_size(total_size)}")
if args.list: return
# 下载
print(f"\n[4/4] {'aria2c 命令' if args.aria2c else '推送 Motrix'} ...")
ok = 0
for f in downloadable:
dl_url = f"{base}/space/api/box/stream/download/all/{f['obj_token']}"
sub = os.path.dirname(f["path"])
out_dir = os.path.join(args.dir, sub) if args.dir and sub else (args.dir or None)
if args.aria2c:
print_aria2c(dl_url, cs, base, f["real_name"], out_dir)
print()
ok += 1
else:
if push_one(dl_url, cs, base, f["real_name"], out_dir, quiet=True):
ok += 1
print(f" [✓] {f['real_name']}")
else:
print(f" [✗] {f['real_name']} — Motrix 未响应")
print(f" 降级 aria2c:\n")
print_aria2c(dl_url, cs, base, f["real_name"], out_dir)
return # Motrix 挂了就不继续了
print(f"\n 完成! {ok}/{len(downloadable)}")
# ─── 入口 ────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description="飞书分享解析下载器 v2")
ap.add_argument("url", help="飞书分享链接 (文件/文件夹)")
ap.add_argument("-d", "--dir", default=None, help="下载目录")
ap.add_argument("--list", action="store_true", help="仅列出,不下载")
ap.add_argument("--aria2c", action="store_true", help="输出 aria2c 命令")
args = ap.parse_args()
tenant, token, lt = parse_share_url(args.url)
if not token:
print(f"[✗] 不支持的链接: {args.url}")
print(f" 格式: https://xxx.feishu.cn/file/xxxToken")
print(f" https://xxx.feishu.cn/drive/folder/xxxToken")
sys.exit(1)
print(f"\n{'📄' if lt=='file' else '📁'} 飞书分享解析 [{lt}] {tenant}/{token}")
print(f"[1/{3 if lt=='file' else 4}] 获取匿名会话 ...")
jar = fetch_session(args.url)
print(f" {sum(1 for _ in jar)} cookies")
if lt == "file":
handle_file(tenant, token, jar, args)
else:
handle_folder(tenant, token, jar, args)
print()
if __name__ == "__main__":
main()

View File

@@ -7,11 +7,14 @@ import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.regex.Pattern.compile;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="https://qaiu.top">QAIU</a>
@@ -77,15 +80,179 @@ 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
public void testLzPatternWebgetstore() {
Pattern lzPattern = PanDomainTemplate.LZ.getPattern();
// webgetstore.com 以前遗漏,现已补入
Matcher m1 = lzPattern.matcher("https://webgetstore.com/somekey");
assertTrue("LZ should match webgetstore.com", m1.find());
assertEquals("somekey", m1.group("KEY"));
Matcher m2 = lzPattern.matcher("https://www.webgetstore.com/somekey");
assertTrue("LZ should match www.webgetstore.com", m2.find());
assertEquals("somekey", m2.group("KEY"));
// t-is.cn 以前遗漏,现已补入
Matcher m3 = lzPattern.matcher("https://t-is.cn/somekey");
assertTrue("LZ should match t-is.cn", m3.find());
assertEquals("somekey", m3.group("KEY"));
Matcher m4 = lzPattern.matcher("https://www.t-is.cn/somekey");
assertTrue("LZ should match www.t-is.cn", m4.find());
assertEquals("somekey", m4.group("KEY"));
// 已有域名仍然正常匹配
Matcher m5 = lzPattern.matcher("https://www.lanzoul.com/somekey");
assertTrue("LZ should match existing domain lanzoul.com", m5.find());
assertEquals("somekey", m5.group("KEY"));
}
@Test
public void testLePatternFix() {
Pattern lePattern = PanDomainTemplate.LE.getPattern();
// lecloud.lenovo.com 应匹配
Matcher m1 = lePattern.matcher("https://lecloud.lenovo.com/share/abc123");
assertTrue("LE should match lecloud.lenovo.com", m1.find());
assertEquals("abc123", m1.group("KEY"));
// leclou.lenovo.com (去掉'd') 不应匹配(原 lecloud? 的 bug
assertFalse("LE should NOT match leclou.lenovo.com",
lePattern.matcher("https://leclou.lenovo.com/share/abc123").find());
}
@Test
public void testCowPatternFix() {
Pattern cowPattern = PanDomainTemplate.COW.getPattern();
// 正常域名
Matcher m1 = cowPattern.matcher("https://cowtransfer.com/s/abc123");
assertTrue("COW should match cowtransfer.com", m1.find());
assertEquals("abc123", m1.group("KEY"));
Matcher m2 = cowPattern.matcher("https://share.cowtransfer.com/s/abc123");
assertTrue("COW should match share.cowtransfer.com", m2.find());
assertEquals("abc123", m2.group("KEY"));
// 潜在的URL注入`(.*)` 是贪婪捕获组,可匹配 `evil.com/redirect/` 等前缀,
// 使形如 `https://evil.com/redirect/cowtransfer.com/s/key` 的 URL 被误识别。
// 修复后改为 `(?:[a-zA-Z\d-]+\.)?` 仅匹配一级合法子域名(可选),消除误匹配。
assertFalse("COW should NOT match redirect URLs containing cowtransfer.com in path",
cowPattern.matcher("https://evil.com/redirect/cowtransfer.com/s/abc").find());
}
@Test
public void testMnePatternFix() {
Pattern mnePattern = PanDomainTemplate.MNE.getPattern();
// 带 #/ 前缀的完整网页链接(修复前因 (y.) 未转义而存在 bug
Matcher m1 = mnePattern.matcher("https://music.163.com/#/song?id=12345");
assertTrue("MNE should match #/song format", m1.find());
assertEquals("12345", m1.group("KEY"));
// 带 m/ 前缀的移动端链接
Matcher m2 = mnePattern.matcher("https://music.163.com/m/song?id=12345");
assertTrue("MNE should match m/song format", m2.find());
assertEquals("12345", m2.group("KEY"));
// y.music.163.com 子域名
Matcher m3 = mnePattern.matcher("https://y.music.163.com/song?id=12345");
assertTrue("MNE should match y.music.163.com", m3.find());
assertEquals("12345", m3.group("KEY"));
// 原 (y.) 中 `.` 未转义(`.` 匹配任意字符):对于 `yXmusic.163.com`
// `(y.)` 会消费 `yX`y + 任意字符),剩余 `music.163.com` 再被 `music\.163\.com` 匹配,导致误匹配。
// 修复后 `(y\.)` 要求字面 `.``yX` 中 X ≠ `.` 无法匹配,不再误匹配。
assertFalse("MNE should NOT match yXmusic.163.com (old (y.) could erroneously match via backtracking)",
mnePattern.matcher("https://yXmusic.163.com/song?id=12345").find());
}
@Test
public void testP115PatternFix() {
Pattern p115Pattern = PanDomainTemplate.P115.getPattern();
// 正常匹配
Matcher m1 = p115Pattern.matcher("https://115.com/s/abc123");
assertTrue("P115 should match 115.com", m1.find());
assertEquals("abc123", m1.group("KEY"));
Matcher m2 = p115Pattern.matcher("https://anxia.com/s/abc123");
assertTrue("P115 should match anxia.com", m2.find());
assertEquals("abc123", m2.group("KEY"));
// 原 .com 未转义时 115Xcom 会被误匹配(现已修复)
assertFalse("P115 should NOT match 115Xcom",
p115Pattern.matcher("https://115Xcom/s/abc123").find());
}
@Test
public void testPgdSubdomain() {
Pattern pgdPattern = PanDomainTemplate.PGD.getPattern();
// 标准链接
Matcher m1 = pgdPattern.matcher("https://drive.google.com/file/d/abc123/view?usp=sharing");
assertTrue("PGD should match standard drive.google.com", m1.find());
assertEquals("abc123", m1.group("KEY"));
// 带子域名的链接(修复后支持)
Matcher m2 = pgdPattern.matcher("https://adsd.drive.google.com/file/d/151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz/view");
assertTrue("PGD should match subdomain.drive.google.com", m2.find());
assertEquals("151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz", m2.group("KEY"));
}
@Test
public void verifyDuplicates() {
Matcher matcher = compile("https://(?:[a-zA-Z\\d-]+\\.)?drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?")
.matcher("https://adsd.drive.google.com/file/d/151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz/view");
if (matcher.find()) {
System.out.println(matcher.group());
System.out.println(matcher.group("KEY"));
}
// 校验重复
Set<String> collect =
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getRegex).collect(Collectors.toSet());

View File

@@ -74,7 +74,7 @@
<dependency>
<groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId>
<version>10.2.3</version>
<version>10.2.5</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -274,7 +274,7 @@
name: '联想乐云'
},
fangcloud: {
reg: /https:\/\/v2\.fangcloud\.(com|cn)\/(s|sharing)\/.+/,
reg: /https:\/\/v2\.fangcloud\.(com|cn)\/(s|share|sharing)\/.+/,
host: /fangcloud\.(com|cn)/,
name: '亿方云'
},