diff --git a/README.md b/README.md
index 95648e3..578f069 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# 一款网盘分享链接云解析快速下载服务
QQ交流群:1017480890
-
+
-
+
@@ -419,7 +419,7 @@ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtow
> 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径
```shell
cd ~
-wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/v0.1.9b7/netdisk-fast-download-bin.zip
+wget -O netdisk-fast-download.zip https://github.com/qaiu/netdisk-fast-download/releases/download/v3.0.2/netdisk-fast-download-bin.zip
unzip netdisk-fast-download-bin.zip
cd netdisk-fast-download
bash service-install.sh
diff --git a/core-database/src/main/java/cn/qaiu/db/ddl/CreateDatabase.java b/core-database/src/main/java/cn/qaiu/db/ddl/CreateDatabase.java
index 6749c15..f740c0f 100644
--- a/core-database/src/main/java/cn/qaiu/db/ddl/CreateDatabase.java
+++ b/core-database/src/main/java/cn/qaiu/db/ddl/CreateDatabase.java
@@ -53,7 +53,7 @@ public class CreateDatabase {
stmt.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
LOGGER.info(">>>>>>>>>>> 数据库'{}'创建成功 <<<<<<<<<<<<", dbName);
} catch (SQLException e) {
- e.printStackTrace();
+ LOGGER.error("创建数据库失败", e);
}
}
diff --git a/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java b/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java
index 2228980..a547e32 100644
--- a/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java
+++ b/core-database/src/main/java/cn/qaiu/db/ddl/CreateTable.java
@@ -24,35 +24,39 @@ import java.util.*;
* @author QAIU
*/
public class CreateTable {
- public static Map, String> javaProperty2SqlColumnMap = new HashMap<>() {{
+ public static final Map, String> javaProperty2SqlColumnMap;
+ static {
+ Map, String> map = new HashMap<>();
// Java类型到SQL类型的映射
- put(Integer.class, "INT");
- put(Short.class, "SMALLINT");
- put(Byte.class, "TINYINT");
- put(Long.class, "BIGINT");
- put(java.math.BigDecimal.class, "DECIMAL");
- put(Double.class, "DOUBLE");
- put(Float.class, "REAL");
- put(Boolean.class, "BOOLEAN");
- put(String.class, "VARCHAR");
- put(Date.class, "TIMESTAMP");
- put(java.time.LocalDateTime.class, "TIMESTAMP");
- put(java.sql.Timestamp.class, "TIMESTAMP");
- put(java.sql.Date.class, "DATE");
- put(java.sql.Time.class, "TIME");
+ map.put(Integer.class, "INT");
+ map.put(Short.class, "SMALLINT");
+ map.put(Byte.class, "TINYINT");
+ map.put(Long.class, "BIGINT");
+ map.put(java.math.BigDecimal.class, "DECIMAL");
+ map.put(Double.class, "DOUBLE");
+ map.put(Float.class, "REAL");
+ map.put(Boolean.class, "BOOLEAN");
+ map.put(String.class, "VARCHAR");
+ map.put(Date.class, "TIMESTAMP");
+ map.put(java.time.LocalDateTime.class, "TIMESTAMP");
+ map.put(java.sql.Timestamp.class, "TIMESTAMP");
+ map.put(java.sql.Date.class, "DATE");
+ map.put(java.sql.Time.class, "TIME");
// 基本数据类型
- put(int.class, "INT");
- put(short.class, "SMALLINT");
- put(byte.class, "TINYINT");
- put(long.class, "BIGINT");
- put(double.class, "DOUBLE");
- put(float.class, "REAL");
- put(boolean.class, "BOOLEAN");
- }};
+ map.put(int.class, "INT");
+ map.put(short.class, "SMALLINT");
+ map.put(byte.class, "TINYINT");
+ map.put(long.class, "BIGINT");
+ map.put(double.class, "DOUBLE");
+ map.put(float.class, "REAL");
+ map.put(boolean.class, "BOOLEAN");
+
+ javaProperty2SqlColumnMap = Collections.unmodifiableMap(map);
+ }
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class);
- public static String UNIQUE_PREFIX = "idx_";
+ public static final String UNIQUE_PREFIX = "idx_";
private static Case getCase(Class> clz) {
return switch (clz.getName()) {
diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java
index 09c2f9f..5fb606f 100644
--- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java
+++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java
@@ -16,6 +16,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.management.ManagementFactory;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;
@@ -62,7 +64,12 @@ public final class Deploy {
path.append("-").append(args[0].replace("app-",""));
}
- // 读取yml配置
+ // 读取yml配置,优先当前目录,其次 resources/ 子目录
+ String configFile = path + ".yml";
+ if (!Files.exists(Path.of(configFile)) && Files.exists(Path.of("resources", configFile))) {
+ path.insert(0, "resources/");
+ LOGGER.info("从 resources/ 目录加载配置: {}", path + ".yml");
+ }
ConfigUtil.readYamlConfig(path.toString(), tempVertx)
.onSuccess(this::readConf)
.onFailure(err -> {
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java
index d9da4ff..3f77baa 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java
@@ -153,7 +153,7 @@ public class CommonUtil {
appVersion = properties.getProperty("app.version") + "build" + properties.getProperty("build");
}
} catch (IOException e) {
- e.printStackTrace();
+ LOGGER.error("读取app.properties失败", e);
}
}
return appVersion;
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java
index 1d5cb50..4b15dc7 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java
@@ -10,6 +10,8 @@ import io.vertx.core.json.JsonObject;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
/**
* 异步读取配置工具类
@@ -62,12 +64,35 @@ public class ConfigUtil {
// 异步获取配置
// 成功直接完成 promise
retriever.getConfig()
- .onSuccess(promise::complete)
- .onFailure(err -> {
- // 配置读取失败,直接返回失败 Future
- promise.fail(new RuntimeException(
- "读取配置文件失败: " + path, err));
+ .onSuccess(config -> {
+ promise.complete(config);
retriever.close();
+ })
+ .onFailure(err -> {
+ retriever.close();
+ // 读取失败时,尝试从 resources/ 子目录读取(兼容 Docker 卷挂载场景)
+ String resourcesPath = "resources/" + path;
+ if (!path.startsWith("resources/") && Files.exists(Path.of(resourcesPath))) {
+ ConfigStoreOptions fallbackStore = new ConfigStoreOptions()
+ .setType("file")
+ .setFormat(format)
+ .setConfig(new JsonObject().put("path", resourcesPath));
+ ConfigRetriever fallbackRetriever = ConfigRetriever
+ .create(vertx, new ConfigRetrieverOptions().addStore(fallbackStore));
+ fallbackRetriever.getConfig()
+ .onSuccess(config -> {
+ promise.complete(config);
+ fallbackRetriever.close();
+ })
+ .onFailure(e2 -> {
+ promise.fail(new RuntimeException(
+ "读取配置文件失败: " + path + " (也尝试了 " + resourcesPath + ")", e2));
+ fallbackRetriever.close();
+ });
+ } else {
+ promise.fail(new RuntimeException(
+ "读取配置文件失败: " + path, err));
+ }
});
return promise.future();
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
index 3d30eee..8cb1b50 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
@@ -25,6 +25,9 @@ import java.net.URL;
import java.text.ParseException;
import java.util.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS;
/**
@@ -36,6 +39,8 @@ import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS;
*/
public final class ReflectionUtil {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionUtil.class);
+
// 缓存Reflections实例,避免重复扫描(每次扫描约35K+值,耗时1-3秒,占用大量内存)
private static final Map REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
@@ -128,7 +133,7 @@ public final class ReflectionUtil {
parameterTypes[j - k]));
}
} catch (NotFoundException e) {
- e.printStackTrace();
+ LOGGER.error("获取方法参数失败", e);
}
return paramMap;
}
@@ -183,7 +188,7 @@ public final class ReflectionUtil {
try {
return DateUtils.parseDate(value, fmt);
} catch (ParseException e) {
- e.printStackTrace();
+ LOGGER.error("日期解析失败: {}", value, e);
throw new RuntimeException("无法将格式化日期");
}
default:
@@ -215,7 +220,7 @@ public final class ReflectionUtil {
}
return arr;
} catch (Exception e) {
- e.printStackTrace();
+ LOGGER.error("数组类型转换失败: {}", value, e);
}
return null;
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
index aa713ea..c6fdfc3 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
@@ -196,7 +196,7 @@ public class HttpProxyVerticle extends AbstractVerticle {
);
})
.onFailure(err -> {
- err.printStackTrace();
+ LOGGER.error("HTTP请求失败", err);
clientRequest.response().setStatusCode(502).end("Bad Gateway: Request failed");
});
}
@@ -222,7 +222,7 @@ public class HttpProxyVerticle extends AbstractVerticle {
}
return port;
} catch (Exception e) {
- e.printStackTrace();
+ LOGGER.error("提取端口失败: {}", urlString, e);
// 出现异常时返回 -1,表示提取失败
return -1;
}
diff --git a/parser/README.md b/parser/README.md
index 9b3c606..ddae166 100644
--- a/parser/README.md
+++ b/parser/README.md
@@ -4,26 +4,26 @@ NFD 解析器模块:聚合各类网盘/分享页解析,统一输出文件列
- 语言:Java 17
- 构建:Maven
-- 模块版本:10.1.17
+- 模块版本:10.2.5
## 依赖(Maven Central)
```xml
cn.qaiu
parser
- 10.1.17
+ 10.2.5
```
- Gradle Groovy DSL:
```groovy
dependencies {
- implementation 'cn.qaiu:parser:10.1.17'
+ implementation 'cn.qaiu:parser:10.2.5'
}
```
- Gradle Kotlin DSL:
```kotlin
dependencies {
- implementation("cn.qaiu:parser:10.1.17")
+ implementation("cn.qaiu:parser:10.2.5")
}
```
diff --git a/parser/doc/CUSTOM_PARSER_GUIDE.md b/parser/doc/CUSTOM_PARSER_GUIDE.md
index 9628cb2..ede1900 100644
--- a/parser/doc/CUSTOM_PARSER_GUIDE.md
+++ b/parser/doc/CUSTOM_PARSER_GUIDE.md
@@ -28,7 +28,7 @@
cn.qaiu
parser
- 10.1.17
+ 10.2.5
```
diff --git a/parser/doc/CUSTOM_PARSER_QUICKSTART.md b/parser/doc/CUSTOM_PARSER_QUICKSTART.md
index 8067805..b17d297 100644
--- a/parser/doc/CUSTOM_PARSER_QUICKSTART.md
+++ b/parser/doc/CUSTOM_PARSER_QUICKSTART.md
@@ -11,7 +11,7 @@
cn.qaiu
parser
- 10.1.17
+ 10.2.5
```
diff --git a/parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java b/parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java
index 856a46a..a92534d 100644
--- a/parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java
+++ b/parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java
@@ -3,9 +3,11 @@ package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase;
+import cn.qaiu.util.CommonUtils;
import cn.qaiu.util.HeaderUtils;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
+import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.slf4j.Logger;
@@ -13,27 +15,33 @@ import org.slf4j.LoggerFactory;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* QQ闪传
- * 只能客户端上传 支持Android QQ 9.2.5, MACOS QQ 6.9.78,可生成分享链接,通过浏览器下载,支持超大文件,有效期默认7天(暂时没找到续期方法)。
+ * 支持多文件、多级目录解析。通过 GetFileList API 获取文件列表,BatchDownload API 获取下载直链。
+ * 有效期默认7天。
*/
public class QQscTool extends PanBase {
Logger LOG = LoggerFactory.getLogger(QQscTool.class);
- private static final String API_URL = "https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.qqntv2.richmedia.InnerProxy/BatchDownload";
+ private static final String BATCH_DOWNLOAD_API =
+ "https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.qqntv2.richmedia.InnerProxy/BatchDownload";
- private static final MultiMap HEADERS = HeaderUtils.parseHeaders("""
+ private static final String GET_FILE_LIST_API =
+ "https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.file.FileFlashTrans/GetFileList";
+
+ private static final MultiMap BATCH_DOWNLOAD_HEADERS = HeaderUtils.parseHeaders("""
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Cookie: uin=9000002; p_uin=9000002
DNT: 1
Origin: https://qfile.qq.com
- Referer: https://qfile.qq.com/q/Xolxtv5b4O
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
@@ -46,86 +54,257 @@ public class QQscTool extends PanBase {
x-oidb: {"uint32_command":"0x9248", "uint32_service_type":"4"}
""");
+ private static final MultiMap GET_FILE_LIST_HEADERS = HeaderUtils.parseHeaders("""
+ Accept-Encoding: gzip, deflate
+ Cookie: uin=9000002; p_uin=9000002
+ Origin: https://qfile.qq.com
+ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
+ content-type: application/json
+ x-oidb: {"uint32_command":"0x93d4", "uint32_service_type":"1"}
+ """);
+
public QQscTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
}
+ @Override
public Future parse() {
- String jsonTemplate = """
- {"req_head":{"agent":8},"download_info":[{"batch_id":"%s","scene":{"business_type":4,"app_type":22,"scene_type":5},"index_node":{"file_uuid":"%s"},"url_type":2,"download_scene":0}],"scene_type":103}
- """;
-
client.getAbs(shareLinkInfo.getShareUrl()).send(result -> {
- if (result.succeeded()) {
- String htmlJs = result.result().bodyAsString();
- LOG.debug("获取到的HTML内容: {}", htmlJs);
- String fileUUID = getFileUUID(htmlJs);
- String fileName = extractFileNameFromTitle(htmlJs);
- if (fileName != null) {
- LOG.info("提取到的文件名: {}", fileName);
- FileInfo fileInfo = new FileInfo();
- fileInfo.setFileName(fileName);
- shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
- } else {
- LOG.warn("未能提取到文件名");
- }
- if (fileUUID != null) {
- LOG.info("提取到的文件UUID: {}", fileUUID);
- String formatted = jsonTemplate.formatted(fileUUID, fileUUID);
- JsonObject entries = new JsonObject(formatted);
- client.postAbs(API_URL)
- .putHeaders(HEADERS)
- .sendJsonObject(entries)
- .onSuccess(result2 -> {
- if (result2.statusCode() == 200) {
- JsonObject body = asJson(result2);
- LOG.debug("API响应内容: {}", body.encodePrettily());
- // {
- // "retcode": 0,
- // "cost": 132,
- // "message": "",
- // "error": {
- // "message": "",
- // "code": 0
- // },
- // "data": {
- // "download_rsp": [{
-
- // 取 download_rsp
- if (!body.containsKey("retcode") || body.getInteger("retcode") != 0) {
- promise.fail("API请求失败,错误信息: " + body.encodePrettily());
- return;
- }
- JsonArray downloadRsp = body.getJsonObject("data").getJsonArray("download_rsp");
- if (downloadRsp != null && !downloadRsp.isEmpty()) {
- String url = downloadRsp.getJsonObject(0).getString("url");
- if (fileName != null) {
- url = url + "&filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8);
- }
- promise.complete(url);
- } else {
- promise.fail("API响应中缺少 download_rsp");
- }
- } else {
- promise.fail("API请求失败,状态码: " + result2.statusCode());
- }
- }).onFailure(e -> {
- LOG.error("API请求异常", e);
- promise.fail(e);
- });
- } else {
- LOG.error("未能提取到文件UUID");
- promise.fail("未能提取到文件UUID");
- }
- } else {
+ if (result.failed()) {
LOG.error("请求失败: {}", result.cause().getMessage());
promise.fail(result.cause());
+ return;
+ }
+ String html = result.result().bodyAsString();
+ String fileName = extractFileNameFromTitle(html);
+ if (fileName != null) {
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setFileName(fileName);
+ shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
+ }
+ // 尝试用 GetFileList API 获取第一个文件的下载链接
+ String filesetId = extractFilesetId(html);
+ if (filesetId != null) {
+ fetchFileList(filesetId, "").onSuccess(fileList -> {
+ for (int i = 0; i < fileList.size(); i++) {
+ JsonObject file = fileList.getJsonObject(i);
+ if (!file.getBoolean("is_dir", false)) {
+ String physicalId = file.getJsonObject("physical").getString("id");
+ String name = file.getString("name");
+ downloadFile(physicalId, name);
+ return;
+ }
+ }
+ promise.fail("未找到可下载的文件");
+ }).onFailure(e -> {
+ LOG.warn("GetFileList 失败,回退到旧解析方式: {}", e.getMessage());
+ parseLegacy(html, fileName);
+ });
+ } else {
+ parseLegacy(html, fileName);
}
});
-
return promise.future();
}
+ @Override
+ public Future> parseFileList() {
+ Promise> resultPromise = Promise.promise();
+ String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
+
+ client.getAbs(shareLinkInfo.getShareUrl()).send(result -> {
+ if (result.failed()) {
+ resultPromise.fail(result.cause());
+ return;
+ }
+ String html = result.result().bodyAsString();
+ String filesetId = extractFilesetId(html);
+ if (filesetId == null) {
+ resultPromise.fail("无法从页面提取 filesetId");
+ return;
+ }
+ String parentId = dirId != null ? dirId : "";
+ fetchFileList(filesetId, parentId).onSuccess(fileList -> {
+ try {
+ List list = new ArrayList<>();
+ String panType = shareLinkInfo.getType();
+ for (int i = 0; i < fileList.size(); i++) {
+ JsonObject file = fileList.getJsonObject(i);
+ FileInfo fileInfo = new FileInfo();
+ String name = file.getString("name");
+ String cliFileid = file.getString("cli_fileid");
+ boolean isDir = file.getBoolean("is_dir", false);
+ String sizeStr = file.getString("file_size");
+
+ fileInfo.setFileName(name)
+ .setFileId(cliFileid)
+ .setPanType(panType)
+ .setSizeStr(sizeStr);
+
+ if (isDir) {
+ fileInfo.setFileType("folder")
+ .setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s",
+ getDomainName(),
+ URLEncoder.encode(shareLinkInfo.getShareUrl(), StandardCharsets.UTF_8),
+ cliFileid));
+ } else {
+ String physicalId = file.getJsonObject("physical").getString("id");
+ JsonObject paramJson = new JsonObject()
+ .put("fileId", physicalId)
+ .put("fileName", name)
+ .put("cliFileid", cliFileid);
+ String param = CommonUtils.urlBase64Encode(paramJson.encode());
+ fileInfo.setFileType("file")
+ .setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
+ getDomainName(), panType, param));
+ }
+ list.add(fileInfo);
+ }
+ resultPromise.complete(list);
+ } catch (Exception e) {
+ resultPromise.fail(e);
+ }
+ }).onFailure(resultPromise::fail);
+ });
+ return resultPromise.future();
+ }
+
+ @Override
+ public Future parseById() {
+ JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
+ String fileId = paramJson.getString("fileId");
+ String fileName = paramJson.getString("fileName");
+
+ Promise p = Promise.promise();
+ callBatchDownload(fileId, fileName, p);
+ return p.future();
+ }
+
+ // ========== 内部方法 ==========
+
+ /**
+ * 调用 BatchDownload API 获取单个文件的下载直链
+ */
+ private void downloadFile(String physicalId, String fileName) {
+ callBatchDownload(physicalId, fileName, promise);
+ }
+
+ private void callBatchDownload(String physicalId, String fileName, Promise p) {
+ String body = """
+ {"req_head":{"agent":8},"download_info":[{"batch_id":"%s","scene":{"business_type":4,"app_type":22,"scene_type":5},"index_node":{"file_uuid":"%s"},"url_type":2,"download_scene":0}],"scene_type":103}
+ """.formatted(physicalId, physicalId);
+
+ client.postAbs(BATCH_DOWNLOAD_API)
+ .putHeaders(BATCH_DOWNLOAD_HEADERS)
+ .sendJsonObject(new JsonObject(body))
+ .onSuccess(resp -> {
+ if (resp.statusCode() != 200) {
+ p.fail("BatchDownload 请求失败,状态码: " + resp.statusCode());
+ return;
+ }
+ JsonObject respBody = asJson(resp);
+ if (!respBody.containsKey("retcode") || respBody.getInteger("retcode") != 0) {
+ p.fail("BatchDownload 请求失败: " + respBody.encodePrettily());
+ return;
+ }
+ JsonArray downloadRsp = respBody.getJsonObject("data").getJsonArray("download_rsp");
+ if (downloadRsp == null || downloadRsp.isEmpty()) {
+ p.fail("BatchDownload 响应中缺少 download_rsp");
+ return;
+ }
+ String url = downloadRsp.getJsonObject(0).getString("url");
+ if (url != null && url.startsWith("&filename=")) {
+ p.fail("该文件已被和谐");
+ return;
+ }
+ if (fileName != null) {
+ url = url + "&filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8);
+ }
+ p.complete(url);
+ })
+ .onFailure(e -> {
+ LOG.error("BatchDownload 请求异常", e);
+ p.fail(e);
+ });
+ }
+
+ /**
+ * 调用 GetFileList API 获取指定目录下的文件列表
+ */
+ private Future fetchFileList(String filesetId, String parentId) {
+ Promise p = Promise.promise();
+ JsonObject body = new JsonObject()
+ .put("fileset_id", filesetId)
+ .put("req_infos", new JsonArray()
+ .add(new JsonObject()
+ .put("parent_id", parentId)
+ .put("req_depth", 1)
+ .put("count", 50)
+ .put("filter_condition", new JsonObject().put("file_category", 0))
+ .put("sort_conditions", new JsonArray()
+ .add(new JsonObject()
+ .put("sort_field", 0)
+ .put("sort_order", 0)))))
+ .put("support_folder_status", true);
+
+ MultiMap headers = GET_FILE_LIST_HEADERS.set("Referer", shareLinkInfo.getShareUrl());
+
+ client.postAbs(GET_FILE_LIST_API)
+ .putHeaders(headers)
+ .sendJsonObject(body)
+ .onSuccess(resp -> {
+ if (resp.statusCode() != 200) {
+ p.fail("GetFileList 请求失败,状态码: " + resp.statusCode());
+ return;
+ }
+ JsonObject respBody = asJson(resp);
+ if (respBody.getInteger("retcode", -1) != 0) {
+ p.fail("GetFileList 请求失败: " + respBody.getString("message", "未知错误"));
+ return;
+ }
+ JsonArray fileLists = respBody.getJsonObject("data").getJsonArray("file_lists");
+ if (fileLists == null || fileLists.isEmpty()) {
+ p.fail("GetFileList 响应中缺少 file_lists");
+ return;
+ }
+ JsonArray fileList = fileLists.getJsonObject(0).getJsonArray("file_list");
+ p.complete(fileList != null ? fileList : new JsonArray());
+ })
+ .onFailure(e -> {
+ LOG.error("GetFileList 请求异常", e);
+ p.fail(e);
+ });
+ return p.future();
+ }
+
+ /**
+ * 从 HTML 的 __NUXT_DATA__ 中提取 fileset_id
+ */
+ String extractFilesetId(String html) {
+ // Nuxt __NUXT_DATA__ 中 fileset_id 出现在缓存 key 的嵌套 JSON 中
+ // 直接匹配 fileset_id 后面最近的 UUID(跳过转义引号、冒号等非hex字符)
+ Pattern pattern = Pattern.compile(
+ "fileset_id[^a-f0-9]*([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})");
+ Matcher matcher = pattern.matcher(html);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ return null;
+ }
+
+ /**
+ * 旧版解析方式(兼容单文件链接,通过 HTML 字符串搜索提取 UUID)
+ */
+ private void parseLegacy(String html, String fileName) {
+ String fileUUID = getFileUUID(html);
+ if (fileUUID == null) {
+ promise.fail("未能提取到文件UUID");
+ return;
+ }
+ LOG.info("使用旧版解析,提取到的文件UUID: {}", fileUUID);
+ downloadFile(fileUUID, fileName);
+ }
+
String getFileUUID(String htmlJs) {
String keyword = "\"download_limit_status\"";
String marker = "},\"";
@@ -140,32 +319,23 @@ public class QQscTool extends PanBase {
String extracted = htmlJs.substring(quoteStart, quoteEnd);
LOG.debug("提取结果: {}", extracted);
return extracted;
- } else {
- LOG.error("未找到结束引号: {}", marker);
}
- } else {
- LOG.error("未找到标记: {} 在关键字: {} 之后", marker, keyword);
}
- } else {
- LOG.error("未找到关键字: {}", keyword);
}
return null;
}
public static String extractFileNameFromTitle(String content) {
- // 匹配和之间的内容
Pattern pattern = Pattern.compile("(.*?)");
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
String fullTitle = matcher.group(1);
- // 按 "|" 分割,取前半部分
int sepIndex = fullTitle.indexOf("|");
if (sepIndex != -1) {
return fullTitle.substring(0, sepIndex);
}
- return fullTitle; // 如果没有分隔符,就返回全部
+ return fullTitle;
}
return null;
}
}
-
diff --git a/parser/src/main/java/cn/qaiu/util/URLUtil.java b/parser/src/main/java/cn/qaiu/util/URLUtil.java
index 916a27f..a8b7bb1 100644
--- a/parser/src/main/java/cn/qaiu/util/URLUtil.java
+++ b/parser/src/main/java/cn/qaiu/util/URLUtil.java
@@ -2,6 +2,9 @@ package cn.qaiu.util;
import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@@ -10,6 +13,8 @@ import java.util.Map;
public class URLUtil {
+ private static final Logger LOGGER = LoggerFactory.getLogger(URLUtil.class);
+
private final Map queryParams = new HashMap<>();
// 构造函数,传入URL并解析参数
@@ -31,7 +36,7 @@ public class URLUtil {
}
}
} catch (Exception e) {
- e.printStackTrace();
+ LOGGER.error("URL解析失败: {}", url, e);
}
}
diff --git a/web-front/public/index.html b/web-front/public/index.html
index 35d7ba4..2d60851 100644
--- a/web-front/public/index.html
+++ b/web-front/public/index.html
@@ -10,7 +10,7 @@
-
+