添加支持 QQ闪传,微雨云,优化前端逻辑

This commit is contained in:
q
2025-08-11 13:14:43 +08:00
parent 8e8ab10a0f
commit e07272a5dc
12 changed files with 965 additions and 48 deletions

View File

@@ -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) {

View File

@@ -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/(.+/)?(?<KEY>.+)"),
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/(.+/)?(?<KEY>.+)"),
"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/(?<KEY>.+)"),
"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/(?<KEY>.+)"),
"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/(?<KEY>.+)(.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/(?<KEY>.+)(.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/(?<KEY>.+)"),
"http://163cn.tv/{shareKey}",
"https://163cn.tv/{shareKey}",
MnesTool.class),
// https://music.163.com/#/song?id=xxx
MNE("网易云音乐歌曲详情",

View File

@@ -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<String> 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<List<FileInfo>> parseFileList() {
// Promise<List<FileInfo>> 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<FileInfo> 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<String> parseById() {
// return super.parseById();
// }
}

View File

@@ -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闪传 <br>
* 只能客户端上传 支持Android QQ 9.2.5, MACOS QQ 6.9.78可生成分享链接通过浏览器下载支持超大文件有效期默认7天暂时没找到续期方法。<br>
*/
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<String> 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) {
// 匹配<title>和</title>之间的内容
Pattern pattern = Pattern.compile("<title>(.*?)</title>");
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;
}
}