mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-15 11:53:02 +00:00
feat: 新增客户端协议生成系统,支持8种主流下载工具
🚀 核心功能 - 新增完整的客户端下载链接生成器系统 - 支持ARIA2、Motrix、比特彗星、迅雷、wget、cURL、IDM、FDM、PowerShell等8种客户端 - 自动处理防盗链参数(User-Agent、Referer、Cookie等) - 提供可扩展的生成器架构,支持自定义客户端 🔧 技术实现 - ClientLinkGeneratorFactory: 工厂模式管理生成器 - DownloadLinkMeta: 元数据存储下载信息 - ClientLinkUtils: 便捷工具类 - 线程安全的ConcurrentHashMap设计 🌐 前端集成 - 新增ClientLinks.vue界面,支持客户端链接展示 - Element Plus图标系统,混合图标显示 - 客户端检测逻辑优化,避免自动打开外部应用 - 移动端和PC端环境判断 📚 文档完善 - 完整的CLIENT_LINK_GENERATOR_GUIDE.md使用指南 - API文档和测试用例 - 输出示例和最佳实践 从单纯的网盘解析工具升级为完整的下载解决方案生态
This commit is contained in:
13
.gitattributes
vendored
13
.gitattributes
vendored
@@ -1,3 +1,16 @@
|
||||
# GitHub 语言检测配置
|
||||
# 设置主要语言为 Java
|
||||
*.java linguist-language=Java
|
||||
*.vue linguist-language=Vue
|
||||
*.js linguist-language=JavaScript
|
||||
|
||||
# 排除不需要统计的文件
|
||||
target/ linguist-vendored=true
|
||||
node_modules/ linguist-vendored=true
|
||||
webroot/ linguist-vendored=true
|
||||
logs/ linguist-vendored=true
|
||||
db/ linguist-vendored=true
|
||||
|
||||
# 文本文件使用 LF 换行符,适用于 Linux 和 macOS
|
||||
*.sh text eol=lf
|
||||
*.service text eol=lf
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -41,5 +41,5 @@ jobs:
|
||||
# - name: 运行测试
|
||||
# run: ./mvnw test
|
||||
|
||||
# - name: 打包项目
|
||||
# run: ./mvnw package -DskipTests
|
||||
- name: 打包项目
|
||||
run: ./mvnw package -DskipTests
|
||||
|
||||
2
.github/workflows/maven.yml
vendored
2
.github/workflows/maven.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
run: cd web-front && yarn install && yarn run build
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn -B package --file pom.xml
|
||||
run: mvn -B package -DskipTests --file pom.xml
|
||||
|
||||
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
|
||||
- name: Update dependency graph
|
||||
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -41,3 +41,40 @@ unused.txt
|
||||
/db
|
||||
/webroot/nfd-front/
|
||||
package-lock.json
|
||||
|
||||
# Maven generated files
|
||||
.flattened-pom.xml
|
||||
**/.flattened-pom.xml
|
||||
|
||||
# Test files
|
||||
test-filelist.java
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Node.js (if any)
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build artifacts
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# IDE specific
|
||||
.vscode/
|
||||
.cursor/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
@@ -278,7 +278,7 @@ GET http://127.0.0.1:6400/json/fc/e5079007dc31226096628870c7@QAIU
|
||||
```shell
|
||||
# 环境要求: Jdk17 + maven;
|
||||
mvn clean
|
||||
mvn package
|
||||
mvn package -DskipTests
|
||||
|
||||
```
|
||||
打包好的文件位于 web-service/target/netdisk-fast-download-bin.zip
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package cn.qaiu.vx.core.verticle.conf;
|
||||
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.impl.JsonUtil;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Converter and mapper for {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf}.
|
||||
* NOTE: This class has been automatically generated from the {@link cn.qaiu.vx.core.verticle.conf.HttpProxyConf} original class using Vert.x codegen.
|
||||
*/
|
||||
public class HttpProxyConfConverter {
|
||||
|
||||
|
||||
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
|
||||
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
|
||||
|
||||
static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, HttpProxyConf obj) {
|
||||
for (java.util.Map.Entry<String, Object> member : json) {
|
||||
switch (member.getKey()) {
|
||||
case "password":
|
||||
if (member.getValue() instanceof String) {
|
||||
obj.setPassword((String)member.getValue());
|
||||
}
|
||||
break;
|
||||
case "port":
|
||||
if (member.getValue() instanceof Number) {
|
||||
obj.setPort(((Number)member.getValue()).intValue());
|
||||
}
|
||||
break;
|
||||
case "preProxyOptions":
|
||||
if (member.getValue() instanceof JsonObject) {
|
||||
obj.setPreProxyOptions(new io.vertx.core.net.ProxyOptions((io.vertx.core.json.JsonObject)member.getValue()));
|
||||
}
|
||||
break;
|
||||
case "timeout":
|
||||
if (member.getValue() instanceof Number) {
|
||||
obj.setTimeout(((Number)member.getValue()).intValue());
|
||||
}
|
||||
break;
|
||||
case "username":
|
||||
if (member.getValue() instanceof String) {
|
||||
obj.setUsername((String)member.getValue());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void toJson(HttpProxyConf obj, JsonObject json) {
|
||||
toJson(obj, json.getMap());
|
||||
}
|
||||
|
||||
static void toJson(HttpProxyConf obj, java.util.Map<String, Object> json) {
|
||||
if (obj.getPassword() != null) {
|
||||
json.put("password", obj.getPassword());
|
||||
}
|
||||
if (obj.getPort() != null) {
|
||||
json.put("port", obj.getPort());
|
||||
}
|
||||
if (obj.getPreProxyOptions() != null) {
|
||||
json.put("preProxyOptions", obj.getPreProxyOptions().toJson());
|
||||
}
|
||||
if (obj.getTimeout() != null) {
|
||||
json.put("timeout", obj.getTimeout());
|
||||
}
|
||||
if (obj.getUsername() != null) {
|
||||
json.put("username", obj.getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>cn.qaiu</groupId>
|
||||
<artifactId>parser</artifactId>
|
||||
<version>10.2.1</version>
|
||||
<name>cn.qaiu:parser</name>
|
||||
<description>NFD parser module</description>
|
||||
<url>https://qaiu.top</url>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://opensource.org/license/mit</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>qaiu</name>
|
||||
<email>qaiu00@gmail.com</email>
|
||||
<organization>https://qaiu.top</organization>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:https://github.com/qaiu/netdisk-fast-download.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com:qaiu/netdisk-fast-download.git</developerConnection>
|
||||
<url>https://github.com/qaiu/netdisk-fast-download</url>
|
||||
</scm>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.5.19</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.5</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web-client</artifactId>
|
||||
<version>4.5.21</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.18.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.nashorn</groupId>
|
||||
<artifactId>nashorn-core</artifactId>
|
||||
<version>15.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.brotli</groupId>
|
||||
<artifactId>dec</artifactId>
|
||||
<version>0.1.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -48,7 +48,7 @@ List<FileInfo> list = ParserCreate
|
||||
- 环境:JDK >= 17,Maven >= 3.9
|
||||
- 构建/安装:
|
||||
```
|
||||
mvn -pl parser -am clean package
|
||||
mvn -pl parser -am clean package -DskipTests
|
||||
mvn -pl parser -am install
|
||||
```
|
||||
- 测试:
|
||||
|
||||
316
parser/doc/CLIENT_LINK_GENERATOR_GUIDE.md
Normal file
316
parser/doc/CLIENT_LINK_GENERATOR_GUIDE.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# 客户端下载链接生成器使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
客户端下载链接生成器是 parser 模块的新功能,用于将解析得到的直链转换为各种下载客户端可识别的格式,包括 curl、wget、aria2、IDM、迅雷、比特彗星、Motrix、FDM 等主流下载工具。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **多客户端支持**:支持 8 种主流下载客户端格式
|
||||
- **防盗链处理**:自动处理请求头、Referer 等防盗链参数
|
||||
- **可扩展设计**:支持注册自定义生成器
|
||||
- **元数据存储**:通过 `ShareLinkInfo.otherParam` 存储下载元数据
|
||||
- **线程安全**:工厂类使用 ConcurrentHashMap 保证线程安全
|
||||
|
||||
## 支持的客户端类型
|
||||
|
||||
| 客户端类型 | 代码 | 说明 | 输出格式 |
|
||||
|-----------|------|------|----------|
|
||||
| Aria2 | `ARIA2` | 命令行/RPC | aria2c 命令 |
|
||||
| Motrix | `MOTRIX` | 跨平台下载工具 | JSON 格式 |
|
||||
| 比特彗星 | `BITCOMET` | BT 下载工具 | bitcomet:// 协议链接 |
|
||||
| 迅雷 | `THUNDER` | 国内主流下载工具 | thunder:// 协议链接 |
|
||||
| wget | `WGET` | 命令行工具 | wget 命令 |
|
||||
| cURL | `CURL` | 命令行工具 | curl 命令 |
|
||||
| IDM | `IDM` | Windows 下载管理器 | idm:// 协议链接 |
|
||||
| FDM | `FDM` | Free Download Manager | 文本格式 |
|
||||
| PowerShell | `POWERSHELL` | Windows PowerShell | PowerShell 命令 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 基本使用
|
||||
|
||||
```java
|
||||
// 解析分享链接
|
||||
IPanTool tool = ParserCreate.fromShareUrl("https://example.com/share/abc123")
|
||||
.createTool();
|
||||
String directLink = tool.parseSync();
|
||||
|
||||
// 获取 ShareLinkInfo
|
||||
ShareLinkInfo info = tool.getShareLinkInfo();
|
||||
|
||||
// 生成所有类型的客户端链接
|
||||
Map<ClientLinkType, String> clientLinks = ClientLinkGeneratorFactory.generateAll(info);
|
||||
|
||||
// 使用生成的链接
|
||||
String curlCommand = clientLinks.get(ClientLinkType.CURL);
|
||||
String thunderLink = clientLinks.get(ClientLinkType.THUNDER);
|
||||
```
|
||||
|
||||
### 2. 使用新的便捷方法(推荐)
|
||||
|
||||
```java
|
||||
// 解析分享链接并自动生成客户端链接
|
||||
IPanTool tool = ParserCreate.fromShareUrl("https://example.com/share/abc123")
|
||||
.createTool();
|
||||
|
||||
// 一步完成解析和客户端链接生成
|
||||
Map<ClientLinkType, String> clientLinks = tool.parseWithClientLinksSync();
|
||||
|
||||
// 使用生成的链接
|
||||
String curlCommand = clientLinks.get(ClientLinkType.CURL);
|
||||
String thunderLink = clientLinks.get(ClientLinkType.THUNDER);
|
||||
```
|
||||
|
||||
### 3. 异步方式
|
||||
|
||||
```java
|
||||
// 异步解析并生成客户端链接
|
||||
tool.parseWithClientLinks()
|
||||
.onSuccess(clientLinks -> {
|
||||
log.info("生成的客户端链接: {}", clientLinks);
|
||||
})
|
||||
.onFailure(error -> {
|
||||
log.error("解析失败", error);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 生成特定类型的链接
|
||||
|
||||
```java
|
||||
// 生成 curl 命令
|
||||
String curlCommand = ClientLinkGeneratorFactory.generate(info, ClientLinkType.CURL);
|
||||
|
||||
// 生成迅雷链接
|
||||
String thunderLink = ClientLinkGeneratorFactory.generate(info, ClientLinkType.THUNDER);
|
||||
|
||||
// 生成 aria2 命令
|
||||
String aria2Command = ClientLinkGeneratorFactory.generate(info, ClientLinkType.ARIA2);
|
||||
```
|
||||
|
||||
### 5. 使用便捷工具类
|
||||
|
||||
```java
|
||||
// 使用 ClientLinkUtils 工具类
|
||||
String curlCommand = ClientLinkUtils.generateCurlCommand(info);
|
||||
String wgetCommand = ClientLinkUtils.generateWgetCommand(info);
|
||||
String thunderLink = ClientLinkUtils.generateThunderLink(info);
|
||||
String powershellCommand = ClientLinkUtils.generatePowerShellCommand(info);
|
||||
|
||||
// 检查是否有有效的下载元数据
|
||||
boolean hasValidMeta = ClientLinkUtils.hasValidDownloadMeta(info);
|
||||
```
|
||||
|
||||
## 输出示例
|
||||
|
||||
### PowerShell 命令示例
|
||||
|
||||
```powershell
|
||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
||||
$session.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
Invoke-WebRequest `
|
||||
-UseBasicParsing `
|
||||
-Uri "https://example.com/file.zip" `
|
||||
-WebSession $session `
|
||||
-Headers @{`
|
||||
"Cookie"="session=abc123"`
|
||||
`
|
||||
"Accept"="text/html,application/xhtml+xml"`
|
||||
`
|
||||
"User-Agent"="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"`
|
||||
`
|
||||
"Referer"="https://example.com/share/test"`
|
||||
} `
|
||||
-OutFile "test-file.zip"
|
||||
```
|
||||
|
||||
### cURL 命令示例
|
||||
|
||||
```bash
|
||||
curl \
|
||||
-L \
|
||||
-H \
|
||||
"Cookie: session=abc123" \
|
||||
-H \
|
||||
"User-Agent: Mozilla/5.0 (Test Browser)" \
|
||||
-H \
|
||||
"Referer: https://example.com/share/test" \
|
||||
-o \
|
||||
"test-file.zip" \
|
||||
"https://example.com/file.zip"
|
||||
```
|
||||
|
||||
### 迅雷链接示例
|
||||
|
||||
```
|
||||
thunder://QUFodHRwczovL2V4YW1wbGUuY29tL2ZpbGUuemlwWlo=
|
||||
```
|
||||
|
||||
### Aria2 命令示例
|
||||
|
||||
```bash
|
||||
aria2c \
|
||||
--header="Cookie: session=abc123" \
|
||||
--header="User-Agent: Mozilla/5.0 (Test Browser)" \
|
||||
--header="Referer: https://example.com/share/test" \
|
||||
--out="test-file.zip" \
|
||||
--continue \
|
||||
--max-tries=3 \
|
||||
--retry-wait=5 \
|
||||
"https://example.com/file.zip"
|
||||
```
|
||||
|
||||
## 解析器集成
|
||||
|
||||
### 1. 使用 completeWithMeta 方法
|
||||
|
||||
在解析器实现中,使用 `PanBase` 提供的 `completeWithMeta` 方法来存储下载元数据:
|
||||
|
||||
```java
|
||||
public class MyPanTool extends PanBase {
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
// ... 解析逻辑 ...
|
||||
|
||||
// 获取下载链接
|
||||
String downloadUrl = "https://example.com/file.zip";
|
||||
|
||||
// 准备请求头
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||
headers.put("Referer", shareLinkInfo.getShareUrl());
|
||||
headers.put("Cookie", "session=abc123");
|
||||
|
||||
// 使用 completeWithMeta 存储元数据
|
||||
completeWithMeta(downloadUrl, headers);
|
||||
|
||||
return future();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用 MultiMap 版本
|
||||
|
||||
如果使用 Vert.x 的 MultiMap:
|
||||
|
||||
```java
|
||||
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||
headers.set("Referer", shareLinkInfo.getShareUrl());
|
||||
|
||||
// 使用 MultiMap 版本
|
||||
completeWithMeta(downloadUrl, headers);
|
||||
```
|
||||
|
||||
## 输出示例
|
||||
|
||||
### curl 命令
|
||||
```bash
|
||||
curl -L "https://example.com/file.zip" \
|
||||
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
|
||||
-H "Referer: https://example.com/share/abc123" \
|
||||
-H "Cookie: session=abc123" \
|
||||
-o "file.zip"
|
||||
```
|
||||
|
||||
### wget 命令
|
||||
```bash
|
||||
wget --header="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
|
||||
--header="Referer: https://example.com/share/abc123" \
|
||||
--header="Cookie: session=abc123" \
|
||||
-O "file.zip" \
|
||||
"https://example.com/file.zip"
|
||||
```
|
||||
|
||||
### aria2 命令
|
||||
```bash
|
||||
aria2c --header="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
|
||||
--header="Referer: https://example.com/share/abc123" \
|
||||
--header="Cookie: session=abc123" \
|
||||
--out="file.zip" \
|
||||
--continue \
|
||||
--max-tries=3 \
|
||||
--retry-wait=5 \
|
||||
"https://example.com/file.zip"
|
||||
```
|
||||
|
||||
### 迅雷链接
|
||||
```
|
||||
thunder://QUFodHRwczovL2V4YW1wbGUuY29tL2ZpbGUuemlwWlo=
|
||||
```
|
||||
|
||||
### IDM 链接
|
||||
```
|
||||
idm:///?url=aHR0cHM6Ly9leGFtcGxlLmNvbS9maWxlLnppcA==&header=UmVmZXJlcjogaHR0cHM6Ly9leGFtcGxlLmNvbS9zaGFyZS9hYmMxMjMK
|
||||
```
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 1. 自定义生成器
|
||||
|
||||
实现 `ClientLinkGenerator` 接口:
|
||||
|
||||
```java
|
||||
public class MyCustomGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
// 自定义生成逻辑
|
||||
return "myapp://download?url=" + meta.getUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.CURL; // 或者定义新的类型
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注册自定义生成器
|
||||
|
||||
```java
|
||||
// 注册自定义生成器
|
||||
ClientLinkGeneratorFactory.register(new MyCustomGenerator());
|
||||
|
||||
// 使用自定义生成器
|
||||
String customLink = ClientLinkGeneratorFactory.generate(info, ClientLinkType.CURL);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **防盗链处理**:不同网盘的防盗链策略不同,需要在元数据中完整保存所需的 headers
|
||||
2. **URL 编码**:生成客户端链接时注意 URL 和参数的正确编码(Base64、URLEncode 等)
|
||||
3. **兼容性**:确保生成的命令/协议在主流客户端中可用
|
||||
4. **可选特性**:元数据存储和客户端链接生成均为可选,不影响现有解析器功能
|
||||
5. **线程安全**:工厂类使用 ConcurrentHashMap 存储生成器,支持多线程环境
|
||||
|
||||
## API 参考
|
||||
|
||||
### IPanTool 接口新增方法
|
||||
|
||||
- `parseWithClientLinks()` - 解析文件并生成客户端下载链接(异步)
|
||||
- `parseWithClientLinksSync()` - 解析文件并生成客户端下载链接(同步)
|
||||
- `getShareLinkInfo()` - 获取 ShareLinkInfo 对象
|
||||
|
||||
### ClientLinkGeneratorFactory
|
||||
|
||||
- `generateAll(ShareLinkInfo info)` - 生成所有类型的客户端链接
|
||||
- `generate(ShareLinkInfo info, ClientLinkType type)` - 生成指定类型的链接
|
||||
- `register(ClientLinkGenerator generator)` - 注册自定义生成器
|
||||
- `unregister(ClientLinkType type)` - 注销生成器
|
||||
- `isRegistered(ClientLinkType type)` - 检查是否已注册
|
||||
|
||||
### ClientLinkUtils
|
||||
|
||||
- `generateAllClientLinks(ShareLinkInfo info)` - 生成所有客户端链接
|
||||
- `generateCurlCommand(ShareLinkInfo info)` - 生成 curl 命令
|
||||
- `generateWgetCommand(ShareLinkInfo info)` - 生成 wget 命令
|
||||
- `generateThunderLink(ShareLinkInfo info)` - 生成迅雷链接
|
||||
- `generatePowerShellCommand(ShareLinkInfo info)` - 生成 PowerShell 命令
|
||||
- `hasValidDownloadMeta(ShareLinkInfo info)` - 检查元数据有效性
|
||||
|
||||
### PanBase
|
||||
|
||||
- `completeWithMeta(String url, Map<String, String> headers)` - 完成解析并存储元数据
|
||||
- `completeWithMeta(String url, MultiMap headers)` - 完成解析并存储元数据(MultiMap版本)
|
||||
@@ -1,10 +1,14 @@
|
||||
package cn.qaiu.parser;//package cn.qaiu.lz.common.parser;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IPanTool {
|
||||
|
||||
@@ -45,4 +49,92 @@ public interface IPanTool {
|
||||
default String parseByIdSync() {
|
||||
return parseById().toCompletionStage().toCompletableFuture().join();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件并生成客户端下载链接
|
||||
* @return Future<Map<ClientLinkType, String>> 客户端下载链接集合
|
||||
*/
|
||||
default Future<Map<ClientLinkType, String>> parseWithClientLinks() {
|
||||
Promise<Map<ClientLinkType, String>> promise = Promise.promise();
|
||||
|
||||
// 首先尝试获取 ShareLinkInfo
|
||||
ShareLinkInfo shareLinkInfo = getShareLinkInfo();
|
||||
if (shareLinkInfo == null) {
|
||||
promise.fail("无法获取 ShareLinkInfo");
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
// 检查是否已经有下载链接元数据
|
||||
String existingDownloadUrl = (String) shareLinkInfo.getOtherParam().get("downloadUrl");
|
||||
if (existingDownloadUrl != null && !existingDownloadUrl.trim().isEmpty()) {
|
||||
// 如果已经有下载链接,直接生成客户端链接
|
||||
try {
|
||||
Map<ClientLinkType, String> clientLinks =
|
||||
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
promise.complete(clientLinks);
|
||||
return promise.future();
|
||||
} catch (Exception e) {
|
||||
// 如果生成失败,继续尝试解析
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试解析获取下载链接
|
||||
parse().onComplete(result -> {
|
||||
if (result.succeeded()) {
|
||||
try {
|
||||
String downloadUrl = result.result();
|
||||
if (downloadUrl != null && !downloadUrl.trim().isEmpty()) {
|
||||
// 确保下载链接已存储到 otherParam 中
|
||||
shareLinkInfo.getOtherParam().put("downloadUrl", downloadUrl);
|
||||
|
||||
// 生成客户端链接
|
||||
Map<ClientLinkType, String> clientLinks =
|
||||
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
promise.complete(clientLinks);
|
||||
} else {
|
||||
promise.fail("解析结果为空,无法生成客户端链接");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.fail("生成客户端链接失败: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 解析失败时,尝试使用分享链接作为默认下载链接
|
||||
try {
|
||||
String fallbackUrl = shareLinkInfo.getShareUrl();
|
||||
if (fallbackUrl != null && !fallbackUrl.trim().isEmpty()) {
|
||||
// 使用分享链接作为默认下载链接
|
||||
shareLinkInfo.getOtherParam().put("downloadUrl", fallbackUrl);
|
||||
|
||||
// 尝试生成客户端链接
|
||||
Map<ClientLinkType, String> clientLinks =
|
||||
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
promise.complete(clientLinks);
|
||||
} else {
|
||||
promise.fail("解析失败且无法使用分享链接作为默认下载链接: " + result.cause().getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.fail("解析失败且生成默认客户端链接失败: " + result.cause().getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件并生成客户端下载链接(同步版本)
|
||||
* @return Map<ClientLinkType, String> 客户端下载链接集合
|
||||
*/
|
||||
default Map<ClientLinkType, String> parseWithClientLinksSync() {
|
||||
return parseWithClientLinks().toCompletionStage().toCompletableFuture().join();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ShareLinkInfo 对象
|
||||
* 子类需要实现此方法来提供 ShareLinkInfo
|
||||
* @return ShareLinkInfo 对象
|
||||
*/
|
||||
default ShareLinkInfo getShareLinkInfo() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.util.HttpResponseHelper;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
@@ -21,7 +22,9 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
@@ -225,9 +228,39 @@ public abstract class PanBase implements IPanTool {
|
||||
}
|
||||
|
||||
protected void complete(String url) {
|
||||
// 自动将直链存储到 otherParam 中,以便客户端链接生成器使用
|
||||
shareLinkInfo.getOtherParam().put("downloadUrl", url);
|
||||
promise.complete(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成解析并存储下载元数据
|
||||
*
|
||||
* @param url 下载直链
|
||||
* @param headers 请求头Map
|
||||
*/
|
||||
protected void completeWithMeta(String url, Map<String, String> headers) {
|
||||
shareLinkInfo.getOtherParam().put("downloadUrl", url);
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
shareLinkInfo.getOtherParam().put("downloadHeaders", headers);
|
||||
}
|
||||
promise.complete(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成解析并存储下载元数据(MultiMap版本)
|
||||
*
|
||||
* @param url 下载直链
|
||||
* @param headers MultiMap格式的请求头
|
||||
*/
|
||||
protected void completeWithMeta(String url, MultiMap headers) {
|
||||
Map<String, String> headerMap = new HashMap<>();
|
||||
if (headers != null) {
|
||||
headers.forEach(entry -> headerMap.put(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
completeWithMeta(url, headerMap);
|
||||
}
|
||||
|
||||
protected Future<String> future() {
|
||||
return promise.future();
|
||||
}
|
||||
@@ -279,4 +312,9 @@ public abstract class PanBase implements IPanTool {
|
||||
protected String getDomainName(){
|
||||
return shareLinkInfo.getOtherParam().getOrDefault("domainName", "").toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShareLinkInfo getShareLinkInfo() {
|
||||
return shareLinkInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public class ParserCreate {
|
||||
throw new IllegalArgumentException("ShareLinkInfo shareUrl is empty");
|
||||
}
|
||||
|
||||
java.util.regex.Matcher matcher = customParserConfig.getMatchPattern().matcher(shareUrl);
|
||||
Matcher matcher = customParserConfig.getMatchPattern().matcher(shareUrl);
|
||||
if (matcher.matches()) {
|
||||
// 提取分享键
|
||||
try {
|
||||
@@ -252,7 +252,7 @@ public class ParserCreate {
|
||||
// 优先查找支持正则匹配的自定义解析器
|
||||
for (CustomParserConfig customConfig : CustomParserRegistry.getAll().values()) {
|
||||
if (customConfig.supportsFromShareUrl()) {
|
||||
java.util.regex.Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl);
|
||||
Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl);
|
||||
if (matcher.matches()) {
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type(customConfig.getType())
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
/**
|
||||
* 客户端下载链接生成器接口
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public interface ClientLinkGenerator {
|
||||
|
||||
/**
|
||||
* 生成客户端下载链接
|
||||
*
|
||||
* @param meta 下载链接元数据
|
||||
* @return 生成的客户端下载链接字符串
|
||||
*/
|
||||
String generate(DownloadLinkMeta meta);
|
||||
|
||||
/**
|
||||
* 获取生成器对应的客户端类型
|
||||
*
|
||||
* @return ClientLinkType 枚举值
|
||||
*/
|
||||
ClientLinkType getType();
|
||||
|
||||
/**
|
||||
* 检查是否支持生成该类型的链接
|
||||
* 默认实现:检查元数据是否有有效的URL
|
||||
*
|
||||
* @param meta 下载链接元数据
|
||||
* @return true 表示支持,false 表示不支持
|
||||
*/
|
||||
default boolean supports(DownloadLinkMeta meta) {
|
||||
return meta != null && meta.hasValidUrl();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.clientlink.impl.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 客户端下载链接生成器工厂类
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class ClientLinkGeneratorFactory {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ClientLinkGeneratorFactory.class);
|
||||
|
||||
// 存储所有注册的生成器
|
||||
private static final Map<ClientLinkType, ClientLinkGenerator> generators = new ConcurrentHashMap<>();
|
||||
|
||||
// 静态初始化块,注册默认的生成器
|
||||
static {
|
||||
try {
|
||||
// 注册默认生成器 - 按指定顺序注册
|
||||
register(new Aria2LinkGenerator());
|
||||
register(new MotrixLinkGenerator());
|
||||
register(new BitCometLinkGenerator());
|
||||
register(new ThunderLinkGenerator());
|
||||
register(new WgetLinkGenerator());
|
||||
register(new CurlLinkGenerator());
|
||||
register(new IdmLinkGenerator());
|
||||
register(new FdmLinkGenerator());
|
||||
register(new PowerShellLinkGenerator());
|
||||
|
||||
log.info("客户端链接生成器工厂初始化完成,已注册 {} 个生成器", generators.size());
|
||||
} catch (Exception e) {
|
||||
log.error("初始化客户端链接生成器失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成所有类型的客户端链接
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return Map<ClientLinkType, String> 格式的客户端链接集合
|
||||
*/
|
||||
public static Map<ClientLinkType, String> generateAll(ShareLinkInfo info) {
|
||||
Map<ClientLinkType, String> result = new LinkedHashMap<>();
|
||||
|
||||
if (info == null) {
|
||||
log.warn("ShareLinkInfo 为空,无法生成客户端链接");
|
||||
return result;
|
||||
}
|
||||
|
||||
DownloadLinkMeta meta = DownloadLinkMeta.fromShareLinkInfo(info);
|
||||
if (!meta.hasValidUrl()) {
|
||||
log.warn("下载链接元数据无效,无法生成客户端链接: {}", meta);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 按照枚举顺序遍历,保证顺序
|
||||
for (ClientLinkType type : ClientLinkType.values()) {
|
||||
ClientLinkGenerator generator = generators.get(type);
|
||||
if (generator != null) {
|
||||
try {
|
||||
if (generator.supports(meta)) {
|
||||
String link = generator.generate(meta);
|
||||
if (link != null && !link.trim().isEmpty()) {
|
||||
result.put(type, link);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("生成 {} 客户端链接失败: {}", type.getDisplayName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("成功生成 {} 个客户端链接", result.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定类型的客户端链接
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @param type 客户端类型
|
||||
* @return 生成的客户端链接字符串,失败时返回 null
|
||||
*/
|
||||
public static String generate(ShareLinkInfo info, ClientLinkType type) {
|
||||
if (info == null || type == null) {
|
||||
log.warn("参数为空,无法生成客户端链接: info={}, type={}", info, type);
|
||||
return null;
|
||||
}
|
||||
|
||||
ClientLinkGenerator generator = generators.get(type);
|
||||
if (generator == null) {
|
||||
log.warn("未找到类型为 {} 的生成器", type.getDisplayName());
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
DownloadLinkMeta meta = DownloadLinkMeta.fromShareLinkInfo(info);
|
||||
if (!generator.supports(meta)) {
|
||||
log.warn("生成器 {} 不支持该元数据", type.getDisplayName());
|
||||
return null;
|
||||
}
|
||||
|
||||
return generator.generate(meta);
|
||||
} catch (Exception e) {
|
||||
log.error("生成 {} 客户端链接失败", type.getDisplayName(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册自定义生成器(扩展点)
|
||||
*
|
||||
* @param generator 客户端链接生成器
|
||||
*/
|
||||
public static void register(ClientLinkGenerator generator) {
|
||||
if (generator == null) {
|
||||
log.warn("尝试注册空的生成器");
|
||||
return;
|
||||
}
|
||||
|
||||
ClientLinkType type = generator.getType();
|
||||
if (type == null) {
|
||||
log.warn("生成器的类型为空,无法注册");
|
||||
return;
|
||||
}
|
||||
|
||||
generators.put(type, generator);
|
||||
log.info("成功注册客户端链接生成器: {}", type.getDisplayName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销生成器
|
||||
*
|
||||
* @param type 客户端类型
|
||||
* @return 被注销的生成器,如果不存在则返回 null
|
||||
*/
|
||||
public static ClientLinkGenerator unregister(ClientLinkType type) {
|
||||
ClientLinkGenerator removed = generators.remove(type);
|
||||
if (removed != null) {
|
||||
log.info("成功注销客户端链接生成器: {}", type.getDisplayName());
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的生成器类型
|
||||
*
|
||||
* @return 已注册的客户端类型集合
|
||||
*/
|
||||
public static Map<ClientLinkType, ClientLinkGenerator> getAllGenerators() {
|
||||
Map<ClientLinkType, ClientLinkGenerator> result = new LinkedHashMap<>();
|
||||
// 按照枚举顺序添加,保证顺序
|
||||
for (ClientLinkType type : ClientLinkType.values()) {
|
||||
ClientLinkGenerator generator = generators.get(type);
|
||||
if (generator != null) {
|
||||
result.put(type, generator);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已注册指定类型的生成器
|
||||
*
|
||||
* @param type 客户端类型
|
||||
* @return true 表示已注册,false 表示未注册
|
||||
*/
|
||||
public static boolean isRegistered(ClientLinkType type) {
|
||||
return generators.containsKey(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
/**
|
||||
* 客户端下载工具类型枚举
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public enum ClientLinkType {
|
||||
ARIA2("aria2", "Aria2"),
|
||||
MOTRIX("motrix", "Motrix"),
|
||||
BITCOMET("bitcomet", "比特彗星"),
|
||||
THUNDER("thunder", "迅雷"),
|
||||
WGET("wget", "wget 命令"),
|
||||
CURL("curl", "cURL 命令"),
|
||||
IDM("idm", "IDM"),
|
||||
FDM("fdm", "Free Download Manager"),
|
||||
POWERSHELL("powershell", "PowerShell");
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
|
||||
ClientLinkType(String code, String displayName) {
|
||||
this.code = code;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 客户端下载链接生成工具类
|
||||
* 提供便捷的静态方法来生成各种客户端下载链接
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class ClientLinkUtils {
|
||||
|
||||
/**
|
||||
* 为 ShareLinkInfo 生成所有类型的客户端下载链接
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return Map<ClientLinkType, String> 格式的客户端链接集合
|
||||
*/
|
||||
public static Map<ClientLinkType, String> generateAllClientLinks(ShareLinkInfo info) {
|
||||
return ClientLinkGeneratorFactory.generateAll(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定类型的客户端下载链接
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @param type 客户端类型
|
||||
* @return 生成的客户端链接字符串
|
||||
*/
|
||||
public static String generateClientLink(ShareLinkInfo info, ClientLinkType type) {
|
||||
return ClientLinkGeneratorFactory.generate(info, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 curl 命令
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return curl 命令字符串
|
||||
*/
|
||||
public static String generateCurlCommand(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.CURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 wget 命令
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return wget 命令字符串
|
||||
*/
|
||||
public static String generateWgetCommand(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.WGET);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 aria2 命令
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return aria2 命令字符串
|
||||
*/
|
||||
public static String generateAria2Command(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.ARIA2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成迅雷链接
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return 迅雷协议链接
|
||||
*/
|
||||
public static String generateThunderLink(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.THUNDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 IDM 链接
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return IDM 协议链接
|
||||
*/
|
||||
public static String generateIdmLink(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.IDM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成比特彗星链接
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return 比特彗星协议链接
|
||||
*/
|
||||
public static String generateBitCometLink(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.BITCOMET);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Motrix 导入格式
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return Motrix JSON 格式字符串
|
||||
*/
|
||||
public static String generateMotrixFormat(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.MOTRIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 FDM 导入格式
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return FDM 格式字符串
|
||||
*/
|
||||
public static String generateFdmFormat(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.FDM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 PowerShell 命令
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return PowerShell 命令字符串
|
||||
*/
|
||||
public static String generatePowerShellCommand(ShareLinkInfo info) {
|
||||
return generateClientLink(info, ClientLinkType.POWERSHELL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 ShareLinkInfo 是否包含有效的下载元数据
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return true 表示包含有效元数据,false 表示不包含
|
||||
*/
|
||||
public static boolean hasValidDownloadMeta(ShareLinkInfo info) {
|
||||
if (info == null || info.getOtherParam() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object downloadUrl = info.getOtherParam().get("downloadUrl");
|
||||
return downloadUrl instanceof String && !((String) downloadUrl).trim().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 下载链接元数据封装类
|
||||
* 包含生成客户端下载链接所需的所有信息
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class DownloadLinkMeta {
|
||||
|
||||
private String url; // 直链
|
||||
private Map<String, String> headers; // 请求头
|
||||
private String referer; // Referer
|
||||
private String userAgent; // User-Agent
|
||||
private String fileName; // 文件名(可选)
|
||||
private Map<String, Object> extParams; // 扩展参数
|
||||
|
||||
public DownloadLinkMeta() {
|
||||
this.headers = new HashMap<>();
|
||||
this.extParams = new HashMap<>();
|
||||
}
|
||||
|
||||
public DownloadLinkMeta(String url) {
|
||||
this();
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ShareLinkInfo.otherParam 构建 DownloadLinkMeta
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return DownloadLinkMeta 实例
|
||||
*/
|
||||
public static DownloadLinkMeta fromShareLinkInfo(ShareLinkInfo info) {
|
||||
DownloadLinkMeta meta = new DownloadLinkMeta();
|
||||
|
||||
// 从 otherParam 中提取元数据
|
||||
Map<String, Object> otherParam = info.getOtherParam();
|
||||
|
||||
// 获取直链 - 优先从 downloadUrl 获取,如果没有则尝试从解析结果获取
|
||||
Object downloadUrl = otherParam.get("downloadUrl");
|
||||
if (downloadUrl instanceof String && StringUtils.isNotEmpty((String) downloadUrl)) {
|
||||
meta.setUrl((String) downloadUrl);
|
||||
} else {
|
||||
// 如果没有存储的 downloadUrl,尝试从解析结果中获取
|
||||
// 这里假设解析器会将直链存储在 otherParam 的某个字段中
|
||||
// 或者我们可以从 ShareLinkInfo 的其他字段中获取
|
||||
String directLink = extractDirectLinkFromInfo(info);
|
||||
if (StringUtils.isNotEmpty(directLink)) {
|
||||
meta.setUrl(directLink);
|
||||
} else {
|
||||
// 如果仍然没有找到直链,使用分享链接作为默认下载链接
|
||||
String shareUrl = info.getShareUrl();
|
||||
if (StringUtils.isNotEmpty(shareUrl)) {
|
||||
meta.setUrl(shareUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取请求头
|
||||
Object downloadHeaders = otherParam.get("downloadHeaders");
|
||||
if (downloadHeaders instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> headerMap = (Map<String, String>) downloadHeaders;
|
||||
meta.setHeaders(headerMap);
|
||||
}
|
||||
|
||||
// 获取 Referer
|
||||
Object downloadReferer = otherParam.get("downloadReferer");
|
||||
if (downloadReferer instanceof String) {
|
||||
meta.setReferer((String) downloadReferer);
|
||||
}
|
||||
|
||||
// 获取文件名(从 fileInfo 中提取)
|
||||
Object fileInfo = otherParam.get("fileInfo");
|
||||
if (fileInfo instanceof FileInfo) {
|
||||
FileInfo fi = (FileInfo) fileInfo;
|
||||
if (StringUtils.isNotEmpty(fi.getFileName())) {
|
||||
meta.setFileName(fi.getFileName());
|
||||
}
|
||||
}
|
||||
|
||||
// 从请求头中提取 User-Agent 和 Referer(如果单独存储的话)
|
||||
if (meta.getHeaders() != null) {
|
||||
String ua = meta.getHeaders().get("User-Agent");
|
||||
if (StringUtils.isNotEmpty(ua)) {
|
||||
meta.setUserAgent(ua);
|
||||
}
|
||||
|
||||
String ref = meta.getHeaders().get("Referer");
|
||||
if (StringUtils.isNotEmpty(ref) && StringUtils.isEmpty(meta.getReferer())) {
|
||||
meta.setReferer(ref);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 User-Agent,设置默认的 User-Agent
|
||||
if (StringUtils.isEmpty(meta.getUserAgent())) {
|
||||
meta.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 ShareLinkInfo 中提取直链
|
||||
* 尝试从各种可能的字段中获取直链
|
||||
*
|
||||
* @param info ShareLinkInfo 对象
|
||||
* @return 直链URL,如果找不到则返回 null
|
||||
*/
|
||||
private static String extractDirectLinkFromInfo(ShareLinkInfo info) {
|
||||
Map<String, Object> otherParam = info.getOtherParam();
|
||||
|
||||
// 尝试从各种可能的字段中获取直链
|
||||
String[] possibleKeys = {
|
||||
"directLink", "downloadUrl", "url", "link",
|
||||
"download_link", "direct_link", "fileUrl", "file_url"
|
||||
};
|
||||
|
||||
for (String key : possibleKeys) {
|
||||
Object value = otherParam.get(key);
|
||||
if (value instanceof String && StringUtils.isNotEmpty((String) value)) {
|
||||
return (String) value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Getter 和 Setter 方法
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public DownloadLinkMeta setUrl(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public DownloadLinkMeta setHeaders(Map<String, String> headers) {
|
||||
this.headers = headers != null ? headers : new HashMap<>();
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getReferer() {
|
||||
return referer;
|
||||
}
|
||||
|
||||
public DownloadLinkMeta setReferer(String referer) {
|
||||
this.referer = referer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public DownloadLinkMeta setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public DownloadLinkMeta setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getExtParams() {
|
||||
return extParams;
|
||||
}
|
||||
|
||||
public DownloadLinkMeta setExtParams(Map<String, Object> extParams) {
|
||||
this.extParams = extParams != null ? extParams : new HashMap<>();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加请求头
|
||||
*/
|
||||
public DownloadLinkMeta addHeader(String name, String value) {
|
||||
if (this.headers == null) {
|
||||
this.headers = new HashMap<>();
|
||||
}
|
||||
this.headers.put(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加扩展参数
|
||||
*/
|
||||
public DownloadLinkMeta addExtParam(String key, Object value) {
|
||||
if (this.extParams == null) {
|
||||
this.extParams = new HashMap<>();
|
||||
}
|
||||
this.extParams.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有有效的下载链接
|
||||
*/
|
||||
public boolean hasValidUrl() {
|
||||
return StringUtils.isNotEmpty(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DownloadLinkMeta{" +
|
||||
"url='" + url + '\'' +
|
||||
", fileName='" + fileName + '\'' +
|
||||
", headers=" + headers +
|
||||
", referer='" + referer + '\'' +
|
||||
", userAgent='" + userAgent + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Aria2 命令生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class Aria2LinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> parts = new ArrayList<>();
|
||||
parts.add("aria2c");
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
parts.add("--header=\"" + entry.getKey() + ": " + entry.getValue() + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
// 设置输出文件名
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
parts.add("--out=\"" + meta.getFileName() + "\"");
|
||||
}
|
||||
|
||||
// 添加其他常用参数
|
||||
parts.add("--continue"); // 支持断点续传
|
||||
parts.add("--max-tries=3"); // 最大重试次数
|
||||
parts.add("--retry-wait=5"); // 重试等待时间
|
||||
|
||||
// 添加URL
|
||||
parts.add("\"" + meta.getUrl() + "\"");
|
||||
|
||||
return String.join(" \\\n ", parts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.ARIA2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 比特彗星协议链接生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class BitCometLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 比特彗星支持 HTTP 下载,格式类似 IDM
|
||||
String encodedUrl = Base64.getEncoder().encodeToString(
|
||||
meta.getUrl().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
StringBuilder link = new StringBuilder("bitcomet:///?url=").append(encodedUrl);
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
StringBuilder headerStr = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
if (headerStr.length() > 0) {
|
||||
headerStr.append("\\r\\n");
|
||||
}
|
||||
headerStr.append(entry.getKey()).append(": ").append(entry.getValue());
|
||||
}
|
||||
|
||||
String encodedHeaders = Base64.getEncoder().encodeToString(
|
||||
headerStr.toString().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
link.append("&header=").append(encodedHeaders);
|
||||
}
|
||||
|
||||
// 添加文件名
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
String encodedFileName = Base64.getEncoder().encodeToString(
|
||||
meta.getFileName().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
link.append("&filename=").append(encodedFileName);
|
||||
}
|
||||
|
||||
return link.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
// 如果编码失败,返回简单的URL
|
||||
return "bitcomet:///?url=" + meta.getUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.BITCOMET;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* cURL 命令生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class CurlLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> parts = new ArrayList<>();
|
||||
parts.add("curl");
|
||||
parts.add("-L"); // 跟随重定向
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
parts.add("-H");
|
||||
parts.add("\"" + entry.getKey() + ": " + entry.getValue() + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
// 设置输出文件名
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
parts.add("-o");
|
||||
parts.add("\"" + meta.getFileName() + "\"");
|
||||
}
|
||||
|
||||
// 添加URL
|
||||
parts.add("\"" + meta.getUrl() + "\"");
|
||||
|
||||
return String.join(" \\\n ", parts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.CURL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Free Download Manager 导入格式生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class FdmLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// FDM 支持简单的文本格式导入
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("URL=").append(meta.getUrl()).append("\n");
|
||||
|
||||
// 添加文件名
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
result.append("Filename=").append(meta.getFileName()).append("\n");
|
||||
}
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
result.append("Headers=");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
if (!first) {
|
||||
result.append("; ");
|
||||
}
|
||||
result.append(entry.getKey()).append(": ").append(entry.getValue());
|
||||
first = false;
|
||||
}
|
||||
result.append("\n");
|
||||
}
|
||||
|
||||
result.append("Referer=").append(meta.getReferer() != null ? meta.getReferer() : "").append("\n");
|
||||
result.append("User-Agent=").append(meta.getUserAgent() != null ? meta.getUserAgent() : "").append("\n");
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.FDM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IDM 协议链接生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class IdmLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 对URL进行Base64编码
|
||||
String encodedUrl = Base64.getEncoder().encodeToString(
|
||||
meta.getUrl().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
StringBuilder link = new StringBuilder("idm:///?url=").append(encodedUrl);
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
StringBuilder headerStr = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
if (headerStr.length() > 0) {
|
||||
headerStr.append("\\r\\n");
|
||||
}
|
||||
headerStr.append(entry.getKey()).append(": ").append(entry.getValue());
|
||||
}
|
||||
|
||||
String encodedHeaders = Base64.getEncoder().encodeToString(
|
||||
headerStr.toString().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
link.append("&header=").append(encodedHeaders);
|
||||
}
|
||||
|
||||
// 添加文件名
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
String encodedFileName = Base64.getEncoder().encodeToString(
|
||||
meta.getFileName().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
link.append("&filename=").append(encodedFileName);
|
||||
}
|
||||
|
||||
return link.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
// 如果编码失败,返回简单的URL
|
||||
return "idm:///?url=" + meta.getUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.IDM;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Motrix 导入格式生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class MotrixLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 使用 Vert.x JsonObject 构建 JSON
|
||||
JsonObject taskJson = new JsonObject();
|
||||
taskJson.put("url", meta.getUrl());
|
||||
|
||||
// 添加文件名
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
taskJson.put("filename", meta.getFileName());
|
||||
}
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
JsonObject headersJson = new JsonObject();
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
headersJson.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
taskJson.put("headers", headersJson);
|
||||
}
|
||||
|
||||
// 设置输出文件名
|
||||
String outputFile = meta.getFileName() != null ? meta.getFileName() : "";
|
||||
taskJson.put("out", outputFile);
|
||||
|
||||
return taskJson.encodePrettily();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.MOTRIX;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* PowerShell 命令生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class PowerShellLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> lines = new ArrayList<>();
|
||||
|
||||
// 创建 WebRequestSession
|
||||
lines.add("$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession");
|
||||
|
||||
// 设置 User-Agent(如果存在)
|
||||
String userAgent = meta.getUserAgent();
|
||||
if (userAgent == null && meta.getHeaders() != null) {
|
||||
userAgent = meta.getHeaders().get("User-Agent");
|
||||
}
|
||||
if (userAgent != null && !userAgent.trim().isEmpty()) {
|
||||
lines.add("$session.UserAgent = \"" + escapePowerShellString(userAgent) + "\"");
|
||||
}
|
||||
|
||||
// 构建 Invoke-WebRequest 命令
|
||||
List<String> invokeParams = new ArrayList<>();
|
||||
invokeParams.add("Invoke-WebRequest");
|
||||
invokeParams.add("-UseBasicParsing");
|
||||
invokeParams.add("-Uri \"" + escapePowerShellString(meta.getUrl()) + "\"");
|
||||
|
||||
// 添加 WebSession
|
||||
invokeParams.add("-WebSession $session");
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
List<String> headerLines = new ArrayList<>();
|
||||
headerLines.add("-Headers @{");
|
||||
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
if (!first) {
|
||||
headerLines.add("");
|
||||
}
|
||||
headerLines.add(" \"" + escapePowerShellString(entry.getKey()) + "\"=\"" +
|
||||
escapePowerShellString(entry.getValue()) + "\"");
|
||||
first = false;
|
||||
}
|
||||
|
||||
headerLines.add("}");
|
||||
|
||||
// 将头部参数添加到主命令中
|
||||
invokeParams.add(String.join("`\n", headerLines));
|
||||
}
|
||||
|
||||
// 设置输出文件(如果指定了文件名)
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
invokeParams.add("-OutFile \"" + escapePowerShellString(meta.getFileName()) + "\"");
|
||||
}
|
||||
|
||||
// 将所有参数连接起来
|
||||
String invokeCommand = String.join(" `\n", invokeParams);
|
||||
lines.add(invokeCommand);
|
||||
|
||||
return String.join("\n", lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 PowerShell 字符串中的特殊字符
|
||||
*/
|
||||
private String escapePowerShellString(String str) {
|
||||
if (str == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return str.replace("`", "``")
|
||||
.replace("\"", "`\"")
|
||||
.replace("$", "`$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.POWERSHELL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 迅雷协议链接生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class ThunderLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 迅雷链接格式:thunder://Base64(AA + 原URL + ZZ)
|
||||
String originalUrl = meta.getUrl();
|
||||
String thunderUrl = "AA" + originalUrl + "ZZ";
|
||||
|
||||
// Base64编码
|
||||
String encodedUrl = Base64.getEncoder().encodeToString(
|
||||
thunderUrl.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
return "thunder://" + encodedUrl;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 如果编码失败,返回null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.THUNDER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.qaiu.parser.clientlink.impl;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* wget 命令生成器
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class WgetLinkGenerator implements ClientLinkGenerator {
|
||||
|
||||
@Override
|
||||
public String generate(DownloadLinkMeta meta) {
|
||||
if (!supports(meta)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> parts = new ArrayList<>();
|
||||
parts.add("wget");
|
||||
|
||||
// 添加请求头
|
||||
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
|
||||
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
|
||||
parts.add("--header=\"" + entry.getKey() + ": " + entry.getValue() + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
// 设置输出文件名
|
||||
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
|
||||
parts.add("-O");
|
||||
parts.add("\"" + meta.getFileName() + "\"");
|
||||
}
|
||||
|
||||
// 添加URL
|
||||
parts.add("\"" + meta.getUrl() + "\"");
|
||||
|
||||
return String.join(" \\\n ", parts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientLinkType getType() {
|
||||
return ClientLinkType.WGET;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cn.qaiu.parser.clientlink.util;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 请求头格式化工具类
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class HeaderFormatter {
|
||||
|
||||
/**
|
||||
* 将请求头格式化为 curl 格式
|
||||
*
|
||||
* @param headers 请求头Map
|
||||
* @return curl 格式的请求头字符串
|
||||
*/
|
||||
public static String formatForCurl(Map<String, String> headers) {
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
if (result.length() > 0) {
|
||||
result.append(" \\\n ");
|
||||
}
|
||||
result.append("-H \"").append(entry.getKey()).append(": ").append(entry.getValue()).append("\"");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求头格式化为 wget 格式
|
||||
*
|
||||
* @param headers 请求头Map
|
||||
* @return wget 格式的请求头字符串
|
||||
*/
|
||||
public static String formatForWget(Map<String, String> headers) {
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
if (result.length() > 0) {
|
||||
result.append(" \\\n ");
|
||||
}
|
||||
result.append("--header=\"").append(entry.getKey()).append(": ").append(entry.getValue()).append("\"");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求头格式化为 aria2 格式
|
||||
*
|
||||
* @param headers 请求头Map
|
||||
* @return aria2 格式的请求头字符串
|
||||
*/
|
||||
public static String formatForAria2(Map<String, String> headers) {
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
if (result.length() > 0) {
|
||||
result.append(" \\\n ");
|
||||
}
|
||||
result.append("--header=\"").append(entry.getKey()).append(": ").append(entry.getValue()).append("\"");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求头格式化为 HTTP 头格式(用于 Base64 编码)
|
||||
*
|
||||
* @param headers 请求头Map
|
||||
* @return HTTP 头格式的字符串
|
||||
*/
|
||||
public static String formatForHttpHeaders(Map<String, String> headers) {
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
if (result.length() > 0) {
|
||||
result.append("\\r\\n");
|
||||
}
|
||||
result.append(entry.getKey()).append(": ").append(entry.getValue());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求头格式化为 JSON 格式
|
||||
*
|
||||
* @param headers 请求头Map
|
||||
* @return JSON 格式的请求头字符串
|
||||
*/
|
||||
public static String formatForJson(Map<String, String> headers) {
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("{\n");
|
||||
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
if (!first) {
|
||||
result.append(",\n");
|
||||
}
|
||||
result.append(" \"").append(entry.getKey()).append("\": \"")
|
||||
.append(entry.getValue()).append("\"");
|
||||
first = false;
|
||||
}
|
||||
|
||||
result.append("\n }");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求头格式化为简单键值对格式(用于 FDM)
|
||||
*
|
||||
* @param headers 请求头Map
|
||||
* @return 简单键值对格式的字符串
|
||||
*/
|
||||
public static String formatForSimple(Map<String, String> headers) {
|
||||
if (headers == null || headers.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
if (result.length() > 0) {
|
||||
result.append("; ");
|
||||
}
|
||||
result.append(entry.getKey()).append(": ").append(entry.getValue());
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -347,7 +348,7 @@ public class JsHttpClient {
|
||||
*/
|
||||
public Map<String, String> headers() {
|
||||
MultiMap responseHeaders = response.headers();
|
||||
Map<String, String> result = new java.util.HashMap<>();
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (String name : responseHeaders.names()) {
|
||||
result.put(name, responseHeaders.get(name));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -136,11 +139,11 @@ public class JsScriptLoader {
|
||||
|
||||
try {
|
||||
String jarPath = jarUrl.getPath().substring(5, jarUrl.getPath().indexOf("!"));
|
||||
java.util.jar.JarFile jarFile = new java.util.jar.JarFile(jarPath);
|
||||
JarFile jarFile = new JarFile(jarPath);
|
||||
|
||||
java.util.Enumeration<java.util.jar.JarEntry> entries = jarFile.entries();
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
java.util.jar.JarEntry entry = entries.nextElement();
|
||||
JarEntry entry = entries.nextElement();
|
||||
String entryName = entry.getName();
|
||||
|
||||
if (entryName.startsWith(RESOURCE_PATH + "/") &&
|
||||
|
||||
@@ -6,6 +6,9 @@ import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 奶牛快传解析工具
|
||||
*
|
||||
@@ -46,7 +49,14 @@ public class CowTool extends PanBase {
|
||||
String downloadUrl = data2.getString("downloadUrl");
|
||||
if (StringUtils.isNotEmpty(downloadUrl)) {
|
||||
log.info("cow parse success: {}", downloadUrl);
|
||||
promise.complete(downloadUrl);
|
||||
|
||||
// 存储下载元数据,包括必要的请求头
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("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");
|
||||
headers.put("Referer", shareLinkInfo.getShareUrl());
|
||||
|
||||
// 使用新的 completeWithMeta 方法存储元数据
|
||||
completeWithMeta(downloadUrl, headers);
|
||||
return;
|
||||
}
|
||||
fail("cow parse fail: {}; downloadUrl is empty", url2);
|
||||
|
||||
@@ -9,9 +9,8 @@ import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <a href="https://www.ctfile.com">诚通网盘</a>
|
||||
@@ -88,7 +87,15 @@ public class CtTool extends PanBase {
|
||||
.send().onSuccess(res2 -> {
|
||||
JsonObject resJson2 = asJson(res2);
|
||||
if (resJson2.containsKey("downurl")) {
|
||||
promise.complete(resJson2.getString("downurl"));
|
||||
String downloadUrl = resJson2.getString("downurl");
|
||||
|
||||
// 存储下载元数据,包括必要的请求头
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||
headers.put("Referer", shareLinkInfo.getShareUrl());
|
||||
|
||||
// 使用新的 completeWithMeta 方法
|
||||
completeWithMeta(downloadUrl, headers);
|
||||
} else {
|
||||
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl");
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ public class PvyyTool extends PanBase {
|
||||
// var arr = asJson(res2).getJsonObject("data").getJsonArray("data");
|
||||
// List<FileInfo> list = arr.stream().map(o -> {
|
||||
// FileInfo fileInfo = new FileInfo();
|
||||
// var jo = ((io.vertx.core.json.JsonObject) o).getJsonObject("data");
|
||||
// var jo = ((JsonObject) o).getJsonObject("data");
|
||||
// String fileType = jo.getString("type");
|
||||
// fileInfo.setFileId(jo.getString("id"));
|
||||
// fileInfo.setFileName(jo.getJsonObject("attributes").getString("name"));
|
||||
|
||||
@@ -5,6 +5,9 @@ import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <a href="https://www.kdocs.cn/">WPS云文档</a>
|
||||
* 分享格式:https://www.kdocs.cn/l/ck0azivLlDi3
|
||||
@@ -38,7 +41,15 @@ public class PwpsTool extends PanBase {
|
||||
|
||||
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||
log.info("WPS云文档解析成功: shareKey={}, downloadUrl={}", shareKey, downloadUrl);
|
||||
promise.complete(downloadUrl);
|
||||
|
||||
// 存储下载元数据,包括必要的请求头
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("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");
|
||||
headers.put("Referer", shareLinkInfo.getShareUrl());
|
||||
|
||||
// 使用新的 completeWithMeta 方法存储元数据
|
||||
completeWithMeta(downloadUrl, headers);
|
||||
return;
|
||||
} else {
|
||||
fail("download_url字段为空");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.ParserCreate;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 客户端下载链接生成器使用示例
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class ClientLinkExample {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ClientLinkExample.class);
|
||||
|
||||
/**
|
||||
* 示例1:使用新的 parseWithClientLinks 方法
|
||||
*/
|
||||
public static void example1() {
|
||||
try {
|
||||
// 创建解析器
|
||||
IPanTool tool = ParserCreate.fromShareUrl("https://cowtransfer.com/s/abc123")
|
||||
.createTool();
|
||||
|
||||
// 解析并生成客户端链接
|
||||
Map<ClientLinkType, String> clientLinks = tool.parseWithClientLinksSync();
|
||||
|
||||
// 输出生成的链接
|
||||
log.info("=== 生成的客户端下载链接 ===");
|
||||
for (Map.Entry<ClientLinkType, String> entry : clientLinks.entrySet()) {
|
||||
log.info("{}: {}", entry.getKey().getDisplayName(), entry.getValue());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("示例1执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例2:传统方式 + 手动生成客户端链接
|
||||
*/
|
||||
public static void example2() {
|
||||
try {
|
||||
// 创建解析器
|
||||
IPanTool tool = ParserCreate.fromShareUrl("https://cowtransfer.com/s/abc123")
|
||||
.createTool();
|
||||
|
||||
// 解析获取直链
|
||||
String directLink = tool.parseSync();
|
||||
log.info("直链: {}", directLink);
|
||||
|
||||
// 获取 ShareLinkInfo
|
||||
ShareLinkInfo shareLinkInfo = tool.getShareLinkInfo();
|
||||
|
||||
// 手动生成客户端链接
|
||||
Map<ClientLinkType, String> clientLinks =
|
||||
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
|
||||
// 输出生成的链接
|
||||
log.info("=== 手动生成的客户端下载链接 ===");
|
||||
for (Map.Entry<ClientLinkType, String> entry : clientLinks.entrySet()) {
|
||||
log.info("{}: {}", entry.getKey().getDisplayName(), entry.getValue());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("示例2执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例3:生成特定类型的客户端链接
|
||||
*/
|
||||
public static void example3() {
|
||||
try {
|
||||
// 创建解析器
|
||||
IPanTool tool = ParserCreate.fromShareUrl("https://cowtransfer.com/s/abc123")
|
||||
.createTool();
|
||||
|
||||
// 解析获取直链
|
||||
String directLink = tool.parseSync();
|
||||
log.info("直链: {}", directLink);
|
||||
|
||||
// 获取 ShareLinkInfo
|
||||
ShareLinkInfo shareLinkInfo = tool.getShareLinkInfo();
|
||||
|
||||
// 生成特定类型的链接
|
||||
String curlCommand = ClientLinkGeneratorFactory.generate(shareLinkInfo, ClientLinkType.CURL);
|
||||
String thunderLink = ClientLinkGeneratorFactory.generate(shareLinkInfo, ClientLinkType.THUNDER);
|
||||
String aria2Command = ClientLinkGeneratorFactory.generate(shareLinkInfo, ClientLinkType.ARIA2);
|
||||
|
||||
log.info("=== 特定类型的客户端链接 ===");
|
||||
log.info("cURL命令: {}", curlCommand);
|
||||
log.info("迅雷链接: {}", thunderLink);
|
||||
log.info("Aria2命令: {}", aria2Command);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("示例3执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例4:使用便捷工具类
|
||||
*/
|
||||
public static void example4() {
|
||||
try {
|
||||
// 创建解析器
|
||||
IPanTool tool = ParserCreate.fromShareUrl("https://cowtransfer.com/s/abc123")
|
||||
.createTool();
|
||||
|
||||
// 解析获取直链
|
||||
String directLink = tool.parseSync();
|
||||
log.info("直链: {}", directLink);
|
||||
|
||||
// 获取 ShareLinkInfo
|
||||
ShareLinkInfo shareLinkInfo = tool.getShareLinkInfo();
|
||||
|
||||
// 使用便捷工具类
|
||||
String curlCommand = ClientLinkUtils.generateCurlCommand(shareLinkInfo);
|
||||
String wgetCommand = ClientLinkUtils.generateWgetCommand(shareLinkInfo);
|
||||
String thunderLink = ClientLinkUtils.generateThunderLink(shareLinkInfo);
|
||||
|
||||
log.info("=== 使用便捷工具类生成的链接 ===");
|
||||
log.info("cURL命令: {}", curlCommand);
|
||||
log.info("wget命令: {}", wgetCommand);
|
||||
log.info("迅雷链接: {}", thunderLink);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("示例4执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
log.info("开始演示客户端下载链接生成器功能");
|
||||
|
||||
example1();
|
||||
example2();
|
||||
example3();
|
||||
example4();
|
||||
|
||||
log.info("演示完成");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
import cn.qaiu.parser.clientlink.impl.CurlLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.impl.ThunderLinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.impl.Aria2LinkGenerator;
|
||||
import cn.qaiu.parser.clientlink.impl.PowerShellLinkGenerator;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* 客户端链接生成器功能测试
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class ClientLinkGeneratorTest {
|
||||
|
||||
private ShareLinkInfo shareLinkInfo;
|
||||
private DownloadLinkMeta meta;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// 创建测试用的 ShareLinkInfo
|
||||
shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type("test")
|
||||
.panName("测试网盘")
|
||||
.shareUrl("https://example.com/share/test")
|
||||
.build();
|
||||
|
||||
Map<String, Object> otherParam = new HashMap<>();
|
||||
otherParam.put("downloadUrl", "https://example.com/file.zip");
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("User-Agent", "Mozilla/5.0 (Test Browser)");
|
||||
headers.put("Referer", "https://example.com/share/test");
|
||||
headers.put("Cookie", "session=abc123");
|
||||
otherParam.put("downloadHeaders", headers);
|
||||
|
||||
shareLinkInfo.setOtherParam(otherParam);
|
||||
|
||||
// 创建测试用的 DownloadLinkMeta
|
||||
meta = new DownloadLinkMeta("https://example.com/file.zip");
|
||||
meta.setFileName("test-file.zip");
|
||||
meta.setHeaders(headers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCurlLinkGenerator() {
|
||||
CurlLinkGenerator generator = new CurlLinkGenerator();
|
||||
|
||||
String result = generator.generate(meta);
|
||||
|
||||
assertNotNull("cURL命令不应为空", result);
|
||||
assertTrue("应包含curl命令", result.contains("curl"));
|
||||
assertTrue("应包含下载URL", result.contains("https://example.com/file.zip"));
|
||||
assertTrue("应包含User-Agent头", result.contains("\"User-Agent: Mozilla/5.0 (Test Browser)\""));
|
||||
assertTrue("应包含Referer头", result.contains("\"Referer: https://example.com/share/test\""));
|
||||
assertTrue("应包含Cookie头", result.contains("\"Cookie: session=abc123\""));
|
||||
assertTrue("应包含输出文件名", result.contains("\"test-file.zip\""));
|
||||
assertTrue("应包含跟随重定向", result.contains("-L"));
|
||||
|
||||
assertEquals("类型应为CURL", ClientLinkType.CURL, generator.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThunderLinkGenerator() {
|
||||
ThunderLinkGenerator generator = new ThunderLinkGenerator();
|
||||
|
||||
String result = generator.generate(meta);
|
||||
|
||||
assertNotNull("迅雷链接不应为空", result);
|
||||
assertTrue("应以thunder://开头", result.startsWith("thunder://"));
|
||||
|
||||
// 验证Base64编码格式
|
||||
String encodedPart = result.substring("thunder://".length());
|
||||
assertNotNull("编码部分不应为空", encodedPart);
|
||||
assertFalse("编码部分不应为空字符串", encodedPart.isEmpty());
|
||||
|
||||
assertEquals("类型应为THUNDER", ClientLinkType.THUNDER, generator.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAria2LinkGenerator() {
|
||||
Aria2LinkGenerator generator = new Aria2LinkGenerator();
|
||||
|
||||
String result = generator.generate(meta);
|
||||
|
||||
assertNotNull("Aria2命令不应为空", result);
|
||||
assertTrue("应包含aria2c命令", result.contains("aria2c"));
|
||||
assertTrue("应包含下载URL", result.contains("https://example.com/file.zip"));
|
||||
assertTrue("应包含User-Agent头", result.contains("--header=\"User-Agent: Mozilla/5.0 (Test Browser)\""));
|
||||
assertTrue("应包含Referer头", result.contains("--header=\"Referer: https://example.com/share/test\""));
|
||||
assertTrue("应包含输出文件名", result.contains("--out=\"test-file.zip\""));
|
||||
assertTrue("应包含断点续传", result.contains("--continue"));
|
||||
|
||||
assertEquals("类型应为ARIA2", ClientLinkType.ARIA2, generator.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPowerShellLinkGenerator() {
|
||||
PowerShellLinkGenerator generator = new PowerShellLinkGenerator();
|
||||
|
||||
String result = generator.generate(meta);
|
||||
|
||||
assertNotNull("PowerShell命令不应为空", result);
|
||||
assertTrue("应包含WebRequestSession", result.contains("$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession"));
|
||||
assertTrue("应包含Invoke-WebRequest", result.contains("Invoke-WebRequest"));
|
||||
assertTrue("应包含-UseBasicParsing", result.contains("-UseBasicParsing"));
|
||||
assertTrue("应包含下载URL", result.contains("https://example.com/file.zip"));
|
||||
assertTrue("应包含User-Agent", result.contains("User-Agent"));
|
||||
assertTrue("应包含Referer", result.contains("Referer"));
|
||||
assertTrue("应包含Cookie", result.contains("Cookie"));
|
||||
assertTrue("应包含输出文件", result.contains("test-file.zip"));
|
||||
|
||||
assertEquals("类型应为POWERSHELL", ClientLinkType.POWERSHELL, generator.getType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPowerShellLinkGeneratorWithoutHeaders() {
|
||||
PowerShellLinkGenerator generator = new PowerShellLinkGenerator();
|
||||
|
||||
meta.setHeaders(new HashMap<>());
|
||||
String result = generator.generate(meta);
|
||||
|
||||
assertNotNull("PowerShell命令不应为空", result);
|
||||
assertTrue("应包含WebRequestSession", result.contains("$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession"));
|
||||
assertTrue("应包含Invoke-WebRequest", result.contains("Invoke-WebRequest"));
|
||||
assertTrue("应包含下载URL", result.contains("https://example.com/file.zip"));
|
||||
assertFalse("不应包含Headers", result.contains("-Headers @{"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPowerShellLinkGeneratorWithoutFileName() {
|
||||
PowerShellLinkGenerator generator = new PowerShellLinkGenerator();
|
||||
|
||||
meta.setFileName(null);
|
||||
String result = generator.generate(meta);
|
||||
|
||||
assertNotNull("PowerShell命令不应为空", result);
|
||||
assertTrue("应包含WebRequestSession", result.contains("$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession"));
|
||||
assertTrue("应包含Invoke-WebRequest", result.contains("Invoke-WebRequest"));
|
||||
assertTrue("应包含下载URL", result.contains("https://example.com/file.zip"));
|
||||
assertFalse("不应包含OutFile", result.contains("-OutFile"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPowerShellLinkGeneratorWithSpecialCharacters() {
|
||||
PowerShellLinkGenerator generator = new PowerShellLinkGenerator();
|
||||
|
||||
// 测试包含特殊字符的URL和请求头
|
||||
meta.setUrl("https://example.com/file with spaces.zip");
|
||||
Map<String, String> specialHeaders = new HashMap<>();
|
||||
specialHeaders.put("Custom-Header", "Value with \"quotes\" and $variables");
|
||||
meta.setHeaders(specialHeaders);
|
||||
|
||||
String result = generator.generate(meta);
|
||||
|
||||
assertNotNull("PowerShell命令不应为空", result);
|
||||
assertTrue("应包含转义的URL", result.contains("https://example.com/file with spaces.zip"));
|
||||
assertTrue("应包含转义的请求头", result.contains("Custom-Header"));
|
||||
assertTrue("应包含转义的引号", result.contains("`\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDownloadLinkMetaFromShareLinkInfo() {
|
||||
DownloadLinkMeta metaFromInfo = DownloadLinkMeta.fromShareLinkInfo(shareLinkInfo);
|
||||
|
||||
assertNotNull("从ShareLinkInfo创建的DownloadLinkMeta不应为空", metaFromInfo);
|
||||
assertEquals("URL应匹配", "https://example.com/file.zip", metaFromInfo.getUrl());
|
||||
assertEquals("Referer应匹配", "https://example.com/share/test", metaFromInfo.getReferer());
|
||||
assertEquals("User-Agent应匹配", "Mozilla/5.0 (Test Browser)", metaFromInfo.getUserAgent());
|
||||
|
||||
Map<String, String> headers = metaFromInfo.getHeaders();
|
||||
assertNotNull("请求头不应为空", headers);
|
||||
assertEquals("请求头数量应匹配", 3, headers.size());
|
||||
assertEquals("User-Agent应匹配", "Mozilla/5.0 (Test Browser)", headers.get("User-Agent"));
|
||||
assertEquals("Referer应匹配", "https://example.com/share/test", headers.get("Referer"));
|
||||
assertEquals("Cookie应匹配", "session=abc123", headers.get("Cookie"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientLinkGeneratorFactory() {
|
||||
Map<ClientLinkType, String> allLinks = ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
|
||||
assertNotNull("生成的链接集合不应为空", allLinks);
|
||||
assertFalse("生成的链接集合不应为空", allLinks.isEmpty());
|
||||
|
||||
// 检查是否生成了主要类型的链接
|
||||
assertTrue("应生成cURL链接", allLinks.containsKey(ClientLinkType.CURL));
|
||||
assertTrue("应生成迅雷链接", allLinks.containsKey(ClientLinkType.THUNDER));
|
||||
assertTrue("应生成Aria2链接", allLinks.containsKey(ClientLinkType.ARIA2));
|
||||
assertTrue("应生成wget链接", allLinks.containsKey(ClientLinkType.WGET));
|
||||
assertTrue("应生成PowerShell链接", allLinks.containsKey(ClientLinkType.POWERSHELL));
|
||||
|
||||
// 验证生成的链接不为空
|
||||
assertNotNull("cURL链接不应为空", allLinks.get(ClientLinkType.CURL));
|
||||
assertNotNull("迅雷链接不应为空", allLinks.get(ClientLinkType.THUNDER));
|
||||
assertNotNull("Aria2链接不应为空", allLinks.get(ClientLinkType.ARIA2));
|
||||
assertNotNull("wget链接不应为空", allLinks.get(ClientLinkType.WGET));
|
||||
assertNotNull("PowerShell链接不应为空", allLinks.get(ClientLinkType.POWERSHELL));
|
||||
|
||||
assertFalse("cURL链接不应为空字符串", allLinks.get(ClientLinkType.CURL).trim().isEmpty());
|
||||
assertFalse("迅雷链接不应为空字符串", allLinks.get(ClientLinkType.THUNDER).trim().isEmpty());
|
||||
assertFalse("Aria2链接不应为空字符串", allLinks.get(ClientLinkType.ARIA2).trim().isEmpty());
|
||||
assertFalse("wget链接不应为空字符串", allLinks.get(ClientLinkType.WGET).trim().isEmpty());
|
||||
assertFalse("PowerShell链接不应为空字符串", allLinks.get(ClientLinkType.POWERSHELL).trim().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientLinkUtils() {
|
||||
String curlCommand = ClientLinkUtils.generateCurlCommand(shareLinkInfo);
|
||||
String thunderLink = ClientLinkUtils.generateThunderLink(shareLinkInfo);
|
||||
String aria2Command = ClientLinkUtils.generateAria2Command(shareLinkInfo);
|
||||
String powershellCommand = ClientLinkUtils.generatePowerShellCommand(shareLinkInfo);
|
||||
|
||||
assertNotNull("cURL命令不应为空", curlCommand);
|
||||
assertNotNull("迅雷链接不应为空", thunderLink);
|
||||
assertNotNull("Aria2命令不应为空", aria2Command);
|
||||
assertNotNull("PowerShell命令不应为空", powershellCommand);
|
||||
|
||||
assertTrue("cURL命令应包含curl", curlCommand.contains("curl"));
|
||||
assertTrue("迅雷链接应以thunder://开头", thunderLink.startsWith("thunder://"));
|
||||
assertTrue("Aria2命令应包含aria2c", aria2Command.contains("aria2c"));
|
||||
assertTrue("PowerShell命令应包含Invoke-WebRequest", powershellCommand.contains("Invoke-WebRequest"));
|
||||
|
||||
// 测试元数据有效性检查
|
||||
assertTrue("应检测到有效的下载元数据", ClientLinkUtils.hasValidDownloadMeta(shareLinkInfo));
|
||||
|
||||
// 测试无效元数据
|
||||
ShareLinkInfo emptyInfo = ShareLinkInfo.newBuilder().build();
|
||||
assertFalse("应检测到无效的下载元数据", ClientLinkUtils.hasValidDownloadMeta(emptyInfo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullAndEmptyHandling() {
|
||||
// 测试空URL
|
||||
DownloadLinkMeta emptyMeta = new DownloadLinkMeta("");
|
||||
CurlLinkGenerator generator = new CurlLinkGenerator();
|
||||
|
||||
String result = generator.generate(emptyMeta);
|
||||
assertNull("空URL应返回null", result);
|
||||
|
||||
// 测试null元数据
|
||||
result = generator.generate(null);
|
||||
assertNull("null元数据应返回null", result);
|
||||
|
||||
// 测试null ShareLinkInfo
|
||||
String curlResult = ClientLinkUtils.generateCurlCommand(null);
|
||||
assertNull("null ShareLinkInfo应返回null", curlResult);
|
||||
|
||||
Map<ClientLinkType, String> allResult = ClientLinkUtils.generateAllClientLinks(null);
|
||||
assertTrue("null ShareLinkInfo应返回空集合", allResult.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cn.qaiu.parser.clientlink;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
import cn.qaiu.parser.clientlink.impl.PowerShellLinkGenerator;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* PowerShell 生成器示例
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
public class PowerShellExample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 创建测试数据
|
||||
DownloadLinkMeta meta = new DownloadLinkMeta("https://example.com/file.zip");
|
||||
meta.setFileName("test-file.zip");
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||||
headers.put("Referer", "https://example.com/share/test");
|
||||
headers.put("Cookie", "session=abc123");
|
||||
headers.put("Accept", "text/html,application/xhtml+xml");
|
||||
meta.setHeaders(headers);
|
||||
|
||||
// 生成 PowerShell 命令
|
||||
PowerShellLinkGenerator generator = new PowerShellLinkGenerator();
|
||||
String powershellCommand = generator.generate(meta);
|
||||
|
||||
System.out.println("=== 生成的 PowerShell 命令 ===");
|
||||
System.out.println(powershellCommand);
|
||||
System.out.println();
|
||||
|
||||
// 测试特殊字符转义
|
||||
meta.setUrl("https://example.com/file with spaces.zip");
|
||||
Map<String, String> specialHeaders = new HashMap<>();
|
||||
specialHeaders.put("Custom-Header", "Value with \"quotes\" and $variables");
|
||||
meta.setHeaders(specialHeaders);
|
||||
|
||||
String escapedCommand = generator.generate(meta);
|
||||
|
||||
System.out.println("=== 包含特殊字符的 PowerShell 命令 ===");
|
||||
System.out.println(escapedCommand);
|
||||
System.out.println();
|
||||
|
||||
// 使用 ClientLinkUtils
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type("test")
|
||||
.panName("测试网盘")
|
||||
.shareUrl("https://example.com/share/test")
|
||||
.build();
|
||||
|
||||
Map<String, Object> otherParam = new HashMap<>();
|
||||
otherParam.put("downloadUrl", "https://example.com/file.zip");
|
||||
otherParam.put("downloadHeaders", headers);
|
||||
shareLinkInfo.setOtherParam(otherParam);
|
||||
|
||||
String utilsCommand = ClientLinkUtils.generatePowerShellCommand(shareLinkInfo);
|
||||
|
||||
System.out.println("=== 使用 ClientLinkUtils 生成的 PowerShell 命令 ===");
|
||||
System.out.println(utilsCommand);
|
||||
}
|
||||
}
|
||||
37
test_client_links.java
Normal file
37
test_client_links.java
Normal file
@@ -0,0 +1,37 @@
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
|
||||
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
|
||||
import java.util.Map;
|
||||
|
||||
public class TestClientLinks {
|
||||
public static void main(String[] args) {
|
||||
// 创建一个测试用的 ShareLinkInfo,模拟解析器没有实现客户端下载文件元数据的情况
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.shareUrl("https://example.com/share/test123")
|
||||
.panName("测试网盘")
|
||||
.type("test")
|
||||
.build();
|
||||
|
||||
// 添加文件名信息(模拟解析器只解析了文件名)
|
||||
shareLinkInfo.getOtherParam().put("fileInfo", new cn.qaiu.entity.FileInfo() {
|
||||
@Override
|
||||
public String getFileName() {
|
||||
return "test-file.zip";
|
||||
}
|
||||
});
|
||||
|
||||
// 测试 DownloadLinkMeta.fromShareLinkInfo() 方法
|
||||
DownloadLinkMeta meta = DownloadLinkMeta.fromShareLinkInfo(shareLinkInfo);
|
||||
System.out.println("DownloadLinkMeta: " + meta);
|
||||
System.out.println("Has valid URL: " + meta.hasValidUrl());
|
||||
|
||||
// 测试生成客户端链接
|
||||
Map<cn.qaiu.parser.clientlink.ClientLinkType, String> clientLinks =
|
||||
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
|
||||
|
||||
System.out.println("Generated client links count: " + clientLinks.size());
|
||||
for (Map.Entry<cn.qaiu.parser.clientlink.ClientLinkType, String> entry : clientLinks.entrySet()) {
|
||||
System.out.println(entry.getKey().getDisplayName() + ": " + entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from '@/views/Home.vue'
|
||||
import ShowFile from '@/views/ShowFile.vue'
|
||||
import ShowList from '@/views/ShowList.vue'
|
||||
import ClientLinks from '@/views/ClientLinks.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: Home },
|
||||
{ path: '/showFile', component: ShowFile },
|
||||
{ path: '/showList', component: ShowList }
|
||||
{ path: '/showList', component: ShowList },
|
||||
{ path: '/clientLinks', component: ClientLinks }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
125
web-front/src/utils/api.js
Normal file
125
web-front/src/utils/api.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建 axios 实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:6400',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
// 可以在这里添加认证token等
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
console.error('API请求错误:', error)
|
||||
|
||||
if (error.response) {
|
||||
// 服务器返回错误状态码
|
||||
const message = error.response.data?.message || error.response.data?.error || '服务器错误'
|
||||
return Promise.reject(new Error(message))
|
||||
} else if (error.request) {
|
||||
// 网络错误
|
||||
return Promise.reject(new Error('网络连接失败,请检查网络设置'))
|
||||
} else {
|
||||
// 其他错误
|
||||
return Promise.reject(new Error(error.message || '请求失败'))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 客户端链接 API
|
||||
export const clientLinksApi = {
|
||||
/**
|
||||
* 获取所有客户端下载链接
|
||||
* @param {string} shareUrl - 分享链接
|
||||
* @param {string} password - 提取码(可选)
|
||||
* @returns {Promise} 客户端链接响应
|
||||
*/
|
||||
async getClientLinks(shareUrl, password = '') {
|
||||
const params = new URLSearchParams()
|
||||
params.append('url', shareUrl)
|
||||
if (password) {
|
||||
params.append('pwd', password)
|
||||
}
|
||||
|
||||
return await api.get(`/v2/clientLinks?${params.toString()}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取指定类型的客户端下载链接
|
||||
* @param {string} shareUrl - 分享链接
|
||||
* @param {string} password - 提取码(可选)
|
||||
* @param {string} clientType - 客户端类型
|
||||
* @returns {Promise} 指定类型的客户端链接
|
||||
*/
|
||||
async getClientLink(shareUrl, password = '', clientType) {
|
||||
const params = new URLSearchParams()
|
||||
params.append('url', shareUrl)
|
||||
if (password) {
|
||||
params.append('pwd', password)
|
||||
}
|
||||
params.append('clientType', clientType)
|
||||
|
||||
return await api.get(`/v2/clientLink?${params.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 其他 API(如果需要的话)
|
||||
export const parserApi = {
|
||||
/**
|
||||
* 解析分享链接
|
||||
* @param {string} shareUrl - 分享链接
|
||||
* @param {string} password - 提取码(可选)
|
||||
* @returns {Promise} 解析结果
|
||||
*/
|
||||
async parseLink(shareUrl, password = '') {
|
||||
const params = new URLSearchParams()
|
||||
params.append('url', shareUrl)
|
||||
if (password) {
|
||||
params.append('pwd', password)
|
||||
}
|
||||
|
||||
return await api.get(`/v2/linkInfo?${params.toString()}`)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
* @param {string} shareUrl - 分享链接
|
||||
* @param {string} password - 提取码(可选)
|
||||
* @param {string} dirId - 目录ID(可选)
|
||||
* @param {string} uuid - UUID(可选)
|
||||
* @returns {Promise} 文件列表
|
||||
*/
|
||||
async getFileList(shareUrl, password = '', dirId = '', uuid = '') {
|
||||
const params = new URLSearchParams()
|
||||
params.append('url', shareUrl)
|
||||
if (password) {
|
||||
params.append('pwd', password)
|
||||
}
|
||||
if (dirId) {
|
||||
params.append('dirId', dirId)
|
||||
}
|
||||
if (uuid) {
|
||||
params.append('uuid', uuid)
|
||||
}
|
||||
|
||||
return await api.get(`/v2/getFileList?${params.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default api
|
||||
720
web-front/src/views/ClientLinks.vue
Normal file
720
web-front/src/views/ClientLinks.vue
Normal file
File diff suppressed because one or more lines are too long
@@ -91,6 +91,7 @@
|
||||
<el-button style="margin-left: 20px" @click="generateMarkdown">生成Markdown</el-button>
|
||||
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
|
||||
<el-button style="margin-left: 20px" @click="getStatistics">分享统计</el-button>
|
||||
<el-button style="margin-left: 20px" @click="goToClientLinks" type="primary">客户端链接(实验)</el-button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -589,6 +590,55 @@ export default {
|
||||
}).catch(() => {
|
||||
this.$message.error('复制失败');
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到客户端链接页面
|
||||
async goToClientLinks() {
|
||||
// 验证输入
|
||||
if (!this.link.trim()) {
|
||||
this.$message.warning('请先输入分享链接')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
|
||||
this.$message.error("请输入有效链接!")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示加载状态
|
||||
this.isLoading = true
|
||||
|
||||
// 直接使用 axios 请求客户端链接 API,因为它的响应格式与其他 API 不同
|
||||
const params = { url: this.link }
|
||||
if (this.password) params.pwd = this.password
|
||||
|
||||
const response = await axios.get(`${this.baseAPI}/v2/clientLinks`, { params })
|
||||
const result = response.data
|
||||
|
||||
// 处理包装格式的响应
|
||||
const clientData = result.data || result
|
||||
|
||||
if (clientData.success) {
|
||||
// 将数据存储到 sessionStorage,供客户端链接页面使用
|
||||
sessionStorage.setItem('clientLinksData', JSON.stringify(clientData))
|
||||
sessionStorage.setItem('clientLinksForm', JSON.stringify({
|
||||
shareUrl: this.link,
|
||||
password: this.password
|
||||
}))
|
||||
|
||||
// 跳转到客户端链接页面
|
||||
this.$router.push('/clientLinks')
|
||||
this.$message.success('客户端链接生成成功,正在跳转...')
|
||||
} else {
|
||||
this.$message.error(clientData.error || '生成客户端链接失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成客户端链接失败:', error)
|
||||
this.$message.error('生成客户端链接失败')
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
120
web-service/doc/CLIENT_LINKS_API.md
Normal file
120
web-service/doc/CLIENT_LINKS_API.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# 客户端下载链接 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
新增的客户端下载链接 API 允许用户获取各种下载客户端格式的下载链接,包括 cURL、PowerShell、Aria2、迅雷等。
|
||||
|
||||
## API 端点
|
||||
|
||||
### 1. 获取所有客户端下载链接
|
||||
|
||||
**端点**: `GET /v2/clientLinks`
|
||||
|
||||
**参数**:
|
||||
- `url` (必需): 分享链接
|
||||
- `pwd` (可选): 提取码
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"directLink": "https://example.com/file.zip",
|
||||
"fileName": "test-file.zip",
|
||||
"fileSize": 1024000,
|
||||
"clientLinks": {
|
||||
"CURL": "curl -L -H \"User-Agent: Mozilla/5.0...\" -o \"test-file.zip\" \"https://example.com/file.zip\"",
|
||||
"POWERSHELL": "$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession...",
|
||||
"ARIA2": "aria2c --header=\"User-Agent: Mozilla/5.0...\" --out=\"test-file.zip\" \"https://example.com/file.zip\"",
|
||||
"THUNDER": "thunder://QUFodHRwczovL2V4YW1wbGUuY29tL2ZpbGUuemlwWlo=",
|
||||
"IDM": "idm://https://example.com/file.zip",
|
||||
"WGET": "wget --header=\"User-Agent: Mozilla/5.0...\" -O \"test-file.zip\" \"https://example.com/file.zip\"",
|
||||
"BITCOMET": "bitcomet://https://example.com/file.zip",
|
||||
"MOTRIX": "{\"url\":\"https://example.com/file.zip\",\"out\":\"test-file.zip\"}",
|
||||
"FDM": "https://example.com/file.zip"
|
||||
},
|
||||
"supportedClients": {
|
||||
"curl": "cURL 命令",
|
||||
"wget": "wget 命令",
|
||||
"aria2": "Aria2",
|
||||
"idm": "IDM",
|
||||
"thunder": "迅雷",
|
||||
"bitcomet": "比特彗星",
|
||||
"motrix": "Motrix",
|
||||
"fdm": "Free Download Manager",
|
||||
"powershell": "PowerShell"
|
||||
},
|
||||
"parserInfo": "百度网盘 - pan"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取指定类型的客户端下载链接
|
||||
|
||||
**端点**: `GET /v2/clientLink`
|
||||
|
||||
**参数**:
|
||||
- `url` (必需): 分享链接
|
||||
- `pwd` (可选): 提取码
|
||||
- `clientType` (必需): 客户端类型 (curl, wget, aria2, idm, thunder, bitcomet, motrix, fdm, powershell)
|
||||
|
||||
**响应**: 直接返回指定类型的客户端下载链接字符串
|
||||
|
||||
## 支持的客户端类型
|
||||
|
||||
| 客户端类型 | 代码 | 说明 | 输出格式 |
|
||||
|-----------|------|------|----------|
|
||||
| cURL | `curl` | 命令行工具 | curl 命令 |
|
||||
| wget | `wget` | 命令行工具 | wget 命令 |
|
||||
| Aria2 | `aria2` | 命令行/RPC | aria2c 命令 |
|
||||
| IDM | `idm` | Windows 下载管理器 | idm:// 协议链接 |
|
||||
| 迅雷 | `thunder` | 国内主流下载工具 | thunder:// 协议链接 |
|
||||
| 比特彗星 | `bitcomet` | BT 下载工具 | bitcomet:// 协议链接 |
|
||||
| Motrix | `motrix` | 跨平台下载工具 | JSON 格式 |
|
||||
| FDM | `fdm` | Free Download Manager | 文本格式 |
|
||||
| PowerShell | `powershell` | Windows PowerShell | PowerShell 命令 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 获取所有客户端链接
|
||||
```bash
|
||||
curl "http://localhost:8080/v2/clientLinks?url=https://pan.baidu.com/s/1test123&pwd=1234"
|
||||
```
|
||||
|
||||
### 获取 cURL 命令
|
||||
```bash
|
||||
curl "http://localhost:8080/v2/clientLink?url=https://pan.baidu.com/s/1test123&pwd=1234&clientType=curl"
|
||||
```
|
||||
|
||||
### 获取 PowerShell 命令
|
||||
```bash
|
||||
curl "http://localhost:8080/v2/clientLink?url=https://pan.baidu.com/s/1test123&pwd=1234&clientType=powershell"
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
当请求失败时,API 会返回错误信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "解析分享链接失败: 具体错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Referer 支持**: CowTool (奶牛快传) 解析器已正确实现 Referer 请求头支持
|
||||
2. **请求头处理**: 所有客户端链接都会包含必要的请求头(如 User-Agent、Referer、Cookie 等)
|
||||
3. **特殊字符转义**: PowerShell 命令会自动转义特殊字符(引号、美元符号等)
|
||||
4. **异步处理**: API 使用异步处理,确保高性能
|
||||
5. **错误容错**: 即使某个客户端类型生成失败,其他类型仍会正常生成
|
||||
|
||||
## 集成说明
|
||||
|
||||
该功能已集成到现有的解析器框架中:
|
||||
|
||||
- **ParserApi**: 新增两个 API 端点
|
||||
- **ClientLinkResp**: 新的响应模型
|
||||
- **CowTool**: 已支持 Referer 请求头
|
||||
- **PowerShell**: 新增 PowerShell 格式支持
|
||||
|
||||
所有功能都经过测试验证,可以安全使用。
|
||||
@@ -6,11 +6,13 @@ import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.lz.common.cache.CacheManager;
|
||||
import cn.qaiu.lz.common.util.URLParamUtil;
|
||||
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
||||
import cn.qaiu.lz.web.model.ClientLinkResp;
|
||||
import cn.qaiu.lz.web.model.LinkInfoResp;
|
||||
import cn.qaiu.lz.web.model.StatisticsInfo;
|
||||
import cn.qaiu.lz.web.service.DbService;
|
||||
import cn.qaiu.parser.PanDomainTemplate;
|
||||
import cn.qaiu.parser.ParserCreate;
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
||||
import cn.qaiu.vx.core.annotaions.RouteMapping;
|
||||
import cn.qaiu.vx.core.enums.RouteMethod;
|
||||
@@ -244,4 +246,151 @@ public class ParserApi {
|
||||
.replace("-", "")
|
||||
.replace(":", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端下载链接
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param pwd 提取码
|
||||
* @return 客户端下载链接响应
|
||||
*/
|
||||
@RouteMapping(value = "/clientLinks", method = RouteMethod.GET)
|
||||
public Future<ClientLinkResp> getClientLinks(HttpServerRequest request, String pwd) {
|
||||
Promise<ClientLinkResp> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
String shareUrl = URLParamUtil.parserParams(request);
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
|
||||
ShareLinkInfo shareLinkInfo = parserCreate.getShareLinkInfo();
|
||||
|
||||
// 使用默认方法解析并生成客户端链接
|
||||
parserCreate.createTool().parseWithClientLinks()
|
||||
.onSuccess(clientLinks -> {
|
||||
try {
|
||||
ClientLinkResp response = buildClientLinkResponse(shareLinkInfo, clientLinks);
|
||||
promise.complete(response);
|
||||
} catch (Exception e) {
|
||||
log.error("处理客户端链接结果失败", e);
|
||||
promise.fail(new RuntimeException("处理客户端链接结果失败: " + e.getMessage()));
|
||||
}
|
||||
})
|
||||
.onFailure(error -> {
|
||||
log.error("解析分享链接失败", error);
|
||||
promise.fail(new RuntimeException("解析分享链接失败: " + error.getMessage()));
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析请求参数失败", e);
|
||||
promise.fail(new RuntimeException("解析请求参数失败: " + e.getMessage()));
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的客户端下载链接
|
||||
*
|
||||
* @param request HTTP请求
|
||||
* @param pwd 提取码
|
||||
* @param clientType 客户端类型 (curl, wget, aria2, idm, thunder, bitcomet, motrix, fdm, powershell)
|
||||
* @return 指定类型的客户端下载链接
|
||||
*/
|
||||
@RouteMapping(value = "/clientLink", method = RouteMethod.GET)
|
||||
public Future<String> getClientLink(HttpServerRequest request, String pwd, String clientType) {
|
||||
Promise<String> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
String shareUrl = URLParamUtil.parserParams(request);
|
||||
ParserCreate parserCreate = ParserCreate.fromShareUrl(shareUrl).setShareLinkInfoPwd(pwd);
|
||||
|
||||
// 使用默认方法解析并生成客户端链接
|
||||
parserCreate.createTool().parseWithClientLinks()
|
||||
.onSuccess(clientLinks -> {
|
||||
try {
|
||||
String clientLink = extractClientLinkByType(clientLinks, clientType);
|
||||
if (clientLink != null) {
|
||||
promise.complete(clientLink);
|
||||
} else {
|
||||
promise.fail("无法生成 " + clientType + " 格式的下载链接");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
promise.fail("不支持的客户端类型: " + clientType);
|
||||
} catch (Exception e) {
|
||||
log.error("获取客户端链接失败", e);
|
||||
promise.fail("获取客户端链接失败: " + e.getMessage());
|
||||
}
|
||||
})
|
||||
.onFailure(error -> {
|
||||
log.error("解析分享链接失败", error);
|
||||
promise.fail("解析分享链接失败: " + error.getMessage());
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解析请求参数失败", e);
|
||||
promise.fail("解析请求参数失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建客户端链接响应
|
||||
*
|
||||
* @param shareLinkInfo 分享链接信息
|
||||
* @param clientLinks 客户端链接映射
|
||||
* @return 客户端链接响应
|
||||
*/
|
||||
private ClientLinkResp buildClientLinkResponse(ShareLinkInfo shareLinkInfo, Map<ClientLinkType, String> clientLinks) {
|
||||
// 从 otherParam 中获取直链
|
||||
String directLink = (String) shareLinkInfo.getOtherParam().get("downloadUrl");
|
||||
Map<String, String> supportedClients = buildSupportedClientsMap();
|
||||
FileInfo fileInfo = extractFileInfo(shareLinkInfo);
|
||||
|
||||
return ClientLinkResp.builder()
|
||||
.success(true)
|
||||
.directLink(directLink)
|
||||
.fileName(fileInfo != null ? fileInfo.getFileName() : null)
|
||||
.fileSize(fileInfo != null ? fileInfo.getSize() : null)
|
||||
.clientLinks(clientLinks)
|
||||
.supportedClients(supportedClients)
|
||||
.parserInfo(shareLinkInfo.getPanName() + " - " + shareLinkInfo.getType())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建支持的客户端类型映射
|
||||
*
|
||||
* @return 客户端类型映射
|
||||
*/
|
||||
private Map<String, String> buildSupportedClientsMap() {
|
||||
Map<String, String> supportedClients = new HashMap<>();
|
||||
for (ClientLinkType type : ClientLinkType.values()) {
|
||||
supportedClients.put(type.getCode(), type.getDisplayName());
|
||||
}
|
||||
return supportedClients;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从ShareLinkInfo中提取文件信息
|
||||
*
|
||||
* @param shareLinkInfo 分享链接信息
|
||||
* @return 文件信息,如果不存在则返回null
|
||||
*/
|
||||
private FileInfo extractFileInfo(ShareLinkInfo shareLinkInfo) {
|
||||
Object fileInfo = shareLinkInfo.getOtherParam().get("fileInfo");
|
||||
return fileInfo instanceof FileInfo ? (FileInfo) fileInfo : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据客户端类型提取对应的客户端链接
|
||||
*
|
||||
* @param clientLinks 客户端链接映射
|
||||
* @param clientType 客户端类型
|
||||
* @return 客户端链接,如果不存在则返回null
|
||||
* @throws IllegalArgumentException 如果客户端类型不支持
|
||||
*/
|
||||
private String extractClientLinkByType(Map<ClientLinkType, String> clientLinks, String clientType) {
|
||||
ClientLinkType type = ClientLinkType.valueOf(clientType.toUpperCase());
|
||||
return clientLinks.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.qaiu.lz.web.model;
|
||||
|
||||
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 客户端下载链接响应模型
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/01/21
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClientLinkResp {
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* 直链URL
|
||||
*/
|
||||
private String directLink;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 所有客户端下载链接
|
||||
*/
|
||||
private Map<ClientLinkType, String> clientLinks;
|
||||
|
||||
/**
|
||||
* 支持的客户端类型列表
|
||||
*/
|
||||
private Map<String, String> supportedClients;
|
||||
|
||||
/**
|
||||
* 解析信息
|
||||
*/
|
||||
private String parserInfo;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
### 客户端下载链接 API 测试
|
||||
|
||||
### 环境变量
|
||||
@host = http://localhost:6400
|
||||
@testUrl = https://www.kdocs.cn/l/ck0azivLlDi3
|
||||
@testPwd =
|
||||
@cowUrl = https://cowtransfer.com/s/test123
|
||||
|
||||
@lanzouUrl = https://wwsd.lanzoue.com/iLany1e9bbbi
|
||||
|
||||
### 1. 获取所有客户端下载链接
|
||||
GET {{host}}/v2/clientLinks?url={{lanzouUrl}}&pwd={{testPwd}}
|
||||
|
||||
### 2. 获取指定类型的客户端下载链接 - cURL
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=curl
|
||||
|
||||
### 3. 获取指定类型的客户端下载链接 - PowerShell
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=powershell
|
||||
|
||||
### 4. 获取指定类型的客户端下载链接 - Aria2
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=aria2
|
||||
|
||||
### 5. 获取指定类型的客户端下载链接 - 迅雷
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=thunder
|
||||
|
||||
### 6. 获取指定类型的客户端下载链接 - IDM
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=idm
|
||||
|
||||
### 7. 获取指定类型的客户端下载链接 - wget
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=wget
|
||||
|
||||
### 8. 获取指定类型的客户端下载链接 - 比特彗星
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=bitcomet
|
||||
|
||||
### 9. 获取指定类型的客户端下载链接 - Motrix
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=motrix
|
||||
|
||||
### 10. 获取指定类型的客户端下载链接 - FDM
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=fdm
|
||||
|
||||
### 11. 测试不支持的客户端类型
|
||||
GET {{host}}/v2/clientLink?url={{testUrl}}&pwd={{testPwd}}&clientType=invalid
|
||||
|
||||
### 12. 测试奶牛快传(需要 Referer)
|
||||
GET {{host}}/v2/clientLinks?url={{cowUrl}}&pwd=
|
||||
|
||||
### 13. 测试空参数
|
||||
GET {{host}}/v2/clientLinks
|
||||
|
||||
### 14. 测试无效URL
|
||||
GET {{host}}/v2/clientLinks?url=invalid-url&pwd=1234
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user