From 0978186679c9420a7a0eb325ed04b5480fbb0760 Mon Sep 17 00:00:00 2001 From: yukaidi Date: Fri, 29 May 2026 14:21:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=8A=9F=E8=83=BD=E4=B8=8E?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QQscTool: 支持多文件和目录解析,通过 GetFileList API 实现递归目录导航 - Home: 从粘贴文本中自动提取分享链接 - DirectoryTree: 目录浏览添加复制直链按钮 - domainName 改为可选,未配置时自动从请求地址推断 - 统一版本号管理,GitHub URL 构建时自动从 git remote origin 识别 - vue.config.js 添加前端构建配置,sync-version.js 构建时同步版本号 --- README.md | 6 +- .../java/cn/qaiu/db/ddl/CreateDatabase.java | 2 +- .../main/java/cn/qaiu/db/ddl/CreateTable.java | 52 +-- .../src/main/java/cn/qaiu/vx/core/Deploy.java | 9 +- .../java/cn/qaiu/vx/core/util/CommonUtil.java | 2 +- .../java/cn/qaiu/vx/core/util/ConfigUtil.java | 35 +- .../cn/qaiu/vx/core/util/ReflectionUtil.java | 11 +- .../vx/core/verticle/HttpProxyVerticle.java | 4 +- parser/README.md | 8 +- parser/doc/CUSTOM_PARSER_GUIDE.md | 2 +- parser/doc/CUSTOM_PARSER_QUICKSTART.md | 2 +- .../java/cn/qaiu/parser/impl/QQscTool.java | 332 +++++++++++++----- .../src/main/java/cn/qaiu/util/URLUtil.java | 7 +- web-front/public/index.html | 2 +- web-front/scripts/sync-version.js | 23 ++ web-front/vue.config.js | 28 ++ .../cn/qaiu/lz/web/controller/ServerApi.java | 23 +- web-service/src/main/resources/app-dev.yml | 5 +- 18 files changed, 418 insertions(+), 135 deletions(-) create mode 100644 web-front/scripts/sync-version.js diff --git a/README.md b/README.md index 95648e3..578f069 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # 一款网盘分享链接云解析快速下载服务 QQ交流群:1017480890

- + - + AtomGit @@ -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 @@ - +