mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 04:13:03 +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
|
# 文本文件使用 LF 换行符,适用于 Linux 和 macOS
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
*.service 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: 运行测试
|
# - name: 运行测试
|
||||||
# run: ./mvnw test
|
# run: ./mvnw test
|
||||||
|
|
||||||
# - name: 打包项目
|
- name: 打包项目
|
||||||
# run: ./mvnw package -DskipTests
|
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
|
run: cd web-front && yarn install && yarn run build
|
||||||
|
|
||||||
- name: Build with Maven
|
- 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
|
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
|
||||||
- name: Update dependency graph
|
- name: Update dependency graph
|
||||||
|
|||||||
37
.gitignore
vendored
37
.gitignore
vendored
@@ -41,3 +41,40 @@ unused.txt
|
|||||||
/db
|
/db
|
||||||
/webroot/nfd-front/
|
/webroot/nfd-front/
|
||||||
package-lock.json
|
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
|
```shell
|
||||||
# 环境要求: Jdk17 + maven;
|
# 环境要求: Jdk17 + maven;
|
||||||
mvn clean
|
mvn clean
|
||||||
mvn package
|
mvn package -DskipTests
|
||||||
|
|
||||||
```
|
```
|
||||||
打包好的文件位于 web-service/target/netdisk-fast-download-bin.zip
|
打包好的文件位于 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
|
- 环境:JDK >= 17,Maven >= 3.9
|
||||||
- 构建/安装:
|
- 构建/安装:
|
||||||
```
|
```
|
||||||
mvn -pl parser -am clean package
|
mvn -pl parser -am clean package -DskipTests
|
||||||
mvn -pl parser -am install
|
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;
|
package cn.qaiu.parser;//package cn.qaiu.lz.common.parser;
|
||||||
|
|
||||||
import cn.qaiu.entity.FileInfo;
|
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.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface IPanTool {
|
public interface IPanTool {
|
||||||
|
|
||||||
@@ -45,4 +49,92 @@ public interface IPanTool {
|
|||||||
default String parseByIdSync() {
|
default String parseByIdSync() {
|
||||||
return parseById().toCompletionStage().toCompletableFuture().join();
|
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 cn.qaiu.util.HttpResponseHelper;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Handler;
|
import io.vertx.core.Handler;
|
||||||
|
import io.vertx.core.MultiMap;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.buffer.Buffer;
|
import io.vertx.core.buffer.Buffer;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
@@ -21,7 +22,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,9 +228,39 @@ public abstract class PanBase implements IPanTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void complete(String url) {
|
protected void complete(String url) {
|
||||||
|
// 自动将直链存储到 otherParam 中,以便客户端链接生成器使用
|
||||||
|
shareLinkInfo.getOtherParam().put("downloadUrl", url);
|
||||||
promise.complete(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() {
|
protected Future<String> future() {
|
||||||
return promise.future();
|
return promise.future();
|
||||||
}
|
}
|
||||||
@@ -279,4 +312,9 @@ public abstract class PanBase implements IPanTool {
|
|||||||
protected String getDomainName(){
|
protected String getDomainName(){
|
||||||
return shareLinkInfo.getOtherParam().getOrDefault("domainName", "").toString();
|
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");
|
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()) {
|
if (matcher.matches()) {
|
||||||
// 提取分享键
|
// 提取分享键
|
||||||
try {
|
try {
|
||||||
@@ -252,7 +252,7 @@ public class ParserCreate {
|
|||||||
// 优先查找支持正则匹配的自定义解析器
|
// 优先查找支持正则匹配的自定义解析器
|
||||||
for (CustomParserConfig customConfig : CustomParserRegistry.getAll().values()) {
|
for (CustomParserConfig customConfig : CustomParserRegistry.getAll().values()) {
|
||||||
if (customConfig.supportsFromShareUrl()) {
|
if (customConfig.supportsFromShareUrl()) {
|
||||||
java.util.regex.Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl);
|
Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||||
.type(customConfig.getType())
|
.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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@@ -347,7 +348,7 @@ public class JsHttpClient {
|
|||||||
*/
|
*/
|
||||||
public Map<String, String> headers() {
|
public Map<String, String> headers() {
|
||||||
MultiMap responseHeaders = response.headers();
|
MultiMap responseHeaders = response.headers();
|
||||||
Map<String, String> result = new java.util.HashMap<>();
|
Map<String, String> result = new HashMap<>();
|
||||||
for (String name : responseHeaders.names()) {
|
for (String name : responseHeaders.names()) {
|
||||||
result.put(name, responseHeaders.get(name));
|
result.put(name, responseHeaders.get(name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,11 +139,11 @@ public class JsScriptLoader {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
String jarPath = jarUrl.getPath().substring(5, jarUrl.getPath().indexOf("!"));
|
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()) {
|
while (entries.hasMoreElements()) {
|
||||||
java.util.jar.JarEntry entry = entries.nextElement();
|
JarEntry entry = entries.nextElement();
|
||||||
String entryName = entry.getName();
|
String entryName = entry.getName();
|
||||||
|
|
||||||
if (entryName.startsWith(RESOURCE_PATH + "/") &&
|
if (entryName.startsWith(RESOURCE_PATH + "/") &&
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import io.vertx.core.Future;
|
|||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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");
|
String downloadUrl = data2.getString("downloadUrl");
|
||||||
if (StringUtils.isNotEmpty(downloadUrl)) {
|
if (StringUtils.isNotEmpty(downloadUrl)) {
|
||||||
log.info("cow parse success: {}", 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;
|
return;
|
||||||
}
|
}
|
||||||
fail("cow parse fail: {}; downloadUrl is empty", url2);
|
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.ext.web.client.HttpRequest;
|
||||||
import io.vertx.uritemplate.UriTemplate;
|
import io.vertx.uritemplate.UriTemplate;
|
||||||
|
|
||||||
import java.net.MalformedURLException;
|
import java.util.HashMap;
|
||||||
import java.net.URL;
|
import java.util.Map;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <a href="https://www.ctfile.com">诚通网盘</a>
|
* <a href="https://www.ctfile.com">诚通网盘</a>
|
||||||
@@ -88,7 +87,15 @@ public class CtTool extends PanBase {
|
|||||||
.send().onSuccess(res2 -> {
|
.send().onSuccess(res2 -> {
|
||||||
JsonObject resJson2 = asJson(res2);
|
JsonObject resJson2 = asJson(res2);
|
||||||
if (resJson2.containsKey("downurl")) {
|
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 {
|
} else {
|
||||||
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl");
|
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ public class PvyyTool extends PanBase {
|
|||||||
// var arr = asJson(res2).getJsonObject("data").getJsonArray("data");
|
// var arr = asJson(res2).getJsonObject("data").getJsonArray("data");
|
||||||
// List<FileInfo> list = arr.stream().map(o -> {
|
// List<FileInfo> list = arr.stream().map(o -> {
|
||||||
// FileInfo fileInfo = new FileInfo();
|
// 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");
|
// String fileType = jo.getString("type");
|
||||||
// fileInfo.setFileId(jo.getString("id"));
|
// fileInfo.setFileId(jo.getString("id"));
|
||||||
// fileInfo.setFileName(jo.getJsonObject("attributes").getString("name"));
|
// 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.Future;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <a href="https://www.kdocs.cn/">WPS云文档</a>
|
* <a href="https://www.kdocs.cn/">WPS云文档</a>
|
||||||
* 分享格式:https://www.kdocs.cn/l/ck0azivLlDi3
|
* 分享格式:https://www.kdocs.cn/l/ck0azivLlDi3
|
||||||
@@ -38,7 +41,15 @@ public class PwpsTool extends PanBase {
|
|||||||
|
|
||||||
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
if (downloadUrl != null && !downloadUrl.isEmpty()) {
|
||||||
log.info("WPS云文档解析成功: shareKey={}, downloadUrl={}", shareKey, downloadUrl);
|
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 {
|
} else {
|
||||||
fail("download_url字段为空");
|
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 Home from '@/views/Home.vue'
|
||||||
import ShowFile from '@/views/ShowFile.vue'
|
import ShowFile from '@/views/ShowFile.vue'
|
||||||
import ShowList from '@/views/ShowList.vue'
|
import ShowList from '@/views/ShowList.vue'
|
||||||
|
import ClientLinks from '@/views/ClientLinks.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: Home },
|
{ path: '/', component: Home },
|
||||||
{ path: '/showFile', component: ShowFile },
|
{ path: '/showFile', component: ShowFile },
|
||||||
{ path: '/showList', component: ShowList }
|
{ path: '/showList', component: ShowList },
|
||||||
|
{ path: '/clientLinks', component: ClientLinks }
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
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="generateMarkdown">生成Markdown</el-button>
|
||||||
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</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="getStatistics">分享统计</el-button>
|
||||||
|
<el-button style="margin-left: 20px" @click="goToClientLinks" type="primary">客户端链接(实验)</el-button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -589,6 +590,55 @@ export default {
|
|||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.$message.error('复制失败');
|
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.cache.CacheManager;
|
||||||
import cn.qaiu.lz.common.util.URLParamUtil;
|
import cn.qaiu.lz.common.util.URLParamUtil;
|
||||||
import cn.qaiu.lz.web.model.CacheLinkInfo;
|
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.LinkInfoResp;
|
||||||
import cn.qaiu.lz.web.model.StatisticsInfo;
|
import cn.qaiu.lz.web.model.StatisticsInfo;
|
||||||
import cn.qaiu.lz.web.service.DbService;
|
import cn.qaiu.lz.web.service.DbService;
|
||||||
import cn.qaiu.parser.PanDomainTemplate;
|
import cn.qaiu.parser.PanDomainTemplate;
|
||||||
import cn.qaiu.parser.ParserCreate;
|
import cn.qaiu.parser.ParserCreate;
|
||||||
|
import cn.qaiu.parser.clientlink.ClientLinkType;
|
||||||
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
import cn.qaiu.vx.core.annotaions.RouteHandler;
|
||||||
import cn.qaiu.vx.core.annotaions.RouteMapping;
|
import cn.qaiu.vx.core.annotaions.RouteMapping;
|
||||||
import cn.qaiu.vx.core.enums.RouteMethod;
|
import cn.qaiu.vx.core.enums.RouteMethod;
|
||||||
@@ -244,4 +246,151 @@ public class ParserApi {
|
|||||||
.replace("-", "")
|
.replace("-", "")
|
||||||
.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