mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 12:23:03 +00:00
- [汽水音乐-qishui_music](https://music.douyin.com/qishui/)
- [咪咕音乐-migu](https://music.migu.cn/) - [一刻相册-baidu_photo](https://photo.baidu.com/)
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a>
|
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a>
|
||||||
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
|
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
|
||||||
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.6-blue?style=flat"></a>
|
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.22-blue?style=flat"></a>
|
||||||
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
||||||
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
||||||
</p>
|
</p>
|
||||||
@@ -73,11 +73,13 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
|
|||||||
- [酷狗音乐分享链接-mkgs](https://www.kugou.com)
|
- [酷狗音乐分享链接-mkgs](https://www.kugou.com)
|
||||||
- [酷我音乐分享链接-mkws](https://kuwo.cn)
|
- [酷我音乐分享链接-mkws](https://kuwo.cn)
|
||||||
- [QQ音乐分享链接-mqqs](https://y.qq.com)
|
- [QQ音乐分享链接-mqqs](https://y.qq.com)
|
||||||
- 咪咕音乐分享链接(开发中)
|
|
||||||
- [Cloudreve自建网盘-ce](https://github.com/cloudreve/Cloudreve)
|
- [Cloudreve自建网盘-ce](https://github.com/cloudreve/Cloudreve)
|
||||||
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
|
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
|
||||||
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
|
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
|
||||||
- [WPS云文档-pwps](https://www.kdocs.cn/)
|
- [WPS云文档-pwps](https://www.kdocs.cn/)
|
||||||
|
- [汽水音乐-qishui_music](https://music.douyin.com/qishui/)
|
||||||
|
- [咪咕音乐-migu](https://music.migu.cn/)
|
||||||
|
- [一刻相册-baidu_photo](https://photo.baidu.com/)
|
||||||
- Google云盘-pgd
|
- Google云盘-pgd
|
||||||
- Onedrive-pod
|
- Onedrive-pod
|
||||||
- Dropbox-pdp
|
- Dropbox-pdp
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<groupId>cn.qaiu</groupId>
|
<groupId>cn.qaiu</groupId>
|
||||||
<artifactId>parser</artifactId>
|
<artifactId>parser</artifactId>
|
||||||
<version>10.2.1</version>
|
<version>10.2.3</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>cn.qaiu:parser</name>
|
<name>cn.qaiu:parser</name>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import io.vertx.core.Future;
|
|||||||
import io.vertx.core.MultiMap;
|
import io.vertx.core.MultiMap;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.buffer.Buffer;
|
import io.vertx.core.buffer.Buffer;
|
||||||
import io.vertx.core.json.JsonArray;
|
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import io.vertx.core.net.ProxyOptions;
|
import io.vertx.core.net.ProxyOptions;
|
||||||
import io.vertx.core.net.ProxyType;
|
import io.vertx.core.net.ProxyType;
|
||||||
@@ -40,7 +39,7 @@ public class JsHttpClient {
|
|||||||
private MultiMap headers;
|
private MultiMap headers;
|
||||||
|
|
||||||
public JsHttpClient() {
|
public JsHttpClient() {
|
||||||
this.client = WebClient.create(WebClientVertxInit.get());
|
this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());;
|
||||||
this.clientSession = WebClientSession.create(client);
|
this.clientSession = WebClientSession.create(client);
|
||||||
this.headers = MultiMap.caseInsensitiveMultiMap();
|
this.headers = MultiMap.caseInsensitiveMultiMap();
|
||||||
// 设置默认的Accept-Encoding头以支持压缩响应
|
// 设置默认的Accept-Encoding头以支持压缩响应
|
||||||
@@ -264,7 +263,13 @@ public class JsHttpClient {
|
|||||||
Promise<HttpResponse<Buffer>> promise = Promise.promise();
|
Promise<HttpResponse<Buffer>> promise = Promise.promise();
|
||||||
Future<HttpResponse<Buffer>> future = executor.execute();
|
Future<HttpResponse<Buffer>> future = executor.execute();
|
||||||
|
|
||||||
future.onComplete(promise);
|
future.onComplete(result -> {
|
||||||
|
if (result.succeeded()) {
|
||||||
|
promise.complete(result.result());
|
||||||
|
} else {
|
||||||
|
promise.fail(result.cause());
|
||||||
|
}
|
||||||
|
}).onFailure(Throwable::printStackTrace);
|
||||||
|
|
||||||
// 等待响应完成(最多30秒)
|
// 等待响应完成(最多30秒)
|
||||||
HttpResponse<Buffer> response = promise.future().toCompletionStage()
|
HttpResponse<Buffer> response = promise.future().toCompletionStage()
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
package cn.qaiu.parser.customjs;
|
package cn.qaiu.parser.customjs;
|
||||||
|
|
||||||
|
import cn.qaiu.WebClientVertxInit;
|
||||||
import cn.qaiu.entity.FileInfo;
|
import cn.qaiu.entity.FileInfo;
|
||||||
import cn.qaiu.entity.ShareLinkInfo;
|
import cn.qaiu.entity.ShareLinkInfo;
|
||||||
import cn.qaiu.parser.IPanTool;
|
import cn.qaiu.parser.IPanTool;
|
||||||
import cn.qaiu.parser.custom.CustomParserConfig;
|
import cn.qaiu.parser.custom.CustomParserConfig;
|
||||||
import io.vertx.core.Future;
|
import io.vertx.core.Future;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.WorkerExecutor;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.script.Invocable;
|
|
||||||
import javax.script.ScriptEngine;
|
import javax.script.ScriptEngine;
|
||||||
import javax.script.ScriptEngineManager;
|
import javax.script.ScriptEngineManager;
|
||||||
import javax.script.ScriptException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JavaScript解析器执行器
|
* JavaScript解析器执行器
|
||||||
@@ -30,13 +28,14 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
|
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
|
||||||
|
|
||||||
|
private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
|
||||||
|
|
||||||
private final CustomParserConfig config;
|
private final CustomParserConfig config;
|
||||||
private final ShareLinkInfo shareLinkInfo;
|
private final ShareLinkInfo shareLinkInfo;
|
||||||
private final ScriptEngine engine;
|
private final ScriptEngine engine;
|
||||||
private final JsHttpClient httpClient;
|
private final JsHttpClient httpClient;
|
||||||
private final JsLogger jsLogger;
|
private final JsLogger jsLogger;
|
||||||
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
||||||
private final Promise<String> promise = Promise.promise();
|
|
||||||
|
|
||||||
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
|
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
@@ -58,6 +57,7 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
* 获取ShareLinkInfo对象
|
* 获取ShareLinkInfo对象
|
||||||
* @return ShareLinkInfo对象
|
* @return ShareLinkInfo对象
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public ShareLinkInfo getShareLinkInfo() {
|
public ShareLinkInfo getShareLinkInfo() {
|
||||||
return shareLinkInfo;
|
return shareLinkInfo;
|
||||||
}
|
}
|
||||||
@@ -93,47 +93,40 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<String> parse() {
|
public Future<String> parse() {
|
||||||
try {
|
jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
|
||||||
jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
|
|
||||||
|
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||||
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
// 直接调用全局parse函数
|
// 直接调用全局parse函数
|
||||||
Object parseFunction = engine.get("parse");
|
Object parseFunction = engine.get("parse");
|
||||||
if (parseFunction == null) {
|
if (parseFunction == null) {
|
||||||
throw new RuntimeException("JavaScript代码中未找到parse函数");
|
throw new RuntimeException("JavaScript代码中未找到parse函数");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseFunction instanceof ScriptObjectMirror) {
|
if (parseFunction instanceof ScriptObjectMirror parseMirror) {
|
||||||
ScriptObjectMirror parseMirror = (ScriptObjectMirror) parseFunction;
|
|
||||||
|
|
||||||
Object result = parseMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
Object result = parseMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
||||||
|
|
||||||
if (result instanceof String) {
|
if (result instanceof String) {
|
||||||
jsLogger.info("解析成功: {}", result);
|
jsLogger.info("解析成功: {}", result);
|
||||||
promise.complete((String) result);
|
return (String) result;
|
||||||
} else {
|
} else {
|
||||||
jsLogger.error("parse方法返回值类型错误,期望String,实际: {}",
|
jsLogger.error("parse方法返回值类型错误,期望String,实际: {}",
|
||||||
result != null ? result.getClass().getSimpleName() : "null");
|
result != null ? result.getClass().getSimpleName() : "null");
|
||||||
promise.fail("parse方法返回值类型错误");
|
throw new RuntimeException("parse方法返回值类型错误");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("parse函数类型错误");
|
throw new RuntimeException("parse函数类型错误");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
jsLogger.error("JavaScript解析器执行失败", e);
|
|
||||||
promise.fail("JavaScript解析器执行失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<List<FileInfo>> parseFileList() {
|
public Future<List<FileInfo>> parseFileList() {
|
||||||
Promise<List<FileInfo>> promise = Promise.promise();
|
jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
|
||||||
|
|
||||||
try {
|
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||||
jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
|
|
||||||
// 直接调用全局parseFileList函数
|
// 直接调用全局parseFileList函数
|
||||||
Object parseFileListFunction = engine.get("parseFileList");
|
Object parseFileListFunction = engine.get("parseFileList");
|
||||||
if (parseFileListFunction == null) {
|
if (parseFileListFunction == null) {
|
||||||
@@ -141,41 +134,32 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用parseFileList方法
|
// 调用parseFileList方法
|
||||||
if (parseFileListFunction instanceof ScriptObjectMirror) {
|
if (parseFileListFunction instanceof ScriptObjectMirror parseFileListMirror) {
|
||||||
ScriptObjectMirror parseFileListMirror = (ScriptObjectMirror) parseFileListFunction;
|
|
||||||
|
|
||||||
Object result = parseFileListMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
Object result = parseFileListMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
||||||
|
|
||||||
if (result instanceof ScriptObjectMirror) {
|
if (result instanceof ScriptObjectMirror resultMirror) {
|
||||||
ScriptObjectMirror resultMirror = (ScriptObjectMirror) result;
|
|
||||||
List<FileInfo> fileList = convertToFileInfoList(resultMirror);
|
List<FileInfo> fileList = convertToFileInfoList(resultMirror);
|
||||||
|
|
||||||
jsLogger.info("文件列表解析成功,共 {} 个文件", fileList.size());
|
jsLogger.info("文件列表解析成功,共 {} 个文件", fileList.size());
|
||||||
promise.complete(fileList);
|
return fileList;
|
||||||
} else {
|
} else {
|
||||||
jsLogger.error("parseFileList方法返回值类型错误,期望数组,实际: {}",
|
jsLogger.error("parseFileList方法返回值类型错误,期望数组,实际: {}",
|
||||||
result != null ? result.getClass().getSimpleName() : "null");
|
result != null ? result.getClass().getSimpleName() : "null");
|
||||||
promise.fail("parseFileList方法返回值类型错误");
|
throw new RuntimeException("parseFileList方法返回值类型错误");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("parseFileList函数类型错误");
|
throw new RuntimeException("parseFileList函数类型错误");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
jsLogger.error("JavaScript文件列表解析失败", e);
|
|
||||||
promise.fail("JavaScript文件列表解析失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Future<String> parseById() {
|
public Future<String> parseById() {
|
||||||
Promise<String> promise = Promise.promise();
|
jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
|
||||||
|
|
||||||
try {
|
// 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
|
||||||
jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
|
return EXECUTOR.executeBlocking(() -> {
|
||||||
|
|
||||||
// 直接调用全局parseById函数
|
// 直接调用全局parseById函数
|
||||||
Object parseByIdFunction = engine.get("parseById");
|
Object parseByIdFunction = engine.get("parseById");
|
||||||
if (parseByIdFunction == null) {
|
if (parseByIdFunction == null) {
|
||||||
@@ -183,29 +167,22 @@ public class JsParserExecutor implements IPanTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用parseById方法
|
// 调用parseById方法
|
||||||
if (parseByIdFunction instanceof ScriptObjectMirror) {
|
if (parseByIdFunction instanceof ScriptObjectMirror parseByIdMirror) {
|
||||||
ScriptObjectMirror parseByIdMirror = (ScriptObjectMirror) parseByIdFunction;
|
|
||||||
|
|
||||||
Object result = parseByIdMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
Object result = parseByIdMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
||||||
|
|
||||||
if (result instanceof String) {
|
if (result instanceof String) {
|
||||||
jsLogger.info("按ID解析成功: {}", result);
|
jsLogger.info("按ID解析成功: {}", result);
|
||||||
promise.complete((String) result);
|
return (String) result;
|
||||||
} else {
|
} else {
|
||||||
jsLogger.error("parseById方法返回值类型错误,期望String,实际: {}",
|
jsLogger.error("parseById方法返回值类型错误,期望String,实际: {}",
|
||||||
result != null ? result.getClass().getSimpleName() : "null");
|
result != null ? result.getClass().getSimpleName() : "null");
|
||||||
promise.fail("parseById方法返回值类型错误");
|
throw new RuntimeException("parseById方法返回值类型错误");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("parseById函数类型错误");
|
throw new RuntimeException("parseById函数类型错误");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
|
||||||
jsLogger.error("JavaScript按ID解析失败", e);
|
|
||||||
promise.fail("JavaScript按ID解析失败: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.future();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
205
parser/src/main/resources/custom-parsers/migu-music.js
Normal file
205
parser/src/main/resources/custom-parsers/migu-music.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 咪咕音乐解析器
|
||||||
|
// @type migu
|
||||||
|
// @displayName 咪咕音乐
|
||||||
|
// @description 解析咪咕音乐分享链接,获取歌曲下载地址
|
||||||
|
// @match https?://c\.migu\.cn/(?<KEY>\w+)(\?.*)?
|
||||||
|
// @author qaiu
|
||||||
|
// @version 2.0.0
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从URL中提取参数值
|
||||||
|
* @param {string} url - URL字符串
|
||||||
|
* @param {string} paramName - 参数名
|
||||||
|
* @returns {string|null} 参数值
|
||||||
|
*/
|
||||||
|
function getUrlParam(url, paramName) {
|
||||||
|
var match = url.match(new RegExp("[?&]" + paramName + "=([^&]*)"));
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取302重定向地址
|
||||||
|
* @param {string} url - 原始URL
|
||||||
|
* @param {JsHttpClient} http - HTTP客户端
|
||||||
|
* @param {JsLogger} logger - 日志记录器
|
||||||
|
* @returns {string} 重定向后的URL
|
||||||
|
*/
|
||||||
|
function getRedirectUrl(url, http, logger) {
|
||||||
|
try {
|
||||||
|
logger.debug("获取重定向地址: " + url);
|
||||||
|
|
||||||
|
// 清理URL,移除?后面的参数
|
||||||
|
var cleanUrl = url;
|
||||||
|
var questionMarkIndex = url.indexOf("?");
|
||||||
|
if (questionMarkIndex !== -1) {
|
||||||
|
cleanUrl = url.substring(0, questionMarkIndex);
|
||||||
|
}
|
||||||
|
logger.debug("清理后的URL: " + cleanUrl);
|
||||||
|
|
||||||
|
// 使用getNoRedirect获取Location头
|
||||||
|
var response = http.getNoRedirect(cleanUrl);
|
||||||
|
var statusCode = response.statusCode();
|
||||||
|
|
||||||
|
// 检查是否是重定向状态码
|
||||||
|
if (statusCode >= 300 && statusCode < 400) {
|
||||||
|
var location = response.header("Location");
|
||||||
|
if (location) {
|
||||||
|
// 处理相对路径
|
||||||
|
if (location.indexOf("http") !== 0) {
|
||||||
|
var baseUrl = cleanUrl.substring(0, cleanUrl.indexOf("/", 8));
|
||||||
|
if (location.indexOf("/") === 0) {
|
||||||
|
location = baseUrl + location;
|
||||||
|
} else {
|
||||||
|
location = baseUrl + "/" + location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("重定向到: " + location);
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有重定向,返回原URL
|
||||||
|
logger.warn("未获取到重定向地址,状态码: " + statusCode);
|
||||||
|
return cleanUrl;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("获取重定向地址失败: " + e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单个文件下载链接
|
||||||
|
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||||
|
* @param {JsHttpClient} http - HTTP客户端
|
||||||
|
* @param {JsLogger} logger - 日志记录器
|
||||||
|
* @returns {string} 下载链接
|
||||||
|
*/
|
||||||
|
function parse(shareLinkInfo, http, logger) {
|
||||||
|
logger.info("===== 开始解析咪咕音乐 =====");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var shareUrl = shareLinkInfo.getShareUrl();
|
||||||
|
logger.info("分享URL: " + shareUrl);
|
||||||
|
|
||||||
|
if (!shareUrl || shareUrl.indexOf("c.migu.cn") === -1) {
|
||||||
|
throw new Error("无效的咪咕音乐分享链接");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
http.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||||
|
http.putHeader("Referer", "https://music.migu.cn/");
|
||||||
|
http.putHeader("Accept", "application/json, text/plain, */*");
|
||||||
|
|
||||||
|
// 步骤1: 获取302重定向地址
|
||||||
|
logger.info("步骤1: 获取302重定向地址...");
|
||||||
|
var redirectUrl = getRedirectUrl(shareUrl, http, logger);
|
||||||
|
logger.info("重定向地址: " + redirectUrl);
|
||||||
|
|
||||||
|
// 步骤2: 从重定向地址中提取contentId (id参数)
|
||||||
|
var contentId = getUrlParam(redirectUrl, "id");
|
||||||
|
if (!contentId) {
|
||||||
|
throw new Error("无法从重定向地址中提取contentId (id参数)");
|
||||||
|
}
|
||||||
|
logger.info("提取到contentId: " + contentId);
|
||||||
|
|
||||||
|
// 步骤3: 调用API获取文件信息
|
||||||
|
logger.info("步骤2: 获取文件信息...");
|
||||||
|
var fileInfoUrl = "https://c.musicapp.migu.cn/MIGUM3.0/resource/song/by-contentids/v2.0?contentId=" + contentId;
|
||||||
|
logger.debug("请求URL: " + fileInfoUrl);
|
||||||
|
|
||||||
|
var fileInfoResponse = http.get(fileInfoUrl);
|
||||||
|
if (fileInfoResponse.statusCode() !== 200) {
|
||||||
|
throw new Error("获取文件信息失败,状态码: " + fileInfoResponse.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileInfoData = fileInfoResponse.json();
|
||||||
|
logger.debug("文件信息响应: " + JSON.stringify(fileInfoData));
|
||||||
|
|
||||||
|
// 提取ringCopyrightId
|
||||||
|
var ringCopyrightId = null;
|
||||||
|
if (fileInfoData.data && fileInfoData.data.length > 0) {
|
||||||
|
var songInfo = fileInfoData.data[0];
|
||||||
|
ringCopyrightId = songInfo.ringCopyrightId;
|
||||||
|
logger.info("歌曲名称: " + (songInfo.songName || "未知"));
|
||||||
|
logger.info("提取到ringCopyrightId: " + ringCopyrightId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ringCopyrightId) {
|
||||||
|
throw new Error("响应中未找到ringCopyrightId");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤4: 调用下载接口获取下载链接
|
||||||
|
logger.info("步骤3: 获取下载链接...");
|
||||||
|
|
||||||
|
// 设置完整的请求头(Referer使用302重定向地址)
|
||||||
|
http.putHeader("Accept", "application/json, text/plain, */*");
|
||||||
|
http.putHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7");
|
||||||
|
http.putHeader("Referer", redirectUrl);
|
||||||
|
http.putHeader("Sec-Fetch-Dest", "empty");
|
||||||
|
http.putHeader("Sec-Fetch-Mode", "cors");
|
||||||
|
http.putHeader("Sec-Fetch-Site", "same-site");
|
||||||
|
http.putHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36");
|
||||||
|
http.putHeader("channel", "014021I");
|
||||||
|
http.putHeader("subchannel", "014021I");
|
||||||
|
|
||||||
|
var downloadApiUrl = "https://c.musicapp.migu.cn/MIGUM3.0/strategy/listen-url/v2.4" +
|
||||||
|
"?contentId=" + contentId +
|
||||||
|
"©rightId=" + ringCopyrightId +
|
||||||
|
"&resourceType=2" +
|
||||||
|
"&netType=01" +
|
||||||
|
"&toneFlag=PQ" +
|
||||||
|
"&scene=" +
|
||||||
|
"&lowerQualityContentId=" + contentId;
|
||||||
|
|
||||||
|
logger.debug("请求URL: " + downloadApiUrl);
|
||||||
|
logger.debug("Referer: " + redirectUrl);
|
||||||
|
|
||||||
|
var downloadResponse = http.get(downloadApiUrl);
|
||||||
|
if (downloadResponse.statusCode() !== 200) {
|
||||||
|
throw new Error("获取下载链接失败,状态码: " + downloadResponse.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadData = downloadResponse.json();
|
||||||
|
logger.info("下载链接响应: " + JSON.stringify(downloadData));
|
||||||
|
|
||||||
|
// 提取最终下载链接
|
||||||
|
if (downloadData.data && downloadData.data.url) {
|
||||||
|
var downloadUrl = downloadData.data.url;
|
||||||
|
logger.info("解析成功,下载链接: " + downloadUrl);
|
||||||
|
return downloadUrl;
|
||||||
|
} else {
|
||||||
|
throw new Error("响应中未找到下载链接");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("解析失败: " + e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析文件列表(可选)
|
||||||
|
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||||
|
* @param {JsHttpClient} http - HTTP客户端
|
||||||
|
* @param {JsLogger} logger - 日志记录器
|
||||||
|
* @returns {FileInfo[]} 文件信息列表
|
||||||
|
*/
|
||||||
|
function parseFileList(shareLinkInfo, http, logger) {
|
||||||
|
// 咪咕音乐通常是单曲,不需要实现文件列表
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件ID获取下载链接(可选)
|
||||||
|
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||||
|
* @param {JsHttpClient} http - HTTP客户端
|
||||||
|
* @param {JsLogger} logger - 日志记录器
|
||||||
|
* @returns {string} 下载链接
|
||||||
|
*/
|
||||||
|
function parseById(shareLinkInfo, http, logger) {
|
||||||
|
// 使用相同的解析逻辑
|
||||||
|
return parse(shareLinkInfo, http, logger);
|
||||||
|
}
|
||||||
231
parser/src/main/resources/custom-parsers/qishui-music.js
Normal file
231
parser/src/main/resources/custom-parsers/qishui-music.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 汽水音乐解析器
|
||||||
|
// @type qishui_music
|
||||||
|
// @displayName 汽水音乐
|
||||||
|
// @description 解析汽水音乐分享链接,获取音乐文件下载链接
|
||||||
|
// @match https://music\.douyin\.com/qishui/share/track\?(.*&)?track_id=(?<KEY>\d+)
|
||||||
|
// @author qaiu
|
||||||
|
// @version 2.0.1
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跟踪302重定向,获取真实URL
|
||||||
|
* @param {string} url - 原始URL
|
||||||
|
* @param {JsHttpClient} http - HTTP客户端
|
||||||
|
* @param {JsLogger} logger - 日志记录器
|
||||||
|
* @returns {string} 真实URL
|
||||||
|
*/
|
||||||
|
function getRealUrl(url, http, logger) {
|
||||||
|
try {
|
||||||
|
logger.debug("跟踪重定向: " + url);
|
||||||
|
// 使用getNoRedirect获取Location头
|
||||||
|
var response = http.getNoRedirect(url);
|
||||||
|
var statusCode = response.statusCode();
|
||||||
|
|
||||||
|
// 检查是否是重定向状态码 (301, 302, 303, 307, 308)
|
||||||
|
if (statusCode >= 300 && statusCode < 400) {
|
||||||
|
var location = response.header("Location");
|
||||||
|
if (location) {
|
||||||
|
// 处理相对路径
|
||||||
|
if (location.indexOf("http") !== 0) {
|
||||||
|
var baseUrl = url.substring(0, url.indexOf("/", 8)); // 获取协议和域名部分
|
||||||
|
if (location.indexOf("/") === 0) {
|
||||||
|
location = baseUrl + location;
|
||||||
|
} else {
|
||||||
|
location = baseUrl + "/" + location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("重定向到: " + location);
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果没有重定向或无法获取Location头,返回原URL
|
||||||
|
logger.debug("无需重定向或无法获取重定向信息");
|
||||||
|
return url;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("获取真实链接失败: " + e.message);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从URL中提取track_id
|
||||||
|
* @param {string} url - URL字符串
|
||||||
|
* @returns {string|null} track_id
|
||||||
|
*/
|
||||||
|
function extractTrackId(url) {
|
||||||
|
var match = url.match(/track_id=(\d+)/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL解码
|
||||||
|
* @param {string} str - 编码的字符串
|
||||||
|
* @returns {string} 解码后的字符串
|
||||||
|
*/
|
||||||
|
function unquote(str) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(str);
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间标签(毫秒转LRC格式)
|
||||||
|
* @param {number} startMs - 开始时间(毫秒)
|
||||||
|
* @returns {string} LRC格式时间标签 [mm:ss.fff]
|
||||||
|
*/
|
||||||
|
function formatTimeTag(startMs) {
|
||||||
|
var minutes = Math.floor(startMs / 60000);
|
||||||
|
var seconds = Math.floor((startMs % 60000) / 1000);
|
||||||
|
var milliseconds = startMs % 1000;
|
||||||
|
|
||||||
|
var minutesStr = (minutes < 10 ? "0" : "") + minutes;
|
||||||
|
var secondsStr = (seconds < 10 ? "0" : "") + seconds;
|
||||||
|
var millisecondsStr = (milliseconds < 10 ? "00" : (milliseconds < 100 ? "0" : "")) + milliseconds;
|
||||||
|
|
||||||
|
return "[" + minutesStr + ":" + secondsStr + "." + millisecondsStr + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单个文件下载链接
|
||||||
|
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||||
|
* @param {JsHttpClient} http - HTTP客户端
|
||||||
|
* @param {JsLogger} logger - 日志记录器
|
||||||
|
* @returns {string} 下载链接
|
||||||
|
*/
|
||||||
|
function parse(shareLinkInfo, http, logger) {
|
||||||
|
logger.info("===== 开始解析汽水音乐 =====");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 优先从ShareKey获取track_id(最快方式)
|
||||||
|
var trackId = shareLinkInfo.getShareKey();
|
||||||
|
|
||||||
|
// 如果ShareKey为空,尝试从URL中提取
|
||||||
|
if (!trackId) {
|
||||||
|
var shareUrl = shareLinkInfo.getShareUrl();
|
||||||
|
logger.info("分享URL: " + shareUrl);
|
||||||
|
|
||||||
|
if (shareUrl) {
|
||||||
|
// 先尝试直接从URL提取track_id(避免重定向超时)
|
||||||
|
trackId = extractTrackId(shareUrl);
|
||||||
|
|
||||||
|
// 如果是短链接且仍未提取到track_id,才进行重定向处理
|
||||||
|
if (!trackId && shareUrl.indexOf("qishui.douyin.com") !== -1) {
|
||||||
|
logger.info("检测到短链接,尝试获取真实URL...");
|
||||||
|
try {
|
||||||
|
shareUrl = getRealUrl(shareUrl, http, logger);
|
||||||
|
logger.info("重定向后URL: " + shareUrl);
|
||||||
|
trackId = extractTrackId(shareUrl);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("短链接重定向处理失败: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("歌曲ID: " + trackId);
|
||||||
|
|
||||||
|
if (!trackId) {
|
||||||
|
throw new Error("无法提取track_id");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置必要的浏览器请求头(最小化,避免触发反爬虫)
|
||||||
|
http.putHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
|
||||||
|
http.putHeader("Accept-Language", "zh-CN,zh;q=0.9");
|
||||||
|
http.putHeader("Referer", "https://music.douyin.com/");
|
||||||
|
http.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||||
|
|
||||||
|
// 请求音乐页面
|
||||||
|
var musicUrl = "https://music.douyin.com/qishui/share/track?track_id=" + trackId;
|
||||||
|
logger.info("请求音乐页面: " + musicUrl);
|
||||||
|
logger.debug("开始请求,请等待...");
|
||||||
|
|
||||||
|
// 使用getWithRedirect自动处理重定向
|
||||||
|
// 注意:如果超时,可能是网络问题或目标网站响应慢
|
||||||
|
var response = http.getWithRedirect(musicUrl);
|
||||||
|
|
||||||
|
logger.debug("请求完成,状态码: " + response.statusCode());
|
||||||
|
|
||||||
|
if (response.statusCode() !== 200) {
|
||||||
|
throw new Error("获取页面内容失败,状态码: " + response.statusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
var htmlContent = response.body();
|
||||||
|
|
||||||
|
if (!htmlContent) {
|
||||||
|
throw new Error("页面内容为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("页面内容长度: " + htmlContent.length);
|
||||||
|
|
||||||
|
// 初始化结果
|
||||||
|
var musicPlayUrl = "";
|
||||||
|
|
||||||
|
// 提取 _ROUTER_DATA 数据(音频地址和歌词)
|
||||||
|
// 匹配模式:<script async="" data-script-src="modern-inline">_ROUTER_DATA = {...};
|
||||||
|
var routerDataPattern = /<script\s+async=""\s+data-script-src="modern-inline">\s*_ROUTER_DATA\s*=\s*({[\s\S]*?});/;
|
||||||
|
var routerDataMatch = htmlContent.match(routerDataPattern);
|
||||||
|
|
||||||
|
if (routerDataMatch) {
|
||||||
|
try {
|
||||||
|
var jsonStr = routerDataMatch[1].trim();
|
||||||
|
var jsonData = JSON.parse(jsonStr);
|
||||||
|
|
||||||
|
logger.debug("解析_ROUTER_DATA成功");
|
||||||
|
|
||||||
|
// 提取音频URL
|
||||||
|
var audioOption = jsonData.loaderData &&
|
||||||
|
jsonData.loaderData.track_page &&
|
||||||
|
jsonData.loaderData.track_page.audioWithLyricsOption;
|
||||||
|
|
||||||
|
if (audioOption && audioOption.url) {
|
||||||
|
musicPlayUrl = audioOption.url;
|
||||||
|
logger.info("提取到音频URL: " + musicPlayUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取歌词(可选,用于日志)
|
||||||
|
if (audioOption && audioOption.lyrics && audioOption.lyrics.sentences) {
|
||||||
|
var sentences = audioOption.lyrics.sentences;
|
||||||
|
logger.debug("提取到歌词,共 " + sentences.length + " 句");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("解析_ROUTER_DATA失败: " + e.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("未找到_ROUTER_DATA");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果未找到音频URL,尝试从application/ld+json中提取(备用方案)
|
||||||
|
if (!musicPlayUrl) {
|
||||||
|
logger.warn("未从_ROUTER_DATA中提取到音频URL,尝试备用方案");
|
||||||
|
|
||||||
|
// 提取 application/ld+json 数据
|
||||||
|
var ldJsonPattern = /<script\s+data-react-helmet="true"\s+type="application\/ld\+json">([\s\S]*?)<\/script>/;
|
||||||
|
var ldJsonMatch = htmlContent.match(ldJsonPattern);
|
||||||
|
|
||||||
|
if (ldJsonMatch) {
|
||||||
|
try {
|
||||||
|
var ldJsonStr = unquote(ldJsonMatch[1]);
|
||||||
|
var ldJsonData = JSON.parse(ldJsonStr);
|
||||||
|
logger.debug("解析ld+json成功,标题: " + (ldJsonData.title || "无"));
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("解析ld+json失败: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!musicPlayUrl) {
|
||||||
|
throw new Error("没有找到相关音乐");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("解析成功: " + musicPlayUrl);
|
||||||
|
return musicPlayUrl;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("解析失败: " + e.message);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
282
parser/src/test/java/cn/qaiu/parser/JsHttpClientTest.java
Normal file
282
parser/src/test/java/cn/qaiu/parser/JsHttpClientTest.java
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package cn.qaiu.parser;
|
||||||
|
|
||||||
|
import cn.qaiu.WebClientVertxInit;
|
||||||
|
import cn.qaiu.parser.customjs.JsHttpClient;
|
||||||
|
import io.vertx.core.Vertx;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JsHttpClient 测试类
|
||||||
|
* 测试HTTP请求功能是否正常
|
||||||
|
*
|
||||||
|
* @author <a href="https://qaiu.top">QAIU</a>
|
||||||
|
* Create at 2025/11/15
|
||||||
|
*/
|
||||||
|
public class JsHttpClientTest {
|
||||||
|
|
||||||
|
private Vertx vertx;
|
||||||
|
private JsHttpClient httpClient;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
// 初始化Vertx
|
||||||
|
vertx = Vertx.vertx();
|
||||||
|
WebClientVertxInit.init(vertx);
|
||||||
|
|
||||||
|
// 创建JsHttpClient实例
|
||||||
|
httpClient = new JsHttpClient();
|
||||||
|
|
||||||
|
System.out.println("=== 测试开始 ===");
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
// 清理资源
|
||||||
|
if (vertx != null) {
|
||||||
|
vertx.close();
|
||||||
|
}
|
||||||
|
System.out.println("=== 测试结束 ===\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSimpleGetRequest() {
|
||||||
|
System.out.println("\n[测试1] 简单GET请求 - httpbin.org/get");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = "https://httpbin.org/get";
|
||||||
|
System.out.println("请求URL: " + url);
|
||||||
|
System.out.println("开始请求...");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
JsHttpClient.JsHttpResponse response = httpClient.get(url);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
|
||||||
|
System.out.println("状态码: " + response.statusCode());
|
||||||
|
System.out.println("响应头数量: " + response.headers().size());
|
||||||
|
|
||||||
|
String body = response.body();
|
||||||
|
System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符");
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertNotNull("响应不能为null", response);
|
||||||
|
assertEquals("状态码应该是200", 200, response.statusCode());
|
||||||
|
assertNotNull("响应体不能为null", body);
|
||||||
|
assertTrue("响应体应该包含url字段", body.contains("\"url\""));
|
||||||
|
|
||||||
|
System.out.println("✓ 测试通过");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("GET请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetWithRedirect() {
|
||||||
|
System.out.println("\n[测试2] GET请求(跟随重定向) - httpbin.org/redirect/1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = "https://httpbin.org/redirect/1";
|
||||||
|
System.out.println("请求URL: " + url);
|
||||||
|
System.out.println("开始请求(会自动跟随重定向)...");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
JsHttpClient.JsHttpResponse response = httpClient.getWithRedirect(url);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
|
||||||
|
System.out.println("状态码: " + response.statusCode());
|
||||||
|
|
||||||
|
String body = response.body();
|
||||||
|
System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符");
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertNotNull("响应不能为null", response);
|
||||||
|
assertEquals("状态码应该是200(重定向后)", 200, response.statusCode());
|
||||||
|
|
||||||
|
System.out.println("✓ 测试通过");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("GET重定向请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetNoRedirect() {
|
||||||
|
System.out.println("\n[测试3] GET请求(不跟随重定向) - httpbin.org/redirect/1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = "https://httpbin.org/redirect/1";
|
||||||
|
System.out.println("请求URL: " + url);
|
||||||
|
System.out.println("开始请求(不跟随重定向)...");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
JsHttpClient.JsHttpResponse response = httpClient.getNoRedirect(url);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
|
||||||
|
System.out.println("状态码: " + response.statusCode());
|
||||||
|
|
||||||
|
String location = response.header("Location");
|
||||||
|
System.out.println("Location头: " + location);
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertNotNull("响应不能为null", response);
|
||||||
|
assertTrue("状态码应该是3xx重定向",
|
||||||
|
response.statusCode() >= 300 && response.statusCode() < 400);
|
||||||
|
assertNotNull("应该有Location头", location);
|
||||||
|
|
||||||
|
System.out.println("✓ 测试通过");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("GET不重定向请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetWithHeaders() {
|
||||||
|
System.out.println("\n[测试4] GET请求(带自定义请求头) - httpbin.org/headers");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = "https://httpbin.org/headers";
|
||||||
|
System.out.println("请求URL: " + url);
|
||||||
|
|
||||||
|
// 设置自定义请求头
|
||||||
|
httpClient.putHeader("X-Custom-Header", "test-value");
|
||||||
|
httpClient.putHeader("X-Another-Header", "another-value");
|
||||||
|
|
||||||
|
System.out.println("设置请求头: X-Custom-Header=test-value, X-Another-Header=another-value");
|
||||||
|
System.out.println("开始请求...");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
JsHttpClient.JsHttpResponse response = httpClient.get(url);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
|
||||||
|
System.out.println("状态码: " + response.statusCode());
|
||||||
|
|
||||||
|
String body = response.body();
|
||||||
|
System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符");
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertNotNull("响应不能为null", response);
|
||||||
|
assertEquals("状态码应该是200", 200, response.statusCode());
|
||||||
|
assertNotNull("响应体不能为null", body);
|
||||||
|
assertTrue("响应体应该包含自定义请求头",
|
||||||
|
body.contains("X-Custom-Header") || body.contains("test-value"));
|
||||||
|
|
||||||
|
System.out.println("✓ 测试通过");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("带请求头的GET请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetJsonResponse() {
|
||||||
|
System.out.println("\n[测试5] GET请求(JSON响应) - jsonplaceholder.typicode.com/posts/1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = "https://jsonplaceholder.typicode.com/posts/1";
|
||||||
|
System.out.println("请求URL: " + url);
|
||||||
|
System.out.println("开始请求...");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
JsHttpClient.JsHttpResponse response = httpClient.get(url);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
|
||||||
|
System.out.println("状态码: " + response.statusCode());
|
||||||
|
|
||||||
|
// 测试JSON解析
|
||||||
|
Object jsonData = response.json();
|
||||||
|
System.out.println("JSON数据: " + jsonData);
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertNotNull("响应不能为null", response);
|
||||||
|
assertEquals("状态码应该是200", 200, response.statusCode());
|
||||||
|
assertNotNull("JSON数据不能为null", jsonData);
|
||||||
|
|
||||||
|
System.out.println("✓ 测试通过");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("JSON响应请求失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTimeout() {
|
||||||
|
System.out.println("\n[测试6] 超时测试 - httpbin.org/delay/5");
|
||||||
|
System.out.println("注意:这个请求会延迟5秒,应该在30秒内完成");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = "https://httpbin.org/delay/5";
|
||||||
|
System.out.println("请求URL: " + url);
|
||||||
|
System.out.println("开始请求(延迟5秒)...");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
JsHttpClient.JsHttpResponse response = httpClient.get(url);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
long duration = endTime - startTime;
|
||||||
|
System.out.println("请求完成,耗时: " + duration + "ms");
|
||||||
|
System.out.println("状态码: " + response.statusCode());
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertNotNull("响应不能为null", response);
|
||||||
|
assertEquals("状态码应该是200", 200, response.statusCode());
|
||||||
|
assertTrue("应该在合理时间内完成(5-10秒)", duration >= 5000 && duration < 10000);
|
||||||
|
|
||||||
|
System.out.println("✓ 测试通过");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("超时测试失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testErrorResponse() {
|
||||||
|
System.out.println("\n[测试7] 错误响应测试 - httpbin.org/status/404");
|
||||||
|
|
||||||
|
try {
|
||||||
|
String url = "https://httpbin.org/status/404";
|
||||||
|
System.out.println("请求URL: " + url);
|
||||||
|
System.out.println("开始请求(预期404错误)...");
|
||||||
|
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
JsHttpClient.JsHttpResponse response = httpClient.get(url);
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
|
||||||
|
System.out.println("状态码: " + response.statusCode());
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
assertNotNull("响应不能为null", response);
|
||||||
|
assertEquals("状态码应该是404", 404, response.statusCode());
|
||||||
|
assertFalse("不应该成功", response.isSuccess());
|
||||||
|
|
||||||
|
System.out.println("✓ 测试通过");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("✗ 测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
fail("错误响应测试失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
pom.xml
2
pom.xml
@@ -60,7 +60,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.qaiu</groupId>
|
<groupId>cn.qaiu</groupId>
|
||||||
<artifactId>parser</artifactId>
|
<artifactId>parser</artifactId>
|
||||||
<version>10.2.1</version>
|
<version>10.2.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|||||||
615
web-service/doc/API_DOCUMENTATION.md
Normal file
615
web-service/doc/API_DOCUMENTATION.md
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
# 网盘快速下载服务 API 文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档描述了网盘快速下载服务的所有 REST API 接口。该服务支持多种网盘的分享链接解析,提供直链下载、预览、客户端下载链接等功能。
|
||||||
|
|
||||||
|
**基础URL**: `http://localhost:6400` (根据实际部署情况调整)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [解析相关接口](#解析相关接口)
|
||||||
|
- [文件列表接口](#文件列表接口)
|
||||||
|
- [预览接口](#预览接口)
|
||||||
|
- [客户端下载链接接口](#客户端下载链接接口)
|
||||||
|
- [统计信息接口](#统计信息接口)
|
||||||
|
- [网盘列表接口](#网盘列表接口)
|
||||||
|
- [版本信息接口](#版本信息接口)
|
||||||
|
- [隔空喊话接口](#隔空喊话接口)
|
||||||
|
- [快捷下载接口](#快捷下载接口)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 解析相关接口
|
||||||
|
|
||||||
|
### 1. 解析分享链接(重定向)
|
||||||
|
|
||||||
|
**接口**: `GET /parser`
|
||||||
|
|
||||||
|
**描述**: 解析分享链接并重定向到直链下载地址
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `url` (必需): 分享链接
|
||||||
|
- `pwd` (可选): 提取码
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /parser?url=https://pan.baidu.com/s/1test123&pwd=1234
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**:
|
||||||
|
- 302 重定向到直链下载地址
|
||||||
|
- 响应头包含:
|
||||||
|
- `nfd-cache-hit`: 是否命中缓存 (true/false)
|
||||||
|
- `nfd-cache-expires`: 缓存过期时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 解析分享链接(JSON)
|
||||||
|
|
||||||
|
**接口**: `GET /json/parser`
|
||||||
|
|
||||||
|
**描述**: 解析分享链接并返回JSON格式的直链信息
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `url` (必需): 分享链接
|
||||||
|
- `pwd` (可选): 提取码
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /json/parser?url=https://pan.baidu.com/s/1test123&pwd=1234
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shareKey": "pan:1test123",
|
||||||
|
"directLink": "https://example.com/download/file.zip",
|
||||||
|
"cacheHit": false,
|
||||||
|
"expires": "2025-01-22 12:00:00",
|
||||||
|
"expiration": 86400000,
|
||||||
|
"fileInfo": {
|
||||||
|
"fileName": "file.zip",
|
||||||
|
"fileId": "123456",
|
||||||
|
"size": 1024000,
|
||||||
|
"sizeStr": "1MB",
|
||||||
|
"fileType": "zip",
|
||||||
|
"createTime": "2025-01-21 10:00:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 根据类型和Key解析(重定向)
|
||||||
|
|
||||||
|
**接口**: `GET /:type/:key`
|
||||||
|
|
||||||
|
**描述**: 根据网盘类型和分享Key解析并重定向到直链
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `type` (必需): 网盘类型标识(如: lz, pan, cow等)
|
||||||
|
- `key` (必需): 分享Key,如果包含提取码,格式为 `key@pwd`
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /lz/ia2cntg
|
||||||
|
GET /lz/icBp6qqj82b@QAIU
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 302 重定向到直链下载地址
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 根据类型和Key解析(JSON)
|
||||||
|
|
||||||
|
**接口**: `GET /json/:type/:key`
|
||||||
|
|
||||||
|
**描述**: 根据网盘类型和分享Key解析并返回JSON格式的直链信息
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `type` (必需): 网盘类型标识
|
||||||
|
- `key` (必需): 分享Key,如果包含提取码,格式为 `key@pwd`
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /json/lz/ia2cntg
|
||||||
|
GET /json/lz/icBp6qqj82b@QAIU
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应格式**: 同 `/json/parser`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 获取链接信息(V2)
|
||||||
|
|
||||||
|
**接口**: `GET /v2/linkInfo`
|
||||||
|
|
||||||
|
**描述**: 获取分享链接的详细信息,包括下载链接、预览链接、统计信息等
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `url` (必需): 分享链接
|
||||||
|
- `pwd` (可选): 提取码
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/linkInfo?url=https://pan.baidu.com/s/1test123&pwd=1234
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"downLink": "http://127.0.0.1:6400/d/pan/1test123",
|
||||||
|
"apiLink": "http://127.0.0.1:6400/json/pan/1test123",
|
||||||
|
"viewLink": "http://127.0.0.1:6400/v2/view/pan/1test123",
|
||||||
|
"cacheHitTotal": 10,
|
||||||
|
"parserTotal": 5,
|
||||||
|
"sumTotal": 15,
|
||||||
|
"shareLinkInfo": {
|
||||||
|
"shareKey": "1test123",
|
||||||
|
"panName": "百度网盘",
|
||||||
|
"type": "pan",
|
||||||
|
"sharePassword": "1234",
|
||||||
|
"shareUrl": "https://pan.baidu.com/s/1test123",
|
||||||
|
"standardUrl": "https://pan.baidu.com/s/1test123",
|
||||||
|
"otherParam": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件列表接口
|
||||||
|
|
||||||
|
### 6. 获取文件列表
|
||||||
|
|
||||||
|
**接口**: `GET /v2/getFileList`
|
||||||
|
|
||||||
|
**描述**: 获取分享链接中的文件列表(适用于目录分享)
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `url` (必需): 分享链接
|
||||||
|
- `pwd` (可选): 提取码
|
||||||
|
- `dirId` (可选): 目录ID,用于获取指定目录下的文件
|
||||||
|
- `uuid` (可选): UUID,某些网盘需要此参数
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/getFileList?url=https://pan.baidu.com/s/1test123&pwd=1234&dirId=dir123
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fileName": "file1.zip",
|
||||||
|
"fileId": "file123",
|
||||||
|
"size": 1024000,
|
||||||
|
"sizeStr": "1MB",
|
||||||
|
"fileType": "zip",
|
||||||
|
"filePath": "/folder/file1.zip",
|
||||||
|
"createTime": "2025-01-21 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileName": "file2.pdf",
|
||||||
|
"fileId": "file456",
|
||||||
|
"size": 2048000,
|
||||||
|
"sizeStr": "2MB",
|
||||||
|
"fileType": "pdf",
|
||||||
|
"filePath": "/folder/file2.pdf",
|
||||||
|
"createTime": "2025-01-21 11:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 预览接口
|
||||||
|
|
||||||
|
### 7. 预览媒体文件(按类型和Key)
|
||||||
|
|
||||||
|
**接口**: `GET /v2/view/:type/:key`
|
||||||
|
|
||||||
|
**描述**: 预览指定类型和Key的媒体文件(图片、视频等)
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `type` (必需): 网盘类型标识
|
||||||
|
- `key` (必需): 分享Key,如果包含提取码,格式为 `key@pwd`
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/view/pan/1test123
|
||||||
|
GET /v2/view/lz/ia2cntg@QAIU
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 302 重定向到预览页面
|
||||||
|
|
||||||
|
**特殊说明**:
|
||||||
|
- WPS网盘类型(pwps)会直接重定向到原分享链接(WPS支持在线预览)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 预览媒体文件(按URL)
|
||||||
|
|
||||||
|
**接口**: `GET /v2/preview`
|
||||||
|
|
||||||
|
**描述**: 通过分享链接预览媒体文件
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `url` (必需): 分享链接
|
||||||
|
- `pwd` (可选): 提取码
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/preview?url=https://pan.baidu.com/s/1test123&pwd=1234
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 302 重定向到预览页面
|
||||||
|
|
||||||
|
**特殊说明**:
|
||||||
|
- WPS网盘类型会直接重定向到原分享链接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 预览URL(目录预览)
|
||||||
|
|
||||||
|
**接口**: `GET /v2/viewUrl/:type/:param`
|
||||||
|
|
||||||
|
**描述**: 预览目录中的文件,param为Base64编码的参数
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `type` (必需): 网盘类型标识
|
||||||
|
- `param` (必需): Base64编码的参数JSON
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/viewUrl/pan/eyJmaWxlSWQiOiIxMjM0NTYifQ==
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 302 重定向到预览页面
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 客户端下载链接接口
|
||||||
|
|
||||||
|
### 10. 获取所有客户端下载链接
|
||||||
|
|
||||||
|
**接口**: `GET /v2/clientLinks`
|
||||||
|
|
||||||
|
**描述**: 获取所有支持的客户端格式的下载链接
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `url` (必需): 分享链接
|
||||||
|
- `pwd` (可选): 提取码
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/clientLinks?url=https://pan.baidu.com/s/1test123&pwd=1234
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"directLink": "https://example.com/file.zip",
|
||||||
|
"fileName": "test-file.zip",
|
||||||
|
"fileSize": 1024000,
|
||||||
|
"clientLinks": {
|
||||||
|
"CURL": "curl -L -H \"User-Agent: Mozilla/5.0...\" -o \"test-file.zip\" \"https://example.com/file.zip\"",
|
||||||
|
"POWERSHELL": "$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession...",
|
||||||
|
"ARIA2": "aria2c --header=\"User-Agent: Mozilla/5.0...\" --out=\"test-file.zip\" \"https://example.com/file.zip\"",
|
||||||
|
"THUNDER": "thunder://QUFodHRwczovL2V4YW1wbGUuY29tL2ZpbGUuemlwWlo=",
|
||||||
|
"IDM": "idm://https://example.com/file.zip",
|
||||||
|
"WGET": "wget --header=\"User-Agent: Mozilla/5.0...\" -O \"test-file.zip\" \"https://example.com/file.zip\"",
|
||||||
|
"BITCOMET": "bitcomet://https://example.com/file.zip",
|
||||||
|
"MOTRIX": "{\"url\":\"https://example.com/file.zip\",\"out\":\"test-file.zip\"}",
|
||||||
|
"FDM": "https://example.com/file.zip"
|
||||||
|
},
|
||||||
|
"supportedClients": {
|
||||||
|
"curl": "cURL 命令",
|
||||||
|
"wget": "wget 命令",
|
||||||
|
"aria2": "Aria2",
|
||||||
|
"idm": "IDM",
|
||||||
|
"thunder": "迅雷",
|
||||||
|
"bitcomet": "比特彗星",
|
||||||
|
"motrix": "Motrix",
|
||||||
|
"fdm": "Free Download Manager",
|
||||||
|
"powershell": "PowerShell"
|
||||||
|
},
|
||||||
|
"parserInfo": "百度网盘 - pan"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**支持的客户端类型**:
|
||||||
|
- `curl`: cURL 命令
|
||||||
|
- `wget`: wget 命令
|
||||||
|
- `aria2`: Aria2
|
||||||
|
- `idm`: IDM
|
||||||
|
- `thunder`: 迅雷
|
||||||
|
- `bitcomet`: 比特彗星
|
||||||
|
- `motrix`: Motrix
|
||||||
|
- `fdm`: Free Download Manager
|
||||||
|
- `powershell`: PowerShell
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. 获取指定类型的客户端下载链接
|
||||||
|
|
||||||
|
**接口**: `GET /v2/clientLink`
|
||||||
|
|
||||||
|
**描述**: 获取指定客户端类型的下载链接
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `url` (必需): 分享链接
|
||||||
|
- `pwd` (可选): 提取码
|
||||||
|
- `clientType` (必需): 客户端类型 (curl, wget, aria2, idm, thunder, bitcomet, motrix, fdm, powershell)
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/clientLink?url=https://pan.baidu.com/s/1test123&pwd=1234&clientType=curl
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 直接返回指定类型的客户端下载链接字符串
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```
|
||||||
|
curl -L -H "User-Agent: Mozilla/5.0..." -o "test-file.zip" "https://example.com/file.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 统计信息接口
|
||||||
|
|
||||||
|
### 12. 获取统计信息
|
||||||
|
|
||||||
|
**接口**: `GET /v2/statisticsInfo`
|
||||||
|
|
||||||
|
**描述**: 获取系统统计信息,包括解析总数、缓存总数等
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/statisticsInfo
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parserTotal": 1000,
|
||||||
|
"cacheTotal": 500,
|
||||||
|
"total": 1500
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 网盘列表接口
|
||||||
|
|
||||||
|
### 13. 获取支持的网盘列表
|
||||||
|
|
||||||
|
**接口**: `GET /v2/getPanList`
|
||||||
|
|
||||||
|
**描述**: 获取所有支持的网盘列表及其信息
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/getPanList
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "蓝奏云",
|
||||||
|
"type": "lz",
|
||||||
|
"shareUrlFormat": "https://www.lanzou*.com/s/{shareKey}",
|
||||||
|
"url": "https://www.lanzou.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "百度网盘",
|
||||||
|
"type": "pan",
|
||||||
|
"shareUrlFormat": "https://pan.baidu.com/s/{shareKey}",
|
||||||
|
"url": "https://pan.baidu.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本信息接口
|
||||||
|
|
||||||
|
### 14. 获取版本号
|
||||||
|
|
||||||
|
**接口**: `GET /v2/build-version`
|
||||||
|
|
||||||
|
**描述**: 获取应用版本号
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/build-version
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 版本号字符串
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```
|
||||||
|
20250121_101530
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 隔空喊话接口
|
||||||
|
|
||||||
|
### 15. 提交消息
|
||||||
|
|
||||||
|
**接口**: `POST /v2/shout/submit`
|
||||||
|
|
||||||
|
**描述**: 提交一条隔空喊话消息,返回6位提取码
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "这是一条消息内容"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
POST /v2/shout/submit
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"content": "Hello World!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"success": true,
|
||||||
|
"data": "123456",
|
||||||
|
"timestamp": 1705896000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- `data` 字段为6位提取码,用于后续提取消息
|
||||||
|
- 内容不能为空
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. 检索消息
|
||||||
|
|
||||||
|
**接口**: `GET /v2/shout/retrieve`
|
||||||
|
|
||||||
|
**描述**: 根据提取码检索消息
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
- `code` (必需): 6位提取码
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/shout/retrieve?code=123456
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"code": "123456",
|
||||||
|
"content": "Hello World!",
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"createTime": "2025-01-21 10:00:00",
|
||||||
|
"expireTime": "2025-01-22 10:00:00",
|
||||||
|
"isUsed": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**:
|
||||||
|
- 如果提取码格式不正确(不是6位数字),返回错误信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快捷下载接口
|
||||||
|
|
||||||
|
### 17. 下载重定向(短链)
|
||||||
|
|
||||||
|
**接口**: `GET /d/:type/:key`
|
||||||
|
|
||||||
|
**描述**: 短链形式的下载重定向,等同于 `/:type/:key`
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `type` (必需): 网盘类型标识
|
||||||
|
- `key` (必需): 分享Key,如果包含提取码,格式为 `key@pwd`
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /d/lz/ia2cntg
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 302 重定向到直链下载地址
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 18. 重定向下载URL(目录文件)
|
||||||
|
|
||||||
|
**接口**: `GET /v2/redirectUrl/:type/:param`
|
||||||
|
|
||||||
|
**描述**: 重定向到目录中指定文件的下载地址,param为Base64编码的参数
|
||||||
|
|
||||||
|
**路径参数**:
|
||||||
|
- `type` (必需): 网盘类型标识
|
||||||
|
- `param` (必需): Base64编码的参数JSON
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
```
|
||||||
|
GET /v2/redirectUrl/pan/eyJmaWxlSWQiOiIxMjM0NTYifQ==
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**: 302 重定向到直链下载地址
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
所有接口在发生错误时,会返回JSON格式的错误信息:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 500,
|
||||||
|
"msg": "错误描述信息",
|
||||||
|
"success": false,
|
||||||
|
"data": null,
|
||||||
|
"timestamp": 1705896000000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常见错误:
|
||||||
|
- 参数缺失或格式错误
|
||||||
|
- 分享链接无效或已过期
|
||||||
|
- 提取码错误
|
||||||
|
- 网盘类型不支持
|
||||||
|
- 服务器内部错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **缓存机制**: 系统会对解析结果进行缓存,响应头中包含缓存相关信息
|
||||||
|
2. **User-Agent**: 某些网盘需要特定的User-Agent,系统会自动处理
|
||||||
|
3. **Referer**: 某些网盘(如奶牛快传)需要Referer请求头
|
||||||
|
4. **提取码格式**: 在路径参数中,提取码使用 `@` 符号分隔,如 `key@pwd`
|
||||||
|
5. **Base64参数**: 目录相关接口的param参数需要Base64编码
|
||||||
|
6. **WPS特殊处理**: WPS网盘类型在预览时会直接使用原分享链接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的网盘类型
|
||||||
|
|
||||||
|
系统支持多种网盘,包括但不限于:
|
||||||
|
- 蓝奏云 (lz)
|
||||||
|
- 百度网盘 (pan)
|
||||||
|
- 奶牛快传 (cow)
|
||||||
|
- 123网盘 (ye)
|
||||||
|
- 移动云空间 (ec)
|
||||||
|
- 小飞机盘 (fj)
|
||||||
|
- 360亿方云 (fc)
|
||||||
|
- 联想乐云 (le)
|
||||||
|
- 文叔叔 (ws)
|
||||||
|
- Cloudreve (ce)
|
||||||
|
- 等等...
|
||||||
|
|
||||||
|
完整列表可通过 `/v2/getPanList` 接口获取。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
- 2025-01-21: 初始版本文档
|
||||||
|
- 支持客户端下载链接功能
|
||||||
|
- 支持隔空喊话功能
|
||||||
|
|
||||||
|
|
||||||
@@ -87,3 +87,4 @@ OpenAPI 3.0 规范的 JSON 文件,可用于:
|
|||||||
|
|
||||||
- 2025-01-21: 初始版本,包含所有接口的完整文档
|
- 2025-01-21: 初始版本,包含所有接口的完整文档
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1379,3 +1379,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ cache:
|
|||||||
mkg: 30
|
mkg: 30
|
||||||
p115: 30
|
p115: 30
|
||||||
ct: 30
|
ct: 30
|
||||||
|
qishui_music: 5
|
||||||
|
baidu_photo: 5
|
||||||
|
migu: 5
|
||||||
|
|
||||||
# httpClient静态代理服务器配置(外网代理)
|
# httpClient静态代理服务器配置(外网代理)
|
||||||
proxy:
|
proxy:
|
||||||
|
|||||||
Reference in New Issue
Block a user