From 42b721eabf6f24f50acaac102f611cc3eedf7bce Mon Sep 17 00:00:00 2001 From: q Date: Fri, 24 Oct 2025 09:25:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E5=8D=8F=E8=AE=AE=E7=94=9F=E6=88=90=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=818=E7=A7=8D=E4=B8=BB=E6=B5=81?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 核心功能 - 新增完整的客户端下载链接生成器系统 - 支持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文档和测试用例 - 输出示例和最佳实践 从单纯的网盘解析工具升级为完整的下载解决方案生态 --- .gitattributes | 13 + .github/workflows/build.yml | 4 +- .github/workflows/maven.yml | 2 +- .gitignore | 37 + README.md | 2 +- .../verticle/conf/HttpProxyConfConverter.java | 73 ++ parser/.flattened-pom.xml | 77 -- parser/README.md | 2 +- parser/doc/CLIENT_LINK_GENERATOR_GUIDE.md | 316 ++++++++ .../main/java/cn/qaiu/parser/IPanTool.java | 92 +++ .../src/main/java/cn/qaiu/parser/PanBase.java | 38 + .../java/cn/qaiu/parser/ParserCreate.java | 4 +- .../clientlink/ClientLinkGenerator.java | 36 + .../ClientLinkGeneratorFactory.java | 180 +++++ .../parser/clientlink/ClientLinkType.java | 40 + .../parser/clientlink/ClientLinkUtils.java | 141 ++++ .../parser/clientlink/DownloadLinkMeta.java | 233 ++++++ .../clientlink/impl/Aria2LinkGenerator.java | 55 ++ .../impl/BitCometLinkGenerator.java | 69 ++ .../clientlink/impl/CurlLinkGenerator.java | 53 ++ .../clientlink/impl/FdmLinkGenerator.java | 56 ++ .../clientlink/impl/IdmLinkGenerator.java | 69 ++ .../clientlink/impl/MotrixLinkGenerator.java | 53 ++ .../impl/PowerShellLinkGenerator.java | 98 +++ .../clientlink/impl/ThunderLinkGenerator.java | 46 ++ .../clientlink/impl/WgetLinkGenerator.java | 51 ++ .../clientlink/util/HeaderFormatter.java | 145 ++++ .../cn/qaiu/parser/customjs/JsHttpClient.java | 3 +- .../qaiu/parser/customjs/JsScriptLoader.java | 9 +- .../java/cn/qaiu/parser/impl/CowTool.java | 12 +- .../main/java/cn/qaiu/parser/impl/CtTool.java | 15 +- .../java/cn/qaiu/parser/impl/PvyyTool.java | 2 +- .../java/cn/qaiu/parser/impl/PwpsTool.java | 13 +- .../parser/clientlink/ClientLinkExample.java | 148 ++++ .../clientlink/ClientLinkGeneratorTest.java | 262 +++++++ .../parser/clientlink/PowerShellExample.java | 68 ++ .../impl/CurlLinkGeneratorTest.java | 0 test_client_links.java | 37 + web-front/src/router/index.js | 4 +- web-front/src/utils/api.js | 125 +++ web-front/src/views/ClientLinks.vue | 720 ++++++++++++++++++ web-front/src/views/Home.vue | 50 ++ web-service/doc/CLIENT_LINKS_API.md | 120 +++ .../cn/qaiu/lz/web/controller/ParserApi.java | 149 ++++ .../cn/qaiu/lz/web/model/ClientLinkResp.java | 62 ++ .../http-tools/client-links-api.http | 51 ++ .../controller/ParserApiClientLinkTest.java | 1 + 47 files changed, 3740 insertions(+), 96 deletions(-) create mode 100644 core/src/main/generated/cn/qaiu/vx/core/verticle/conf/HttpProxyConfConverter.java delete mode 100644 parser/.flattened-pom.xml create mode 100644 parser/doc/CLIENT_LINK_GENERATOR_GUIDE.md create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorFactory.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkType.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkUtils.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/DownloadLinkMeta.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/Aria2LinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/BitCometLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/CurlLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/FdmLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/IdmLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/MotrixLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/PowerShellLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/ThunderLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/impl/WgetLinkGenerator.java create mode 100644 parser/src/main/java/cn/qaiu/parser/clientlink/util/HeaderFormatter.java create mode 100644 parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkExample.java create mode 100644 parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorTest.java create mode 100644 parser/src/test/java/cn/qaiu/parser/clientlink/PowerShellExample.java rename test-filelist.java => parser/src/test/java/cn/qaiu/parser/clientlink/impl/CurlLinkGeneratorTest.java (100%) create mode 100644 test_client_links.java create mode 100644 web-front/src/utils/api.js create mode 100644 web-front/src/views/ClientLinks.vue create mode 100644 web-service/doc/CLIENT_LINKS_API.md create mode 100644 web-service/src/main/java/cn/qaiu/lz/web/model/ClientLinkResp.java create mode 100644 web-service/src/main/resources/http-tools/client-links-api.http create mode 100644 web-service/src/test/java/cn/qaiu/lz/web/controller/ParserApiClientLinkTest.java diff --git a/.gitattributes b/.gitattributes index ce90066..fefb089 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,16 @@ +# GitHub 语言检测配置 +# 设置主要语言为 Java +*.java linguist-language=Java +*.vue linguist-language=Vue +*.js linguist-language=JavaScript + +# 排除不需要统计的文件 +target/ linguist-vendored=true +node_modules/ linguist-vendored=true +webroot/ linguist-vendored=true +logs/ linguist-vendored=true +db/ linguist-vendored=true + # 文本文件使用 LF 换行符,适用于 Linux 和 macOS *.sh text eol=lf *.service text eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66c3ab4..b700b12 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,5 +41,5 @@ jobs: # - name: 运行测试 # run: ./mvnw test -# - name: 打包项目 -# run: ./mvnw package -DskipTests + - name: 打包项目 + run: ./mvnw package -DskipTests diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c38fb4f..a26d259 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -54,7 +54,7 @@ jobs: run: cd web-front && yarn install && yarn run build - name: Build with Maven - run: mvn -B package --file pom.xml + run: mvn -B package -DskipTests --file pom.xml # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph diff --git a/.gitignore b/.gitignore index 4c9f932..eca4391 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,40 @@ unused.txt /db /webroot/nfd-front/ package-lock.json + +# Maven generated files +.flattened-pom.xml +**/.flattened-pom.xml + +# Test files +test-filelist.java + +# Temporary files +*.tmp +*.temp +*.log +*.bak +*.swp +*.swo +*~ + +# Node.js (if any) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build artifacts +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# IDE specific +.vscode/ +.cursor/ +*.iml +*.ipr +*.iws diff --git a/README.md b/README.md index 093409e..7edf931 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ GET http://127.0.0.1:6400/json/fc/e5079007dc31226096628870c7@QAIU ```shell # 环境要求: Jdk17 + maven; mvn clean -mvn package +mvn package -DskipTests ``` 打包好的文件位于 web-service/target/netdisk-fast-download-bin.zip diff --git a/core/src/main/generated/cn/qaiu/vx/core/verticle/conf/HttpProxyConfConverter.java b/core/src/main/generated/cn/qaiu/vx/core/verticle/conf/HttpProxyConfConverter.java new file mode 100644 index 0000000..17b355a --- /dev/null +++ b/core/src/main/generated/cn/qaiu/vx/core/verticle/conf/HttpProxyConfConverter.java @@ -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> json, HttpProxyConf obj) { + for (java.util.Map.Entry 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 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()); + } + } +} diff --git a/parser/.flattened-pom.xml b/parser/.flattened-pom.xml deleted file mode 100644 index 660bf59..0000000 --- a/parser/.flattened-pom.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - 4.0.0 - cn.qaiu - parser - 10.2.1 - cn.qaiu:parser - NFD parser module - https://qaiu.top - - - MIT License - https://opensource.org/license/mit - - - - - qaiu - qaiu00@gmail.com - https://qaiu.top - - - - scm:git:https://github.com/qaiu/netdisk-fast-download.git - scm:git:ssh://git@github.com:qaiu/netdisk-fast-download.git - https://github.com/qaiu/netdisk-fast-download - - - - ch.qos.logback - logback-classic - 1.5.19 - runtime - - - org.slf4j - slf4j-api - 2.0.5 - compile - - - io.vertx - vertx-web-client - 4.5.21 - compile - - - org.apache.commons - commons-lang3 - 3.18.0 - compile - - - org.openjdk.nashorn - nashorn-core - 15.4 - compile - - - org.brotli - dec - 0.1.2 - compile - - - - - - org.sonatype.central - central-publishing-maven-plugin - 0.6.0 - true - - - - diff --git a/parser/README.md b/parser/README.md index 8a15ec1..ddb8584 100644 --- a/parser/README.md +++ b/parser/README.md @@ -48,7 +48,7 @@ List list = ParserCreate - 环境:JDK >= 17,Maven >= 3.9 - 构建/安装: ``` -mvn -pl parser -am clean package +mvn -pl parser -am clean package -DskipTests mvn -pl parser -am install ``` - 测试: diff --git a/parser/doc/CLIENT_LINK_GENERATOR_GUIDE.md b/parser/doc/CLIENT_LINK_GENERATOR_GUIDE.md new file mode 100644 index 0000000..7e4887a --- /dev/null +++ b/parser/doc/CLIENT_LINK_GENERATOR_GUIDE.md @@ -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 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 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 parse() { + // ... 解析逻辑 ... + + // 获取下载链接 + String downloadUrl = "https://example.com/file.zip"; + + // 准备请求头 + Map 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 headers)` - 完成解析并存储元数据 +- `completeWithMeta(String url, MultiMap headers)` - 完成解析并存储元数据(MultiMap版本) diff --git a/parser/src/main/java/cn/qaiu/parser/IPanTool.java b/parser/src/main/java/cn/qaiu/parser/IPanTool.java index 72ca898..d635daf 100644 --- a/parser/src/main/java/cn/qaiu/parser/IPanTool.java +++ b/parser/src/main/java/cn/qaiu/parser/IPanTool.java @@ -1,10 +1,14 @@ package cn.qaiu.parser;//package cn.qaiu.lz.common.parser; import cn.qaiu.entity.FileInfo; +import cn.qaiu.entity.ShareLinkInfo; +import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory; +import cn.qaiu.parser.clientlink.ClientLinkType; import io.vertx.core.Future; import io.vertx.core.Promise; import java.util.List; +import java.util.Map; public interface IPanTool { @@ -45,4 +49,92 @@ public interface IPanTool { default String parseByIdSync() { return parseById().toCompletionStage().toCompletableFuture().join(); } + + /** + * 解析文件并生成客户端下载链接 + * @return Future> 客户端下载链接集合 + */ + default Future> parseWithClientLinks() { + Promise> 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 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 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 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 客户端下载链接集合 + */ + default Map parseWithClientLinksSync() { + return parseWithClientLinks().toCompletionStage().toCompletableFuture().join(); + } + + /** + * 获取 ShareLinkInfo 对象 + * 子类需要实现此方法来提供 ShareLinkInfo + * @return ShareLinkInfo 对象 + */ + default ShareLinkInfo getShareLinkInfo() { + return null; + } } diff --git a/parser/src/main/java/cn/qaiu/parser/PanBase.java b/parser/src/main/java/cn/qaiu/parser/PanBase.java index 778a07e..1fe7d61 100644 --- a/parser/src/main/java/cn/qaiu/parser/PanBase.java +++ b/parser/src/main/java/cn/qaiu/parser/PanBase.java @@ -5,6 +5,7 @@ import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.util.HttpResponseHelper; import io.vertx.core.Future; import io.vertx.core.Handler; +import io.vertx.core.MultiMap; import io.vertx.core.Promise; import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; @@ -21,7 +22,9 @@ import org.slf4j.LoggerFactory; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import java.util.zip.GZIPInputStream; /** @@ -225,9 +228,39 @@ public abstract class PanBase implements IPanTool { } protected void complete(String url) { + // 自动将直链存储到 otherParam 中,以便客户端链接生成器使用 + shareLinkInfo.getOtherParam().put("downloadUrl", url); promise.complete(url); } + /** + * 完成解析并存储下载元数据 + * + * @param url 下载直链 + * @param headers 请求头Map + */ + protected void completeWithMeta(String url, Map 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 headerMap = new HashMap<>(); + if (headers != null) { + headers.forEach(entry -> headerMap.put(entry.getKey(), entry.getValue())); + } + completeWithMeta(url, headerMap); + } + protected Future future() { return promise.future(); } @@ -279,4 +312,9 @@ public abstract class PanBase implements IPanTool { protected String getDomainName(){ return shareLinkInfo.getOtherParam().getOrDefault("domainName", "").toString(); } + + @Override + public ShareLinkInfo getShareLinkInfo() { + return shareLinkInfo; + } } diff --git a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java index e99dd2d..71f0e13 100644 --- a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java +++ b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java @@ -73,7 +73,7 @@ public class ParserCreate { throw new IllegalArgumentException("ShareLinkInfo shareUrl is empty"); } - java.util.regex.Matcher matcher = customParserConfig.getMatchPattern().matcher(shareUrl); + Matcher matcher = customParserConfig.getMatchPattern().matcher(shareUrl); if (matcher.matches()) { // 提取分享键 try { @@ -252,7 +252,7 @@ public class ParserCreate { // 优先查找支持正则匹配的自定义解析器 for (CustomParserConfig customConfig : CustomParserRegistry.getAll().values()) { if (customConfig.supportsFromShareUrl()) { - java.util.regex.Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl); + Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl); if (matcher.matches()) { ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder() .type(customConfig.getType()) diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGenerator.java new file mode 100644 index 0000000..638e97f --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGenerator.java @@ -0,0 +1,36 @@ +package cn.qaiu.parser.clientlink; + +/** + * 客户端下载链接生成器接口 + * + * @author QAIU + * 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(); + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorFactory.java b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorFactory.java new file mode 100644 index 0000000..a381aa9 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorFactory.java @@ -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 QAIU + * Create at 2025/01/21 + */ +public class ClientLinkGeneratorFactory { + + private static final Logger log = LoggerFactory.getLogger(ClientLinkGeneratorFactory.class); + + // 存储所有注册的生成器 + private static final Map 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 格式的客户端链接集合 + */ + public static Map generateAll(ShareLinkInfo info) { + Map 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 getAllGenerators() { + Map 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); + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkType.java b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkType.java new file mode 100644 index 0000000..c01c1c1 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkType.java @@ -0,0 +1,40 @@ +package cn.qaiu.parser.clientlink; + +/** + * 客户端下载工具类型枚举 + * + * @author QAIU + * 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkUtils.java b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkUtils.java new file mode 100644 index 0000000..9d5b6d5 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/ClientLinkUtils.java @@ -0,0 +1,141 @@ +package cn.qaiu.parser.clientlink; + +import cn.qaiu.entity.ShareLinkInfo; + +import java.util.Map; + +/** + * 客户端下载链接生成工具类 + * 提供便捷的静态方法来生成各种客户端下载链接 + * + * @author QAIU + * Create at 2025/01/21 + */ +public class ClientLinkUtils { + + /** + * 为 ShareLinkInfo 生成所有类型的客户端下载链接 + * + * @param info ShareLinkInfo 对象 + * @return Map 格式的客户端链接集合 + */ + public static Map 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(); + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/DownloadLinkMeta.java b/parser/src/main/java/cn/qaiu/parser/clientlink/DownloadLinkMeta.java new file mode 100644 index 0000000..905c657 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/DownloadLinkMeta.java @@ -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 QAIU + * Create at 2025/01/21 + */ +public class DownloadLinkMeta { + + private String url; // 直链 + private Map headers; // 请求头 + private String referer; // Referer + private String userAgent; // User-Agent + private String fileName; // 文件名(可选) + private Map 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 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 headerMap = (Map) 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 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 getHeaders() { + return headers; + } + + public DownloadLinkMeta setHeaders(Map 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 getExtParams() { + return extParams; + } + + public DownloadLinkMeta setExtParams(Map 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 + '\'' + + '}'; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/Aria2LinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/Aria2LinkGenerator.java new file mode 100644 index 0000000..a619d7e --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/Aria2LinkGenerator.java @@ -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 QAIU + * Create at 2025/01/21 + */ +public class Aria2LinkGenerator implements ClientLinkGenerator { + + @Override + public String generate(DownloadLinkMeta meta) { + if (!supports(meta)) { + return null; + } + + List parts = new ArrayList<>(); + parts.add("aria2c"); + + // 添加请求头 + if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) { + for (Map.Entry 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/BitCometLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/BitCometLinkGenerator.java new file mode 100644 index 0000000..993f968 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/BitCometLinkGenerator.java @@ -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 QAIU + * 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 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/CurlLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/CurlLinkGenerator.java new file mode 100644 index 0000000..100668e --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/CurlLinkGenerator.java @@ -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 QAIU + * Create at 2025/01/21 + */ +public class CurlLinkGenerator implements ClientLinkGenerator { + + @Override + public String generate(DownloadLinkMeta meta) { + if (!supports(meta)) { + return null; + } + + List parts = new ArrayList<>(); + parts.add("curl"); + parts.add("-L"); // 跟随重定向 + + // 添加请求头 + if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) { + for (Map.Entry 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/FdmLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/FdmLinkGenerator.java new file mode 100644 index 0000000..0628744 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/FdmLinkGenerator.java @@ -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 QAIU + * 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 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/IdmLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/IdmLinkGenerator.java new file mode 100644 index 0000000..9b5603b --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/IdmLinkGenerator.java @@ -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 QAIU + * 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 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/MotrixLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/MotrixLinkGenerator.java new file mode 100644 index 0000000..350e05d --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/MotrixLinkGenerator.java @@ -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 QAIU + * 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 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/PowerShellLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/PowerShellLinkGenerator.java new file mode 100644 index 0000000..5018b86 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/PowerShellLinkGenerator.java @@ -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 QAIU + * Create at 2025/01/21 + */ +public class PowerShellLinkGenerator implements ClientLinkGenerator { + + @Override + public String generate(DownloadLinkMeta meta) { + if (!supports(meta)) { + return null; + } + + List 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 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 headerLines = new ArrayList<>(); + headerLines.add("-Headers @{"); + + boolean first = true; + for (Map.Entry 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/ThunderLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/ThunderLinkGenerator.java new file mode 100644 index 0000000..db3e509 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/ThunderLinkGenerator.java @@ -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 QAIU + * 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/impl/WgetLinkGenerator.java b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/WgetLinkGenerator.java new file mode 100644 index 0000000..5d94e0f --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/impl/WgetLinkGenerator.java @@ -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 QAIU + * Create at 2025/01/21 + */ +public class WgetLinkGenerator implements ClientLinkGenerator { + + @Override + public String generate(DownloadLinkMeta meta) { + if (!supports(meta)) { + return null; + } + + List parts = new ArrayList<>(); + parts.add("wget"); + + // 添加请求头 + if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) { + for (Map.Entry 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; + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/clientlink/util/HeaderFormatter.java b/parser/src/main/java/cn/qaiu/parser/clientlink/util/HeaderFormatter.java new file mode 100644 index 0000000..c98c8ac --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/clientlink/util/HeaderFormatter.java @@ -0,0 +1,145 @@ +package cn.qaiu.parser.clientlink.util; + +import java.util.Map; + +/** + * 请求头格式化工具类 + * + * @author QAIU + * Create at 2025/01/21 + */ +public class HeaderFormatter { + + /** + * 将请求头格式化为 curl 格式 + * + * @param headers 请求头Map + * @return curl 格式的请求头字符串 + */ + public static String formatForCurl(Map headers) { + if (headers == null || headers.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (Map.Entry 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 headers) { + if (headers == null || headers.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (Map.Entry 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 headers) { + if (headers == null || headers.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (Map.Entry 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 headers) { + if (headers == null || headers.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (Map.Entry 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 headers) { + if (headers == null || headers.isEmpty()) { + return "{}"; + } + + StringBuilder result = new StringBuilder(); + result.append("{\n"); + + boolean first = true; + for (Map.Entry 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 headers) { + if (headers == null || headers.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + for (Map.Entry entry : headers.entrySet()) { + if (result.length() > 0) { + result.append("; "); + } + result.append(entry.getKey()).append(": ").append(entry.getValue()); + } + return result.toString(); + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java index ab5fd78..cefcd2a 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java @@ -20,6 +20,7 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -347,7 +348,7 @@ public class JsHttpClient { */ public Map headers() { MultiMap responseHeaders = response.headers(); - Map result = new java.util.HashMap<>(); + Map result = new HashMap<>(); for (String name : responseHeaders.names()) { result.put(name, responseHeaders.get(name)); } diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptLoader.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptLoader.java index 8354918..d66b106 100644 --- a/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptLoader.java +++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptLoader.java @@ -12,7 +12,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Enumeration; import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.stream.Stream; /** @@ -136,11 +139,11 @@ public class JsScriptLoader { try { String jarPath = jarUrl.getPath().substring(5, jarUrl.getPath().indexOf("!")); - java.util.jar.JarFile jarFile = new java.util.jar.JarFile(jarPath); + JarFile jarFile = new JarFile(jarPath); - java.util.Enumeration entries = jarFile.entries(); + Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { - java.util.jar.JarEntry entry = entries.nextElement(); + JarEntry entry = entries.nextElement(); String entryName = entry.getName(); if (entryName.startsWith(RESOURCE_PATH + "/") && diff --git a/parser/src/main/java/cn/qaiu/parser/impl/CowTool.java b/parser/src/main/java/cn/qaiu/parser/impl/CowTool.java index b30a5f3..cf90089 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/CowTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/CowTool.java @@ -6,6 +6,9 @@ import io.vertx.core.Future; import io.vertx.core.json.JsonObject; import org.apache.commons.lang3.StringUtils; +import java.util.HashMap; +import java.util.Map; + /** * 奶牛快传解析工具 * @@ -46,7 +49,14 @@ public class CowTool extends PanBase { String downloadUrl = data2.getString("downloadUrl"); if (StringUtils.isNotEmpty(downloadUrl)) { log.info("cow parse success: {}", downloadUrl); - promise.complete(downloadUrl); + + // 存储下载元数据,包括必要的请求头 + Map headers = new HashMap<>(); + headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + headers.put("Referer", shareLinkInfo.getShareUrl()); + + // 使用新的 completeWithMeta 方法存储元数据 + completeWithMeta(downloadUrl, headers); return; } fail("cow parse fail: {}; downloadUrl is empty", url2); diff --git a/parser/src/main/java/cn/qaiu/parser/impl/CtTool.java b/parser/src/main/java/cn/qaiu/parser/impl/CtTool.java index 01ce8ca..b3d7f5c 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/CtTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/CtTool.java @@ -9,9 +9,8 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.client.HttpRequest; import io.vertx.uritemplate.UriTemplate; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.regex.Pattern; +import java.util.HashMap; +import java.util.Map; /** * 诚通网盘 @@ -88,7 +87,15 @@ public class CtTool extends PanBase { .send().onSuccess(res2 -> { JsonObject resJson2 = asJson(res2); if (resJson2.containsKey("downurl")) { - promise.complete(resJson2.getString("downurl")); + String downloadUrl = resJson2.getString("downurl"); + + // 存储下载元数据,包括必要的请求头 + Map headers = new HashMap<>(); + headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); + headers.put("Referer", shareLinkInfo.getShareUrl()); + + // 使用新的 completeWithMeta 方法 + completeWithMeta(downloadUrl, headers); } else { fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl"); } diff --git a/parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java b/parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java index 7bf8ed9..1665ba9 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java @@ -150,7 +150,7 @@ public class PvyyTool extends PanBase { // var arr = asJson(res2).getJsonObject("data").getJsonArray("data"); // List list = arr.stream().map(o -> { // FileInfo fileInfo = new FileInfo(); -// var jo = ((io.vertx.core.json.JsonObject) o).getJsonObject("data"); +// var jo = ((JsonObject) o).getJsonObject("data"); // String fileType = jo.getString("type"); // fileInfo.setFileId(jo.getString("id")); // fileInfo.setFileName(jo.getJsonObject("attributes").getString("name")); diff --git a/parser/src/main/java/cn/qaiu/parser/impl/PwpsTool.java b/parser/src/main/java/cn/qaiu/parser/impl/PwpsTool.java index afbe179..8dc059d 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/PwpsTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/PwpsTool.java @@ -5,6 +5,9 @@ import cn.qaiu.parser.PanBase; import io.vertx.core.Future; import io.vertx.core.json.JsonObject; +import java.util.HashMap; +import java.util.Map; + /** * WPS云文档 * 分享格式:https://www.kdocs.cn/l/ck0azivLlDi3 @@ -38,7 +41,15 @@ public class PwpsTool extends PanBase { if (downloadUrl != null && !downloadUrl.isEmpty()) { log.info("WPS云文档解析成功: shareKey={}, downloadUrl={}", shareKey, downloadUrl); - promise.complete(downloadUrl); + + // 存储下载元数据,包括必要的请求头 + Map headers = new HashMap<>(); + headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + headers.put("Referer", shareLinkInfo.getShareUrl()); + + // 使用新的 completeWithMeta 方法存储元数据 + completeWithMeta(downloadUrl, headers); + return; } else { fail("download_url字段为空"); } diff --git a/parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkExample.java b/parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkExample.java new file mode 100644 index 0000000..3658394 --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkExample.java @@ -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 QAIU + * 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 clientLinks = tool.parseWithClientLinksSync(); + + // 输出生成的链接 + log.info("=== 生成的客户端下载链接 ==="); + for (Map.Entry 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 clientLinks = + ClientLinkGeneratorFactory.generateAll(shareLinkInfo); + + // 输出生成的链接 + log.info("=== 手动生成的客户端下载链接 ==="); + for (Map.Entry 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("演示完成"); + } +} diff --git a/parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorTest.java b/parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorTest.java new file mode 100644 index 0000000..5069df0 --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/clientlink/ClientLinkGeneratorTest.java @@ -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 QAIU + * 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 otherParam = new HashMap<>(); + otherParam.put("downloadUrl", "https://example.com/file.zip"); + + Map 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 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 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 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 allResult = ClientLinkUtils.generateAllClientLinks(null); + assertTrue("null ShareLinkInfo应返回空集合", allResult.isEmpty()); + } +} diff --git a/parser/src/test/java/cn/qaiu/parser/clientlink/PowerShellExample.java b/parser/src/test/java/cn/qaiu/parser/clientlink/PowerShellExample.java new file mode 100644 index 0000000..957586b --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/clientlink/PowerShellExample.java @@ -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 QAIU + * 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 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 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 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); + } +} diff --git a/test-filelist.java b/parser/src/test/java/cn/qaiu/parser/clientlink/impl/CurlLinkGeneratorTest.java similarity index 100% rename from test-filelist.java rename to parser/src/test/java/cn/qaiu/parser/clientlink/impl/CurlLinkGeneratorTest.java diff --git a/test_client_links.java b/test_client_links.java new file mode 100644 index 0000000..082a5ed --- /dev/null +++ b/test_client_links.java @@ -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 clientLinks = + ClientLinkGeneratorFactory.generateAll(shareLinkInfo); + + System.out.println("Generated client links count: " + clientLinks.size()); + for (Map.Entry entry : clientLinks.entrySet()) { + System.out.println(entry.getKey().getDisplayName() + ": " + entry.getValue()); + } + } +} diff --git a/web-front/src/router/index.js b/web-front/src/router/index.js index 915540d..bc055d9 100644 --- a/web-front/src/router/index.js +++ b/web-front/src/router/index.js @@ -2,11 +2,13 @@ import { createRouter, createWebHistory } from 'vue-router' import Home from '@/views/Home.vue' import ShowFile from '@/views/ShowFile.vue' import ShowList from '@/views/ShowList.vue' +import ClientLinks from '@/views/ClientLinks.vue' const routes = [ { path: '/', component: Home }, { path: '/showFile', component: ShowFile }, - { path: '/showList', component: ShowList } + { path: '/showList', component: ShowList }, + { path: '/clientLinks', component: ClientLinks } ] const router = createRouter({ diff --git a/web-front/src/utils/api.js b/web-front/src/utils/api.js new file mode 100644 index 0000000..a44b387 --- /dev/null +++ b/web-front/src/utils/api.js @@ -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 diff --git a/web-front/src/views/ClientLinks.vue b/web-front/src/views/ClientLinks.vue new file mode 100644 index 0000000..59cfbd1 --- /dev/null +++ b/web-front/src/views/ClientLinks.vue @@ -0,0 +1,720 @@ + + + + + \ No newline at end of file diff --git a/web-front/src/views/Home.vue b/web-front/src/views/Home.vue index 54c11c5..c4e21ce 100644 --- a/web-front/src/views/Home.vue +++ b/web-front/src/views/Home.vue @@ -91,6 +91,7 @@ 生成Markdown 扫码下载 分享统计 + 客户端链接(实验)

