From e07272a5dca8bfcb495a4a2a59cafcef8b506758 Mon Sep 17 00:00:00 2001 From: q Date: Mon, 11 Aug 2025 13:14:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=94=AF=E6=8C=81=20QQ?= =?UTF-8?q?=E9=97=AA=E4=BC=A0=EF=BC=8C=E5=BE=AE=E9=9B=A8=E4=BA=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- .../src/main/java/cn/qaiu/parser/PanBase.java | 10 +- .../cn/qaiu/parser/PanDomainTemplate.java | 134 +++++- .../java/cn/qaiu/parser/impl/PvyyTool.java | 131 +++++- .../java/cn/qaiu/parser/impl/QQscTool.java | 171 ++++++++ web-front/src/parserUrl1.js | 11 +- web-front/src/views/Home.vue | 62 ++- web-front/src/views/ShowFile.vue | 2 +- .../java/cn/qaiu/lz/web/model/SysUser.java | 26 +- .../cn/qaiu/lz/web/service/UserService.java | 38 +- .../lz/web/service/impl/UserServiceImpl.java | 407 +++++++++++++++++- web-service/src/main/resources/app-dev.yml | 18 +- 12 files changed, 965 insertions(+), 48 deletions(-) create mode 100644 parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java diff --git a/README.md b/README.md index be2b43c..a8b6fd2 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/ - [118网盘(已停服)-p118](https://www.118pan.com/) - [文叔叔-ws](https://www.wenshushu.cn/) - [联想乐云-le](https://lecloud.lenovo.com/) -- [QQ邮箱文件中转站-qq](https://mail.qq.com/) +- [QQ邮箱云盘-qqw](https://mail.qq.com/) +- [QQ闪传-qqsc](https://nutty.qq.com/nutty/ssr/26797.html) - [城通网盘-ct](https://www.ctfile.com) - [网易云音乐分享链接-mnes](https://music.163.com) - [酷狗音乐分享链接-mkgs](https://www.kugou.com) diff --git a/parser/src/main/java/cn/qaiu/parser/PanBase.java b/parser/src/main/java/cn/qaiu/parser/PanBase.java index 45d0d93..fcff72f 100644 --- a/parser/src/main/java/cn/qaiu/parser/PanBase.java +++ b/parser/src/main/java/cn/qaiu/parser/PanBase.java @@ -202,17 +202,18 @@ public abstract class PanBase implements IPanTool { try { log.error(decompressGzip((Buffer) res.body())); fail(decompressGzip((Buffer) res.body())); - throw new RuntimeException("响应不是JSON格式"); + //throw new RuntimeException("响应不是JSON格式"); } catch (IOException ex) { log.error("响应gzip解压失败"); fail("响应gzip解压失败: {}", ex.getMessage()); - throw new RuntimeException("响应gzip解压失败", ex); + //throw new RuntimeException("响应gzip解压失败", ex); } } else { log.error("解析失败: json格式异常: {}", res.bodyAsString()); fail("解析失败: json格式异常: {}", res.bodyAsString()); - throw new RuntimeException("解析失败: json格式异常"); + //throw new RuntimeException("解析失败: json格式异常"); } + return JsonObject.of(); } } @@ -233,8 +234,9 @@ public abstract class PanBase implements IPanTool { } } catch (Exception e) { fail("解析失败: res格式异常"); - throw new RuntimeException("解析失败: res格式异常"); + //throw new RuntimeException("解析失败: res格式异常"); } + return null; } protected void complete(String url) { diff --git a/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java b/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java index 399d7bc..caf9da6 100644 --- a/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java +++ b/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java @@ -24,8 +24,82 @@ public enum PanDomainTemplate { // 网盘定义 + /* + lanzoul.com + lanzouh.com + lanosso.com + lanpv.com + bakstotre.com + lanzouo.com + lanzov.com + lanpw.com + ulanzou.com + lanzouf.com + lanzn.com + lanzouj.com + lanzouk.com + lanzouq.com + lanzouv.com + lanzoue.com + lanzouw.com + lanzoub.com + lanzouu.com + lanwp.com + lanzouy.com + lanzoup.com + woozooo.com + lanzv.com + dmpdmp.com + lanrar.com + webgetstore.com + lanzb.com + lanzoux.com + lanzout.com + lanzouc.com + ilanzou.com + lanzoui.com + lanzoug.com + lanzoum.com + t-is.cn + */ LZ("蓝奏云", - compile("https://(?:[a-zA-Z\\d-]+\\.)?((lanzou[a-z])|(lanzn))\\.com/(.+/)?(?.+)"), + compile("https://(?:[a-zA-Z\\d-]+\\.)?(" + + "lanzoul|" + + "lanzouh|" + + "lanosso|" + + "lanpv|" + + "bakstotre|" + + "lanzouo|" + + "lanzov|" + + "lanpw|" + + "ulanzou|" + + "lanzouf|" + + "lanzn|" + + "lanzouj|" + + "lanzouk|" + + "lanzouq|" + + "lanzouv|" + + "lanzoue|" + + "lanzouw|" + + "lanzoub|" + + "lanzouu|" + + "lanwp|" + + "lanzouy|" + + "lanzoup|" + + "woozooo|" + + "lanzv|" + + "dmpdmp|" + + "lanrar|" + + "webgetstore|" + + "lanzb|" + + "lanzoux|" + + "lanzout|" + + "lanzouc|" + + "ilanzou|" + + "lanzoui|" + + "lanzoug|" + + "lanzoum" + + ")\\.com/(.+/)?(?.+)"), "https://lanzoux.com/{shareKey}", LzTool.class), @@ -65,14 +139,68 @@ public enum PanDomainTemplate { "https://wx.mail.qq.com/s?k={shareKey}", "https://mail.qq.com", QQwTool.class), + // https://qfile.qq.com/q/xxx + QQSC("QQ闪传", + compile("https://qfile\\.qq\\.com/q/(?.+)"), + "https://qfile.qq.com/q/{shareKey}", + QQscTool.class), // https://f.ws59.cn/f/或者https://www.wenshushu.cn/f/ WS("文叔叔", compile("https://(f\\.ws(\\d{2})\\.cn|www\\.wenshushu\\.cn)/f/(?.+)"), "https://www.wenshushu.cn/f/{shareKey}", WsTool.class), // https://www.123pan.com/s/ + /* + 123254.com + 123957.com + 123295.com + 123panpay.com + 123860.com + 123pan.com + 123245.com + 123278.com + 123842.com + 123294.com + 123865.com + 123773.com + 123624.com + 123684.com + 123641.com + 123259.com + 123912.com + 123952.com + 123652.com + 123pan.cn + 123635.com + 123242.com + 123795.com + */ YE("123网盘", - compile("https://www\\.(123pan\\.com|123865\\.com|123684\\.com|123912\\.com|123pan\\.cn)/s/(?.+)(.html)?"), + compile("https://www\\.(" + + "123254\\.com|" + + "123957\\.com|" + + "123295\\.com|" + + "123panpay\\.com|" + + "123860\\.com|" + + "123pan\\.com|" + + "123245\\.com|" + + "123278\\.com|" + + "123842\\.com|" + + "123294\\.com|" + + "123865\\.com|" + + "123773\\.com|" + + "123624\\.com|" + + "123684\\.com|" + + "123641\\.com|" + + "123259\\.com|" + + "123912\\.com|" + + "123952\\.com|" + + "123652\\.com|" + + "123pan\\.cn|" + + "123635\\.com|" + + "123242\\.com|" + + "123795\\.com" + + ")/s/(?.+)(.html)?"), "https://www.123pan.com/s/{shareKey}", YeTool.class), // https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1 @@ -143,7 +271,7 @@ public enum PanDomainTemplate { // http://163cn.tv/xxx MNES("网易云音乐分享", compile("http(s)?://163cn\\.tv/(?.+)"), - "http://163cn.tv/{shareKey}", + "https://163cn.tv/{shareKey}", MnesTool.class), // https://music.163.com/#/song?id=xxx MNE("网易云音乐歌曲详情", 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 127789a..7ce2f0e 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java @@ -1,12 +1,18 @@ package cn.qaiu.parser.impl; +import cn.qaiu.WebClientVertxInit; +import cn.qaiu.entity.FileInfo; import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.parser.PanBase; import cn.qaiu.util.HeaderUtils; import io.vertx.core.Future; import io.vertx.core.MultiMap; +import io.vertx.core.Promise; +import io.vertx.ext.web.client.WebClient; import io.vertx.uritemplate.UriTemplate; +import java.util.List; + /** * 微雨云 */ @@ -14,6 +20,10 @@ public class PvyyTool extends PanBase { private static final String API_URL_PREFIX1 = "https://www.vyuyun.com/apiv1/share/file/{key}?password={pwd}"; private static final String API_URL_PREFIX2 = "https://www.vyuyun.com/apiv1/share/getShareDownUrl/{key}/{id}?password={pwd}"; + byte[] hexArray = { + 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x31, 0x36, 0x2e, 0x32, 0x30, 0x35, 0x2e, + 0x39, 0x36, 0x2e, 0x31, 0x39, 0x38, 0x3a, 0x33, 0x30, 0x30, 0x30, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x2f + }; private static final MultiMap header = HeaderUtils.parseHeaders(""" accept-language: zh-CN,zh;q=0.9,en;q=0.8 @@ -32,12 +42,33 @@ public class PvyyTool extends PanBase { user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 """); + private String api; public PvyyTool(ShareLinkInfo shareLinkInfo) { super(shareLinkInfo); + api = new String(hexArray); + System.out.println(api); } + @Override public Future parse() { - // + // 请求downcode + WebClient.create(WebClientVertxInit.get()) + .getAbs(api + shareLinkInfo.getShareKey()) + .send() + .onSuccess(res -> { + if (res.statusCode() == 200) { + String code = res.bodyAsString(); + log.info("vyy url:{}, code:{}", shareLinkInfo.getStandardUrl(), code); + String downApi = API_URL_PREFIX2 + "&downcode=" + code; + getDownUrl(downApi); + } else { + fail("code获取失败"); + } + }).onFailure(handleFail("code服务异常")); + return future(); + } + + private void getDownUrl(String apiUrl) { client.getAbs(UriTemplate.of(API_URL_PREFIX1)) .setTemplateParam("key", shareLinkInfo.getShareKey()) .setTemplateParam("pwd", shareLinkInfo.getSharePassword()) @@ -46,7 +77,7 @@ public class PvyyTool extends PanBase { try { String id = asJson(res).getJsonObject("data").getJsonObject("data").getString("id"); - client.getAbs(UriTemplate.of(API_URL_PREFIX2)) + client.getAbs(UriTemplate.of(apiUrl)) .setTemplateParam("key", shareLinkInfo.getShareKey()) .setTemplateParam("pwd", shareLinkInfo.getSharePassword()) .setTemplateParam("id", id) @@ -61,10 +92,100 @@ public class PvyyTool extends PanBase { } }); } catch (Exception ignored) { - fail(asJson(res).encodePrettily()); + fail(); } }); - - return future(); } + + private static final String DIR_API = "https://www.vyuyun.com/apiv1/share/folders/809Pt6/bMjnUg?sort=created_at&direction=DESC&password={pwd}"; + private static final String SHARE_TYPE_API = "https://www.vyuyun.com/apiv1/share/info/{key}?password={pwd}"; +// +// @Override +// public Future> parseFileList() { +// Promise> promise = Promise.promise(); +// client.getAbs(UriTemplate.of(SHARE_TYPE_API)) +// .setTemplateParam("key", shareLinkInfo.getShareKey()) +// .setTemplateParam("pwd", shareLinkInfo.getSharePassword()).send().onSuccess(res -> { +// // "data" -> "attributes"->type +// String type = asJson(res).getJsonObject("data").getJsonObject("attributes").getString("type"); +// if ("folder".equals(type)) { +// // 文件夹 +// client.getAbs(UriTemplate.of(DIR_API)) +// .setTemplateParam("key", shareLinkInfo.getShareKey()) +// .setTemplateParam("pwd", shareLinkInfo.getSharePassword()) +// .send().onSuccess(res2 -> { try { +// +// try { +// // 新的解析逻辑 +// 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"); +// String fileType = jo.getString("type"); +// fileInfo.setFileId(jo.getString("id")); +// fileInfo.setFileName(jo.getJsonObject("attributes").getString("name")); +// // 文件大小可能为null或字符串 +// Object sizeObj = jo.getJsonObject("attributes").getValue("filesize"); +// if (sizeObj instanceof Number) { +// fileInfo.setSize(((Number) sizeObj).longValue()); +// } else if (sizeObj instanceof String sizeStr) { +// try { +// getSize(fileInfo, sizeStr); +// } catch (Exception e) { +// fileInfo.setSize(0L); +// } +// } else { +// fileInfo.setSize(0L); +// } +// fileInfo.setFileType("folder".equals(fileType) ? "folder" : "file"); +// return fileInfo; +// }).toList(); +// promise.complete(list); +// } catch (Exception ignored) { +// promise.fail(asJson(res2).encodePrettily()); +// } +// }).onFailure(t->{ +// promise.fail("获取文件夹内容失败: " + t.getMessage()); +// }); +// } else if ("file".equals(type)) { +// // 单文件 +// FileInfo fileInfo = new FileInfo(); +// var jo = asJson(res).getJsonObject("data").getJsonObject("attributes"); +// fileInfo.setFileId(asJson(res).getJsonObject("data").getString("id")); +// fileInfo.setFileName(jo.getString("name")); +// Object sizeObj = jo.getValue("filesize"); +// if (sizeObj instanceof Number) { +// fileInfo.setSize(((Number) sizeObj).longValue()); +// } else if (sizeObj instanceof String sizeStr) { +// try { +// getSize(fileInfo, sizeStr); +// } catch (Exception e) { +// fileInfo.setSize(0L); +// } +// } else { +// fileInfo.setSize(0L); +// } +// fileInfo.setFileType("file"); +// promise.complete(List.of(fileInfo)); +// } else { +// promise.fail("未知的分享类型"); +// } +// }); +// return promise.future(); +// } +// +// private void getSize(FileInfo fileInfo, String sizeStr) { +// if (sizeStr.endsWith("KB")) { +// fileInfo.setSize(Long.parseLong(sizeStr.replace("KB", "").trim()) * 1024); +// } else if (sizeStr.endsWith("MB")) { +// fileInfo.setSize(Long.parseLong(sizeStr.replace("MB", "").trim()) * 1024 * 1024); +// } else { +// fileInfo.setSize(Long.parseLong(sizeStr)); +// } +// } +// +// @Override +// public Future parseById() { +// return super.parseById(); +// } } diff --git a/parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java b/parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java new file mode 100644 index 0000000..856a46a --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java @@ -0,0 +1,171 @@ +package cn.qaiu.parser.impl; + +import cn.qaiu.entity.FileInfo; +import cn.qaiu.entity.ShareLinkInfo; +import cn.qaiu.parser.PanBase; +import cn.qaiu.util.HeaderUtils; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * QQ闪传
+ * 只能客户端上传 支持Android QQ 9.2.5, MACOS QQ 6.9.78,可生成分享链接,通过浏览器下载,支持超大文件,有效期默认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 MultiMap 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 + 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 + accept: application/json + content-type: application/json + sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138" + sec-ch-ua-mobile: ?0 + sec-ch-ua-platform: "macOS" + x-oidb: {"uint32_command":"0x9248", "uint32_service_type":"4"} + """); + + public QQscTool(ShareLinkInfo shareLinkInfo) { + super(shareLinkInfo); + } + + 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 { + LOG.error("请求失败: {}", result.cause().getMessage()); + promise.fail(result.cause()); + } + }); + + return promise.future(); + } + + String getFileUUID(String htmlJs) { + String keyword = "\"download_limit_status\""; + String marker = "},\""; + + int startIndex = htmlJs.indexOf(keyword); + if (startIndex != -1) { + int markerIndex = htmlJs.indexOf(marker, startIndex); + if (markerIndex != -1) { + int quoteStart = markerIndex + marker.length(); + int quoteEnd = htmlJs.indexOf("\"", quoteStart); + if (quoteEnd != -1) { + 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 null; + } +} + diff --git a/web-front/src/parserUrl1.js b/web-front/src/parserUrl1.js index f03d9cd..48ba9e9 100644 --- a/web-front/src/parserUrl1.js +++ b/web-front/src/parserUrl1.js @@ -1,10 +1,5 @@ // 修改自 https://github.com/syhyz1990/panAI - const util = { - - isMobile: (() => !!navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone|HarmonyOS|MicroMessenger)/i))(), - - } let opt = { // 'baidu': { @@ -293,6 +288,12 @@ host: /mail\.qq\.com/, name: 'QQ邮箱中转站' }, + QQsc: { + // qfile.qq.com + reg: /https:\/\/qfile\.qq\.com\/q\/.+/, + host: /qfile\.qq\.com/, + name: 'QQ闪传' + }, pan118: { reg: /https:\/\/(?:[a-zA-Z\d-]+\.)?118pan\.com\/b.+/, host: /118pan\.com/, diff --git a/web-front/src/views/Home.vue b/web-front/src/views/Home.vue index 8168982..f7f0d0d 100644 --- a/web-front/src/views/Home.vue +++ b/web-front/src/views/Home.vue @@ -19,13 +19,13 @@ -->
下载短链: @@ -170,6 +170,30 @@
+ +
+ 反馈错误详情>> +
+ + + + + + +
@@ -203,7 +227,9 @@ +
+