From 0fd78defcbf7d3714e74f2eff002fbe98d32bc95 Mon Sep 17 00:00:00 2001 From: qaiu Date: Wed, 3 Jun 2026 09:45:58 +0000 Subject: [PATCH] =?UTF-8?q?feat(ct):=20=E5=8D=87=E7=BA=A7=E5=9F=8E?= =?UTF-8?q?=E9=80=9A=E7=BD=91=E7=9B=98=E6=8E=A5=E5=8F=A3=E5=B9=B6=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E7=9B=AE=E5=BD=95=E5=88=86=E4=BA=AB=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除token参数,新增url参数至API1请求 - API2新增start_time/wait_seconds/verifycode/share_id/acheck=1参数 - parse()中从API1响应提取FileInfo(file_name/file_id/file_size/file_time/username) - 新增parseFileList()实现目录分享解析,通过getdir.php+file_list API获取文件列表 - 新增CTD枚举项匹配城通网盘目录分享链接(/d/路径) --- .../cn/qaiu/parser/PanDomainTemplate.java | 8 +- .../main/java/cn/qaiu/parser/impl/CtTool.java | 200 ++++++++++++++++-- 2 files changed, 190 insertions(+), 18 deletions(-) diff --git a/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java b/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java index b9f2657..f66b4fc 100644 --- a/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java +++ b/parser/src/main/java/cn/qaiu/parser/PanDomainTemplate.java @@ -249,7 +249,13 @@ public enum PanDomainTemplate { CT("城通网盘", compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" + "(?[0-9a-zA-Z_-]+)(\\?p=(?\\w+))?"), - "https://474b.com/file/{shareKey}", + "https://ctfile.com/file/{shareKey}", + CtTool.class), + // https://url94.ctfile.com/d/64115194-164803691-48508c?p=7609&d=164803691&fk=decb36 + CTD("城通网盘-目录", + compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/d/" + + "(?[0-9a-zA-Z_-]+)(\\?p=(?\\w+))?"), + "https://ctfile.com/d/{shareKey}", CtTool.class), // https://www.vyuyun.com/s/QMa6ie?password=I4KG7H // https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H 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 b3d7f5c..b075513 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/CtTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/CtTool.java @@ -1,16 +1,23 @@ package cn.qaiu.parser.impl; +import cn.qaiu.entity.FileInfo; import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.parser.PanBase; -import cn.qaiu.util.RandomStringGenerator; +import cn.qaiu.util.FileSizeConverter; import io.vertx.core.Future; +import io.vertx.core.Promise; import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.client.HttpRequest; import io.vertx.uritemplate.UriTemplate; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * 诚通网盘 @@ -18,19 +25,34 @@ import java.util.Map; public class CtTool extends PanBase { private static final String API_URL_PREFIX = "https://webapi.ctfile.com"; - // https://webapi.ctfile.com/getfile.php?path=f&f=55050874-1246660795-6464f6& - // passcode=7548&token=30wiijxs1fzhb6brw0p9m6&r=0.5885881231735761& - // ref=&url=https%3A%2F%2F474b.com%2Ff%2F55050874-1246660795-6464f6%3Fp%3D7548 + // https://webapi.ctfile.com/getfile.php?path=f&f=64115194-17569800420720-06c697& + // passcode=7609&r=0.6611183001986635&ref=&url=https%3A%2F%2Furl94.ctfile.com%2Ff%2F64115194-17569800420720-06c697%3Fp%3D7609 private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" + - "&f={shareKey}&passcode={pwd}&token={token}&r={rand}&ref="; + "&f={shareKey}&passcode={pwd}&r={rand}&ref=&url={url}"; - //https://webapi.ctfile.com/get_file_url.php?uid=55050874&fid=1246660795&folder_id=0& - // file_chk=054bc20461f5c63ff82015b9e69fb7fc&mb=1&token=30wiijxs1fzhb6brw0p9m6&app=0& - // acheck=1&verifycode=&rd=0.965929071503574 + // https://webapi.ctfile.com/get_file_url.php?uid=64115194&fid=17569800420720&folder_id=0& + // share_id=&file_chk=af5c8757a49cbc69a557eb3da59b246c&start_time=1780471868&wait_seconds=0& + // mb=0&app=0&acheck=1&verifycode=1780471868.2951fe63abedf36ec02f34ed5711ce70&rd=0.36350981353622636 private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" + - "uid={uid}&fid={fid}&folder_id=0&file_chk={file_chk}&mb=0&token={token}&app=0&acheck=0&verifycode=" + - "&rd={rand}"; + "uid={uid}&fid={fid}&folder_id=0&share_id=&file_chk={file_chk}" + + "&start_time={start_time}&wait_seconds={wait_seconds}&mb=0&app=0&acheck=1" + + "&verifycode={verifycode}&rd={rand}"; + // https://webapi.ctfile.com/getdir.php?path=d&d=64115194-164803691-48508c& + // folder_id=164803691&fk=decb36&passcode=7609&r=0.23...&ref=&url=https://url94.ctfile.com/d/... + private static final String API_GETDIR = API_URL_PREFIX + "/getdir.php?path={path}" + + "&d={shareKey}&folder_id={folder_id}&fk={fk}&passcode={pwd}&r={rand}&ref=&url={url}"; + + // DataTables参数,用于获取目录文件列表 + private static final String FILE_LIST_PARAMS = "&sEcho=1&iColumns=4&sColumns=%2C%2C%2C" + + "&iDisplayStart=0&iDisplayLength=500&mDataProp_0=0&mDataProp_1=1&mDataProp_2=2&mDataProp_3=3" + + "&iSortCol_0=3&sSortDir_0=desc&iSortingCols=1"; + + // 文件列表HTML解析正则 + private static final Pattern FILE_ID_PATTERN = Pattern.compile("value=\"f(\\d+)\""); + private static final Pattern FILE_HREF_PATTERN = Pattern.compile("href=\"#/f/([^\"]+)\""); + private static final Pattern FILE_NAME_PATTERN = Pattern.compile(">([^<]+)"); + private static final Pattern FILE_ICON_PATTERN = Pattern.compile("alt=\"([^\"]+)\""); /** * 子类重写此构造方法不需要添加额外逻辑 @@ -57,7 +79,6 @@ public class CtTool extends PanBase { } String[] split = shareKey.split("-"); String uid = split[0], fid = split[1]; - String token = RandomStringGenerator.generateRandomString(); // 获取url path int i1 = shareLinkInfo.getShareUrl().indexOf("com/"); int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/"); @@ -67,8 +88,8 @@ public class CtTool extends PanBase { .setTemplateParam("path", path) .setTemplateParam("shareKey", shareKey) .setTemplateParam("pwd", shareLinkInfo.getSharePassword()) - .setTemplateParam("token", token) - .setTemplateParam("r", Math.random() + ""); + .setTemplateParam("rand", String.valueOf(Math.random())) + .setTemplateParam("url", shareLinkInfo.getShareUrl()); bufferHttpRequest1 .send().onSuccess(res -> { @@ -77,23 +98,40 @@ public class CtTool extends PanBase { var fileJson = resJson.getJsonObject("file"); if (fileJson.containsKey("file_chk")) { var file_chk = fileJson.getString("file_chk"); + String startTime = fileJson.getValue("start_time").toString(); + String waitSeconds = fileJson.getValue("wait_seconds").toString(); + String verifycode = fileJson.getString("verifycode"); + + // 提取文件信息并存储 + FileInfo fileInfo = new FileInfo() + .setFileName(fileJson.getString("file_name")) + .setFileId(String.valueOf(fileJson.getLong("file_id", 0L))) + .setSizeStr(fileJson.getString("file_size")) + .setCreateTime(fileJson.getString("file_time")) + .setCreateBy(fileJson.getString("username")) + .setFileType("file") + .setPanType(shareLinkInfo.getType()); + shareLinkInfo.getOtherParam().put("fileInfo", fileInfo); + HttpRequest bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2)) .setTemplateParam("uid", uid) .setTemplateParam("fid", fid) .setTemplateParam("file_chk", file_chk) - .setTemplateParam("token", token) - .setTemplateParam("rd", Math.random() + ""); + .setTemplateParam("start_time", startTime) + .setTemplateParam("wait_seconds", waitSeconds) + .setTemplateParam("verifycode", verifycode) + .setTemplateParam("rand", String.valueOf(Math.random())); bufferHttpRequest2 .send().onSuccess(res2 -> { JsonObject resJson2 = asJson(res2); if (resJson2.containsKey("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 { @@ -109,4 +147,132 @@ public class CtTool extends PanBase { }).onFailure(handleFail(bufferHttpRequest1.queryParams().toString())); return promise.future(); } + + @Override + public Future> parseFileList() { + Promise> listPromise = Promise.promise(); + + final String shareKey = shareLinkInfo.getShareKey(); + final String shareUrl = shareLinkInfo.getShareUrl(); + final String pwd = shareLinkInfo.getSharePassword(); + + // shareKey格式: uid-folder_id-hash (例如 64115194-164803691-48508c) + String[] split = shareKey.split("-"); + if (split.length < 2) { + listPromise.fail(baseMsg() + " shareKey格式不正确: " + shareKey); + return listPromise.future(); + } + String folderId = split[1]; + + // 从分享URL中提取fk参数 + String fk = extractQueryParam(shareUrl, "fk"); + + // 从URL中提取path (例如从 "https://url94.ctfile.com/d/xxx?p=..." 中提取 "d") + int comIdx = shareUrl.indexOf("com/"); + int qIdx = shareUrl.indexOf('?'); + String pathAndKey = qIdx > 0 ? shareUrl.substring(comIdx + 4, qIdx) : shareUrl.substring(comIdx + 4); + int slashIdx = pathAndKey.indexOf('/'); + String path = slashIdx > 0 ? pathAndKey.substring(0, slashIdx) : pathAndKey; + + clientSession.getAbs(UriTemplate.of(API_GETDIR)) + .setTemplateParam("path", path) + .setTemplateParam("shareKey", shareKey) + .setTemplateParam("folder_id", folderId) + .setTemplateParam("fk", fk != null ? fk : "") + .setTemplateParam("pwd", pwd != null ? pwd : "") + .setTemplateParam("rand", String.valueOf(Math.random())) + .setTemplateParam("url", shareUrl) + .send().onSuccess(res -> { + var resJson = asJson(res); + if (!resJson.containsKey("file")) { + listPromise.fail(baseMsg() + " 目录解析失败: " + resJson.encode()); + return; + } + var dirInfo = resJson.getJsonObject("file"); + String fileListRelUrl = dirInfo.getString("url"); + if (fileListRelUrl == null) { + listPromise.fail(baseMsg() + " 文件列表URL为空"); + return; + } + + String fileListUrl = API_URL_PREFIX + fileListRelUrl + FILE_LIST_PARAMS; + clientSession.getAbs(fileListUrl) + .send().onSuccess(res2 -> { + var listJson = asJson(res2); + JsonArray aaData = listJson.getJsonArray("aaData"); + if (aaData == null) { + listPromise.fail(baseMsg() + " 文件列表为空"); + return; + } + List fileList = new ArrayList<>(); + String panType = shareLinkInfo.getType(); + for (int i = 0; i < aaData.size(); i++) { + var row = aaData.getJsonArray(i); + try { + String checkboxHtml = row.getString(0); + String nameCellHtml = row.getString(1); + String sizeStr = row.getString(2).trim(); + + // 从checkbox HTML中提取文件ID + String fileId = null; + Matcher idMatcher = FILE_ID_PATTERN.matcher(checkboxHtml); + if (idMatcher.find()) fileId = idMatcher.group(1); + + // 从文件名单元格HTML中提取临时分享key + String fileShareKey = null; + Matcher hrefMatcher = FILE_HREF_PATTERN.matcher(nameCellHtml); + if (hrefMatcher.find()) fileShareKey = hrefMatcher.group(1); + + // 提取文件名 + String fileName = null; + Matcher nameMatcher = FILE_NAME_PATTERN.matcher(nameCellHtml); + if (nameMatcher.find()) fileName = nameMatcher.group(1).trim(); + + // 提取文件图标/类型 + String fileIcon = null; + Matcher iconMatcher = FILE_ICON_PATTERN.matcher(nameCellHtml); + if (iconMatcher.find()) fileIcon = iconMatcher.group(1); + + if (fileName == null || fileShareKey == null) continue; + + long sizeBytes = 0; + try { + sizeBytes = FileSizeConverter.convertToBytes(sizeStr); + } catch (Exception ignored) {} + + FileInfo fileInfo = new FileInfo() + .setFileName(fileName) + .setFileId(fileId) + .setSizeStr(sizeStr) + .setSize(sizeBytes) + .setFileType(fileIcon) + .setFileIcon(fileIcon) + .setPanType(panType) + .setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", + getDomainName(), panType, fileShareKey)); + fileList.add(fileInfo); + } catch (Exception e) { + log.warn("解析文件行失败: {}", e.getMessage()); + } + } + listPromise.complete(fileList); + }).onFailure(listPromise::fail); + }).onFailure(listPromise::fail); + + return listPromise.future(); + } + + private String extractQueryParam(String url, String paramName) { + if (url == null) return null; + int qIdx = url.indexOf('?'); + if (qIdx < 0) return null; + String query = url.substring(qIdx + 1); + for (String param : query.split("&")) { + int eqIdx = param.indexOf('='); + if (eqIdx > 0 && param.substring(0, eqIdx).equals(paramName)) { + return param.substring(eqIdx + 1); + } + } + return null; + } }