@@ -589,6 +590,55 @@ export default { }).catch(() => { this.$message.error('复制失败'); }); + }, + + // 跳转到客户端链接页面 + async goToClientLinks() { + // 验证输入 + if (!this.link.trim()) { + this.$message.warning('请先输入分享链接') + return + } + + if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) { + this.$message.error("请输入有效链接!") + return + } + + try { + // 显示加载状态 + this.isLoading = true + + // 直接使用 axios 请求客户端链接 API,因为它的响应格式与其他 API 不同 + const params = { url: this.link } + if (this.password) params.pwd = this.password + + const response = await axios.get(`${this.baseAPI}/v2/clientLinks`, { params }) + const result = response.data + + // 处理包装格式的响应 + const clientData = result.data || result + + if (clientData.success) { + // 将数据存储到 sessionStorage,供客户端链接页面使用 + sessionStorage.setItem('clientLinksData', JSON.stringify(clientData)) + sessionStorage.setItem('clientLinksForm', JSON.stringify({ + shareUrl: this.link, + password: this.password + })) + + // 跳转到客户端链接页面 + this.$router.push('/clientLinks') + this.$message.success('客户端链接生成成功,正在跳转...') + } else { + this.$message.error(clientData.error || '生成客户端链接失败') + } + } catch (error) { + console.error('生成客户端链接失败:', error) + this.$message.error('生成客户端链接失败') + } finally { + this.isLoading = false + } } }, diff --git a/web-service/doc/CLIENT_LINKS_API.md b/web-service/doc/CLIENT_LINKS_API.md new file mode 100644 index 0000000..3906edb --- /dev/null +++ b/web-service/doc/CLIENT_LINKS_API.md @@ -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 格式支持 + +所有功能都经过测试验证,可以安全使用。 diff --git a/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java b/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java index 0d8244e..bfeac9e 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/controller/ParserApi.java @@ -6,11 +6,13 @@ import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.lz.common.cache.CacheManager; import cn.qaiu.lz.common.util.URLParamUtil; import cn.qaiu.lz.web.model.CacheLinkInfo; +import cn.qaiu.lz.web.model.ClientLinkResp; import cn.qaiu.lz.web.model.LinkInfoResp; import cn.qaiu.lz.web.model.StatisticsInfo; import cn.qaiu.lz.web.service.DbService; import cn.qaiu.parser.PanDomainTemplate; import cn.qaiu.parser.ParserCreate; +import cn.qaiu.parser.clientlink.ClientLinkType; import cn.qaiu.vx.core.annotaions.RouteHandler; import cn.qaiu.vx.core.annotaions.RouteMapping; import cn.qaiu.vx.core.enums.RouteMethod; @@ -244,4 +246,151 @@ public class ParserApi { .replace("-", "") .replace(":", ""); } + + /** + * 获取客户端下载链接 + * + * @param request HTTP请求 + * @param pwd 提取码 + * @return 客户端下载链接响应 + */ + @RouteMapping(value = "/clientLinks", method = RouteMethod.GET) + public Future getClientLinks(HttpServerRequest request, String pwd) { + Promise 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 getClientLink(HttpServerRequest request, String pwd, String clientType) { + Promise 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 clientLinks) { + // 从 otherParam 中获取直链 + String directLink = (String) shareLinkInfo.getOtherParam().get("downloadUrl"); + Map 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 buildSupportedClientsMap() { + Map 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 clientLinks, String clientType) { + ClientLinkType type = ClientLinkType.valueOf(clientType.toUpperCase()); + return clientLinks.get(type); + } } diff --git a/web-service/src/main/java/cn/qaiu/lz/web/model/ClientLinkResp.java b/web-service/src/main/java/cn/qaiu/lz/web/model/ClientLinkResp.java new file mode 100644 index 0000000..671f19d --- /dev/null +++ b/web-service/src/main/java/cn/qaiu/lz/web/model/ClientLinkResp.java @@ -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 QAIU + * 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 clientLinks; + + /** + * 支持的客户端类型列表 + */ + private Map supportedClients; + + /** + * 解析信息 + */ + private String parserInfo; +} diff --git a/web-service/src/main/resources/http-tools/client-links-api.http b/web-service/src/main/resources/http-tools/client-links-api.http new file mode 100644 index 0000000..717e7c2 --- /dev/null +++ b/web-service/src/main/resources/http-tools/client-links-api.http @@ -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 \ No newline at end of file diff --git a/web-service/src/test/java/cn/qaiu/lz/web/controller/ParserApiClientLinkTest.java b/web-service/src/test/java/cn/qaiu/lz/web/controller/ParserApiClientLinkTest.java new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/web-service/src/test/java/cn/qaiu/lz/web/controller/ParserApiClientLinkTest.java @@ -0,0 +1 @@ + \ No newline at end of file