mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-04-18 22:56:56 +00:00
Compare commits
16 Commits
copilot/an
...
copilot/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
803e0a1cb3 | ||
|
|
51b2d3119b | ||
|
|
cc9d0a4b30 | ||
|
|
696ef832f8 | ||
|
|
442f9d1d2e | ||
|
|
1a949725f3 | ||
|
|
7c14f3437b | ||
|
|
bc402da365 | ||
|
|
b95b474660 | ||
|
|
691a3770d9 | ||
|
|
49ec54a3b5 | ||
|
|
2fc15f437e | ||
|
|
190f6ca7ab | ||
|
|
c683fd27d4 | ||
|
|
d815cc1010 | ||
|
|
fd84ff1200 |
43
README.md
43
README.md
@@ -1,3 +1,15 @@
|
|||||||
|
# 一款网盘分享链接云解析快速下载服务
|
||||||
|
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;">
|
<div align="center" style="display:flex; justify-content:center; gap:10px; align-items:flex-start;">
|
||||||
<img
|
<img
|
||||||
src="https://github.com/user-attachments/assets/bf266d0a-aaf8-4772-9231-e38a4b7bb6cb"
|
src="https://github.com/user-attachments/assets/bf266d0a-aaf8-4772-9231-e38a4b7bb6cb"
|
||||||
@@ -10,21 +22,14 @@
|
|||||||
style="width:300px; max-width:300px; flex:none;"
|
style="width:300px; max-width:300px; flex:none;"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<p align="center">
|
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链,可广泛应用于各类下载站,资源站,个人博客,图床,APP下载更新,视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||||
<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>
|
|
||||||
|
|
||||||
# netdisk-fast-download 网盘分享链接云解析服务
|
[官方文档](https://nfd-parser.github.io/)
|
||||||
QQ交流群:1017480890
|
[API接入](https://nfdparser.apifox.cn/)
|
||||||
|
[公益解析,lz站](https://lz.qaiu.top)
|
||||||
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
[公益解析,lz0站](https://lz0.qaiu.top)
|
||||||
|
[专业版189站,注册体验](https://189.qaiu.top)
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
命令行下载分享文件:
|
命令行下载分享文件:
|
||||||
@@ -50,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)
|
**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)
|
**注意⚠️小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
||||||
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
|
**注意⚠️收到很多用户反馈,小飞机近期封号频繁,请尽可能选择其他网盘分享**
|
||||||
**小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
**注意⚠️请不要过度依赖 lz.qaiu.top,建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制,遇到解析失败的分享链接不要着急提issues,请先检查分享是否有效,** [lz站](https://lz.qaiu.top) 和 [lz0](https://lz0.qaiu.top) 不支持大文件,请使用 [189站](https://189.qaiu.top) 注册体验。
|
||||||
**注意: 请不要过度依赖lz.qaiu.top预览地址服务,建议本地搭建或者云服务器自行搭建。解析次数过多IP会被部分网盘厂商限制,不推荐做公共解析。**
|
|
||||||
|
|
||||||
## 网盘支持情况:
|
## 网盘支持情况:
|
||||||
> 20230905 奶牛云直链做了防盗链,需加入请求头:Referer: https://cowtransfer.com/
|
> 20230905 奶牛云直链做了防盗链,需加入请求头:Referer: https://cowtransfer.com/
|
||||||
@@ -87,6 +87,7 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
|
|||||||
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
|
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
|
||||||
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
|
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
|
||||||
- [WPS云文档-pwps](https://www.kdocs.cn/)
|
- [WPS云文档-pwps](https://www.kdocs.cn/)
|
||||||
|
- [飞书云盘-fs](https://www.feishu.cn/)
|
||||||
- [汽水音乐-qishui_music](https://music.douyin.com/qishui/)
|
- [汽水音乐-qishui_music](https://music.douyin.com/qishui/)
|
||||||
- [咪咕音乐-migu](https://music.migu.cn/)
|
- [咪咕音乐-migu](https://music.migu.cn/)
|
||||||
- [一刻相册-baidu_photo](https://photo.baidu.com/)
|
- [一刻相册-baidu_photo](https://photo.baidu.com/)
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ public enum PanDomainTemplate {
|
|||||||
t-is.cn
|
t-is.cn
|
||||||
*/
|
*/
|
||||||
LZ("蓝奏云",
|
LZ("蓝奏云",
|
||||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?(" +
|
compile("https://(?:[a-zA-Z\\d-]+\\.)?(?:" +
|
||||||
"lanzoul|" +
|
"(?:lanzoul|" +
|
||||||
"lanzouh|" +
|
"lanzouh|" +
|
||||||
"lanosso|" +
|
"lanosso|" +
|
||||||
"lanpv|" +
|
"lanpv|" +
|
||||||
@@ -95,14 +95,16 @@ public enum PanDomainTemplate {
|
|||||||
"lanzv|" +
|
"lanzv|" +
|
||||||
"dmpdmp|" +
|
"dmpdmp|" +
|
||||||
"lanrar|" +
|
"lanrar|" +
|
||||||
|
"webgetstore|" +
|
||||||
"lanzb|" +
|
"lanzb|" +
|
||||||
"lanzoux|" +
|
"lanzoux|" +
|
||||||
"lanzout|" +
|
"lanzout|" +
|
||||||
"lanzouc|" +
|
"lanzouc|" +
|
||||||
"lanzoui|" +
|
"lanzoui|" +
|
||||||
"lanzoug|" +
|
"lanzoug|" +
|
||||||
"lanzoum" +
|
"lanzoum)\\.com" +
|
||||||
")\\.com/(?<KEY>.+)"),
|
"|t-is\\.cn" +
|
||||||
|
")/(?<KEY>.+)"),
|
||||||
"https://w1.lanzn.com/{shareKey}",
|
"https://w1.lanzn.com/{shareKey}",
|
||||||
LzTool.class),
|
LzTool.class),
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ public enum PanDomainTemplate {
|
|||||||
|
|
||||||
// https://lecloud.lenovo.com/share/
|
// https://lecloud.lenovo.com/share/
|
||||||
LE("联想乐云",
|
LE("联想乐云",
|
||||||
compile("https://lecloud?\\.lenovo\\.com/share/(?<KEY>.+)"),
|
compile("https://lecloud\\.lenovo\\.com/share/(?<KEY>.+)"),
|
||||||
"https://lecloud.lenovo.com/share/{shareKey}",
|
"https://lecloud.lenovo.com/share/{shareKey}",
|
||||||
LeTool.class),
|
LeTool.class),
|
||||||
|
|
||||||
@@ -241,7 +243,7 @@ public enum PanDomainTemplate {
|
|||||||
EcTool.class),
|
EcTool.class),
|
||||||
// https://cowtransfer.com/s/
|
// https://cowtransfer.com/s/
|
||||||
COW("奶牛快传",
|
COW("奶牛快传",
|
||||||
compile("https://(.*)cowtransfer\\.com/s/(?<KEY>.+)"),
|
compile("https://(?:[a-zA-Z\\d-]+\\.)?cowtransfer\\.com/s/(?<KEY>.+)"),
|
||||||
"https://cowtransfer.com/s/{shareKey}",
|
"https://cowtransfer.com/s/{shareKey}",
|
||||||
CowTool.class),
|
CowTool.class),
|
||||||
CT("城通网盘",
|
CT("城通网盘",
|
||||||
@@ -264,7 +266,7 @@ public enum PanDomainTemplate {
|
|||||||
PodTool.class),
|
PodTool.class),
|
||||||
// 404网盘 https://drive.google.com/file/d/xxx/view?usp=sharing
|
// 404网盘 https://drive.google.com/file/d/xxx/view?usp=sharing
|
||||||
PGD("GoogleDrive",
|
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",
|
"https://drive.google.com/file/d/{shareKey}/view?usp=sharing",
|
||||||
PgdTool.class),
|
PgdTool.class),
|
||||||
// iCloud https://www.icloud.com.cn/iclouddrive/xxx#fonts
|
// iCloud https://www.icloud.com.cn/iclouddrive/xxx#fonts
|
||||||
@@ -274,11 +276,11 @@ public enum PanDomainTemplate {
|
|||||||
PicTool.class),
|
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
|
// 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",
|
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",
|
"https://www.dropbox.com/scl/fi/{shareKey}/?rlkey={pwd}&dl=0",
|
||||||
PdbTool.class),
|
PdbTool.class),
|
||||||
P115("115网盘",
|
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}",
|
"https://115.com/s/{shareKey}?password={pwd}",
|
||||||
P115Tool.class),
|
P115Tool.class),
|
||||||
// 链接:https://www.yunpan.com/surl_yD7wz4VgU9v(提取码:fc70)
|
// 链接:https://www.yunpan.com/surl_yD7wz4VgU9v(提取码:fc70)
|
||||||
@@ -310,6 +312,13 @@ public enum PanDomainTemplate {
|
|||||||
compile("https://pan\\.quark\\.cn/s/(?<KEY>\\w+)([&#].*)?"),
|
compile("https://pan\\.quark\\.cn/s/(?<KEY>\\w+)([&#].*)?"),
|
||||||
"https://pan.quark.cn/s/{shareKey}",
|
"https://pan.quark.cn/s/{shareKey}",
|
||||||
QkTool.class),
|
QkTool.class),
|
||||||
|
// https://kcncuknojm60.feishu.cn/file/VnCxbt35KoowKoxldO3c3C7VnMc?from=from_copylink
|
||||||
|
// https://kcncuknojm60.feishu.cn/drive/folder/RQSKf8EQ4l7dMedqzHucpMbancg?from=from_copylink
|
||||||
|
FS("飞书云盘",
|
||||||
|
compile("https://[a-zA-Z\\d]+\\.feishu\\.cn/(file|drive/folder)/(?<KEY>[a-zA-Z\\d_-]+)(\\?.*)?"),
|
||||||
|
"https://feishu.cn/file/{shareKey}",
|
||||||
|
"https://www.feishu.cn/",
|
||||||
|
FsTool.class),
|
||||||
|
|
||||||
// =====================音乐类解析 分享链接标志->MxxS (单歌曲/普通音质)==========================
|
// =====================音乐类解析 分享链接标志->MxxS (单歌曲/普通音质)==========================
|
||||||
// http://163cn.tv/xxx
|
// http://163cn.tv/xxx
|
||||||
@@ -319,7 +328,7 @@ public enum PanDomainTemplate {
|
|||||||
MnesTool.class),
|
MnesTool.class),
|
||||||
// https://music.163.com/#/song?id=xxx
|
// https://music.163.com/#/song?id=xxx
|
||||||
MNE("网易云音乐歌曲详情",
|
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}",
|
"https://music.163.com/#/song?id={shareKey}",
|
||||||
MnesTool.MneTool.class),
|
MnesTool.MneTool.class),
|
||||||
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
|
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
|
||||||
@@ -340,7 +349,7 @@ public enum PanDomainTemplate {
|
|||||||
MkgsTool.class),
|
MkgsTool.class),
|
||||||
// https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4"
|
// https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4"
|
||||||
MKGS2("酷狗音乐分享2",
|
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",
|
"https://www.kugou.com/share/{shareKey}.html",
|
||||||
MkgsTool.Mkgs2Tool.class),
|
MkgsTool.Mkgs2Tool.class),
|
||||||
// https://www.kugou.com/mixsong/2bi8Fe9CSV3
|
// https://www.kugou.com/mixsong/2bi8Fe9CSV3
|
||||||
|
|||||||
100
parser/src/main/java/cn/qaiu/parser/impl/FsTool.java
Normal file
100
parser/src/main/java/cn/qaiu/parser/impl/FsTool.java
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.parser.PanBase;
|
||||||
|
import io.vertx.core.Future;
|
||||||
|
import io.vertx.core.json.JsonObject;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://www.feishu.cn/">飞书云盘(fs)</a>
|
||||||
|
* 分享格式:
|
||||||
|
* <ul>
|
||||||
|
* <li>文件: https://xxx.feishu.cn/file/TOKEN?from=from_copylink</li>
|
||||||
|
* <li>文件夹: https://xxx.feishu.cn/drive/folder/TOKEN?from=from_copylink</li>
|
||||||
|
* </ul>
|
||||||
|
* ?from=from_copylink 是可选参数,没有分享密码
|
||||||
|
*/
|
||||||
|
public class FsTool extends PanBase {
|
||||||
|
|
||||||
|
private static final String DOWNLOAD_API_PATH = "/space/api/box/stream/download/all/";
|
||||||
|
private static final String UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||||
|
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
|
public FsTool(ShareLinkInfo shareLinkInfo) {
|
||||||
|
super(shareLinkInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<String> parse() {
|
||||||
|
String shareUrl = shareLinkInfo.getShareUrl();
|
||||||
|
try {
|
||||||
|
// 去除查询参数以获取干净的URL
|
||||||
|
String cleanUrl = shareUrl.contains("?") ? shareUrl.substring(0, shareUrl.indexOf("?")) : shareUrl;
|
||||||
|
URL url = new URL(cleanUrl);
|
||||||
|
String host = url.getHost();
|
||||||
|
String path = url.getPath();
|
||||||
|
String token = shareLinkInfo.getShareKey();
|
||||||
|
|
||||||
|
if (path.contains("/file/")) {
|
||||||
|
// 文件分享 - 获取下载直链
|
||||||
|
getDownloadUrl(host, token);
|
||||||
|
} else if (path.contains("/drive/folder/")) {
|
||||||
|
fail("飞书文件夹分享暂不支持直接下载,请使用文件分享链接");
|
||||||
|
} else {
|
||||||
|
fail("不支持的飞书链接格式: {}", path);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail(e, "解析飞书分享链接失败");
|
||||||
|
}
|
||||||
|
return promise.future();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过飞书下载API获取文件直链
|
||||||
|
*
|
||||||
|
* @param host 飞书域名 (如 kcncuknojm60.feishu.cn)
|
||||||
|
* @param token 文件token
|
||||||
|
*/
|
||||||
|
private void getDownloadUrl(String host, String token) {
|
||||||
|
String downloadApiUrl = "https://" + host + DOWNLOAD_API_PATH + token + "?mount_point=explorer";
|
||||||
|
|
||||||
|
clientNoRedirects.getAbs(downloadApiUrl)
|
||||||
|
.putHeader("User-Agent", UA)
|
||||||
|
.putHeader("Referer", "https://" + host + "/")
|
||||||
|
.send()
|
||||||
|
.onSuccess(res -> {
|
||||||
|
int statusCode = res.statusCode();
|
||||||
|
if (statusCode == 302 || statusCode == 301) {
|
||||||
|
String location = res.getHeader("Location");
|
||||||
|
if (location != null && !location.isEmpty()) {
|
||||||
|
log.info("飞书文件解析成功: token={}", token);
|
||||||
|
complete(location);
|
||||||
|
} else {
|
||||||
|
fail("飞书下载API返回{}但没有Location头", statusCode);
|
||||||
|
}
|
||||||
|
} else if (statusCode == 200) {
|
||||||
|
// 部分情况下API返回JSON格式的下载链接
|
||||||
|
try {
|
||||||
|
JsonObject json = asJson(res);
|
||||||
|
if (json.containsKey("code") && json.getInteger("code") == 0) {
|
||||||
|
String downloadUrl = json.getString("url");
|
||||||
|
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||||
|
log.info("飞书文件解析成功(JSON): token={}", token);
|
||||||
|
complete(downloadUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
// 如果返回200且不是JSON,则API地址本身可能就是下载地址
|
||||||
|
complete(downloadApiUrl);
|
||||||
|
} else {
|
||||||
|
fail("飞书下载API返回非预期状态码: {}, body: {}", statusCode,
|
||||||
|
res.bodyAsString());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onFailure(handleFail("请求飞书下载API"));
|
||||||
|
}
|
||||||
|
}
|
||||||
117
parser/src/main/resources/py/feishu-dl.py
Normal file
117
parser/src/main/resources/py/feishu-dl.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
飞书云盘分享链接解析工具
|
||||||
|
支持格式:
|
||||||
|
文件: https://xxx.feishu.cn/file/TOKEN?from=from_copylink
|
||||||
|
文件夹: https://xxx.feishu.cn/drive/folder/TOKEN?from=from_copylink
|
||||||
|
?from=from_copylink 是可选参数,没有分享密码
|
||||||
|
|
||||||
|
用法: python feishu-dl.py <飞书分享链接>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def download_feishu_file(share_url):
|
||||||
|
"""
|
||||||
|
解析飞书云盘文件分享链接,获取直链下载地址
|
||||||
|
|
||||||
|
:param share_url: 飞书分享链接
|
||||||
|
:return: 下载直链 或 None
|
||||||
|
"""
|
||||||
|
# 提取域名和文件token
|
||||||
|
match = re.match(
|
||||||
|
r'https://([a-zA-Z\d]+)\.feishu\.cn/(file|drive/folder)/([a-zA-Z\d_-]+)',
|
||||||
|
share_url
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
print(f"无法解析链接: {share_url}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
tenant = match.group(1)
|
||||||
|
link_type = match.group(2)
|
||||||
|
token = match.group(3)
|
||||||
|
host = f"{tenant}.feishu.cn"
|
||||||
|
|
||||||
|
print(f"租户: {tenant}")
|
||||||
|
print(f"类型: {'文件' if link_type == 'file' else '文件夹'}")
|
||||||
|
print(f"Token: {token}")
|
||||||
|
|
||||||
|
if link_type == "drive/folder":
|
||||||
|
print("文件夹分享暂不支持直接下载")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 构建下载API URL
|
||||||
|
download_api_url = (
|
||||||
|
f"https://{host}/space/api/box/stream/download/all/{token}"
|
||||||
|
f"?mount_point=explorer"
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': (
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
||||||
|
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||||
|
'Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
),
|
||||||
|
'Referer': f'https://{host}/',
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"\n请求下载API: {download_api_url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
download_api_url,
|
||||||
|
headers=headers,
|
||||||
|
allow_redirects=False,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in (301, 302):
|
||||||
|
download_url = response.headers.get('Location')
|
||||||
|
if download_url:
|
||||||
|
print(f"解析成功!")
|
||||||
|
print(f"下载链接: {download_url}")
|
||||||
|
return download_url
|
||||||
|
else:
|
||||||
|
print("重定向但没有Location头")
|
||||||
|
elif response.status_code == 200:
|
||||||
|
# 尝试解析JSON响应
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
if data.get('code') == 0 and data.get('url'):
|
||||||
|
print(f"解析成功(JSON)!")
|
||||||
|
print(f"下载链接: {data['url']}")
|
||||||
|
return data['url']
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# 如果返回200且不是JSON, API地址本身可能就是下载地址
|
||||||
|
return download_api_url
|
||||||
|
else:
|
||||||
|
print(f"非预期状态码: {response.status_code}")
|
||||||
|
print(f"响应: {response.text[:500]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"请求失败: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("=" * 60)
|
||||||
|
print(" 飞书云盘分享链接解析工具")
|
||||||
|
print("=" * 60)
|
||||||
|
print("\n用法: python feishu-dl.py <飞书分享链接>")
|
||||||
|
print("\n示例:")
|
||||||
|
print(" python feishu-dl.py "
|
||||||
|
"https://xxx.feishu.cn/file/TOKEN")
|
||||||
|
print(" python feishu-dl.py "
|
||||||
|
"https://xxx.feishu.cn/file/TOKEN?from=from_copylink")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
url = sys.argv[1]
|
||||||
|
download_feishu_file(url)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -129,15 +129,130 @@ public class PanDomainTemplateTest {
|
|||||||
wsPattern.matcher("https://www.evil.com/f/abc123").matches());
|
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
|
@Test
|
||||||
public void verifyDuplicates() {
|
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 =
|
Set<String> collect =
|
||||||
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getRegex).collect(Collectors.toSet());
|
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getRegex).collect(Collectors.toSet());
|
||||||
|
|||||||
135
parser/src/test/java/cn/qaiu/parser/impl/FsToolTest.java
Normal file
135
parser/src/test/java/cn/qaiu/parser/impl/FsToolTest.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package cn.qaiu.parser.impl;
|
||||||
|
|
||||||
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
|
import cn.qaiu.parser.PanDomainTemplate;
|
||||||
|
import cn.qaiu.parser.ParserCreate;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 飞书云盘解析测试
|
||||||
|
*/
|
||||||
|
public class FsToolTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFsPatternMatchFile() {
|
||||||
|
Pattern fsPattern = PanDomainTemplate.FS.getPattern();
|
||||||
|
|
||||||
|
// 文件链接 (带 ?from=from_copylink)
|
||||||
|
Matcher m1 = fsPattern.matcher(
|
||||||
|
"https://kcncuknojm60.feishu.cn/file/VnCxbt35KoowKoxldO3c3C7VnMc?from=from_copylink");
|
||||||
|
assertTrue("FS pattern should match file URL with query params", m1.matches());
|
||||||
|
assertEquals("VnCxbt35KoowKoxldO3c3C7VnMc", m1.group("KEY"));
|
||||||
|
|
||||||
|
// 文件链接 (不带查询参数)
|
||||||
|
Matcher m2 = fsPattern.matcher(
|
||||||
|
"https://kcncuknojm60.feishu.cn/file/VnCxbt35KoowKoxldO3c3C7VnMc");
|
||||||
|
assertTrue("FS pattern should match file URL without query params", m2.matches());
|
||||||
|
assertEquals("VnCxbt35KoowKoxldO3c3C7VnMc", m2.group("KEY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFsPatternMatchFolder() {
|
||||||
|
Pattern fsPattern = PanDomainTemplate.FS.getPattern();
|
||||||
|
|
||||||
|
// 文件夹链接 (带 ?from=from_copylink)
|
||||||
|
Matcher m1 = fsPattern.matcher(
|
||||||
|
"https://kcncuknojm60.feishu.cn/drive/folder/RQSKf8EQ4l7dMedqzHucpMbancg?from=from_copylink");
|
||||||
|
assertTrue("FS pattern should match folder URL with query params", m1.matches());
|
||||||
|
assertEquals("RQSKf8EQ4l7dMedqzHucpMbancg", m1.group("KEY"));
|
||||||
|
|
||||||
|
// 文件夹链接 (不带查询参数)
|
||||||
|
Matcher m2 = fsPattern.matcher(
|
||||||
|
"https://kcncuknojm60.feishu.cn/drive/folder/RQSKf8EQ4l7dMedqzHucpMbancg");
|
||||||
|
assertTrue("FS pattern should match folder URL without query params", m2.matches());
|
||||||
|
assertEquals("RQSKf8EQ4l7dMedqzHucpMbancg", m2.group("KEY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFsPatternDifferentSubdomains() {
|
||||||
|
Pattern fsPattern = PanDomainTemplate.FS.getPattern();
|
||||||
|
|
||||||
|
// 不同的租户子域名
|
||||||
|
String[] subdomains = {"abc123", "kcncuknojm60", "tenant01", "xyz"};
|
||||||
|
for (String subdomain : subdomains) {
|
||||||
|
String url = "https://" + subdomain + ".feishu.cn/file/TestToken123";
|
||||||
|
Matcher m = fsPattern.matcher(url);
|
||||||
|
assertTrue("FS pattern should match subdomain: " + subdomain, m.matches());
|
||||||
|
assertEquals("TestToken123", m.group("KEY"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFsPatternNegativeCases() {
|
||||||
|
Pattern fsPattern = PanDomainTemplate.FS.getPattern();
|
||||||
|
|
||||||
|
// 不匹配: 非feishu.cn域名
|
||||||
|
assertFalse("Should not match non-feishu domain",
|
||||||
|
fsPattern.matcher("https://evil.com/file/TOKEN").matches());
|
||||||
|
|
||||||
|
// 不匹配: 其他路径
|
||||||
|
assertFalse("Should not match other paths",
|
||||||
|
fsPattern.matcher("https://abc.feishu.cn/docs/TOKEN").matches());
|
||||||
|
|
||||||
|
// 不匹配: 没有子域名的feishu.cn
|
||||||
|
assertFalse("Should not match feishu.cn without subdomain",
|
||||||
|
fsPattern.matcher("https://feishu.cn/file/TOKEN").matches());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFromShareUrlFile() {
|
||||||
|
String fileUrl = "https://kcncuknojm60.feishu.cn/file/VnCxbt35KoowKoxldO3c3C7VnMc?from=from_copylink";
|
||||||
|
|
||||||
|
ParserCreate parserCreate = ParserCreate.fromShareUrl(fileUrl);
|
||||||
|
ShareLinkInfo info = parserCreate.getShareLinkInfo();
|
||||||
|
|
||||||
|
assertNotNull("ShareLinkInfo should not be null", info);
|
||||||
|
assertEquals("Type should be fs", "fs", info.getType());
|
||||||
|
assertEquals("Pan name should be 飞书云盘", "飞书云盘", info.getPanName());
|
||||||
|
assertEquals("Share key should be the token",
|
||||||
|
"VnCxbt35KoowKoxldO3c3C7VnMc", info.getShareKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFromShareUrlFolder() {
|
||||||
|
String folderUrl = "https://kcncuknojm60.feishu.cn/drive/folder/RQSKf8EQ4l7dMedqzHucpMbancg?from=from_copylink";
|
||||||
|
|
||||||
|
ParserCreate parserCreate = ParserCreate.fromShareUrl(folderUrl);
|
||||||
|
ShareLinkInfo info = parserCreate.getShareLinkInfo();
|
||||||
|
|
||||||
|
assertNotNull("ShareLinkInfo should not be null", info);
|
||||||
|
assertEquals("Type should be fs", "fs", info.getType());
|
||||||
|
assertEquals("Pan name should be 飞书云盘", "飞书云盘", info.getPanName());
|
||||||
|
assertEquals("Share key should be the token",
|
||||||
|
"RQSKf8EQ4l7dMedqzHucpMbancg", info.getShareKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFromType() {
|
||||||
|
ParserCreate parserCreate = ParserCreate.fromType("fs")
|
||||||
|
.shareKey("VnCxbt35KoowKoxldO3c3C7VnMc");
|
||||||
|
|
||||||
|
ShareLinkInfo info = parserCreate.getShareLinkInfo();
|
||||||
|
|
||||||
|
assertNotNull("ShareLinkInfo should not be null", info);
|
||||||
|
assertEquals("Type should be fs", "fs", info.getType());
|
||||||
|
assertEquals("Pan name should be 飞书云盘", "飞书云盘", info.getPanName());
|
||||||
|
assertEquals("Share key should match", "VnCxbt35KoowKoxldO3c3C7VnMc", info.getShareKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFromShareUrlFileWithoutQueryParams() {
|
||||||
|
String fileUrl = "https://kcncuknojm60.feishu.cn/file/VnCxbt35KoowKoxldO3c3C7VnMc";
|
||||||
|
|
||||||
|
ParserCreate parserCreate = ParserCreate.fromShareUrl(fileUrl);
|
||||||
|
ShareLinkInfo info = parserCreate.getShareLinkInfo();
|
||||||
|
|
||||||
|
assertNotNull("ShareLinkInfo should not be null", info);
|
||||||
|
assertEquals("fs", info.getType());
|
||||||
|
assertEquals("VnCxbt35KoowKoxldO3c3C7VnMc", info.getShareKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user