mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-04-11 19:36:54 +00:00
feat: 添加getNoRedirect方法支持302重定向处理
- 在JsHttpClient中添加getNoRedirect方法,支持不自动跟随重定向的HTTP请求 - 修改baidu-photo.js解析器,使用getNoRedirect获取真实的下载链接 - 更新测试用例断言,验证重定向处理功能正常工作 - 修复百度一刻相册解析器302重定向问题,现在能正确获取真实下载链接
This commit is contained in:
238
parser/src/main/resources/custom-parsers/README.md
Normal file
238
parser/src/main/resources/custom-parsers/README.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# JavaScript解析器扩展使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
本项目支持用户使用JavaScript编写自定义网盘解析器,提供灵活的扩展能力。JavaScript解析器运行在Nashorn引擎中,支持ES5.1语法。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
custom-parsers/
|
||||
├── types.js # 类型定义文件(JSDoc注释)
|
||||
├── jsconfig.json # VSCode配置文件
|
||||
├── example-demo.js # 示例解析器
|
||||
└── README.md # 本说明文档
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 创建解析器脚本
|
||||
|
||||
在 `custom-parsers/` 目录下创建 `.js` 文件,使用以下格式:
|
||||
|
||||
```javascript
|
||||
// ==UserScript==
|
||||
// @name 你的解析器名称
|
||||
// @type 解析器类型标识
|
||||
// @displayName 显示名称
|
||||
// @description 解析器描述
|
||||
// @match 匹配URL的正则表达式
|
||||
// @author 作者
|
||||
// @version 版本号
|
||||
// ==/UserScript==
|
||||
|
||||
/**
|
||||
* 解析单个文件下载链接
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志记录器
|
||||
* @returns {string} 下载链接
|
||||
*/
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
// 你的解析逻辑
|
||||
return "https://example.com/download/file.zip";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件列表(可选)
|
||||
* @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) {
|
||||
// 你的按ID解析逻辑
|
||||
return "https://example.com/download/" + fileId;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 自动加载
|
||||
|
||||
解析器会在应用启动时自动加载和注册。支持两种加载方式:
|
||||
|
||||
#### 内置解析器(jar包内)
|
||||
- 位置:jar包内的 `custom-parsers/` 资源目录
|
||||
- 特点:随jar包一起发布,无需额外配置
|
||||
|
||||
#### 外部解析器(用户自定义)
|
||||
- 默认位置:应用运行目录下的 `./custom-parsers/` 文件夹
|
||||
- 配置方式:
|
||||
- **系统属性**:`-Dparser.custom-parsers.path=/path/to/your/parsers`
|
||||
- **环境变量**:`PARSER_CUSTOM_PARSERS_PATH=/path/to/your/parsers`
|
||||
- **默认路径**:`./custom-parsers/`(相对于应用运行目录)
|
||||
|
||||
#### 配置示例
|
||||
|
||||
**Maven项目中使用:**
|
||||
```bash
|
||||
# 方式1:系统属性
|
||||
mvn exec:java -Dexec.mainClass="your.MainClass" -Dparser.custom-parsers.path=./src/main/resources/custom-parsers
|
||||
|
||||
# 方式2:环境变量
|
||||
export PARSER_CUSTOM_PARSERS_PATH=./src/main/resources/custom-parsers
|
||||
mvn exec:java -Dexec.mainClass="your.MainClass"
|
||||
```
|
||||
|
||||
**jar包运行时:**
|
||||
```bash
|
||||
# 方式1:系统属性
|
||||
java -Dparser.custom-parsers.path=/path/to/your/parsers -jar your-app.jar
|
||||
|
||||
# 方式2:环境变量
|
||||
export PARSER_CUSTOM_PARSERS_PATH=/path/to/your/parsers
|
||||
java -jar your-app.jar
|
||||
```
|
||||
|
||||
## API参考
|
||||
|
||||
### ShareLinkInfo
|
||||
|
||||
分享链接信息对象:
|
||||
|
||||
```javascript
|
||||
shareLinkInfo.getShareUrl() // 获取分享URL
|
||||
shareLinkInfo.getShareKey() // 获取分享Key
|
||||
shareLinkInfo.getSharePassword() // 获取分享密码
|
||||
shareLinkInfo.getType() // 获取网盘类型
|
||||
shareLinkInfo.getPanName() // 获取网盘名称
|
||||
shareLinkInfo.getOtherParam(key) // 获取其他参数
|
||||
```
|
||||
|
||||
### JsHttpClient
|
||||
|
||||
HTTP客户端对象:
|
||||
|
||||
```javascript
|
||||
http.get(url) // GET请求
|
||||
http.post(url, data) // POST请求
|
||||
http.putHeader(name, value) // 设置请求头
|
||||
http.sendForm(data) // 发送表单数据
|
||||
http.sendJson(data) // 发送JSON数据
|
||||
```
|
||||
|
||||
### JsHttpResponse
|
||||
|
||||
HTTP响应对象:
|
||||
|
||||
```javascript
|
||||
response.body() // 获取响应体(字符串)
|
||||
response.json() // 解析JSON响应
|
||||
response.statusCode() // 获取HTTP状态码
|
||||
response.header(name) // 获取响应头
|
||||
response.headers() // 获取所有响应头
|
||||
```
|
||||
|
||||
### JsLogger
|
||||
|
||||
日志记录器:
|
||||
|
||||
```javascript
|
||||
logger.debug(message) // 调试日志
|
||||
logger.info(message) // 信息日志
|
||||
logger.warn(message) // 警告日志
|
||||
logger.error(message) // 错误日志
|
||||
```
|
||||
|
||||
### FileInfo
|
||||
|
||||
文件信息对象:
|
||||
|
||||
```javascript
|
||||
{
|
||||
fileName: "文件名",
|
||||
fileId: "文件ID",
|
||||
fileType: "file|folder",
|
||||
size: 1024,
|
||||
sizeStr: "1KB",
|
||||
createTime: "2024-01-01",
|
||||
updateTime: "2024-01-01",
|
||||
createBy: "创建者",
|
||||
downloadCount: 100,
|
||||
fileIcon: "file",
|
||||
panType: "网盘类型",
|
||||
parserUrl: "解析URL",
|
||||
previewUrl: "预览URL"
|
||||
}
|
||||
```
|
||||
|
||||
## 开发提示
|
||||
|
||||
### VSCode支持
|
||||
|
||||
1. 确保安装了JavaScript扩展
|
||||
2. `types.js` 文件提供类型定义和代码补全
|
||||
3. `jsconfig.json` 配置了项目设置
|
||||
|
||||
### 调试
|
||||
|
||||
- 使用 `logger.debug()` 输出调试信息
|
||||
- 查看应用日志了解解析过程
|
||||
- 使用 `console.log()` 在Nashorn中输出信息
|
||||
|
||||
### 错误处理
|
||||
|
||||
```javascript
|
||||
try {
|
||||
var response = http.get(url);
|
||||
if (response.statusCode() !== 200) {
|
||||
throw new Error("请求失败: " + response.statusCode());
|
||||
}
|
||||
return response.json();
|
||||
} catch (e) {
|
||||
logger.error("解析失败: " + e.message);
|
||||
throw e;
|
||||
}
|
||||
```
|
||||
|
||||
## 示例
|
||||
|
||||
参考 `example-demo.js` 文件,它展示了完整的解析器实现,包括:
|
||||
|
||||
- 元数据配置
|
||||
- 三个核心方法的实现
|
||||
- 错误处理
|
||||
- 日志记录
|
||||
- 文件信息构建
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **ES5.1兼容**:只使用ES5.1语法,避免ES6+特性
|
||||
2. **同步API**:HTTP客户端提供同步接口,无需处理异步回调
|
||||
3. **全局函数**:解析器函数必须定义为全局函数,不能使用模块导出
|
||||
4. **错误处理**:始终包含适当的错误处理和日志记录
|
||||
5. **性能考虑**:避免在解析器中执行耗时操作
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **解析器未加载**:检查元数据格式是否正确
|
||||
2. **类型错误**:确保函数签名与接口匹配
|
||||
3. **HTTP请求失败**:检查URL和网络连接
|
||||
4. **JSON解析错误**:验证响应格式
|
||||
|
||||
### 日志查看
|
||||
|
||||
查看应用日志了解详细的执行过程和错误信息。
|
||||
400
parser/src/main/resources/custom-parsers/baidu-photo.js
Normal file
400
parser/src/main/resources/custom-parsers/baidu-photo.js
Normal file
@@ -0,0 +1,400 @@
|
||||
// ==UserScript==
|
||||
// @name 一刻相册解析器
|
||||
// @type baidu_photo
|
||||
// @displayName 百度一刻相册(JS)
|
||||
// @description 解析百度一刻相册分享链接,获取文件列表和下载链接
|
||||
// @match https?://photo\.baidu\.com/photo/(web/share\?inviteCode=|wap/albumShare\?shareId=)(?<KEY>\w+)
|
||||
// @author qaiu
|
||||
// @version 1.0.0
|
||||
// ==/UserScript==
|
||||
|
||||
/**
|
||||
* API端点配置
|
||||
*/
|
||||
var API_CONFIG = {
|
||||
// 文件夹分享:通过pcode获取share_id
|
||||
QUERY_PCODE: "https://photo.baidu.com/youai/album/v1/querypcode",
|
||||
// 文件列表:获取文件列表
|
||||
LIST_FILES: "https://photo.baidu.com/youai/share/v2/list",
|
||||
|
||||
// 请求参数
|
||||
CLIENT_TYPE: "70",
|
||||
LIMIT: "100"
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置标准请求头
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {string} referer - Referer URL
|
||||
*/
|
||||
function setStandardHeaders(http, referer) {
|
||||
var headers = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"DNT": "1",
|
||||
"Origin": "https://photo.baidu.com",
|
||||
"Pragma": "no-cache",
|
||||
"Referer": referer,
|
||||
"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/141.0.0.0 Safari/537.36 Edg/141.0.0.0",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"sec-ch-ua": "\"Microsoft Edge\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": "\"macOS\""
|
||||
};
|
||||
|
||||
for (var key in headers) {
|
||||
http.putHeader(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享ID
|
||||
* @param {string} shareKey - 分享键
|
||||
* @param {boolean} isFileShare - 是否为文件分享
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志记录器
|
||||
* @returns {string} 分享ID
|
||||
*/
|
||||
function getShareId(shareKey, isFileShare, http, logger) {
|
||||
if (isFileShare) {
|
||||
logger.info("文件分享模式,直接使用shareId: " + shareKey);
|
||||
return shareKey;
|
||||
}
|
||||
|
||||
// 文件夹分享:通过pcode获取share_id
|
||||
var queryUrl = API_CONFIG.QUERY_PCODE + "?clienttype=" + API_CONFIG.CLIENT_TYPE + "&pcode=" + shareKey + "&web=1";
|
||||
logger.debug("文件夹分享查询URL: " + queryUrl);
|
||||
|
||||
setStandardHeaders(http, "https://photo.baidu.com/photo/web/share?inviteCode=" + shareKey);
|
||||
|
||||
var queryResponse = http.get(queryUrl);
|
||||
if (queryResponse.statusCode() !== 200) {
|
||||
throw new Error("获取分享ID失败,状态码: " + queryResponse.statusCode());
|
||||
}
|
||||
|
||||
var queryData = queryResponse.json();
|
||||
logger.debug("查询响应: " + JSON.stringify(queryData));
|
||||
|
||||
if (queryData.errno !== undefined && queryData.errno !== 0) {
|
||||
throw new Error("API返回错误,errno: " + queryData.errno);
|
||||
}
|
||||
|
||||
var shareId = queryData.pdata && queryData.pdata.share_id;
|
||||
if (!shareId) {
|
||||
throw new Error("未找到share_id");
|
||||
}
|
||||
|
||||
logger.info("获取到分享ID: " + shareId);
|
||||
return shareId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
* @param {string} shareId - 分享ID
|
||||
* @param {string} shareKey - 分享键
|
||||
* @param {boolean} isFileShare - 是否为文件分享
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志记录器
|
||||
* @returns {Array} 文件列表
|
||||
*/
|
||||
function getFileList(shareId, shareKey, isFileShare, http, logger) {
|
||||
var listUrl = API_CONFIG.LIST_FILES + "?clienttype=" + API_CONFIG.CLIENT_TYPE + "&share_id=" + shareId + "&limit=" + API_CONFIG.LIMIT;
|
||||
logger.debug("获取文件列表 URL: " + listUrl);
|
||||
|
||||
var referer = isFileShare ?
|
||||
"https://photo.baidu.com/photo/wap/albumShare?shareId=" + shareKey :
|
||||
"https://photo.baidu.com/photo/web/share?inviteCode=" + shareKey;
|
||||
|
||||
setStandardHeaders(http, referer);
|
||||
|
||||
var listResponse = http.get(listUrl);
|
||||
if (listResponse.statusCode() !== 200) {
|
||||
throw new Error("获取文件列表失败,状态码: " + listResponse.statusCode());
|
||||
}
|
||||
|
||||
var listData = listResponse.json();
|
||||
logger.debug("文件列表响应: " + JSON.stringify(listData));
|
||||
|
||||
if (listData.errno !== undefined && listData.errno !== 0) {
|
||||
throw new Error("获取文件列表API返回错误,errno: " + listData.errno);
|
||||
}
|
||||
|
||||
var fileList = listData.list;
|
||||
if (!fileList || fileList.length === 0) {
|
||||
logger.warn("文件列表为空");
|
||||
return [];
|
||||
}
|
||||
|
||||
logger.info("获取到文件列表,共 " + fileList.length + " 个文件");
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个文件下载链接
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
|
||||
* @param {JsHttpClient} http - HTTP客户端实例
|
||||
* @param {JsLogger} logger - 日志记录器实例
|
||||
* @returns {string} 下载链接
|
||||
*/
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
logger.info("===== 开始执行 parse 方法 =====");
|
||||
|
||||
var shareKey = shareLinkInfo.getShareKey();
|
||||
logger.info("分享Key: " + shareKey);
|
||||
|
||||
try {
|
||||
// 判断分享类型
|
||||
// 如果shareKey是纯数字且长度较长,很可能是文件分享的share_id
|
||||
// 如果shareKey包含字母,很可能是文件夹分享的inviteCode
|
||||
var isFileShare = /^\d{10,}$/.test(shareKey);
|
||||
logger.info("分享类型: " + (isFileShare ? "文件分享" : "文件夹分享"));
|
||||
|
||||
// 获取分享ID
|
||||
var shareId = getShareId(shareKey, isFileShare, http, logger);
|
||||
|
||||
// 获取文件列表
|
||||
var fileList = getFileList(shareId, shareKey, isFileShare, http, logger);
|
||||
|
||||
if (fileList.length === 0) {
|
||||
throw new Error("文件列表为空");
|
||||
}
|
||||
|
||||
// 返回第一个文件的下载链接
|
||||
var firstFile = fileList[0];
|
||||
var downloadUrl = firstFile.dlink;
|
||||
|
||||
if (!downloadUrl) {
|
||||
throw new Error("未找到下载链接");
|
||||
}
|
||||
|
||||
// 获取真实的下载链接(处理302重定向)
|
||||
var realDownloadUrl = getRealDownloadUrl(downloadUrl, http, logger);
|
||||
|
||||
logger.info("解析成功,返回URL: " + realDownloadUrl);
|
||||
return realDownloadUrl;
|
||||
|
||||
} catch (e) {
|
||||
logger.error("解析失败: " + e.message);
|
||||
throw new Error("解析失败: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件列表
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
|
||||
* @param {JsHttpClient} http - HTTP客户端实例
|
||||
* @param {JsLogger} logger - 日志记录器实例
|
||||
* @returns {FileInfo[]} 文件信息列表
|
||||
*/
|
||||
function parseFileList(shareLinkInfo, http, logger) {
|
||||
logger.info("===== 开始执行 parseFileList 方法 =====");
|
||||
|
||||
var shareKey = shareLinkInfo.getShareKey();
|
||||
logger.info("分享Key: " + shareKey);
|
||||
|
||||
try {
|
||||
// 判断分享类型
|
||||
// 如果shareKey是纯数字且长度较长,很可能是文件分享的share_id
|
||||
// 如果shareKey包含字母,很可能是文件夹分享的inviteCode
|
||||
var isFileShare = /^\d{10,}$/.test(shareKey);
|
||||
logger.info("分享类型: " + (isFileShare ? "文件分享" : "文件夹分享"));
|
||||
|
||||
// 获取分享ID
|
||||
var shareId = getShareId(shareKey, isFileShare, http, logger);
|
||||
|
||||
// 获取文件列表
|
||||
var fileList = getFileList(shareId, shareKey, isFileShare, http, logger);
|
||||
|
||||
if (fileList.length === 0) {
|
||||
logger.warn("文件列表为空");
|
||||
return [];
|
||||
}
|
||||
|
||||
logger.info("解析文件列表成功,共 " + fileList.length + " 项");
|
||||
|
||||
var result = [];
|
||||
for (var i = 0; i < fileList.length; i++) {
|
||||
var file = fileList[i];
|
||||
|
||||
/** @type {FileInfo} */
|
||||
var fileInfo = {
|
||||
fileName: extractFileName(file.path) || ("文件_" + (i + 1)),
|
||||
fileId: String(file.fsid),
|
||||
fileType: "file",
|
||||
size: file.size || 0,
|
||||
sizeStr: formatBytes(file.size || 0),
|
||||
createTime: formatTimestamp(file.ctime),
|
||||
updateTime: formatTimestamp(file.mtime),
|
||||
createBy: "",
|
||||
downloadCount: 0,
|
||||
fileIcon: "file",
|
||||
panType: "baidu_photo",
|
||||
parserUrl: "",
|
||||
previewUrl: ""
|
||||
};
|
||||
|
||||
// 设置下载链接
|
||||
if (file.dlink) {
|
||||
fileInfo.parserUrl = file.dlink;
|
||||
}
|
||||
|
||||
// 设置预览链接(取第一个缩略图)
|
||||
if (file.thumburl && file.thumburl.length > 0) {
|
||||
fileInfo.previewUrl = file.thumburl[0];
|
||||
}
|
||||
|
||||
result.push(fileInfo);
|
||||
}
|
||||
|
||||
logger.info("文件列表解析成功,共 " + result.length + " 个文件");
|
||||
return result;
|
||||
|
||||
} catch (e) {
|
||||
logger.error("解析文件列表失败: " + e.message);
|
||||
throw new Error("解析文件列表失败: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件ID获取下载链接
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
|
||||
* @param {JsHttpClient} http - HTTP客户端实例
|
||||
* @param {JsLogger} logger - 日志记录器实例
|
||||
* @returns {string} 下载链接
|
||||
*/
|
||||
function parseById(shareLinkInfo, http, logger) {
|
||||
logger.info("===== 开始执行 parseById 方法 =====");
|
||||
|
||||
var shareKey = shareLinkInfo.getShareKey();
|
||||
var otherParam = shareLinkInfo.getOtherParam("paramJson");
|
||||
var fileId = otherParam ? otherParam.fileId || otherParam.id : null;
|
||||
|
||||
logger.info("分享Key: " + shareKey);
|
||||
logger.info("文件ID: " + fileId);
|
||||
|
||||
if (!fileId) {
|
||||
throw new Error("未提供文件ID");
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断分享类型
|
||||
// 如果shareKey是纯数字且长度较长,很可能是文件分享的share_id
|
||||
// 如果shareKey包含字母,很可能是文件夹分享的inviteCode
|
||||
var isFileShare = /^\d{10,}$/.test(shareKey);
|
||||
logger.info("分享类型: " + (isFileShare ? "文件分享" : "文件夹分享"));
|
||||
|
||||
// 获取分享ID
|
||||
var shareId = getShareId(shareKey, isFileShare, http, logger);
|
||||
|
||||
// 获取文件列表
|
||||
var fileList = getFileList(shareId, shareKey, isFileShare, http, logger);
|
||||
|
||||
if (fileList.length === 0) {
|
||||
throw new Error("文件列表为空");
|
||||
}
|
||||
|
||||
// 查找指定ID的文件
|
||||
var targetFile = null;
|
||||
for (var i = 0; i < fileList.length; i++) {
|
||||
var file = fileList[i];
|
||||
if (String(file.fsid) == fileId || String(i) == fileId) {
|
||||
targetFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetFile) {
|
||||
throw new Error("未找到指定ID的文件: " + fileId);
|
||||
}
|
||||
|
||||
var downloadUrl = targetFile.dlink;
|
||||
if (!downloadUrl) {
|
||||
throw new Error("文件无下载链接");
|
||||
}
|
||||
|
||||
// 获取真实的下载链接(处理302重定向)
|
||||
var realDownloadUrl = getRealDownloadUrl(downloadUrl, http, logger);
|
||||
|
||||
logger.info("根据ID解析成功: " + realDownloadUrl);
|
||||
return realDownloadUrl;
|
||||
|
||||
} catch (e) {
|
||||
logger.error("根据ID解析失败: " + e.message);
|
||||
throw new Error("根据ID解析失败: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节大小
|
||||
* @param {number} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return "0 B";
|
||||
var k = 1024;
|
||||
var sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从路径中提取文件名
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function extractFileName(path) {
|
||||
if (!path) return "";
|
||||
var parts = path.split("/");
|
||||
return parts[parts.length - 1] || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取真实的下载链接(处理302重定向)
|
||||
* @param {string} downloadUrl - 原始下载链接
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志记录器
|
||||
* @returns {string} 真实的下载链接
|
||||
*/
|
||||
function getRealDownloadUrl(downloadUrl, http, logger) {
|
||||
try {
|
||||
logger.info("获取真实下载链接: " + downloadUrl);
|
||||
|
||||
// 使用不跟随重定向的方法获取Location头
|
||||
var headResponse = http.getNoRedirect(downloadUrl);
|
||||
|
||||
if (headResponse.statusCode() >= 300 && headResponse.statusCode() < 400) {
|
||||
// 处理重定向
|
||||
var location = headResponse.header("Location");
|
||||
if (location) {
|
||||
logger.info("获取到重定向链接: " + location);
|
||||
return location;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有重定向或无法获取Location,返回原链接
|
||||
logger.debug("下载链接无需重定向或无法获取重定向信息");
|
||||
return downloadUrl;
|
||||
|
||||
} catch (e) {
|
||||
logger.error("获取真实下载链接失败: " + e.message);
|
||||
// 如果获取失败,返回原链接
|
||||
return downloadUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间戳
|
||||
* @param {number} timestamp
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return "";
|
||||
var date = new Date(timestamp * 1000);
|
||||
return date.toISOString().replace("T", " ").substring(0, 19);
|
||||
}
|
||||
170
parser/src/main/resources/custom-parsers/example-demo.js
Normal file
170
parser/src/main/resources/custom-parsers/example-demo.js
Normal file
@@ -0,0 +1,170 @@
|
||||
// ==UserScript==
|
||||
// @name 演示解析器
|
||||
// @type demo_js
|
||||
// @displayName 演示网盘(JS)
|
||||
// @description 演示JavaScript解析器的完整功能(使用JSONPlaceholder测试API)
|
||||
// @match https?://demo\.example\.com/s/(?<KEY>\w+)
|
||||
// @author qaiu
|
||||
// @version 1.0.0
|
||||
// ==/UserScript==
|
||||
|
||||
// 注意:require调用仅用于IDE类型提示,运行时会被忽略
|
||||
// var types = require('./types');
|
||||
|
||||
/**
|
||||
* 解析单个文件下载链接
|
||||
* 使用 https://jsonplaceholder.typicode.com/posts/1 作为测试
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志对象
|
||||
* @returns {string} 下载链接URL
|
||||
*/
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
logger.info("===== 开始执行 parse 方法 =====");
|
||||
|
||||
var shareKey = shareLinkInfo.getShareKey();
|
||||
var password = shareLinkInfo.getSharePassword();
|
||||
|
||||
logger.info("分享Key: " + shareKey);
|
||||
logger.info("分享密码: " + (password || "无"));
|
||||
|
||||
// 使用JSONPlaceholder测试API
|
||||
var apiUrl = "https://jsonplaceholder.typicode.com/posts/" + (shareKey || "1");
|
||||
logger.debug("请求URL: " + apiUrl);
|
||||
|
||||
try {
|
||||
var response = http.get(apiUrl);
|
||||
logger.debug("HTTP状态码: " + response.statusCode());
|
||||
|
||||
var data = response.json();
|
||||
logger.debug("响应数据: " + JSON.stringify(data));
|
||||
|
||||
// 模拟返回下载链接(实际是返回post的标题作为"下载链接")
|
||||
var downloadUrl = "https://cdn.example.com/file/" + data.id + "/" + data.title;
|
||||
logger.info("解析成功,返回URL: " + downloadUrl);
|
||||
|
||||
return downloadUrl;
|
||||
} catch (e) {
|
||||
logger.error("解析失败: " + e.message);
|
||||
throw new Error("解析失败: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件列表
|
||||
* 使用 https://jsonplaceholder.typicode.com/users 作为测试
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志对象
|
||||
* @returns {FileInfo[]} 文件列表数组
|
||||
*/
|
||||
function parseFileList(shareLinkInfo, http, logger) {
|
||||
logger.info("===== 开始执行 parseFileList 方法 =====");
|
||||
|
||||
var dirId = shareLinkInfo.getOtherParam("dirId") || "1";
|
||||
logger.info("目录ID: " + dirId);
|
||||
|
||||
// 使用JSONPlaceholder的users API模拟文件列表
|
||||
var apiUrl = "https://jsonplaceholder.typicode.com/users";
|
||||
logger.debug("请求URL: " + apiUrl);
|
||||
|
||||
try {
|
||||
var response = http.get(apiUrl);
|
||||
var users = response.json();
|
||||
|
||||
var fileList = [];
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i];
|
||||
|
||||
// 模拟文件和目录
|
||||
var isFolder = (user.id % 3 === 0); // 每3个作为目录
|
||||
var fileSize = isFolder ? 0 : user.id * 1024 * 1024; // 模拟文件大小
|
||||
|
||||
/** @type {FileInfo} */
|
||||
var fileInfo = {
|
||||
fileName: user.name + (isFolder ? " [目录]" : ".txt"),
|
||||
fileId: user.id.toString(),
|
||||
fileType: isFolder ? "folder" : "file",
|
||||
size: fileSize,
|
||||
sizeStr: formatFileSize(fileSize),
|
||||
createTime: "2024-01-01",
|
||||
updateTime: "2024-01-01",
|
||||
createBy: user.username,
|
||||
downloadCount: Math.floor(Math.random() * 1000),
|
||||
fileIcon: isFolder ? "folder" : "file",
|
||||
panType: "demo_js",
|
||||
parserUrl: "",
|
||||
previewUrl: ""
|
||||
};
|
||||
|
||||
// 如果是目录,设置解析URL
|
||||
if (isFolder) {
|
||||
fileInfo.parserUrl = "/v2/getFileList?url=demo&dirId=" + user.id;
|
||||
} else {
|
||||
// 如果是文件,设置下载URL
|
||||
fileInfo.parserUrl = "/v2/redirectUrl/demo_js/" + user.id;
|
||||
}
|
||||
|
||||
fileList.push(fileInfo);
|
||||
}
|
||||
|
||||
logger.info("解析文件列表成功,共 " + fileList.length + " 项");
|
||||
return fileList;
|
||||
|
||||
} catch (e) {
|
||||
logger.error("解析文件列表失败: " + e.message);
|
||||
throw new Error("解析文件列表失败: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件ID获取下载链接
|
||||
* 使用 https://jsonplaceholder.typicode.com/todos/:id 作为测试
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志对象
|
||||
* @returns {string} 下载链接URL
|
||||
*/
|
||||
function parseById(shareLinkInfo, http, logger) {
|
||||
logger.info("===== 开始执行 parseById 方法 =====");
|
||||
|
||||
var paramJson = shareLinkInfo.getOtherParam("paramJson");
|
||||
if (!paramJson) {
|
||||
throw new Error("缺少paramJson参数");
|
||||
}
|
||||
|
||||
var fileId = paramJson.fileId || paramJson.id || "1";
|
||||
logger.info("文件ID: " + fileId);
|
||||
|
||||
// 使用JSONPlaceholder的todos API
|
||||
var apiUrl = "https://jsonplaceholder.typicode.com/todos/" + fileId;
|
||||
logger.debug("请求URL: " + apiUrl);
|
||||
|
||||
try {
|
||||
var response = http.get(apiUrl);
|
||||
var todo = response.json();
|
||||
|
||||
// 模拟返回下载链接
|
||||
var downloadUrl = "https://cdn.example.com/download/" + todo.id + "/" + todo.title + ".zip";
|
||||
logger.info("根据ID解析成功: " + downloadUrl);
|
||||
|
||||
return downloadUrl;
|
||||
|
||||
} catch (e) {
|
||||
logger.error("根据ID解析失败: " + e.message);
|
||||
throw new Error("根据ID解析失败: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:格式化文件大小
|
||||
* @param {number} bytes - 字节数
|
||||
* @returns {string} 格式化后的大小
|
||||
*/
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return "0B";
|
||||
var k = 1024;
|
||||
var sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(2) + sizes[i];
|
||||
}
|
||||
19
parser/src/main/resources/custom-parsers/jsconfig.json
Normal file
19
parser/src/main/resources/custom-parsers/jsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"target": "ES5",
|
||||
"lib": ["ES5"],
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"*.js",
|
||||
"types.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
68
parser/src/main/resources/custom-parsers/types.js
Normal file
68
parser/src/main/resources/custom-parsers/types.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* JavaScript解析器类型定义文件
|
||||
* 使用JSDoc注释提供代码补全和类型提示
|
||||
* 兼容ES5.1和Nashorn引擎
|
||||
*/
|
||||
|
||||
// 全局类型定义,使用JSDoc注释
|
||||
// 这些类型定义将在VSCode中提供代码补全和类型检查
|
||||
|
||||
/**
|
||||
* @typedef {Object} ShareLinkInfo
|
||||
* @property {function(): string} getShareUrl - 获取分享URL
|
||||
* @property {function(): string} getShareKey - 获取分享Key
|
||||
* @property {function(): string} getSharePassword - 获取分享密码
|
||||
* @property {function(): string} getType - 获取网盘类型
|
||||
* @property {function(): string} getPanName - 获取网盘名称
|
||||
* @property {function(string): any} getOtherParam - 获取其他参数
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} JsHttpResponse
|
||||
* @property {function(): string} body - 获取响应体(字符串)
|
||||
* @property {function(): any} json - 解析JSON响应
|
||||
* @property {function(): number} statusCode - 获取HTTP状态码
|
||||
* @property {function(string): string|null} header - 获取响应头
|
||||
* @property {function(): Object} headers - 获取所有响应头
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} JsHttpClient
|
||||
* @property {function(string): JsHttpResponse} get - 发起GET请求
|
||||
* @property {function(string, any=): JsHttpResponse} post - 发起POST请求
|
||||
* @property {function(string, string): JsHttpClient} putHeader - 设置请求头
|
||||
* @property {function(Object): JsHttpResponse} sendForm - 发送表单数据
|
||||
* @property {function(any): JsHttpResponse} sendJson - 发送JSON数据
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} JsLogger
|
||||
* @property {function(string): void} debug - 调试日志
|
||||
* @property {function(string): void} info - 信息日志
|
||||
* @property {function(string): void} warn - 警告日志
|
||||
* @property {function(string): void} error - 错误日志
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} FileInfo
|
||||
* @property {string} fileName - 文件名
|
||||
* @property {string} fileId - 文件ID
|
||||
* @property {string} fileType - 文件类型: "file" | "folder"
|
||||
* @property {number} size - 文件大小(字节)
|
||||
* @property {string} sizeStr - 文件大小(可读格式)
|
||||
* @property {string} createTime - 创建时间
|
||||
* @property {string} updateTime - 更新时间
|
||||
* @property {string} createBy - 创建者
|
||||
* @property {number} downloadCount - 下载次数
|
||||
* @property {string} fileIcon - 文件图标
|
||||
* @property {string} panType - 网盘类型
|
||||
* @property {string} parserUrl - 解析URL
|
||||
* @property {string} previewUrl - 预览URL
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ParserExports
|
||||
* @property {function(ShareLinkInfo, JsHttpClient, JsLogger): string} parse - 解析单个文件下载链接
|
||||
* @property {function(ShareLinkInfo, JsHttpClient, JsLogger): FileInfo[]} parseFileList - 解析文件列表
|
||||
* @property {function(ShareLinkInfo, JsHttpClient, JsLogger): string} parseById - 根据文件ID获取下载链接
|
||||
*/
|
||||
Reference in New Issue
Block a user